server
- server에서 getaddrinfo 함수를 호출하면 IP 주소와 Port에 대한 정보를 가져와서 socket을 만든다.
- socket을 만들고 bind를 통해서 만들어진 socket을 가지고 listen을 한다.
- listen은 server가 client로부터 오는 요청을 받겠다는 상황이다.
- client가 connection request를 하게 되면 accept 함수를 통해서 connection을 설정한다.
- accept을 하면 coonection이 설정된다.
- client와 connection이 만들어져서 메시지를 주고받을 수 있다.
client
- client는 getaddrinfo를 통해서 IP 주소와 Port에 대한 정보로 socket을 만들고,
- connect 함수를 통해서 만든 socket을 통해서 connection request를 보낸다.
- server에서 connection request를 Kernel network stack에 있는 TCP 매니저를 통해서 이 Connection을 queue 한다.
- queue 하고 dequeue 하여 accept 하게 된다. 그다음 accept 되면 client와 server가 connection이 build 된다.
- 이 상황에서 accept 한 다음에 server는 기다린다.
- readlineb-> 데이터가 오기를 기다리고 있다.
- client는 connection이 설정되었으니 그때 write 한다.
- write 하여 server에 메시지가 전송되고 server에서 메시지를 읽고 socket을 통해서 쓴다.
- 다음에 server는 다시 메시지가 오는 것을 기다리려고 loop를 진행한다.
- server에서 client로 메시지가 전송된다.
- client에서 메시지를 쓰고 server에서 데이터가 오기를 기다린다.
- 다음에 메시지가 다시 돌아오면 읽고 다시 쓴다.
- 이 과정을 반복한다.
- client가 connection을 끊게 되면 server에서 이 connection을 끊는다.
- 이 상황에서 다른 client가 요청을 하게 된다면 그 client는 계속 기다려야 한다.
The socket Function
client와 server는 socket 함수를 사용하여 socket descriptor를 생성할 수 있다.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Returns: nonnegative descriptor if OK, −1 on error
socket이 connection의 end-point가 되도록 하려면, socket 함수를 아래와 같은 인자로 호출하면 된다.
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
- AF_INET: 32비트 IP 주소를 사용하고 있음을 나타낸다.
- SOCK_STREAM: socket이 connection의 end-point가 될 것임을 나타낸다.
이보다 더 나은 방법은 getaddrinfo 함수를 사용하여 이러한 매개 변수를 자동으로 생성하여 코드가 프로토콜에 의존하지 않도록 하는 것이다.
socket에 의해 반환된 clientfd descriptor는 open만 되고 아직 read/write를 사용할 수 없다.
socket을 open 하는 방법은 client인지 server인지에 따라 달라진다.
The bind Function
bind 함수는 server의 socket 주소를 socket descriptor socketfd와 연결하도록 kernel에 요청한다.
#include <sys/socket.h>
int bind (int sockfd,
const struct sockaddr *addr,
socklen_t addrlen);
Returns: 0 if OK, −1 on error
서버 프로세스는 descriptor sockfd에서 read 하여 end-point가 addr인 connection에 도착하는 bytes를 읽을 수 있다.
마찬가지로, sockfd에 대한 write는 end-point이 addr인 connection을 따라 전송된다.
다음으로 server는 sockfd를 listen 한다.
The listen Function
client는 connection request를 시작하는 active entity이다.
server는 client의 connection request를 기다리는 passive entity이다.
기본적으로 kernel은 socket function에 의해 생성된 descriptor가 connection의 client end에 존재하는 active socket에 해당한다고 가정한다.
server는 listen function을 호출하여 client 대신 server가 descriptor를 사용할 것임을 kernel에 알린다.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
Returns: 0 if OK, −1 on error
listen function은 active socket에서 client의 connection request를 accept 할 수 있는 listening socket으로 변환한다.
backlog는 kernel이 request를 거부하기 전에 대기열에서 처리되지 않은 connection request의 수에 대한 hint다.
backlog의 일반적으로 1024 같은 큰 값으로 설정한다.
The accept Function
server는 accept 함수를 호출하여 client의 connection request를 기다린다.
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
Returns: nonnegative connected descriptor if OK, −1 on error
accept 함수는
- client의 connection request이 listening descriptor인 listenfd에 도착할 때까지 기다린다.
- client의 socket 주소를 addr에 입력한다.
- Unix I/O 함수를 사용하여 client와 통신하는 데 사용할 수 있는 connected descriptor를 반환한다.
The connect Function
client는 connet 함수를 호출하여 server와 연결을 설정할 수 있다.
#include <sys/socket.h>
int connect(int clientfd, const struct sockaddr *addr,
socklen_t addrlen);
Returns: 0 if OK, −1 on error
connet 함수는 server의 socket 주소인 addr와 Internet 연결을 설정하려고 시도한다. addrlen은 socketaddr_in의 크기다.
connet 함수는 connetion이 성공적으로 설정되거나 오류가 발생할 때까지 block 한다.
연결에 성공하면 clientfd가 read/write 할 준비가 되고, 연결은 socket pair로 특정지 어진다.
(x:y, addr.sin_addr:addr.sin_port)
여기서 x는 client의 IP 주소이고, y는 client host에서 client process를 고유하게 식별하는 임시 port이다.
Connected vs. Listening Descriptors
The roles of the listening and connected descriptors
Distinction between a listening descriptor and a connected descriptor
listening descriptor는 client connection request의 end-point 역할을 한다.
일반적으로 한 번 생성되며 server가 종료될 때까지 존재한다.
connected descriptor는 client와 server 간에 설정된 connection의 end-point 역할을 한다.
server가 connection request를 accept 할 때마다 생성되며, server가 client를 service 하는 동안에만 존재한다.
Why the distinction?
많은 client 연결을 동시에 처리할 수 있는 concurrent server를 구축할 수 있기 때문에 매우 유용하다.
예를 들어 connection request가 listening descriptor에 도착할 때마다 connected descriptor를 통해 client와 통신하는 새 프로세스를 fork 할 수 있다.
The open_clientfd Function
client는 open_clientfd 함수를 호출하여 server와 connection을 설정한다.
#include "csapp.h"
int open_clientfd(char *hostname, char *port);
Returns: descriptor if OK, −1 on error
open_clientfd 함수는 호스트 이름에서 실행되고 port에서 연결 요청을 listening server와 connection을 설정한다.
Unix I/O 함수를 사용하여 입출력 준비가 된 open socket descriptor를 반환한다.
Example
int open_clientfd(char *hostname, char *port) {
int clientfd;
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 */
Getaddrinfo(hostname, port, &hints, &listp);
/* 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 */
Close(clientfd);
/* Connect failed, try another */
}
/* Clean up */
Freeaddrinfo(listp);
if (!p)
/* All connects failed */
return -1;
else
/* The last connect succeeded */
return clientfd;
}
- addrinfo 구조체의 list를 반환하는 getaddrinfo 함수를 호출한다.
- list의 구조체들은 호스트 이름에서 실행 중이고 해당하는 port에서 listening 하고 있는 connection을 설정하기에 적절한 socket address 구조체를 가리킨다.
- 그런 다음 socket과 connet에 대한 호출이 성공할 때까지 list를 순회하며 각 list와 connect를 시도한다.
- connect에 실패하면 다음 entry를 시도하기 전에 socket descriptor를 닫아야 한다.
- connect에 성공하면 list를 free 하고 socket descriptor를 client에 반환한다.
- 성공 후 client는 Unix I/O를 사용하여 server와 통신할 수 있다.
코드에서 특정 버전의 IP에 의존하지 않는 것을 기억해야 한다.
socket과 connet 인자들은 getaddrinfo에 의해 자동으로 생성되며, 이를 통해 코드를 간결하게 만들 수 있다.
The open_listenfd Function
server는 open_listenfd 함수를 호출함으로써 connection request를 받을 준비가 된 listening descriptor를 생성한다.
#include "csapp.h"
int open_listenfd(char *port);
Returns: descriptor if OK, −1 on error
open_listenfd 함수는 port에서 connection request를 받을 준비가 된 listening descriptor를 반환한다.
Example
int open_listenfd(char * port) {
struct addrinfo hints, * listp, * p;
int listenfd, 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 */
Getaddrinfo(NULL, port, &hints, &listp);
/* 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,
(const void *) &optval,
sizeof(int)
);
/* Bind the descriptor to the address */
if (bind(listenfd, p -> ai_addr, p -> ai_addrlen) == 0)
break;/* Success */
Close(listenfd);/* Bind failed, try the next */
}
/* 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_cilentfd 함수와 비슷하다.
- getaddrinfo 함수를 호출하고 socket과 bind 함수가 성공할 때까지 호출하며 list를 순회한다.
- setsocket 함수는 종료, 재시작 또는 connection request를 즉시 받을 수 있도록 server를 설정하는 함수이다.
- 기본적으로 재시작된 server는 connection request를 약 30초 동안 거부한다. 이것은 디버깅을 어렵게 한다.
- AI_PASSIVE flag, NULL host를 인자로 getaddrinfo 함수를 호출했기 때문에 각 socket 주소 구조체의 주소 필드는
server가 호스트의 IP 주소 중 하나의 request를 accept 할 것이라는 것을 kernel에 알리는 wildcard 주소로 설정된다. - 마지막으로, listenfd를 listening descriptor로 변환하고 caller에게 반환하기 위해 listenfd 함수를 호출한다.
listen이 실패할 경우 반환하기 전에 descriptor를 닫아 memory leak을 방지한다.
Key point: open_clientfd and open_listenfd are both independent of any particular version of IP.
Example Echo Client and Server
Echo Client main routine
#include "csapp.h"
int main(int argc, char **argv) {
int clientfd;
char *host,
*port,
buf[MAXLINE];
rio_t rio;
if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1];
port = argv[2];
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);
exit(0);
}
- server와 연결을 설정하고 난 뒤 client는 loop에 진입한다.
loop는 standard input에서 text line을 반복하여 읽고
server에 text line을 보내고
server로부터 echo line을 읽고
standard output에 결과를 출력한다. - loop는 fgets 함수가 EOF에 닿거나 ctrl + D를 만나면 종료된다.
- loop가 종료되고 client는 descriptor를 close 한다.
- 이로 인해 server에 EOF 알림이 전송되고, 이 알림은 rio_readlineb 함수로부터 return 코드가 0인 경우 detect 한다.
- descriptor를 close 한 뒤 client는 종료된다.
Iterative echo server main routine.
#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];
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(connfd);
Close(connfd);
}
exit(0);
}
- listeing descriptor를 open 한 뒤, loop에 진입한다.
- 각 반복은 client의 connection request를 기다린다.
연결된 client의 도메이 이름, port를 print 한 뒤 다음 client를 service 하는 echo 함수를 호출한다. - echo routine이 return 되면, main routine이 connected descriptor를 close 한다.
- client와 server가 각각의 descriptor를 close 하면 연결이 종료된다.
clientaddr 변수는 accept로 전달되는 socket address 구조체이다.
acppet가 반환되기 전에 clientaddr을 연결의 다른 끝에 있는 client의 socket 주소로 채운다.
이 예제는 한 번에 한 개의 client만 다룰 수 있다.