2013-03-05 52 views
1

对不起,打扰所有人,但我已经被困了一段时间了。如何通过套接字和选择模块管理聊天服务器(Python)的套接字连接

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

我早前问过如何让我的服务器'管理'这些​​客户端的连接,以便它可以重定向它们之间的数据。我得到了一个很好的答案,它为我提供了准确的代码,我显然需要这样做。

问题是我不明白它是如何工作的,我在评论中询问过,但除了一些文档链接外,我没有得到太多答复。

这里是我得到了什么:

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的情况下,可等待对象的能力。

我已经得到了rlist,待读取列表,wlist,等待写入列表,然后是xlist,即将发生的异常情况。

他将挂起写入列表分配给在我的聊天服务器部分中的“s”,它是在指定端口上监听的套接字。

这就像我觉得我理解得很清楚一样。但我真的很喜欢一些解释。

如果您不觉得我问了一个合适的问题,请在评论中告诉我,我会将其删除。我不想违反任何规则,而且我敢肯定我没有重复线索,因为我在做一些研究之前会诉诸问题。

谢谢!

+0

你看过Twisted Python吗?它是现成的 - http://twistedmatrix.com/documents/current/core/examples/#auto1 – 2013-03-06 00:30:25

回答

7

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

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

在下面的解释中记住系统正在做这种缓冲通常很有用,因为您会意识到操作系统中的TCP/IP堆栈独立于应用程序发送和接收数据 - 这样做是为了让您的应用程序可以有一个简单的接口(这就是套接字,这是隐藏代码中所有TCP/IP复杂性的一种方式)。

这样做读写的一种方法是阻止。例如,使用该系统,当您拨打recv()时,如果系统中有数据等待,则会立即返回。但是,如果没有数据等待,那么调用会阻止 - 也就是说,程序会暂停,直到有数据要读取。有时你可以用超时来做到这一点,但是在纯粹的阻塞IO中,你真的可以永远等待,直到另一端发送一些数据或关闭连接。

对于一些简单的情况,这不起作用,但只在与另一台机器通话的地方 - 当您在多个套接字上通信时,不能等待来自一台机器的数据因为另一个人可能会向你发送东西。还有其他问题,我不会在这里详细介绍 - 足以说这不是一个好方法。

一个解决方案是为每个连接使用不同的线程,所以阻塞是可以的 - 其他线程可以被阻塞而不会相互影响。在这种情况下,每个连接需要两个线程,一个读取,一个写入。然而,线程可能是棘手的野兽 - 你需要仔细地同步它们之间的数据,这可能会使编码变得复杂一些。而且,对于像这样的简单任务来说,它们效率不高。

select模块,可以单线程解决了这个问题 - 而不是阻塞在单个连接上,它可以让你一个函数,它说:“才睡觉,这些插座中的至少一个有一些数据,我可以读在上面“(这是我稍后会纠正的简化)。因此,一旦对select.select()的调用返回,您可以确定您正在等待的其中一个连接有一些数据,并且您可以放心地读取它(即使阻止了IO,如果您小心 - 因为您确定那里有数据,你永远不会阻止等待它)。

当你第一次启动你的应用程序时,你只有一个套接字,它是你的监听套接字。所以,你只能通过select.select()的通话。我之前做的简化是,实际上这个调用接受三个用于读,写和错误的套接字列表。第一个列表中的套接字被监视读取 - 因此,如果其中任何一个有数据要读取,则select.select()函数会将控制权返回给您的程序。第二个列表是用于写入的 - 你可能认为你总是可以写入一个套接字,但实际上,如果连接的另一端没有足够快地读取数据,那么系统的写入缓冲区可能会被填满,并且你可能暂时无法写入。看起来像给你代码的人忽略了这种复杂性,这对于一个简单的例子来说并不算太坏,因为通常缓冲区足够大,你不可能在这样的简单情况下遇到问题,但是这是一个问题,你应该在您的代码的其余部分工作后,将来的地址。最终列表会收到错误信息 - 这不是广泛使用,所以我现在就跳过它。通过空列表在这里没问题。

在这一点上,有人连接到你的服务器 - 至于select.select()所关心的是,这使得监听套接字“可读”,所以函数返回并且可读套接字列表(第一个返回值)将包括listen插座。

下一部分运行所有有数据读取的连接,并且您可以看到您的侦听套接字s的特例。该代码在其上调用accept(),它将从侦听套接字中获取下一个等待的新连接,并将其变为该连接的全新套接字(侦听套接字继续侦听,并且可能还有其他新连接正在等待,但这没关系 - 我会在第二秒)。全新的套接字被添加到connections列表中,这是处理侦听套接字的结束 - continue将移动到从select.select()返回的下一个连接(如果有的话)。

对于其他可读的连接,代码会调用recv()来恢复下一个1024字节(或者小于1024字节时可用的任何字节)。重要注意事项 - 如果您没有使用select.select()来确保连接可读,对recv()的这个调用可能会阻止并暂停程序,直到数据到达特定连接 - 希望这可以说明为什么需要select.select()

一旦读取了一些数据,代码就会在所有其他连接(如果有)上运行,并使用send()方法将数据向下复制。代码正确跳过与刚刚到达的数据(这是关于q != i的业务)相同的连接,并且跳过s,但正如它发生的情况,这不是必需的,因为据我所见,它实际上从未添加到connections列表中。

所有可读连接处理完毕后,代码返回到select.select()循环等待更多数据。请注意,如果连接仍有数据,则该调用立即返回 - 这就是为什么只接受来自侦听套接字的单个连接即可。如果有更多的连接,select.select()将立即再次返回,循环可以处理下一个可用的连接。你可以使用非阻塞IO来提高效率,但它使事情变得更加复杂,所以让我们现在就让事情变得简单。

这是一个合理的说明,但不幸的是它的一些问题遭受:

  1. 正如我所说,该代码假定您可以随时拨打send()安全的,但如果你有一个连接在另一端的ISN” t正确接收(也许该机器已超载),那么您的代码可能会填满发送缓冲区,然后在尝试呼叫send()时挂起。
  2. 该代码无法应对连接关闭,这通常会导致从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++版本)。

希望我已经在这里充分解释了一些事情 - 如果您还有问题,请在评论中告诉我,我可以在我的答案中添加更多详细信息。