이제 서버구현이다... 서버 구현이 진짜 겁나게 재밋다.
클라이언트에 대한 설명에서 빠진 부분이 있어서 채워넣으려고한다.
그럼 레쓰고...

소켓 네트워크 기반 네트워크에서 해당 흐름으로 이어진다. 이전 포스팅에서 구현했던 client 메인 함수를 잘 기억해 보면 흐름이 이렇다.
getaddrinfo함수에서 필요 시 DNS를 통해 hostname을 IP 주소로 변환하고,
변환된 정보를 바탕으로 연결 가능한 주소 리스트(addrinfo 구조체)를 생성한 후 그 포인터를 반환한다.- 클라이언트는 반환된 주소 리스트를 순회하며, 각 주소 정보에 대해
socket함수를 호출해 소켓 파일 디스크립터(clientfd)를 생성한다.- 생성된 소켓에 대해
connect()함수를 호출함으로써, 상대방 주소로TCP연결을 시도한다. !- 이후
rio_writlen,rio_readlineb함수를 사용하여, 사용자 버퍼에 값을 쓴 후
해당 값을 커널 버퍼에 밀어넣는 과정과, 커널 버퍼의 값을 사용자 버퍼로 가져온 후 읽어오는 과정을 한다 !
이런 흐름인데, 전 포스팅에서 공부하며 포스팅하다보니 완전 뒤죽박죽이 되어있었다...
곧 수정해야지...
그럼 이제 서버를 구현해야하니 함수를 하나하나 파해쳐 보자...
클라이언트와 비슷하지만, 많이 다르다.
int listenfd, connfd; // 서버 리슨 소켓 및 연결 소켓 (클라이언트마다 새로 생성)
socklen_t clientlen; // 클라이언트 주소 구조체의 크기 (accept에 사용됨)
// 클라이언트 주소를 저장할 구조체 (IPv4/IPv6 모두 호환 가능)
// IPv6, IPv4 모두 호환하는 범용 구조체 사용
struct sockaddr_storage clientaddr;
// 클라이언트 호스트 이름, 포트 번호 저장용 문자열 버퍼
char client_hostname [MAXLINE], client_port[MAXLINE];
먼저 위에서 보여준 대로
서버를 구현할 때는listenfd와connfd의 소켓 파일 디스크립터 두 개가 필요하다.
서버는 누구에게 요청이 올지 모르기 때문에 소켓 파일을 두 개 사용해야한다.
하나는 자신의 정보를 넣어놓고 상대의 정보는 비어놓은 소켓
다른 하나는 나에게 연결 요청이 온 소켓을 따로 저장해야 놓기 떄문이다.

