想象一下以下场景:您正在编写一个服务器应用程序。客户端向服务器发送他们的查询,对于每个新的客户端连接,服务器启动一个新进程,负责回答从客户端收到的所有查询。对于收到的每个查询,该过程会工作一段时间,最后将结果发送到查询返回给客户端。
到现在为止还挺好。但是,如果在进程处理刚刚从客户端收到的查询时客户端关闭了与服务器的连接,会发生什么情况?在这种情况下,没有人会查看查询结果,因此一旦客户端关闭连接,立即终止进程是很有意义的。问题是:我们如何判断套接字连接是否已被客户端关闭?
我们需要一个单独的进程,它只检查客户端是否关闭了一个连接(当然,这个进程可能会同时监视多个连接)。实现这一点的最明显的方法是在连接的套接字上读取该进程调用,并检查 read 是否返回 0(即从套接字读取零字节),在这种情况下,我们知道连接已关闭。问题是我们实际上不想调用 read 因为这会从流中删除通常由负责该连接的进程读取的数据。在这种情况下,我们如何将数据从监控进程获取到查询处理器?所以,显然,调用 read 不是一个好主意。
幸运的是,我们有 select 和 poll 函数,可用于检测套接字是否已被远程主机关闭。不幸的是,两者的问题在于它们只告诉我们“数据可用”,即使连接已关闭。这是因为“可用数据”实际上意味着完全不同的东西,即“可以在不阻塞的情况下调用此套接字上的读取”。为了确定连接是否实际上已关闭(或者实际上是否有新数据可供读取),我们需要再次调用 read。坏的。
该问题的解决方案称为 recv,它做得很好。诀窍是 recv 支持两个非常好的标志: MSG_PEEK 告诉 recv 不要从流中删除任何内容,而 MSG_DONTWAIT 使函数在没有数据可供读取时立即返回。下面是一个代码片段,它接受一个新的客户端连接并生成一个新进程,该进程响应通过该连接接收到的查询。父进程一直等到套接字关闭。这可能是因为子进程已完成执行(并关闭套接字)或因为客户端在子进程完成其任务之前关闭了套接字。
fd = accept(listenSocket);
pid_t childProcess = fork();
if (childProcess == (pid_t)-1) {
perror("Unable to create new process for client connection");
exit(1);
}
else if (childProcess == 0) {
// read from socket, process queries, etc.
}
else {
// use the poll system call to be notified about socket status changes
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN | POLLHUP | POLLRDNORM;
pfd.revents = 0;
while (pfd.revents == 0) {
// call poll with a timeout of 100 ms
if (poll(&pfd, 1, 100) > 0) {
// if result > 0, this means that there is either data available on the
// socket, or the socket has been closed
char buffer[32];
if (recv(fd, buffer, sizeof(buffer), MSG_PEEK | MSG_DONTWAIT) == 0) {
// if recv returns zero, that means the connection has been closed:
// kill the child process
kill(childProcess, SIGKILL);
waitpid(childProcess, &status, WNOHANG);
close(fd);
// do something else, e.g. go on vacation
}
}
}
}
Python Module:
from ctypes import (
CDLL, c_int, POINTER, Structure, c_void_p, c_size_t,
c_short, c_ssize_t, c_char, ARRAY
)
__all__ = 'is_remote_alive',
class pollfd(Structure):
_fields_ = (
('fd', c_int),
('events', c_short),
('revents', c_short),
)
MSG_DONTWAIT = 0x40
MSG_PEEK = 0x02
EPOLLIN = 0x001
EPOLLPRI = 0x002
EPOLLRDNORM = 0x040
libc = CDLL(None)
recv = libc.recv
recv.restype = c_ssize_t
recv.argtypes = c_int, c_void_p, c_size_t, c_int
poll = libc.poll
poll.restype = c_int
poll.argtypes = POINTER(pollfd), c_int, c_int
class IsRemoteAlive: # not needed, only for debugging
def __init__(self, alive, msg):
self.alive = alive
self.msg = msg
def __str__(self):
return self.msg
def __repr__(self):
return 'IsRemoteClosed(%r,%r)' % (self.alive, self.msg)
def __bool__(self):
return self.alive
def is_remote_alive(fd):
fileno = getattr(fd, 'fileno', None)
if fileno is not None:
if hasattr(fileno, '__call__'):
fd = fileno()
else:
fd = fileno
p = pollfd(fd=fd, events=EPOLLIN|EPOLLPRI|EPOLLRDNORM, revents=0)
result = poll(p, 1, 0)
if not result:
return IsRemoteAlive(True, 'empty')
buf = ARRAY(c_char, 1)()
result = recv(fd, buf, len(buf), MSG_DONTWAIT|MSG_PEEK)
if result > 0:
return IsRemoteAlive(True, 'readable')
elif result == 0:
return IsRemoteAlive(False, 'closed')
else:
return IsRemoteAlive(False, 'errored')