我正在尝试模拟以下简单的socat(1)
命令的行为:
socat tcp-listen:SOME_PORT,fork,reuseaddr exec:'SOME_PROGRAM'
上面的命令创建一个派生的TCP服务器,该服务器分叉并为每个连接执行SOME_PROGRAM
,将上述命令的stdin
和stdout
都重定向到TCP套接字。
这是我想要实现的目标:
asyncio
创建一个简单的TCP服务器以处理多个并发连接。SOME_PROGRAM
作为子过程启动。SOME_PROGRAM
的标准输入。SOME_PROGRAM
的标准输出中接收到的所有数据传递到套接字。SOME_PROGRAM
时,向套接字写一个告别消息和退出代码,然后关闭连接。我想用纯Python做到这一点,而无需使用asyncio
模块使用外部库。
这是我到目前为止编写的代码(如果很长,不要害怕,它只是被大量注释和隔开):
import asyncio
class ServerProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.client_addr = transport.get_extra_info('peername')
self.transport = transport
self.child_process = None
print('Connection with {} enstablished'.format(self.client_addr))
asyncio.ensure_future(self._create_subprocess())
def connection_lost(self, exception):
print('Connection with {} closed.'.format(self.client_addr))
if self.child_process.returncode is not None:
self.child_process.terminate()
def data_received(self, data):
print('Data received: {!r}'.format(data))
# Make sure the process has been spawned
# Does this even make sense? Looks so awkward to me...
while self.child_process is None:
continue
# Write any received data to child_process' stdin
self.child_process.stdin.write(data)
async def _create_subprocess(self):
self.child_process = await asyncio.create_subprocess_exec(
*TARGET_PROGRAM,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE
)
# Start reading child stdout
asyncio.ensure_future(self._pipe_child_stdout())
# Ideally I would register some callback here so that when
# child_process exits I can write to the socket a goodbye
# message and close the connection, but I don't know how
# I could do that...
async def _pipe_child_stdout(self):
# This does not seem to work, this function returns b'', that is an
# empty buffer, AFTER the process exits...
data = await self.child_process.stdout.read(100) # Arbitrary buffer size
print('Child process data: {!r}'.format(data))
if data:
# Send to socket
self.transport.write(data)
# Reschedule to read more data
asyncio.ensure_future(self._pipe_child_stdout())
SERVER_PORT = 6666
TARGET_PROGRAM = ['./test']
if __name__ == '__main__':
loop = asyncio.get_event_loop()
coro = loop.create_server(ServerProtocol, '0.0.0.0', SERVER_PORT)
server = loop.run_until_complete(coro)
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
还有我正在尝试作为子流程运行的./test
程序:
#!/usr/bin/env python3
import sys
if sys.stdin.read(2) == 'a\n':
sys.stdout.write('Good!\n')
else:
sys.exit(1)
if sys.stdin.read(2) == 'b\n':
sys.stdout.write('Wonderful!\n')
else:
sys.exit(1)
sys.exit(0)
不幸的是,上面的代码并没有真正起作用,我对下一步的尝试感到迷茫。
有效的方法:
htop
中看到它,并且我也可以在发送b\n
后立即看到它终止。无法正常工作:
基本上其他任何事情...
await self.child_process.stdout.read(100)
似乎永远不会终止:相反,它只会在子进程死后 终止,结果只是b''
(空bytes
对象)。 self.child_process.returncode
一起向套接字发送“再见”消息,但是我不知道不知道如何以一种有意义的方式做到这一点。我尝试过的事情:
asyncio.loop.subprocess_exec()
而不是asyncio.create_subprocess_exec()
创建子进程。这解决了知道进程何时终止的问题,因为我可以实例化asyncio.SubprocessProtocol
的子类并使用其process_exited()
方法,但 并不能真正帮到我,因为如果我采用这种方式,那么我就不再有必要与流程stdin
或stdout
进行交流了!也就是说,我没有Process
对象可与...进行交互... asyncio.loop.connect_write_pipe()
和loop.connect_read_pipe()
并没有碰运气。那么,有人可以帮我弄清楚我在做什么错吗?必须有一种使这项工作顺利进行的方法。刚开始时,我在寻找一种轻松使用某些管道重定向的方法,但是我不知道这是否可能。是吗?看起来应该是这样。
我可以在15分钟内使用fork()
,exec()
和dup2()
用C语言编写此程序,因此我必须缺少一些东西!任何帮助表示赞赏。
答案 0 :(得分:5)
您的代码有两个立即实施的问题:
"a\n"
,则子进程将仅接收"a"
。这样,子进程就永远不会遇到预期的"a\n"
字符串,并且它总是在读取两个字节后终止。这说明了来自子流程的空字符串(EOF)。另一个问题是在设计级别。如评论中所述,除非您的明确意图是实现新的异步协议,否则recommended会坚持使用更高级别的stream-based API,在本例中为start_server
函数。甚至更低级的功能,例如SubprocessProtocol
,connect_write_pipe
和connect_read_pipe
也不是您想要在应用程序代码中使用的功能。该答案的其余部分假定基于流的实现。
start_server
接受协程,当客户端连接时,协程将作为新任务生成。它用两个异步流参数调用,一个用于读取,一个用于写入。协程包含与客户沟通的逻辑;在您的情况下,它将生成子流程并在其与客户端之间传输数据。
请注意,套接字与子进程之间的双向数据传输无法通过简单的循环来实现,即先进行读取再进行写入。例如,考虑以下循环:
# INCORRECT: can deadlock (and also doesn't detect EOF)
child = await asyncio.create_subprocess_exec(...)
while True:
proc_data = await child.stdout.read(1024) # (1)
sock_writer.write(proc_data)
sock_data = await sock_reader.read(1024)
child.stdin.write(sock_data) # (2)
这种循环容易产生死锁。如果子进程正在响应从TCP客户端接收到的数据,则有时它仅在接收到一些输入后才提供输出。这将无限期地阻塞(1)处的循环,因为只有在向孩子发送stdout
后,它才能从孩子的sock_data
获取数据,稍后在(2)处发生。实际上,(1)等待(2),反之亦然,构成死锁。请注意,反转传输顺序将无济于事,因为如果TCP客户端正在处理服务器子进程的输出,则循环将死锁。
使用asyncio时,这种死锁很容易避免:只需并行生成两个协程 ,一个协程将数据从套接字传输到子进程的stdin,另一个将数据从子进程的stdin传输。标准输出到套接字。例如:
# correct: deadlock-free (and detects EOF)
async def _transfer(src, dest):
while True:
data = await src.read(1024)
if data == b'':
break
dest.write(data)
child = await asyncio.create_subprocess_exec(...)
loop.create_task(_transfer(child.stdout, sock_writer))
loop.create_task(_transfer(sock_reader, child.stdin))
await child.wait()
此设置与第一个while
循环之间的区别在于,两次传输彼此独立。之所以不会发生死锁,是因为从套接字进行的读取永远不会等待从子进程进行的读取,反之亦然。
适用于该问题,整个服务器将如下所示:
import asyncio
class ProcServer:
async def _transfer(self, src, dest):
while True:
data = await src.read(1024)
if data == b'':
break
dest.write(data)
async def _handle_client(self, r, w):
loop = asyncio.get_event_loop()
print(f'Connection from {w.get_extra_info("peername")}')
child = await asyncio.create_subprocess_exec(
*TARGET_PROGRAM, stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE)
sock_to_child = loop.create_task(self._transfer(r, child.stdin))
child_to_sock = loop.create_task(self._transfer(child.stdout, w))
await child.wait()
sock_to_child.cancel()
child_to_sock.cancel()
w.write(b'Process exited with status %d\n' % child.returncode)
w.close()
async def start_serving(self):
await asyncio.start_server(self._handle_client,
'0.0.0.0', SERVER_PORT)
SERVER_PORT = 6666
TARGET_PROGRAM = ['./test']
if __name__ == '__main__':
loop = asyncio.get_event_loop()
server = ProcServer()
loop.run_until_complete(server.start_serving())
loop.run_forever()
还必须修改随附的test
程序,使其在每个sys.stdout.write()
之后调用sys.stdout.flush()
,否则消息将在其stdio缓冲区中徘徊,而不是发送给父级。
当我刚开始时,我正在寻找一种方法来轻松地使用一些管道重定向,但是我不知道这是否可能。是吗?看起来应该是这样。
在类似Unix的系统上,当然可以将套接字重定向到生成的子进程,以便子进程直接与客户端通信。 (旧的inetd
Unix服务器是这样工作的。)但是asyncio不支持该操作模式,原因有两个:
即使您不关心可移植性,也请考虑第二点:您可能需要处理或记录TCP客户端和子流程之间交换的数据,如果将它们焊接在一起,则不能这样做。内核级别。此外,与仅处理不透明的子流程相比,在异步协程中更容易实现超时和取消。
如果您的用例很好地解决了不可移植性和无法控制通信的问题,那么您可能首先不需要asyncio-没有什么阻止您生成运行经典的阻塞服务器的线程,该服务器处理每个客户端与您在C语言中编写的os.fork
,os.dup2
和os.execlp
顺序相同。
编辑
正如OP在注释中指出的那样,原始代码通过杀死子进程来处理TCP客户端断开连接。在流层,流丢失反映了连接丢失,它表示文件结束或引发异常。在上面的代码中,可以用处理该情况的更具体的协程替换通用的self._transfer()
来轻松应对连接丢失。例如,代替:
sock_to_child = loop.create_task(self._transfer(r, child.stdin))
...一个人可以写:
sock_to_child = loop.create_task(self._sock_to_child(r, child))
并这样定义_sock_to_child
(未经测试):
async def _sock_to_child(self, reader, child):
try:
await self._transfer(reader, child.stdin)
except IOError as e:
# IO errors are an expected part of the workflow,
# we don't want to propagate them
print('exception:', e)
child.kill()
如果子项的寿命超过TCP客户端的时间,则child.kill()
行可能永远不会执行,因为协程将在_handle_client
内的src.read()
中暂停时,被_transfer()
取消。 / p>