[Network] TCP/IP Concept & Programming(Single-Thread)

녹차·2025년 12월 30일

Network

목록 보기
4/15
post-thumbnail

Introduction

이번 글에서는 Unity와의 연동이 아닌, Visual Studio 환경에서 동작하는 클라이언트 단독 코드를 통해 TCP/IP 통신을 다룹니다. 구현 언어는 C++이지만, 개념 이해에 집중할 수 있도록 C 스타일의 코드 형식으로 작성하여 관련 기능들을 단계적으로 학습해보겠습니다.

다만 이전 글에서 기본적인 네트워크 개념을 다루었더라도, TCP 자체에 대한 이해가 부족한 상태에서 곧바로 코드를 작성하면 흐름을 놓치기 쉽습니다. 따라서 본격적인 프로그래밍에 들어가기 전에, 먼저 TCP의 동작 원리에 대한 개념 설명을 간단히 정리한 뒤 구현을 진행하겠습니다.

(이 글은 여러 기술 문서와 교재 등 다양한 자료를 참고하였습니다)

Body

TCP/IP Concept

TCP(Transmission Control Protocol)는 보내는 쪽 데이터가 받는 쪽에서 완전히 동일함을 보장해 주는 프로토콜입니다. 이후에 설명하겠지만, UDP는 그저 sendTo(), recvfrom() 함수만 있으면 데이터를 주고 받을 수 있습니다.

하지만 TCP는 데이터를 주고받기 전에 '연결'이라는 과정을 먼저 해야 합니다. 이를 보고 연결 지향형(connection oriented)이라고 합니다.

연결한 후에야 데이터를 주고받을 수 있다는 불편함이 있지만, 그 대신 보낸 데이터가 받은 쪽에서 정확히 모두 도착한다는 것을 보장합니다. 이를 '신뢰성을 보장한다'고 표현합니다.

TCP란? 신뢰성을 보장하는 연결 지향 프로토콜입니다.

TCP/IP Characteristics

위에 TCP 특징 + 알파로 자세히 설명해보겠습니다.

1. TCP는 'Connection-oriented 프로토콜'입니다.

TCP 통신은 데이터를 보내기 전에 반드시 상대방과 연결을 맺습니다. 이 연결 과정은 흔히 3-way handshake라고 불리는 절차를 통해 이루어집니다.

송신자는 연결 요청을 보내고(SYN), 수신자는 이에 응답하며(SYN-ACK), 마지막으로 송신자가 다시 확인 응답(ACK)을 보냄으로써 양쪽 모두 통신 준비가 되었음을 확인합니다.

이 과정을 거친 후에야 비로소 데이터 송수신이 가능해집니다. 이 때문에 TCP에서는 “상대방이 존재하는지”, “통신 가능한 상태인지”를 명확히 알 수 있습니다.

3-way handshake대한 설명은 이 글에서 너무 벗어나므로 아래와 관련 링크를 걸어두었습니다.

2. TCP는 신뢰성 있는 데이터 전송을 보장합니다

TCP는 IP가 제공하지 않는 신뢰성(Reliability)을 자체적으로 구현합니다. IP는 패킷 유실, 중복, 순서 뒤바뀜이 발생할 수 있는 비신뢰적인 프로토콜이지만, TCP는 이러한 문제를 거의 느끼지 않도록 감춰줍니다.

TCP는 내부적으로 스트림 데이터를 세그먼트 단위로 나누어 전송하고, 수신자는 정상적으로 받은 데이터에 대해 ACK(Acknowledgement)를 송신자에게 보냅니다. 송신자는 일정 시간 안에 ACK를 받지 못하면 해당 세그먼트를 다시 전송합니다. 이 재전송 메커니즘 덕분에 TCP는 패킷 유실이 발생하더라도 결과적으로 데이터를 정확하게 전달할 수 있습니다.

segment에 대한 설명은 이 글에서 너무 벗어나므로 아래와 관련 링크를 걸어두었습니다.

