本篇将按照套路, 先介绍传输层的另一个核心协议 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 服务器
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- import socket
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.bind(("127.0.0.1", 6000))
- print("UDP bound on port 6000...")
- while True:
- data, addr = s.recvfrom(1024)
- print("Receive from %s:%s" % addr)
- if data == b"exit":
- s.sendto(b"Good bye!\n", addr)
- continue
- 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 客户端
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- import socket
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- addr = ("127.0.0.1", 6000)
- while True:
- data = input("Please input your name:")
- if not data:
- continue
- s.sendto(data.encode(), addr)
- response, addr = s.recvfrom(1024)
- print(response.decode())
- if data == "exit":
- print("Session is over from the server %s:%s\n" % addr)
- break
- 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() 函数用于创建网络通信中的套接字对象. 函数原型如下:
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 地址与端口号. 注意, 套接字对象必须未被绑定, 并且端口号未被占用, 否则会报错. 函数原型如下:
socket.bind(address)
address 参数代表套接字要绑定的地址, 其格式取决于套接字的 family 参数. 若 family 参数为 AF_INET, 则 address 参数表示为二元组 (host, port), 其中 host 是用字符串表示的主机地址, port 是用整型表示的端口号.
sendto() 函数
sendto() 函数用于向远程套接字对象发送数据. 注意, 该函数用于 UDP 进程间的无连接通信, 远程套接字的地址在参数中指定, 因此使用前不需要先与远程套接字连接. 相对地, TCP 进程间面向连接的通信过程需要用 send() 函数. 函数原型如下:
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 进程间通信. 函数原型如下:
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() 函数用于关闭本地套接字对象, 释放与该套接字连接的所有资源.
socket.close()
0×07 总结
本文介绍了 UDP 协议的基础知识, 并与 TCP 协议进行对比, 再用 Python 3 实现并演示了 UDP 服务器与客户端的通信过程, 最后将脚本中涉及到的 Python API 做成了的参考索引, 有助于读者理解实现过程.
来源: http://server.51cto.com/News-579025.htm