socket 통신을 하는 간단한 서버, 클라이언트를 구현하기 위한 실습이다.
클라이언트의 어떤 요청이 있을 때, 서버가 어떻게 응답할 지를 정의한다.
echo는 메아리다. 클라이언트의 메시지를 동일하게 돌려준다.
미리 결과를 보자면 다음과 같다.

이렇게 동작하는 프로그램을 만들 것이다.
서버가 클라이언트와 연결된 소켓을 통해 입력받은 데이터를 그대로 다시 돌려주는 echo(connfd) 함수이다.
클라이언트의 요청에 따른 서버의 동작을 정의해둔 함수라고 보면 된다.
/*
* echo - read and echo text lines until client closes connection
*/
/* $begin echo */
#include "csapp.h"
void echo(int connfd) // 클라이언트와 연결 완료된 소켓 디스크립터
{
size_t n;
char buf[MAXLINE];
rio_t rio; // Robust I/O를 위한 구조체(커널 버퍼 + 사용자 버퍼 사이 관리)
Rio_readinitb(&rio, connfd); // connfd 소켓을 rio와 연결하여 입출력 구조체를 사용할 수 있도록 한다.
while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) // 클라이언트로부터 받은 데이터를 한 줄씩 읽는다.
{
printf("server received %d bytes\n", (int)n); // 서버가 받은 데이터의 bytes 단위 사이즈
Rio_writen(connfd, buf, n); // 클라이언트로 받은 버퍼와 동일한 버퍼를 response로 전송한다.
}
}
/* $end echo */
echo를 수행할 server 프로그램이다.
argv[1])로 받는다./*
* echoserveri.c - An iterative echo server
*/
/* $begin echoserverimain */
#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]; // 연결될 클라이언트의 host(IP 혹은 도메인), port를 담는다.
if (argc != 2) // 인자료 포트 번호를 요구한다.
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
// getaddrinfo(), socket(), bind(), listen() 과정이 포함된다.
listenfd = Open_listenfd(argv[1]); // 서버의 소켓 디스크립터를 만들고 + 주소 바인딩 + 클라이언트의 요청을 기다린다.
while (1)
{
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); // 클라이언트의 연결 요청을 수락한다.(connfd→클라이언트와의 통신용 소켓)
Getnameinfo((SA *)&clientaddr, clientlen, client_hostname, MAXLINE, // 연결된 클라이언트의 호스트 이름과 포트를 출력
client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
echo(connfd); // 클라이언트 요청에 대한 처리를 echo로 수행한다.
Close(connfd); // 종료
}
exit(0);
}
/* $end echoserverimain */
+) 더하여, Open_listenfd 함수가 어떻게 생겨 먹었는 지도 훑어보자.
서버는 이 함수를 호출해서 연결 요청을 받을 준비가 된 듣기 식별자(bind + listen)를 생성한다.
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval = 1;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0)
{
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next)
{
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, // line:netp:csapp:setsockopt
(const void *)&optval, sizeof(int));
/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
if (close(listenfd) < 0)
{ /* Bind failed, try the next */
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* No address worked */
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0)
{
close(listenfd);
return -1;
}
return listenfd;
}
echo를 수행할 TCP client 프로그램을 정의한다.
argv[1]), 포트 번호(argv[2])를 인자로 받는다./*
* echoclient.c - An echo client
*/
/* $begin echoclientmain */
#include "csapp.h"
// 프로그램은 호스트 주소와 포트 번호를 인자로 받는다.
int main(int argc, char **argv)
{
int clientfd; // 서버와 연결될 소캣 식별자가 담긴다.
char *host, *port, buf[MAXLINE]; // 연결할 서버의 host, port, 입출력 버퍼가 담긴다.
rio_t rio; // Robust I/O를 위한 구조체(커널 버퍼 + 사용자 버퍼 사이 관리)
if (argc != 3) // 요구하는 인자를 다 받지 못했을 경우 에러 처리
{
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1]; // 첫 번째 인자는 연결하는 서버의 host IP 주소(혹은 도메인)를 의미한다.
port = argv[2]; // 두 번째 인자는 연결하는 서버의 포트를 의미한다.
// socket(), getaddrinfo(), connect() 과정이 포함된다.
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); // line:netp:echoclient:close // 연결 종료
exit(0);
}
/* $end echoclientmain */
+) 더하여 open_clientfd() 함수도 살펴보자.
이 함수는 연결하고자 하는 서버의 host, port를 인자로 받아, 클라이언트 소켓을 생성하고 연결된 소켓을 반환한다.
int open_clientfd(char *hostname, char *port)
{
int clientfd, rc;
struct addrinfo hints, *listp, *p;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0)
{
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next)
{
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
if (close(clientfd) < 0)
{ /* Connect failed, try another */ // line:netp:openclientfd:closefd
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}