본문 바로가기

CS/네트워크

TCP흐름 Wireshark로 확인해보기

개요

간단한 테스트 서버를 만들고 TCP 연결을 서버와 클라이언트 간 해보았습니다.

이 때 어떠한 패킷이 나가는지 와이어샤크로 확인해보고 이는 어떤 의미를 가지는지 확인해봅시다.

 

테스트 코드

서버

#include <WinSock2.h>
#include <WS2tcpip.h>
#include <iostream>

#pragma comment(lib, "ws2_32")

#define SERVER_PORT 12345
#define SERVER_IP  L"127.0.0.1" //LOOPBACK
#define MAX_BUF_SIZE 10000

using namespace std;

int main()
{
    WSAData wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        cout << WSAGetLastError() << '\n';
    }

    SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

    if (listenSocket == INVALID_SOCKET)
    {
        cout << WSAGetLastError() << '\n';
    }

    SOCKADDR_IN server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    InetPton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    if (bind(listenSocket, (sockaddr*)&server_addr, sizeof(server_addr)) != 0)
    {
        cout << WSAGetLastError() << '\n';
    }

    if (listen(listenSocket, SOMAXCONN) != 0)
    {
        cout << WSAGetLastError() << '\n';
    }

    SOCKADDR_IN client_addr;
    int addr_len = sizeof(client_addr);
    SOCKET sock = accept(listenSocket, (sockaddr*)&client_addr, &addr_len);

    if (sock == INVALID_SOCKET)
    {
        cout << WSAGetLastError() << '\n';
    }

    char buf[MAX_BUF_SIZE];
    ZeroMemory(&buf, MAX_BUF_SIZE);


    recv(sock, buf, MAX_BUF_SIZE, 0);

    WSACleanup();
    return 0;
}

 

클라이언트

#include <WinSock2.h>
#include <WS2tcpip.h>
#include <iostream>

#pragma comment(lib, "ws2_32")

#define SERVER_PORT 12345
#define SERVER_IP  L"127.0.0.1" //LOOPBACK

using namespace std;

int main()
{
	WSAData wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	{
		cout << WSAGetLastError() << '\n';
	}

	SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock == INVALID_SOCKET)
	{
		cout << WSAGetLastError() << '\n';
	}
	SOCKADDR_IN server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	InetPton(AF_INET, SERVER_IP, &server_addr.sin_addr);

	if (connect(sock, (sockaddr*)&server_addr, sizeof(server_addr)) != 0)
	{
		cout << WSAGetLastError() << '\n';
	}


	while (1)
	{
		Sleep(1000);
	}
	WSACleanup();
	return 0;
}

현재는 가장 간단한 코드이고 이를 변경해보며 TCP의 흐름을 확인해보겠습니다.

물론 절대로 프로젝트에서 사용할 수 없는 테스트용 코드입니다.


TCP 3 way handshake

현재 클라이언트는 connect이후에 아무런 데이터를 보내지 않고 busy wait하는 상태이며,

서버는 accept 이후에 recv를 시도하지만 수신 버퍼는 완전히 비어있기 때문에 blocking된 상태입니다.

 

또한 현재 SERVER_IP는 루프백 주소이기 때문에 패킷이 이더넷 바깥으로 나가지는 않습니다.

이 때 서버 port를 캡쳐하면 위와 같은 핸드쉐이크 과정을 확인할 수 있습니다.

 

클라이언트가 서버에게 SYN을 보내고, 서버는 클라이언트에게 SYN, ACK, 다시 클라가 서버에게 ACK를 보내며 연결을 수립합니다.

와이어샤크에서 확인가능한 전체 패킷의 간략한 정보인데 확인해 보면 TCP, IP 헤더는 설정되어있지만, 루프백으로 돌아오기 때문에 데이터링크 계층 헤더는 없다.

 

또한 옵션에서 Window scale 등 이후 사용할 통신 정보에 대한 클라 서버 양측간 합의도 이루어진다.


간단한 전송 수신

이제는 코드를 약간 고쳐 간단하게 전송과 수신을 보겠다.

    // 서버
    char buf[MAX_BUF_SIZE];
    ZeroMemory(&buf, MAX_BUF_SIZE);

    while (1)
    {
        int retval = recv(sock, buf, MAX_BUF_SIZE, 0);
        if (retval==SOCKET_ERROR)
        {
            cout << WSAGetLastError() << '\n';
            break;
        }
        cout << "recv: " << retval << '\n';
    }
    
    // 클라이언트
	char* buf = new char[500];

	send(sock, buf, 500, 0);
	send(sock, buf, 500, 0);
	send(sock, buf, 500, 0);
	send(sock, buf, 500, 0);

서버측은 2초마다 recv를 수행하며,

클라측은 500byte의 데이터를 4번 보낸다.

 

어떻게 될까?

일단 전체 흐름이다.

클라(10700)은 서버(12345)에게 길이500의 데이터을 보내며, 서버는 클라에게 데이터를 받았다며 ACK를 보내준다. 

서버의 window는 점점 줄어들며(window scale로 인해 정확히 500이 줄어들지는 않는다.) 클라이언트의 Seq에 대한 Ack를 보내고 있음을 확인할 수도 있다.

 

다만 서버에서 몇 바이트를 recv했는지 확인해보면 2000이 나온다.

이는 recv라는 동작이 TCP 수신 버퍼에서 값을 가져오는 행위이고, recv의 인자로 최대 MAX_BUF_SIZE를 가져올 수 있게했기 때문에 보낸 모든 데이터가 한번에 들어오는 것이다. 


Nagle's Algorithm?

생각해보면 네이글 알고리즘에 의해 MSS만큼 모아서 보내야될거같기도 한데 TCP는 stream이지만 500씩 4번 송신했다.

https://en.wikipedia.org/wiki/Nagle%27s_algorithm

 

Nagle's algorithm - Wikipedia

From Wikipedia, the free encyclopedia Means of improving the efficiency of TCP/IP networks Nagle's algorithm is a means of improving the efficiency of TCP/IP networks by reducing the number of packets that need to be sent over the network. It was defined b

en.wikipedia.org

네이글 알고리즘은 MSS만큼 모아서 보내야한다는 제약 외에도, ACK가 돌아오면 바로 보낸다는 제약도 있다.

따라서 현재는 루프백 환경이기 때문에 ACK가 바로 돌아와 매번 500씩 보내게 된 것이다.

 

그렇다면 환경을 바꿔 루프백이 아니라면 모아서 보내게 될까?

 

    //InetPton(AF_INET, SERVER_IP, &server_addr.sin_addr);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

이번에는 서버측에서는 다른 곳에서 오는 요청도 받기 위해 모든 NIC + 루프백에 대한 요청을 여는 INADDR_ANY를 주소로 하고,

로컬 환경 내의 다른 IP를 가진 PC환경을 클라이언트로 하여 send, recv 해보았다.

 이 때 wireshark의 이더넷 패킷캡쳐 상황이다.

 

 

이전과 다르게 500, 1460, 40으로 패킷이 전송된 것을 확인할 수 있다.

먼저 첫 500은 핸드쉐이킹 과정에서 도착한 ACK로 인해 바로 전송한 것임을 알 수 있고

이후에는 MSS크기에 따라 1460, 40으로 쪼개서 받음을 알 수 있다.

 

또한 수신측은 ACK를 매번 보내지 않고 모아서 누적 ACK를 보내는 것도 확인할 수 있다.


이후 수정하며 내용 추가 예정