Network / / 2024. 9. 26. 16:13

TCP 소켓 프로그래밍

Socket

네트워크 소켓(network socket)은 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점입니다. 오늘날 컴퓨터 간 통신의 대부분은 인터넷 프로토콜을 기반으로 하고 있으므로, 대부분의 네트워크 소켓은 인터넷 소켓 입니다. 네트워크 통신을 위한 프로그램들은 소켓을 생성하고, 이 소켓을 통해서 서로 데이터를 교환합니다. 소켓은 RFC 147에 기술사항이 정의되어 있습니다.

 

RFC 147 텍스트는 1971년 5월 7일에 작성된 네트워크 소켓에 대한 초기 정의를 설명하는 문서입니다. J. M. Winett가 매사추세츠 공과대학교(MIT) 링컨 연구소에서 네트워크 소켓 위원회와 네트워크 커뮤니티에 보낸 것입니다. 주요 내용은 다음과 같습니다: 

1. 소켓의 정의

  • 소켓은 네트워크에서 정보가 전송되는 고유한 식별자로 정의됩니다.
  • 32비트 숫자로 지정되며, 짝수 소켓은 수신 소켓, 홀수 소켓은 송신 소켓으로 구분됩니다.
  • 소켓은 송수신 프로세스가 위치한 호스트에 의해 식별됩니다.

2. 포트와의 관계

  • 이전 문서에서는 호스트 운영 체제 하에서 실행되는 프로세스가 여러 포트에 접근할 것이라고 가정했습니다.
  • 포트는 물리적 또는 논리적 I/O 장치일 수 있으며, 시스템 호출을 통해 운영 체제에 의해 지원됩니다.

3. 소켓의 사용

  • 소켓은 ARPA 네트워크를 통한 기계 간 통신을 위한 포트의 식별자로 정의됩니다.
  • 각 호스트에 할당된 소켓은 알려진 프로세스와 고유하게 연결되어야 하며, 일부 소켓의 이름은 전 세계적으로 알려져 있어야 합니다.

4. 소켓 명명 및 사용자 투명성

  • 네트워크 프로그램 사용자는 소켓 이름을 알 필요가 없을 수 있으며, 소켓 명세는 사용자에게 투명할 수 있습니다.
  • 프로세스가 동일한 목적으로 나중에 사용하는 소켓은 이전에 사용했던 것과 동일해야 할 수 있습니다.

5. 소켓과 네트워크 사용 계정

  • 네트워크 제어 프로그램(NCP)은 각 연결을 기록하고, 연결된 시간, 전송된 메시지 및 비트 수, 송수신 호스트, 그리고 연결에 참여한 송수신 호스트의 소켓을 기록해야 합니다.

6. 32비트 소켓의 구조

  • 32비트 소켓은 8비트 "홈" 필드, 16비트 "사용자" 필드, 8비트 "태그" 필드로 나뉩니다.
  • 태그는 7비트 "플러그"와 1비트 "편향"으로 구성되며, "0" 편향은 수신 소켓, "1" 편향은 송신 소켓을 나타냅니다.

이 문서는 네트워크 소켓의 초기 정의와 구조, 그리고 네트워크 통신에서의 사용 방법에 대한 중요한 정보를 제공합니다. 이는 오늘날의 네트워크 프로토콜과 시스템의 기반이 되는 중요한 개념 중 하나입니다.

 

인터넷 소켓은 크게 두 개의 타입으로 분류할 수 있다.

  • UDP 프로토콜을 사용하는 경우
  • TCP 프로토콜을 사용하는 경우

 

TCP Server Socket Programming

다음은 윈도우즈 OS에서 TCP 기반 소켓 프로그래밍-서버측 코드입니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

#define MAX_BUFFER_SIZE 1024

int main() {    
    WSADATA wsaData;
    SOCKET serverSocket, clientSocket;
    struct sockaddr_in serverAddr, clientAddr;
    char buffer[MAX_BUFFER_SIZE];
    int clientAddrSize;

    // Winsock 초기화
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("Failed to initialize winsock.\n");
        return -1;
    }

    // 소켓 생성
    if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Failed to create socket.\n");
        return -1;
    }

    // 서버 설정
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(5001);

    // 소켓 바인딩
    if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
        printf("Failed to bind socket.\n");
        return -1;
    }

    // 클라이언트의 연결 대기
    if (listen(serverSocket, 1) == SOCKET_ERROR) {
        printf("Failed to listen on socket.\n");
        return -1;
    }

    printf("서버가 실행 중입니다...\n");

    // 클라이언트 연결 수락
    clientAddrSize = sizeof(clientAddr);
    if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize)) == INVALID_SOCKET) {
        printf("Failed to accept client connection.\n");
        return -1;
    }

    printf("클라이언트가 연결되었습니다.\n");

    while (1) {
        // 클라이언트로부터 메시지 수신
        memset(buffer, 0, MAX_BUFFER_SIZE);
        int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
        if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
            break;
        }

        printf("수신한 메시지: %s\n", buffer);

        // 클라이언트에게 응답 전송
        char response[MAX_BUFFER_SIZE];
        snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer);
        send(clientSocket, response, strlen(response), 0);
    }

    // 연결 종료
    closesocket(clientSocket);
    closesocket(serverSocket);
    WSACleanup();

    return 0;
}

 

WSAData

WSADATA wsaData;

