C언어에서 POSIX 기반 네트워크 소켓 프로그래밍 완벽 가이드

POSIX 기반의 네트워크 소켓 프로그래밍은 네트워크 애플리케이션 개발의 핵심 기술입니다. 소켓 프로그래밍을 통해 C언어를 사용하여 클라이언트와 서버 간의 통신을 구현할 수 있으며, 이는 웹 서버, 채팅 애플리케이션, 파일 전송 프로그램 등 다양한 응용 프로그램의 기반이 됩니다. 본 기사에서는 POSIX 표준에 기반한 소켓 API의 기초 개념부터 실제 구현 방법과 주요 활용 예제를 단계적으로 살펴봅니다. 이를 통해 네트워크 소켓 프로그래밍의 원리를 이해하고 실제 프로젝트에 응용할 수 있는 실력을 키우는 것을 목표로 합니다.

POSIX 소켓 프로그래밍 개요


POSIX 소켓 프로그래밍은 UNIX 계열 운영체제에서 네트워크 통신을 구현하기 위해 사용되는 표준 API입니다. 이 표준은 다양한 운영체제 간의 호환성을 보장하며, 네트워크를 통한 데이터 교환을 효율적으로 처리할 수 있는 기능을 제공합니다.

POSIX 표준이란?


POSIX(Portable Operating System Interface)은 이식 가능한 운영체제 인터페이스를 정의하는 표준으로, UNIX 기반 시스템의 다양한 구현 간에 프로그램의 호환성을 확보하기 위해 만들어졌습니다. 네트워크 소켓 프로그래밍은 POSIX 표준의 일부로 포함되어 있습니다.

소켓 프로그래밍의 핵심 개념


소켓은 네트워크를 통한 양방향 통신을 가능하게 하는 소프트웨어 인터페이스입니다. 프로세스 간 데이터를 주고받기 위해 소켓을 생성하고, 설정하며, 데이터를 송수신합니다.

주요 소켓 유형

  • TCP 소켓: 신뢰성 있는 연결 지향 프로토콜로, 데이터의 순서와 무결성을 보장합니다.
  • UDP 소켓: 비연결 지향 프로토콜로, 빠른 전송 속도가 필요하지만 데이터의 무결성 보장이 필요하지 않은 경우에 사용됩니다.

소켓의 주요 특징

  • 확장성: 서버와 클라이언트 간의 통신을 효율적으로 확장 가능
  • 표준화: POSIX 표준으로 다양한 플랫폼에서 일관되게 작동

POSIX 소켓의 기본 구조


POSIX 소켓 프로그래밍은 다음의 순서로 진행됩니다:

  1. 소켓 생성 (socket 함수)
  2. 소켓 설정 (bind, listen, connect 함수 등)
  3. 데이터 송수신 (send, recv 함수)
  4. 연결 종료 (close 함수)

POSIX 소켓 프로그래밍은 네트워크 애플리케이션 개발의 핵심 요소이며, 기본 원리를 이해하는 것이 성공적인 개발의 첫걸음입니다.

소켓 생성과 설정


POSIX 소켓 프로그래밍의 첫 단계는 소켓을 생성하고, 필요한 설정을 적용하는 것입니다. 소켓은 네트워크 통신의 핵심 역할을 하며, 올바른 생성과 설정이 프로그램의 안정성을 보장합니다.

소켓 생성: `socket` 함수


socket 함수는 소켓을 생성하는 데 사용되며, 다음과 같은 세 가지 주요 매개변수를 필요로 합니다:

  1. 주소 체계(Protocol Family): IPv4의 경우 AF_INET, IPv6의 경우 AF_INET6를 사용합니다.
  2. 소켓 유형(Type): SOCK_STREAM(TCP) 또는 SOCK_DGRAM(UDP)을 지정합니다.
  3. 프로토콜(Protocol): 보통 0으로 설정하여 기본 프로토콜을 사용합니다.

소켓 생성 코드 예제:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

소켓 설정


생성된 소켓에 주소와 포트를 할당하는 과정이 필요합니다. 이 작업은 bind 함수로 수행됩니다.

주소 구조체 설정


