0
点赞
收藏
分享

微信扫一扫

第三十五章 网络应用

yundejia 2022-01-06 阅读 92

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

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

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

我们通过以下例子来了解下如何使用QUdpSocket——服务端程序不断发送系统时间给客户端,客户端接收数据并进行显示:

服务端

img

客户端

img

以下是服务端代码:

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一起使用来连接主机或者搭建服务器。以下是我们可能会在程序中用到的一些地址:

img

运行截图如下:

img

点击按钮后QLabel显示系统时间,同时该时间数据也不断被发送到客户端:

img

如果再按下按钮的话,时间停止更新,数据也会停止发送。

以下是客户端代码:

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控件上;

  1. 实例化一个QTextBrowser文本浏览框对象并进行布局。

运行截图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dms1c6Tb-1641379059140)(data:image/svg+xml;utf8, )]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSIRO6JU-1641379059142)(data:image/svg+xml;utf8, )]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MJnzPZEd-1641379059150)(data:image/svg+xml;utf8, )]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0SwQXG0N-1641379059151)(data:image/svg+xml;utf8, )]

客户端

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eETsnTRK-1641379059152)(data:image/svg+xml;utf8, )]

以下是服务端代码:

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()方法获取到客户端所在的主机地址和以及使用的端口;

  1. 在与readyRead信号连接的read_data_slot()槽函数中,我们将来自客户端的数据解码,并作为参数传给get_answer()函数来获取青云客智能机器人的回答(关于requests库的使用方法,大家可以去看下它的文档,非常简单)。接着将answer编码后再调用write()方法发送数据给客户端;

  2. 当连接关闭的话,就会发射disconnected信号。当客户端窗口关闭,那么与服务端的连接就会关闭,此时disconnected信号就会发射。在disconnected_slot槽函数中,我们在屏幕上显示失联客户端所在的主机地址和使用的端口。接着调用close()方法关闭套接字。

好我们现在先运行服务端代码:

img

img

接着再打开一个命令行窗口运行客户端代码,客户端界面显示“Connected! Ready to chat! 😃”文本:

img

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

img

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

img

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

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

img

35.3 小结

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

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

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

举报

相关推荐

0 条评论