
가장 기본적인 네트워크 애플리케이션 구조는 클라이언트-서버 모델입니다.
클라이언트와 서버 프로세스가 데이터를 주고받는 물리적인 경로와 기본적인 네트워크 구성 요소입니다.
데이터는 CPU에서 처리되어 메모리에 저장된 후, 시스템 버스, I/O 브릿지, I/O 버스를 거쳐 네트워크 어댑터(랜 카드)에 도달합니다. 어댑터는 데이터를 전기 또는 광 신호로 변환하여 네트워크 매체(케이블 등)를 통해 전송합니다. 수신 시에는 역순으로 진행됩니다.
서로 다른 네트워크를 거쳐 데이터가 전달되는 과정입니다.
send() 시스템 콜을 호출하면 데이터가 커널 버퍼로 복사됩니다.recv() 시스템 콜을 호출하면 이 데이터가 앱의 메모리로 복사됩니다. (역캡슐화)우리가 흔히 '인터넷'이라고 부르는 전 지구적인 네트워크입니다.
struct in_addr: C에서는 보통 이 구조체에 IP 주소를 저장합니다.struct in_addr {
uint32_t s_addr; /* 네트워크 바이트 순서(Big Endian)로 저장된 주소 */
};#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // Host to Network Long (32비트)
uint16_t htons(uint16_t hostshort); // Host to Network Short (16비트)
uint32_t ntohl(uint32_t netlong); // Network to Host Long (32비트)
uint16_t ntohs(uint16_t netshort); // Network to Host Short (16비트)hton 계열: 데이터를 보내거나 소켓 주소 구조체에 넣기 전에 사용.ntoh 계열: 받은 데이터를 프로그램에서 사용하기 전에 사용.128.2.194.242 같은 점으로 구분된 십진수 표기법을 사용합니다.#include <arpa/inet.h>
// 문자열 -> 바이너리 (네트워크 바이트 순서)
int inet_pton(AF_INET, const char *src, void *dst);
// 바이너리 (네트워크 바이트 순서) -> 문자열
const char *inet_ntop(AF_INET, const void *src, char *dst, socklen_t size);www.google.com과 같은 사람이 읽기 쉬운 도메인 이름을 사용합니다..com (최상위), google (2차), www (서브)).localhost: 항상 자신의 컴퓨터를 가리키는 특별한 이름으로, 보통 IP 주소 127.0.0.1에 매핑됩니다.IP 주소:포트 번호 (예: 128.2.194.242:80)로 구성됩니다.(클라이언트 IP:클라이언트 포트, 서버 IP:서버 포트) 형태의 튜플로 표현됩니다.(cliaddr:cliport, servaddr:servport)네트워크 애플리케이션을 만들기 위해 사용하는 함수들의 집합(API)입니다. UNIX I/O 함수(read, write, close 등)와 함께 사용됩니다.
sockaddr_in):struct sockaddr_in {
uint16_t sin_family; /* 주소 체계 (항상 AF_INET) */
uint16_t sin_port; /* 포트 번호 (네트워크 바이트 순서) */
struct in_addr sin_addr; /* IP 주소 (네트워크 바이트 순서) */
unsigned char sin_zero[8]; /* 사용되지 않음 (패딩) */
};sockaddr):struct sockaddr {
uint16_t sa_family; /* 주소 체계 */
char sa_data[14]; /* 주소 데이터 (IP 주소 및 포트 번호 포함) */
};sockaddr_in 구조체를 bind, connect, accept 같은 함수에 전달할 때는 (struct sockaddr *)로 타입 캐스팅합니다. 커널은 sa_family 필드를 보고 실제 구조체 타입을 파악합니다.read(), write(), close() 같은 표준 I/O 함수를 사용하여 데이터를 주고받을 수 있습니다.socket() 함수: 소켓 생성클라이언트와 서버 모두 통신을 시작하기 전에 소켓 디스크립터를 생성해야 합니다.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain: 프로토콜 체계 (e.g., AF_INET for IPv4).type: 소켓 타입 (e.g., SOCK_STREAM for TCP, SOCK_DGRAM for UDP).protocol: 특정 프로토콜 지정 (보통 0을 사용하여 domain과 type에 맞는 기본 프로토콜 자동 선택).int sockfd = socket(AF_INET, SOCK_STREAM, 0);connect() 함수: 클라이언트가 서버에 연결 요청클라이언트가 생성된 소켓을 사용하여 서버와의 연결을 시도합니다.
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
clientfd: socket()으로 생성한 클라이언트의 소켓 디스크립터.addr: 연결할 서버의 소켓 주소 정보를 담고 있는 구조체 포인터 (struct sockaddr_in을 struct sockaddr *로 캐스팅).addrlen: 서버 주소 구조체의 크기.clientfd는 데이터를 읽고 쓸 준비가 되며, 연결은 소켓 페어 (클라이언트 IP:자동 할당된 포트, 서버 IP:서버 포트)로 식별됩니다.bind(), listen(), accept()bind() 함수: 소켓에 주소 할당서버가 사용할 소켓 디스크립터에 자신의 IP 주소와 잘 알려진 포트 번호를 "묶는" 역할을 합니다.
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd: socket()으로 생성한 서버의 소켓 디스크립터.addr: 서버 자신의 IP 주소와 포트 번호를 담고 있는 소켓 주소 구조체 포인터 (struct sockaddr_in을 struct sockaddr *로 캐스팅). IP 주소 부분에 INADDR_ANY를 사용하면 커널이 알아서 적절한 호스트 IP를 사용합니다.addrlen: 서버 주소 구조체의 크기.sockfd로 전달되도록 합니다.listen() 함수: 소켓을 리스닝 상태로 전환서버가 클라이언트의 연결 요청을 받아들일 준비가 되었음을 커널에 알립니다. socket()으로 생성된 소켓은 기본적으로 클라이언트용(능동 소켓)으로 간주되므로, 서버는 이 함수를 호출하여 소켓을 서버용(수동 리스닝 소켓)으로 만들어야 합니다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
sockfd: bind()까지 완료된 서버의 소켓 디스크립터.backlog: 연결 요청 대기 큐(Queue)의 최대 크기. 아직 accept()되지 않은 연결 요청을 임시로 저장하는 공간입니다.sockfd는 이제 리스닝 소켓(Listening Socket)이 됩니다.accept() 함수: 클라이언트 연결 수락리스닝 소켓으로 들어오는 클라이언트의 연결 요청을 수락하고, 실제 통신에 사용할 새로운 소켓을 생성합니다.
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen);
listenfd: listen() 함수 호출로 만들어진 리스닝 소켓 디스크립터.addr: 연결 요청을 한 클라이언트의 주소 정보를 저장할 구조체 포인터.addrlen: addr 구조체의 크기를 저장할 변수의 포인터. 함수 호출 전에는 버퍼 크기를, 호출 후에는 실제 클라이언트 주소 구조체의 크기를 담습니다.addr에 채웁니다.listenfd): 클라이언트 연결 요청을 받는 용도. 서버가 실행되는 동안 계속 존재합니다. 이 디스크립터로 직접 데이터 통신을 하지는 않습니다.connfd, accept의 반환값): 특정 클라이언트와의 실제 데이터 통신에 사용되는 소켓. 각 클라이언트 연결마다 새로 생성되며, 해당 클라이언트와의 통신이 끝나면 close()됩니다.프로그램에서는 도메인 이름(www.example.com)이나 서비스 이름(http)을 사용하지만, 소켓 함수는 바이너리 IP 주소와 포트 번호가 필요합니다. 이 변환을 도와주는 함수들이 있습니다.
getaddrinfo(): 호스트 이름/주소 문자열, 서비스 이름/포트 문자열을 입력받아, 소켓 주소 구조체(struct sockaddr) 정보가 담긴 연결 리스트(linked list)를 반환합니다. 이 함수는 프로토콜(IPv4/IPv6) 독립적인 코드를 작성하는 데 매우 유용합니다.
#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);
void freeaddrinfo(struct addrinfo *res); // 결과 리스트 메모리 해제
const char *gai_strerror(int errcode); // 에러 코드 -> 에러 메시지 문자열
host: 호스트 이름 (e.g., "www.google.com") 또는 IP 주소 문자열 (e.g., "142.250.196.196").service: 서비스 이름 (e.g., "http") 또는 포트 번호 문자열 (e.g., "80").hints: 검색 조건을 지정하는 addrinfo 구조체 포인터 (e.g., 주소 체계, 소켓 타입 지정). NULL일 경우 기본값 사용.result: 결과를 저장할 addrinfo 구조체 포인터의 주소. 성공 시 이 포인터는 결과 연결 리스트의 시작을 가리킵니다.getaddrinfo 호출 -> 반환된 리스트 순회 -> 각 노드 정보로 socket() 시도 -> 성공하면 connect() (클라이언트) 또는 bind() (서버) 시도 -> 성공하면 루프 탈출, 실패하면 소켓 닫고 다음 노드 시도 -> 사용 후 freeaddrinfo로 메모리 해제.getnameinfo(): 소켓 주소 구조체(struct sockaddr)를 입력받아, 호스트 이름과 서비스 이름 문자열로 변환합니다. (역방향 변환)
#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);
sa: 변환할 소켓 주소 구조체 포인터.salen: sa 구조체의 크기.host, hostlen: 호스트 이름을 저장할 버퍼와 그 크기.service, servlen: 서비스 이름을 저장할 버퍼와 그 크기.flags: 동작을 제어하는 플래그 (e.g., NI_NUMERICHOST - DNS 조회 없이 IP 주소 문자열 반환, NI_NUMERICSERV - 서비스 이름 조회 없이 포트 번호 문자열 반환).CSAPP 라이브러리 등에서는 일반적인 클라이언트/서버 설정을 단순화하는 래퍼(wrapper) 함수를 제공합니다.
open_clientfd(): 클라이언트 측에서 getaddrinfo, socket, connect 과정을 캡슐화하여 지정된 호스트/포트로의 연결을 설정하고 연결된 소켓 디스크립터를 반환합니다.int open_clientfd(char *hostname, char *port);open_listenfd(): 서버 측에서 getaddrinfo, socket, bind, listen 과정을 캡슐화하여 리스닝 소켓 디스크립터를 생성하고 반환합니다.int open_listenfd(char *port);소켓 인터페이스 사용법을 보여주는 간단한 예제입니다. 클라이언트는 표준 입력에서 텍스트를 읽어 서버로 보내고, 서버는 받은 텍스트를 그대로 클라이언트에게 돌려보냅니다. 클라이언트는 서버로부터 받은 텍스트를 표준 출력에 표시합니다.
클라이언트 (주요 로직):
open_clientfd(host, port) 호출하여 서버에 연결하고 clientfd 얻기.Fgets).clientfd를 통해 서버로 전송 (Rio_writen).clientfd로부터 서버의 응답(에코) 읽기 (Rio_readlineb).Fputs).Close(clientfd) 호출하여 연결 닫기.서버 (주요 로직):
open_listenfd(port) 호출하여 리스닝 소켓 listenfd 얻기.Accept(listenfd, ...) 호출하여 클라이언트 연결 요청 대기 및 수락, 새로운 연결 소켓 connfd 얻기.Getnameinfo로 연결된 클라이언트 정보 얻기.echo(connfd) 함수 호출하여 클라이언트와 통신 처리.Close(connfd) 호출하여 해당 클라이언트와의 연결 닫기. (다음 클라이언트 요청 처리를 위해 listenfd는 계속 열려 있음).echo() 함수 (서버측):
connfd를 인자로 받음.connfd로부터 데이터 읽기 (Rio_readlineb). 읽은 바이트 수가 0이면 클라이언트가 연결을 닫은 것이므로 루프 종료.connfd로 다시 쓰기 (Rio_writen).커널의 역할: 클라이언트와 서버 프로세스는 소켓 API(시스템 콜)를 통해 커널에 요청합니다. 실제 TCP/IP 통신 처리(연결 설정, 데이터 패킷화, 전송, 수신, 오류 제어, 버퍼링 등)는 운영체제 커널이 담당합니다.

그림으로도 어렵다