위 사진처럼 listenfd로 기다리고 있다가 요청이 온다면, connfd로 소켓 통신을 하도록 만드는 것이다 ! 이런식으로 구성하면, 일대다로 통신하는 웹서버 특성 상 많은 요청에도 유연하게 대처할 수 있을 것 같다.
우리는
IPv4,IPv6둘 중 어떤 요청이 올지 모른다.
따라서 두 요청 모두 호환하는 양식인socket.h에서 제공하는
sockaddr_storage구조체를 사용하여 정보를 입력한다 !
다른 변수들은 클라이언트에 대한 정보들을 담은 후
나중에 Accept함수에 인자로 넘겨주기 위한 변수들이다.
if(argc != 2) { // 명령행 인자는 프로그램명 + 포트번호여야 하므로 argc == 2 검사
fprintf(stderr, "usage : %s <port>\n", argv[0]);
exit(0);
}
// 지정된 포트 번호로 리슨 소켓을 열고, 연결 대기 상태로 진입
listenfd = Open_listenfd(argv[1]); // 입력한 포트 번호로 소켓 구성
서버를 열기 전 설정된 인자들을 확인 후 준비하는 과정이다.
먼저 서버를 열기 위해서는
ip 주소 + port 인자가 필요한데, 해당 인자를 제대로 넘기지 않았을 때
예외 처리를 하는 과정을 거친 후 제대로 입력이 되었다면
Open_listenfd로 두번째 인자인 port를 이용하여 소켓을 구성한다.
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 */
//null 인자로 와일드 카드로 설정하여 모든 인자에 대해 바인드가능 주소 구조체를 rc에 반환
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;
}
해당 함수로 들어가면 이전
open_clientfd함수보다 복잡하다...
천천히 알아보면 조금 다른 점은bind함수와 추가 되었고,getaddrinfo함수의host인자가NULL로 되어있다.
이때NULL로 해주는 이유는 서버는 모든ip에 대해서 요청을 받아야하기 때문에port만 지정하여 열어 준 후, 그곳에 오는 모든ip에 대한 요청을 받게 해주기 위해서 이다.
1. 먼저 모든ip에 대한 요청을 받을listen 소켓을 만들어주고
2. 해당 추상 소켓을bind함수를 통해 지정 포트에 대한 모든IP에 대한 요청을 허용하도록 열어준다.
3. 그리고listen함수를 호출하여 해당bind된 소켓을수동 대기상태로 만들고, 커널이 클라이언트 요청을 쌓아 놓을 수 있는 최대 큐 개수를 정의해준다.
-> 이때LISTENQ는 SYN-RECV 상태 연결 요청을 커널이 대기시킬 수 있는 최대 개수
4. 그리고수동 대기상태가 된 소켓의 디스크립터를 반환한다 !
addrinfo에 대한 설명이 빠졌는데도 이렇게 복잡하다...
하지만 나는 이해했지롱 ㅋ
while(1) {
clientlen = sizeof(struct sockaddr_storage); // 초기화 (매번 필요)
// 클라이언트가 연결 요청 → 수락하고 새로운 연결 소켓 반환
// 타입 별칭 SA 사용해서 sockaddr 인자 넘기기
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);
이제 드디어 서버를 가동해서 서로 인자를 주고받는 부분이다 !
웹 서버를 구현하며 느낀 점은 커널은 신이다...
커널 함수를 딸깍 하면, 뭔가 그 친구가 네트워크 부분을 다 담당해주고,
우리는 받은 인자로 그냥 처리하면 되는 느낌이였다. 그러니까 쫄지마라 커널이 해줄꺼야...
Accept함수를 사용하여, 클라이언트를 위해 만들었던수동대기 상태패킷과 인자를 넘긴다.
- 커널은 클라이언트에 대한 정보를 채운 후에 클라이언트 연결 전용 소켓인
connfd를 반환해준다.- 해당 클라이언트에 대한
로그를 남기기위해Getnameinfo를 사용하여 클라이언트에 대한 정보를 가져온다.- 그 후
echo함수를 실행하여, 클라이언트와echo통신을 진행한다 !
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);
}
}
이 함수는 client에서 구현한 echo와 비슷하지만 좀 다르다.
Rio_readinitb함수로connfd소켓에 대한 요청을 받을 버퍼를 생성한다.Rio_readlineb함수로 커널 버퍼에 쌓인 요청들을 사용자 공간 버퍼로read한다.- 이 후
byte수 를 로그로 찍은 후에- 방금 전 읽어온 요청을 커널 버퍼로 전달 한다 !
이렇게 Echo 서버 구현을 완료하였다...
지금 구현한 것은 한 명의 클라이언트만 받을 수 있는 구조이기 때문에
나중에 fork를 사용하여 프로세스를 분리한다면, 많은 클라이언트와 통신 할 수 있도록 업그레이드도 가능 할 것 같다..
일단 구현해 봤으니까 맛은 봐야겠지 !

지금까지 했던거 중에 제일 재밌다....!!
다음엔 tiny웹 서버를 만들어볼 예정이다... 화...화이팅 !
밤샜어?