Python-通过套接字与子进程通信

问题描述:

我正在寻找复制Git和Rsync之类的程序通过SSH连接进行通讯和传输数据的方式,但是使用Python.我知道这些程序会分叉并执行一个SSH命令,该命令会在服务器端启动一个进程,并且通过与分叉子进程的STDIN和STDOUT进行对话的父进程来实现通信.

I am looking to replicate the way programs like Git and Rsync communicate and transfer data over an SSH connection, but in Python. I understand that these programs fork and exec an SSH command that starts a process on the server side and communication is achieved by the parent processing talking to the STDIN and STDOUT of the forked child process.

在CI中,通过创建一个Unix套接字对(s0,s1),分叉该进程,将分叉进程的stdin/stdout指向子进程上的s1,然后读取和写入套接字s0来完成此操作在父进程上.

In C I have seen this done by creating a Unix socket pair (s0, s1), forking the process, pointing the stdin/stdout of the forked process to s1 on the child process, and then reading and writing to the socket s0 on the parent process.

我想在Python3中做同样的事情.作为概念验证,这是我的玩具远程外壳的实现,该外壳向套接字发送命令并从同一套接字接收输出:

I want to do the same thing in Python3. As a proof of concept, here is my implementation of a toy remote shell that sends commands down a socket and receives the output from the same socket:

import subprocess
import socket


ssh_cmd = 'ssh -q -T ubuntu@xxxxxxxx'
s_local, s_remote = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)

ssh = subprocess.Popen(ssh_cmd, shell=True,
                       stdout=s_remote,
                       stdin=s_remote,
                       close_fds=True)
s_remote.close()

s_local.send('ls -l\n'.encode())
print(s_local.recv(1024).decode())
s_local.send('uname -a\n'.encode())
print(s_local.recv(1024).decode())
s_local.close()
ssh.kill()

这类作品.但是如果我向套接字发送"true \ n",则可以使它死锁,因为"recv"正在阻止,所以效果不佳.

This kind of works. But I can deadlock it if I send 'true \n' down the socket because 'recv' is blocking so that's not great.

  1. 在网络编程中这是明智/常见的事情吗?
  2. 使用不会死锁的Python3标准库,有什么更惯用的方式?

我将网络浏览器连接到在远程计算机上运行的命令的 stdin stdout SSH连接.听起来您正在尝试做类似的事情:将套接字连接到在远程计算机上运行的程序,以便客户端可以连接到主机/端口并使用所述远程程序.

I connected a web browser to the stdin and stdout of a command running on the remote end of a SSH connection. It sounds like you're trying to do something similar: connect a socket to a program running on a remote machine so that clients can connect to a host/port and use said remote program.

您需要做的第一件事是建立SSH连接.我在子进程中使用外部 ssh 程序(取决于操作系统,是 ssh 还是 plink ).您可以只使用 subprocess.Popen 类来执行此操作.子进程启动(外部) ssh 应用程序以连接到远程主机并运行所需的命令.然后坐在那里聆听其"stdin",并愿意回复其"stdout".

The first thing you need to do is establish a SSH connection. I use an external ssh program (either ssh or plink depending on OS) in a subprocess. You could just use class subprocess.Popen to do that. The subprocess launches the (external) ssh application to connect to the remote host and runs the required command. That then sits there listening on its 'stdin' and willing to reply on its 'stdout'.

(您可以在此处使用Python SSH实现,例如Paramiko)

(You could possibly use Python SSH implementation such as Paramiko here)

非常高的水平(其中 remote_command 是要在远端运行的东西):

At a very high level (where remote_command is the thing to run on the far end):

import subprocess
command = ['ssh', 'user@host', 'remote_command']

sproc = subprocess.Popen(command, shell=False,
                         stdin = subprocess.PIPE,
                         stdout = subprocess.PIPE,
                         stderr = subprocess.PIPE)

这将为您提供一个子流程,您可以通过其 stdin stdout stderr 与之进行交互.

This leaves you with a subprocess that you can interact with through its stdin, stdout and stderr.

首先使此工作正常.

下一步,您需要绑定套接字,监听并接受连接.然后从客户端读取并写入服务器.我写了一个轻量级的 Socat (受 Socat 的启发)类:

The next thing you need is to bind a socket, listen and accept a connection. Then read from the client and write to the server. I wrote a lightweight Socat (inspired by Socat) class:

import socket

class Socat(object):

EOT = '\004\r\n'

def __init__(self, binding, stream, eot = EOT):

  self.stream = stream
  self.eot = eot

  self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  self.socket.bind(binding)
  self.socket.listen(5)

  while True:
    print "waiting for connection"
    (clientsocket, address) = self.socket.accept()
    print "connection from %s:%s" % address
    data = clientsocket.recv(1024)
    self.stream.write(data)
    while data:
      data = self.stream.readline()
      if data:
        print "read %s bytes" % len(data)
        if data == self.eot:
          print "read termination signature"
          clientsocket.close()
          data = None
        else:
          clientsocket.send(data)

您应该阅读以下套接字:在此处很有用.

You should read up on Sockets: this and this are useful here.

然后您可以使用它连接到您的 remote_command :

You can then use this to connect to your remote_command:

Socat( ('localhost', 8080), sproc)

有一些遗漏的地方

您会注意到 socat 类引入了 stream (不同于我的示例),该流具有 write readline 代码>方法.我实际上将SSH连接封装在一个类中,但是我将其留给读者练习!

You'll notice the socat class takes in a stream (unlike my example) that has write and readline methods. I actually encapsulated the SSH connection in a class but I'll leave that as an exercise for the reader!

(基本上是 class Stream ,其中包含 sproc ,并提供 write readline 方法-有效地 sproc.stdin.write() sproc.stdout.readline()-您明白了!)

(Basically a class Stream that contains the sproc and provides write and readline methods - effectively sproc.stdin.write() and sproc.stdout.readline() - you get the idea!)

此外,您还需要一个协议以允许 remote_command 发出传输结束信号.我使用了ASCII EOT 字符,并且我的远程程序在响应结束时发送了该字符. socat 使用它来关闭客户端连接并接受新的连接.根据您的用例,您需要提出一个适合您的协议.

Also, you need a protocol to allow remote_command to signal the end of a transmission. I used the ASCII EOT character and my remote program sends that at the end of a response. The socat uses that to close the client connection and accept a new one. Depending on your use-case you will need to come up with a protocol that works for you.

最后

以上所有内容均可改进.上面是我的第一篇文章,因为很容易看到内部工作原理.从那以后,我将它扩展为在线程中运行接受循环并优雅地处理断开连接等.

All of the above can be improved upon. What's above is my first cut because it is simple to see the inner workings. I have since extended it to run the accept loop in a thread and gracefully handle disconnects and so-on.

但希望以上内容是一个合理的起点.

But hopefully what's above is a reasonable starting point.