3. TCP는 Unicast 통신을 지원합니다

TCP는 Unicast 통신을 맺고 그 연결 안에서 양방향(Full-Duplex) 통신을 지원합니다.

  • Client → Server 데이터 송신
  • Server → Client 데이터 송신

이 두 흐름은 논리적으로 독립된 송신/수신 흐름으로 동시에 동작합니다.

즉, 한쪽이 데이터를 보내고 있는 동안에도 반대편은 자유롭게 데이터를 보낼 수 있습니다.

4. TCP는 Stream 기반입니다

일반적인 Stream이란 데이터를 연속적인 흐름으로 다루는 추상적인 개념입니다. 두 단말기를 연결한 후, 그 연결을 끊기 전까지 한쪽에서 다른 한쪽으로 흘러가는 연속된 데이터 흐름 하나를 Stream이라고 합니다.

Stream자체는 데이터의 경계를 인식하지 않습니다. 즉, Stream 안에 있는 데이터를 어디에서 끊어 해석할지는 전적으로 개발자의 몫이며, Stream은 단순히 바이트(Byte)의 연속만을 전달합니다.

IO Stream, File Stream 등의 Stream 또한 위에 의미를 갖습니다.

하지만 네트워크에서의 Stream 개념은 조금 더 독특합니다. 단말기 A에서 Stream 형태로 데이터를 송신하고, 단말기 B에서 이를 수신한다고 가정해 봅시다.

이때 단말기 A가 한 번의 송신 함수(send)로 보낸 데이터와, 단말기 B가 한 번의 수신 함수(recv)로 받은 데이터가 1:1로 정확히 대응된다고 보장할 수 없습니다. 하지만 중요한 성질이 하나 있습니다.

TCP 연결이 유지되는 동안, 송신 측에서 보낸 바이트 스트림의 순서는 수신 측에서도 동일하게 유지됩니다.

즉, 보낸 횟수와 받은 횟수는 다를 수 있고 한 번에 보낸 데이터의 경계가 수신 시점에서는 깨질 수 있으며 여러 번 보낸 데이터가 하나로 합쳐져 수신될 수도 있고 하나로 보낸 데이터가 여러 번에 나뉘어 수신될 수도 있습니다

그러나, 수신한 데이터를 모두 이어 붙이면, 송신 측에서 보낸 데이터를 이어 붙인 결과와 정확히 동일합니다.

Stream 동작 예시
send("aaa");
send("bbb");
send("ccc");

수신 측에서는 다음과 같이 받을 수도 있습니다.

recv() -> "aa"
recv() -> "abbb"
recv() -> "cc"
recv() -> "c"

송신 측에서는 총 3번의 send 호출로 데이터를 보냈지만, 수신 측에서는 4번의 recv 호출을 해야 모든 데이터를 받을 수 있습니다. 또한 두 번째 recv에서는 aaa의 뒷부분과 bbb가 합쳐진 데이터가 수신되기도 합니다.

이러한 동작은 이상한 현상이 아니라 정상적인 동작이며, TCP 프로토콜이 가지는 Stream 기반 통신의 본질적인 특징입니다.

Q1. 왜 이런 처리가 필요한가요?

거의 모든 인터넷 프로그램은 이러한 특성을 반드시 고려해야 합니다. TCP는 메시지 단위(Message-based) 가 아닌, 바이트 스트림(Byte Stream) 기반 프로토콜이기 때문입니다.

5. TCP는 IP 위에서 동작합니다

TCP는 인터넷 프로토콜(IP) 위에서 동작합니다. OSI 7계층 모델 기준으로 보면, IP는 3계층(Network Layer)에 해당하고, TCP는 그 위의 4계층(Transport Layer)에 위치합니다.

IP는 기본적으로 다음과 같은 특징을 가집니다.

  • 비연결형(Connectionless) 프로토콜
  • 데이터 전달의 성공 여부를 보장하지 않음
  • 패킷의 순서, 중복, 유실을 관리하지 않음

