본문 바로가기

JAVA/네트워크 프로그래밍

[JAVA] 소켓으로 서버에서 읽기(Client)



위 예제는 Telnet 프로토콜을 이용하여 미국 국립표준기술연구소(NIST)의 daytime 서버13번 포트에 현재시간에 대한 요청을 보낸 것이다.


이 결과에서 "57634 16-09-03 06:46:59 50 0 0 150.2 UTC(NIST)" 라인이 daytime 서버에서 전송된 것이다.


소켓의 InputStream 을 사용하여 읽으면, 바로 이 값이 반환된다. 그 외의 다른 줄은 유닉스 셸이나 텔넷 프로그램에 의해 출력된 것이다.


지금은 소켓을 사용하여 코드상에서 동일한 결과를 얻는 방법을 보도록 하자. 먼저 time.nist.gov의 13번 포트에 대한 소켓을 연다.


Socket socket = new Socket("time.nist.gov", 13);


이 코드는 객체를 만들 뿐만 아니라 실제로 네트워크를 통한 연결을 생성한다.


연결시에 시간 초과가 발생하거나 해당 서버의 13번 포트가 대기하고 있지 않아서 실패할 경우, 생성자는 IOException 예외를 발생시킨다.


그래서 일반적으로 이 코드를 try 블록으로 감싼다. 자바 7에서 Socket 클래스는 AutoCloseable 인터페이스를 구현하고 있으므로, try-with-resources 구문을 사용하여 Socket 객체를 생성할 수 있다.


try(Socket socket = new Socket("time.nist.gov", 13)){

// 연결된 소켓에서 읽기....

}catch (IOException ex) {

System.err.println("time.nist.gov에 연결할 수 없습니다.");

}


다음 단계는 선택 사항이지만 설정할 것을 권장한다. setSoTimeout() 메소드를 사용하여 연결에 대한 타임아웃을 설정할 수 있다. 타임아웃은 밀리초 단위로 설정되며 다음 코드는 15초 동안 응답이 없을 경우 해당 소켓을 타임아웃 시킨다.


socket.setSoTimeout(15000);


소켓은 서버가 연결을 거부하거나 라우터가 목적지로 패킷을 보내는 방법을 찾지 못할 경우 재빨리 ConnectionException이나 NoRouteHostException 예외를 발생시키지만, 이 두 경우 모두 서버가 연결을 받아들인 다음 명시적인 소켓의 종료 없이 대화를 멈추는 경우와 같은 비정상적인 상황을 대처하지는 못한다.


소켓에 타임아웃을 설정하는 것은 소켓에 대한 읽고 쓰기가 느린 경우에도 지정된 밀리초 이상을 소요하지 않을 것임을 나타낸다.


프로그램이 서버에 연결되어 있는 동안 서버가 과부하나 다양한 문제로 응답이 없는 경우, 지정된 밀리초가 지나면 프로그램은 소켓으로부터 SocketTimeoutException 예외를 받게 된다.


어느 정도의 타임아웃을 설정해야 할지는 프로그래머의 재량이다. 15초라는  시간은 로컬 인트라넷 서버의 응답시간으로는 꽤 긴 편이지만, time.nist.gov와 같은 부하가 많은 공공 서버에 대해서는 다소 짧은 편이다.



소켓은 열고 타임아웃을 설정하고 나서, getInputStream() 메소드를 호출하면 소켓으로부터 바이트를 읽는 데 사용할 수 있는 InputStream이 반환된다. 


일반적으로 서버는 어떤 바이트 값이라도 보낼 수 있지만, 아래 코드에서는 서버가 아스키 문자만 보낸다고 가정하고 작성하였다.


InputStream in = socket.getInputStream();

StringBuilder time = new StringBuilder();

InputStreamReader reader = new InputStreamReadere(in, "ASCII");

for (int c = reader.read(); c != -1; c = reader.read()){

time.append((char) c);

}

System.out.println(time);



소켓을 통해 받은 결과값


이 예제에서는 읽은 바이트를 StringBuilder에 저장했지만, 상황에 적절한 다른 어떤 데이터 구조체라도 사용할 수 있다.


조금 더 심화된 예제를 보면, 지금까지의 내용과 함께 다른 daytime 서버를 설정할 수도 있다.

package network;


import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.net.Socket;


public class TelnetEx {

public static void main(String[] args){

String hostname = args.length > 0 ? args[0]: "time.nist.gov";

Socket socket = null;

try{

socket = new Socket(hostname, 13);

socket.setSoTimeout(15000);

InputStream in = socket.getInputStream();

StringBuilder time = new StringBuilder();

InputStreamReader reader = new InputStreamReader(in, "ASCII");

for(int c = reader.read(); c != -1; c =reader.read()){

time.append((char) c);

}

System.out.println(time);

}catch(IOException ex){

System.err.println(ex);

}finally{

if(socket != null){

try{

socket.close();

}catch(IOException ex){}

}

}

}

}


이 프로그램의 일반적인 실행 결과는 텔넷으로 연결했을 때와 거의 같다.



네트워크와 관련된 코드를 작성하는 한 대부분 이와 유사한 코드를 작성하게 된다.


이와 같은 대부분의 네트워크 프로그램에서 실제 프로그래머는 프로토콜로 통신하고 데이터 구조를 이해하는 데 많은 시간을 할애한다. 


예를 들어, 서버가 보낸 데이터를 단순히 화면에 출력하지 않고, 대신 분석하여 java.util.Date 객체를 만들고 싶을 때가 있다.


