자바로 알아보는 클라이언트, 서버 소켓 통신
이번 글은 김영한 님의 실전 자바 - 고급 2편을 참고했습니다.
네트워크에서는 클라이언트와 서버가 서로 데이터를 주고받는다.
이때 클라이언트는 주로 무언가를 요청하는 주체를 말하고, 서버는 요청을 받아 응답을 하는 주체를 의미한다.

만약에 컨트롤러와 서비스로 대입을 해본다면 여기서 컨트롤러는 정보를 요청하는 주체니 클라이언트가 되고, 서비스는 응답을 제공하는 주체니 서버의 역할을 담당하게 된다.

그러면 클라이언트와 서버는 어떻게 데이터를 주고받을까?
데이터를 주고받기 위해서는 먼저 서로간의 연결이 진행되어야 한다.
그 연결을 하는 방법이 TCP 연결이다.
https://qkrqkrrlrl.tistory.com/139
TCP와 UDP의 차이점에 대해 설명해주세요
TCP (Transmission Control Protocol)전송 제어 프로토콜 특징 1. 연결 지향형 성격을 지니고 있다.-> 즉 서로 간의 연결을 지향한다는 의미. 신뢰성을 보장하기 위한 방법 2. 연결을 위해 추가 작업이 필
qkrqkrrlrl.tistory.com
TCP 연결을 하기 위해서 소켓이라는 개념을 사용하게 된다.
소켓은 서로 데이터를 주고 받기 위한 다리의 역할을 한다고 보면 된다.

즉, TCP 연결이 완료되면 서로의 소켓을 통해 데이터를 주고받는 다리를 연결하게 된다.
위 내용은 함축적인 내용이 많으므로 아래에서 코드와 함께 조금 더 자세하게 확인해 보자.
서버 코드
public class Server {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("서버 시작");
ServerSocket serverSocket = new ServerSocket(PORT);
log("서버 소켓 시작 - 리스닝 포트: " + PORT);
// 12345 포트에 클라이언트가 접속하면 소켓을 만들어준다.
Socket socket = serverSocket.accept();
log("소켓 연결: " + socket);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
// 클라이언트로부터 문자 받기
String received = input.readUTF();
log("client -> server: " + received);
// 클라이언트에게 문자 보내기
String toSend = received + " World!";
output.writeUTF(toSend);
log("client <- server: " + toSend);
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
serverSocket.close();
}
}
서버는 ServerSocket 객체를 만들어놓고 accept() 메서드를 호출해 클라이언트의 연결 정보를 대기하게 된다.
여기서 주의해야 할 점은 Socket이랑 ServerSocket은 다르다는 점이다.
ServerSocket은 Listening Socket으로 포트 번호를 연결해 놓고 클라이언트의 연결을 대기하는 소켓이다.
(나 해당 PORT 번호 열고 대기하고 있어~)
반면에 Socket은 Accepted Socket으로 클라이언트와의 연결이 완료된 이후 데이터를 주고받기 위해 사용되는 소켓이다.
연결이 완료되면 Socket 객체를 반환하게 되고 소켓 내부의 Stream을 사용해서 데이터를 주고받게 된다.
(위 코드에서는 Data 보조 스트림을 사용해서 편리하게 이용했다.)
클라이언트 코드
public class Client {
private static final int PORT = 12345;
public static void main(String[] args) throws IOException {
log("클라이언트 시작");
Socket socket = new Socket("localhost", PORT);
DataInputStream input = new DataInputStream(socket.getInputStream());
DataOutputStream output = new DataOutputStream(socket.getOutputStream());
log("소켓 연결: " + socket);
// 서버에게 문자 보내기
String toSend = "Hello";
output.writeUTF(toSend);
log("client -> server: " + toSend);
// 서버로부터 문자 받기
String received = input.readUTF();
log("client <- server: " + received);
// 자원 정리
log("연결 종료: " + socket);
input.close();
output.close();
socket.close();
}
}
클라이언트는 호스트 이름과 서버의 포트 번호를 넣어서 Socket을 생성하게 된다.
서버와의 TCP 연결을 성공적으로 마치면 Socket 객체가 반환되어 데이터 통신을 진행할 수 있게 된다.

소켓 내부의 생성자를 보면 호스트 이름과 포트 번호를 받아서 소켓을 생성하게 된다.
즉 순서는 아래의 순서로 이루어진다.
1. 서버는 ServerSocket을 만들어서 클라이언트의 연결을 대기한다.
2. 클라이언트는 Server의 포트번호로 TCP 연결을 진행한다.
3. TCP 연결이 성공적으로 끝나면 서버는 OS backlog Queue에 TCP 연결 정보를 저장해 놓는다.
강의에서 말한 OS backlog Queue는 일반적으로는 listen backlog라고 불립니다.
백로그 큐는 서버가 동시에 처리하지 못한 클라이언트의 연결 요청 정보를 보관해 놓는 큐입니다.
백로그는 크기를 의미하며 아래 생성자를 통해 백로그의 크기도 같이 설정할 수 있습니다.
(백로그가 1000이면 대기할 수 있는 클라이언트의 요청은 1000 개가 됩니다.)
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
김영한 선생님 강의에서는 서버가 OS backlog Queue에 저장한다고 설명되어있지만
다른 개발자 분과 얘기하고 GPT의 대화를 합친 결과 커널에서 syn 큐로 연결 중인 정보를 관리하고, 연결이 완료된 정보를 백로그에 저장 후 accept()를 통해 소켓을 생성한다고 합니다.
해당 내용은 더 자세하게 알아보고 수정하겠습니다.(우선 강의 내용대로 정리했습니다.)

4. 서버는 소켓의 accept() 메서드를 호출해 OS backlog Queue에서 TCP 연결 정보를 꺼내 소켓 객체를 생성한다.
이때 accept() 메서드는 블로킹으로 동작합니다.
서버는 백로그에 데이터가 있다면 요청을 꺼내 소켓을 생성하게 되고 요청이 없다면 대기하게 됩니다.
5. 서로의 소켓 객체를 통해 데이터를 주고받게 된다.
조금 더 쉽게 그림으로 알아보자.
1. 서버는 포트를 지정한 ServerSocket을 하나 만들어서 해당 PORT 번호로 대기 상태에 들어간다.

2. 클라이언트는 포트가 12352인 서버에 연결을 시도한다. (이때 클라이언트의 포트 번호는 임의로 지정이 된다.)

3. 서버의 소켓을 찾으면 TCP 연결을 진행하게 된다.

4. TCP 연결이 성공적으로 진행되면 서버는 OS backlog Queue에 TCP 연결 정보를 저장해 놓는다.

5. 이후 accept() 메서드를 호출하면 OS backlog Queue에서 하나씩 꺼내 서버는 소켓을 생성한 뒤 클라이언트와 데이터 통신을 진행하게 된다.
(이때 Socket의 스트림을 사용한다)

위 과정이 끝나면 클라이언트와 서버는 서로의 소켓을 가지고 데이터 통신을 진행하게 된다.
소켓이라는 개념이 많이 모호한 개념이었었는데 그림으로 정리를 하고 찾아보니 이제야 흐름을 이해할 수 있게 되었다.