Python 在网络重定向命令输出中的作用。





5.00/5 (4投票s)
Python 有助于多线程 GUI 和网络应用程序的开发。
引言
如果你在派对上大声说你用 Python 在 Linux 中创建了一个有趣的应用程序,并且派对上的人们想了解更多,那么你绝对是找对了人。你将看到的用 Python 实现的应用程序涉及网络编程(UDP)和 GUI 开发。还有什么比 Python 应用程序更有趣的呢,对吧?作为其强大功能的证明,用 Python 编写脚本感觉就像用编程语言编写代码。你可以进行许多使用 C++ 等编程语言时可以进行的系统调用。大多数情况下,无论你使用的操作系统是什么,进行此类调用的效果都相似。
如果我开始滔滔不绝地讲如何使用 Linux 命令做某事,你们中的一些人将能够向我展示更好的命令来执行相同的任务。Linux 提供了大量的命令供日常使用。最重要的是,你可以将一个命令的输出通过管道和重定向传递给子进程中的另一个命令。想象一下 Linux 中所有命令组合的数量。一般来说,我遵循的座右铭是“让 Linux 做它最擅长的事情”,而不是急于用 Java 或 C++ 编写代码。但是,我承认 Linux 并非万能药,我解决难题的下一步是看看是否可以用脚本来解决问题。我和许多其他程序员选择的脚本语言是 Python。我之所以被 Python 吸引,是因为其严格的缩进规则使其具有可读性。
现在我们来谈谈问题的关键。我想做的是在 Linux 中运行一个命令,并将输出显示在可能在另一台计算机上运行的另一个进程中。我设想能够使用一个名为 sotp
(send output to process,发送输出到进程)的命令,如下所示
echo "Bello mondo" | sotp
发生这种情况时,如果程序 ReceDisplay
正在运行,它应该接收并显示 Bello mondo
,因为那是 echo "Bello mondo"
的输出。
帮帮我,好吗?我们知道,两个进程(无论是在同一台计算机上还是在不同的计算机上)进行通信的一种方式是通过网络协议。我相信你会同意 UDP(无连接协议)足以满足我们的需求,所以我们不会去管 TCP。对于此应用程序,我们也不会关心 IPV6;我们的网络地址将只是 IPV4。在开发过程中,我们将使用 IPV4 地址 127.0.0.1,这是环回地址或本地地址。发送到此地址的数据包将只是循环回来并返回到发起机器。此时,为了方便起见,我们将在同一台机器上运行源应用程序和目标应用程序。在部署之前,地址可以很容易地修改为适当的数字。
计算机通过其 NIC(网络接口卡)与网络交互,以传输和接收数据包。每个 NIC 都由其制造商标记一个假定唯一的 MAC(媒体访问控制)号。理论上,网络上的两台计算机应该能够通过交换数据包成功通信,如果网络知道它们的 MAC 号码。但是,我们反而为 NIC 分配了 IP 地址,以便为网络外部的交换提供路由信息。网络编程中另一个重要的数字是端口号。因此,当数据包到达目标 IP 地址时,驻留的操作系统应该将它们带到它们旅程的最后一站,即可以使用它们的应用程序。机器的 NIC 或 IP 地址在机器上运行的许多应用程序之间共享。这些应用程序中的每一个都占用 NIC 的一个插槽,由端口号标识。在我们的应用程序中,我们将使用 51001 作为我们的端口号。如果需要更多的端口号,我们将向上移动一个数字到 51002、51003、51004,依此类推。
发送方和接收方
考虑到这些因素,现在让我们创建将接收和显示输出数据的应用程序。
#!/usr/bin/env python3
#filename ReceDisplay.py
import socket
IP_ADDR = ""
PORT = 51001
sock = socket.socket(socket.AF_INET, # IPv4
socket.SOCK_DGRAM) # UDP
sock.bind((IP_ADDR, PORT))
print "Server Started: ", PORT
while True:
(data_line, addr) = sock.recvfrom(1024) # buffer size is 1024 bytes
print(data_line.decode())
如您所见,正在建立一个端口为 51001 且 IP_ADDR 为“any”的套接字。此套接字将嗅探机器上所有 IP 地址的数据并一次接收 1024 字节。这足以容纳一行字符。我们将确保一次发送一行数据。
没有发送方就无法测试接收方,为此,我们有下一个程序。
#!/usr/bin/env python3
#filename sotp.py
import socket
import sys
IP_ADDR = "127.0.0.1"
PORT = 51001
MESSAGE = "Transfer successful, but no message passed."
num_args = len(sys.argv)
# Check if stdin might be filled
if not sys.stdin.isatty():
MESSAGE = sys.stdin.read()
# Remove the newline character
MESSAGE = MESSAGE[:-1]
print("Target port:", PORT)
print("Message:", MESSAGE)
sock = socket.socket(socket.AF_INET, # IPv4
socket.SOCK_DGRAM) # UDP
sock.sendto(MESSAGE.encode(), (IP_ADDR, PORT))
此程序将把 MESSAGE
发送到 IP 地址 127.0.0.1 上端口号为 51001 的应用程序,我们说过这是本地地址。如果我们想将其发送到不同的目标,我们可以简单地修改此数字以达到所需目标。我们还可以做一些巧妙的事情,例如将 MESSAGE
广播到多个 IP 地址。
请注意,在 sotp.py 中,MESSAGE
是从 stdin
(标准输入流)读取的。嗯,当管道命令时,本质上发生的是当前进程中命令的输出被作为输入传输到子进程中管道符号后面的命令。例如,如果您将 command1
和 command2
视为有效的 Linux 命令,像这样通过管道连接 command1
和 command2
command1 | command2
将 command1
的输出从当前进程的 stdout
重定向到 command2
运行的子进程的 stdin
。现在,将 command2
替换为 python sotp.py
,您就会明白为什么 MESSAGE
是从 stdin
读取的。
至此,我们已拥有运行应用程序所需的一切。首先,启动程序 ReceDisplay.py 并保持打开状态。接下来,运行以下命令
echo "Bello mondo" | python3 sotp.py
您将观察到 B<span style="text-decoration: none;">ello mondo</span>
的输出显示在 ReceDisplay.py 上。
GUI 的便利性
让我们看看是否可以进一步改进我们的应用程序。在当前的设置下,ReceDisplay
只能处理一个命令的输出。但是,如果我们将 ReceDisplay
变成一个带有多个页面的笔记本,我们可以在每个页面上打印更多命令的输出。Python GUI 程序以 .pyw 结尾。ReceDisplay.pyw 调用 tkinter
。
#!/usr/bin/env python3
#filename ReceDisplay.pyw
from tkinter import *
from tkinter.ttk import *
root = Tk()
nb = Notebook(root, height=700, width=600)
nb.pack(fill='both', expand='yes')
page1 = Frame()
page2 = Frame()
page3 = Frame()
page4 = Frame()
page5 = Frame()
page6 = Frame()
nb.add(page1, text='Page 1')
nb.add(page2, text='Page 2')
nb.add(page3, text='Page 3')
nb.add(page4, text='Page 4')
nb.add(page5, text='Page 5')
nb.add(page6, text='Page 6')
t1 = Text(page1)
t2 = Text(page2)
t3 = Text(page3)
t4 = Text(page4)
t5 = Text(page5)
t6 = Text(page6)
t1.insert(END, "Server 1: work in progress")
t1.see(END)
t2.insert(END, "Server 2: work in progress")
t2.see(END)
t3.insert(END, "Server 3: work in progress")
t3.see(END)
t4.insert(END, "Server 4: work in progress")
t4.see(END)
t5.insert(END, "Server 5: work in progress")
t5.see(END)
t5.insert(END, "Server 6: work in progress")
t5.see(END)
t1.pack(fill='both', expand='yes')
t2.pack(fill='both', expand='yes')
t3.pack(fill='both', expand='yes')
t4.pack(fill='both', expand='yes')
t5.pack(fill='both', expand='yes')
t6.pack(fill='both', expand='yes')
root.mainloop()
这是应用程序的骨架。它只生成 GUI;可以说,它没有实质内容。为了充实它,您需要注意的第一件事是代码目前是单线程的。我们希望运行六个服务器(每个页面一个)来从六个命令接收 MESSAGE
。为了不干扰绘制 GUI 的主线程,我们希望在六个单独的线程上运行我们的小型服务器。但是,请注意代码是如何以六个块的形式增长的。很快,我们将拥有一个臃肿的六个块代码,它们做着相同的事情(只是重复了六次)。嗯,Python 的另一个优点是它提供了面向对象编程的设施。如果我们只是将我们的设计实现为一个类,我们可以通过创建类的对象来重用代码。这正是我们将要做的。
#!/usr/bin/env python3
#filename ReceDisplay.pyw
from tkinter import *
from tkinter.ttk import *
from queue import Queue
import socket
import threading
IP_ADDR = ""
PORTS = [51001, 51002, 51003, 51004, 51005, 51006]
class LittleServer(): # Many instances of this class can be created
def __init__(self, PORTX): # PORT is passed during object initialization
self.sockx = socket.socket(socket.AF_INET, # IPv4
socket.SOCK_DGRAM) # UDP
self.sockx.bind((IP_ADDR, PORTX))
self.bthreadx = threading.Thread(target=self.run_server, args=())
self.bthreadx.daemon = True # Daemonize thread
self.bthreadx.start() # Thread starter with run_server()
self.eventx = threading.Event()
self.pagex = Frame() # Pages to be added to notebook
self.textx = Text(self.pagex)
self.textx.pack(fill='both', expand='yes')
self.textx.insert(END, "Server started - " + str(PORTX) + "\n")
self.textx.see(END)
self.textx.update_idletasks()
self.queuex = Queue()
def run_server(self):
buf_size = 1024 # buffer size is 1024 bytes
while True:
(data, addr) = self.sockx.recvfrom(buf_size)
self.queuex.put(data.decode()) # Put messages in queue for display_message()
self.eventx.set() # Set event to signal run_disp_msgs() on another thread
def display_message(self):
self.thx = threading.Thread(target=self.run_disp_msgs, args=())
self.thx.daemon = True # Daemonize thread
self.thx.start() # Thread started with run_disp_msgs()
def run_disp_msgs(self):
while True:
self.eventx.wait()
while not self.queuex.empty():
s = self.queuex.get()
self.textx.insert(END, s + "\n")
self.textx.see(END)
self.textx.update_idletasks()
self.eventx.clear()
root = Tk()
nb = Notebook(root, height=700, width=600)
nb.pack(fill='both', expand='yes')
lservs = [] # this list will contain LittleServers
for PORT in PORTS:
lserv = LittleServer(PORT)
nb.add(lserv.pagex, text='Page ' + str(PORT))
# add each server's page to notebook
lservs.append(lserv)
for lserv in lservs:
lserv.display_message() # display server content
root.mainloop()
太棒了,每个页面都有一个与之关联的套接字;现在六个命令可以通过将它们的 MESSAGE
发送到六个 PORT
来显示它们的输出。但是,为了实现这一点,port number
必须作为参数传递给修改后的 sotp.py 程序。不过,在进一步操作之前,让我们使用 ping
命令测试我们的应用程序。
ping bing.com | python3 sotp.py
请注意页面 51001 上的一行输出。这并非我们所期望的。通常,ping
命令的输出是无休止的行,我们通常用 Ctrl+C 中断。
不要绝望
显然,我们不能再相信管道能工作了。不过,我们不会轻易放弃。我们将改变策略。新策略,如修改后的 sotp.py 所示,采用 stdout
而不是 stdin
。另一个突出之处是,我们将把要执行的命令作为参数(除了 PORT
),并且传递的命令作为子进程运行(这可能是一个不变的东西)。
#!/usr/bin/env python3
#filename sotp.py
import socket
import subprocess
import sys
IP_ADDR = "127.0.0.1"
PORT = int(sys.argv[1]) # First argument is the PORT
MESSAGE = "Transfer successful, but no message passed."
sock = socket.socket(socket.AF_INET, # IPv4
socket.SOCK_DGRAM) # UDP
print("Target port:", PORT)
cmd = []
if len(sys.argv) > 2:
cmd = sys.argv[2:]
proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # stderr combined with stdout
universal_newlines=True)
while True:
if cmd != []:
MESSAGE = proc.stdout.readline()
if MESSAGE == '' and proc.poll() != None:
break
if MESSAGE:
# Remove the newline character
MESSAGE = MESSAGE[:-1]
sock.sendto(MESSAGE.encode(), (IP_ADDR, PORT))
if cmd == []:
break
战争结束
现在,最后,如果我们创建别名 sotp
alias sotp=python3 sotp.py
并发出我们的命令,就像
sotp 51004 ping bing.com
除了收获我们的劳动成果,别无他求。