WSADATA는 Windows Sockets API에서 사용하는 구조체입니다.  
이 API는 네트워크 통신을 위해 윈도우 운영 체제에서 제공하는 표준 인터페이스입니다.  
WSADATA 구조체는 WSAStartup 함수를 호출할 때 Windows Sockets 구현에 대한 정보를 반환하는 데 사용됩니다.  
이 구조체는 윈도우 소켓 구현의 버전과 다양한 기능적 특성을 설명합니다.

 

 

Winsock 초기화

다음 코드는 Windows Sockets API를 초기화하는 과정을 보여줍니다. 

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
    printf("Failed to initialize winsock.\n");
    return -1;
}


여기에서 WSAStartup 함수는 Windows Sockets 네트워크 통신을 사용하기 위한 필수적인 초기화 단계입니다.  
이 함수는 프로그램이 Windows Sockets API를 사용하기 전에 반드시 호출해야 합니다.  
1. WSAStartup 함수: 이 함수는 Windows Sockets 라이브러리를 초기화하고 사용할 준비를 합니다. 
    함수의 첫 번째 파라미터는 프로그램에서 요구하는 Windows Sockets 버전을 지정합니다. 
    두 번째 파라미터는 WSADATA 구조체에 대한 포인터입니다. 
   이 구조체는 초기화 후 시스템의 Windows Sockets 구현에 대한 정보를 담게 됩니다. 
2. MAKEWORD(2, 2) : 이 매크로는 원하는 Windows Sockets 버전을 지정합니다. 
    여기서 2, 2는 버전 2.2를 의미합니다. 
    이는 널리 사용되는 버전으로, 많은 현재 시스템에서 지원됩니다. 
3. &wsaData : WSADATA 구조체에 대한 포인터입니다.  
    WSAStartup 함수는 이 구조체를 통해 시스템에서 사용 가능한 소켓의 상세 정보를 제공합니다. 

WSAStartup 함수는 성공적으로 완료되면 0을 반환합니다. 
반환 값이 0이 아닌 경우, 즉 ! = 0일 때, 이는 초기화가 실패했음을 의미합니다. 
이 조건문은 WSAStartup의 반환 값을 검사하여 초기화가 실패했을 때 오류 메시지를 출력하고, 프로그램을 종료하는 로직을 구현합니다. 
이러한 초기화 단계는 Windows 환경에서 네트워크 프로그래밍을 할 때 필수적입니다. 
초기화가 성공적으로 완료되면 소켓을 생성하고, 데이터를 송수신하는 등의 네트워크 관련 작업을 진행할 수 있습니다.

 

SOCKET 데이터 타입

SOCKET이라는 용어는 윈도우 네트워크 프로그래밍, 특히 Windows Sockets API (Winsock)에서 사용되는 개념입니다. 그러나 SOCKET은 실제로 구조체(structure)가 아니라, 소켓 핸들을 나타내는 데이터 타입입니다. 이는 네트워크 소켓을 참조하기 위한 정수형 값(핸들)으로, 네트워크 연결을 관리하는 데 사용됩니다. 

SOCKET 데이터 타입에 대한 상세한 설명은 다음과 같습니다: 

1. 정의: SOCKET은 윈도우 플랫폼에서 정의된 특별한 데이터 타입입니다. 이는 기본적으로 unsigned int 형이나 그와 유사한 다른 정수형 타입으로 정의됩니다. 특정한 구조체를 나타내는 것이 아니라, 소켓의 핸들(식별자)을 위한 타입입니다.

typedef unsigned __int64 UINT_PTR

typedef UINT_PTR        SOCKET;


2. 용도: SOCKET 타입은 네트워크 프로그래밍에서 소켓의 인스턴스를 식별하기 위해 사용됩니다. 이 핸들은 소켓을 생성, 관리, 송수신 및 닫는 데 필요한 연산에 사용됩니다. 

3. 값: 유효한 SOCKET 값은 일반적으로 0 이상의 값을 갖습니다. INVALID_SOCKET은 오류 상황을 나타내는 특별한 값으로, 보통 -1 또는 다른 특정한 음수 값을 가집니다. 

4. 생성과 소멸: SOCKET은 socket 함수를 사용하여 생성되며, closesocket 함수를 사용하여 닫힙니다. 소켓이 더 이상 필요하지 않을 때는 반드시 닫아야 합니다. 

5. 사용 예시: SOCKET 타입은 TCP 또는 UDP 소켓 생성, 클라이언트의 연결 요청 수락, 데이터 송수신 등 다양한 네트워크 연산에서 사용됩니다. 

6. 플랫폼 의존성: SOCKET 타입은 주로 윈도우 플랫폼의 Winsock 라이브러리에 특화되어 있으며, UNIX나 Linux 같은 다른 시스템에서는 다른 방식(예: 파일 디스크립터)을 사용할 수 있습니다. 

요약하자면, SOCKET은 구조체가 아니라 네트워크 소켓의 인스턴스를 나타내는 핸들로 사용되는 데이터 타입입니다. 이는 윈도우 기반의 네트워크 프로그래밍에서 중요한 역할을 하며, 소켓 관련 연산을 수행하는 데 필수적입니다.

 

Socket 생성

다음 코드 조각은 소켓을 생성하는 과정을 보여줍니다. 여기서 socket 함수는 네트워크 통신을 위한 소켓을 생성합니다. 이 함수는 성공적으로 소켓을 생성하면 소켓에 대한 핸들(또는 소켓 식별자)을 반환하고, 실패하면 INVALID_SOCKET을 반환합니다.

