首页 > 代码库 > apue和unp的学习之旅10——基本udp套接字编程
apue和unp的学习之旅10——基本udp套接字编程
使用UDP编写的一些常见的应用程序有:DNS(域名系统),NFS(网络文件系统),SNMP(简单网络管理协议)。
//---------------------------------1.recvfrom函数和sendto函数----------------------------------
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void* buff, size_t nbytes, int flags, struct sockaddr* from,
socklen_t* addrlen);
// ret: 若成功则为读或写的字节数,若出错则返回-1
ssize_t
sendto(int sockfd, const void* buff, size_t nbytes, int flags, const struct sockaddr* to,
socklen_t addrlen);
// ret: 若成功则为读或写的字节数,若出错则返回-1
注意:
1.sendto的最后一个参数是也给整数值,而recvfrom的最后一个参数是一个指向整数值的指针(即值结果参数)。
写一个长度为0的数据报是可行的,在UDP情况下,这会形成一个只包含一个IP首部(对于IPV4通常为20个字节,对于IPv6通常为40个字节)和一个8字节的UDP首部而没有数据的IP数据报。这也意味着对于数据报协议,recvfrom返回值为0是可以接受的,它并不像TCP套接字上read 返回0值那样表示对端已经关闭连接,既然UDP是无连接的,因此也就没有诸如关闭一个UDP连接之类的说法。
2.如果recvfrom的from参数是个空指针,那么相应的长度参数addrlen也必须是个空指针,表示我们并不关心数据发送者的协议地址。
3.每个UDP套接字都有一个接收缓冲区,到达该套接字的每个数据报都进入这个套接字接收缓冲区,当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO(先入先出)顺序返回给进程。这样,在进程能够读套接字中任何已排好队的数据报之前,如果有多个数据报到达该套接字,那么相继到达的数据报仅仅加到该套接字的接收缓冲区中。
4.如果客户尚未请求内核给它的套接字指派一个临时端口,对于TCP客户而言,我们说过connect调用正是这种指派发生之处,对于一个UDP套接字,如果其进程首次调用sendto时没有绑定一个本地端口,那么内核就在此时为它选择一个临时端口,跟TCP一样,客户也可以显示调用bind,不过很少这样做。客户的临时端口是在第一次调用sendto时一次性选定,不能改变,然而客户的IP地址却可以随客户发送的每个UDP数据报而变动()假设客户没有捆绑一个具体的IP地址到其套接字上),因为客户可能是多宿的,由内核基于外出数据链路选择的客户ip地址将随每个数据报而改变。
5.若调用recvfrom指定的第五个和第六个参数指针是空指针,这样是告知内核我们并不关心应答数据报由谁发送。这样存在一个风险:任何进程不论是在于本客户进程相同的主机上还是在不同的主机上,都可以向本客户的IP地址和端口发送数据报,这些数据报将被客户读入并被认为是服务器的应答。
6.对于UDP套接字,目的IP地址只能通过为IPv4设置IP_RECVDSTADDR套接字选项(或IPv6设置IPV6_PKTINFO套接字选项)然后调用recvmsg取得。由于UDP是无连接的,因此目的IP地址可随发送到服务器的每个数据报而改变。
//-------------------------------------2.数据报的丢失--------------------------------------
下面是简单的UDP回射客户程序:
#include "unp.h" int main(int argc, char **argv) { int sockfd; struct sockaddr_in servaddr; if (argc != 2) err_quit("usage: udpcli <IPaddress>"); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); sockfd = Socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr)); exit(0); }
#include "unp.h" void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; while (Fgets(sendline, MAXLINE, fp) != NULL) { Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); recvline[n] = 0; /* null terminate */ Fputs(recvline, stdout); } }
我们的UDP客户服务器例子是不可靠的,如果一个客户数据报丢失(譬如说,被客户主机与服务器之间的某个路由器丢弃),客户将永远阻塞于dg_cli函数recvfrom的调用,等待一个永远不会到大的服务器应答,类似地,如果客户数据报到达服务器,但是服务器的应答丢失了,客户也将永远阻塞于recvfrom调用,防止这样永久阻塞的一般方法是给客户的recvfrom调用设置一个超时。但是仅仅是给recvfrom调用设置超时并不是完整的解决办法,举例来说,如果确实超时了,我们将无从判定超时原因是我们的数据报没有到达服务器,还是服务器的应答没有回到客户。
//-------------------------------------3.服务器进程未运行--------------------------------------、
在不启动服务器的前提系启动客户,键入一行文本后,客户将永远阻塞与它的recvfrom调用。
在客户主机能够往服务器主机发送给那个UDP数据报之前,需要一次ARP请求和应答的交换,在IP数据报可发往本地网络上另一个主机或路由器之前,还是有可能出现ARP请求应答的。
遮掩会引起ICMP错误,一种异步错误、,由sendto 引起,但是sendto本身成功返回。我们知道从UDP输出操作成功返回只是表示在接口输出队列中具有存放所形成IP数据报的空间,该ICMP错误直到后来才返回。
一个基本规则是:对于一个UDP套接字,由它引发的异步错误却并不返回给它,除非它已连接。为什么会这样设计呢?原因如下:考虑在单个UDP套接字上接连发送3个数据报给3个不同的服务器(即3个不同的IP地址)的一个UDP客户,其中有2个数据报被正确递送(也就是说3个主机中有2个在运行服务器),但是第三个主机没有运行服务器,第三个主机于是以一个ICMP端口不可达错误响应,这个ICMP错误出错消息包含引起错误的数据报的IP首部和UDP首部(ICMPv4和OCMPv6出错消息总是包含IP首部和所有的UDP首部或部分TCP首部)以便其接收者确定是由哪个套接字引发该错误。于是发送这3个数据报的客户需要知道引发该错误的数据报的目的地址以区分究竟是哪一个数据报引发了该错误,但是内核如何把该信息返回给客户进程呢?recvfrom可以返回的信息仅仅是errno的值,它没有办法返回出错数据报的目的IP地址和目的UDP端口号。因此作出决定:仅在进程已经其UDP套接字连接到恰恰一个对端后,这些异步错误才返回给进程。
//---------------------------------------4.UDP的connect函数---------------------------------------
上面提到,除非套接字已连接,否则异步错误是不会返回到UDP套接字的。我们确实可以给UDP套接字调用connect,然而这样做的结果却与TCP连接大相径庭:没有三路握手的过程。内核只是检查是否存在立即可知的错误(例如一个显然不可达的目的地)记录对端的IP地址和端口号(取自传递给connect的套接字地址结构),然后立即返回到调用进程。
对于已经连接的UDP套接字区别于默认的未连接的套接字如下:
1).我们再也不能给输出操作指定目的IP地址和端口号。也就是说,我们不使用sendto,而改用write或send,写到已连接套接字上的任何内容都自动发送到由connect指定的协议地址(例如ip地址和端口号)。其实我们可以给已经连接UDP套接字调用sendto,但是不能指定目的地址,级第5个参数为空,第6个参数为0,当然POSIX规定指出当第5个参数为空时,第6个参数取值就不再考虑了。
2).我们不必使用recvfrom获悉数据报的发送者,而改用read,recv或recvmsg。目的地为这个已连接UDP套接字的本地协议地址(例如IP地址和端口号),发源地却不是该套接字早先connect到的协议地址的数据报,不会投递到该套接字,这样就限制一个已连接UDP套接字能且仅能与一个对端交换数据报。这些数据报可能投递给同一主机上的其他某个UDP套接字,如果没有相匹配的其他套接字,UDP将丢弃它们并生成相应的ICMP端口不可达错误。
3).由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接UDP套接字不接收任何异步错误。
//--------------------------------5.给一个UDP套接字多次调用connect---------------------------------
拥有一个已连接UDP套接字的进程可出于以下2个目的再次调用connect,
1).指定新的IP地址和端口号
2).断开套接字
第一个目的(即给一个已经连接的套接字指定新的对端)不同于TCP套接字中的connect的使用:对于TCP套接字,connect只能调用一次。
第二个目的,为了断开一个已连接UDP套接字,我们再次调用connect时把套接字地址结构的地址族成员(对于IPv4为sin_famliy,对于IPv6为sin6_family)设置为AF_UNSPEC.这么做可能会返回一个EAFNOSUPPRT,这么做可能会返回一个EAFNOSUPPORT错误。
//-----------------------------------6.容易忽略的性能问题的考虑----------------------------------------
当应用进程在一个未连接的UDP套接字上调用sendto时,源自Berkley的内核暂时连接该套接字,发送数据报,然后断开连接,也就有如下情况,在一个未连接的UDP套接字上给两个数据报调用sendto函数于是涉及内核执行下列6个步骤,
1)连接套接字
2)sendto输出第一个数据报
3)断开套接字连接
4)连接套接字
5)sendto输出第二个数据报
6)断开套接字连接
另外还要考虑的是搜索路由表的次数,第一次临时连接需要为目的IP地址搜索路由表并高速缓存这条信息,第二次临时连接注意到目的地址等于已高速缓存的路由表信息的目的地,于是就不必再次查找路由表。
所以啊,当应用进程知道自己要给同一目的地址发送多个数据报时,显式连接套接字效率更高,调用connect后调用两次write涉及内核执行如下步骤:
1)连接套接字
2)send输出第一个数据报、
3)send输出第二个数据报
在这种情况下,内核只复制一次含有目的IP地址和端口号的套接字地址结构,相反当调用两次sendto时,需要复制2次。临时连接未连接的UDP套接字大约会耗费每个UDP传输三分之一的开销。