35.1 编写UDP客户/服务端代码
35.2 编写TCP客户/服务端代码
35.3 小结
PyQt5提供了QUdpSocket和QTcpSocket类分别用于实现UDP和TCP传输协议。这两个协议都可以用来创建网络客户端和服务端的应用程序。前者(UDP)以包的形式将数据从一台主机发送到另一台主机上。它只负责发送,但并不在乎是否发送成功。优点是轻巧快速;而后者(TCP)能够为应用程序提供可靠的通信连接,它以流的形式来发送数据,能够确保数据无差错地送达到其他计算机。优点是安全可靠。
TCP几乎已经是两个互联网程序通信的默认选择,但是UDP在某些方面还是有优势的(比如广播)。总之,具体情况还是要具体分析。在这一章我们就来了解下如果使用PyQt5所提供的相关网络模块来进行通信。
我们通过以下例子来了解下如何使用QUdpSocket——服务端程序不断发送系统时间给客户端,客户端接收数据并进行显示:
服务端
客户端
以下是服务端代码:
import sys
from PyQt5.QtCore import Qt, QTimer, QDateTime
from PyQt5.QtNetwork import QUdpSocket, QHostAddress
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel, QVBoxLayout
class Server(QWidget):
def __init__(self):
super(Server, self).__init__()
# 1
self.sock = QUdpSocket(self)
# 2
self.label = QLabel('0', self)
self.label.setAlignment(Qt.AlignCenter)
self.btn = QPushButton('Start Server', self)
self.btn.clicked.connect(self.start_stop_slot)
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.label)
self.v_layout.addWidget(self.btn)
self.setLayout(self.v_layout)
# 3
self.timer = QTimer(self)
self.timer.timeout.connect(self.send_data_slot)
def start_stop_slot(self):
if not self.timer.isActive():
self.btn.setText('Stop Server')
self.timer.start(1000)
else:
self.btn.setText('Start Server')
self.timer.stop()
def send_data_slot(self):
message = QDateTime.currentDateTime().toString()
self.label.setText(message)
datagram = message.encode()
self.sock.writeDatagram(datagram, QHostAddress.LocalHost, 6666)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Server()
demo.show()
sys.exit(app.exec_())
1. 实例化一个QUdpSocket对象;
2. 实例化QLabel和QPushButton控件并布局,按钮所连接的槽函数用来控制定时器QTimer的启动与停止。当定时器启动后,服务器每过一秒就会向客户端发送数据;
3. 实例化一个QTimer对象,并将timeout信号和槽函数连接起来。在槽函数中,笔者首先获取到当前的系统时间并存储到message变量中,然后将QLabel控件的值设为message显示在窗口中。接着调用encode()方法对message进行编码以用于传输。最后调用QUdpSocket对象的writedatagram()方法将编码后的字节数据发送到本地主机地址,目标端口为6666;
上述程序中用到的QHostAddress类通常与QTcpSocket, QTcpServer和QUdpSocket一起使用来连接主机或者搭建服务器。以下是我们可能会在程序中用到的一些地址:
常量 | 值 | 描述 |
QHostAddress.Null |
0 | 空地址对象,等同于QHostAddress() |
QHostAddress.LocalHost |
2 | IPv4本地主机地址,等同于QHostAddress(“127.0.0.1”) |
QHostAddress.LocalHostIPv6 |
3 | IPv6本地主机地址,等同于QHostAddress("::1") |
QHostAddress.Broadcast |
1 | IPv4广播地址,等同于QHostAddress("255.255.255.255") |
QHostAddress.AnyIPv4 |
6 | 任何IPv4地址,等同于QHostAdress("0.0.0.0"),与该常量绑定的套接字只会监听IPv4接口 |
QHostAddress.AnyIPv4 | 5 | 任何IPv6地址,等同于QHostAdress("::"),与该常量绑定的套接字只会监听IPv6接口 |
QHostAddress.Any |
4 | 任何双协议栈地址,与该常量绑定的套接字可以监听IPv4接口和IPv6接口 |
点击按钮后QLabel显示系统时间,同时该时间数据也不断被发送到客户端:
如果再按下按钮的话,时间停止更新,数据也会停止发送。
以下是客户端代码:
import sys
from PyQt5.QtNetwork import QUdpSocket, QHostAddress
from PyQt5.QtWidgets import QApplication, QWidget, QTextBrowser, QVBoxLayout
class Client(QWidget):
def __init__(self):
super(Client, self).__init__()
# 1
self.sock = QUdpSocket(self)
self.sock.bind(QHostAddress.LocalHost, 6666)
self.sock.readyRead.connect(self.read_data_slot)
# 2
self.browser = QTextBrowser(self)
self.layout = QVBoxLayout()
self.layout.addWidget(self.browser)
self.setLayout(self.layout)
def read_data_slot(self):
while self.sock.hasPendingDatagrams():
datagram, host, port = self.sock.readDatagram(
self.sock.pendingDatagramSize()
)
messgae = 'Date time: {}\nHost: {}\nPort: {}\n\n'.format(datagram.decode(), host.toString(), port)
self.browser.append(messgae)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Client()
demo.show()
sys.exit(app.exec_())
1. 实例化QUdpSocket对象并调用bind()方法绑定地址和端口。每次可以准备读取新数据时,readyRead信号就会发射,我们在该信号所连接的槽函数中进行读取操作。首先调用hasPendingDatagrams()来判断是否还有要读取的数据,如果有的话就调用readDatagram()来读取数据,传入该方法的参数为要读取的数据大小,我们可以用pendingDatagramSize()方法获取。
readDatagram()一共返回三个值,分别是数据(字节),主机地址(QHostAddress对象)以及端口号(整型值)。之后我们用decode()将数据解码,用QHostAddress对象的toString()方法来获取到地址字符串。最后调用append()方法将message值显示在QTextBrowser控件上;
2. 实例化一个QTextBrowser文本浏览框对象并进行布局。
运行截图如下:
好,我们现在先在命令行窗口中运行服务端代码,并点击按钮开发发送数据:
接着再打开一个命令行窗口运行客户端代码,可以发现客户端显示了来自服务端程序的数据,地址以及使用的端口(该端口是系统随机分配的):
---
服务端和客户端程序如果都运行在一台主机上进行通信的话,我们也可以把它看成是进程间通信。如果要实现局域网通信(连接到同一网络下的两台主机),我们可以把服务端和客户端程序中的代码稍微修改下。
笔者现准备将服务端代码运行在另一台装有Windows系统的电脑上,而让客户端代码继续留在Mac电脑上运行。
首先需要获取下Mac电脑的内部IP地址,打开命令行窗口,输入ifconfig命令来查看(Linux上也是ifconfig,Windows上为ipconfig),可以看到地址为"192.168.1.102":
接着我们需要将服务端代码中数据发送的目标地址修改为上方地址:
self.sock.writeDatagram(datagram, QHostAddress("192.168.1.102"), 6666)
然后再将客户端代码中绑定的地址修改掉:
self.sock.bind(QHostAddress.Any, 6666)
其余代码保持不变。现在先在命令行窗口中运行服务端代码:
再运行客户端代码,发现通信成功:
在这一节中我们来编写一个聊天程序——客户端向服务端发送聊天内容,服务端将接收到的数据通过api传给青云客智能机器人处理(当然大家也可以选择用小黄鸡SimSimi或者图灵机器人),获取到返回的数据后再发送给客户端:
服务端
客户端
我们先来编写客户端代码:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtNetwork import QTcpSocket, QHostAddress
from PyQt5.QtWidgets import QApplication, QWidget, QTextBrowser, QTextEdit, QSplitter, QPushButton, \
QHBoxLayout, QVBoxLayout
class Client(QWidget):
def __init__(self):
super(Client, self).__init__()
self.resize(500, 450)
# 1
self.browser = QTextBrowser(self)
self.edit = QTextEdit(self)
self.splitter = QSplitter(self)
self.splitter.setOrientation(Qt.Vertical)
self.splitter.addWidget(self.browser)
self.splitter.addWidget(self.edit)
self.splitter.setSizes([350, 100])
self.send_btn = QPushButton('Send', self)
self.close_btn = QPushButton('Close', self)
self.h_layout = QHBoxLayout()
self.v_layout = QVBoxLayout()
# 2
self.sock = QTcpSocket(self)
self.sock.connectToHost(QHostAddress.LocalHost, 6666)
self.layout_init()
self.signal_init()
def layout_init(self):
self.h_layout.addStretch(1)
self.h_layout.addWidget(self.close_btn)
self.h_layout.addWidget(self.send_btn)
self.v_layout.addWidget(self.splitter)
self.v_layout.addLayout(self.h_layout)
self.setLayout(self.v_layout)
def signal_init(self):
self.send_btn.clicked.connect(self.write_data_slot) # 3
self.close_btn.clicked.connect(self.close_slot) # 4
self.sock.connected.connect(self.connected_slot) # 5
self.sock.readyRead.connect(self.read_data_slot) # 6
def write_data_slot(self):
message = self.edit.toPlainText()
self.browser.append('Client: {}'.format(message))
datagram = message.encode()
self.sock.write(datagram)
self.edit.clear()
def connected_slot(self):
message = 'Connected! Ready to chat! :)'
self.browser.append(message)
def read_data_slot(self):
while self.sock.bytesAvailable():
datagram = self.sock.read(self.sock.bytesAvailable())
message = datagram.decode()
self.browser.append('Server: {}'.format(message))
def close_slot(self):
self.sock.close()
self.close()
def closeEvent(self, event):
self.sock.close()
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Client()
demo.show()
sys.exit(app.exec_())
1. 实例化控件并完成界面布局,布局代码放在layout_init()函数中。如果大家忘了QSplitter控件的用法,可以去看下第二十四章;
2. 实例化一个QTcpSockset对象,并调用connectToHost()方法在指定端口上连接目标主机(此时会进行三次握手操作),如果客户端和服务端连接成功,则会发射connected()信号;
3. 在signal_init()函数中进行信号和槽连接的操作。当用户在文本编辑框QTextEdit中打完字后,点击发送按钮就可以将文本发送给服务端。在write_data_slot()槽函数中,我们首先获取文本编辑框中的文字,然后将它编码并用write()方法发送(不用再写目标地址和端口,因为之前已经用connectToHost()方法指定了),当然发送完后我们还有把文本编辑框清空掉。
4. 当用户点击关闭按钮后,调用close()方法关闭QTcpSocket套接字,当然窗口也得关掉。
5. 之前说过,当客户端和服务端连接成功的话,就会发射connected信号,我们将该信号连接到connected_slot()槽函数上,在该槽函数中我们只是简单的往屏幕上加了一行“Connected! Ready to chat! :)”文本来提示用户可以聊天了。
6. 跟QUdpSocket一样,当准备可以读取新数据时,readyRead信号就会发射。我们通过bytesAvailable()方法判断是否有数据,如果是的话则调用read()方法获取bytesAvailable()大小的数据。接着将数据解码并显示在屏幕上。
运行截图如下:
以下是服务端代码:
import sys
import json
import requests
from PyQt5.QtNetwork import QTcpServer, QHostAddress
from PyQt5.QtWidgets import QApplication, QWidget, QTextBrowser, QVBoxLayout
class Server(QWidget):
def __init__(self):
super(Server, self).__init__()
self.resize(500, 450)
# 1
self.browser = QTextBrowser(self)
self.v_layout = QVBoxLayout()
self.v_layout.addWidget(self.browser)
self.setLayout(self.v_layout)
# 2
self.server = QTcpServer(self)
if not self.server.listen(QHostAddress.LocalHost, 6666):
self.browser.append(self.server.errorString())
self.server.newConnection.connect(self.new_socket_slot)
def new_socket_slot(self):
sock = self.server.nextPendingConnection()
peer_address = sock.peerAddress().toString()
peer_port = sock.peerPort()
news = 'Connected with address {}, port {}'.format(peer_address, str(peer_port))
self.browser.append(news)
sock.readyRead.connect(lambda: self.read_data_slot(sock))
sock.disconnected.connect(lambda: self.disconnected_slot(sock))
# 3
def read_data_slot(self, sock):
while sock.bytesAvailable():
datagram = sock.read(sock.bytesAvailable())
message = datagram.decode()
answer = self.get_answer(message).replace('{br}', '\n')
new_datagram = answer.encode()
sock.write(new_datagram)
def get_answer(self, message):
payload = {'key': 'free', 'appid': '0', 'msg': message}
r = requests.get("http://api.qingyunke.com/api.php?", params=payload)
answer = json.loads(r.text)['content']
return answer
# 4
def disconnected_slot(self, sock):
peer_address = sock.peerAddress().toString()
peer_port = sock.peerPort()
news = 'Disconnected with address {}, port {}'.format(peer_address, str(peer_port))
self.browser.append(news)
sock.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Server()
demo.show()
sys.exit(app.exec_())
1. 实例化一个QTextBrowser控件并进行布局;
2. 实例化一个QTcpServer对象,调用listen()方法对指定地址和端口进行监听。如果能够监听,则返回True,否则返回False。可以调用errorString()方法来获取监听失败的原因;
每当有来自客户端的新连接请求,QTcpServer就会发送newConnection信号。在与该信号连接的new_slot_socket()槽函数中,我们调用nextPendingConnection()方法来得到一个与客户端连接的QTcpSocket对象,并通过peerAddress()方法和peerPort()方法获取到客户端所在的主机地址和以及使用的端口;
3. 在与readyRead信号连接的read_data_slot()槽函数中,我们将来自客户端的数据解码,并作为参数传给get_answer()函数来获取青云客智能机器人的回答(关于requests库的使用方法,大家可以去看下它的文档,非常简单)。接着将answer编码后再调用write()方法发送数据给客户端;
4. 当连接关闭的话,就会发射disconnected信号。当客户端窗口关闭,那么与服务端的连接就会关闭,此时disconnected信号就会发射。在disconnected_slot槽函数中,我们在屏幕上显示失联客户端所在的主机地址和使用的端口。接着调用close()方法关闭套接字。
好我们现在先运行服务端代码:
接着再打开一个命令行窗口运行客户端代码,客户端界面显示“Connected! Ready to chat! :)”文本:
服务端界面显示客户端所在主机的地址和使用的端口:
通过客户端发送文本,并接收来自服务端的回答:
我们还可以再打开一个命令行窗口来运行客户端代码(可以多开,这里就不再演示了)。
关闭客户端窗口,服务端界面显示失联客户端所在主机的地址和使用的端口:
1. 编写基于UDP协议的客户端和服务端代码,我们只需要用到QUdpSocket即可。但如果是基于TCP协议的话,我们需要QTcpSocket和QTcpServer两个类来进行编写;
2. 如果想要建立安全的SSL/TLS连接,大家可以使用QSslSocket来代替QTcpSocket;
3. 笔者在这章只是对PyQt5的相关网络模块作了一个简单的使用介绍,也并没有讲解太多理论内容。如果想要更深入了解用Python进行网络编程的相关知识,大家可以去看下这本由Brandon Rhodes和John Goerzen共同编写的《Python网络编程》
----------------------------------------------------------------------
喜欢的小伙伴可以加入这个Python QQ交流群一起学习:820934083