Post Lists

2018년 7월 27일 금요일

7. Slightly Advanced Techniques

http://beej.us/guide/bgnet/html/multi/advanced.html

7. Slightly Advanced Techniques
이것들은 실제로 고급은 아니지만, 우리가 이미 다루었던 기본 레벨에서 벗어나는 것ㅅ이다. 사실, 만약너가 이것을 멀리에서 봤었더라면, 너는 너 자신을 Unix network programming 기본에서 꽤 성취한 사람으로 봐야한다. 축하한다!

그래서 여기에서, 우리는 소켓에 대해서 너가 배우고 싶어할지도 모르는 좀 더 소수만 아는 것들의 용감한 새로운 세계에 들어간다.

7.1. Blocking
Blocking. 너는 그것을 들어왔다 - 이제, 그게 뭔데? 핵심만 말해서, "block"은 "sleep"을 위한 techie jargon이다. 너는 아마도, 너가 위에서 listener를 작동시킬 때, 그것이 그냥 패킷이 도착할 때까지 앉아 있는 것을 눈치챘었다. 발생한 것은 그것이 recvfrom()을 호출했다는 것이고, 어떠한 데이터가 없었고, 그래서 recvfrom()은 어떤 데이터가 도착할 때까지 "block"한다고 말해진다. (즉, 거기에서 sleep한다)

많은 함수들은 block한다. accept()는 block한다. 모든 recv() 함수들은 block한다. 그들이 이것을할 수 있는 이유는, 그것들이 그렇게 하도록 혀용되기 때문이다. 너가 처음에 socket descriptor를 socket()으로 만들 때, 커널은 그것에 blocking을 설정한다. 만약 너가 socket이 blocking 하는 것을 원치 않는다면, 너는 fcntl()를 호출해야만 한다.


1
2
3
4
5
#include <unistd.h>
#include <fcntl.h>

sockfd = socket(PF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK);

한 socket을 non-blocking으로 설정하여, 너는 효과적으로 정보를 위해 그소켓을  "poll"한다. (poll : check the status of (a device), especially as part of a repeated cycle.) 만약 너가 non-blocking socket으로부터 읽으려고 시도하고, 거기에 데이터가 없다면, block 하는 것이 허용되지 않는다. - 그것은 -1을 반환할 것이고, errno는 EAGAIN 또는 EWOULDBLOCK으로 설정될 것이다.

(잠깐 - 그것이 EAGAIN 또는 EWOULDBLOCK을 반환한다고? 너는 어떤 것을 체크해야 하는가? 그 명세는 실제로 너의 시스템이 어떤 것을 반환할지를 명시하지 않는다. 그래서 이식성을 위해, 그것들 둘 다 체크해라.)

일반적으로 말해서, 그러나, 이러한 polling의 유형은 나쁜 아이디어이다. 만약 너의 프로그램을 소켓에서 데이터를 찾으려고 바쁘게 기다리게 둔다면 (busy-wait), 너는 CPU 시간을 망칠것이다. 읽혀지기를 기다리는 데이터가 있는지를 보기위해 체크하는 좀 더 우아한 솔루션은 다음의 섹션 select()에서 온다.

7.2 select() - Synchronous I/O Multiplexing
이 함수는 어느정도 이상하지만, 매우 유용하다. 다음의 상황을 가져보자: 너는 서버이고, 너는 들어오는 연결을 listen하고 싶을 뿐만 아니라, 너가 이미 가진 connections들로부터 계속해서 read하고 싶어한다.

문제는 없다. 너는 그냥 accept()하고, 몇 개의 recv()를해라. 매우 빠르지는 않다. 너가 한 accept() call에 대해 blocking() 하면 어떨까? 어떻게 너는 동시에 data를 recv() 할 것인가? "non-blocking sockets를 사용해라!" 말도안돼! 너는 CPU 돼지를 원치 않는다. 그러고나서 뭐?

