본문 바로가기
임베디드SW 기초

리눅스 네트워크 프로그래밍(2) - C 소켓 프로그래밍(Socket Programming

by sw-develop-record 2025. 3. 7.

소켓 프로그래밍은 네트워크 통신을 위한 기본적인 도구로, 리눅스에서는 C 언어로 간단하게 구현할 수 있습니다. 이 글에서는 기본적인 클라이언트-서버 소켓 프로그래밍 방법을 알려드리겠습니다.


소켓 프로그래밍이란?

소켓은 네트워크 통신을 위한 엔드포인트로, 프로세스 간 통신의 핵심 요소입니다. 쉽게 말해 소켓은 네트워크 상에서 데이터를 주고받을 수 있는 통로라고 생각하면 됩니다. 소켓은 IP 주소와 포트 번호의 조합으로 식별됩니다.


클라이언트-서버 모델의 기본 흐름

서버 측 동작 순서:

  1. 소켓 생성
  2. 소켓을 특정 주소와 포트에 바인딩
  3. 연결 요청 대기(listen)
  4. 클라이언트 연결 수락(accept)
  5. 데이터 송수신
  6. 연결 종료

클라이언트 측 동작 순서:

  1. 소켓 생성
  2. 서버에 연결 요청
  3. 데이터 송수신
  4. 연결 종료

이 기본적인 흐름을 코드로 구현해보겠습니다.


서버 구현하기

다음은 기본적인 에코 서버의 코드입니다. 클라이언트로부터 메시지를 받아 그대로 다시 전송하는 간단한 서버입니다.

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

int main()
{
    int socket_desc, client_sock, c, read_size;
    struct sockaddr_in server, client;
    char client_message[2000];
    
    *// 소켓 생성*
    socket_desc = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_desc == -1)
    {
        printf("소켓을 생성할 수 없습니다");
        return 1;
    }
    
    *// 서버 주소 구조체 준비*
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY; *// 모든 인터페이스에서 연결 수락*
    server.sin_port = htons(8888);       *// 포트 8888 사용*
    
    *// 소켓을 주소에 바인딩*
    if(bind(socket_desc, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        perror("바인딩 실패");
        return 1;
    }
    
    *// 연결 대기*
    listen(socket_desc, 3);
    
    *// 연결 수락*
    printf("연결을 기다리는 중...\\n");
    c = sizeof(struct sockaddr_in);
    client_sock = accept(socket_desc, (struct sockaddr *)&client, (socklen_t*)&c);
    
    if (client_sock < 0)
    {
        perror("연결 수락 실패");
        return 1;
    }
    printf("연결이 수락되었습니다\\n");
    
    *// 클라이언트로부터 메시지 수신 및 에코*
    while((read_size = recv(client_sock, client_message, 2000, 0)) > 0)
    {
        *// 받은 메시지를 클라이언트에게 다시 전송*
        write(client_sock, client_message, strlen(client_message));
    }
    
    if(read_size == 0)
    {
        printf("클라이언트 연결 종료\\n");
    }
    else if(read_size == -1)
    {
        perror("수신 실패");
    }
    
    return 0;
}

클라이언트 구현하기

이제 서버에 연결하여 메시지를 주고받을 클라이언트 코드를 만들어 봅시다.

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

int main()
{
    int sock;
    struct sockaddr_in server;
    char message[1000], server_reply[2000];
    
    *// 소켓 생성*
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock == -1)
    {
        printf("소켓을 생성할 수 없습니다");
        return 1;
    }
    
    *// 서버 주소 설정*
    server.sin_addr.s_addr = inet_addr("127.0.0.1"); *// 로컬호스트*
    server.sin_family = AF_INET;
    server.sin_port = htons(8888);
    
    *// 서버에 연결*
    if (connect(sock, (struct sockaddr *)&server, sizeof(server)) < 0)
    {
        perror("연결 실패");
        return 1;
    }
    
    printf("연결 성공\\n");
    
    *// 서버와 통신*
    while(1)
    {
        printf("메시지 입력: ");
        scanf("%s", message);
        
        *// 서버에 데이터 전송*
        if(send(sock, message, strlen(message), 0) < 0)
        {
            puts("전송 실패");
            return 1;
        }
        
        *// 서버로부터 응답 수신*
        if(recv(sock, server_reply, 2000, 0) < 0)
        {
            puts("수신 실패");
            break;
        }
        
        printf("서버 응답: %s\\n", server_reply);
        
        *// 메시지 버퍼 초기화*
        memset(server_reply, 0, 2000);
    }
    
    close(sock);
    return 0;
}

 

컴파일 및 실행 방법

리눅스 환경에서 작성한 코드를 컴파일하고 실행하는 방법은 다음과 같습니다.

 

1. 컴파일하기

*# 서버 컴파일*
gcc server.c -o server

*# 클라이언트 컴파일*
gcc client.c -o client

 

2. 실행하기

 

두 개의 터미널 창을 열고 다음과 같이 실행합니다.

 

첫 번째 터미널 (서버):

./server

두 번째 터미널 (클라이언트):

./client

주요 소켓 함수 정리

소켓 프로그래밍에서 사용하는 주요 함수들의 역할을 간략히 정리하면 다음과 같습니다.

  • socket(): 소켓 생성
  • bind(): 소켓을 특정 IP 주소와 포트에 바인딩
  • listen(): 연결 요청 대기 상태로 전환
  • accept(): 클라이언트 연결 수락
  • connect(): 서버에 연결 요청
  • send(), write(): 데이터 전송
  • recv(), read(): 데이터 수신
  • close(): 소켓 연결 종료

자주 발생하는 문제 및 해결 방법

소켓 프로그래밍 시 자주 발생하는 문제와 해결 방법을 알아봅시다.

  1. Address already in use 오류
    • 문제: 서버를 다시 시작할 때 이미 포트가 사용 중이라는 오류
    • 해결: SO_REUSEADDR 옵션 설정
    int opt = 1;
    setsockopt(socket_desc, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
  2. Connection refused
    • 문제: 서버가 실행 중이지 않거나 포트가 다를 때 발생
    • 해결: 서버 상태 확인 및 포트 번호 확인
  3. 데이터 수신 버퍼 오버플로우
    • 문제: 예상보다 큰 데이터 수신 시 버퍼 오버플로우 발생
    • 해결: 충분한 크기의 버퍼 사용 및 수신 데이터 크기 제한