"Grasp the PyQt5" chapter thirty-fifth network applications

Thirty-fifth chapter of network applications

35.1 preparation of UDP client / server code

35.2 write TCP client / server code

35.3 Summary


Providing QUdpSocket PyQt5 QTcpSocket class and are used to implement TCP and UDP transport protocols. Both protocols can be used to create applications network client and server. The former (UDP) packets in the form of transmitting data from one host to another host. It is only responsible for sending, but did not care whether the transmission was successful. The advantage is light fast; the latter (TCP) to provide reliable communication for the applications connected to it in the form of a data stream is transmitted, to ensure error-free data delivery to the other computers. The advantage is safe and reliable.

 

TCP is the default selection procedure almost two Internet communication, but UDP in some respects still have an advantage (such as radio). In short, specific circumstances or to specific analysis. In this chapter we have to learn to communicate if you use the relevant network module PyQt5 offer.

 

35.1 preparation of UDP client / server code

We continue to learn how to send the next time the system to the client, the client receives the data and displays it using QUdpSocket-- server program by the following example:

Server

Client

 

The following is the server code:

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. Examples of a QUdpSocket objects;

2. Examples of control and layout QPushButton QLabel and groove connection function buttons for controlling the start and stop the timer QTimer. When the timer is started, every one second server sends data to the client;

3. Examples of a QTimer object and timeout signal and connect the slot function. Function in the slot, the author is first acquired current system time and stored in the variable message, and set the value QLabel control message displayed in the window. Then call encode () method of encoding message for transmission. Finally, call object QUdpSocket writedatagram () method sends the bytes of encoded data to the local host address, destination port 6666;

 

Used in the above-described procedure is typically used in conjunction with class QHostAddress QTcpSocket, QTcpServer QUdpSocket and structures to connect a host or server. Here are some addresses we might use in the program:

constant value description

QHostAddress.Null

0 Empty address object, equivalent to QHostAddress ()

QHostAddress.LocalHost

2 IPv4 address of the local host, equivalent to QHostAddress ( "127.0.0.1")

QHostAddress.LocalHostIPv6

3 IPv6 local host address, equivalent to QHostAddress ( ":: 1")

QHostAddress.Broadcast

1 IPv4 broadcast address, equivalent to QHostAddress ( "255.255.255.255")

QHostAddress.AnyIPv4

6 Any IPv4 address, equivalent to QHostAdress ( "0.0.0.0"), bound with the constant listening socket only IPv4 Interface
QHostAddress.AnyIPv4 5 Any IPv6 address, equivalent to QHostAdress ( "::"), and the binding constant of the socket will listen to IPv6 Interface

QHostAddress.Any

4 Any dual stack address, and the socket is bound can listen to the constant IPv4 interfaces and IPv6 interfaces


Run shot as follows:

Clicking on the button QLabel display system, while the time data is constantly sent to the client:

If we press the button, the time to stop the update, will stop sending data.

 

The following is the client code:

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. Examples of the object and call QUdpSocket bind () method bound address and port. Each time new data is ready to read, will transmit the readyRead signal, we read operation of the function in the slot of the connected signal. First call hasPendingDatagrams () to determine whether there is data to be read, if any, is called readDatagram () to read the data passed to the method parameters for the size of the data to be read, we can use pendingDatagramSize () method to get.

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万+

Guess you like

Origin blog.csdn.net/La_vie_est_belle/article/details/89810254