# 네트워크 (5) - TCP/UDP ## TCP(Transmission Control Protocol) ### TCP란? - TCP는 인터넷 프로토콜 스택, 즉 TCP/IP 프로토콜 스택의 핵심 프로토콜 중 하나로, 전송 계층에서 작동한다. - 이 프로토콜은 신뢰성 있는 데이터 전송을 보장한다. 즉, 데이터는 손상, 손실, 중복 또는 순서 변경 없이 정확하게 도착한다. - TCP는 데이터를 패킷 단위로 분할하여 IP 네트워크를 통해 전송하며, 수신측에서는 이 패킷들을 원래의 순서대로 재조립한다. - TCP는 3-way Handshake를 통해 연결을 설정하고 4-way Handshake를 통해 연결을 종료한다. 이렇게 연결 지향적인 통신을 제공한다. - 각 TCP 패킷에는 순서 번호 및 확인 응답 번호(ACK 번호)가 포함되어 있어, 데이터의 정확한 전송을 보장한다. - 또한, TCP는 흐름 제어와 혼잡 제어 기능을 제공하여 네트워크의 효율적인 사용을 지원한다. ### TCP 통신 과정 1. TCP 연결 확립 - 3-way Handshake 2. TCP 데이터 전송 3. TCP 연결 끊기 - 4-way Handshake ![image](https://hackmd.io/_uploads/HJMjY0DF2.png) ![image](https://hackmd.io/_uploads/HypKTCwth.png) ### TCP 연결 확립 - 3-way Handshake ![image](https://hackmd.io/_uploads/SkfL_RwK2.png) - 3-way Handshake는 TCP (Transmission Control Protocol)에서 연결을 시작할 때 사용되는 프로세스이다. - 이 프로세스는 클라이언트와 서버 사이의 통신이 시작되기 전에 세션을 정확하게 설정하고 동기화하기 위해 사용된다. - 3단계 프로세스는 다음과 같다 1. SYN: 클라이언트가 서버에게 연결 요청을 보내기 시작하며, 이를 'SYN' 패킷이라고 한다. 이 패킷은 초기 시퀀스 번호(ISN)를 포함하며, 이 번호는 랜덤하게 생성된다. 2. SYN-ACK: 서버가 클라이언트의 요청을 받으면, 요청을 수락하기 위해 'SYN-ACK' 패킷을 클라이언트에게 보낸다. 이 패킷은 서버의 초기 시퀀스 번호와, 클라이언트의 시퀀스 번호에 1을 더한 ACK 번호를 포함한다. 3. ACK: 클라이언트는 마지막으로 'ACK' 패킷을 보내어 서버의 SYN-ACK 패킷을 확인한다. 이 패킷은 서버의 시퀀스 번호에 1을 더한 ACK 번호를 포함한다. - 이렇게 3-way Handshake를 통해 클라이언트와 서버 사이에 안정적인 연결이 성립된다. > `SYN` = `Synchronize` > `ACK` = `Acknowledgement` ### TCP 데이터 전송 ![image](https://hackmd.io/_uploads/HJzlcAwY3.png) - TCP는 데이터를 송신하기 전에 패킷으로 분할하는 프로세스를 사용한다. 이렇게 하면 각 패킷은 IP 네트워크를 통해 개별적으로 전송될 수 있다. - 패킷 분할 과정 1. 먼저, 데이터는 TCP 세그먼트라는 작은 단위로 나뉜다. 2. 각 TCP 세그먼트에는 헤더가 추가되며, 헤더에는 중요한 제어 정보(예: 시퀀스 번호, ACK 번호, 플래그 등)가 포함된다. 3. TCP 세그먼트는 IP 데이터그램에 캡슐화되고, 이 데이터그램은 IP 네트워크를 통해 수신측으로 전송된다. - 패킷 조립 과정 1. 수신측에서는 각 IP 데이터그램을 해독하여 TCP 세그먼트를 추출한다. 2. 각 TCP 세그먼트의 헤더를 검사하여 세그먼트가 올바른 순서대로 조립되고, 손실되거나 중복되는 세그먼트가 없는지 확인한다. 3. TCP 세그먼트들이 올바르게 조립되면, 데이터는 원래의 순서대로 재조립되고, 응용 프로그램에 전달된다. - TCP의 이런 패킷 분할과 조립 메커니즘은 신뢰성 있는 데이터 전송을 가능하게 한다. - TCP에서 데이터를 분할하는 단위를 MSS(Maximum Segment Size, 최대 세그먼트 길이)라고 한다. - 네트워크에서 한 번에 보낼 수 있는 데이터 크기를 MTU(Maximum Transmission Unit)라고 한다. - ![image](https://hackmd.io/_uploads/SJAObAqFh.png) ![image](https://hackmd.io/_uploads/r1ByCRDF2.png) ![image](https://hackmd.io/_uploads/S1pg0CPY3.png) #### TCP의 흐름 제어 ![image](https://hackmd.io/_uploads/SJlkkyOKn.png) - 흐름 제어는 TCP 통신에서 중요한 역할을 하는 메커니즘이다. 이는 송신자가 수신자의 처리 능력을 초과하는 데이터를 전송하는 것을 방지하는 역할을 한다. - TCP는 이를 위해 '슬라이딩 윈도우'라는 기법을 사용한다. 이 기법에서, 각 TCP 세그먼트는 순차적인 번호를 가지며, 수신자는 이 번호를 바탕으로 데이터를 올바르게 재조립하고, 누락된 세그먼트를 감지한다. - 또한 수신자는 '윈도우 크기'라는 정보를 보내서 현재 자신이 받아들일 수 있는 데이터의 양을 알려준다. 송신자는 이 정보를 바탕으로 데이터 전송량을 조절한다. - 이 기법은 네트워크의 혼잡도나 수신자의 처리 능력에 따라 동적으로 데이터의 전송 속도를 조절할 수 있게 해 주므로, TCP 통신의 효율성과 신뢰성을 크게 높인다. ![image](https://hackmd.io/_uploads/ByoXJkOKn.png) ### TCP 연결 끊기 - 4-way Handshake ![image](https://hackmd.io/_uploads/BJJHp0PYn.png) - TCP 연결을 종료할 때는 4단계의 '핸드셰이크' 과정이 진행된다. 이 과정은 연결 설정에 사용되는 3-way handshake와 유사하지만, 종료를 위한 추가적인 단계가 있다. - 연결 종료 과정은 다음과 같다 1. FIN: 연결을 종료하고자 하는 호스트는 'FIN' 플래그가 설정된 TCP 세그먼트를 보낸다. 이는 "나는 더 이상 데이터를 보낼 것이 없으며 연결을 종료하고 싶다"라는 의미다. 2. ACK: 수신 호스트는 이 FIN 세그먼트를 받고, 확인 응답으로 'ACK' 세그먼트를 보낸다. 이는 "나는 당신의 종료 요청을 받았으며 이를 처리하고 있다"라는 의미다. 3. FIN: 이후 수신 호스트는 자신이 보낼 모든 데이터를 전송한 후, 자신의 'FIN' 세그먼트를 보낸다. 이는 "나도 이제 더 이상 데이터를 보낼 것이 없으며 연결을 종료하려 한다"는 의미다. 4. ACK: 원래의 호스트는 마지막으로 'ACK' 세그먼트를 보내, 상대방의 'FIN' 세그먼트를 확인한다. 이는 "나는 당신의 연결 종료 준비를 인지하였으며 이제 연결을 종료해도 좋다"라는 의미다. - 이렇게 4-way handshake 과정을 통해 TCP 연결은 안정적으로 종료되며, 이 과정은 양측이 데이터 전송을 완료했음을 보장한다. ## TCP/UDP Echo 서버/클라이언트 예시 > Wireshark를 설치하여 실재 패킷을 주고받는 과정을 확인해보기 > > - Wireshark : 네트워크 패킷 분석을 위한 가장 널리 사용되는 오픈소스 도구로, 실시간으로 네트워크 트래픽을 캡처하고 분석할 수 있다. 또한, Wireshark는 이미 캡처된 데이터의 로그 파일을 분석하는 데도 사용할 수 있다. ### UDP 서버 ```java import java.io.*; import java.net.*; public class UdpEchoServer { public static void main(String[] args) throws IOException { // 데이터그램 소켓을 생성하고, 8000번 포트로 수신 대기한다. DatagramSocket serverSocket = new DatagramSocket(8000); // 데이터를 받을 바이트 배열을 준비한다. byte[] receiveData = new byte[1024]; while (true) { // 데이터그램 패킷을 생성하고, 데이터를 받는다. DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); serverSocket.receive(receivePacket); // 받은 데이터를 문자열로 변환한다. String message = new String(receivePacket.getData()); // 메시지를 출력한다. System.out.println("Received: " + message); } } } ``` ### UDP 클라이언트 ```java import java.io.*; import java.net.*; public class UdpEchoClient { public static void main(String[] args) throws IOException { // 데이터그램 소켓을 생성한다. DatagramSocket clientSocket = new DatagramSocket(); // 서버의 IP 주소를 가져온다. InetAddress IPAddress = InetAddress.getByName("localhost"); // 메시지를 준비한다. String message = "Hello Server!"; // 메시지를 바이트 배열로 변환한다. byte[] sendData = message.getBytes(); // 데이터그램 패킷을 생성하고, 서버로 보낸다. DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, 8000); clientSocket.send(sendPacket); } } ``` ### TCP 서버 ```java import java.io.*; import java.net.*; public class TcpEchoServer { public static void main(String[] args) throws IOException { // 서버 소켓을 생성하고, 8000번 포트로 수신 대기한다. ServerSocket serverSocket = new ServerSocket(8000); while (true) { // 클라이언트로부터 연결 요청을 받는다. Socket socket = serverSocket.accept(); // 클라이언트로부터 받은 메시지를 읽는다. String message = new String(socket.getInputStream().readAllBytes()); // 메시지를 출력한다. System.out.println("Received: " + message); } } } ``` ### TCP 클라이언트 ```java import java.io.*; import java.net.*; public class TcpEchoClient { public static void main(String[] args) throws IOException { // 소켓을 생성하고, localhost의 8000번 포트로 연결 요청을 보낸다. Socket socket = new Socket("localhost", 8000); // 서버로 메시지를 전송한다. socket.getOutputStream().write("Hello Server!".getBytes()); } } ``` ## TCP 파일 송수신 서버/클라이언트 예시 ```java import java.io.*; import java.net.*; public class TcpFileServer { public static void main(String[] args) throws IOException { // 서버 소켓을 생성하고, 8000번 포트로 수신 대기한다. ServerSocket serverSocket = new ServerSocket(8000); while (true) { // 클라이언트로부터 연결 요청을 받는다. Socket socket = serverSocket.accept(); // 전송할 파일을 읽는다. File file = new File("file.txt"); FileInputStream fileIn = new FileInputStream(file); // 소켓으로부터 출력 스트림을 가져온다. OutputStream out = socket.getOutputStream(); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = fileIn.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } fileIn.close(); out.close(); socket.close(); // 메시지를 출력한다. System.out.println("File sent successfully!"); } } } ``` ```java import java.io.*; import java.net.*; public class TcpFileClient { public static void main(String[] args) throws IOException { // 소켓을 생성하고, localhost의 8000번 포트로 연결 요청을 보낸다. Socket socket = new Socket("localhost", 8000); // 소켓으로부터 입력 스트림을 가져와서 버퍼드 인풋 스트림으로 읽는다. InputStream in = socket.getInputStream(); // 서버로부터 받은 파일을 읽는다. File receivedFile = new File("received.txt"); FileOutputStream out = new FileOutputStream(receivedFile); byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } out.close(); in.close(); socket.close(); System.out.println("Received file successfully!"); } } ``` > 위의 코드를 실행할 때는 네트워크를 통해 전송되는 것이 아니라, TCP 소켓을 통해 직접 데이터를 주고 받고 있기 때문에, 이 경우 운영 체제는 네트워크 스택에 있는 TCP 버퍼를 직접 읽고 쓰며, 이는 MTU 제한에 영향을 받지 않는다.