select()는 너에게 몇 가지 sockets을 동시에 감시하는 힘을 준다. 그것은 너에게 어떤 것이 reading할 준비가 되었는지 말해줄 것이고, 어떤 것이 writing할 준비가 되었는지, 그리고 sockets이 예외를 보냈는지를 말해줄 것이다. 만약 너가 정말 그것을 원한다면.

말해지고 있는 이것은 현대에서 select() 인데, 매우 portaable하지만, sockets을 감시하기에 가장 느린 방법들 중 하나이다. 한 가지 가능한 대안은 libevent이거나 또는 유사한 어떤 것이다. 이것은 소켓 알림을 얻는 것이 포함된 모든 시스템-의존 stuff를 캡슐화 한다.

더 나아가지 않고, 나는 select()에 대한 개요를 제공할 것이다:


1
2
3
4
5
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int numfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timecut);

그 함수는 file descriptors의 "sets"를 감시한다; 특정한 readfds, writefds, 그리고 exceptfds에서. 만약 너가 standard input과 몇몇 소켓 descriptor sockfd로부터 read할 수 있는지를 보길 원한다면, 그냥 file descriptors에 0을 추가하고, sockfd를 set readfds에 추가해라. 인자 numfds는 가장 높은 filedescriptor에 1을 더한 값으로 설정되어야 한다. 이 예제에서, 그것은 sockfd + 1로 설정되어야만한다. 그것이 확실히 standard input(0) 보다 더 높기 때문이다.

select()가 반환할 때, readfds는 너가 선택한 file descriptors들 중에 어떤 것이 reading에 대해 준비되어있는지를 반영하도록 수정될 것이다. 너는 그것들을 FD_ISSET()으로 아래에서 테스트 해볼 수 있다.

좀 더 나아가기 전에, 나는 이러한 세트들을 어떻게 조작하는지에 대해 말할 것이다. 각각의 세트는 fd_set type이다. 다음의 매크로들은 이 타입에 대해서 작동한다:


  • FD_SET(int fd, fd_set* set) : fd를 set에 추가
  • FD_CLR(int fd, fd_set* set) : set로부터 fd를 제거
  • FD_ISSET(int fd, fd_set* set) : 만약 fd가 set에 있으면 true를 반환.
  • FD_ZERO(fd_set* set) : set로부터 모든 요소들을 제거
마지막으로, 이 이상한 structtimeval은 무엇인가? 음, 가끔식 너는 누군가가 너에게 data를 보내기를 영원히 기다리길우너치않는다. 아마도 96초마다, 너는 "Still Going.."을 터미널에게 출력하길 원한다. 비록 어떠한 것이 발생하지 않을지라도. 이 번에, structure는 너가 timeout period를 명시하도록 한다. 만약 그 시간이 초과되고, select()는 여전히 어떤 준비된 file descriptors를 발견하지 못한다면, 그것은 return 할 것이다. 그래서 너는 계속해서 처리할 수 있다.

struct timeval은 다음의 필드들을 가진다:


1
2
3
4
5
struct timeval
{
  int tv_sec; // seconds
  int tv_usec; // microseconds
};

tv_sec을 기다릴 초의 숫자로 정하고, tv_usec을 기다릴 microseconds의 숫자로 설정해라. 그래, 그것은 milliseconds가 아닌, microsecond이다. 1 millisecond는 1,000 microseconds이고, 1,000 milliseconds는 1초이다. 따라서 1초는 1,000,000 microseconds이다. 왜 그것은 "usec"인가? "u"는 우리가 "micro"를 위해 사용하는 그리스 문자 μ(Mu)와 비슷하게 보일 예정이다. 또한 그 함수가 return할 때, timeout은 남아있는 시간을 보여주기위해 업데이트 될지도 모른다. 이것은 너가 어떤 Unix를 작동하느냐에 달려있다.

우리는 microsecond 단위의 타이머를 가지고 있다. 음, 그것을 중요시 여기지 마라. 너는 아마도 너의 표준 Unix timeslice의 몇몇 부분을 기다려야 한다. 너가 너의 struct timeval을 아무리 작게 할지라도.