if ((serverSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
    printf("Failed to create socket.\n");
    return -1;
}


위 코드에 대한 설명은 다음과 같습니다: 

1. socket 함수: 이 함수는 네트워크 통신을 위한 새로운 소켓을 생성합니다. 함수의 파라미는 다음과 같습니다:

  • AF_INET(AF[Address Family]): 이 파라미터는 소켓이 IPv4 인터넷 프로토콜을 사용할 것임을 나타냅니다.
  • SOCK_STREAM: 이 파라미터는 스트림 소켓을 생성할 것임을 지정합니다. 스트림 소켓은 연결 지향적이며, 주로 TCP/IP 프로토콜을 사용합니다.
  • 0: 이 파라미터는 프로토콜을 지정합니다. 대부분의 경우, 특정 소켓 타입에 대해 단일 프로토콜만 사용할 수 있으므로, 이 값을 0으로 설정하면 기본 프로토콜을 사용합니다. SOCK_STREAM의 경우, 이는 TCP를 의미합니다.

2. 변수 할당과 비교: serverSocket = socket(AF_INET, SOCK_STREAM, 0)는 socket 함수를 호출하여 반환된 소켓 핸들을 serverSocket 변수에 할당합니다. 이후, == INVALID_SOCKET을 사용하여 생성된 소켓이 유효한지 확인합니다. INVALID_SOCKET은 소켓 생성이 실패했음을 나타내는 특별한 값입니다.


3. 오류 처리: 만약 socket 함수가 INVALID_SOCKET을 반환하면, 이는 소켓 생성에 실패했음을 의미합니다. 이 경우, Failed to create socket.\n이라는 오류 메시지를 출력하고, 함수는 -1을 반환하여 오류 상태를 나타냅니다. 

이 코드는 네트워크 프로그래밍의 초기 단계에서 매우 중요합니다. 소켓 생성은 클라이언트와 서버 간의 통신을 위한 기본적인 조건이며, 이후의 모든 네트워크 작업(예: 연결 수립, 데이터 송수신 등)의 기반이 됩니다. 소켓 생성에 실패하면 이러한 네트워크 작업을 진행할 수 없기 때문에, 적절한 오류 처리가 필수적입니다.

 

struct sockaddr_in 데이터 타입

sockaddr_in은 C 언어에서 인터넷 소켓 주소를 정의하기 위해 사용되는 구조체입니다. 이 구조체는 인터넷(IPv4) 주소를 나타내며, 네트워크 프로그래밍에서 주로 사용됩니다. sockaddr_in은 sockaddr 구조체의 특화된 버전으로, 인터넷 주소를 다루기 위해 설계되었습니다. 

sockaddr_in 구조체의 구성은 다음과 같습니다: 
1. sin_family: 이 필드는 주소 체계(Address Family)를 지정합니다. 인터넷 주소의 경우, 이 값은 AF_INET으로 설정됩니다. AF_INET은 IPv4 네트워크 주소 체계를 나타냅니다. 

2. sin_port: 이 필드는 포트 번호를 저장합니다. 네트워크 바이트 순서(big-endian)로 표현되어야 합니다. htons 함수를 사용하여 호스트 바이트 순서에서 네트워크 바이트 순서로 변환하는 것이 일반적입니다. 이 포트 번호는 소켓이 통신할 때 사용하는 포트를 지정합니다.

htons 함수는 "Host to Network Short"의 약어입니다. 이는 호스트 바이트 순서(Host Byte Order)를 네트워크 바이트 순서(Network Byte Order)로 변환하는 함수입니다. 네트워크 통신에서 사용되는 데이터는 네트워크 바이트 순서(빅엔디언, Big Endian)로 전송되는데, 대부분의 컴퓨터는 호스트 바이트 순서(리틀엔디언, Little Endian)를 사용합니다. htons는 16비트(2바이트) 정수 값, 즉 short 값을 호스트 바이트 순서에서 네트워크 바이트 순서로 변환할 때 사용됩니다. 예를 들어, 포트 번호를 네트워크 바이트 순서로 변환할 때 htons를 사용할 수 있습니다.


3. sin_addr: 이 구조체는 실제 인터넷 주소를 저장합니다. sin_addr는 struct in_addr 타입으로, IPv4 주소를 나타냅니다. 이 필드는 주로 inet_addr 함수나 inet_pton 함수를 사용하여 설정됩니다. 

inet_addr 함수는 문자열로 표현된 IPv4 주소를 32비트의 네트워크 바이트 순서로 변환하는 함수입니다. 즉, 사람이 읽을 수 있는 점으로 구분된 4개의 십진수 형태(예: "192.168.1.1")의 IP 주소를 소켓 프로그래밍에서 사용하는 이진수 형태로 변환해줍니다.
inet_pton 함수는 "Presentation to Numeric"의 약어로, 사람이 읽을 수 있는 텍스트 형태의 IP 주소(프레젠테이션 형태)를 네트워크가 사용하는 이진수(숫자) 형태로 변환하는 함수입니다. 이 함수는 IPv4 뿐만 아니라 IPv6 주소 변환도 지원하므로 inet_addr 함수보다 더 범용적으로 사용할 수 있습니다. 


4. sin_zero: 이 필드는 sockaddr_in 구조체의 크기를 struct sockaddr와 동일하게 맞추기 위해 사용됩니다. 일반적으로 이 필드는 0으로 설정되어야 하며, 구조체의 크기를 16바이트로 맞추는 데 사용됩니다. 

struct sockaddr_in 구조체는 주로 소켓을 생성하고, 바인딩하며, 연결을 수립하는 데 사용됩니다. 예를 들어, 서버는 이 구조체를 사용하여 어떤 IP 주소와 포트 번호에서 수신 대기할지를 결정하고, 클라이언트는 서버에 연결할 때 서버의 IP 주소와 포트 번호를 지정하는 데 사용합니다.

sockaddr_in 구조체는 소켓 프로그래밍에서 IPv4 주소 정보를 저장하는 데 사용되는 구조체입니다. 소켓을 통해 데이터를 송수신할 때, IP 주소와 포트 번호를 저장하여 네트워크 연결을 설정하는 데 사용됩니다.

sockaddr_in 구조체의 정의

다음은 일반적인 sockaddr_in 구조체의 정의입니다:

struct sockaddr_in {
    sa_family_t    sin_family;   // 주소 체계(AF_INET)
    in_port_t      sin_port;     // 16비트 포트 번호 (네트워크 바이트 순서)
    struct in_addr sin_addr;     // 32비트 IPv4 주소
    char           sin_zero[8];  // 구조체 크기를 맞추기 위한 패딩 (사용되지 않음)
};​

주요 필드 설명
 sin_family: 주소 체계를 지정합니다. IPv4를 사용할 때는 항상 AF_INET으로 설정됩니다.
 sin_port: 16비트의 포트 번호로, 네트워크 바이트 순서(빅엔디언)로 저장됩니다. 보통 htons 함수를 사용하여 호스트 바이트 순서를 네트워크 바이트 순서로 변환합니다.
 sin_addr: 32비트의 IPv4 주소로, struct in_addr 구조체를 사용하여 저장됩니다. IP 주소는 문자열에서 숫자로 변환하기 위해 inet_pton이나 inet_addr 같은 함수를 사용합니다.
 sin_zero: 구조체 크기를 맞추기 위한 패딩 필드로, 특별한 용도로 사용되지 않으며 항상 0으로 채워집니다.


간단한 예시는 다음과 같습니다: 

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(80); // HTTP 포트 (80)
addr.sin_addr.s_addr = inet_addr("192.168.0.1");
memset(&(addr.sin_zero), 0, 8); // sin_zero 필드를 0으로 초기화

 

이 예시에서는 sockaddr_in 구조체를 사용하여 IP 주소 192.168.0.1 및 포트 번호 80으로 통신을 설정하고 있습니다.

 

Server 설정 

serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
serverAddr.sin_port = htons(5001);

 

위 코드는 네트워크 프로그래밍에서 사용되며, 소켓 주소 구조체인 serverAddr를 설정하는 과정을 보여줍니다. 이 구조체는 일반적으로 sockaddr_in 타입으로 선언되며, 인터넷 프로토콜(IP) 주소와 포트 번호를 지정하는 데 사용됩니다. 여기서 설정된 값들은 서버 소켓이 특정 네트워크 인터페이스와 포트 번호에 바인딩되는 데 필요합니다. 

각 줄의 의미는 다음과 같습니다: 

1. serverAddr.sin_family = AF_INET;: 이 줄은 serverAddr 구조체의 sin_family 멤버를 AF_INET으로 설정합니다. AF_INET은 이 주소가 IPv4 인터넷 프로토콜을 사용한다는 것을 의미합니다. sin_family 필드는 소켓이 사용할 주소 체계를 지정하는 데 필요합니다.

※ sin_family는 Socket INternet Family를 의미합니다.

2. serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);: 여기서는 serverAddr 구조체의 sin_addr 필드를 설정합니다. sin_addr in_addr 구조체를 사용하여 IP 주소를 저장합니다.

  • INADDR_ANY는 모든 사용 가능한 인터페이스에 서버를 바인딩하겠다는 것을 의미합니다. 즉, 서버는 호스트의 모든 네트워크 인터페이스를 통해 들어오는 연결 요청을 수락할 준비가 되어 있습니다.
  • htonl 함수는 호스트 바이트 순서에서 네트워크 바이트 순서로 32비트 숫자를 변환합니다. IP 주소는 네트워크 바이트 순서로 저장되어야 하기 때문에 이 변환은 필수적입니다.

