概述
使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。然而相比TCP,有些场合更适合UDP。使用UDP编写的一些常见应用程序有:DNS(域名系统)、NFS(网络文件系统)和SNMP(简单网络管理协议)。
下图给出了典型的UDP客户/服务器程序的函数调用。客户不必与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似的,服务器不接受来自客户的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将于所接受的数据报一道返回客户的协议地址,因此服务器可以把响应发送给正确的客户。
recvfrom和sendto函数
这个函数类似于标准的read和write函数,不过需要三个额外的参数:
#include<sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void* buf, size_t nbytes, int flags,
const struct sockaddr* to, sockelen_t addrlen);
前三个参数sockfd,buf和nbytes等同于read和write函数的三个参数:描述符、指向读入或写出缓冲区的指针和读写字节数。
sendto的to参数指向一个含有数据报接收者的协议地址(如IP地址和端口号)的套接字地址结构,其大小是由addrlen参数指定。recvfrom的from参数指向一个将由该函数在返回时填写的数据报发送者的协议地址的套接字地址结构,而在该套接字结构中填写的字节数则在addrlen参数所指的整数中返回给调用者。注意,sendto的最后一个参数是一个整数值,而recvfrom的最后一个参数是一个指向整数值的指针(即值-结果参数)
recvfrom的最后两个参数类似于accept的最后两个参数:返回时其中套接字的地址结构告诉我们是谁发送了数据报(UDP情况下)或是谁发起了连接(TCP情况下)。sendto的最后两个参数类似于connect的最后两个参数:调用时其中套接字的地址结构被我们填入数据报将发往(UDP情况下)或与之建立连接(TCP情况下)的协议地址。
这两个函数都把所读写的数据的长度作为函数返回值。在recvfrom使用数据报协议的典型用途中,返回值就是所接收数据报中的用户数据量。
写一个长度为0的数据报是可行的。在UDP情况下,这会形成一个只包含一个IP首部(对于IPV4通常为20字节,对于IPV6通常是40字节)和一个8字节的UDP首部而没有数据的IP数据报。这也意味着对于数据报协议,recvfrom返回0值是可以接受的:它不像TCP套接字上read返回0值那样表示对端已关闭连接。既然UDP是无连接的,因此也就没有诸如关闭一个UDP连接之类的事情。
如果recvfrom的from是一个空指针,那么相应的长度参数(addrlen)也必须是一个空指针,表示我们并不关心数据报发送者的协议地址。
UDP回射服务器
一般来说,大多数TCP服务器是并发的,而大多数UDP服务器是迭代的。
事实上每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字的接收缓冲区。这样,在进程能够读该套接字任何已排好队的数据报之前,如果有多个数据报到达该套接字,那么相继到达的数据报仅仅加到该套接字的接收缓冲区中。然而这个缓冲区的大小是由限制的
UDP的connect函数
可以给UDP套接字调用connect,然而这样做的结果与TCP连接大相径庭:没有三路握手过程。内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地),记录对端的IP地址和端口号(取自传递给connect的套接字地址结构),然后立即返回到调用进程。
对于已连接UDP套接字(调用connect后),与默认的未连接UDP套接字相比,发生了三个变化:
-
我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sento,而改用write或send。写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址(如IP地址和端口号)。
其实我们可以给已连接UDP套接字调用sendto,但是不能指定目的地址。sendto的第五个参数(指向指明目的地址的套接字地址结构的指针)必须为空指针,第六个参数(该套接字地址结构的大小)应该为0。POSIX规范指出当第五个参数为空指针时,第六个参数的取值就不再考虑。
-
我们不必使用recvfrom以获悉数据报的发送者,而改用read、recv或recvmsg。在一个已连接UDP套接字上,由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报。目的地为这个已连接UDP套接字的本地协议地址(如IP地址和端口号),发源地却不是该套接字早先connect到的协议地址的数据报,不会投递到该套接字。这样就限制一个已连接UDP套接字能且仅能与一个对端交换数据报。
确切地说,一个已连接UDP套接字仅仅与一个IP地址交换数据报,因为connect到多播或广播地址是可能的。
-
由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
应用进程首先调用connect指定对端的IP地址和端口号,然后使用read和write与对端进程交换数据。
来自任何其他IP地址或端口的数据报(上图中的"???"表示)不投递给这个已连接套接字,因为它们要么源IP地址要么源UDP端口不与该套接字connect到的协议地址相匹配。这些数据报可能投递给同一个主机上的其他某个UDP套接字。如果没有相匹配的其他套接字,UDP将丢弃它们并生成相应的ICMP端口不可达错误。
给一个UDP套接字多次调用connect
拥有一个已连接UDP套接字的进程可出于下列两个目的之一再次调用connect:
- 指定新的IP地址和端口号
- 断开套接字
第一个目的(即给一个已连接UDP套接字指定新的对端)不同于TCP套接字中connect的使用:对于TCP套接字,connect只能调用一次。
为了断开一个已连接UDP套接字,我们再次调用connect时把套接字地址结构的地址族成员(对于IPv4为sin_family,对于IPv6为sin6_family)设置为AF_UNSPEC。这么做可能会返回一个EAFNOSUPPORT错误,不过没有关系。使套接字断开连接的是在已连接UDP套接字上调用connect的进程。
性能
当应用进程在一个为连接的UDP套接字上调用sendto时,源自Berkeley的内核暂时连接该套接字,发送数据报,然后断开该连接。在一个未连接UDP套接字上给两个数据报调用sendto函数于是涉及内核执行下列6个步骤:
-
连接套接字
-
输出第一个数据报
-
断开套接字
-
连接套接字
-
输出第二个数据报
-
断开套接字连接
另一个考虑是搜索路由表的次数。第一次临时连接需为目的IP地址搜索路由表并高速缓存这条信息。第二次临时连接注意到目的地址等于已高速缓存的路由表信息的目的地(我们假设这两个sendto调用相同的目的地址),于是就不必再次查找路由表。
当应用进程知道自己要给同一目的地址发送多个数据报时,显示连接套接字的效率更高。调用connect后调用两次write涉及内核执行以下步骤:
- 连接套接字
- 输出第一个数据报
- 输出第二个数据报
在这种情况下,内核只复制一次含有目的IP地址和端口号的套接字地址结构,相反当调用sendto时,需要赋值两次。