【问题标题】:Issue using select() and TCP Keepalive with Python sockets将 select() 和 TCP Keepalive 与 Python 套接字一起使用的问题
【发布时间】:2019-01-29 22:21:28
【问题描述】:

我想要一个等待客户端连接的 TCP 服务器,一旦它们连接,就会不断地向它们发送一些数据。我还希望服务器能够注意到客户端是否突然消失而无影无踪,并将其从打开的套接字列表中删除。

我的代码如下所示:

#!/usr/bin/env python3

import select, socket 

# Listen Port
LISTEN_PORT = 1234

# Create socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Setup the socket
server.setblocking(0)
server.bind(('0.0.0.0', LISTEN_PORT))
server.listen(5)

# Make socket reusable
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Setup TCP Keepalive
server.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)

# Tell user we are listening
print("Listening on port %s" % LISTEN_PORT)

inputs = [server]
outputs = []

while True:
    # Detecting clients that disappeared does NOT work when we ARE
    # watching if any sockets are writable
    #readable, writable, exceptional = select.select(inputs, outputs, inputs)

    # Detecting clients that disappeared works when we aren't watching
    # if any sockets are writable
    readable, writable, exceptional = select.select(inputs, [], inputs)

    for s in readable:
        if s is server:
            connection, client_address = s.accept()

            print("New client connected: %s" % (client_address,))

            connection.setblocking(0)

            inputs.append(connection)
            outputs.append(connection)
        else:
            try:
                data = s.recv(1024)
            except TimeoutError:
                print("Client dropped out")

                inputs.remove(s)

                if s in outputs:
                    outputs.remove(s)
                    continue

            if data:
                print("Data from %s: %s" % (s.getpeername(), data.decode('ascii').rstrip()))
            else:
                print("%s disconnected" % (s.getpeername(),))

    for s in writable:
        s.send(b".")

如您所见,我正在使用 TCP Keepalive 来查看客户端是否已消失。我看到的问题是这样的:

  • 当我没有 select() 监视可写套接字时,当客户端消失时,select() 将在 TCP Keepalive 超时到期后停止阻塞,并且套接字将在 readable 列表中,所以我可以删除从inputoutput 消失的客户端(这很好)
  • 当我让 select() 监视可写套接字时,当客户端消失时,select() 在 TCP Keepalive 超时到期后不会停止阻塞,并且客户端套接字永远不会在 readablewritable 中结束列表,所以它永远不会被删除

我正在使用来自不同机器的 telnet 作为客户端。为了复制消失的客户端,我使用 iptables 来阻止客户端在客户端连接时与服务器通信。

有人知道怎么回事吗?

【问题讨论】:

  • 因为是连续发送,所以根本不需要 TCP keepalive。如果客户端消失,您最终将重置连接。而且你不需要在写作之前选择可写性。只需编写,并且仅在产生 EAGAIN/EWOULDBLOCK 时使用select()。注意使用iptables 不会模拟消失的客户端。
  • 1.即使是一个简单的单线程 python 应用程序,它等待单个客户端连接并不停地写入它也不会注意到客户端在 5 分钟后消失(我刚刚尝试过)。 2. iptables 确保客户端没有机会向服务器发出断开连接的信号。不拔网线就好了 3. 如果我不使用 select() 来检查可写性,我最终只检查新连接,看起来像this,在这种情况下,选择。 select 将阻塞,阻止写入。我错过了什么吗?
  • 我在五分钟内什么都没说,但它肯定会发生。你可以随时写。你不需要select()'permission`,除非我提到的情况。

标签: python-3.x sockets networking


【解决方案1】:

正如您问题的 cmets 所提到的,TCP_KEEPALIVE 的内容不会对您的用例产生任何影响。 TCP_KEEPALIVE 是一种在 TCP 连接另一端的对等方离开时通知程序的机制在其他空闲的 TCP 连接上。由于您定期在 TCP 连接上发送数据,因此永远不会调用(或需要)TCP_KEEPALIVE 功能,因为通过连接发送数据的行为本身就足以使 TCP 堆栈尽快识别当远程客户端离开时。