흥미 있는 다른 것 : 만약 너가 너의 struct timeval을 0으로 설정한다면, select()는 즉시 timeout할 것이고, 효과적으로 너의 sets에 있는 모든 file descriptors를 polling한다. 만약 너가 인자 timeout을 NULL로 설정한다면, 그것은 결코 timeout하지 않을 것이고, 첫 번째 file descriptor가 준비될 때 까지 기다릴 것이다. 마지막으로, 너가 어떤 set를 기다리는 것을 신경쓰지 않는다면, 너는 select()에 대한 호출에서 그것을 NULL로 설정할 수 있다.

다음의 코드 조각은 standard input에 어떤 것이 나타나는 것을 2.5초 간 기다린다.


  1 /*
  2    select.c -- a select() demo
  3    */
  4
  5 #include <stdio.h>
  6 #include <sys/time.h>
  7 #include <sys/types.h>
  8 #include <unistd.h>
  9
 10 #define STDIN 0 // file descriptor for standard input
 11
 12 int main(void)
 13 {
 14         struct timeval tv;
 15         fd_set readfds;
 16
 17         tv.tv_sec = 2;
 18         tv.tv_usec = 500000;
 19
 20         FD_ZERO(&readfds);
 21         FD_SET(STDIN, &readfds);
 22
 23         // don't care about writefds and exceptfds:
 24         select(STDIN + 1, &readfds, NULL, NULL, &tv);
 25
 26         if(FD_ISSET(STDIN, &readfds))
 27                 printf("A key was pressed!\n");
 28         else
 29                 printf("Timed out.\n");
 30
 31         return 0;
 32 }

만약 너가 line buffered terminal에 있다면, 너가 치는 키는 되돌아 오거나 또는 그것은 시간 초과가 될 것이다.

이제 너는 datagram socket에서 데이터를 기다리는훌륭한 방법이라고 생각할지도 모른다 - 그리고 너는 옳다: 그것은 그럴지도 모른다. 몇몇 유닉스들은 이러한 방식으로 select를 사용할 수 있고, 몇몇 것들은 사용할 수 없다. 너는 너의 local man page가 너의 문제에 대해 무엇을 말하는지를 보아야만 한다. 너가 그것을 시도하기 원한다면.

몇몇 유닉스들은 timeout전에 남아 있는 시간의 양을 반영하기 위해 너의 struct timeval에서 시간을 업데이트 한다. 그러나 다른 것들은 그러지 않는다. 만약 너가 portable해지고 싶다면 그 발생하는 것을 의존하지말라. (만약 너가 경과 시간을 추적할 필요가 있다면, gettimeofday()를 사용해라. 실망스럽지만 그것이 그렇게 하는 방법이다.)


만약 read set에 있는 한 socket이 연결을 끊는다면 무슨 일이 발생하는가? 음, 그 경우에, select()는 "ready to read"로서 그 socket descriptor를 반환한다. 너가 실제로 그것으로 부터 recv()를 할 때, recv()는 0을 반환 할 것이다. 그것은 너가 클라이언트가 연결을 끊었다는 것을 알 수 있는 방법이다.

select()에 대해 한 가지 더 흥미로운 점 : 만약 너가 listen()하고 있는 한 소켓을 가지고 있다면, 너는 그 소켓의 file descriptor를 readfds set에 넣어서 새로운 연결이 있는지를 보기위해 체크할 수 있다.

그리고 그것은 select()함수의 빠른 overview이다.

그러나, 인기있는 요구에 의해, 여기에 좀 더 심화 예제가 있다. 불행하게도, 위의 더럽고-간단한 예제 사이의 차이점과 여기에 있는 것은 중요하다. 그러나 한 번 보아라. 그러고나서 그것이 따라오는 묘사들을 읽어라.