즉, IP 자체는 “어디로 보낼지”만 책임질 뿐, “제대로 도착했는지”까지는 관여하지 않습니다. 이러한 한계를 보완하기 위해 4계층에서 TCP는 전송 프로토콜이 설계되었습니다.

UDP 역시 IP 위에서 동작하는 전송 계층 프로토콜입니다.

6. TCP는 전송되는 데이터의 순서를 보장합니다

각 세그먼트에는 시퀀스 번호가 부여되며, 수신 측은 이를 기준으로 데이터의 순서를 재조립합니다. 만약 네트워크 상황으로 인해 세그먼트가 뒤섞여 도착하더라도, 애플리케이션에는 올바른 순서로 전달됩니다.

또한 TCP는 중복 수신된 데이터도 자동으로 제거합니다. 동일한 세그먼트가 여러 번 도착하더라도, 수신 측은 이를 한 번만 처리합니다. 이 역시 개발자가 직접 처리하지 않아도 되는 부분입니다.

TCP/IP Programming

TCP의 개념을 이해했으니, 이제 실제 코드로 확인해 볼 차례입니다. 이번 글의 예제 코드는 개념을 최대한 직관적으로 전달하기 위해 C++이지만 C 스타일로 작성되었으며, 싱글 스레드 기준으로 동작합니다.

(추후 멀티 스레드 환경, IOCP 모델, C++ 스타일, Unity 연동 등등 점점 발전시켜 보겠습니다.)

위 사진이 TCP Server와 CLient에 대한 다이어그램입니다. 하나씩 설명해보며 최종적으로 코드까지 이해할 수 있도록 해보겟습니다. 먼저 아래는 함수에 대한 간략한 설명입니다.

  • bind() : 소켓의 지역IP주소와 지역 포트 번호를 결정합니다.
  • listen() : 소켓의 TCP 상태를 LISTENING으로 변경합니다.
  • accept() : Client 접속을 수용하고, 접속한 Client와 통신할 수 있는 새로운 소켓을 생성합니다. 이때 원격IP주소와 원격 포트 번호가 결정됩니다.
  • connect() : 서버와 논리적 연결을 설정합니다.
  • send(), recv() : 데이터 전송 함수로 클라이언트와 통신을 수행한 후 closesocket() 함수로 소켓을 닫습니다.

몇몇 함수들의 인자를 살펴보기 전에, 소켓 프로그래밍은 대부분 운영체제에서 제공하는 라이브러리 함수들을 사용한다는 점을 짚고 넘어가야 합니다. 따라서 각 함수가 어떤 인자를 요구하는지 이해하는 것이 중요하며, 그중에서도 소켓 주소 구조체(Socket Address Structure)는 여러 핵심 함수의 인자로 반복적으로 등장합니다.

그렇기에 혹시나 소켓 주소 구조체에 대해 잘 모른다면 제 글을 참고해주시면 됩니다.
https://velog.io/@whdrnr545/Network-Socket-Programming-IP-Port-Number-Socket-Address-Struct


#include<winsock2.h>
int bind
(
     SOCKET sock;
     const struct sockaddr *addr
     int addlen
)

SOCKET sock : 클라이언트 접속을 수용하는 소켓으로, 지역IP주소지역 포트 번호가 아직 결정되지 않은 상태입니다.
const struct sockaddr *addr : 소켓 주소 구조체로, 통신 프로토콜에 따른 주소 정보를 담고 있습니다. TCP/IP의 경우 sockaddr_in 또는 sockadd_in6 구조체를 사용하되, 지역IP 주소지역 포트 번호로 초기화하여 전달합니다.
int addlen : 소켓 주소 구초체의 길이(byte 단위)입니다.

.
.
.

#include<winsock2.h>
int connect
(
     SOCKET sock;
     const struct sockaddr *addr
     int addlen
)

