《快速掌握PyQt5》第三十五章 网络应用

第三十五章 网络应用

35.1 编写UDP客户/服务端代码

35.2 编写TCP客户/服务端代码

35.3 小结


PyQt5提供了QUdpSocket和QTcpSocket类分别用于实现UDP和TCP传输协议。这两个协议都可以用来创建网络客户端和服务端的应用程序。前者(UDP)以包的形式将数据从一台主机发送到另一台主机上。它只负责发送,但并不在乎是否发送成功。优点是轻巧快速;而后者(TCP)能够为应用程序提供可靠的通信连接,它以流的形式来发送数据,能够确保数据无差错地送达到其他计算机。优点是安全可靠。

TCP几乎已经是两个互联网程序通信的默认选择,但是UDP在某些方面还是有优势的(比如广播)。总之,具体情况还是要具体分析。在这一章我们就来了解下如果使用PyQt5所提供的相关网络模块来进行通信。

35.1 编写UDP客户/服务端代码

我们通过以下例子来了解下如何使用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)

其余代码保持不变。现在先在命令行窗口中运行服务端代码:

再运行客户端代码,发现通信成功:

35.2 编写TCP客户/服务端代码

在这一节中我们来编写一个聊天程序——客户端向服务端发送聊天内容,服务端将接收到的数据通过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! :)”文本:

服务端界面显示客户端所在主机的地址和使用的端口:

通过客户端发送文本,并接收来自服务端的回答:

我们还可以再打开一个命令行窗口来运行客户端代码(可以多开,这里就不再演示了)。

关闭客户端窗口,服务端界面显示失联客户端所在主机的地址和使用的端口:

35.3 小结

1. 编写基于UDP协议的客户端和服务端代码,我们只需要用到QUdpSocket即可。但如果是基于TCP协议的话,我们需要QTcpSocket和QTcpServer两个类来进行编写;

2. 如果想要建立安全的SSL/TLS连接,大家可以使用QSslSocket来代替QTcpSocket;

3. 笔者在这章只是对PyQt5的相关网络模块作了一个简单的使用介绍,也并没有讲解太多理论内容。如果想要更深入了解用Python进行网络编程的相关知识,大家可以去看下这本由Brandon Rhodes和John Goerzen共同编写的《Python网络编程》

欢迎关注我的微信公众号,发现更多有趣内容:

发布了83 篇原创文章 · 获赞 157 · 访问量 14万+

猜你喜欢

转载自blog.csdn.net/La_vie_est_belle/article/details/89810254
今日推荐