소켓의 주소 정보를 저장하기 위해 sockaddr_in 구조체를 설정합니다:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;  // 모든 네트워크 인터페이스에서 수신
serv_addr.sin_port = htons(8080);       // 포트를 8080으로 설정

`bind` 함수


소켓에 IP 주소와 포트를 연결합니다:

if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("bind failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

추가 설정


TCP 소켓의 경우, 서버는 클라이언트 연결 요청을 수신하기 위해 listen 함수를 호출합니다:

if (listen(sockfd, 5) < 0) {  // 최대 5개의 대기 연결 허용
    perror("listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

결론


소켓 생성과 설정은 POSIX 소켓 프로그래밍의 필수 단계입니다. 올바른 설정은 통신의 안정성과 효율성을 보장하며, 이후 단계에서 원활한 데이터 송수신을 가능하게 합니다.

서버와 클라이언트의 차이


POSIX 소켓 프로그래밍에서 서버와 클라이언트는 서로 다른 역할과 동작 방식을 가집니다. 서버는 클라이언트의 요청을 수신하고 처리하며, 클라이언트는 서버와의 연결을 통해 필요한 데이터를 주고받습니다.

서버의 역할


서버는 소켓을 생성하고, 특정 포트와 바인딩한 후, 클라이언트 연결 요청을 대기합니다. 서버는 다수의 클라이언트와 통신하기 위해 효율적인 소켓 관리가 필요합니다.

`bind` 함수로 주소와 포트 할당


서버는 자신이 수신할 포트와 네트워크 인터페이스를 정의해야 합니다. 이를 위해 bind 함수가 사용됩니다.

`listen` 함수로 연결 요청 대기


서버는 listen 함수를 호출하여 클라이언트의 연결 요청을 대기합니다:

if (listen(sockfd, 10) < 0) {  // 최대 10개의 대기 연결 허용
    perror("listen failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

`accept` 함수로 연결 수락


클라이언트의 연결 요청이 수신되면, accept 함수로 요청을 수락합니다:

int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_sock < 0) {
    perror("accept failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

클라이언트의 역할


클라이언트는 서버와 연결을 생성하고 데이터를 요청하거나 응답을 받는 역할을 합니다.

`connect` 함수로 서버에 연결


클라이언트는 connect 함수를 호출하여 서버와 연결을 설정합니다:

if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("connection failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}

클라이언트와 서버의 통신

  • 클라이언트: 요청 데이터를 서버에 보냅니다.
  • 서버: 요청을 처리한 후 응답 데이터를 클라이언트에 보냅니다.

서버와 클라이언트의 주요 차이

  • 서버는 소켓에 포트와 주소를 바인딩하고 연결을 수락합니다.
  • 클라이언트는 서버에 직접 연결을 요청합니다.
  • 서버는 다수의 클라이언트를 처리할 수 있도록 설계되며, 클라이언트는 특정 서버와만 통신합니다.

결론


서버와 클라이언트는 각자의 역할에 따라 소켓 프로그래밍에서 서로 다른 API 호출과 동작 방식을 가집니다. 이러한 차이를 이해하고 적절히 구현하는 것이 네트워크 애플리케이션 개발의 핵심입니다.

데이터 송수신 원리


데이터 송수신은 네트워크 소켓 프로그래밍의 핵심 기능으로, 서버와 클라이언트가 정보를 주고받기 위해 소켓을 통해 데이터를 전송하거나 수신하는 과정을 포함합니다. POSIX 소켓 API는 이를 위해 다양한 함수들을 제공합니다.

데이터 송신: `send` 함수


send 함수는 소켓을 통해 데이터를 송신하는 데 사용됩니다.

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  • sockfd: 데이터 전송에 사용할 소켓의 파일 디스크립터
  • buf: 송신할 데이터가 저장된 버퍼
  • len: 송신할 데이터의 길이
  • flags: 전송 동작을 제어하는 옵션 (보통 0으로 설정)

송신 예제:

char *message = "Hello, Client!";
if (send(sockfd, message, strlen(message), 0) < 0) {
    perror("send failed");
}

데이터 수신: `recv` 함수


recv 함수는 소켓을 통해 데이터를 수신하는 데 사용됩니다.

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  • sockfd: 데이터 수신에 사용할 소켓의 파일 디스크립터
  • buf: 수신한 데이터를 저장할 버퍼
  • len: 수신할 데이터의 최대 크기
  • flags: 수신 동작을 제어하는 옵션 (보통 0으로 설정)

수신 예제:

char buffer[1024] = {0};
ssize_t bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
if (bytes_received < 0) {
    perror("recv failed");
} else {
    buffer[bytes_received] = '\0';  // Null-terminate the received data
    printf("Received: %s\n", buffer);
}

TCP 소켓의 데이터 흐름

  1. 클라이언트 송신: 클라이언트가 send 함수로 데이터를 전송
  2. 서버 수신: 서버가 recv 함수로 데이터를 수신
  3. 서버 송신: 서버가 클라이언트에게 응답 데이터를 send 함수로 전송
  4. 클라이언트 수신: 클라이언트가 서버의 응답을 recv 함수로 수신

UDP 소켓의 데이터 송수신


UDP는 연결 지향적이지 않으므로, 데이터를 송수신할 때 sendtorecvfrom 함수가 사용됩니다.

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

데이터 송수신의 주요 고려사항

  • 데이터의 크기 제한: 송신 데이터가 너무 클 경우, 여러 번 나눠서 전송 필요
  • 네트워크 지연: 데이터 송수신 시 네트워크 지연을 고려하여 타임아웃 설정
  • 에러 처리: sendrecv 함수의 반환값을 확인하여 에러를 처리

결론


POSIX 소켓 프로그래밍에서 sendrecv는 데이터를 송수신하는 주요 함수입니다. 올바른 사용법을 익히고 다양한 상황에서 적절히 활용하는 것이 네트워크 애플리케이션 개발의 필수입니다.

연결 및 연결 해제


POSIX 소켓 프로그래밍에서 연결 설정과 해제는 네트워크 통신의 시작과 끝을 의미합니다. 서버와 클라이언트 간 안정적이고 효율적인 연결을 생성하고 종료하는 방법을 이해하는 것은 중요한 기술입니다.

클라이언트의 연결 설정: `connect` 함수


클라이언트는 서버와 연결하기 위해 connect 함수를 사용합니다.

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: 연결할 소켓의 파일 디스크립터
  • addr: 서버의 주소 정보를 담은 구조체
  • addrlen: 주소 구조체의 크기

클라이언트 연결 예제:

struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
    perror("connect failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Connected to the server\n");

서버의 연결 관리


서버는 클라이언트 연결 요청을 처리하기 위해 accept 함수를 호출합니다:

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • sockfd: 수신 대기 중인 소켓의 파일 디스크립터
  • addr: 연결된 클라이언트의 주소 정보가 저장될 구조체
  • addrlen: 주소 구조체의 크기에 대한 포인터

서버 연결 수락 예제:

struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);

int client_sock = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len);
if (client_sock < 0) {
    perror("accept failed");
    close(sockfd);
    exit(EXIT_FAILURE);
}
printf("Client connected\n");

연결 해제: `close` 함수


소켓 연결을 종료하려면 close 함수를 호출합니다. 이 함수는 소켓을 닫아 리소스를 반환하고, 추가 데이터 송수신을 중단합니다.

close(sockfd);

안전한 연결 종료


네트워크 통신 중 연결을 해제할 때, 남아 있는 데이터를 전송한 후 종료하는 것이 중요합니다.

  • shutdown 함수를 사용하여 송수신을 단계적으로 중단할 수 있습니다:
shutdown(sockfd, SHUT_RDWR);  // 읽기와 쓰기 모두 중단

타임아웃 설정


연결 대기 시간이나 통신 중 타임아웃을 설정하여, 비정상적인 연결로 인한 무한 대기를 방지할 수 있습니다.

struct timeval timeout;
timeout.tv_sec = 5;  // 5초 대기
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

결론


POSIX 소켓 프로그래밍에서 연결 설정(connect, accept)과 해제(close, shutdown)는 네트워크 애플리케이션의 기본 작업입니다. 적절한 연결 관리와 에러 처리는 안정적이고 효율적인 통신의 핵심입니다.

비동기 I/O 및 다중 클라이언트 처리


네트워크 애플리케이션에서 다수의 클라이언트를 처리하거나 비동기적으로 데이터를 송수신하려면 효율적인 I/O 관리가 필수적입니다. POSIX 소켓 프로그래밍에서는 selectpoll 같은 비동기 I/O 기술을 활용해 이러한 문제를 해결할 수 있습니다.

비동기 I/O의 필요성


비동기 I/O는 하나의 서버가 여러 클라이언트의 요청을 동시에 처리할 수 있도록 합니다.

  • 블로킹 I/O의 한계: 하나의 클라이언트 요청 처리 중 다른 요청이 대기 상태가 됩니다.
  • 비동기 I/O의 장점: 동시에 여러 소켓을 모니터링하고, 준비된 소켓에서만 작업을 수행합니다.

`select` 함수


select 함수는 다중 소켓의 상태를 모니터링하는 데 사용됩니다.

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds: 모니터링할 파일 디스크립터 중 가장 큰 값 + 1
  • readfds: 읽기 작업을 모니터링할 파일 디스크립터 집합
  • writefds: 쓰기 작업을 모니터링할 파일 디스크립터 집합
  • exceptfds: 예외 처리를 모니터링할 파일 디스크립터 집합
  • timeout: 대기 시간 설정

예제:

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);

int activity = select(sockfd + 1, &readfds, NULL, NULL, NULL);
if (activity < 0) {
    perror("select error");
}
if (FD_ISSET(sockfd, &readfds)) {
    // 읽기 가능한 데이터 처리
}

`poll` 함수


poll 함수는 select의 대안으로, 더 많은 소켓을 효율적으로 모니터링할 수 있습니다.

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds: 모니터링할 파일 디스크립터 배열
  • nfds: 배열의 크기
  • timeout: 대기 시간(ms 단위, -1은 무한 대기)

예제:

struct pollfd fds[2];
fds[0].fd = sockfd;
fds[0].events = POLLIN;  // 읽기 이벤트

int ret = poll(fds, 1, 5000);  // 5초 대기
if (ret < 0) {
    perror("poll error");
} else if (fds[0].revents & POLLIN) {
    // 읽기 가능한 데이터 처리
}

다중 클라이언트 처리

  • 소켓 배열 관리: 각 클라이언트의 소켓을 배열이나 리스트로 관리합니다.
  • select 또는 poll 사용: 비동기 I/O 함수를 사용해 여러 소켓을 동시에 모니터링합니다.
  • 새로운 클라이언트 처리: 서버 소켓에서 accept 호출을 통해 연결을 수락합니다.

예제: 다중 클라이언트 처리

fd_set master_set, read_set;
FD_ZERO(&master_set);
FD_SET(server_sock, &master_set);
int max_fd = server_sock;

while (1) {
    read_set = master_set;  // 복사본 생성
    int activity = select(max_fd + 1, &read_set, NULL, NULL, NULL);
    for (int i = 0; i <= max_fd; i++) {
        if (FD_ISSET(i, &read_set)) {
            if (i == server_sock) {
                // 새로운 클라이언트 연결 처리
                int new_client = accept(server_sock, NULL, NULL);
                FD_SET(new_client, &master_set);
                if (new_client > max_fd) max_fd = new_client;
            } else {
                // 기존 클라이언트 데이터 처리
                char buffer[1024];
                ssize_t bytes_received = recv(i, buffer, sizeof(buffer), 0);
                if (bytes_received <= 0) {
                    close(i);
                    FD_CLR(i, &master_set);
                } else {
                    buffer[bytes_received] = '\0';
                    printf("Received: %s\n", buffer);
                }
            }
        }
    }
}

결론


비동기 I/O와 다중 클라이언트 처리는 대규모 네트워크 애플리케이션의 성능과 효율성을 높이는 핵심 기술입니다. selectpoll의 활용법을 익히고, 이를 적용한 다중 클라이언트 처리 패턴을 학습하면 더욱 안정적이고 효율적인 네트워크 서비스를 구축할 수 있습니다.

네트워크 소켓 프로그래밍의 주요 오류 해결


네트워크 소켓 프로그래밍은 네트워크 환경과 프로세스 간의 복잡한 상호작용으로 인해 다양한 오류가 발생할 수 있습니다. POSIX 소켓 API에서 자주 발생하는 문제와 그 해결 방법을 살펴봅니다.

소켓 생성 실패

  • 원인: 시스템 리소스 부족, 권한 문제
  • 해결 방법:
  • 시스템 리소스를 점검하고, 불필요한 소켓을 닫아 리소스를 확보합니다.
  • 충분한 권한을 가진 사용자로 실행합니다.

예제:

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("socket creation failed");
    exit(EXIT_FAILURE);
}

바인딩 실패 (`bind`)

  • 원인:
  • 포트 충돌 (이미 사용 중인 포트를 바인딩 시도)
  • 잘못된 주소 또는 포트 설정
  • 해결 방법:
  • 포트 상태를 확인 (netstat, ss 명령 사용)
  • SO_REUSEADDR 옵션을 설정하여 포트를 재사용 가능하도록 설정
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

연결 실패 (`connect`)

  • 원인:
  • 서버가 실행 중이지 않거나 네트워크 연결 문제
  • 잘못된 IP 주소 또는 포트
  • 해결 방법:
  • 서버 상태를 점검하고 네트워크 연결을 확인합니다.
  • 올바른 주소와 포트를 사용했는지 확인합니다.

수신 오류 (`recv`)

  • 원인:
  • 연결 해제
  • 네트워크 지연 또는 패킷 손실
  • 해결 방법:
  • 반환값을 확인하여 연결 종료 여부를 처리합니다.
  • 네트워크 타임아웃을 설정하여 지연을 방지합니다.
struct timeval timeout;
timeout.tv_sec = 5;  // 5초 타임아웃
timeout.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));

클라이언트 연결 과다로 인한 서버 과부하

  • 원인: 동시에 연결된 클라이언트 수가 서버의 처리 한도를 초과
  • 해결 방법:
  • 최대 대기 연결 수를 적절히 설정 (listen 함수의 큐 크기)
  • 비동기 I/O(select, poll)를 활용하여 효율적으로 처리

데이터 크기 초과

  • 원인: 수신 버퍼 크기보다 큰 데이터 전송
  • 해결 방법: 데이터를 청크로 나누어 송수신
char buffer[1024];
size_t bytes_left = total_size;
while (bytes_left > 0) {
    ssize_t sent = send(sockfd, data, min(bytes_left, sizeof(buffer)), 0);
    if (sent < 0) {
        perror("send error");
        break;
    }
    bytes_left -= sent;
    data += sent;
}

디버깅 및 테스트 도구

  • strace: 소켓 호출의 시스템 레벨 로그 확인
  • tcpdump, Wireshark: 네트워크 트래픽 캡처 및 분석
  • netstat, ss: 네트워크 포트 및 연결 상태 점검

일반적인 트러블슈팅 팁

  1. 반환값 확인: 소켓 API 호출 후 반환값을 반드시 확인하고 오류를 처리합니다.
  2. 로그 활용: 주요 이벤트와 오류를 기록해 문제를 추적합니다.
  3. 단계별 점검: 각 단계별로 독립적으로 테스트하여 문제의 원인을 파악합니다.

결론


네트워크 소켓 프로그래밍에서 발생할 수 있는 다양한 오류를 사전에 방지하고, 발생 시 적절히 대처하는 방법을 이해하면 안정적이고 신뢰성 있는 애플리케이션을 개발할 수 있습니다. 디버깅 도구와 에러 처리 기법을 적극적으로 활용해 소켓 프로그래밍의 복잡성을 효과적으로 관리하십시오.

응용 예제: 간단한 채팅 프로그램


이 섹션에서는 POSIX 소켓 프로그래밍을 활용해 간단한 채팅 프로그램을 구현합니다. 서버와 다수의 클라이언트 간 메시지를 주고받는 구조를 통해 소켓의 실용적 사용법을 익힐 수 있습니다.

프로그램 개요

  • 서버: 클라이언트 연결 요청을 수락하고, 메시지를 수신하여 다른 클라이언트로 전달
  • 클라이언트: 메시지를 서버로 전송하고, 서버로부터 수신된 메시지를 출력

서버 코드


서버는 다중 클라이언트를 처리하기 위해 비동기 I/O(select)를 사용합니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_sock, client_sock, max_fd, activity;
    struct sockaddr_in server_addr, client_addr;
    fd_set master_set, read_set;
    int client_socks[MAX_CLIENTS] = {0};

    // 서버 소켓 생성
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    // 소켓 바인딩
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    // 연결 대기
    if (listen(server_sock, MAX_CLIENTS) < 0) {
        perror("Listen failed");
        close(server_sock);
        exit(EXIT_FAILURE);
    }

    // fd_set 초기화
    FD_ZERO(&master_set);
    FD_SET(server_sock, &master_set);
    max_fd = server_sock;

    printf("Server is running on port %d...\n", PORT);

    while (1) {
        read_set = master_set;  // fd_set 복사
        activity = select(max_fd + 1, &read_set, NULL, NULL, NULL);

        if (activity < 0) {
            perror("Select error");
            break;
        }

        // 새 클라이언트 연결 처리
        if (FD_ISSET(server_sock, &read_set)) {
            socklen_t addr_len = sizeof(client_addr);
            client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &addr_len);
            if (client_sock < 0) {
                perror("Accept failed");
                continue;
            }
            printf("New connection accepted\n");

            // 클라이언트 소켓 추가
            for (int i = 0; i < MAX_CLIENTS; i++) {
                if (client_socks[i] == 0) {
                    client_socks[i] = client_sock;
                    FD_SET(client_sock, &master_set);
                    if (client_sock > max_fd) max_fd = client_sock;
                    break;
                }
            }
        }

        // 클라이언트 데이터 처리
        for (int i = 0; i < MAX_CLIENTS; i++) {
            int sock = client_socks[i];
            if (sock > 0 && FD_ISSET(sock, &read_set)) {
                char buffer[1024] = {0};
                ssize_t bytes_received = recv(sock, buffer, sizeof(buffer), 0);

                if (bytes_received <= 0) {
                    // 연결 종료 처리
                    close(sock);
                    FD_CLR(sock, &master_set);
                    client_socks[i] = 0;
                } else {
                    buffer[bytes_received] = '\0';
                    printf("Message from client: %s\n", buffer);

                    // 메시지 브로드캐스트
                    for (int j = 0; j < MAX_CLIENTS; j++) {
                        if (client_socks[j] > 0 && client_socks[j] != sock) {
                            send(client_socks[j], buffer, bytes_received, 0);
                        }
                    }
                }
            }
        }
    }

    close(server_sock);
    return 0;
}

