프로그래밍/소켓 프로그래밍 입문

TCP 서버의 소켓 처리와 데이터 송수신

데일리 백수 2025. 1. 8. 23:16

TCP 서버는 클라이언트와의 연결을 처리하고, 데이터를 송수신하는 기본 동작을 수행합니다. 이 글에서는 서버가 클라이언트 요청을 처리하는 과정을 단계별로 설명하고, 주요 개념을 살펴보겠습니다.

 

#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")

int _tmain(int argc, _TCHAR* argv[])
{
	WSADATA wsa = { 0 };
	if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
	{
		puts("ERROR: 윈속을 초기화 할 수 없습니다.");
		return 0;
	}

	SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
	{
		puts("ERROR: 접속 대기 소켓을 생성할 수 없습니다.");
		return 0;
	}

	SOCKADDR_IN	svraddr = { 0 };
	svraddr.sin_family = AF_INET;
	svraddr.sin_port = htons(25000);
	svraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
	if (::bind(hSocket, (SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
	{
		puts("ERROR: 소켓에 IP주소와 포트를 바인드 할 수 없습니다.");
		return 0;
	}

	if (::listen(hSocket, SOMAXCONN) == SOCKET_ERROR)
	{
		puts("ERROR: 리슨 상태로 전환할 수 없습니다.");
		return 0;
	}

	SOCKADDR_IN clientaddr = { 0 };
	int nAddrLen = sizeof(clientaddr);
	SOCKET hClient = 0;
	char szBuffer[128] = { 0 };
	int nReceive = 0;

	while ((hClient = ::accept(hSocket,
		(SOCKADDR*)&clientaddr,
		&nAddrLen)) != INVALID_SOCKET)
	{
		puts("새 클라이언트가 연결되었습니다."); fflush(stdout);

		while ((nReceive = ::recv(hClient, szBuffer, sizeof(szBuffer), 0)) > 0)
		{

			::send(hClient, szBuffer, sizeof(szBuffer), 0);
			puts(szBuffer); fflush(stdout);
			memset(szBuffer, 0, sizeof(szBuffer));
		}

	
		::shutdown(hClient, SD_BOTH);
		::closesocket(hClient);
		puts("클라이언트 연결이 끊겼습니다."); fflush(stdout);
	}


	::closesocket(hSocket);


	::WSACleanup();
	return 0;
}

 

1. 서버의 기본 흐름

(1) 서버 소켓 준비

서버는 먼저 리슨 소켓을 생성하고, 이를 통해 클라이언트 연결 요청을 대기합니다.
리슨 소켓은 다음의 과정을 거칩니다:

  1. 소켓 생성
  2. 바인딩 (IP 주소와 포트 번호 연결)
  3. 리슨 상태 전환
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
::bind(hSocket, (SOCKADDR*)&svraddr, sizeof(svraddr));
::listen(hSocket, SOMAXCONN);

리슨 소켓이 준비되면 서버는 클라이언트의 연결 요청을 수락할 준비를 마칩니다.

 

(2) 클라이언트 연결 요청 수락

클라이언트가 서버에 연결 요청을 보낼 때, 서버는 **accept()**를 호출하여 요청을 수락하고 통신 소켓을 반환합니다.

SOCKET hClient = ::accept(hSocket, (SOCKADDR*)&clientaddr, &nAddrLen);
  • 리턴값: accept()는 클라이언트와의 통신에 사용할 새 소켓을 반환.
  • 매개변수:
    • hSocket: 리슨 소켓.
    • clientaddr: 연결 요청을 보낸 클라이언트의 IP 주소와 포트 번호가 저장.
    • nAddrLen: clientaddr 구조체의 크기.

통신 소켓

  • 서버에서 반환된 소켓(hClient)은 해당 클라이언트와의 데이터 송수신에 사용됩니다.
  • 서버 입장에서 클라이언트는 원격(remote) 엔드포인트로, clientaddr에 저장된 IP 주소와 포트 번호를 통해 식별됩니다.

2. 데이터 송수신

(1) 데이터 수신: recv()

클라이언트로부터 데이터를 수신하려면 recv()를 사용합니다.

char szBuffer[128] = { 0 };
int nReceive = ::recv(hClient, szBuffer, sizeof(szBuffer), 0);

 

  • hClient: 통신 소켓.
  • szBuffer: 데이터를 저장할 메모리 버퍼.
  • 리턴값: 실제 수신한 데이터의 바이트 수.
    • 0: 클라이언트가 연결을 종료한 경우.
    • >0: 수신된 데이터의 바이트 수.
    • <0: 오류 발생.

(2) 데이터 송신: send()

수신한 데이터를 클라이언트로 다시 전송하려면 **send()**를 사용합니다.

::send(hClient, szBuffer, nReceive, 0);

 

 

  • szBuffer: 송신할 데이터.
  • nReceive: 실제 수신한 데이터 크기만큼 전송.

주의점

  • 버퍼 크기(szBuffer)와 실제 데이터 크기(nReceive)를 구분해야 합니다.
    • 예: 버퍼 크기가 128바이트이더라도 실제 수신 데이터가 5바이트라면 5바이트만 전송해야 합니다.

(3) 데이터 송수신 루프

서버는 클라이언트가 연결을 유지하는 동안 수신-송신 루프를 유지합니다.

while ((nReceive = ::recv(hClient, szBuffer, sizeof(szBuffer), 0)) > 0)
{
    ::send(hClient, szBuffer, nReceive, 0);
    memset(szBuffer, 0, sizeof(szBuffer));
}

 

recv()가 0을 반환하면 루프가 종료됩니다. 이는 클라이언트가 연결을 종료한 상태를 의미합니다.

 

3. 연결 종료

(1) 클라이언트 요청 종료

클라이언트가 연결 종료를 요청하면 서버는 이를 감지하고 통신 소켓을 닫습니다.

::shutdown(hClient, SD_BOTH);
::closesocket(hClient);
  • shutdown(): 송수신 채널을 닫습니다.
  • closesocket(): 소켓 자원을 해제합니다.

(2) 서버 소켓 종료

리슨 소켓을 닫으려면 서버 애플리케이션이 종료될 때 closesocket()을 호출합니다.

::closesocket(hSocket);