이 프로그램은 간단한 multi-user chat server같이 작동한다.그것을한 윈도우에서 작동시키고, 그리고 다양한 다른 윈도우들로부터 그것에 telnet 해라 ("telnet hostname 9034") 너가 한 telnet session에서 어떤 것을 칠 때, 그것은 모든 다른 이들에게 나타나야 한다.


  1 /*
  2    selectserver.c -- a cheezy multiperson chat server
  3    */
  4
  5 #include <stdio.h>
  6 #include <stdlib.h>
  7 #include <unistd.h>
  8 #include <sys/types.h>
  9 #include <sys/socket.h>
 10 #include <netinet/in.h>
 11 #include <arpa/inet.h>
 12 #include <netdb.h>
 13
 14 #define PORT "9034" // port we're listening on
 15
 16 // get sockaddr, IPv4 or IPv6:
 17 void* get_in_addr(struct sockaddr* sa)
 18 {
 19         if(sa->sa_family == AF_INET)
 20                 return &(((struct sockaddr_in*)sa)->sin_addr);
 21
 22         return &(((struct sockaddr_in6*)sa)->sin6_addr);
 23 }
 24
 25 int main(void)
 26 {
 27         fd_set master; // master file descriptor list
 28         fd_set read_fds; // temp file descriptor list for select()
 29         int fdmax;  // maximum file descriptor number
 30
 31         int listener; // listening socket descriptor
 32         int newfd; // newly accept()ed socket descriptor
 33         struct sockaddr_storage remoteaddr; // client address
 34         socklen_t addrlen;
 35
 36         char buf[256]; // buffer for client data
 37         int nbytes;
 38
 39         char remoteIP[INET6_ADDRSTRLEN];
 40
 41         int yes = 1; // for setsockopt() SO_REUSEADDR, below
 42         int i, j, v, rv;
 43
 44         struct addrinfo hints, *ai, *p;
 45
 46         FD_ZERO(&master); // clear the master and temp sets
 47         FD_ZERO(&read_fds);
 48
 49         // get us a socket and bind it
 50         memset(&hints, 0, sizeof(hints));
 51         hints.ai_family = AF_UNSPEC;
 52         hints.ai_socktype = SOCK_STREAM;
 53         hints.ai_flags = AI_PASSIVE;
 54         if((rv = getaddrinfo(NULL, PORT, &hints, &ai)) != 0)
 55         {
 56                 fprintf(stderr, "selectserver: %s\n", gai_strerror(rv));
 57                 exit(1);
 58         }
 59
 60         for(p = ai; p != NULL; p = p->ai_next)
 61         {
 62                 listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
 63                 listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
 64                 if(listener < 0)
 65 #include <stdio.h>      continue;
 66
 67                 // lose the pesky "address already in use" error message
 68                 setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int));
 69
 60                 if(bind(listener, p->ai_addr, p->ai_addrlen) < 0)
 71                 {
 72                         close(listener);
 73                         continue;
 74 #define PORT "90}4" // port we're listening on
 75                 break;
 76 // get s}ckaddr, IPv4 or IPv6:
 77
 78         // if we got here, it means we didn't get bound
 79         if(p == NULL)ily == AF_INET)
 70         {
 81                 fprintf(stderr, "selectserver : failed to bind\n");
 82         return &exit(2);t sockaddr_in6*)sa)->sin6_addr);
 83 }       }
 84
 85 int mainfreeaddrinfo(ai); // all done with this.
 86
 87         // listen(sock , backlog) : backlog is the limit number in the queue
 88         if(listen(listener, 10) == -1)
 89         {
 80                 perror("listen");
 91         int listexit(3); listening socket descriptor
 92         }
 93
 94         // add the listener to the master set
 95         FD_SET(listener, &master);
 96         char buf[256]; // buffer for client data
 97         // keep track of the biggest file descriptor
 98         fdmax = listener; // so far, it's this one
 99         char remoteIP[INET6_ADDRSTRLEN];
 90         // main loop
