具有多个客户端和无限循环的Python asyncio协议行为

时间:2018-06-22 13:48:22

标签: python-3.x server client-server microservices python-asyncio

我很难理解更改后的回显服务器的行为,该服务器试图利用python 3的asyncio模块。

基本上,我有一个无限循环(可以说我想在建立连接时无限期地将某些数据从服务器流传输到客户端)。 MyServer.py

#! /usr/bin/python3
import asyncio
import os
import time

class MyProtocol(asyncio.Protocol):

    def connection_made(self, transport):
        peername = transport.get_extra_info('peername')
        print('Connection from {}'.format(peername))
        self.transport = transport

    def connection_lost(self, exc):
        asyncio.get_event_loop().stop()

    def data_received(self, data):
        i = 0
        while True:
            self.transport.write(b'>> %i' %i)
            time.sleep(2)
            i+=1

loop = asyncio.get_event_loop()
coro = loop.create_server(MyProtocol, 
    os.environ.get('MY_SERVICE_ADDRESS', 'localhost'), 
    os.environ.get('MY_SERVICE_PORT', 8100))
server = loop.run_until_complete(coro)

try:
    loop.run_forever()
except:
    loop.run_until_complete(server.wait_closed())
finally:
    loop.close()

下一步,当我与nc ::1 8100连接并发送一些文本(例如“测试”)时,我得到以下信息:

user@machine$ nc ::1 8100
*** Connection from('::1', 58503, 0, 0) ***
testing
>> 1
>> 2
>> 3
^C

现在,当我再次尝试使用nc进行连接时,没有收到任何欢迎消息,并且在尝试向服务器发送一些新文本后,我得到了以下错误的无尽信息:

user@machine$ nc ::1 8100
Is there anybody out there?
socket.send() raised exception
socket.send() raised exception
...
^C

只是向伤口添加盐,socket.send() raised exception消息继续向我的终端发送垃圾邮件,直到我杀死python服务器进程为止。

由于我是Web技术的新手(成为台式恐龙的时间太长了!),我不确定为什么会出现上述行为,也不清楚如何产生预期的行为,大致如下所示:

  1. 服务器启动
  2. 客户端1连接到服务器
  3. 服务器向客户端发送欢迎消息 4客户端1发送任意消息
  4. 只要客户端已连接,服务器就会将消息发送回客户端1
  5. 客户端1断开连接(假设电缆已拔出)
  6. 客户端2连接到服务器
  7. 对客户端2重复步骤3-6

任何启发都将受到欢迎!

1 个答案:

答案 0 :(得分:4)

代码有多个问题。

首先,ts = Timestamp('2001-02-10 00:01:00') print(ts.value) #981763260000000000 永不返回。在传输/协议级别,异步编程是单线程且基于回调。应用程序代码分散在data_received之类的回调中,事件循环运行show,监视文件描述符并根据需要调用回调。每个回调只允许执行简短的计算,调用传输方法并安排进一步的回调执行。回调不能做的是花费大量时间来完成或阻止等待。永远不会退出的data_received循环特别糟糕,因为它根本不允许事件循环运行。

这就是为什么代码仅在客户端断开连接后才会吐出异常的原因:从未调用while。它应该由事件循环调用,永不返回的connection_lost并没有给事件循环恢复的机会。在事件循环被阻止的情况下,该程序无法响应其他客户端,data_received继续尝试将数据发送到断开连接的客户端,并记录其失败的操作。

表达想法的正确方法如下:

data_received

请注意def data_received(self, data): self.i = 0 loop.call_soon(self.write_to_client) def write_to_client(self): self.transport.write(b'>> %i' % self.i) self.i += 1 loop.call_later(2, self.write_to_client) data_received的工作量很少,并且很快就会返回。没有对write_to_client的调用,并且绝对没有无限循环-“循环”隐藏在对time.sleep()的递归调用中。

此更改揭示了代码中的第二个问题。其write_to_client停止整个事件循环并退出程序。这使程序无法响应第二个客户端。解决方法可能是在MyProtocol.connection_lost中设置标志来替换loop.stop()

connection_lost

这允许多个客户端连接。


与上述问题无关,基于回调的代码编写起来有些麻烦,尤其是在考虑复杂的代码路径和异常处理时。 (想象一下试图用回调来表达嵌套循环,或者传播深度嵌入的回调中发生的异常。)asyncio支持基于协程的 streams 来替代基于回调的传输和协议。

协程可以编写看起来自然的代码,其中包含循环,并且看起来包含阻塞调用,这些代码在后台被转换为暂停点,使事件循环得以恢复。使用流,来自问题的代码如下所示:

def data_received(self, data):
    self._done = False
    self.i = 0
    loop.call_soon(self.write_to_client)

def write_to_client(self):
    if self._done:
        return
    self.transport.write(b'>> %i' % self.i)
    self.i += 1
    loop.call_later(2, self.write_to_client)

def connection_lost(self, exc):
    self._done = True

async def talk_to_client(reader, writer): peername = writer.get_extra_info('peername') print('Connection from {}'.format(peername)) data = await reader.read(1024) i = 0 while True: writer.write(b'>> %i' % i) await writer.drain() await asyncio.sleep(2) i += 1 loop = asyncio.get_event_loop() coro = asyncio.start_server(talk_to_client, os.environ.get('MY_SERVICE_ADDRESS', 'localhost'), os.environ.get('MY_SERVICE_PORT', 8100)) server = loop.run_until_complete(coro) loop.run_forever() 看起来很像talk_to_client的原始实现,但是没有缺点。如果没有可用的数据,则在使用data_received的每个点恢复事件循环。 awaittime.sleep(n)取代,其等效于await asyncio.sleep(n)。等待loop.call_later(n, <resume current coroutine>)可以确保协程在对等方无法处理其获取的输出时暂停,并在对等方断开连接时引发异常。