gpt4 book ai didi

python - 如何通过套接字和选择模块管理聊天服务器(Python)的套接字连接

转载 作者:太空宇宙 更新时间:2023-11-04 07:43:04 25 4
gpt4 key购买 nike

抱歉打扰大家,但是我已经被困了一段时间了。

问题是我决定重新配置我使用套接字的聊天程序,以便它有一个服务器,然后是两个单独的客户端,而不是一个客户端和一个服务器/客户端。

前面我问过我如何让服务器“管理”客户端的这些连接,以便它可以在它们之间重定向数据。而且我得到了一个奇妙的答案,该答案为我提供了我显然需要执行此操作的确切代码。

问题是我不知道它是如何工作的,我确实在评论中提出了要求,但是除了一些文档链接之外,我没有得到太多答复。

这是给我的:

connections = []

while True:
rlist,wlist,xlist = select.select(connections + [s],[],[])
for i in rlist:
if i == s:
conn,addr = s.accept()
connections.append(conn)
continue
data = i.recv(1024)
for q in connections:
if q != i and q != s:
q.send(data)

据我了解,在select.select的情况下,select模块可以使对象成为可等待对象。

我有rlist,待读取的待处理列表,wlist,待写入的待处理列表,然后是xlist,待处理的异常条件。

他将待写列表分配给“s”,在我的聊天服务器中,该列表是正在分配端口上监听的套接字。

就我所知,这足够多了。但是我真的很想解释一下。

如果您不希望我提出适当的问题,请在评论中告诉我,然后将其删除。我不想违反任何规则,而且我很确定自己不会重复线程,因为在进行询问之前,我做了一段时间的研究。

谢谢!

最佳答案

注意:我在这里的解释假设您正在谈论TCP套接字,或者至少是某种基于连接的类型。 UDP和其他数据报(即非基于连接的)套接字在某些方面相似,但是在它们上使用select的方式略有不同。

每个套接字就像一个打开的文件,可以读取和写入数据。您写入的数据进入系统内部的缓冲区,等待在网络上发送出去。从网络到达的数据将在系统内部进行缓冲,直到您读取它为止。下面有很多聪明的东西,但是当您使用套接字时,您真正需要知道的全部(至少在最初是这样)。

记住,系统在下面的说明中进行缓冲通常是很有用的,因为您将意识到OS中的TCP/IP堆栈独立于您的应用程序发送和接收数据-这样做是为了使您的应用程序可以拥有一个简单的接口(interface)(套接字就是这种接口(interface),一种从代码中隐藏所有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 half-close的可能性,在该位置,远程端已发送了一个请求并关闭了它的端,但仍希望返回数据。我相信HTTP的旧版本有时会用来指示请求的结束,但是实际上,这已经很少使用了,并且可能与您的示例无关。

    同样值得注意的是,很多人在使用 select时使套接字无阻塞-这意味着对 recv()send()的调用将被阻塞,否则将返回错误(在Python术语中引发异常)。这样做的部分目的是为了安全起见,以确保不会导致粗心的代码最终不会阻塞应用程序。但是它还允许使用一些效率更高的方法,例如以多个块的形式读取或写入数据,直到没有剩余的块为止。使用阻塞IO是不可能的,因为 select.select()调用仅保证可以读取或写入一些数据,而不保证有多少数据。因此,您只能在每个连接上安全地调用一次阻塞的 send()recv(),然后才能再次调用 select.select()以查看是否可以再次调用。监听套接字上的 accept()也是如此。

    通常,在具有大量繁忙连接的系统上,效率节省只是一个问题,因此,在您的情况下,我将使事情变得简单,而现在不必担心阻塞。在您的情况下,如果您的应用程序似乎挂断并且变得无响应,那么您很有可能在本不应该进行的地方进行阻塞调用。

    最后,如果您想使此代码具有可移植性和/或更快的速度,可能值得看一下 libev 之类的东西,它实际上是 select.select()的几种替代品,它们在不同的平台上都可以很好地工作。但是,它们的原理大致相似,因此最好暂时关注 select,直到您运行代码,然后研究更改它。

    另外,我注意到评论者建议使用 Twisted,它是一个提供更高级别抽象的框架,因此您无需担心所有细节。我个人过去曾遇到过一些问题,例如很难以方便的方式捕获错误,但是很多人都非常成功地使用了它-这只是他们的方法是否适合您对事物的思考方式的问题。至少值得调查一下,看看它的风格是否比我更适合您。我来自使用C/C++编写网络代码的背景知识,所以也许我只是坚持我所知道的(Python select模块与它所基于的C/C++版本非常接近)。

    希望我在那里已经做了足够的解释-如果您还有问题,请在评论中告诉我,我可以在答案中添加更多详细信息。

    关于python - 如何通过套接字和选择模块管理聊天服务器(Python)的套接字连接,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15231861/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com