101         for(;;) = 1; // for setsockopt() SO_REUSEADDR, below
102         {
103                 read_fds = master; // copy it
104         struct aif(select(fdmax + 1, &read_fds, NULL, NULL, NULL) == -1)
105                 {
106         FD_ZERO(&master)perror("select");ster and temp sets
107                         exit(4);
108                 }
109         // get us a socket and bind it
100                 for(i = 0; i <= fdmax; ++i)
111                 {
112                         if(FD_ISSET(i, &read_fds)) // we got one!!
113                         {A
114                                 if(i == listener)&ai)) != 0)
115                                 {
116                 fprintf(stderr, "selects// handle new connectionsr(rv));
117                                         addrlen = sizeof(remoteaddr);
118         }                               newfd = accept(listener, (struct sockaddr*)&remoteaddr, &addrlen);
119
110         for(p = ai; p != NULL; p = p->aiif(newfd == -1)
121                                                 perror("accept");
122                 listener = socket(p->ai_elsely, p->ai_socktype, p->ai_protocol);
123                                                 FD_SET(newfd, &master); // add to master set
124                                                 if(newfd > fdmax)
125                                                 {
126                                                         // keep track of the max
127                                                         fdmax = newfd;
128                                                 }
129
130                                                 printf("selectserver: new connection from %s on socket %d",
131                                                                 inet_ntop(remoteaddr.ss_family,
132                                                                         get_in_addr((struct sockaddr*)&remoteaddr),
133                                                                         remoteIP,INET6_ADDRSTRLEN),
134                                                                 newfd);
135                                         }
136                                 }
137                                 else
138                                 {
139                                         // handle data from a client
140                                         if((nbytes = recv(i, buf, sizeof(buf), 0)) <= 0)
141                                         {
142                                                 // got error or connetion closed by client
143                                                 if(nbytes == 0)
144                                                 {
145                                                         //connection closed
146                                                         printf("selectserver: socket %d hung up\n", i);
147                                                 }
148                                                 else
149                                                 {
150                                                         perror("recv");
151                                                 }
152                                                 close(i); // bye!
153                                                 FD_CLR(i, &master); // remove from master set
154                                         }
155                                         else
156                                         {
157                                                 // we got some data from a client
158                                                 for(j = 0; j <= fdmax; ++j)
159                                                 {
160                                                         //send to everyone!
161                                                         if(FD_ISSET(j,&master))
162                                                         {
163                                                                 //except the listener and ourselves
164                                                                 if(j != listener && j!= i)
165                                                                 {
166                                                                         if(send(j, buf, nbytes, 0) == -1)
167                                                                         {
168                                                                                 perror("send");
169                                                                         }
170                                                                 }
171                                                         }
172                                                 }
173                                         }
174                                 } // END handle data from client
175                         } // END got new incoming connection
176                 } // END looping through file descriptor
177         } // END for(;;) -- and you thought it would never end!
178
179
180
181         return 0;
182 }

내가 코드에서 두 개의 file desriptor sets를 가진 것을 주목해라: master와 read_fds. 그 첫 번째 master는 현재 연결되어진 모든 소켓 descriptors를 가지고 있을 뿐만 아니라, 새로운 연결을 위해 listen하고 있는 소켓 descriptor 또한 포함한다.

내가 master set을 가진 이유는 select()는 어떤 소켓이 read할 준비가 되었는지를 반영하기 위해 실제로 너가 그것에 넘긴 set를 변화시키기 때문이다. 나는 select()의 한 호출에서 다음 것 까지의 connections들을 추적해야하기 때문에, 나는 이러한 것들을 안저하게 어딘가에 저장해야만 한다. 마지막 에서, 나는 master를 read_fds에 복사하고, 그러고나서 select()를 호출한다.

그러나 이것은 내가 새로운 connection을 가질 때 마다, 내가 그것을 master set에 추가해야만 하는 것을 의미하지 않는가? 그렇다. 한 connection이 close할 때 마다, 나는 그것을 master set으로부터 그것을 제거해야 하는가? 그렇다.

내가 언제 listener socket이 read할 주비가 되었는지를 보는 것을 체크하는 것에 유의해라.

댓글 없음:

댓글 쓰기