-
[SW 정글 48일차] 소켓 인터페이스 구현부터 Echo 클라이언트와 서버까지기타/SW 사관학교 정글 2021. 9. 19. 02:39
오늘은 본격적으로 socket 인터페이스를 구현하여 echo 클라이언트와 echo 서버를 통해 네트워크 실습을 해보았다.
소켓 인터페이스에는 각자의 역할을 하는 다양한 함수가 있고 그 함수들을 하나로 묶어 효율적인 기능을 해주는 open_cliented()와 open_listenfd()가 있다.
이러한 소켓 인터페이스를 기반으로 클라이언트와 서버를 만들어 실습을 직접 진행해보니 신기했다.
1. 소켓 주소 구조체
/* IP socket address structure*/ struct sockaddr_in { uint16_t sin_family uint16_t sin_port struct in_addr sin_addr unsigned char sin_zero[8] } /* Generic socket address structure (for connect, bind, and accept) */ struct sockaddr { uint16_t sa_family; char sa_data[14]; }
sockaddr_in 구조체는 IP(Internet Protocol)을 사용하는 소켓을 위한 구조체이다.
총 16바이트로 주소 체계를 알려주는 sin_family(항상 AF_INET임), port number를 가지는 sin_port, IP주소를 가지고 있는 sin_addr, 아래에서 설명할 sockaddr 구조체와 크기를 맞쳐주기 위해 존재하는 sin_zero[8]로 이루어져있다.
sockaddr 구조체는 IP이외에 많은 프로토콜들이 존재하는데 connect, bind, accept 함수에서 모든 프로토콜들에 대한 소켓을 수용할 수 있는 generic한 소켓 구조체이다.
총 16바이트로 주소 체계를 알려주는 sin_family와 주소에 대한 데이터를 담고 있는 sa_data[14]로 이루어져있다.
2. socket 함수
#include <sys/types.h> #include <sys/socket.h> int socket(int domain, int type, int protocol);
클라이언트와 서버는 근본적으로 소켓을 통한 통신을 위해 소켓을 가져야하고 socket함수를 이용해서 각자의 소켓을 생성하여 소켓 식별자를 받을 수 있다.
이 함수를 통해 얻은 소켓 식별자는 이제 소켓 인터페이스 구현의 시작일 뿐이고 그냥 소켓이라는 빈 통을 얻은 것이라고 생각하면 된다.
3. connect 함수
#include <sys/socket.h> int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
connect 함수는 클라이언트가 서버와의 연결을 하고자 할 때 쓰이는 함수이다.
인자로 자신의 소켓 식별자(socket 함수로 얻은 값), 서버의 소켓 구조체 주소, 소켓 길이가 있다.
connect 함수는 연결이 성공하기 전까지는 블록되어 있거나 에러가 발생한다.
만약 성공됐다면 clinetfd(클라이언트의 소켓)은 읽거나 쓸 준비가 된 것이다.
4. bind, listen, accept 함수
/* bind */ #include <sys.socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); /* listen */ #include <sys/socket.h> int listen(int sockfd, int backlog); /* accept */ #include <sys/socket.h> int accept(int listenfd, struct sockaddr *addr, int *addrlen);
위 3개의 함수는 서버 측에서 쓰이는 함수이다.
bind 함수는 커널에게 socket함수로 얻은 소켓에 서버의 주소와 포트번호를 연결하라고 물어보는 역할을 한다.
listen 함수는 서버 측의 소켓이 클라이언트로부터의 연결 요청을 수락할 수 있는 상태로 변환하는 역할을 한다.
accept 함수는 연결가능상태(listen)인 서버 소켓에게 들어온 클라이언트의 연결 요청을 승락하는 역할을 한다.
5. getaddinfo 함수와 getnamein 함수
/* getaddinfo 함수 */ #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result); /* 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);
getaddinfo함수는 호스트 이름, 호스트 주소, 서비스 이름, 포트 번호의 스트링 표시를 수켓 주소 구조체로 변환해주는 역할을 한다.
호스트명과 포트번호를 인자로 주면 그에 대응하는 소켓 주소 구조체를 가리키는 addrinfo구조체의 연결리스트를 가리키를 result를 반환한다.
즉, 위에 사진처럼 연결리스트가 형성되고 위 연결리스트를 가리키는 주소가 반환된다는 것이다.
연결리스트의 각각의 구초제인 addrinfo는 아래와 같이 이루어져 있다.
struct addinfo { 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_cannoname; /* Canonical hostname */ size_t ai_addrlen; /* Size of ai_addr struct */ struct sockaddr *ai_addr; /* pointer to socket address structure */ struct addrinfo *ai_next; /* pointer to next item in linked list */ }
getaddrinfo함수에서 host 인자는 도메인 이름 혹은 숫자 주소(xxx.xxx.xxx.xxx)일 수 있고 service 인자는 서비스이름(http, ftp...) 혹은 십진수 포트번호일 수 있다.
host와 service NULL로 값을 줄 수도 있는데 둘 다 NULL로 주는 것은 불가능하다.
getnameinfo 함수는 소켓 주소 구조체의 호스트명과 서비스이름을 스트링으로 변환하는 함수이다.
호스트이름은 원치 않는다면 host 인자에 NULL을 주고 hostlen을 0으로 설정하면 된다.
서비스이름도 마찬가지인데 둘 다 NULL로 주는 것은 불가능하다.
6. open_clientfd 함수
/* $begin open_clientfd */ 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; }
open_clientfd함수를 쉽게 설명하면 위에서 얘기한 클라이언트측의 getaddrinfo, socket, connect 함수의 기능을 모두 수행하는 함수이다.
인자로 연결하고자하는 서버의 hostname과 port number를 주면 해당 서버의 port에서 연결 요청을 듣고 있는 서버와 연결을 설정한다.
함수 내부를 보면 getaddrinfo함수를 통해 hostname과 port number에 맞는 addrinfo 구조체를 가지는 연결리스트를 얻어온다.
그 후에 반복문으로 연결리스트에 들어있는 구조체를 보며 connect를 시도한다.
connect에 실패하면 다음 항목을 시도하기 전에 클라이언트의 소켓 식별자를 close하고
connect에 성공하면 getaddrinfo를 통해 얻은 연결리스트를 담고 있던 메모리를 반납하고 서버와 통신을 시작할 수 있도록 한다.
7. open_listenfd 함수
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; }
open_listenfd 함수는 서버 측에서 getaddrinfo, socket, bind, listen 함수의 기능을 수행하는 역할을 한다.
open_listenfd 함수를 호출하여 클라이언트로부터의 연결요청을 받을 준비가 된 listen descriptor를 생성한다.
인자로는 port number를 주어 해당 port number로 연결 요청을 받을 준비를 하는 것이다.
open_clientfd함수와 다른 부분은 getaddrinfo함수의 인자에 host를 NULL로 넣어준 것이다.
NULL을 넣어주는 의미는 각 소켓 주소 구조체의 주소 필드가 와일드 카드 주소로 설정되어 커널에게 서버가 이 호스트에 대한 모든 IP 주소에 대해 요청을 받을 것이라고 말해주는 것이다.
echo client와 echo server에 대한 코드는 아래 깃헙에 저장되어있다.
https://github.com/JJong-Min/webproxy/tree/main/socket_programming
위에서 설명한 open_clientfd와 open_listenfd함수 그리고 Rio 패키지를 이용하여 클라이언트 측에서 문자열을 보내면 서버에서 받아 문자열의 크기를 byte로 나타내준다.
[오늘의 나는 어땠을까?]
오늘은 정글에 들어오고나서 처음으로 운동을 한 날이다.
헬스장을 가서 한 것은 아니고 등산을 좋아하는 동료 2명과 함께 오저 8시 30분에 근처 산을 등산했다.
등, 하산 포함하면 10km정도 걸은 것같다.
오랜만에 운동이라는 것을 하니 스트레스도 풀리고 긴 시간동안 동료들과 얘기를 하며 걸으니 기분도 좋아졌다.
주기적으로 하고 싶지만 이제 정글에 있을 시간이 얼마 남지 않았고 그 시간들은 매우 소중하고 중요하기에 공부에 투자해야한다.
등산의 후유증은 저녁 이후부터 나타나기 시작했다.
양쪽 무릎과 종아리가 알이 베긴 듯 아프다...
그래도 좋은 경험이고 추억이기에 오늘 하루가 만족스럽고 오늘 목표로 한 부분까지 공부를 완료해서 하루에 대한 만족도는 높다.
내일 부터는 Tiny 구현을 시작할텐데 잘 이해하면서 술술 풀렸으면 좋겠다.
'기타 > SW 사관학교 정글' 카테고리의 다른 글
[SW 정글 49일차] Tiny 웹 서버 구현하기 (1) 2021.09.20 [SW 정글 추석 특집] Missing Semester 1일차 (0) 2021.09.19 [SW 정글 47일차] 소켓 (Socket) 입문 (0) 2021.09.18 [SW 정글 46일차] 네트워크 용어 익숙해지기 (0) 2021.09.17 SW 정글 6주차 회고 (0) 2021.09.16