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초 간 기다린다.
만약 너가 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에서 어떤 것을 칠 때, 그것은 모든 다른 이들에게 나타나야 한다.
내가 코드에서 두 개의 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할 주비가 되었는지를 보는 것을 체크하는 것에 유의해라.
우리는 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할 주비가 되었는지를 보는 것을 체크하는 것에 유의해라.
댓글 없음:
댓글 쓰기