使用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来实现,下面是一个简单样例

服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

客户端

1
2
3
4
5
6
7
8
9
10
11
12
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编程

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

pyqtgraph


作者:@臭咸鱼

转载请注明出处:https://www.cnblogs.com/chouxianyu/

欢迎讨论和交流!