使用Python語言通過PyQt5和socket實現UDP伺服器

前言

最近做了一個小軟體,記錄一下相關內容。

已有條件

現在已有一個硬體設備作為客戶端(暫稱其為「電路」)。

基於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來實現,下面是一個簡單樣例

伺服器端

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)

客戶端

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,他說要用內網穿透,並推薦了NATAPP等軟體。

NATAPP的使用

怎麼用呢?看看官方教程就知道了,鏈接放在文章末尾了。

講一個比較關鍵的點,以理解下NATAPP是幹嘛的

NATAPP截圖

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編程

//blog.csdn.net/vict_wang/article/details/81587093

//www.jb51.net/article/165933.htm

理解NAT和內網穿透

//baike.baidu.com/item/nat/320024

//baike.baidu.com/item/內網穿透

NATAPP

//natapp.cn/#

//natapp.cn/article/natapp_newbie

PyQt5

pyqtgraph


作者:@臭鹹魚

轉載請註明出處://www.cnblogs.com/chouxianyu/

歡迎討論和交流!