3. serverAddr.sin_port = htons(5001);: 이 줄은 serverAddr 구조체의 sin_port 필드에 포트 번호를 할당합니다.

  • 5001은 서버가 사용할 TCP 포트 번호입니다.
  • htons 함수는 호스트 바이트 순서에서 네트워크 바이트 순서로 16비트 숫자를 변환합니다. 포트 번호는 네트워크 바이트 순서로 저장되어야 하므로, 이 변환도 필수적입니다.

이 코드는 네트워크 프로그래밍에서 소켓을 설정할 때 필수적인 단계입니다. 서버 소켓이 올바르게 설정되면, 해당 소켓을 특정 포트에 바인딩하고, 클라이언트의 연결 요청을 수신 대기할 수 있습니다.

 

Socket Binding

// 소켓 바인딩
if (bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
    printf("Failed to bind socket.\n");
    return -1;
}

위 코드는 네트워크 프로그래밍에서 소켓을 특정 주소(일반적으로 IP 주소와 포트 번호)에 바인딩하는 과정을 보여줍니다. bind 함수는 소켓에 로컬 주소를 할당하는 데 사용됩니다. 이 작업은 서버 소켓을 특정 포트에 연결하는 데 필요하며, 클라이언트가 해당 서버에 연결할 수 있게 만듭니다. 