SOCKET sock : server와 통신하는 소켓입니다.
const struct sockaddr *addr : 소켓 주소 구조체를 서버주소(TCP/IP의 경우 원격IP주소와 원격 포트 번호)로 초기화하여 전달합니다.
int addlen : 소켓 주소 구초체의 길이(byte 단위)입니다.

  • 지역IP주소 : 내 프로그램(내 컴퓨터, 내 프로세스)이 사용하는 IP주소
  • 지역 Port Number : 내 프로그램이 사용하는 '문 번호'
  • 원격IP주소 : 통신 상대방 IP주소
  • 원격 ProtNumber : 상대방 프로그램이 열어둔 포트 번호

bind(), connect() 두 함수 모두 두 번째에 소켓 주소 구조체를 갖고 있습니다. 그 외에 함수들은 내용이 길어지는 관계로 아래에 관련 링크를 걸어두겠습니다.

그러면, 다이어그램에 따른 몇몇 함수들도 봤으니 Server.cpp 전체코드를 보며 하나씩 설명해보겠습니다.

 Server.cpp

#include<winsock2.h>
#include<ws2tcpip.h>
#include<iostream>

#pragma comment(lib, "ws2_32.lib") // Link with Winsock library

#define SERVERPORT 9000
#define BUFSIZE 512

using std::cout;
using std::endl;

int main()
{
	int retval;

	//윈속 초기화
	WSADATA wsa;

	if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)
		return 1;

	//소켓 생성
	SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0); //IPv4  + TCP Socket을 생성해줘!
	if (listen_sock == INVALID_SOCKET)
	{
		//TODD : 에러 처리
	}

	//bind()
	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

	if (retval == SOCKET_ERROR)
	{
		//TODO : 에러 처리
	}

	//listen()
	retval = listen(listen_sock, SOMAXCONN);
	if(retval == SOCKET_ERROR)
	{
		//TODO : 에러 처리
	}

	//데이터 통신에 사용할 변수 
	SOCKET client_sock;
	struct sockaddr_in clientaddr;
	int addlen;
	char buf[BUFSIZE + 1];

	while(true)
	{
		//accept()
		addlen = sizeof(clientaddr);
		client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addlen);

		if(client_sock == INVALID_SOCKET)
		{
			//TODO : 에러 처리
			break;
		}

		//접속한 클라이언트 정보 출력
		char addr[INET_ADDRSTRLEN];
		inet_ntop(AF_INET, &clientaddr.sin_addr, addr, sizeof(addr));
		cout << "[TCP Server] Client connected : IP주소 = " << addr << ", 포트 번호 = " << ntohs(clientaddr.sin_port) << endl;

		//클라이언트와 데이터 통신
		while (true)
		{
			//데이터 수신
			retval = recv(client_sock, buf, BUFSIZE, 0);

			if(retval == SOCKET_ERROR)
			{
				//TODO : 에러 처리
				break;
			}
			else if(retval == 0) //접속 종료
			{
				cout << "[TCP Server] Client disconnected." << endl;
				break;
			}

			//받은 데이터 출력
			buf[retval] = '\0'; //문자열 종료 문자 추가
			cout << "[TCP/" << addr << ":" << ntohs(clientaddr.sin_port) << "] " << buf;

			//데이터 보내기
			retval = send(client_sock, buf, retval, 0);

			if (retval == SOCKET_ERROR)
			{
				//TODO : 에러 처리
				break;
			}
			
		}
		//소켓 닫기
		closesocket(client_sock);
		cout << "[TCP Server] Client disconnected : IP주소 = " << addr << ", 포트 번호 = " << ntohs(clientaddr.sin_port) << endl;
	}

	//소켓 닫기
	closesocket(listen_sock);

	//원속 종료
	WSACleanup();
	return 0;
}

