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) 서버 소켓 준비
서버는 먼저 리슨 소켓을 생성하고, 이를 통해 클라이언트 연결 요청을 대기합니다.
리슨 소켓은 다음의 과정을 거칩니다:
- 소켓 생성
- 바인딩 (IP 주소와 포트 번호 연결)
- 리슨 상태 전환
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);
'프로그래밍 > 소켓 프로그래밍 입문' 카테고리의 다른 글
에코 클라이언트 서버 통신 확인 테스트 (0) | 2025.01.11 |
---|---|
TCP 3-Way Handshake와 4-Way Handshake (0) | 2025.01.11 |
IP 주소와 포트 번호, 소켓의 관계+ 바인딩이란? (0) | 2025.01.08 |
TCP 서버 소켓 프로그래밍에서 SOCKET 설정 (0) | 2025.01.08 |
TCP 에코 서비스: 흐름과 동작 원리 (0) | 2025.01.08 |