코드의 각 부분에 대한 상세한 설명은 다음과 같습니다: 

1. bind 함수: bind 함수는 serverSocket이라는 소켓을 serverAddr이라는 주소 구조체에 바인딩합니다. 이 함수는 소켓, 바인딩할 주소, 그리고 주소 구조체의 크기를 매개변수로 받습니다.

  • serverSocket: 바인딩할 소켓의 식별자입니다. 이전에 socket 함수를 사용하여 생성된 소켓이 이 자리에 옵니다.
  • (struct sockaddr*)&serverAddr: serverAddr 구조체의 주소를 struct sockaddr 타입으로 캐스팅합니다. serverAddr는 struct sockaddr_in 타입이지만, bind 함수는 더 일반적인 struct sockaddr 타입을 요구합니다.
  • sizeof(serverAddr): 바인딩할 주소 구조체의 크기입니다. 이 값은 bind 함수에 주소의 크기를 알려줍니다.

2. 조건문: bind 함수는 성공적으로 완료되면 0을 반환하고, 실패하면 SOCKET_ERROR를 반환합니다. 이 조건문은 bind 함수가 SOCKET_ERROR를 반환하는지 확인합니다. 만약 그렇다면, 바인딩 과정에서 오류가 발생했음을 의미합니다. 

3. 오류 메시지와 반환: 만약 bind 함수가 실패하면, "Failed to bind socket.\n"이라는 메시지가 출력되고, 프로그램은 -1을 반환하여 오류 상태를 나타냅니다. 

이 코드는 서버 소켓을 설정하고 네트워크상에서 해당 서버를 식별할 수 있도록 준비하는 핵심 단계입니다. 소켓이 성공적으로 바인딩되면, 서버는 해당 주소에서 들어오는 클라이언트의 연결 요청을 수신 대기할 수 있습니다.

 

Listen 대기

if (listen(serverSocket, 1) == SOCKET_ERROR) {
    printf("Failed to listen on socket.\n");
    return -1;
}

위 코드는 네트워크 서버 프로그래밍의 두 중요한 단계중, 클라이언트의 연결 요청에 대한 수신 대기를 보여줍니다: 클라이언트의 연결 요청을 수신 대기하는 과정(listen)과 실제로 클라이언트의 연결을 수락하는 과정(accept). 

 

Listen

  • listen 함수는 소켓을 TCP Connection Request를 수신할 수 있게 만드는 명령입니다. 이 함수는 두 파라미터를 가집니다:
    • serverSocket: 클라이언트의 연결 요청을 수신 대기할 서버의 소켓 핸들입니다.
    • 1: 이 매개변수는 소켓의 연결 대기 큐의 최대 길이를 나타냅니다. 여기서 1은 한 번에 하나의 연결 요청만 수신 대기하겠다는 의미입니다.
  • SOCKET_ERROR 체크: listen 함수가 SOCKET_ERROR를 반환하면, 연결 대기를 시작하는 데 실패했음을 의미합니다. 이 경우 오류 메시지를 출력하고 -1을 반환합니다.

 

Client와의 Connection Establishment

clientAddrSize = sizeof(clientAddr);
if ((clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize)) == INVALID_SOCKET) {
    printf("Failed to accept client connection.\n");
    return -1;
}

 

위 코드는 네트워크 서버 프로그래밍의 두 중요한 단계중, 

클라이언트의 연결 요청을 수락하는 과정(accept)을 보여줍니다: 

클라이언트의 연결 요청을 수신 대기하는 과정(listen)과 실제로 클라이언트의 연결 요청을 수락하는 과정(accept). 

 

클라이언트 연결 수락 (Accept)

  • accept 함수는 연결 대기 큐에서 첫 번째 연결 요청을 받아들이고, 새로운 소켓을 생성합니다. 이 새로운 소켓은 서버와 클라이언트 간의 통신에 사용됩니다.
    • serverSocket: 클라이언트의 연결 요청을 수신하는 서버의 소켓 핸들입니다.
    • (struct sockaddr*)&clientAddr: 클라이언트의 주소 정보를 저장할 구조체입니다. accept 함수는 이 구조체를 통해 연결한 클라이언트의 주소 정보를 반환합니다.
    • &clientAddrSize: clientAddr 구조체의 크기입니다. accept 함수가 호출되기 전에 이 값을 설정해야 합니다.
  • INVALID_SOCKET 체크: accept 함수가 INVALID_SOCKET을 반환하면, 클라이언트 연결을 수락하는 데 실패했음을 의미합니다. 이 경우 오류 메시지를 출력하고 -1을 반환합니다.

위 코드들은 서버가 클라이언트의 연결 요청을 기다리고, 실제로 연결을 수락하는 전형적인 흐름을 나타냅니다. 이 과정은 네트워크 기반 서버 애플리케이션의 기본적인 동작입니다.

 
 

클라이언트와 데이터 송수신

while (1) {
    // 클라이언트로부터 메시지 수신
    memset(buffer, 0, MAX_BUFFER_SIZE);
    int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
    if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
        break;
    }

    printf("수신한 메시지: %s\n", buffer);

    // 클라이언트에게 응답 전송
    char response[MAX_BUFFER_SIZE];
    snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer);
    send(clientSocket, response, strlen(response), 0);
}
위 코드는 서버가 클라이언트로부터 메시지를 수신하고, 수신된 메시지에 대해 응답을 보내는 반복적인 과정을 구현하고 있습니다. 네트워크 프로그래밍에서 이러한 패턴은 클라이언트-서버 간의 통신을 처리하는 표준적인 방법 중 하나입니다. 

