我目前正在使用select循环来管理代理中的套接字。此代理的一个要求是,如果代理向外部服务器发送消息并且在特定时间内未收到响应,则代理应关闭该套接字并尝试连接到辅助服务器。结束发生在一个单独的线程中,而select线程阻塞等待活动。
我无法确定如何检测此套接字是否专门关闭,以便我可以处理故障。如果我在另一个线程中调用close(),我得到一个EBADF,但是我无法判断哪个套接字已关闭。我试图通过异常fdset检测套接字,认为它将包含已关闭的套接字,但我没有得到任何返回。我也听说调用shutdown()会将FIN发送到服务器并收回FIN,这样我就可以关闭它;但重点是我试图通过在超时期限内没有得到回应来关闭它,所以我也不能这样做。
如果我的假设是错误的,请告诉我。任何想法都将不胜感激。
编辑: 响应有关使用select time out的建议:我需要异步关闭,因为连接到代理的客户端将超时,我不能等待选择被轮询。这只有在我把选择时间缩小到非常小的情况下才有效,这会不断地进行轮询和浪费我不想要的资源。
答案 0 :(得分:9)
通常我只是将套接字标记为在另一个线程中关闭,然后当select()从activity或timeout返回时,我运行一个清理传递并关闭所有死连接并更新fd_set。以任何其他方式执行此操作会打开您放弃连接的竞争条件,就像select()最终识别它的某些数据一样,然后关闭它,但另一个线程尝试处理检测到的数据并获取心烦找到连接关闭。
哦,并且poll()通常比select()更好,无需复制尽可能多的数据。
答案 1 :(得分:3)
当另一个线程正在或可能正在使用它时,您无法在一个线程中释放资源。在另一个线程中可能正在使用的套接字上调用close
将永远无法正常工作。总会有潜在的灾难性的竞争条件。
您的问题有两个很好的解决方案:
让调用select
的线程总是使用不超过您愿意等待处理超时的最长时间。发生超时时,请指示调用select
的线程从select
返回时会注意到的某个位置。让该线程在close
之间调用套接字的实际select
。
让线程检测套接字上的超时调用shutdown
。这将导致select
返回,然后让该线程执行close
。
答案 2 :(得分:1)
如何在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()调用检查套接字是否仍然“活动”,如果没有,我们就会采取措施删除死套接字及其相关的客户端信息。
答案 3 :(得分:0)
当只看到大象的一小部分时,很难评论,但也许你已经结束了复杂的事情?
据推测,您有一些结构可以跟踪每个套接字及其信息(比如接收回复的时间)。您可以更改select()循环以使用超时。在其中检查是否是时候关闭套接字。做你需要做的关闭,不要在下次将它添加到fd集。
答案 4 :(得分:0)
如果你按照其他答案中的建议使用poll(2),你可以使用POLLNVAL状态,它基本上是EBADF,但是基于每个文件描述符,而不是整个系统调用,因为它是select( 2)。
答案 5 :(得分:-1)
对select使用超时,如果read-ready / write-ready / had-error序列都为空(w.r.t那个socket),请检查它是否已关闭。
答案 6 :(得分:-1)
在每个可能以零超时关闭的套接字上运行“test select”并检查选择结果和errno,直到找到已关闭的那个。
以下示例代码在不同的线程上启动两个服务器套接字,并创建两个客户端套接字以连接到任一服务器套接字。然后它启动另一个线程,它将在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()
。 Select是过去相当丑陋的遗留API,只留下来与现有代码向后兼容。 Poll有一个更好的接口来完成这个任务,它有一个额外的标志来直接向你发出套接字已在本地关闭的信号:如果这个套接字已经关闭,POLLNVAL
将在revents
上设置,无论哪个标志您请求事件,因为POLLNVAL
是仅输出标志,这意味着在events
上设置时会忽略它。如果套接字未在本地关闭但远程服务器刚刚关闭连接,则标记POLLHUP
将在revents
中设置(也是仅输出标志)。 poll的另一个优点是超时只是一个int值(毫秒,对于真实的网络套接字来说足够精细),并且对可以监视的套接字数量或它们的数值范围没有限制。