socket发送和接受数据(基于UDP协议)详解

< 上一页UDP协议是什么 UDP多点广播下一页 >

程序在创建 socket 时,可通过 type 参数指定该 socket 的类型,如果将该参数指定为 SOCK_DGRAM,则意味着创建基于 UDP 协议的 socket。

在创建了基于UDP 协议的 socket 之后,程序可以通过如下两个方法来发送和接收数据:
  • socket.sendto(bytes, address):将 bytes 数据发送到 address 地址。
  • socket.recvfrom(bufsize[, flags]):接收数据。该方法可以同时返回 socket 中的数据和数据来源地址。

从这两个方法的介绍可以看出,使用 UDP 协议的 socket 在发送数据时必须使用 sendto() 方法,这是因为程序必须指定发送数据的目标地址(通过 address 参数指定);使用 UDP 协议的 socket 在接收数据时,既可使用普通的 recv() 方法,也可使用 recvfrom() 方法。如果程序需要得到数据报的来源,则应该使用 recvfrom() 方法。

从上面的介绍不难看出,由于 UDP 协议没有建立虚拟链路,因此程序使用 socket 发送数据报时,scoket 并不知道将该数据报发送到哪里,必须通过 sendto() 方法的 address 参数来指定数据报的目的地,这个目的地的地址会被附加在所发送的数据报上。

就像码头并不知道每个集装箱的目的地一样,码头只是将这些集装箱发送出去,而集装箱本身包含了该集装箱的目的地。

程序在使用 UDP 协议进行网络通信时,实际上并没有明显的服务器端和客户端,因为双方都需要先建立一个 socket 对象,用来接收或发送数据报。但在实际编程中,通常具有固定 IP 地址和端口的 socket 对象所在的程序被称为服务器,因此该 socket 应该调用 bind() 方法被绑定到指定 IP 地址和端口,这样其他 socket(客户端 socket)才可向服务器端 socket(绑定了固定 IP 地址和端口的 socket)发送数据报,而服务器端 socket 就可以接收这些客户端数据报。

当服务器端(也可以是客户端)接收到一个数据报后,如果想向该数据报的发送者“反馈”一些信息,此时就必须获取数据报的“来源信息”,这就到了 recvfrom() 方法“闪亮登场”的时候,该方法不仅可以获取 socket 中的数据,也可以获取数据的来源地址,程序就可以通过该来源地址来“反馈”信息。

一般来说,服务器端 socket 的 IP 地址和端口应该是固定的,因此客户端程序可以直接向服务器端 socket 发送数据,但服务器端无法预先知道各客户端 socket 的 IP 地址和端口,因此必须调用 recvfrom() 方法来获取客户端 socket 的 IP 地址和端口。

下面程序使用 UDP 协议的 socket 实现了 C/S 结构的网络通信。本程序的服务器端通过循环 1000 次来读取 socket 中的数据报,每当读取到内容之后,便向该数据报的发送者发送一条信息。服务器端程序的代码如下:
import socket

PORT = 30000;
# 定义每个数据报的大小最大为4KB
DATA_LEN = 4096;
# 定义一个字符串数组,服务器端发送该数组的元素
books = ("疯狂Python讲义",
    "疯狂Kotlin讲义",
    "疯狂Android讲义",
    "疯狂Swift讲义")
# 通过type属性指定创建基于UDP协议的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 将该socket绑定到本机的指定IP和端口
s.bind(('192.168.1.88', PORT))
# 采用循环接收数据
for i in range(1000):
    # 读取s中的数据的数据的发送地址
    data, addr = s.recvfrom(DATA_LEN)
    # 将接收到的内容转换成字符串后输出
    print(data.decode('utf-8'))
    # 从字符串数组中取出一个元素作为发送数据
    send_data = books[i % 4].encode('utf-8')
    # 将数据报发送给addr地址
    s.sendto(send_data, addr)
s.close()
上面程序中的代码就是使用 UDP 协议的 socket 发送和接收数据报的关键代码,该程序可以接收 1000 个客户端发送过来的数据。

客户端程序的代码与此类似,客户端采用循环不断地读取用户的键盘输入内容,每当读取到用户输入的内容后,就将该内容通过数据报发送出去;接下来再读取来自 socket 中的信息(也就是来自服务器端的数据)。客户端程序的代码如下:
import socket

PORT = 30000;
# 定义每个数据报的大小最大为4KB
DATA_LEN = 4096;
DEST_IP = "192.168.1.88";
# 通过type属性指定创建基于UDP协议的socket
s = socket.socket(type=socket.SOCK_DGRAM)
# 不断地读取键盘输入
while True:
    line = input('')
    if line is None or line == 'exit':
        break
    data = line.encode('utf-8')
    # 发送数据报
    s.sendto(data, (DEST_IP, PORT))
    # 读取socket中的数据
    data = s.recv(DATA_LEN)
    print(data.decode('utf-8'))
s.close()
上面程序中的代码就是使用 UDP 协议的 socket 发送和接收数据报的关键代码,这些代码与服务器端程序的代码基本相似。而客户端与服务器端的唯一区别在于,服务器端的 IP 地址和端口是固定的,所以客户端可以直接将数据报发送给服务器端;而服务器端则需要根据所接收到的数据报来决定“反馈”数据报的目的地。

读者可能会发现,在使用 UDP 协议进行网络通信时,服务器端无须也无法保存每个客户端的状态,客户端把数据报发送到服务器端后,完全有可能立即退出。但不管客户端是否退出,服务器端都无法知道客户端的状态。

当使用 UDP 协议时,如果想让一个客户端发送的聊天信息被转发到其他所有的客户端也是可以的,程序可以考虑在服务器端使用 list 列表来保存所有的客户端信息,每当接收到一个客户端的数据报之后,程序检查该数据报的来源地址是否在 list 列表中,如果不在就将该来源地址添加到 list 列表中。这样又涉及一个问题,可能有些客户端发送一个数据报之后,永久地退出了程序,但服务器端依然会将该客户端的 IP 地址和端口保存在 list 列表中。

因此,程序还需要定义一个定时器,定期检查每个客户端有多长时间没向服务器端发送数据了,如果超过一定的时间(比如 10 分钟)该客户端还没有发送数据,则服务器端就将该客户端的 IP 地址和端口从 list 列表中删除……总之,这种方式需要处理的问题比较多,编程比较烦琐。幸好 UDP 协议还支持多点广播,Python 也为 UDP 协议的多点广播提供了支持。
< 上一页UDP协议是什么 UDP多点广播下一页 >