코드의 주요 부분은 다음과 같습니다: 

1. 무한 루프 (while (1)): 서버는 클라이언트와의 연결이 유지되는 동안 계속해서 메시지를 수신하고 응답을 보내는 작업을 반복합니다. 

2. 클라이언트로부터 메시지 수신
  • memset(buffer, 0, MAX_BUFFER_SIZE): 수신 버퍼[buffer]를 0으로 초기화합니다. 이는 이전 메시지의 데이터가 남아 있지 않도록 하기 위함입니다.
  • recv 함수: 클라이언트로부터 데이터를 수신합니다. clientSocket은 클라이언트와의 통신에 사용되는 소켓, buffer는 수신된 데이터를 저장할 버퍼, MAX_BUFFER_SIZE는 버퍼의 최대 크기를 나타냅니다.
  • bytesRead: recv 함수가 반환하는 값으로, 실제로 수신된 바이트 수입니다. SOCKET_ERROR 또는 0(연결이 닫힘)을 반환하면 루프를 빠져나옵니다.
3. 수신 메시지 출력
   printf("수신한 메시지: %s\n", buffer): 수신된 메시지를 콘솔에 출력합니다. 

4. 클라이언트에게 응답 전송
  • char response[MAX_BUFFER_SIZE]: 응답 메시지를 저장할 버퍼를 선언합니다.
  • snprintf(response, MAX_BUFFER_SIZE, "서버가 메시지를 수신했습니다: %s", buffer): 수신된 메시지에 대한 응답 메시지를 생성합니다.
  • send 함수: 생성된 응답 메시지를 클라이언트에게 전송합니다. clientSocket은 목적지 클라이언트의 소켓, response는 전송할 데이터, strlen(response)는 전송할 데이터의 크기를 나타냅니다.
이 코드는 네트워크 프로그래밍에서 클라이언트와 서버 간의 기본적인 대화형 통신을 구현하는 방법을 보여줍니다. 서버는 클라이언트로부터 메시지를 수신하고, 그에 대해 응답을 보내는 과정을 계속 반복합니다.
 

Connection Close

// 연결 종료
closesocket(clientSocket);
closesocket(serverSocket);
WSACleanup();

이 코드 조각은 Windows Sockets API를 사용하는 네트워크 프로그램에서 연결을 종료하고, 리소스를 정리하는 과정을 보여줍니다. 각 함수는 다음과 같은 역할을 합니다: 

1. closesocket(clientSocket);

  • 이 함수는 클라이언트와의 연결을 담당하는 소켓(clientSocket)을 닫습니다. clientSocket은 클라이언트와의 통신에 사용된 소켓으로, accept 함수를 통해 생성되었습니다.
  • 소켓을 닫는 것은 연결을 종료하고, 해당 소켓에 할당된 모든 리소스를 해제하는 것을 의미합니다. 이는 네트워크 프로그래밍에서 매우 중요한 단계로, 사용하지 않는 소켓을 열어 두면 리소스 누수가 발생할 수 있습니다.

2. closesocket(serverSocket);

  • 이 함수는 서버의 메인 소켓(serverSocket)을 닫습니다. serverSocket은 서버가 클라이언트의 연결 요청을 수신 대기하는 데 사용된 소켓입니다.
  • serverSocket도 사용이 끝난 후에는 반드시 닫아야 하며, 이를 통해 해당 소켓에 대한 리소스가 해제됩니다.

3. WSACleanup();

  • WSACleanup 함수는 Windows Sockets API의 사용을 종료합니다. 이 함수는 WSAStartup 함수에 의해 초기화된 Winsock 라이브러리의 리소스를 해제합니다.
  • 프로그램이 Winsock API의 사용을 완전히 마쳤을 때 한 번 호출해야 합니다. 모든 소켓 작업이 완료된 후에 WSACleanup을 호출하는 것이 일반적입니다.
  • WSACleanup 호출 없이 프로그램이 종료되면 시스템 리소스가 제대로 해제되지 않을 수 있으며, 이는 잠재적인 메모리 누수나 다른 네트워크 문제를 일으킬 수 있습니다.

이 코드는 네트워크 서버 프로그램을 정상적으로 종료하고 모든 네트워크 리소스를 정리하는 데 필수적인 단계입니다. 이러한 종료 및 정리 과정은 프로그램의 안정성과 시스템 리소스의 효율적 관리를 위해 중요합니다.

 

 

TCP Client Socket Programming

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>

#pragma comment(lib, "ws2_32.lib")

#define MAX_BUFFER_SIZE 1024

