[TIL/크래프톤 정글9기] 54일차 (Week 08 Socket 통신 & echo 서버 만들기)

blueprint·2025년 7월 4일

크래프톤정글9기

목록 보기
45/55

소켓 인터페이스 (The Sockets Interface)

개요

소켓 인터페이스는 Unix I/O 함수와 함께 사용되어 네트워크 애플리케이션을 구축하는 함수들의 집합

소켓 인터메이스 기반 네트워크 클라이언트-서버 개요


서버 측 순서

  • getaddrinfo()
    • → 바인딩할 수 있는 서버 주소(IP + port) 정보 획득
  • socket()
    • → 리스닝 소켓 생성
  • bind()
    • → 해당 IP/port에 소켓 바인딩
  • listen()
    • → 클라이언트 요청을 기다릴 수 있도록 리스닝 상태 진입
  • accept()
    • → 클라이언트의 연결 요청을 수락하고 새로운 연결 소켓 생성
  • rio_readlineb()
    • → 클라이언트가 보낸 데이터 읽기
  • rio_writen()
    • → 클라이언트에게 응답 쓰기
  • rio_readlineb() (EOF 처리용, 클라이언트 종료 감지)
  • close()
    • → 연결 종료

🔁 이후에는 다시 accept()로 돌아가 다음 클라이언트를 기다림

클라이언트 측 순서

  • getaddrinfo()
    • → 서버 주소 정보 획득
  • socket()
    • → 클라이언트 소켓 생성
  • connect()
    • → 서버에 연결 요청
  • rio_writen()
    • → 서버로 요청 데이터 전송
  • rio_readlineb()
    • → 서버의 응답 읽기
  • close()
    • → 연결 종료

소켓 주소 구조체

소켓의 정의

  • Linux 커널 관점: 통신의 끝점 (end point)
  • Linux 프로그램 관점: 해당 디스크립터가 있는 열린 파일

IP 소켓 주소 구조체

/* IP socket address structure */
struct sockaddr_in {
    uint16_t sin_family;       /* Protocol family (always AF_INET) */
    uint16_t sin_port;         /* Port number in network byte order */
    struct in_addr sin_addr;   /* IP address in network byte order */
    unsigned char sin_zero[8]; /* Pad to sizeof(struct sockaddr) */
};

일반 소켓 주소 구조체

/* Generic socket address structure */
struct sockaddr {
    uint16_t sa_family;   /* Protocol family */
    char sa_data[14];     /* Address data */
};

중요 특징

  • 16바이트 구조: 인터넷 소켓 주소는 16바이트 구조체
  • 네트워크 바이트 순서: IP 주소와 포트 번호는 빅 엔디안 형식
  • _in 접미사: "internet"의 줄임말 (input이 아님)

주요 소켓 함수들

1. socket() 함수

#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
// Returns: nonnegative descriptor if OK, -1 on error

사용 예시:

clientfd = socket(AF_INET, SOCK_STREAM, 0);
  • domain: AF_INET (32비트 IP 주소)
  • type: SOCK_STREAM (TCP/UDP 인지)
  • protocol: 0 (기본값)

2. connect() 함수 (클라이언트용)

#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, -1 on error

특징:

  • 클라이언트에서 서버와의 연결 수립
  • 성공 시 소켓 페어 생성: (x:y, addr.sin_addr:addr.sin_port)
  • 블로킹 함수 (연결 수립까지 대기)

3. bind() 함수 (서버용)

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// Returns: 0 if OK, -1 on error

역할:

  • 소켓을 특정 주소에 바인딩
  • 서버가 클라이언트 요청을 받을 포트 지정

4. listen() 함수 (서버용)

#include <sys/socket.h>
int listen(int sockfd, int backlog);
// Returns: 0 if OK, -1 on error

특징:

  • 활성 소켓을 수신 대기 소켓으로 변환
  • backlog: 대기 큐 크기 (보통 1,024로 설정)

5. accept() 함수 (서버용)