也就是说,我修改/简化了您的示例服务器代码,以使其在我的机器(Mac,FWIW)上(尽可能正确)工作。我所做的是:

  1. socket.setsockopt(SO_REUSEADDR) 移动到bind() 行之前,这样bind() 在您杀死并重新启动程序后不会失败。

  2. 更改了 select() 调用以监视可写套接字。

  3. 围绕send() 调用添加了异常处理。

  4. 将 remove-socket-from-lists 代码移动到单独的 RemoveSocketFromLists() 函数中,以避免冗余代码

请注意,TCP 的预期行为是,如果您轻轻退出客户端(例如,通过 control-C'ing 或通过任务管理器将其终止,或以其他方式导致其退出,使其主机 TCP 堆栈仍然能够与服务器通信以告诉服务器客户端已死)然后服务器应该或多或少地立即识别死客户端。

另一方面,如果客户端的网络连接突然断开(例如,因为有人拔掉了客户端计算机的以太网或电源线),那么服务器程序可能需要几分钟才能检测到客户端已经离开,并且这是预期的行为,因为服务器无法告诉(在这种情况下)客户端是否已死。 (即它不想仅仅因为路由器丢弃了一些 TCP 数据包,从而导致与仍然活动的客户端的通信暂时中断而终止一个可行的 TCP 连接)

如果您想尝试在这种情况下快速删除客户端,您可以尝试要求客户端每秒左右向服务器发送send() 一些虚拟数据。服务器可以跟踪它上次从每个客户端接收任何数据的时间戳,并强制关闭它在“太长时间”内没有收到任何数据的任何客户端(不管你的想法太长)。这或多或少会起作用,尽管如果您将超时阈值设置得太低,它可能会产生误报(即丢弃仍然活着的客户端,只是速度慢或丢包)。

#!/usr/bin/env python3

import select, socket

# Listen Port
LISTEN_PORT = 1234

# Create socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Make socket reusable
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Setup the socket
server.setblocking(0)
server.bind(('0.0.0.0', LISTEN_PORT))
server.listen(5)

# Tell user we are listening
print("Listening on port %s" % LISTEN_PORT)

inputs  = [server]
outputs = []

# Removes the specified socket from every list in the list-of-lists
def RemoveSocketFromLists(s, listOfLists):
   for nextList in listOfLists:
      if s in nextList:
         nextList.remove(s)

while True:
   # Detecting clients that disappeared does NOT work when we ARE
   # watching if any sockets are writable
   readable, writable, exceptional = select.select(inputs, outputs, [])

   for s in readable:
      if s is server:
         connection, client_address = s.accept()

         print("New client connected: %s" % (client_address,))
         connection.setblocking(0)
         inputs.append(connection)
         outputs.append(connection)
      else:
         try:
            data = s.recv(1024)
            print("Data from %s: %s" % (s.getpeername(), data.decode('ascii').rstrip()))
         except:
            print("recv() reports that %s disconnected" % s)
            RemoveSocketFromLists(s, [inputs, outputs, writable])

   for s in writable:
      try:
         numBytesSent = s.send(b".")
      except:
         print("send() reports that %s disconnected" % s)
         RemoveSocketFromLists(s, [inputs, outputs])

【讨论】:

  • 你的解释太棒了!精炼到位。我一直有很多关于 python 套接字编程的问题,你解决了很多问题。另外,我总是很困惑为什么SO_REUSEADDR 不适合我——现在我明白了。谢谢!
猜你喜欢
  • 2017-02-10
  • 2020-11-30
  • 2023-03-31
  • 2013-11-16
  • 1970-01-01
  • 2011-01-12
  • 2013-12-29
  • 2014-12-31
  • 2011-06-17
相关资源
最近更新 更多