TCP/IP 의 연결 과정

도도새·2025년 2월 14일

악수과정..?

CS를 또! 공부하다보니 TCP/IP에 대해 작성한 것을 봤고, 문득 궁금한 점이 생겼습니다. 패킷은 실제로 어떻게 생겼을까..?
사실 이론 공부를 하고, 네트워크 수업에서 TCP의 구조는 자주 봐왔으나 (심지어 와이어샤크로 보여주기까지 했었던 걸로 기억함) 역시나 내가하지 않으면 기억나지 않는법인거 같습니다.
오늘은 간단한 TCP/IP의 연결 과정을 한번 봐볼까 합니다.

준비물

TCP 과정을 직접 보기 위해선 간단한 준비물이 필요합니다.

  • 건강한 인텔리제이
  • 싱싱한 프로젝트
  • Java 클래스 2kg
  • 와이어샤크

사실 간단한 프로그램이라 인텔리제이없이 CLI 환경에서 자바를 실행해도 무방하나, 있는김에 사용했습니다. 코드는 간단합니다.

import java.io.*;
import java.net.*;

public class TCPServer {
    public static void main(String[] args) {
        int port = 5000; // 사용할 포트 번호
        try (ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("서버: 포트 " + port + "에서 대기 중...");
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("서버: 클라이언트 연결됨: " + clientSocket.getRemoteSocketAddress());
                
                // 간단하게 클라이언트에게 메시지 전송
                PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
                out.println("안녕하세요, 클라이언트님!");
                
                // 클라이언트와의 통신 후 소켓 종료
                clientSocket.close();
                System.out.println("서버: 클라이언트 연결 종료됨: " + clientSocket.getRemoteSocketAddress());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
import java.io.*;
import java.net.*;

public class TCPClient {
    public static void main(String[] args) {
        String serverAddress = "127.0.0.1"; // 로컬호스트
        int port = 5000;
        try (Socket socket = new Socket(serverAddress, port)) {
            System.out.println("클라이언트: 서버에 연결됨: " + socket.getRemoteSocketAddress());
            
            // 서버로부터 메시지 읽기
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            String message = in.readLine();
            System.out.println("클라이언트: 서버로부터 메시지 받음: " + message);
            
            // 연결 종료 후 소켓이 자동으로 close()됨 (try-with-resources 사용)
            System.out.println("클라이언트: 연결 종료");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

순서에 맞게 서버를 먼저 실행시켜 주시면 됩니다. 서버는 5000 포트에 소켓을 열고, 리스닝 상태로 대기합니다. 클라이언트 또한 소켓을 열고 서버의 포트에 연결을 시도합니다. 서버와 클라이언트가 연결이 된다면 이후 해당 소켓에게 getOutputStream 을 통해 println 메세지를 전달합니다. 클라이언트 측은 이를 받아 화면에 출력 후 종료합니다.

이로서 간단한 로직은 완료가 되었습니다. 이 과정에서 서버는 클라이언트와 연결을 맺고(3-way-handshake) 끊는 과정(4-way-handshake)을 순차적으로 진행합니다. 이를 와이어 샤크를 통해 확인해보겠습니다.

위의 과정을 모두 와이어샤크를 통해 캡쳐한 사진입니다.
순서대로 보겠습니다!

3-way-handshake

  1. No.225: 127.0.0.1:60963 -> 127.0.0.1:5000 [SYN] 클라이언트가 서버에게 연결을 요청
  2. No.226: 127.0.0.1:5000 -> 127.0.0.1:60963 [SYN, ACK] 서버가 그에 대한 응답으로 반환
  3. No.227: 127.0.0.1:60963 -> 127.0.0.1:5000 [ACK] 확인한 클라이언트가 다시 ACK를 반환하며 TCP 연결 성립

이처럼 일련의 과정 이후 TCP 연결이 완료됩니다.

데이터 전달

  1. No.230: 127.0.0.1:5000 -> 127.0.0.1:60963 [PSH, ACK] 서버에서 클라이언트 측으로 데이터를 전송합니다. 이때는 페이로드 쪽에 데이터를 담아 전달합니다. 이때 클라이언트 측에 "안녕하세요, 클라이언트님!" 라는 메세지는 전달하는데 이는 UTF-8 기준 36 Byte입니다. 사진에서 38 byte로 나오는데 이는 println 으로 인한 "\r\n" 추가로 2바이트가 추가돼 총 38 byte로 추정됩니다.
  2. No.231: 127.0.0.1:60963 -> 127.0.0.1:5000 [ACK] 전달받은 클라이언트는 수신 시 다시 서버에게 ACK를 반환합니다. 이때 ACK 값은 기존 byte 값에 +1을 더한 값인 39입니다.

4-way-handshake

  1. No.232: 127.0.0.1:60963 -> 127.0.0.1:5000 [FIN, ACK] 클라이언트가 먼저 연결 종료를 요청하면서 이전까지의 데이터에 대한 ACK를 함께 전달합니다(이를 위해 가장 마지막에 받았던 ACK를 Seq에 반환합니다).
  2. No.233: 127.0.0.1:5000 -> 127.0.0.1:60963 [ACK] 서버가 클라이언트의 FIN을 잘 받았음을 알리는 ACK를 보냅니다. 하지만 서버 측에서 아직 데이터를 보낼 게 남았거나 종료 준비가 덜 되었을 수 있어, 즉시 FIN을 보내지 않을 수도 있습니다.
  3. No.234: 127.0.0.1:5000 -> 127.0.0.1:60963 [FIN, ACK] 서버도 연결을 종료하겠다는 FIN을 보냅니다(동시에 이전 패킷에 대한 ACK 의미 포함합니다.)
  4. No.235: 127.0.0.1:60963 -> 127.0.0.1:5000 [ACK] 클라이언트가 서버의 FIN을 최종 확인(ACK)합니다. 이로써 양방향 모두 연결을 닫았으므로, TCP 세션이 완전히 종료됩니다.

도중에 클라이언트가 비정상 종료를 하게 된다면 FIN 패킷을 보내지 못하기 때문에 서버는 FIN을 기다리는 상태(TIME_WAIT 또는 CLOSE_WAIT)에 들어가게 됩니다.
하지만 TCP Keep-Alive 또는 Timeout 설정에 의해 연결이 자동 종료됩니다.

마치며

오늘도 짧게 TCP를 직접 실행해봤습니다. 워낙 중요하고, 자주 나오는 개념이라 알고 있었지만 이렇게 와이어샤크와 간단한 프로그램을 통해 확인한 것은 처음이라 흥미로웠습니다.
음... 그럼 이만..

profile
깃허브 : https://github.com/YoHanKi

0개의 댓글