echo 서버 구현 실습하기
소켓 프로그래밍의 기본 개념을 이해할 수 있었다면, 다음 목표는 echo 서버를 직접 구현해보는 것인데요. echo 서버는 서버-클라이언트 간 연결이 맺어진 후, 클라이언트의 입력을 받아 그대로 다시 클라이언트에게 전송해주는 구조입니다. 언뜻 보기엔 간단해 보이지만, 실제로는 연결과 통신 과정에서 복잡한 처리 로직이 필요하죠.
다만 기존 webproxy_lab에서는 echo 서버에 대한 별도 실습을 제공하지 않기 때문에, 직접 echo 서버 실습을 진행한 후 이를 실습용 폴더로 구성하여 더 편리하게 실습해 볼 수 있도록 공유해드리려고 합니다.
파일 다운로드 및 구성 파일 소개
우선 아래의 zip 파일을 다운로드해주세요.
그 다음, Docker 환경에서 동일하게 실행할 수 있도록 webproxy-lab 디렉터리 안에 압축을 해제하면 됩니다.
구성 파일 소개
echo_server.c
: 서버 코드 템플릿 (main, open_listenfd, echo 직접 구현)echo_client.c
: 클라이언트 코드 템플릿 (main, open_clientfd 직접 구현)csapp.c
/csapp.h
: 실습용 함수 모음 (기존의 open_clientfd, open_listenfd 등은 주석처리됨)Makefile
:make
로 빌드 가능 (echo_server
,echo_client
생성)
실습 방법
- CSAPP 11.4를 참고해 echo_server.c와 echo_client.c의 빈 함수들을 모두 구현한다.
- echo 디렉터리에서 make 명령어를 실행해 서버, 클라이언트 각각의 실행파일을 만든다.
- 터미널을 2개 연다. (서버용 / 클라이언트용)
- 터미널1에서 서버를 실행한다
./echo_server 12345
- 터미널2에서 클라이언트를 실행한다
./echo_client localhost 12345
- 이후 터미널2에서 메시지를 입력해서 동일한 문자가 출력되는지 확인한다.
- 윈도우 기준, 종료 시 Ctrl + D (클라이언트) / Ctrl + C (서버)
구현 코드 해석하기
open_clientfd 함수
주어진 호스트 이름과 포트 번호를 이용해 서버에 TCP 연결을 시도하고, 연결에 성공한 소켓 디스크립터를 반환합니다.
int open_clientfd(char *hostname, char *port) {
int clientfd; // 클라이언트 측 소켓 파일 디스크립터 (연결 성공 시 반환됨)
struct addrinfo hints, *listp, *p; // 주소 정보를 위한 구조체들
// 'hints' 구조체를 0으로 초기화 (초기값 설정)
memset(&hints, 0, sizeof(struct addrinfo));
// 연결 지향형 TCP 스트림을 원한다는 것을 명시
hints.ai_socktype = SOCK_STREAM;
// 포트 번호가 숫자임을 명시 (예: "80"은 숫자이므로 DNS 조회 필요 없음)
hints.ai_flags = AI_NUMERICSERV;
// 현재 시스템의 네트워크 환경에 맞는 주소만 조회
hints.ai_flags |= AI_ADDRCONFIG;
// hostname과 port에 해당하는 주소 정보를 가져옴
// listp는 여러 개의 주소 정보를 가리키는 연결 리스트의 시작점이 됨
Getaddrinfo(hostname, port, &hints, &listp);
// 주소 리스트를 돌면서 접속 시도
for (p = listp; p != NULL; 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);
}
// 주소 정보 해제 (메모리 반환)
Freeaddrinfo(listp);
// 연결이 실패한 경우(p == NULL)
if (p == NULL) {
return -1; // 모든 연결 실패
}
// 연결 성공한 경우: 해당 소켓 파일 디스크립터 반환
return clientfd;
}
open_listenfd 함수
주어진 포트 번호로 클라이언트 연결을 수신할 수 있는 TCP 서버 소켓을 생성 및 바인딩한 뒤, 대기 상태로 설정된 소켓 디스크립터를 반환합니다.
int open_listenfd(char *port) {
int listenfd, optval = 1; // 서버 소켓 디스크립터, 소켓 옵션 설정용 변수
struct addrinfo hints, *listp, *p; // 주소 정보 힌트와 결과 리스트 포인터들
// hints 초기화: 어떤 종류의 주소 정보를 원하는지 설정
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; // TCP(연결 지향형 스트림)
hints.ai_flags = AI_NUMERICSERV; // 포트가 숫자 문자열이라는 의미 ("80" 등)
hints.ai_flags |= AI_ADDRCONFIG; // 현재 시스템의 IPv4/IPv6 환경을 고려
// NULL을 넘겨서 "모든 IP 주소"에 바인딩 (서버의 모든 네트워크 인터페이스에서 접속 허용)
Getaddrinfo(NULL, port, &hints, &listp);
// 주소 리스트를 하나씩 돌면서 소켓 생성 + 바인딩 시도
for (p = listp; p != NULL; p = p->ai_next) {
// 소켓 생성
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) {
continue; // 실패 시 다음 주소 시도
}
// 포트 재사용 설정 (서버 재시작 시 "Address already in use" 방지)
Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (const void*)&optval, sizeof(int));
// 바인딩 시도 (서버 소켓에 IP/포트 연결)
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0) {
break; // 바인딩 성공
}
// 바인딩 실패 → 소켓 닫고 다음 주소 시도
Close(listenfd);
}
// 주소 리스트 메모리 해제
Freeaddrinfo(listp);
// 모든 바인딩 시도 실패
if (p == NULL) {
return -1;
}
// 클라이언트의 연결 요청을 대기 상태로 설정
if (listen(listenfd, LISTENQ) < 0) {
// 실패 시 소켓 닫고 -1 반환
Close(listenfd);
return -1;
}
// 성공 시, 클라이언트 연결을 받을 수 있는 서버 소켓 디스크립터 반환
return listenfd;
}
main 함수(echo_client)
사용자가 입력한 문자열을 서버에 전송하고, 그 응답을 받아 출력하는 TCP 클라이언트 프로그램의 기본 구조를 가지고 있습니다.
int main(int argc, char **argv) {
int clientfd; // 서버와 연결된 소켓 파일 디스크립터
char *host, *port, buf[MAXLINE]; // 서버 주소, 포트 번호, 메시지 저장 버퍼
rio_t rio; // Robust I/O를 위한 버퍼 구조체 (CSAPP 제공)
// 인자 개수 확인: 실행 시 반드시 <host> <port> 두 인자를 받아야 함
if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0); // 인자 부족 시 프로그램 종료
}
// 명령행 인자로부터 서버 주소와 포트 번호 가져오기
host = argv[1]; // 예: "localhost" 또는 "127.0.0.1"
port = argv[2]; // 예: "8080"
// 클라이언트 소켓을 열고 서버에 연결
clientfd = open_clientfd(host, port);
// Robust I/O(안정적인 입출력)를 위한 rio 버퍼 초기화
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);
}
main 함수(echo_server)
실행 시 주어진 포트로 TCP 서버를 열고 클라이언트의 연결을 무한히 수락하게 되는데요. 이후 연결된 클라이언트와 데이터를 주고받는 echo 서비스를 수행하는 간단한 서버 프로그램입니다.
int main(int argc, char **argv) {
int listenfd, connfd; // listenfd: 클라이언트 연결 요청을 수신하는 소켓
// connfd: 연결이 수락된 후 실제 데이터 통신에 사용되는 소켓
socklen_t clientlen; // 클라이언트 주소 구조체의 크기 (accept 시 필요)
struct sockaddr_storage clientaddr; // 클라이언트 주소 정보를 저장 (IPv4/IPv6 모두 대응)
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 함수 호출: 클라이언트와 데이터 송수신 처리 (클라이언트가 보낸 데이터를 그대로 돌려줌)
echo(connfd);
// 통신 종료 후 소켓 닫기
Close(connfd);
}
// 정상 종료 (사실상 도달하지 않음)
exit(0);
}
echo 함수
클라이언트로부터 한 줄씩 입력을 받아 그대로 다시 돌려주는 간단한 에코 기능을 수행하며, Robust I/O를 이용해 안정적인 데이터 입출력을 보장합니다.
void echo(int connfd) {
size_t n; // 읽어온 바이트 수
char buf[MAXLINE]; // 데이터를 읽고 쓸 버퍼
rio_t rio; // Robust I/O 구조체 (CSAPP에서 제공)
// Robust I/O 입력 버퍼 초기화 (connfd를 기반으로 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);
}
// 클라이언트가 EOF(연결 종료)를 보내면 반복문 탈출
}
'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글
[WebProxy] Tiny 서버 숙제 문제 분석 및 해결하기 (0) | 2025.05.03 |
---|---|
[WebProxy] Tiny 서버 구현 코드 해석 및 응답 확인하기 (0) | 2025.05.03 |
[WebProxy] VSCode에서 빨간 줄이 뜨는 현상 해결하기 (0) | 2025.05.02 |
[Malloc] (참고용) Segregated Free List 기반 동적 할당기 구현하기 (0) | 2025.04.28 |
[Malloc] Explicit Free List 기반 동적 할당기 구현하기 + 최적화하기 (0) | 2025.04.27 |