当前位置: 首页 > 科技观察

Python绝技——UDP服务端和客户端

时间:2023-03-12 07:16:11 科技观察

0×00前言本文将按照套路,首先介绍传输层的另一个核心协议UDP,然后比较TCP和UDP的特点,最后演示UDP服务端和客户端借助Python脚本终端通信过程。0×01UDP协议UDP(UserDatagramProtocol,用户数据报协议)是一种无连接的、不可靠的、基于数据报的传输层通信协议。与TCP相比,UDP的通信过程相对简单,不需要复杂的三次握手和四次挥手,体现无连接;UDP传输速度比TCP快,但容易丢包,数据到达的顺序得不到保证,缺乏拥塞控制,尽力而为原则体现不可靠;UDP的无连接、不可靠的特性注定无法采用字节流通信方式,这可以通过协议名称中的“Datagram”和套接字类型Datagram-based通信方式中的“SOCK_DGRAM”来体现。为了更直观地比较TCP和UDP的异同,笔者整理成下表:0×02NetworkSocket网络套接字(networksocket)是计算机网络中进程间通信的数据流端点,而也代表广义上的操作系统。一种进程间通信机制。进程间通信(IPC)的基本前提是能够唯一标识每个进程。在本地主机的进程间通信中,可以使用PID(进程ID)来唯一标识每个进程,但PID只是本地唯一的,网络中不同主机的PID可能会发生冲突,所以“IP地址+传输层协议+端口号”来唯一标识网络中的一个进程。Tips:网络层的IP地址可以唯一标识主机,传输层的TCP/UDP协议和端口号可以唯一标识主机的一个进程。注意TCP协议和UDP协议在同一台主机上可以使用相同的端口号。所有支持网络通信的编程语言都提供了一套socketAPI。下面以Python3为例,说明一下服务器端和客户端建立UDP通信连接的交互过程:可以看出UDP的通信过程比TCP简单很多,服务器也少。监听和接受连接的过程,客户端也少了请求连接的过程。客户端只需要知道服务器的地址并直接向其发送数据,服务器也敞开大门接收任何发往自己地址的数据。Tips:由于UDP采用的是无连接模式,可见UDP服务器在收到客户端发送的数据之前并不知道客户端的地址,所以客户端必须先发送数据,服务器必须响应数据。但是,TCP不同。TCP服务器接受客户端的连接后,既可以先向客户端发送数据,也可以等待客户端发送数据后再响应。0×03UDP服务器#!/usr/bin/envpython3#-*-coding:utf-8-*-importsockets=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)s.bind(("127.0.0.1",6000))print("UDPboundonport6000...")whileTrue:data,addr=s.recvfrom(1024)print("Receivefrom%s:%s"%addr)ifdata==b"exit":s.sendto(b"Goodbye!\n",addr)continues.sendto(b"Hello%s!\n"%data,addr)第5行:创建socket对象,第一个参数为socket.AF_INET,代表使用IPv4协议对于网络通信,第二个参数是socket.SOCK_DGRAM,表示使用UDP协议进行无连接网络通信。第6行:将服务器主机地址(“127.0.0.1”,6000)绑定到socket对象上,即本地主机的UDP端口6000。第9行:进入与客户端数据交互的循环阶段。第10行:接收客户端发送的数据,包括bytes对象数据,以及客户端的IP地址和端口号addr,其中addr为二元组(host,port)。第11行:打印接收到的信息,表示从地址为addr的客户端接收到数据。第12行:如果bytes对象为b"exit",则向地址为addr的客户端发送结束响应消息b"Goodbye!\n"。发送后,继续等待其他UDP客户端发送数据。第15行:如果bytes对象不是b"exit",则向地址为addr的客户端发送问候响应消息b"Hello%s!\n",其中%s为客户端发送的bytes对象。发送后,继续等待任何一个UDP客户端发送数据。与TCP服务器相比,UDP服务器不需要使用多线程,因为它不需要为每个通信过程创建一个独立的连接,而是采用“接收和发送”的模式,再次体现了UDP的无连接特性.0×04UDP客户端#!/usr/bin/envpython3#-*-coding:utf-8-*-importsockets=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)addr=("127.0.0.1",6000)whileTrue:data=input("Pleaseinputyourname:")ifnotdata:continues.sendto(data.encode(),addr)response,addr=s.recvfrom(1024)print(response.decode())ifdata=="exit":print("Sessionisoverfromtheserver%s:%s\n"%addr)breaks.close()第5行:创建一个socket对象,第一个参数是socket.AF_INET,表示网络通信使用IPv4协议,第二个参数是socket.SOCK_DGRAM,代表使用UDP协议进行无连接网络通信。第6行:初始化UDP服务器的地址(“127.0.0.1”,6000),也就是本机的UDP端口6000。第8行:进入与服务器交换数据的循环阶段。第9行:要求用户输入名称。第10行:当用户输入为空时,循环重新开始,要求用户重新输入。第12行:当用户输入不为空时,将字符串转换为bytes对象,发送给地址为("127.0.0.1",6000)的UDP服务器。第13行:接收服务器的响应数据,包括bytes对象response,以及服务器的IP地址和端口号addr,其中addr为二元组(host,port)。第14行:将响应字节对象response转换为字符串并打印出来。第15行:当用户输入“exit”时,打印sessionend消息,结束与服务器的数据交换循环阶段,即将关闭socket。第19行:关闭套接字,不再向服务器发送数据。0×05UDP进程间通信将UDP服务器和客户端的脚本分别命名为udp_server.py和udp_client.py,然后保存到桌面。笔者将在Windows10系统下使用PowerShell进行演示。Tips:读者复现时,请确保机器上安装了Python3。注意,作者将默认的启动路径名python修改为python3。单服务器VS多客户端在其中一个PowerShell中运行命令python3./udp_server.py,服务器绑定本地主机的UDP端口6000,并打印UDP绑定在端口6000上的信息...,等待客户端发送数据;在另外两个PowerShell中运行命令python3./udp_client.py,将字符串Client1和Client2发送到服务器;服务器打印接收到的信息,表示已经从UDP端口63643和63644接收到数据,并分别向客户端发送问候响应信息;客户端Client1发送空字符串,需要重新输入;客户端Client2首先发送字符串Alice获取服务器的问候响应消息,然后发送字符串exit获取服务器的结束响应消息,最后打印会话结束消息,终止与服务器的数据交互;客户端Client1发送字符串exit,获取服务端的结束响应信息,并打印会话结束信息,终止与服务端的数据交互;服务器根据客户端的上述数据发送顺序打印接收到的信息,并继续等待任何一个UDP客户端发送数据。0×06PythonAPIReferencesocket模块本节介绍上述代码中使用的内置模块socket,它是Python网络编程的核心模块。socket()函数socket()函数用于在网络通信中创建套接字对象。函数原型如下:socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)family参数表示地址族(AddressFamily),默认值为AF_INET,用于IPv4网络通信,常用AF_INET6,用于IPv6网络通信。family参数的可选值取决于本机操作系统。type参数表示套接字的类型。默认值为SOCK_STREAM,用于TCP协议(面向连接)网络通信,SOCK_DGRAM常用于UDP协议(无连接)网络通信。proto参数代表socket的协议,默认值为0,一般忽略这个参数,除非family参数是AF_CAN,那么proto参数需要设置为CAN_RAW或CAN_BCM。fileno参数表示socket的文件描述符,默认值为None。如果设置了这个参数,其他三个参数将被忽略。socket对象创建后,需要使用对象的内置函数来完成网络通信过程。注意下面函数原型中的“socket”指的是socket对象,不是上面说的socket模块。bind()函数bind()函数用于将IP地址和端口号绑定到套接字对象。注意socket对象一定不能绑定,端口号也不能被占用,否则会报错。函数原型如下:socket.bind(address)address参数表示要绑定到socket的地址,其格式取决于socket的family参数。如果family参数是AF_INET,address参数表示为二元组(host,port),其中host是用字符串表示的主机地址,port是用整数表示的端口号。sendto()函数sendto()函数用于向远程套接字对象发送数据。注意该函数用于UDP进程间的无连接通信,参数中指定了远程套接字的地址,因此使用前无需连接远程套接字。相比之下,TCP进程之间面向连接的通信过程需要使用send()函数。函数原型如下:socket.sendto(bytes[,flags],address)bytes参数表示要发送的bytes对象数据。例如,对于字符串“helloworld!”,需要使用encode()函数将其转换为bytes对象b“helloworld!”。用于网络传输。flags可选参数用于设置sendto()函数的特殊功能,默认值为0,也可以由一个或多个预定义的值组成,以位或运算符|分隔。详见Unix函数手册中的sendto(2)。flags参数常见的取值有MSG_OOB、MSG_EOR、MSG_DONTROUTE等。address参数表示远程socket的地址,其格式取决于socket的family参数。如果family参数是AF_INET,address参数表示为二元组(host,port),其中host是用字符串表示的主机地址,port是用整数表示的端口号。sendto()函数的返回值是发送数据的字节数。recvfrom()函数recvfrom()函数用于从远程套接字对象接收数据。请注意,与sendto()函数不同,recvfrom()函数可用于UDP进程间通信和TCP进程间通信。函数原型如下:socket.recvfrom(bufsize[,flags])其中bufsize参数表示socket可以接收的最大数据字节数。注意,为了更好的匹配硬件设备和网络传输,bufsize参数的值最好设置为2的幂,比如4096。flags可选参数用于设置recv()的特殊函数函数,默认值为0,也可以由一个或多个预定义值组成,以位或运算符|分隔。具体可以参考Unix函数手册中的recvfrom(2)。flags参数常见的取值包括MSG_OOB、MSG_PEEK、MSG_WAITALL等。recvfrom()函数的返回值是一个二元组(bytes,address),其中bytes是接收到的bytes对象数据,address是发送方的IP地址和端口号,用二元组(host,港口)。请注意,recv()函数的返回值只有字节对象数据。close()函数close()函数用于关闭本地套接字对象并释放连接到该套接字的所有资源。socket.close()0×07总结本文介绍了UDP协议的基础知识,并与TCP协议进行了比较,然后使用Python3实现并演示了UDP服务端与客户端的通信过程。最后,脚本中涉及的PythonAPI完成参考索引,帮助读者理解实现过程。