如何唤醒套接字上的select()?

我目前使用select循环来pipe理代理中的套接字。 此代理的要求之一是,如果代理向外部服务器发送消息,并且在一定时间内没有得到响应,则代理应closures该套接字并尝试连接到辅助服务器。 closures发生在一个单独的线程,而select线程阻止等待活动。

我很难弄清楚如何检测这个套接字是专门closures的,这样我才能处理失败。 如果我在另一个线程中调用close(),我得到一个EBADF,但是我不知道哪个套接字closures了。 我试图通过fdsetexception检测套接字,认为它将包含封闭的套接字,但我没有得到任何返回的东西。 我也听说过调用shutdown()会向服务器发送一个FIN并接收一个FIN回来,这样我就可以closures它; 但整个问题是我试图通过在超时期限内没有得到答复来解决这个问题,所以我也不能这么做。

如果我的假设是错误的,请告诉我。 任何想法,将不胜感激。

编辑:为了响应使用select超时的build议:我需要做asynchronousclosures,因为连接到代理的客户端将超时,我不能等待周围的select被轮询。 如果我把select的时间做得很小,那么这个工作就会起作用,然后不断地投票,浪费我不想要的资源。

通常我只是在另一个线程中标记关闭的套接字,然后当select()从activity或timeout返回时,我运行一个清理过程并关闭所有死连接并更新fd_set。 做任何其他的方式,你打开你放弃了连接的竞争条件,就像select()最终识别了一些数据,然后关闭它,但另一个线程试图处理检测到的数据并获取不高兴找到连接关闭。

哦,poll()通常比select()好,不需要复制太多的数据。

如何在select()上处理EBADF:

