Python 绝技 —— UDP 服务器与客户端

服务器
本篇将按照套路,先介绍传输层的另一个核心协议 UDP,再比较 TCP 与 UDP 的特点,最后借助 Python 脚本演示 UDP 服务器与客户端的通信过程。

0×00 前言

本篇将按照套路,先介绍传输层的另一个核心协议 UDP,再比较 TCP 与 UDP 的特点,最后借助 Python 脚本演示 UDP 服务器与客户端的通信过程。

0×01 UDP 协议

UDP(User Datagram Protocol,用户数据报协议)是一种无连接、不可靠、基于数据报的传输层通信协议。

UDP 的通信过程与 TCP 相比较为简单,不需要复杂的三次握手与四次挥手,体现了无连接;

UDP 传输速度比 TCP 快,但容易丢包、数据到达顺序无保证、缺乏拥塞控制、秉承尽最大努力交付的原则,体现了不可靠;

UDP 的无连接与不可靠特性注定无法采用字节流的通信模式,由协议名中的「Datagram」与 socket 类型中的「SOCK_DGRAM」即可体现它基于数据报的通信模式。

为了更直观地比较 TCP 与 UDP 的异同,笔者将其整理成以下表格:

0×02 Network Socket

Network Socket(网络套接字)是计算机网络中进程间通信的数据流端点,广义上也代表操作系统提供的一种进程间通信机制。

进程间通信(Inter-Process Communication,IPC)的根本前提是能够唯一标示每个进程。在本地主机的进程间通信中,可以用 PID(进程 ID)唯一标示每个进程,但 PID 只在本地唯一,在网络中不同主机的 PID 则可能发生冲突,因此采用「IP 地址 + 传输层协议 + 端口号」的方式唯一标示网络中的一个进程。

小贴士:网络层的 IP 地址可以唯一标示主机,传输层的 TCP/UDP 协议和端口号可以唯一标示该主机的一个进程。注意,同一主机中 TCP 协议与 UDP 协议的可以使用相同的端口号。

所有支持网络通信的编程语言都各自提供了一套 socket API,下面以 Python 3 为例,讲解服务器与客户端建立 UDP 通信连接的交互过程:

可见,UDP 的通信过程比 TCP 简单许多,服务器少了监听与接受连接的过程,而客户端也少了请求连接的过程。客户端只需要知道服务器的地址,直接向其发送数据即可,而服务器也敞开大门,接收任何发往自家地址的数据。

小贴士:由于 UDP 采用无连接模式,可知 UDP 服务器在接收到客户端发来的数据之前,是不知道客户端的地址的,因此必须是客户端先发送数据,服务器后响应数据。而 TCP 则不同,TCP 服务器接受了客户端的连接后,既可以先向客户端发送数据,也可以等待客户端发送数据后再响应。

