使用Python語言通過PyQt5和socket實現UDP伺服器
阿新 • • 發佈:2020-08-10
# 前言
最近做了一個小軟體,記錄一下相關內容。
## 已有條件
現在已有一個硬體裝置作為客戶端(暫稱其為“電路”)。
基於SIM卡,電路可以通過UDP協議傳輸資料(程式已經內建在電路中),只需要修改配置檔案(位於SD卡中,主要修改伺服器端的IP和埠)即可。
## 需求
我面向的需求是這樣的:我需要開發一個伺服器端的程式,接收多個客戶端發來的資料並開發視覺化介面。
## 總結
從開發角度和技術角度來看,軟體的基礎和核心技術是使用**UDP協議**進行資料傳輸,並使用PyQt5和pyqtgraph做視覺化介面(還用到了QThread和自定義的下拉複選框),開發過程中還涉及到了內網穿透和NATAPP。
# 理論基礎:運輸層
為使用UDP協議進行資料傳輸,我大致複習了一下計算機網路中的運輸層。
## 功能
運輸層實現兩臺主機中**程序**之間的通訊,一個主機中的多個程序可以和另一臺主機中的多個程序通訊。
運輸層實現上述功能的方案是埠(port)
## 兩個主要協議
運輸層有兩個主要協議:
- 傳輸控制協議TCP(Transmission Control Protocol)
- 使用者資料報協議UDP(User Datagram Protocol)
## TCP
- TCP是**面向連線**的
- 應用程序在傳輸資料前必須先建立連線,資料傳送結束後要釋放連線
- TCP連線是**點對點**的
- 每一條TCP連線只能有兩個端點
- TCP不提供廣播或多播服務
- TCP提供**可靠交付**的服務
- 通過TCP連線傳送的資料,無差錯、不丟失、不重複,並且按序到達
- TCP**面向位元組流**
- 雖然應用程式和TCP的互動是一次一個資料塊(大小不等),但TCP把應用程式交下來的資料僅僅看成一連串的無結構的位元組流。
- TCP不保證接收方應用程式所收到的資料塊和傳送方應用程式所發出的資料塊具有對應大小
- TCP保證接收方應用程式收到的位元組流必須和傳送方應用程式發出的位元組流完全一樣,同時接收方應用程式必須有能力識別收到的位元組流,把它還原成有意義的應用層資料
## UDP
- UDP是**無連線**的
- 在傳輸資料前不需要先建立連線,主機在收到UDP報文後不需要給出任何確認
- UDP是**面向報文**的
- 傳送方:UDP對應用層交下來的報文,不合並也不拆分,新增首部後就交付給IP層
- 接收方:UDP對IP層交上來的UDP使用者資料包,在去除首部後就直接交付給應用層的程序
- UDP**盡最大努力交付**
- 不保證可靠交付
- UDP**支援一對一、一對多、多對一和多對多**的互動通訊
# Python中的UDP程式設計
Python中的UDP程式設計可以通過`socket`來實現,下面是一個簡單樣例
## 伺服器端
```python
import socket
server_ip = '127.0.0.1'
server_port = 9999
# 建立套接字
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket.SOCK_DGRAM代表是UDP通訊
# 繫結IP和埠
s.bind((server_ip, server_port))
print('Bind UDP Server on %s:%s' % (server_ip, server_port))
while True:
# 接收資料
data, addr = s.recvfrom(1024)
print(addr, "\t", data)
# 傳送資料
s.sendto(b'Received:%s'%data, addr)
```
## 客戶端
```python
import socket
server_ip = '127.0.0.1'
server_port = 59955
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # socket.SOCK_DGRAM代表是UDP通訊
for data in [b'Michael', b'Tracy', b'Sarah']:
# 傳送資料
s.sendto(data, (server_ip, server_port))
# 接收資料
# print(s.recv(1024).decode('utf-8'))
s.close()
```
## 值得注意的問題:緩衝區機制
UDP通訊時,兩個主機都要建立一個socket。
我這裡的情況是客戶端會一直給伺服器端發資料。
在伺服器端我發現socket一旦建立(準確來講是建立socket物件並繫結至本地埠),就會一直接收資料,而不是呼叫recvfrom等函式(這類函式用來接收資料)時才會接收。
估計這是緩衝區機制,UDP應該就是這麼設計的。大概就是socket物件建立後,收到的內容就會放入緩衝區,如果呼叫了recvfrom等資料接收函式就從緩衝區中取出資料。
# 內網穿透
## 為什麼要用內網穿透
先不講內網穿透是什麼,有興趣的可以自己去查查,下面我大概講講我淺顯的理解。
在開發伺服器端程式的過程中,我用的是自己的電腦,連線的網路是手機熱點(因為在宿舍),因此我的電腦是沒有公網IP的。
客戶端程式用的是SIM卡,用的是公網(外網)IP,我開發的伺服器端程式用的是私網(內網)IP。
公網IP是無法訪問私網IP的(因為NAT),所以**我需要讓我的伺服器端程式能夠被外網訪問**。
問了一下[@roadwide](https://www.cnblogs.com/roadwide/),他說要用內網穿透,並推薦了**NATAPP**等軟體。
## NATAPP的使用
怎麼用呢?看看官方教程就知道了,連結放在文章末尾了。
講一個比較關鍵的點,以理解下NATAPP是幹嘛的
![NATAPP截圖](https://cdn.natapp.cn/uploads/ueditor/php/upload/image/20170118/1484723077222282.png)
NATAPP執行起來後,就會將上圖紅框裡的URL對映到本機(127.0.0.1)的80埠。
NATAPP會給我一個URL(作為我的外網IP),這樣客戶端程式通過訪問NATAPP給我的URL就可以間接訪問我在本機執行的伺服器端程式。
# PyQt5
## QThread
伺服器端程式的介面上有兩個作用分別是開始接收資料和停止接收資料的按鈕。
接收資料是通過一個while迴圈(迴圈體中接收一個數據)實現的,如果點選開始接收資料的按鈕,那就執行while迴圈直到停止接收資料的按鈕被點選。
剛開始實現資料接收功能時發現程式介面會崩潰、點選不動,因為直接把while迴圈寫在**軟體主介面**的程式碼中。
後來使用了PyQt5中的QThread(也有人說QThread並不是一個執行緒),在一個執行緒中實現while迴圈,然後就成功了。
在實現時我參考了其他網友的程式碼,參考連結放在文章末尾,注意一點是實現方式不止一種,比如說有些網友說用threading也可以,而且我也發現我的思路和參考的那份程式碼稍有不一樣(我們實現的功能是相似的,但我只用了一個pyqtSignal,而那位網友用了兩個)。
## 下拉複選框
這個軟體需要有一個下拉複選框,而PyQt5中並沒有這個東西,因此需要手動實現,這裡我參考了其他網友的實現方式,參考連結見文章末尾。
# 參考連結
## Python中的UDP程式設計
https://blog.csdn.net/vict_wang/article/details/81587093
https://www.jb51.net/article/165933.htm
## 理解NAT和內網穿透
https://baike.baidu.com/item/nat/320024
https://baike.baidu.com/item/%E5%86%85%E7%BD%91%E7%A9%BF%E9%80%8F
## NATAPP
https://natapp.cn/#
https://natapp.cn/article/natapp_newbie
## PyQt5
- PyQt5下拉式複選框QComboCheckBox
https://blog.csdn.net/LJX4ever/article/details/78039318
- QThread實現迴圈
https://segmentfault.com/a/1190000020746912?utm_source=tag-newest
- AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute 'connect'
## pyqtgraph
- pyqtgraph中繪製多個線條(我實現這個功能時也看了pyqtgraph的example)
https://zmister.com/archives/219.html#plot-2
- pyqtgraph中新增圖例(legend)
https://zmister.com/archives/220.html?replytocom=558
---
作者:[@臭鹹魚](https://github.com/chouxianyu)
轉載請註明出處:
歡迎討論和交