#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
// Returns: nonnegative connected descriptor if OK, -1 on error

역할:

  • 클라이언트 연결 요청 수락
  • 새로운 연결 디스크립터 반환

수신 대기 디스크립터 vs 연결 디스크립터

수신 대기 디스크립터연결 디스크립터
클라이언트 연결 요청의 끝점클라이언트-서버 연결의 끝점
서버 생존 기간 동안 존재클라이언트 서비스 기간 동안만 존재
한 번 생성되어 계속 사용각 연결마다 새로 생성

장점: 동시 다중 클라이언트 처리 가능 (각 클라이언트마다 별도의 연결 디스크립터)


호스트와 서비스 변환

getaddrinfo() 함수

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *host, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **result);
// Returns: 0 if OK, nonzero error code on error

void freeaddrinfo(struct addrinfo *result);
const char *gai_strerror(int errcode);

특징:

  • 호스트명/서비스명을 소켓 주소 구조체로 변환
  • 프로토콜 독립적
  • 재진입 가능 (reentrant)

addrinfo 구조체

struct addrinfo {
    int ai_flags;          /* Hints argument flags */
    int ai_family;         /* First arg to socket function */
    int ai_socktype;       /* Second arg to socket function */
    int ai_protocol;       /* Third arg to socket function */
    char *ai_canonname;    /* Canonical hostname */
    size_t ai_addrlen;     /* Size of ai_addr struct */
    struct sockaddr *ai_addr;  /* Ptr to socket address structure */
    struct addrinfo *ai_next;  /* Ptr to next item in linked list */
};

주요 hints 설정

ai_family

  • AF_INET: IPv4만
  • AF_INET6: IPv6만
  • AF_UNSPEC: 둘 다

ai_socktype

  • SOCK_STREAM: TCP 연결

ai_flags

  • AI_ADDRCONFIG: 로컬 호스트 설정에 맞는 주소만 반환
  • AI_CANONNAME: 정식 호스트명 반환
  • AI_NUMERICSERV: 서비스를 포트 번호로 강제
  • AI_PASSIVE: 서버용 (와일드카드 주소 사용)

getnameinfo() 함수

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                char *host, size_t hostlen,
                char *service, size_t servlen, int flags);
// Returns: 0 if OK, nonzero error code on error

특징:

  • 소켓 주소를 호스트명/서비스명으로 변환
  • getaddrinfo의 역함수

사용 예시: hostinfo 프로그램

#include "csapp.h"

int main(int argc, char **argv) {
    struct addrinfo *p, *listp, hints;
    char buf[MAXLINE];
    int rc, flags;
    
    /* Get a list of addrinfo records */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_family = AF_INET;       /* IPv4 only */
    hints.ai_socktype = SOCK_STREAM; /* Connections only */
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
        exit(1);
    }
    
    /* Walk the list and display each IP address */
    flags = NI_NUMERICHOST;
    for (p = listp; p; p = p->ai_next) {
        getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
        printf("%s\n", buf);
    }
    
    /* Clean up */
    freeaddrinfo(listp);
    exit(0);
}

헬퍼 함수들

open_clientfd() 함수

#include "csapp.h"
int open_clientfd(char *hostname, char *port);
// Returns: descriptor if OK, -1 on error

구현:

int open_clientfd(char *hostname, char *port) {
    int clientfd;
    struct addrinfo hints, *listp, *p;

    // 1. 서버 주소 정보를 얻기 위한 힌트 설정
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;     // TCP 연결
    hints.ai_flags = AI_NUMERICSERV;     // 포트는 숫자로 처리
    hints.ai_flags |= AI_ADDRCONFIG;     // 환경에 맞는 주소 체계 사용

    // 2. 호스트 이름과 포트를 기반으로 주소 정보 가져오기
    Getaddrinfo(hostname, port, &hints, &listp);

    // 3. 주소 리스트를 순회하며 연결 시도
    for (p = listp; p; p = p->ai_next) {
        // 소켓 생성
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
            continue; // 소켓 생성 실패 시 다음 주소로

        // 서버에 연결 시도
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
            break; // 연결 성공 시 루프 종료

        // 연결 실패 시 소켓 닫기
        Close(clientfd);
    }

    // 4. 주소 정보 메모리 해제
    Freeaddrinfo(listp);

    // 5. 연결 성공 여부 확인 후 결과 반환
    if (!p)               // 모든 연결 시도 실패
        return -1;
    else                  // 연결 성공
        return clientfd;
}