클라이언트 코드


클라이언트는 사용자 입력을 서버로 전송하고, 서버의 응답 메시지를 출력합니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define PORT 8080

int main() {
    int sock;
    struct sockaddr_in server_addr;
    char buffer[1024];

    // 클라이언트 소켓 생성
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr) <= 0) {
        perror("Invalid address");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 서버 연결
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Connection failed");
        close(sock);
        exit(EXIT_FAILURE);
    }

    printf("Connected to the server. Type your message:\n");

    while (1) {
        // 사용자 입력
        fgets(buffer, sizeof(buffer), stdin);
        send(sock, buffer, strlen(buffer), 0);

        // 서버 응답 수신
        ssize_t bytes_received = recv(sock, buffer, sizeof(buffer) - 1, 0);
        if (bytes_received > 0) {
            buffer[bytes_received] = '\0';
            printf("Message from server: %s\n", buffer);
        }
    }

    close(sock);
    return 0;
}

결론


이 예제는 서버와 클라이언트가 메시지를 주고받는 기본적인 채팅 프로그램입니다. 이 코드를 기반으로 파일 전송, 인증 시스템, 데이터 암호화 등 다양한 기능을 추가하며 네트워크 프로그래밍 능력을 확장할 수 있습니다.

요약


POSIX 소켓 프로그래밍은 네트워크 애플리케이션의 핵심 기술로, 소켓 생성, 데이터 송수신, 다중 클라이언트 처리, 오류 해결 등을 다룹니다. 본 기사에서는 네트워크 소켓 프로그래밍의 기초부터 간단한 채팅 프로그램 구현까지 단계적으로 설명했습니다. 이를 통해 독자는 네트워크 기반 애플리케이션 개발에 필요한 필수적인 지식을 익힐 수 있습니다.