HTTP를 통해 서버와 클라이언트가 데이터를 통신하기 위해
애플리케이션 계층에서 Socket Library를 사용하여 효율적으로 운영체제 자원을 사용할 수 있다.
자바 기반 Spring을 사용하는 경우 대부분의 기능이 적절하게 구현되어 있기 때문에,
사실 직접적으로 Socket Library의 메서드를 사용하는 일은 드물다.
하지만 어떻게 서버와 클라이언트가 어떤식으로 소통하는지 대략적인 흐름을 알아보자.
서버는 먼저 TCP 연결만을 담당하는 특수한 소켓인 ServerSocket을 생성한다.
생성한 serverSocket을 통해 클라이언트 Request에 응답할 수 있으며,
serverSocket의 accpet() 메서드를 통해 데이터 통신을 위한 Socket을 생성할 수 있다.
ServerSocket serverSocket = new ServerSocket(PORT);
Socket socket = serverSocket.accpet();
서버는 TCP 연결을 위한 ServerSocket을 생성할 때, PORT 번호를 지정해야 한다.
PORT 번호를 구분하여 서비스를 분리하여 관리할 수 있다.
ServerSocket은 연결 정보를 backlog queue에 보관한다.
ServerSocket은 서버에서 클라이언트와의 TCP 연결만을 담당하는 특수한 소켓이다.
이후에는 accpet() 메서드를 통하여 데이터 통신을 위한 소켓을 생성해야 한다.
Socket socket = serverSocket.accpet(); // ** 블로킹 메서드 **
accpet() 메서드는 블로킹 메서드로 TCP 연결 요청이 발생할 때까지 대기한다.
서버는 보통 여러 클라이언트와 데이터를 주고 받기 때문에,
TCP 연결을 위해 accept() 메서드를 호출하는 서버와
Socket 생성 이후 데이터를 통신하는 다른 스레드를 생성하여 멀티스레드로 운영해야 한다.
Socket socket = serverSocket.accpet();
// accpet() 메서드 이후에는 스레드를 생성하여 멀티스레드 운영
Task task = new Task();
Thread thread = new Thread(task);
thread.start();
클라이언트는 IP(또는 DNS)와 PORT 번호를 지정하고
Socket을 생성하여 서버와 연결할 수 있다.
Socekt socket = new Socket(IP, PORT);
응답이 오지 않는 경우 일정 시간 뒤에 예외를 발생시키는
OS 별 연결 대기 타임 아웃이 작동한다.
Socket을 IP, PORT를 지정하지 않고 생성한 후,
connect() 메서드를 사용하면 타임 아웃 시간을 직접 지정할 수 있다.
Socket socket = new Socket();
socket.connet(new InetSocketAddress(IP, PORT), time);
Server와 Client는 데이터 통신을 위해
socket 스트림의 read(), write() 메서드를 호출할 수 있다.
Socket socket = new Socket(IP, PORT);
DataInputStream dis = new DataInputStream(socket.getInputStream());
DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
dis.read(); // ** 블로킹 메서드 **
socket.setSoTimeout(time); // ** read() 메서드 시간 제한 **
dos.write(); // ** 블로킹 메서드 **
socket Stream의 read(), write() 메서드는 블로킹 메서드이기 때문에,
데이터를 읽고 쓰는 스레드를 별도로 생성하여 멀티스레드로 운영한다.
데이터 수신을 대기한 read() 메서드의 경우,
socket의 setSoTimeout() 메서드를 통해 타임아웃 시간을 설정할 수 있다.
클라이언트의 요청은 HTTP Request 메시지로,
서버의 응답은 HTTP Response 메시지로 데이터 통신을 수행할 수 있다.
HTTP Message는 start-line, headers, empty-line, message body로 이루어져있다.
자바는 HTTP Message 구성 자체를 적절히 파싱하여 클래스로 만들어 관리하는데,
각 요소를 편리하게 사용하고 화면을 출력할 수 있는 기능을 제공한다.
이를 Servlet이라고 하는데 자세한 내용은 Spring을 학습할 때 알아보자.
URL과 headers는 ASCII만을 사용한다.
그렇기 때문에 Client Request는 UTF-8 글자를
byte별로 나누어 %로 구분하고 16진수로 Encoding하는 Encoding을 수행한다.
서버는 Client Request 메시지를 Servlet에서 파싱할 수 있도록
%를 제거하고 byte로 변환하여 Decoding을 수행한다.
자바는 인스턴스 객체가 아닌 외부 자원의 경우 명시적으로 자원을 정리해주어야 한다.
Socket과 Socket의 스트림은 close() 메서드를 호출하여 자원을 정리할 수 있다.
serverSocket.close();
socket.close();
dis.close();
dos.close();
Server 또는 Client 측에서 close() 메서드를 호출하면,
상대방에게 FIN Packet이라는 네트워크 신호를 보낸다.
FIN Packet을 받으면 FIN Packet의 응답과 함께 자신도 FIN Packet 신호를 보낸다.
처음에 호출한 쪽이 전달 받은 FIN Packet의 응답을 보내면서 자원이 정리된다.
FIN Packet은 네트워크 자원을 정리하겠다는 신호로,
FIN Packet을 받으면 이에 대한 응답 외에 다른 작업을 수행하지 말고 연결을 끊어야 한다.
FIN Packet 응답이 아닌 다른 작업을 수행하면 RST Packet을 전달받는데,
이 경우에도 다른 작업을 수행하지 말고 연결을 종료해야 한다.
클라이언트는 보통 서버와 단일 연결로 단순한 구조를 가지고 있기 때문에
try-with-resources와 같은 구문을 사용하여 간단하게 자원을 정리할 수 있다.
하지만 서버의 경우 다중 클라이언트와 연결되어 있기 때문에
try-with-resources 구문으로 서버 종료 상황에 모든 세션들을 정리할 수 없어
try-catch-finally 구문으로 명시적으로 close() 메서드를 호출해야 한다.
이 때 사용되는 것이 바로 ShutdownHook이다.
Runtime.getRuntime().addShutdownHook(new Thread(new CloseTask()));
예를 들어 모든 자원을 정리하는 CloseTask가 있다고 한다면,
Runtime.getRuntime().addShutdownHook() 메서드에 스레드 객체를 전달하여
자바 종료 시 실행되는 ShutdownHook을 등록할 수 있다.
보통 클라이언트와 연결된 소켓을 담당하는 클래스를 두어 관리한다.
또한, 서버 종료의 경우 여러 곳에서 호출될 수 있기 때문에
close() 메서드는 동기화(synchronized) 메서드로 수행된다.
Socket Library를 사용할 일은 드물지만, 언젠가 반드시 마주쳐야할 날은 온다.
자바의 문법을 학습하고 본격적으로 Spring을 배우기 전에,
그냥 Spring의 기능을 배우고 사용하기 보다는 실체를 알고 이해해서 숙련도를 높여보자.