# 네트워크 (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


### TCP 연결 확립 - 3-way Handshake

- 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 데이터 전송

- 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)라고 한다.
- 


#### TCP의 흐름 제어

- 흐름 제어는 TCP 통신에서 중요한 역할을 하는 메커니즘이다. 이는 송신자가 수신자의 처리 능력을 초과하는 데이터를 전송하는 것을 방지하는 역할을 한다.
- TCP는 이를 위해 '슬라이딩 윈도우'라는 기법을 사용한다. 이 기법에서, 각 TCP 세그먼트는 순차적인 번호를 가지며, 수신자는 이 번호를 바탕으로 데이터를 올바르게 재조립하고, 누락된 세그먼트를 감지한다.
- 또한 수신자는 '윈도우 크기'라는 정보를 보내서 현재 자신이 받아들일 수 있는 데이터의 양을 알려준다. 송신자는 이 정보를 바탕으로 데이터 전송량을 조절한다.
- 이 기법은 네트워크의 혼잡도나 수신자의 처리 능력에 따라 동적으로 데이터의 전송 속도를 조절할 수 있게 해 주므로, TCP 통신의 효율성과 신뢰성을 크게 높인다.

### TCP 연결 끊기 - 4-way Handshake

- 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 제한에 영향을 받지 않는다.