일반적으로, 애플리케이션 계층에서는 전송 계층까지만 직접 닿을 수 있다.
하지만, 잘 알고 있는 애플리케이션 계층과는 다르게, 전송 계층까지는 그리 잘 알지 못한다.
TCP를 파헤쳐보자...
1. TCP. 전송 계층에서 사용하는 세그먼트 구조
헤더 필드
+
데이터 필드
세그먼트는 위와 같이 구성된다.
MSS : 세그먼트에 담길 수 있는 데이터의 최대 크기이다.
MTU : 네트워크 계층에서 패킷의 최대 크기이다.
ex) MSS = MTU - IP 패킷 헤더 크기
**보통 MTU는 1500 바이트, MSS는 1460 바이트라고 한다. 그러면, IP 패킷 헤더는 40이겠지?**
2. MSS(1000바이트)의 상황에서 이보다 큰 파일을 TCP를 통해 전송한다면?
500000바이트의 파일을 보낸다고 가정해보자.
MSS는 1000바이트이다.
총 500개의 세그먼트가 필요하겠다.
이때, TCP 세그먼트의 헤더 필드에 보이는 Sequence Number와 Acknowledgment Number가 큰 일을 한다.
아래는 TCP 전송 프로토콜에서 사용되는 TCP 관련 구조체이다.
ISS | Initial Send Sequence: 연결 시작 시 임의로 정해진 최초 시퀀스 번호 |
SND.UNA | Send UNAcknowledged: 아직 상대방에게 ACK 받지 못한, 가장 작은 시퀀스 번호 |
SND.NXT | Send NeXT: 다음으로 보낼(할당할) 시퀀스 번호 |
SND.WND | Send WINdow: 상대방이 현재 허용한 윈도우 크기(바이트 단위) == 흐름 제어(백프레셔) |
SND.MAX | (일부 구현) 최대 할당 가능한 시퀀스 번호(초과 시 버퍼 오버플로 방지) |
2-1. 애플리케이션 계층에서 TCP 송신 버퍼로 파일을 복붙하다.
애플리케이션 계층에서 write()를 통해 500000바이트의 파일을 전송 계층의 송신 버퍼로 복사
그저 버퍼 상에 바이트 스트림(바이트 배열[])로 복사되어 올라간다.(포인터가 아니라, 복 붙이다.)
2-2. TCP 송신 버퍼에서 세그먼트 단위로 묶어 하나의 헤더를 할당하다.
바이트 스트림을 MSS만큼씩 잘라, 세그먼트 구조체로 묶어, 헤더를 할당한다.
이때, 헤더에는 0,1000,2000,3000 ... ,499000의 순서 번호(Sequence Number)가 들어갈 수 있겠다.
**주의할 점은 한 번에 모든 바이트 스트림을 세그먼트로 구성하는게 아니라, 보낼 데이타만 세그먼트로 구성한다.**
할당한 후, SND.NXT에 방금 생성된 바이트 스트림 크기만큼 더한다. (0 + 1000 = 1000 => 다음 세그먼트 순서번호)
2-3. 네트워크 계층에서 패킷으로 변환 등 물리...등등 으로 보내다.
보낸다.
SND.UNA = {순서 번호}
위의 {순서 번호}는 5번 목차의 신뢰성 보장에서 달라질 수 있다.
2-4. 윈도우 체크(흐름 제어)
1000개의 바이트 스트림을 세그먼트로 만들어(순서번호 0) 보냈고, 아직 이 순서번호를 ACK로 받지 못했다.(SND.UNA = 0)
if(SND.NXT – SND.UNA ≤ SND.WND)인 동안만 전송을 허용한다.
만약, 이 if문에서 false가 나온다면, TCP 전송을 막는다.
**SND.WND는 수신자 쪽에서 동적으로 할당해준다. 아래 흐름 제어 서비스(6번 목차)에서 자세하게 다룬다.**
**뭔가 Reactive Streams의 백프레셔와 매우 비슷한 것 같다. 네트워크 흐름 제어 서비스 여러 전략들 공부 후 Reactive Streams에 적용해보자.**
2-5. ACK 수신
수신자로부터 처리 응답을 받으면, SND.UNA를 갱신해준다.
SND.UNA = Math.max(SND.UNA, {수신자쪽에서 현재 처리한 순서 번호})
아래 신뢰성 보장(5번 목차)에서 자세히 보자.
3. 순서 번호와 응답 확인
https://icanchangeworld.tistory.com/188
WireShark로 TCP 플로우 확인하기
1. 와이어 샤크 클릭2. 이더넷 클릭=> 내 컴퓨터와 가정용 공유기 사이 인터페이스를 캡쳐하는 방식=> 내 컴퓨터의 송/수신 인터페이스에서 송/수신되는 패킷들을 볼 수 있다.3. 뭐가 막 파바박 뜬
icanchangeworld.tistory.com
네이버에 드갈때, 3-way-handshake가 보인다.
3-1. 내 컴퓨터가 네이버에 SYN을 보낸다.
내 컴퓨터{Seq : 0, Ack: 0} -> {Seq : 1, Ack : 0}
서버{Seq : 0, Ack : 0} -> {Seq : 0, Ack : 1}
=> 연결 요청할게!
=> SYN는 1 바이트
3-2. 네이버 서버가 내 컴퓨터에 SYN, ACK을 보낸다.
내 컴퓨터{Seq : 1, Ack: 0} -> {Seq : 1, Ack : 1}
서버{Seq : 0, Ack : 1} -> {Seq : 1, Ack : 1}
=> ㅇㅋ 잘 알아들었고, 연결 가능해!
=> 응답 SYN 또한 1바이트
3-3. 내 컴퓨터는 마지막으로 ACK를 보내 3-way-handshake를 완료한다.
내 컴퓨터{Seq : 1, Ack: 1} -> {Seq : 1, Ack : 1}
서버{Seq : 1, Ack : 1} -> {Seq : 1, Ack : 1}
=> 마지막 확인, 내 컴퓨터<->서버 동기화 확인
4. 왕복 시간(RTT) 예측, 타임 아웃
클라이언트가 ACK를 보냈다.
서버가 ACK를 받았고, ACK 응답을 보낸다.
클라이언트는 서버가 보낸 ACK 응답을 받지 못하고 있다.
타임아웃이된다.
서버는 자신이 보낸 응답에 대한 ACK를 받지 못했기에, 클라이언트가 수신에 문제가 있다고 생각한다.
떄문에, 재전송을 한다.
이때, 2가지 생각할 것이 생긴다.
=> 서버 자신이 보낸 응답에 대한 ACK를 얼마나 최대로 기다려야 할까?
=> 만약, 클라이언트에게 여러개의 세그먼트를 보냈을때, 어디서부터 다시 보내야 할까?
4번 목차에서는 첫 번째 사항을 생각해본다.
4-1. ACK를 얼마나 최대로 기다릴까?
다시, 클라이언트 입장에서 생각해보겠다.
만약, 클라이언트가 송신을 했다면, 내 요청이 가는 시간 + 서버에서 응답을 보내 여기까지 오는 시간만큼은 기다려야하지 않겠는가?
하지만, 중간에 어떠한 다른 변수를 만날 수 있기 때문에, 찐막 찐찐막 찐찐찐막 이렇게 조금은 더 기다려줘야하지 않을까?
이를 RTT라고 한다.
4-2. RTT
SampleRTT : 왕복 시간. 여러 세그먼트 중 하나의 세그먼트 왕복 시간만을 측정한다.
EstimatedRTT : 많은 세그먼트들의 왕복에서 SampleRTT들을 모아, 평균을 낸다.
지수적 가중 이동 평균(EWMA) : 이런 평균들의 집합이 그래프 모양처럼 된 것을 말한다.
DevRTT : RTT 변화율이다.
**이런 시간들이 있고, RTT보다 조금 더 긴 시간을 타임아웃 주기로 설정한다고 보면 될 것 같다.**
5. TCP에서 신뢰성 보장
6. 개별 수신 버퍼. 그리고 흐름 제어 서비스(백프레셔)
7. 3-way-handshake
8. 혼잡 제어
9. Linux에서의 소캣 통신
위의 함수들을 살펴보자.
9-1. socket() : 소켓을 생성하다.(서버, 클라이언트)
int socketfd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
빈 소켓을 생성한다. 위에서는 IPv4, 스트림 소켓 타입, IP프로토콜을 사용하는 빈 소켓을 초기화함을 의미한다.
아래는 이렇게 생성된 소켓의 구조체이다.(아직 빈 값이다.)
// IPv4
struct sockaddr_in{
sa_family_t sin_family; //IPv4를 뜻한다.
in_port_t sin_port; //port를 정해준다.
struct in_addr sin_addr; //수신자 IP를 32비트로 정해준다.
char sin_zero[8]; //패딩
};
struct in_addr{
in_addr_t s_addr
};
===========================================================
// IPv6
struct sockaddr_in{
sa_family_t sin6_family; //IPv6를 뜻한다.
in_port_t sin6_port; //port
uint32_t sin6_flowinfo; //흐름 제어에 쓰인다.
struct in6_addr sin6_addr; //수신자 IP를 128비트로 정해준다.
uint32_t sin6_scope_id; //서브넷(LAN) 내에서만 유효하다.
};
struct in6_addr{
uint8_t s6_addr[16];
};
9-2. bind() : 소켓에 외부 인터페이스를 등록하다.(서버)
구조체에 정보들을 세팅해주고 최종적으로 정보들과 소켓 파일 기술자(SD)를 합쳐주는 작업이다.
즉, 소켓에 정보를 설정해주는 작업들의 집합이라 할 수 있겠다.
struct sockaddr_in saddr_s = {};
char *addrstr_listen = "192.168.0.10"
saddr_s.sin_family = AF_INET;
saddr_s.sin_addr.s_addr = inet_addr(addrstr_listen);
sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
bind(sd, (struct sockaddr *)saddr_s, sizeof(struct sockaddr_in));
9-3. listen() : 백로그를 설정하다.(서버)
listnen()은 connect() 함수 : 3-way-handshake를 위한 함수이다.
2가지의 백로그를 초기화함으로 안정적은 connection이 가능하도록 한다.
웹 환경에서는 여러 TCP가 계속해서 생성되고, 삭제된다.
9-3-1. 백로그
백로그는 큐 구조로, 커널이 보류 상태로 유지할 수 있게 하는 큐이다.
백로그는 두가지로 나눌 수 있는데, SYN 큐와 Accept 큐이다.
9-4. connect() : 클라이언트 측이 연결을 시도하다.(클라이언트)
서버가 listen() 까지 마치면, 클라이언트는 listen()에서 준비된 큐로 연결 요청을 날릴 수 있다.
클라이언트 측에서 생성한 소켓은 listen()은 필요없고(클라이언트로 여러 연결 요청이 날라오지 않을 것이기 때문)
bind()가 필요한데, 이 또한 connect()에서 실행한다.
보통 동적으로 소캣 Port를 생성/할당 시켜주는데, connect()에서 동적할당이 아닌, 직접 bind()를 호출해 소켓을 설정해주면, 정적으로 사용자가 소켓 Port를 생성/할당 시켜줄 수 있다.
struct sockaddr_in saddr_c = {};
char a_ipaddr[INET_ADDRSTRLEN];
...
if((fd_client = socket(AF_INET, SOCK_STREAM, IPPROTO_IP)) == -1){error}
snprintf(a_ipaddr, sizeof(a_ipaddr), "192.169.x.xxx");
saddr_c.sin_family = AF_INET;
saddr_c.sin_addr.s_addr = inet_addr(a_ipaddr);
saddr_c.sin_port = htons((short)atoi("1080"));
if(connect(fd_client, (struct sockaddr *)&addr_c, sizeof(saddr_c)) == -1){error}
9-5. accept() : listen의 백로그 확인 후, 연결을 하다.(서버)
struct sockaddr_storage ss_client;
socklen_t len_ss_client = sizeof(ss_client);
fd_client = accept(fd_listener, (struct sockaddr *)&ss_client, &len_ss_client);
if(fd_client == -1)
**이제, TCP 소켓 연결이 완료되었다. 데이터를 송/수신 해보자**
9-6. send(), recv() : 보내고 받다.
정상적으로 연결에 성공했다. == 서버 측: accept() 성공으로, 파일 기술자 리턴 받았다. 클라이언트 측 : connect()를 성공했다.
ssize_t send(int sockfd, const void *buffer, size_t length, int flags);
ssize_t recv(int sockfd, void *buffer, size_t length, int flags);
send(파일 기술자, 송신버퍼, 버퍼에 담긴 데이터 크기, 여러 옵션)
recv(파일 기술자, 수신 버퍼, 버퍼에 담긴 데이터 크기, 여러 옵션)
9-7. close()/shutdown() : TCP를 마치다.
9. WireShark로 살펴보기
10. java.net.Socket을 사용한 TCP 통신 구현
11. Pcap4J를 통한 세그먼트 파싱 구현
12. UDP를 TCP로 간단하게 커스텀해보기
**들어가기 전에**
https://icanchangeworld.tistory.com/185
UDP에 대하여
icanchangeworld.tistory.com
13. 12와 함께 HTTP/3을 사용해보기
'네트워크' 카테고리의 다른 글
WireShark로 TCP 플로우 확인하기 (0) | 2025.05.26 |
---|---|
UDP에 대하여 (0) | 2025.05.24 |
웹 페이지 요청이 있을 때 플로우 (1) | 2025.05.24 |
여러가지 네트워크 환경 분석(인프라 시작하기) (0) | 2025.05.23 |
데이터 링크 계층 (0) | 2023.11.07 |