int fopts = 0; for (int i = 0; i < num_clients; ++i) { if (fcntl(client[i].fd, F_GETFL, &fopts) < 0) { // call close(), FD_CLR(), and remove i'th element from client list } } 

此代码假定您有一个客户端结构数组,其中具有套接字描述符的“fd”成员。 fcntl()调用检查套接字是否仍然“活着”,如果没有,我们做我们必须去除死亡套接字及其相关的客户端信息。

看到只有一小部分的大象很难评论,但也许你是在复杂的事情?

据推测,你有一些结构来跟踪每个插座及其信息(如留下接受答复的时间)。 您可以更改select()循环以使用超时。 在它内部检查是否是时间关闭插座。 做你需要做的关闭,并且不要在下一次把它添加到fd集。

如果您按照其他答案的建议使用poll(2),则可以使用基本上是EBADF的POLLNVAL状态,但是基于每个文件描述符,而不是像整个系统调用那样使用select(2)。

当另一个线程正在或可能正在使用它时,您无法在一个线程中释放资源。 调用close可能在另一个线程中使用的套接字将永远不能正常工作。 总会有潜在的灾难性的竞赛条件。

有两个很好的解决方案来解决你的问题:

  1. 让调用select的线程总是使用一个不超过你愿意等待处理超时的最长时间。 当发生超时时,请指出某个地方调用select的线程在从select返回时会注意到。 让那个线程做实际的close之间的调用select的套接字。

  2. 检测套接字上的超时调用shutdown的线程。 这将导致select返回,然后让该线程close

只需在每个可能关闭的套接字上运行一个“测试选择”,然后检查选择结果和错误号,直到找到关闭的结果为止。

下面的一段演示代码在不同的线程上启动两个服务器套接字,并创建两个客户端套接字连接到任一服务器套接字。 然后它启动另一个线程,10秒后将会随机杀死一个客户端套接字(它会关闭它)。 关闭任何一个客户端套接字都会导致select在主线程中出现错误,下面的代码现在将测试两个套接字中的哪一个实际关闭了。

 #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <assert.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <stdint.h> #include <pthread.h> #include <stdbool.h> #include <arpa/inet.h> #include <netinet/in.h> #include <sys/select.h> #include <sys/socket.h> static void * serverThread ( void * threadArg ) { int res; int connSo; int servSo; socklen_t addrLen; struct sockaddr_in soAddr; uint16_t * port = threadArg; servSo = socket(PF_INET, SOCK_STREAM, 0); assert(servSo >= 0); memset(&soAddr, 0, sizeof(soAddr)); soAddr.sin_family = AF_INET; soAddr.sin_port = htons(*port); // Uncommend line below if your system offers this field in the struct // and also needs this field to be initialized correctly. // soAddr.sin_len = sizeof(soAddr); res = bind(servSo, (struct sockaddr *)&soAddr, sizeof(soAddr)); assert(res == 0); res = listen(servSo, 10); assert(res == 0); addrLen = 0; connSo = accept(servSo, NULL, &addrLen); assert(connSo >= 0); for (;;) { char buffer[2048]; ssize_t bytesRead; bytesRead = recv(connSo, buffer, sizeof(buffer), 0); if (bytesRead <= 0) break; printf("Received %zu bytes on port %d.\n", bytesRead, (int)*port); } free(port); close(connSo); close(servSo); return NULL; } static void * killSocketIn10Seconds ( void * threadArg ) { int * so = threadArg; sleep(10); printf("Killing socket %d.\n", *so); close(*so); free(so); return NULL; } int main ( int argc, const char * const * argv ) { int res; int clientSo1; int clientSo2; int * socketArg; uint16_t * portArg; pthread_t killThread; pthread_t serverThread1; pthread_t serverThread2; struct sockaddr_in soAddr; // Create a server socket at port 19500 portArg = malloc(sizeof(*portArg)); assert(portArg != NULL); *portArg = 19500; res = pthread_create(&serverThread1, NULL, &serverThread, portArg); assert(res == 0); // Create another server socket at port 19501 portArg = malloc(sizeof(*portArg)); assert(portArg != NULL); *portArg = 19501; res = pthread_create(&serverThread1, NULL, &serverThread, portArg); assert(res == 0); // Create two client sockets, one for 19500 and one for 19501 // and connect both to the server sockets we created above. clientSo1 = socket(PF_INET, SOCK_STREAM, 0); assert(clientSo1 >= 0); clientSo2 = socket(PF_INET, SOCK_STREAM, 0); assert(clientSo2 >= 0); memset(&soAddr, 0, sizeof(soAddr)); soAddr.sin_family = AF_INET; soAddr.sin_port = htons(19500); res = inet_pton(AF_INET, "127.0.0.1", &soAddr.sin_addr); assert(res == 1); // Uncommend line below if your system offers this field in the struct // and also needs this field to be initialized correctly. // soAddr.sin_len = sizeof(soAddr); res = connect(clientSo1, (struct sockaddr *)&soAddr, sizeof(soAddr)); assert(res == 0); soAddr.sin_port = htons(19501); res = connect(clientSo2, (struct sockaddr *)&soAddr, sizeof(soAddr)); assert(res == 0); // We want either client socket to be closed locally after 10 seconds. // Which one is random, so try running test app multiple times. socketArg = malloc(sizeof(*socketArg)); srandomdev(); *socketArg = (random() % 2 == 0 ? clientSo1 : clientSo2); res = pthread_create(&killThread, NULL, &killSocketIn10Seconds, socketArg); assert(res == 0); for (;;) { int ndfs; int count; fd_set readSet; // ndfs must be the highest socket number + 1 ndfs = (clientSo2 > clientSo1 ? clientSo2 : clientSo1); ndfs++; FD_ZERO(&readSet); FD_SET(clientSo1, &readSet); FD_SET(clientSo2, &readSet); // No timeout, that means select may block forever here. count = select(ndfs, &readSet, NULL, NULL, NULL); // Without a timeout count should never be zero. // Zero is only returned if select ran into the timeout. assert(count != 0); if (count < 0) { int error = errno; printf("Select terminated with error: %s\n", strerror(error)); if (error == EBADF) { fd_set closeSet; struct timeval atonce; FD_ZERO(&closeSet); FD_SET(clientSo1, &closeSet); memset(&atonce, 0, sizeof(atonce)); count = select(clientSo1 + 1, &closeSet, NULL, NULL, &atonce); if (count == -1 && errno == EBADF) { printf("Socket 1 (%d) closed.\n", clientSo1); break; // Terminate test app } FD_ZERO(&closeSet); FD_SET(clientSo2, &closeSet); // Note: Standard requires you to re-init timeout for every // select call, you must never rely that select has not changed // its value in any way, not even if its all zero. memset(&atonce, 0, sizeof(atonce)); count = select(clientSo2 + 1, &closeSet, NULL, NULL, &atonce); if (count == -1 && errno == EBADF) { printf("Socket 2 (%d) closed.\n", clientSo2); break; // Terminate test app } } } } // Be a good citizen, close all sockets, join all threads close(clientSo1); close(clientSo2); pthread_join(killThread, NULL); pthread_join(serverThread1, NULL); pthread_join(serverThread2, NULL); return EXIT_SUCCESS; } 

用于运行此测试代码两次的示例输出:

 $ ./sockclose Killing socket 3. Select terminated with error: Bad file descriptor Socket 1 (3) closed. $ ./sockclose Killing socket 4. Select terminated with error: Bad file descriptor Socket 1 (4) closed. 

但是,如果您的系统支持poll() ,我强烈建议您考虑使用此API而不是select() 。 选择是一个相当丑陋的旧版API,只是为了向后兼容现有的代码。 轮询有一个更好的接口这个任务,它有一个额外的标志,直接告诉你,一个套接字本地关闭: POLLNVAL将被设置为POLLNVAL ,如果此套接字已关闭,无论哪些标志您请求的事件,因为POLLNVAL是一个输出只有标志,这意味着它被设置为events时被忽略。 如果套接字没有在本地关闭,但是远程服务器刚刚关闭了连接, POLLHUP标志将被设置为revents (也是一个输出标志)。 轮询的另一个好处是超时只是一个int值(毫秒,对于真正的网络套接字足够精细),并且对可监视的套接字数量或其数值范围没有限制。

对select使用超时,如果read-ready / write-ready / had-error序列全部为空(对于那个套接字),检查它是否关闭。