open_listenfd() 함수

int open_listenfd(char *port)
{
    struct addrinfo hints, *listp, *p;
    int listenfd, optval = 1;

    // 1. 주소 정보를 위한 힌트 설정
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;               // TCP 소켓
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;   // 모든 IP에서 수신
    hints.ai_flags |= AI_NUMERICSERV;              // 포트 번호는 숫자

    // 2. 포트에 대해 가능한 주소 목록 획득
    Getaddrinfo(NULL, port, &hints, &listp);

    // 3. 가능한 주소 중 하나에 바인딩 시도
    for (p = listp; p; p = p->ai_next) {
        // 소켓 생성
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
            continue;

        // 주소 재사용 옵션 설정 (bind error 방지)
        Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
                   (const void *)&optval, sizeof(int));

        // 바인딩 성공하면 루프 종료
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break;

        // 바인딩 실패한 경우 소켓 닫기
        Close(listenfd);
    }

    // 4. 주소 정보 메모리 해제
    Freeaddrinfo(listp);
    if (!p)  // 바인딩 성공한 주소가 없으면 실패
        return -1;

    // 5. 리스닝 상태로 전환
    if (listen(listenfd, LISTENQ) < 0) {
        Close(listenfd);
        return -1;
    }

    return listenfd;  // 준비된 리스닝 소켓 반환
}

핵심 특징

1. hints 설정

  • AI_PASSIVE: 서버용 소켓 (와일드카드 주소 사용)
  • AI_ADDRCONFIG: 로컬 호스트 설정에 맞는 주소만 반환
  • AI_NUMERICSERV: 포트 번호 사용

2. SO_REUSEADDR 옵션

Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,
           (const void *)&optval, sizeof(int));
  • 목적: "Address already in use" 오류 방지
  • 효과: 서버 재시작 시 즉시 포트 재사용 가능
  • 기본값: 약 30초 대기 후 포트 재사용

3. 오류 처리

  • 소켓 생성 실패 시 다음 주소 시도
  • 바인딩 실패 시 소켓 닫고 다음 주소 시도
  • 모든 주소 실패 시 -1 반환

특징:

  • 서버용 수신 대기 소켓 생성
  • AI_PASSIVE 플래그 사용 (와일드카드 주소)
  • setsockopt로 즉시 재시작 가능하도록 설정

헬퍼 함수의 장점

  1. 프로토콜 독립성: IPv4/IPv6 모두 지원
  2. 재진입 가능: 멀티스레드 환경에서 안전
  3. 오류 처리: 자동으로 다음 주소 시도
  4. 코드 간소화: 복잡한 설정 과정 숨김

echo 서버 클라이언트

Echo 프로그램의 동작

  • 클라이언트: 사용자 입력을 서버로 전송
  • 서버: 받은 메시지를 그대로 클라이언트에게 다시 전송
  • 종료: 클라이언트가 EOF를 만나면 연결 종료

Echo 클라이언트

클라이언트 코드

#include "csapp.h"

int main(int argc, char **argv)
{
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;

    if (argc != 3) {
        fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
        exit(0);
    }
    host = argv[1];
    port = argv[2];

    clientfd = Open_clientfd(host, port);
    Rio_readinitb(&rio, clientfd);

    while (Fgets(buf, MAXLINE, stdin) != NULL) {
        Rio_writen(clientfd, buf, strlen(buf));
        Rio_readlineb(&rio, buf, MAXLINE);
        Fputs(buf, stdout);
    }
    Close(clientfd);
    exit(0);
}