우선, 전처리기와 원속 초기화 & 종료에 대해서는 이전 글에 설명했기에 socket() 함수에 대해 설명하겠습니다. socket() 함수는 사용할 프로토콜을 결정합니다.

socket(AF_INET, SOCK_STREAM, 0); //IPv4  + TCP Socket을 생성해줘!

세번 째 인자에 0을 넣을 시 앞의 두 인자에 맞는 기본 프로토콜을 OS가 알아서 선택해줍니다. 만일UDP라면, SOCK_STREAM이 아닌 SOCK_DGRAM입니다.

이때 listen_sock은 클라이언트와 실제로 데이터 통신을 하는 소켓이 아닌 "연결을 받아들이기만 하는 소켓"입니다.

Q1. 왜 포트 번호 9000을 사용하나요?

Port Number는 범위따라 나눠진다고 설명한 적이 있습니다. 포트 번호 9000은 Registered Port로 특정 서비스용으로 이용되지만 9000은 개발자 마음대로 써도 되는 안전한 포트이기에 관례적으로 자주 쓰입니다.

bind()

bind() 함수는 지역IP주소와 지역 포트 번호를 결정하기에 이에 맞는 인자 값을 변수로 선언합니다.

sockaddr_in 구조체는 내부에 포트 번호, IP주소, 주소 체계 등을 가지고 있기에 아래와 같이 선언합니다. 또한 Byte Order을 맞추기 위해 hton()함수를 사용하였습니다.

  • INADDR_ANY : 서버 소켓을 시스템의 모든 사용 가능한 네트워크 인터페이스(IP 주소)에 바인딩하도록 지시하는 값입니다.
  • sin_addr.s_addr : IPv4 주소를 담는 필드이며, IPv6에서는 sockaddr_in6sin6_addr를 사용한다
  • hton : Host Byte Order로 저장된 값을 입력받아 Network Byte Order로 변환한 값을 리턴합니다.
  • ntoh : Network Byte Order로 저장된 값을 입력받아 Host Byte Order로 변환한 값을 리턴합니다.
  • s: 16bit값을 입력으로 받습니다.
  • l : 32bit값을 입력으로 받습니다.
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET; 
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); 
serveraddr.sin_port = htons(SERVERPORT); 

