해당 포스트는 "안드로이드 통신+보안 프로그래밍" 책의 내용을 요약한 것이다.



: 간단한 자바 클라이언트/서버 소켓 프로그램 구현에 대해서 살펴볼 것이다.


※ 클라이언트 소켓 프로그램 

1. 소캣 객체 만들기

Socket socket = new Socket(host, port);

: host에는 목적지 호스트의 이름 또는 InetAddress 객체가 들어간다. port에는 목적지 호스트의 포트번호를 넣는다. 위 코드는 호스트 서버 이름과 포트 번호를 이용해서 서버와 연결을 위한 소켓을 만든다. 이 때 다음과 같은 예외가 발생할 수 있다. 

- UnknownHostException : host로 서버를 찾을 수 없을 때

- IOException : 네트워크 문제로 소켓을 생성할 수 없을 때

- SecurityException : 보안 매니저에 의해 연결이 허용되지 않을 때

위 코드는 다음 코드와 똑같다.

Socket socekt = new Socket(); SocketAddress sa = enw InetSocketAddress(host, port); int timeout = 2000; socket.connet(sa,timeout);

connect 메서드는 목적지 호스트와 연결 요청하는 SYN 패킷을 발생시킨다. 해당 메서드의 두 번째 변수는 타임 아웃을 나타낸다.  2000밀리초 이내에 서버와 연결되지 않으면 SocketTImeoutException 예외가 발생한다. 타임아웃 시간이 0이면 무한대로 서버와의 연결을 기다리지만 실제로는 시스템 상의 일정 시간이 지나면 예외가 발생한다. 추가로 connect 메서드는 블록 모드로 작동해 작업이 끝날 때까지 리턴하지 않는다. 


2. 스트림 객체 생성

: 데이터 전송을 위한 getInput(Output)Stream() 메서드로 스트림 객체를 생성한다. 이 과정에서 클라이언트와 서버 간에 three-way handshaking이 성공해 세션이 성립되었다는 확인 패킷을 받으면 스트림 객체가 생성된다. 그렇지 않으면 IOException 예외가 발생한다.


3. write와 read를 사용한 메시지 송수신

: write와 read 메서드를 통해 서버와 클라이언트 간에 데이터 통신이 이루어진다. 해당 메서드들은 블록 모드로 작동한다. 서버에서 클라이언트 사이에 데이터 전송이 완료되야 CPU에서 데이터를 처리할 수 있기 때문이다.


2,3 과정에 대한 예를 보겠다. 

Writer out = new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"); out.write(query); //query는 문자열 out.write("\r\n"); out.flush();

: 먼저 socket.getOutputStream()으로 서버로 데이터를 보낼 스트림 객체를 생성한다. OutputStreamWriter로 스트림 객체와 인코딩할 문자셋을 전달한다. OutputStreamWriter 객체는 자바 문자열 유니코드 문자를 인코딩 방식에 따라 바이트 형식으로 변환시켜 OutputStream 객체로 출력하는 역할을 한다. OutputStream 객체는 바이트 기반 스트림이다. 즉 위 예의 첫 번째 줄은 유니코드를 socket과 연결된 서버에서 요구하는 'ISO-8859-1' 문자셋으로 인코딩한 바이트 형태로 변환시켜 서버에 전송하겠다는 뜻이다. 그리고 out.write(query)로 문자열을 버퍼 내에 쓰고 "\r\n"을 write한다. "\r\n"는 문장의 끝을 표시하므로 반드시 넣어야 한다. 참고로 \r는 캐리지 리턴으로 커서를 행의 맨 좌측으로 옮긴다. out.flush()는 버퍼 내 데이터를 파일에 저장하거나 또는 외부로 출력시키는 기능을 한다. 만약 버퍼가 꽉 차 자동 전송을 하지 않거나 flush() 메서드를 사용하지 않는다면 close() 메서드를 만날 때까지 데이터가 전송되지 않는다. 위의 방식 대신 PrintWriter을 사용하면 더 편리하게 구현할 수 있다. 다음 코드는 이전 코드와 똑같은 기능을 한다. 

PrintWriter out = new PrintWriter(socket.getOutputStream(), true); out.println(query);

println() 메서드를 사용하면 "\r\n"을 버퍼에 write 하지 않아도 되고 PrintWriter 생성자의 두 번째 인자로 true이라면 flush도 호출할 필요가 없다. 단 false 일시 flush를 호출해야 한다. PrintWriter를 사용하면 위와 같이 편리하게 문장 단위로 데이터 전송이 가능하다.  

InputStreamReader isr = new InputStreamReader(sc.getInputStream()); BufferedReader in = new BufferedReader(isr); String line = ""; while((line=in.readLine())!=null){ System.out.println(line); }

위 코드는 서버에서 클라이언트로 보내는 데이터를 읽는다. 서버에서 수신된 데이터가 바이트이기 때문에 유니코드로 변환시키기 위해 InputStreamReader을 사용했다. InputStreamReader 객체를 BufferedReader 객체로 덮었는 데 BufferedReader는 입력 데이터에 대해 버퍼를 제공해 빠르게 데이터를 읽을 수 있고 readLine() 메소드를 제공해 String 단위로 데이터를 읽게 해준다. readLine 메소드는 파일의 끝을 나타내는 EOF를 만나면 null을 반환하기 때문에 이를 이용해 데이터를 읽는다. 통신에서 null의 의미는 'FIN' 패킷을 받아 정상 종료되었음을 의미한다. 반면 'RST' 패킷을 받으면 문제가 발생해 강제 종료되었다는 의미이기 때문에 IOException 예외가 발생한다. 


4. 소켓과 스트림 close

: close() 메서드로 소켓과 스트림을 닫을 수 있다.

※ 서버 소켓 프로그램 

1. SoverSocket() 생성자를 이용해 서버 소켓 객체 생성

ServerSocket svr = new ServerSocket(port); ServerSocket svr = new ServerSocket(port, backlog);

port는 서비스할 포트번호이고 backlog는 클라이언트와의 최대 연결 수이다. backlog 인자를 안 줄시 디폴트 값 50이 된다. 


2. bind()로 서버소켓에 ip주소와 포트번호 bind
bind(SocketAddress localAddr);
bind(SocketAddress localAddr, int backlog);

bind 메서드를 통해 서버소켓에 IP주소와 포트번호를 부여한다. backlog 값을 안 줄시 디폴트 값 50이 된다. bind 메서드는 대부분 프로그램에서 사용하지 않는다. ServerSocket 생성자 내부에서 bind 메서드를 지원하기 때문이다.


3. 클라이언트로부터 연결 요청 대기
: 서버소켓에서 accept() 메서드를 호출하면 클라이언트가 connect 메서드를 호출해 서버에 연결 요청이 있을 때까지 블록된다. 만약 클라이언트에서 connect 메서드를 호출해 서버와 세션을 맺게 되면 해당 클라이언트와 통신할 수 있는 Socket 객체를 반환한다. 

4. 스트림 객체 생성 후 write, read 메서드로 통신
: 클라이언트 소켓 프로그램과 똑같이 동작한다.

5. 소켓 close
: 클라이언트와 통신이 완료되면 close 메서드를 호출해 소켓을 닫는다. 서버에서 소켓을 닫으면 클라이언트에 'FIN' 패킷을 전송한다. 만약 서버 자체를 끝내고 싶다면 서버 소켓의 객체도 닫아야 한다. 



+ Recent posts