동작 과정

1. 연결 수립

clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd);
  • Open_clientfd(): 서버와 연결 수립
  • Rio_readinitb(): 버퍼링된 읽기를 위한 초기화

2. 메인 루프

while (Fgets(buf, MAXLINE, stdin) != NULL) {
    Rio_writen(clientfd, buf, strlen(buf));    // 서버로 전송
    Rio_readlineb(&rio, buf, MAXLINE);         // 서버로부터 수신
    Fputs(buf, stdout);                        // 표준 출력에 출력
}

3. 종료 조건

  • Fgets()가 NULL 반환 (EOF 만남)
  • 사용자가 Ctrl+D 입력
  • 리다이렉션된 파일의 끝에 도달

4. 연결 종료

Close(clientfd);
  • 클라이언트 측 연결 종료
  • 서버에 EOF 신호 전송

Echo 서버

서버 메인 코드

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr; /* Enough space for any address */
    char client_hostname[MAXLINE], client_port[MAXLINE];

    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(0);
    }

    listenfd = Open_listenfd(argv[1]);
    while (1) {
        clientlen = sizeof(struct sockaddr_storage);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
                    client_port, MAXLINE, 0);
        printf("Connected to (%s, %s)\n", client_hostname, client_port);
        echo(connfd);
        Close(connfd);
    }
    exit(0);
}

핵심 특징

1. sockaddr_storage 사용

struct sockaddr_storage clientaddr;
  • 목적: 모든 유형의 소켓 주소를 저장할 수 있는 충분한 공간
  • 장점: 프로토콜 독립적 (IPv4, IPv6 모두 지원)
  • 기존 방식: struct sockaddr_in은 IPv4만 지원

2. 클라이언트 정보 출력

Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
            client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
  • 연결된 클라이언트의 호스트명과 포트 출력
  • 디버깅과 로깅에 유용

3. 반복적 서버 (Iterative Server)

while (1) {
    connfd = Accept(listenfd, ...);
    echo(connfd);           // 한 번에 하나의 클라이언트만 처리
    Close(connfd);
}
  • 특징: 한 번에 하나의 클라이언트만 처리
  • 장점: 구현이 간단
  • 단점: 동시 다중 클라이언트 처리 불가

4. 연결 생명주기

  1. Accept: 클라이언트 연결 수락
  2. Echo: 클라이언트 서비스 제공
  3. Close: 연결 종료
  4. Repeat: 다음 클라이언트 대기

Echo 함수

Echo 함수 코드

#include "csapp.h"

void echo(int connfd)
{
    size_t n;
    char buf[MAXLINE];
    rio_t rio;

    Rio_readinitb(&rio, connfd);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
        printf("server received %d bytes\n", (int)n);
        Rio_writen(connfd, buf, n);
    }
}

동작 과정

1. 초기화

Rio_readinitb(&rio, connfd);
  • 연결 디스크립터에 대한 버퍼링된 읽기 초기화

2. 메인 루프

while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {
    printf("server received %d bytes\n", (int)n);
    Rio_writen(connfd, buf, n);
}
  • 읽기: 클라이언트로부터 한 줄씩 읽기
  • 출력: 받은 바이트 수 표시 (디버깅용)
  • 쓰기: 받은 내용을 그대로 클라이언트에게 전송

3. 종료 조건

  • Rio_readlineb()가 0 반환 (EOF 감지)
  • 클라이언트가 연결을 닫았을 때

실행 흐름

서버 실행
클라이언트 실행(telnet)
tcpdump 패킷 확인

  • 3 way-handshake 확인
  • 문자열 주고 받는 패킷 확인

위 hello를 입력하면 서버에서 7 Byte를 받은 이유는
hello(문자열 5) + \r\n(캐리지 리턴 + 개행 문자 = 2) = 7 Byte 이기 때문이다.


0개의 댓글