注意:我在这里的解释假设您正在谈论 TCP 套接字,或者至少是某种基于连接的类型。UDP 和其他数据报(即非基于连接的)套接字在某些方面相似,但您select
在它们上使用的方式略有不同。
每个套接字就像一个打开的文件,可以读取和写入数据。您写入的数据进入系统内部的缓冲区,等待在网络上发送出去。从网络到达的数据会在系统内部缓冲,直到您读取它。下面有很多聪明的东西,但是当您使用套接字时,您真正需要知道的是(至少最初是这样)。
在下面的解释中记住系统正在执行此缓冲通常很有用,因为您会意识到操作系统中的 TCP/IP 堆栈独立于您的应用程序发送和接收数据 - 这样做是为了让您的应用程序可以有一个简单的接口(这就是套接字,一种从代码中隐藏所有 TCP/IP 复杂性的方法)。
进行这种读写的一种方法是阻塞。使用该系统,recv()
例如,当您调用 时,如果系统中有数据等待,那么它将立即返回。但是,如果没有数据等待,则调用会阻塞- 也就是说,您的程序会暂停,直到有数据要读取。有时您可以通过超时来执行此操作,但在纯阻塞 IO 中,您真的可以永远等待,直到另一端发送一些数据或关闭连接。
对于一些简单的情况,这并不算太糟糕,但仅适用于您与另一台机器通信的情况 - 当您在多个套接字上通信时,您不能只等待来自一台机器的数据,因为另一台机器可能会给你寄东西。还有其他一些问题,我不会在这里详细介绍——我只想说这不是一个好方法。
一种解决方案是为每个连接使用不同的线程,因此阻塞是可以的——其他连接的其他线程可以被阻塞而不会相互影响。在这种情况下,每个连接需要两个线程,一个用于读取,一个用于写入。但是,线程可能是棘手的野兽——您需要小心地在它们之间同步数据,这会使编码变得有点复杂。此外,对于像这样的简单任务,它们效率低下。
该select
模块允许您使用单线程解决此问题 - 而不是阻塞单个连接,它允许您执行“进入睡眠状态,直到这些套接字中的至少一个有一些我可以读取的数据”(这是一个简化,我稍后会更正)。因此,一旦调用select.select()
返回,您就可以确定您正在等待的连接之一有一些数据,并且您可以安全地读取它(即使使用阻塞 IO,如果您小心的话 - 因为您确定存在那里的数据,你永远不会阻塞等待它)。
当你第一次启动你的应用程序时,你只有一个套接字,它是你的监听套接字。所以,你只在对select.select()
. 我之前所做的简化是,实际上该调用接受三个用于读取、写入和错误的套接字列表。第一个列表中的套接字被监视读取 - 因此,如果其中任何一个有数据要读取,则select.select()
函数将控制权返回给您的程序。第二个列表用于写入 - 您可能认为您始终可以写入套接字,但实际上如果连接的另一端读取数据的速度不够快,那么您系统的写入缓冲区可能会填满,您可能暂时无法写入. 看起来给你代码的人忽略了这种复杂性,这对于一个简单的例子来说还不错,因为通常缓冲区足够大,你不太可能在这样的简单情况下遇到问题,但这是你应该解决的问题一旦你的代码的其余部分工作,未来的地址。最后的列表会被监视错误 - 这并没有被广泛使用,所以我现在将跳过它。在这里传递空列表很好。
此时有人连接到您的服务器 - 就这select.select()
方面而言,这算作使监听套接字“可读”,因此函数返回并且可读套接字列表(第一个返回值)将包括监听套接字。
下一部分将遍历所有要读取数据的连接,您可以看到侦听套接字的特殊情况s
。代码调用accept()
它,它将从侦听套接字获取下一个等待的新连接并将其转换为该连接的全新套接字(侦听套接字继续侦听并且可能还有其他新连接也在等待它,但这很好 -我稍后会介绍这个)。全新的套接字被添加到connections
列表中,这就是处理侦听套接字的结束 -如果有的话,continue
将继续从 返回的下一个连接select.select()
。
对于其他可读的连接,代码调用recv()
它们来恢复下一个1024
字节(或者如果小于 1024 字节,则任何可用的字节)。重要说明 - 如果您不习惯select.select()
确保连接可读,则此调用recv()
可能会阻塞并暂停您的程序,直到数据到达该特定连接 - 希望这说明了为什么select.select()
需要。
一旦读取了一些数据,代码就会在所有其他连接(如果有的话)上运行,并使用该send()
方法将数据复制下来。代码正确地跳过了与刚刚到达的数据相同的连接(这是关于 的业务q != i
)并且也跳过s
了 ,但碰巧这不是必需的,因为据我所知,它实际上从未添加到connections
列表中。
处理完所有可读连接后,代码将返回select.select()
循环以等待更多数据。请注意,如果连接仍有数据,则调用会立即返回——这就是为什么只接受来自监听套接字的单个连接是可以的。如果有更多连接,select.select()
将立即再次返回,并且循环可以处理下一个可用连接。您可以使用非阻塞 IO 来提高效率,但这会使事情变得更复杂,所以现在让我们保持简单。
这是一个合理的说明,但不幸的是它存在一些问题:
- 正如我所提到的,代码假定您始终可以
send()
安全地调用,但是如果您有一个连接,而另一端没有正确接收(可能该机器过载),那么您的代码可能会填满发送缓冲区,然后在它挂起时挂起试图打电话send()
。
- 该代码无法处理连接关闭,这通常会导致从
recv()
. 这应该会导致连接被关闭并从connections
列表中删除,但是这段代码不会这样做。
我稍微更新了代码以尝试解决这两个问题:
connections = []
buffered_output = {}
while True:
rlist,wlist,xlist = select.select(connections + [s],buffered_output.keys(),[])
for i in rlist:
if i == s:
conn,addr = s.accept()
connections.append(conn)
continue
try:
data = i.recv(1024)
except socket.error:
data = ""
if data:
for q in connections:
if q != i:
buffered_output[q] = buffered_output.get(q, b"") + data
else:
i.close()
connections.remove(i)
if i in buffered_output:
del buffered_output[i]
for i in wlist:
if i not in buffered_output:
continue
bytes_sent = i.send(buffered_output[i])
buffered_output[i] = buffered_output[i][bytes_sent:]
if not buffered_output[i]:
del buffered_output[i]
我应该在这里指出,我假设如果远程端关闭连接,我们也想立即关闭这里。严格来说,这忽略了 TCP半关闭的可能性,即远程端已发送请求并关闭其端,但仍期望返回数据。我相信非常旧的 HTTP 版本有时会这样做来指示请求的结束,但实际上这很少使用,并且可能与您的示例无关。
另外值得注意的是,很多人在使用时使他们的套接字是非阻塞的select
——这意味着调用recv()
或send()
否则会阻塞将返回错误(用 Python 术语引发异常)。这样做的部分原因是为了安全,以确保粗心的代码不会最终阻塞应用程序;但它也允许一些稍微更有效的方法,例如在多个块中读取或写入数据,直到没有剩余。使用阻塞 IO 这是不可能的,因为select.select()
调用只保证有一些数据要读取或写入 - 它不保证有多少。因此,在需要调用之前,您只能在每个连接上安全地调用阻塞send()
或一次recv()
select.select()
再次查看您是否可以再次这样做。这同样适用于accept()
监听套接字。
但是,效率节省通常只是在具有大量繁忙连接的系统上存在的问题,因此在您的情况下,我会保持简单,暂时不用担心阻塞。在您的情况下,如果您的应用程序似乎挂断并且没有响应,那么您很可能在不应该的地方进行阻塞调用。
最后,如果您想使此代码可移植和/或更快,可能值得看一下类似的东西libev
,它本质上有几种替代方案,select.select()
可以在不同的平台上很好地工作。但是,这些原则大体相似,因此最好先关注一下select
,直到您的代码运行起来,然后再研究更改它。
另外,我注意到一位评论者建议使用Twisted,这是一个提供更高级别抽象的框架,因此您无需担心所有细节。就我个人而言,过去我遇到过一些问题,例如很难以方便的方式捕获错误,但很多人都非常成功地使用它——这只是他们的方法是否适合你思考事物的方式的问题。至少值得调查一下,看看它的风格是否比我更适合你。我来自用 C/C++ 编写网络代码的背景,所以也许我只是坚持我所知道的(Pythonselect
模块非常接近它所基于的 C/C++ 版本)。
希望我已经在那里充分解释了事情 - 如果您仍有问题,请在评论中告诉我,我可以在我的答案中添加更多细节。