基本UDP编程

UDP 用户数据报协议

TCP的通信流程好比“打电话”,拨通号码,需要等对方接起电话(建立了通话渠道),才能进行交流。UDP的通信流程就好比“写信件”,写好信,填好对方的地址和名字,就可以寄出去了。如果没有告知对方,对方并不会知道他将会收到你的一封信。可能因为某些原因(信在运输被弄掉了等等),对方收不到你的信,你也不知道对方收到了没有。更糟糕的是,信可能到对方家里了,但是他不在,或者没有从邮箱里拿出你的信。类似信件的通信行为,UDP把用户进程每次传过来的数据不加拆解、完整的打包成udp数据包交给IP协议层。

UDP协议面向非连接。“非连接”怎么理解?就是说,UDP不管通信对方是否准备好接收消息,甚至不管对方是不是存在。直接把数据打包交给IP层去发送就是了。不像TCP,需要建立起一个连接,才能发送数据。

UDP协议是不可靠的。适用于一次传输数据量较少的通信。

UDP头部

UDP基本通信模型

请求——应答模式

recvfrom和sendto函数

这两个函数是UDP下的读写函数,类似于标准的read和write函数。可以说,这两个函数就是为UDP而生的。为什么这样说呢?

因为UDP是不建立连接的数据传输,这里隐含:对于一个udp套接字,它可以一会儿发给a服务器,一会发给b服务器。同样,它可以接受来自不同IP主机的数据包。(只要知道其地址和端口号就行了)。也就是说,每次发送/接收的行为都是独立的,那么怎么区分数据包呢?解决方法就是在read/write函数加两个参数:地址结构体及其长度。这也就演变出两个函数:recvfrom和sendto.

TCP是面向连接的,单向的。recvfrom/sendto没有必要使用在TCP上。

1
2
3
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buff, size_t hbytes, int flags, struct sockaddr* from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void* buff, size_t nbytes, int flags, struct sockaddr* to, socklen_t addrlen);

针对这两个函数,注意几点:

  • recvfrom的地址结构及其长度参数都是指针类型,即“值——结果”参数。sendto的地址结构长度是“值”类型。
  • recvfrom返回值为0是允许的,且不像TCP,返回0就代表对端关闭连接。对于UDP来说,写一个长度为0的数据报是允许的。
  • recvfrom的地址结构指针是NULL,那么其长度也必须为NULL,表示我们不关心数据发送者的协议地址。

各种小问题

数据报丢了。。。

客户端sendto一个数据报之后,就兴高采烈转入recvfrom中阻塞等待接收。凡是都有万一,万一数据报在路上被路由器丢弃了,或者服务器根本不存在,又或者服务器返回应答了但是应答丢了。。。会出现什么情况:客户端永久阻塞在recvfrom调用。

一般的解决方法是:给recvfrom调用设置一个超时。

数据报不请自来——验证响应

客户端或服务器本来在等待A端的数据包,但是却接收到了非A的数据报。如果应用进程不想与之通信,就需要有机制来忽略或处理这样的情况。

解决方法:通过recvfrom传回来的地址结构来判断是否是我们想要的响应。

服务器进程未运行

客户端在未获知服务器是否启动的情况下,就像服务器发送数据包,会发生这样的情况,即服务器进程还没有启动时,客户端就sendto一个数据包,然后直接转入到recvfrom中,那么什么也不会发生。客户端将永远阻塞在recvfrom。为什么是这样的?

针对IP协议来说,如果目标进程不可达,服务器端将会返回一个“端口不可达”ICMP错误(异步错误)。但是在默认情况(未连接)下,这个错误是不会被发送进程感知到的(原因后解)。

sendto成功返回仅仅表示在接口输出队列中具有足够的空间存放本次发送形成的IP数据报。也就不能说明对方已经收到数据报。

一个基本规则就是:对于一个UDP套接字,由他引发的异步错误并不返回给套接字本身,除非该UDP套接字已经连接。为什么这样做呢?原因其实很简单,想象一个场景,一个未连接UDP套接字同时发给多个不同的主机,其中有个主机响应了ICMP错误,如果将此异步错误传递给UDP套接字,那么它也判断不了是哪个通信方出现问题。已连接的UDP套接字可以感知到ICMP错误,就是因为它仅仅只有一个对端。

一言:UDP套接字进程感知不了ICMP错误,除非它是已连接的。

UDP的connect行为

与TCP的connect操作有三次握手过程不同,UDP的connect仅仅是检查是否存在立即可知的错误(地址是不是错误的),把套接字的目标地址和目标端口提前填好而已。

设置好目的IP和目的端口的UDP套接字称为“已连接UDP套接字”。它与未连接UDP套接字有三处不同:

  • 设置好目的IP和目的端口号,也就不用每次调用sendto和recvfrom这样需要填地址结构的函数了,可以像TCP一样,调用write/send。
  • 同样的,recvfrom也不必使用,而改用read、recv或recvmsg。
  • 由已连接UDP套接字引发的异步错误将返回它们所在的进程,而未连接UDP套接字不接受任何异步错误。

已连接UDP套接字只是确定在当下要通信的对端,是可变的。改变目的IP和目的端口的方法就是再次调用connect。

已连接和未连接UDP套接字的性能

在一个未连接的UDP套接字上调用两次sendto函数,内核行为经历6个步骤:

  • 连接套接字;
  • 输出第一个数据报;
  • 断开套接字连接;
  • 连接套接字;
  • 输出第二个数据报;
  • 断开套接字连接;

在一个已连接的UDP套接字上调用两次sendto函数,内核行为经历6个步骤:

  • 连接套接字;
  • 输出第一个数据报;
  • 输出第二个数据报;
  • 断开套接字连接;