int main() {
    WSADATA wsaData;
    SOCKET clientSocket;
    struct sockaddr_in serverAddr;
    struct in_addr ipAddr;

    char buffer[MAX_BUFFER_SIZE];
    char message[MAX_BUFFER_SIZE];

    // Winsock 초기화
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        printf("Failed to initialize winsock.\n");
        return -1;
    }

    // 소켓 생성
    if ((clientSocket = socket(AF_INET, SOCK_STREAM, 0)) == INVALID_SOCKET) {
        printf("Failed to create socket.\n");
        return -1;
    }

    // 서버 정보 설정
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(5001);

    if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
        printf("Invalid address/ Address not supported.\n");
        return -1;
    }

    // 서버에 연결
    if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
        printf("Failed to connect to server.\n");
        return -1;
    }

    while (1) {
        // 사용자로부터 메시지 입력
        printf("메시지를 입력하세요 (종료하려면 q 또는 Q): ");
        fgets(message, MAX_BUFFER_SIZE, stdin);
        message[strcspn(message, "\n")] = '\0';

        if (strcmp(message, "q") == 0 || strcmp(message, "Q") == 0) {
            break;
        }

        // 서버로 메시지 전송
        send(clientSocket, message, strlen(message), 0);

        // 서버로부터 응답 수신
        memset(buffer, 0, MAX_BUFFER_SIZE);
        int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
        if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
            break;
        }

        printf("서버로부터 받은 응답: %s\n", buffer);
    }

    // 연결 종료
    closesocket(clientSocket);
    WSACleanup();

    return 0;
}

 

서버 설정

	// 서버 정보 설정
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(5001);

    if (inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr) <= 0) {
        printf("Invalid address/ Address not supported.\n");
        return -1;
    }

위 코드는 네트워크 프로그래밍에서 서버 주소를 설정하는 과정을 보여줍니다. 구체적으로는 serverAddr, 즉 struct sockaddr_in 타입의 구조체를 설정하고 있습니다. 이 구조체는 인터넷(IPv4) 주소를 다루는 데 사용됩니다.

inet_pton은 Internet Protocol string TO Number의 약자입니다.


코드의 각 부분에 대한 설명은 다음과 같습니다: 

1. 서버 정보 설정

  • serverAddr.sin_family = AF_INET;: sin_family 필드를 AF_INET으로 설정합니다. 이는 주소 체계가 IPv4임을 나타냅니다.
  • serverAddr.sin_port = htons(5001);: sin_port 필드에 서버의 포트 번호를 설정합니다. 여기서 htons 함수는 호스트 바이트 순서의 숫자(여기서는 5001)를 네트워크 바이트 순서로 변환합니다. 

2. IP 주소 설정

  • inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr): inet_pton 함수는 문자열 형태의 IP 주소("127.0.0.1")를 네트워크 바이트 순서의 이진 형태로 변환하고 serverAddr.sin_addr에 저장합니다. 
  • AF_INET은 IPv4 주소 체계를 나타내며, "127.0.0.1"은 로컬 호스트 주소(즉, 현재 컴퓨터)를 나타냅니다.

3. 오류 확인

  • inet_pton 함수는 변환에 성공하면 1을, 실패하면 0 또는 음수를 반환합니다. 여기서 조건문은 inet_pton의 반환 값이 0 이하인지 확인하여 IP 주소 변환에 실패했는지 검사합니다.
  • IP 주소 변환에 실패하면, "Invalid address/ Address not supported."라는 오류 메시지를 출력하고 프로그램을 -1로 종료시킵니다.

이 코드는 특히 서버 소켓을 로컬 호스트의 특정 포트에 바인딩하기 위한 설정을 포함하고 있습니다. 이러한 설정은 클라이언트가 서버에 접속할 수 있게 하는 데 중요한 역할을 합니다.

 

 

서버 연결

// 서버에 연결
if (connect(clientSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
    printf("Failed to connect to server.\n");
    return -1;
}

 

위 코드는 클라이언트 프로그램이 서버에 연결을 시도하는 과정을 보여줍니다. connect 함수는 클라이언트 소켓을 서버의 주소에 지정된 소켓에 연결합니다. 이는 네트워크 기반 클라이언트 애플리케이션에서 서버에 연결하기 위한 핵심 단계입니다. 

코드의 각 부분은 다음과 같이 해석됩니다: 

1. connect 함수

  • clientSocket: 이는 클라이언트 측에서 생성된 소켓의 핸들입니다. 이전에 socket 함수를 호출하여 생성된 소켓을 사용합니다.
  • (struct sockaddr*)&serverAddr: 이 매개변수는 서버의 주소 정보를 담고 있는 serverAddr 구조체의 주소입니다. 여기서 serverAddr는 struct sockaddr_in 타입이지만, connect 함수는 더 일반적인 struct sockaddr 타입을 요구하기 때문에 캐스팅이 필요합니다.
  • sizeof(serverAddr): 이는 serverAddr 구조체의 크기입니다. connect 함수에 서버 주소의 정확한 크기를 전달하는 것이 중요합니다.

2. 오류 확인

  • connect 함수는 연결 시도가 성공적이면 0을 반환하고, 실패하면 음수를 반환합니다.
  • 조건문 if (connect(...) < 0) connect 함수가 실패했는지 확인합니다. 실패한 경우, "Failed to connect to server.\n"라는 오류 메시지를 출력하고 프로그램을 -1로 종료시킵니다.

이 코드의 목적은 서버에 대한 클라이언트의 연결을 설정하는 것입니다. 연결이 성공적으로 이루어지면, 클라이언트와 서버 간의 데이터 교환을 시작할 수 있습니다. 연결 실패는 네트워크 문제, 잘못된 서버 주소, 서버가 연결을 수락하지 않는 상황 등 다양한 이유로 발생할 수 있으므로, 이를 적절히 처리하는 것이 중요합니다.

 

데이터 송수신

while (1) {
        // 사용자로부터 메시지 입력
        printf("메시지를 입력하세요 (종료하려면 q 또는 Q): ");
        fgets(message, MAX_BUFFER_SIZE, stdin);
        message[strcspn(message, "\n")] = '\0';

        if (strcmp(message, "q") == 0 || strcmp(message, "Q") == 0) {
            break;
        }

        // 서버로 메시지 전송
        send(clientSocket, message, strlen(message), 0);

        // 서버로부터 응답 수신
        memset(buffer, 0, MAX_BUFFER_SIZE);
        int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0);
        if (bytesRead == SOCKET_ERROR || bytesRead == 0) {
            break;
        }

        printf("서버로부터 받은 응답: %s\n", buffer);
    }