이 다음 예제는 이를 처리하는 방법을 보여준다. 이 예제에서는 자바 7에서 제공하는 AutoCloseable과 try-with-resources 구문을 이용했다.

package network;


import java.io.IOException;

import java.io.InputStream;

import java.io.InputStreamReader;

import java.net.Socket;

import java.text.DateFormat;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;


public class Daytime {

public Date getDateFromNetwork() throws IOException, ParseException{

try(Socket socket = new Socket("time.nist.gov", 13)){

socket.setSoTimeout(15000);

InputStream in = socket.getInputStream();

StringBuilder time = new StringBuilder();

InputStreamReader reader = new InputStreamReader(in,"ASCII");

for(int c = reader.read(); c != -1; c = reader.read()){

time.append((char) c); 

}

return parseDate(time.toString());

}

}


static Date parseDate(String s) throws  ParseException {

String[] pieces = s.split(" ");

String dateTime = pieces[1] + " " + pieces[2] + "UTC";

DateFormat format = new SimpleDateFormat("yy-MM-dd hh:mm:ss z");

return format.parse(dateTime);

}

}


이 클래스가 앞선 예제와 다른 네트워크 작업을 하는 것은 아니며, 단지 문자열을 date로 변환하는 코드만 추가되었을 뿐이다.


네트워크로부터 데이터를 읽을 때는 모든 프로토콜이 아스키나 텍스트만을 사용하지는 않는다는 사실을 명심해야 한다.


예를 들어, RFC 868에 기술된 타임 프로토콜은 1900년 1월 1일 자정 이후의 초 시간을 응답으로 보낸다고 명시하고 있다.


그러나 이 값은 2,524,521,600 또는 -1297728000와 같은 아스키 문자열로 보내지지 않는다. 대신 이 값은 32비트 부호없는 빅엔디안 이진 숫자로 보내진다.



타임 프로토콜이 반환하는 값은 텍스트가 아니기 때문에 텔넷을 사용하여 이 서비스를 쉽게 확인할 수 없다.


그리고 프로그램에서도 이 값을 Reader 또는 readLine() 메소드의 종류를 사용하여 읽을 수 없다.


타임 서버에 연결하는 자바 프로그램은 raw 바이트를 읽어서 적절히 해석해야 한다.


아래 예제에서는 자바가 32비트 부호 없는 정수 타입을 지원하지 않기 때문에 다소 복잡하게 처리하고 있다.


결과적으로 한 번에 1바이트씩 읽은 다음, 이 값을 수동으로 비트 연산자 <<와 | 를 사용하여 long 타입으로 변환해야 한다.



다른 프로토콜에서도 역시 자바에서 쉽게 처리할 수 없는 데이터 형식이 존재할 수 있다.


예를 들어, 일부 네트워크 프로토콜에서 64비트 고정 소수점 숫자를 사용하는 경우가 있다.


모든 상황을 처리할 수 있는 쉬운 방법은 없다. 서버가 어떤 형식의 데이터를 보내든지 이를 악물고 필요한 코드를 작성하는 수 밖에 없다.



타임 프로토콜 클라이언트

package network;


import java.io.IOException;

import java.io.InputStream;

import java.net.Socket;

import java.util.Calendar;

import java.util.Date;

import java.util.TimeZone;


public class TimeClient {

private static final String HOSTNAME = "time.nist.gov";

public static void main(String[] args) throws IOException {

Date d = TimeClient.getDateFromNetwork();

System.out.println("It is " + d);

}

public static Date getDateFromNetwork() throws  IOException{

//타임 프로토콜은 1900년을 기준으로 하지만,

//자바 Date 클래스는 1970년을 기준으로 한다.

// 아래 숫자는 시간을 변환하는 데 사용된다.

long differenceBetweenEpochs = 2208988800L;

//위 숫자를 사용하지 않고 직접 계산하고자 할 경우

//아래 영역의 주석을 해제하면 된다

/*TimeZone gmt = TimeZone.getTimeZone("GMT");

Calendar epoch1900 = Calendar.getInstance(gmt);

epoch1900.set(1900, 01, 01, 00, 00, 00);

long epoch1900ms = epoch1900.getTime().getTime();

Calendar epoch1970 = Calendar.getInstance(gmt);

epoch1970.set(1970, 01, 01, 00, 00, 00);

long epoch1970ms = epoch1970.getTime().getTime();

long differenceInMS = epoch1970ms - epoch1900ms;

long differenceBetweenEpochs = differenceInMS/1000;

*/

Socket socket = null;

try{

socket = new Socket(HOSTNAME, 37);

socket.setSoTimeout(15000);

InputStream raw = socket.getInputStream();

long secondsSince1900 = 0;

for(int i = 0;i < 4; i++){

secondsSince1900 = (secondsSince1900 << 8) | raw.read();

}

long secondsSince1970 = secondsSince1900 - differenceBetweenEpochs;

long msSince1970 = secondsSince1970 * 1000;

Date time = new Date(msSince1970);

return time;

}finally{

try{

if(socket != null) socket.close();

}

catch(IOException ex){}

}

}

}


실행결과



타임 프로토콜은 실제 GMT 기준의 시간을 반환하지만, 자바의 Date 클래스가 제공하는 toString() 메소드(System.out.println()에 의해 은연중에 호출된다)는 로컬 호스트의 시간대로 반환한다.

이 예제에서는 EST로 반환되었다.