0×03 UDP 服务器

  1. #!/usr/bin/env python3 
  2. # -*- coding: utf-8 -*- 
  3. import socket 
  4.  
  5. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
  6. s.bind(("127.0.0.1", 6000)) 
  7. print("UDP bound on port 6000..."
  8.  
  9. while True
  10.     data, addr = s.recvfrom(1024) 
  11.     print("Receive from %s:%s" % addr) 
  12.     if data == b"exit"
  13.         s.sendto(b"Good bye!\n", addr) 
  14.         continue 
  15.     s.sendto(b"Hello %s!\n" % data, addr) 

Line 5:创建 socket 对象,第一个参数为 socket.AF_INET,代表采用 IPv4 协议用于网络通信,第二个参数为 socket.SOCK_DGRAM,代表采用 UDP 协议用于无连接的网络通信。

Line 6:向 socket 对象绑定服务器主机地址 (“127.0.0.1″, 6000),即本地主机的 UDP 6000 端口。

Line 9:进入与客户端交互数据的循环阶段。

Line 10:接收客户端发来的数据,包括 bytes 对象 data,以及客户端的 IP 地址和端口号 addr,其中 addr 为二元组 (host, port)。

Line 11:打印接收信息,表示从地址为 addr 的客户端接收到数据。

Line 12:若 bytes 对象为 b"exit",则向地址为 addr 的客户端发送结束响应信息 b"Good bye!\n"。发送完毕后,继续等待其他 UDP 客户端发来数据。

Line 15:若 bytes 对象不为 b"exit",则向地址为 addr 的客户端发送问候响应信息 b"Hello %s!\n",其中 %s是客户端发来的 bytes 对象。发送完毕后,继续等待任意 UDP 客户端发来数据。

与 TCP 服务器相比,UDP 服务器不必使用多线程,因为它无需为每个通信过程创建独立连接,而是采用「即收即发」的模式,又一次体现了 UDP 的无连接特性。

0×04 UDP 客户端

  1. #!/usr/bin/env python3 
  2. # -*- coding: utf-8 -*- 
  3. import socket 
  4.  
  5. s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
  6. addr = ("127.0.0.1", 6000) 
  7.  
  8. while True
  9.     data = input("Please input your name: "
  10.     if not data: 
  11.         continue 
  12.     s.sendto(data.encode(), addr) 
  13.     response, addr = s.recvfrom(1024) 
  14.     print(response.decode()) 
  15.     if data == "exit"
  16.         print("Session is over from the server %s:%s\n" % addr) 
  17.         break 
  18.  
  19. s.close() 

Line 5:创建 socket 对象,第一个参数为 socket.AF_INET,代表采用 IPv4 协议用于网络通信,第二个参数为 socket.SOCK_DGRAM,代表采用 UDP 协议用于无连接的网络通信。

Line 6:初始化 UDP 服务器的地址 (“127.0.0.1″, 6000),即本地主机的 UDP 6000 端口。

Line 8:进入与服务器交互数据的循环阶段。

Line 9:要求用户输入名字。

Line 10:当用户的输入为空时,则重新开始循环,要求用户重新输入。

Line 12:当用户的输入非空时,则将字符串转换为 bytes 对象后,发送至地址为 (“127.0.0.1″, 6000) 的 UDP 服务器。

Line 13:接收服务器的响应数据,包括 bytes 对象 response,以及服务器的 IP 地址和端口号 addr,其中 addr 为二元组 (host, port)。

Line 14:将响应的 bytes 对象 response 转换为字符串后打印输出。

Line 15:当用户的输入为 "exit" 时,则打印会话结束信息,终止与服务器交互数据的循环阶段,即将关闭套接字。

Line 19:关闭套接字,不再向服务器发送数据。

0×05 UDP 进程间通信

将 UDP 服务器与客户端的脚本分别命名为 udp_server.py 与 udp_client.py,然后存至桌面,笔者将在 Windows 10 系统下用 PowerShell 进行演示。

小贴士:读者进行复现时,要确保本机已安装 Python 3,注意笔者已将默认的启动路径名 python 改为了 python3。

单服务器 VS 多客户端

在其中一个 PowerShell 中运行命令 python3 ./udp_server.py,服务器绑定本地主机的 UDP 6000 端口,并打印信息 UDP bound on port 6000...,等待客户端发来数据;

在另两个 PowerShell 中分别运行命令 python3 ./udp_client.py,并向服务器发送字符串 Client1、Client2;

服务器打印接收信息,表示分别从 UDP 63643、63644端口接收到数据,并分别向客户端发送问候响应信息;

客户端 Client1 发送空字符串,则被要求重新输入;

客户端 Client2 先发送字符串 Alice,得到服务器的问候响应信息,再发送字符串 exit,得到服务器的结束响应信息,最后打印会话结束信息,终止与服务器的数据交互;

客户端 Client1 发送字符串 exit,得到服务器的结束响应信息,并打印会话结束信息,终止与服务器的数据交互;

服务器按照以上客户端的数据发送顺序打印接收信息,并继续等待任意 UDP 客户端发来数据。

0×06 Python API Reference

socket 模块

本节介绍上述代码中用到的内建模块 socket,是 Python 网络编程的核心模块。

socket() 函数

socket() 函数用于创建网络通信中的套接字对象。函数原型如下:

  1. socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None) 

family 参数代表地址族(Address Family),默认值为 AF_INET,用于 IPv4 网络通信,常用的还有 AF_INET6,用于 IPv6 网络通信。family 参数的可选值取决于本机操作系统。

type 参数代表套接字的类型,默认值为 SOCK_STREAM,用于 TCP 协议(面向连接)的网络通信,常用的还有 SOCK_DGRAM,用于 UDP 协议(无连接)的网络通信。

proto 参数代表套接字的协议,默认值为 0,一般忽略该参数,除非 family 参数为 AF_CAN,则 proto 参数需设置为 CAN_RAW 或 CAN_BCM。

fileno 参数代表套接字的文件描述符,默认值为 None,若设置了该参数,则其他三个参数将会被忽略。

创建完套接字对象后,需使用对象的内置函数完成网络通信过程。注意,以下函数原型中的「socket」是指 socket 对象,而不是上述的 socket 模块。

bind() 函数

bind() 函数用于向套接字对象绑定 IP 地址与端口号。注意,套接字对象必须未被绑定,并且端口号未被占用,否则会报错。函数原型如下:

  1. socket.bind(address) 

address 参数代表套接字要绑定的地址,其格式取决于套接字的 family 参数。若 family 参数为 AF_INET,则 address 参数表示为二元组 (host, port),其中 host 是用字符串表示的主机地址,port 是用整型表示的端口号。

sendto() 函数

sendto() 函数用于向远程套接字对象发送数据。注意,该函数用于 UDP 进程间的无连接通信,远程套接字的地址在参数中指定,因此使用前不需要先与远程套接字连接。相对地,TCP 进程间面向连接的通信过程需要用 send() 函数。函数原型如下:

  1. socket.sendto(bytes[, flags], address) 

bytes 参数代表即将发送的 bytes 对象数据。例如,对于字符串 "hello world!" 而言,需要用 encode() 函数转换为 bytes 对象 b"hello world!" 才能进行网络传输。

flags 可选参数用于设置 sendto() 函数的特殊功能,默认值为 0,也可由一个或多个预定义值组成,用位或操作符 | 隔开。详情可参考 Unix 函数手册中的 sendto(2),flags 参数的常见取值有 MSG_OOB、MSG_EOR、MSG_DONTROUTE 等。

address 参数代表远程套接字的地址,其格式取决于套接字的 family 参数。若 family 参数为 AF_INET,则 address 参数表示为二元组 (host, port),其中 host 是用字符串表示的主机地址,port 是用整型表示的端口号。

sendto() 函数的返回值是发送数据的字节数。

recvfrom() 函数

recvfrom() 函数用于从远程套接字对象接收数据。注意,与 sendto() 函数不同,recvfrom() 函数既可用于 UDP 进程间通信,也能用于 TCP 进程间通信。函数原型如下:

  1. socket.recvfrom(bufsize[, flags]) 

bufsize 参数代表套接字可接收数据的最大字节数。注意,为了使硬件设备与网络传输更好地匹配,bufsize 参数的值最好设置为 2 的幂次方,例如 4096。

flags 可选参数用于设置 recv() 函数的特殊功能,默认值为 0,也可由一个或多个预定义值组成,用位或操作符 |隔开。详情可参考 Unix 函数手册中的 recvfrom(2),flags 参数的常见取值有 MSG_OOB、MSG_PEEK、MSG_WAITALL 等。

recvfrom() 函数的返回值是二元组 (bytes, address),其中 bytes 是接收到的 bytes 对象数据,address 是发送方的 IP 地址与端口号,用二元组 (host, port) 表示。注意,recv() 函数的返回值只有 bytes 对象数据。

close() 函数

close() 函数用于关闭本地套接字对象,释放与该套接字连接的所有资源。

  1. socket.close() 

0×07 总结

本文介绍了 UDP 协议的基础知识,并与 TCP 协议进行对比,再用 Python 3 实现并演示了 UDP 服务器与客户端的通信过程,最后将脚本中涉及到的 Python API 做成了的参考索引,有助于读者理解实现过程。

责任编辑:武晓燕 来源: Freebuf
相关推荐

2018-12-18 10:47:37

2019-08-28 15:19:15

PythonTCP服务器

2014-01-17 15:23:55

Nagios

2024-02-22 13:47:40

2009-08-18 12:51:19

服务器+客户端

2011-06-09 10:51:26

Qt 服务器 客户端

2010-07-06 15:21:25

UDP客户端

2009-09-16 16:09:41

NIS服务器客户端NIS

2009-12-25 10:47:17

DNS服务器

2018-12-19 10:31:32

客户端IP服务器

2009-06-10 16:25:02

2014-06-01 11:03:13

VDI零客户端

2010-08-27 10:18:24

DHCP服务

2010-06-09 14:39:58

2010-10-26 13:54:45

连接Oracle服务器

2009-06-27 20:32:00

LinuxNFS客户端

2018-01-12 09:20:55

2012-05-29 09:38:04

Linux客户端服务器

2009-09-16 15:44:25

2009-09-17 18:06:44

Nis服务器
点赞
收藏

51CTO技术栈公众号