위 코드는 클라이언트 프로그램에서 서버와의 통신을 처리하는 무한 루프를 구현하고 있습니다. 클라이언트는 사용자로부터 메시지를 입력받아 서버로 전송하고, 서버로부터 응답을 받아 출력하는 과정을 반복합니다. 

코드의 각 부분에 대한 설명은 다음과 같습니다: 

1. 사용자로부터 메시지 입력 받기
   printf("메시지를 입력하세요 (종료하려면 q 또는 Q): "): 사용자에게 메시지 입력을 안내하는 문구를 출력합니다. 
   fgets(message, MAX_BUFFER_SIZE, stdin): 표준 입력(stdin)에서 최대 MAX_BUFFER_SIZE 길이의 문자열을

      message 배열에 저장합니다. fgets는 개행 문자까지 읽어들이기 때문에... 
   message[strcspn(message, "\n")] = '\0: fgets로 인해 입력된 문자열 끝에 포함된 개행 문자(\n)를 널 문자(\0)로 대체하

      여 문자열의 끝을 나타냅니다. 

2. 종료 조건 체크
   if (strcmp(message, "q") == 0 || strcmp(message, "Q") == 0): 입력된 메시지가 "q" 또는 "Q"인 경우, 루프를 빠져나

      와 프로그램을 종료합니다. 

3. 서버로 메시지 전송
   send(clientSocket, message, strlen(message), 0): clientSocket을 통해 입력된 message를 서버에 전송합니

      다. strlen(message)는 전송할 문자열의 길이를 나타냅니다. 

4. 서버로부터 응답 수신
   memset(buffer, 0, MAX_BUFFER_SIZE): 수신 버퍼를 0으로 초기화합니다. 
   int bytesRead = recv(clientSocket, buffer, MAX_BUFFER_SIZE, 0): 서버로부터 데이터를 수신하여 buffer에 저장

      합니다. 수신된 바이트 갯수는 bytesRead에 저장됩니다. 
   if (bytesRead == SOCKET_ERROR || bytesRead == 0): recv 함수가 SOCKET_ERROR를 반환하거나 수신된 데이터

      가 없는 경우(즉, 서버가 연결을 종료한 경우), 루프를 빠져나옵니다. 

5. 서버로부터 받은 응답 출력
   printf("서버로부터 받은 응답: %s\n", buffer): 서버로부터 수신된 응답 메시지를 출력합니다. 

이 코드는 클라이언트가 서버와의 대화형 통신을 처리하는 방식을 보여줍니다. 사용자가 "q" 또는 "Q"를 입력하여 대화를 종료할 때까지 클라이언트는 계속해서 메시지를 전송하고 서버의 응답을 받습니다.

 

 

 send recv 함수의 네 번째 매개변수는 flags로, 메시지 전송 및 수신 방식에 대한 추가적인 옵션을 지정하는 데 사용됩니다. 이 파라미터를 통해 특정 동작을 제어하거나, 특정 상황에서의 동작을 세밀하게 조절할 수 있습니다. 

1. send 함수의 네 번째 매개변수 (flags)

  • send 함수의 flags 매개변수는 전송 동작을 제어하는 데 사용됩니다. 
  • 일반적으로 사용되는 플래그는 0이며, 이는 기본 동작(즉, 메시지를 정상적으로 보내는 것)을 나타냅니다.
  • 다른 가능한 값으로는 MSG_DONTROUTE, MSG_OOB 등이 있습니다. 예를 들어, MSG_OOB는 긴급 데이터(out-of-band data)를 보낼 때 사용되며, MSG_DONTROUTE는 라우터를 거치지 않고 직접 데이터를 보낼 때 사용됩니다.

2. recv 함수의 네 번째 매개변수 (flags)

  • recv 함수에서도 flags 매개변수는 수신 동작을 제어하는 데 사용됩니다.
  • recv의 기본 flags 값은 0으로, 일반적인 수신 동작을 나타냅니다.
  • MSG_PEEK MSG_OOB와 같은 값도 사용될 수 있습니다. MSG_PEEK는 수신된 데이터를 큐에서 제거하지 않고 엿보기만 하고자 할 때 사용되며, MSG_OOB는 긴급 데이터를 수신할 때 사용됩니다.

이러한 플래그들은 소켓 프로그래밍에서 특정 네트워크 조건이나 요구 사항에 맞춰 통신 동작을 세밀하게 조정하는 데 유용합니다. 그러나 대부분의 일반적인 응용 프로그램에서는 기본값인 0을 사용합니다.

'Network' 카테고리의 다른 글

Http 프로토콜  (5) 2024.09.27
UDP 소켓 프로그래밍  (0) 2024.09.26
네트워크 프로토콜  (4) 2024.09.26
네트워크 필수 개념: DHCP, ARP, NAT, 그리고 ZeroConf  (8) 2024.09.25
네트워크 기초  (0) 2024.09.25
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유