retval = bind(listen_sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

listen()

listen() 함수는 소켓의 TCP 상태를 LISTENING으로 변경합니다. 이는 클라이언트 접속을 받아 들일 수 있는 상태가 되었다는 것을 의미합니다.

retval = listen(listen_sock, SOMAXCONN);

두번 째 인자는 Server가 당장 처리하지 않더라도 접속 가능한 Client의 개수입니다. 클라이언의 접속 정보는 연결 큐에 저장되는데 이 연결 큐의 길이를 나타냅니다.

  • SOMAXCONN : 하부 프로토콜에서 지원 가능한 최대값입니다.

"Server가 당장 처리하지 않는다"는 것은 곧이어 소개할 "accept() 함수를 호출하지 않는다"
는 것을 뜻합니다.

accept()

앞서 말했듯이 accept() 함수는 클라이언트 접속을 수용하고, 접속한 클라이언트와 통신할 수 있는 새로운 소켓을 생성하여 리턴하기에 이에 맞는 인자값을 변수로 선언합니다.

클라이언트가 접속했다는 것은 TCP 프로토콜 수준에서 연결 설정이 성공적으로 이루어졌다는 것을 의미합니다.

또한 Client가 한 개가 아닌 여러 개일 수 있기에 무한 루프를 돌아 클라이언트의 요청을 처리합니다.

SOCKET client_sock; //accept()의 반환값을 받는 변수
struct sockaddr_in clientaddr; //접속한 클라이언트 주소 정보를 저장
int addlen; //clientaddr 구조체 크기
char buf[BUFSIZE + 1];

(생략)
client_sock = accept(listen_sock, (struct sockaddr*)&clientaddr, &addlen);

만일 접속한 클라이언트가 없을 경우accept() 함수는 블로킹 상태로 대기하며 CPU를 거의 사용하지 않습니다. 클라이언트가 접속하면 서버 스레드는 깨어나고 accept() 함수가 반환됩니다.


Server.cpp를 보았으니 Client.cpp 전체 코드를 보겠습니다.

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

#pragma comment(lib, "ws2_32.lib") // Link with Winsock library

char* SERVERIO = (char*)"127.0.0.1";
#define SERVERPORT 9000
#define BUFSIZE 512

using std::cout;
using std::endl;

int main()
{
	int retval;
	
	WSADATA wsa;
	if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
		return 1;

	SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock == INVALID_SOCKET)
	{
		//TODD : 애러처리
	}

	struct sockaddr_in serveraddr;
	memset(&serveraddr, 0, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	inet_pton(AF_INET, SERVERIO, &serveraddr.sin_addr);
	serveraddr.sin_port = htons(SERVERPORT);
	retval = connect(sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

	if (retval == SOCKET_ERROR)
	{
		//TODD : 애러처리
	}

	//데이터 통신에 사용할 변수
	char buf[BUFSIZE + 1];
	int len;

	while (true)
	{
		//데이터 입력
		cout << "[보낼 데이터] ";
		if (fgets(buf, BUFSIZE + 1, stdin) == NULL)
			break;

		// '\n' 문자 제거
		len = (int)strlen(buf);
		if (buf[len - 1] == '\n')
			buf[len - 1] = '\0';

		if (strlen(buf) == 0)
			break;

		//데이터 보내기
		retval = send(sock, buf, (int)strlen(buf), 0);
		if (retval == SOCKET_ERROR)
		{
			//TODO : 에러 처리
		}

		cout << "[TCP Client] " << retval << " bytes sent." << endl;

		//데이터 받기
		retval = recv(sock, buf, BUFSIZE, 0);
		if (retval == SOCKET_ERROR)
		{
			//TODO : 에러 처리
		}
		else if (retval == 0)
		{
			cout << "[TCP Client] Server disconnected." << endl;
			break;
		}

		//받은 데이터 출력
		buf[retval] = '\0';
		cout << "[TCP Client] " << retval << " bytes received. Data = " << buf << endl << endl;
	}

	//소켓 닫기
	closesocket(sock);

	//윈속 종료
	WSACleanup();

	return 0;
}
Q1. 왜 IP주소가 "127.0.0.1"인가요?

먼저 127.0.0.0~8 대역은 전부 Loopback 네트워크입니다. 그 중에서도 가장 대표적인 주소가 127.0.0.1입니다. Loopback 주소란? 자기 자신을 가리키기 위한 목적으로 쓰기 위해 예약된 IP주소입니다.

즉 외부 네트워크를 거치지 않고 같은 머신에서 서버와 클라이언트를 테스트하기 위한 Loopback 주소입니다.

Loopback IP(나무위키)
IPv4 및 IPv6에서, 자기 자신을 가리키기 위한 목적으로 쓰기 위해 예약된 IP 주소이다. 말 그대로 현재 조작 중인 컴퓨터를 네트워크 연결 대상으로 인지하기 위해 설정된 IP 주소 구간으로, 이더넷 장치가 물리적으로 연결되지 않더라도 현재 사용 중인 컴퓨터에서 연결을 시도하는 대상은 그 컴퓨터 자체가 된다.

connect()

connect() 함수는 TCP 프로토콜 수준에서 서버와 논리적 연결을 설정하기에 이에 맞는 인자값을 변수로 선언합니다.

일반적으로 클라이언트는 서버와 달리 bind() 함수를 호출하지 않습니다. bind() 함수를 호출하지 않은 상태에서 connect() 함수를 호출하면, OS가 자동으로 지역 IP 주소와 지역 포트 번호를 할당해줍니다.

자동으로 포트 번호를 할당하는 정책은 OS에 따라 달라집니다.

  • inet_pton : 문자열 형태의 IPv4/Ipv6 주소를 입력받아 32/128bit 숫자(Network Byte Order)형태로 리턴합니다.
  • inet_ntop : 32/128bit 숫자(Network Byte Order)형태의 IPv4/IPv6 주소를 입력받아 문자열 형태로 리턴합니다.
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, SERVERIO, &serveraddr.sin_addr);
serveraddr.sin_port = htons(SERVERPORT);
retval = connect(sock, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

send() & recv()

여러 소켓 함수 중에서도 송수신 함수는 핵심이므로, 이번에는 이를 중심으로 자세히 살펴보겠습니다. OS는 소켓 하나당 내부적으로 소켓 객체를 관리하며, 이 객체에는 로컬/원격 주소 정보, 상태 정보와 함께 송신 버퍼수신 버퍼가 포함됩니다.

송신 버퍼(Send Buffer)는 응용 프로그램이 send()로 전달한 데이터를 실제 네트워크로 전송하기 전까지 임시로 저장하는 영역입니다.

수신 버퍼(Receive Buffer)는 네트워크로부터 수신한 데이터를 응용 프로그램이 recv()로 읽어가기 전까지 저장하는 영역입니다

#include<winsock2.h>

int send
(
	SOCKET sock,
    const char *buf,
    int len,
    int flags
) 

sock : 통신할 대상(서버)과 연결된 소켓입니다.
buf : 보낼 데이터를 담고 있는 응용 프로그램 버퍼의 주소입니다.
len : 보낼 데이터의 크기(Byte 단위)입니다.
flags : send() 함수의 동작을 바꾸는 옵션으로, 거의 항상 0을 사용합니다.

send() 함수는 응용 프로그램의 데이터 전송을 위해 OS의 송신 버퍼에 데이터를 복사하고, 곧바로 리턴합니다. send() 함수가 리턴했다고 실제 데이터가 전송된 것은 아니며, 일정 시간이 흘러서 하부 프로토콜(Ex : TCP/IP)을 통해 전송이 완료됩니다.

#include<winsock2.h>

int recv
(
	SOCKET sock,
    char *buf,
    int len,
    int flags
) 

sock : 통신할 대상(서버)과 연결된 소켓입니다.
buf : 받은 데이터를 저장할 응용 프로그램 버퍼의 주소입니다.
len : OS의 수신 버퍼로부터 복사할 최대 데이터 크기(Byte)입니다. 이 값은 buf가 가리키는 응용 프로그램 버퍼보다 크지 않아야 합니다.
flags : recv() 함수의 동작을 바꾸는 옵션입니다. 유용한 것은 MSG_PEEKMSG_WAITALL이 있습니다.

recv() 함수는 OS의 수신 버퍼에 도착한 데이터를 응용 프로그램 버퍼에 복사합니다. recv() 함수 사용 시 특히 주의할 점은 세 번째 인수인 len으로 저장한 것보다 적은 데이터가 응용 프로그램 버퍼에 복사될 수 있다는 사실입니다. 이는 TCP가 데이터 경계를 구분하지 않는다는 특성에 기인합니다.

TCP/IP Programming 실행 & 결과

실행시 문제없이 잘 작동합니다.

TCPServer.cpp

TCPClient.cpp

이때 CMD로 진입하여 netstat -a -n -p tcp | findstr 9000 명령을 실행하면 아래와 같이 출력됩니다.

  • netstat 명령은 네트워크 연결과 통계 정보를 표시하는 유틸리티로, Window에서 기본으로 제공합니다.
  • '-a' 옵션은 모든 연결과 연결 대기 포트를 표시합니다.
  • '-n' 옵션은 주소와 포트 번호를 숫자 형식으로 표시합니다.
  • '-p tcp' 옵션을 사용하면 TCP 상태만 골라서 볼 수 있습니다. findstr명령은 특정 패턴을 찾을 때 사용하는데, 여기서는 포트 번호 9000을 포함하는 줄만 골라내는 기능을 합니다.
  • '|('파이프'라 부름)은 두 명령을 연결하여 앞쪽 명령의 출력을 다음 명령의 입력으로 넘기는 역할을 합니다.

위 사진을 보면 첫번 째 행은 LISTENING(연결 대기 중)상태이고, 나머지 두 행은 ESTABLISHED(연결됨)상태입니다. 또한 Registered Port인 9000과 달리 Dynamic Port인 57112는 Client의 Port Number입니다.

이후 TCPClient.cpp에서 아무것도 입력하지 않은 상태에서 Enter를 누르면 TCPServer와 연결을 끊기면서 Client가 종료됩니다.

이때 마찬가지로 이때 CMD로 진입하여 netstat -a -n -p tcp | findstr 9000 명령을 실행하면 아래와 같이 출력됩니다.

이번에는 첫번 째 행은 LISTENING(연결 대기 중)상태이고, 나머지 행은 TIME_WAIT상태입니다.

Q1, 저는 클라이언트 포트 번호 57112로 설정한 적이 없는데요?

맞습니다. 클라이언트 프로그램에서 bind()를 통해 포트 번호를 명시적으로 지정하지 않으면, 운영체제는 TCP 연결을 생성하는 시점에 사용 가능한 임시 포트(Ephemeral Port)를 자동으로 할당합니다.

이 포트는 Dynamic Port Range에서 선택되며, Windows 환경에서는 일반적으로 49152 ~ 65535 범위의 포트가 사용됩니다. 위 예시에서 보이는 57112 포트 역시 이 범위에 포함된 클라이언트 측 임시 포트 번호입니다.

따라서 하나의 서버 포트(9000)에 대해 여러 클라이언트가 동시에 접속하더라도, 각 클라이언트는 서로 다른 포트 번호를 사용함으로써 Kernel은 각 TCP 연결을 명확히 구분할 수 있습니다.

Q2, LISTENING, ESTABLISHED, TIME_WAIT 상태는 무엇인가요?

먼저 클라이언트 접속을 수용하는 서버 측의 TCP Socket은 LISTENING 상태에 계속 머물러 있습니다.

이후 클라이언트가 접속하면 서버 측과 클라이언트 측의 ESTABLISHED 상태의 TCP 소켓이 각각 존재합니다. 서버 측의 ESTABLISHED 상태 소켓은 새로운 클라이언트가 접속할 때마다 새로 만들어집니다. 반면, 클라이언트 측의 ESTABLISHED 상태 소켓은 클라이언트당 한 개만 존재합니다.

마지막 클라리언트가 접속을 끊으면 서버 측의 ESTABLISHED 상태 소켓은 사라지고, 클라이언트 측의 ESTABLISHED 상태 소켓은 TIME_WAIT 상태가 됩니다.

TIME_WAIT 상태의 TCP Socket은 일정 시간이 지나면 사라집니다. TIME_WAIT 상태는 같은 포트 번호를 가진 소켓 생성을 일정 시간 금지하여, 이전 TCP 연결에서 발생했다가 뒤늦게 도착한 데이터를 잘못 수신하는 상황을 막아줍니다.

Conclusion

이번 글에서는 TCP/IP의 개념과 최소 구현에 대해 알아보았습니다.

다음 글에서는 TCP가 아닌 UDP로도 구현해보겠습니다.

참고문헌

[위키백과 TCP]
https://ko.wikipedia.org/wiki/%EC%A0%84%EC%86%A1_%EC%A0%9C%EC%96%B4_%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C

[토스페이먼츠 개발자센터]
https://docs.tosspayments.com/resources/glossary/tcp

[교보문구]
https://product.kyobobook.co.kr/detail/S000061352062
https://product.kyobobook.co.kr/detail/S000001792817

profile
CK23 Game programmer

0개의 댓글