网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】


文章目录

    • 1、概述
    • 2、recvfrom和sendto
    • 3、UDP回射服务器程序
    • 4、dg_echo函数
    • 4、UDP回射客户程序
    • 5、UDP客户程序:dg_cli函数
    • 6、数据报丢失
    • 7、验证接收到的响应
    • 8、服务器进程未运行
    • 9、UDP的connect
    • 10、 给一个UDP套接字多次调用connect
    • 11、性能
    • 12、dg_cli函数
    • 13、UDP缺乏流量控制
        • 13.1 UDP套接字接收缓冲区
    • 14、UDP中的外出接口的确定
    • 15、使用select函数的TCP和UDP回射服务器程序

1、概述
TCP和UDP两个传输层的差别: - UDP是无连接不可靠的数据报协议; - 非常不同于TCP提供的面向连接的可靠字节流; - 常见用于:DNS、NFS、SNMP;

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

上图中,客户不与服务器建立连接,只是使用sendto给服务器发送数据报(必须指定目的地址); 服务器只管调用recvfrom,不接受连接,等待来自某个客户的数据到达;

2、recvfrom和sendto
#include /** sockfd, buff, nbytes即描述符,缓冲区,字节数; */ ssize_t recvfrom(int sockfd, void *buff, size_t vbytes, int flags , struct sockaddr *from, socklen_t *addrlen); /** @param from: 指向一个由该函数在返回时填写数据报发送者的协议地址的套接字地址结构,长度由addrlen返回; 若该参数为空,则我们不关系发送者的协议地址; */ ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags , const struct socoaddr *to, socklen_t *addrlen); /** @param to: 指向一个含有数据报接收者的协议地址的套接字地址结构,大小由addrlen指定; */ 【注意】:UDP下,会形成一个只包含一个IP首部和一个8字节UDP首部而没有数据的IP数据报;故recvfrom可返回0;

3、UDP回射服务器程序 网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

int main() {int sockfd; struct sockadd_in servaddr, cliaddr; sockfd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); dg_echo(sockfd, (struct sockaddr*)&cliaddr, sizeof(cliaddr)); return 0; }

4、dg_echo函数
void dg_echo(int sockfd, struct sockaddr* pcliaddr, socklen_t clilen) { int n; socklen_t len; char mesg[MAXLINE]; while (1) { len = clilen; n = recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len); sendto(sockfd, mesg, n, 0, pcliaddr, len); } }

【读数据报并回射给发送者】: - 该函数不终止,由于UDP是一个无连接协议; - 该函数为一个迭代服务器,单个服务器进程处理所有客户; - UDP层中会有排队发生,每一个UDP套接字都有一个接收缓冲区;当进程调用recvfrom时,缓冲区中的下一个数据报以FIFO顺序返回给进程; 由于缓冲区的大小有限;

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

UDP只有一个服务器进程,单个套接字接收所有到达的数据报并发回所有的响应;

4、UDP回射客户程序
int main(int argc, char **argv) {int sockfd; struct sockaddr_in servaddr; if(argc != ) err_quit("usage: udpcli"); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &servaddr, sizeof(servaddr)); sockfd = socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); return 0; }

5、UDP客户程序:dg_cli函数
void dg_cli(FILE *fp, int sockfd, const struct sockaddr *pseraddr, socklen_t servlen){ int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; while (fgets(sendline, MAXLINE, fp) != NULL) { sendto(sockfd, sendline, strlen(sendline), 0, pseraddr, servlen); n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); recvline[n] = 0; fputs(recvline, stdout); } }

使用fgets读入,在用sendto将该额呢绒发生给服务器,使用recvfrom读会服务器的回射,在使用fputs输出; 对于UDP,若其进程首次调用sendto时它没有绑定本地端口,故内核为其选择一个临时端口;

6、数据报丢失
UDP是不可靠传输,若一个客户数据报丢失,客户将永远阻塞于dg_cli函数中的recvfrom调用; 如果客户数据报到达服务器,但服务器的应答丢失了,客户也将永远阻塞于recvfrom调用; 为了出现上述情况我们将增加其可靠性;

7、验证接收到的响应
通常我们通过recvfrom返回的发送者IP地址和端口号进行筛选;

void dg_cli02(FILE *fp, int sockfd, const struct sockaddr *pseraddr, socklen_t servlen){ int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; socklen_t len; struct sockaddr *preply_addr; preply_addr = malloc(servlen); while (fgets(sendline, MAXLINE, fp) != NULL) { sendto(sockfd, sendline, strlen(sendline), 0, pseraddr, servlen); n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len); /* 添加验证 */ if(len != servlen || memcmp(preply_addr, preply_addr, len) != 0) { cout << "reply from" << sock_ntop(preply_addr, len) <<" (ignored)" << endl; } recvline[n] = 0; fputs(recvline, stdout); } }

当服务器主机是多宿的,客户可能会运行失败,解决方法: - 得到由recvfrom返回的IP地址后,客户通过在DNS中查找服务器主机的名字来验证该主机的域名; - UDP服务器给服务器主机上配置的每个IP地址创建一个套接字,用bind捆绑每个IP地址到各自的套接字,然后在所有这些套接字上使 用select(等待其中任何一个变得可读),再从可读的套接字给出应答; 既然用于给出应答的套接字上绑定的IP地址就是客户请求的目的IP地址,这就保证应答的源地址与请求的目的地址相同;

8、服务器进程未运行
不启动服务器的前提下启动客户;如果这样在客户上键入一行文本,客户永远阻塞于它的recvfrom,等待一个永不出现的服务器应答; 会出现ICMP错误未异步错误,该错误由sendto引起,但sendto本身却返回成功; 由于从UDP输出操作成功返回仅仅标识在接口输出队列中具有存放形成IP数据报的空间,该ICMP错误直到后来才返回;【基本规则】:对于一个UDP套接字,由它引发的异步错误却并不返回给它,除非它已连接; 当单个UDP套接字接连发送3个数据报给3个不同 的服务器的一个UDP客户,第三个出现ICMP不可达错误时,如何将该错误返回给客户进程呢? - 仅在进程已将其UDP套接字连接到恰恰一个对端后,异步才返回给进程;

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

9、UDP的connect
由于错误返回需要连接,故UDP套接字调用connect,内核只坚持是否存在立即可知的错误,记录对端的IP地址和端口号 后立即返回调用进程;

区分连接与未连接
【未连接】:新创建UDP套接字默认; 【已连接】:对UDP套接字调用connect的结果; 对于已连接,相比有三个变化: - 不能给输出操作指定目的IP地址和端口号;即sendto改用write或send;写到已连接UDP套接字上的任何内容都自动发送到由connect指定的协议地址; - recvfrom改用read、recv或recvmsg;由内核为输入操作返回的数据报只有那些来自connect所指定协议地址的数据报; - 由已连接UDP套接字引发的异步错误会返回给它们所在的进程,而未连接的不会接收任何异步错误;

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

应用进程首先调用connect指定对端的IP地址和端口号,后使用read和write与对端进程交换数据; 【小结】: - UDP客户进程或服务器进程只在使用自己的UDP套接字与确定的唯一对端进行通信时,才可以调用connect; - 且一般为UDP客户调用connect; - 若UDP服务器会与单个客户长时间通信,客户和服务器都可能调用connect;

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

通常通过在/etc/resolv.conf文件中列出服务器主机的IP地址,一个DNS客户主机就能被配置成使用一个或多个DNS服务器; 如果列出的是单个服务器主机,客户进程就可以调用connect; 但是如果列出的是多个服务器主机客户进程就不能调用connect; DNS服务器进程通常是处理客户请求的,因此服务器进程不能调用connect;

10、 给一个UDP套接字多次调用connect
拥有一个已连接UDP套接字的进程可出于下列两个目的之一再次调用connect: - 指定新的IP地址和端口号; - 断开套接字;

11、性能
当应用进程在一个未连接的UDP套接字上调用sendto时,源自Berkeley的内核暂时连接该套接字,发送数据报,然后断开该连接; 在一个未连接的UDP套接字上给两个数据报调用sendto函数于是涉及内核执行下列6个步骤; - 连接套接字; - 输出第一个数据报; - 断开套接字连接; - 连接套接字; - 输出第二个数据报; - 断开套接字连接; 另一个考虑是搜索路由表的次数; 第一次临时连接需为目的IP地址搜索路由表并高速缓存这条信息; 第二次临时连接注意到目的地址等于已高速缓存的路由表信息的目的地,于是就不必再次查找路由表;当应用进程直到自己要给同一目的地址发送多个数据报时,显示连接套接字效率更高;调用connect后调用两次write设计内核: - 连接套接字; - 输出第一个数据报; - 输出第二个数据报; 在该情况下,内核只复制一次含有目的IP地址和端口号的套接字地址结构,相反当调用两次sendto时,需复制两次; 临时连接未连接的UDP套接字大约耗费每个UDP传输三分之一的开销;

12、dg_cli函数
void dg_cli03(FILE * fp, int sockfd, const struct sockaddr*pservaddr , socklen_t serlen) { int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; connect(sockfd, (struct sockaddr*)pservaddr, serlen); while (fgets(sendline, MAXLINE, fp) != NULL) { writen(sockfd, sendline, strlen(sendline)); n = read(sockfd, recvline, MAXLINE); recvline[n] = 0; fputs(recvline, stdout); } }

上述使用connect,并以read和write代替sendto和recvfrom调用;该函数不查看传递给connecy的套接字结构的内容;

13、UDP缺乏流量控制
查看无任何流量控制的UDP对数据报传输的影响; - 首先,我们把dg_cli函数修改为发送固定数据的数据报,并不从标准输入读;

/** * 测试流量控制:写固定数目的数据报道服务器 * */ #define NDG 2000 #define DGLEN 1400void dg_cli04(FILE *fp, int sockfd, const struct sockaddr *pseraddr, socklen_t servlen) { int i; char sendline[DGLEN]; for (i = 0; i < NDG; ++i) { sendto(sockfd, sendline, DGLEN, 0, pseraddr, servlen); } }

/** 将服务器程序修改为接收数据报并对其计数,并不再把数据报回射给客户; 可使用终端终端键终止服务器,显示所接收道数据报的数目并终止; */ static int count; static void recvfrom_int(int) { cout << endl << "received " << count << " datagrams" << endl; exit(0); }void dg_echo02(int sockfd, struct sockaddr *pcliaddr, socklen_t clilen) { socklen_t len; char mesg[MAXLINE]; signal(SIGINT, recvfrom_int); while (1) { len = clilen; recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len); count++; } }

【网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】】网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

网络编程|【UNIX网络编程】|【06】基本UDP套接字编程【数据报丢失、性能、流量控制....】
文章图片

13.1 UDP套接字接收缓冲区
由UDP给某个特定套接字排队的UDP数据报数目受限于该套接字接收缓冲区的大小; 可使用SO_RCVBUF套接字选项修改该值,在FreeBSD下UDP套接字接收缓冲区的默认为42080字节,即只有30个1400字节数据报的容纳空间;

14、UDP中的外出接口的确定
已连接UDP套接字还可用来确定用于某个特定目的地的外出接口; 这是由connect函数应用到UDP套接字时的一个副作用造成的:内核选择本地IP地址; 这个本地IP地址通过为目的IP地址搜索路由表得到外出接口,然后选用该接口的主IP地址而选定;

- 当使用遵循默认路径的IP地址,内核把本地P地址指派成默认路径所指接口的主IP地址; - 当使用连接到另一个以太网接口的一个系统的IP地址,内核把本地IP地址指派成该接口的主地址,在UDP套接字上调用connect并不给对端主机发送任何信息 是一个本地操作,只是保存对端的IP地址和端口号; - 当在一个为绑定端口号的UDP套接字上调用connect同时也给该套接字指派一个临时端口号;

int main(int argc, char **argv) {int sockfd; socklen_t len; struct sockaddr_in cliaddr, servaddr; if(argc != 2) err_quit("usage: udpcli"); sockfd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &servaddr, sizeof(servaddr)); len = sizeof(cliaddr); getsockname(sockfd, (struct sockaddr*)&cliaddr, &len); cout << "local address " << sock_ntop((struct sockaddr*)&cliaddr, len) << endl; exit(0); return 0; }

15、使用select函数的TCP和UDP回射服务器程序
int main(int argc, char **argv) { int lfd, cfd, ufd, nready, maxfdp1; char mesg[MAXLINE]; pid_t childpid; fd_set rset; ssize_t n; socklen_t len; const int on = 1; struct sockaddr_in cliaddr, servaddr; void sig_chld(int); /** * 创建监听TCP套接字并捆绑端口号,设置SO_REUSEADDR以防该端口上已有连接存在 * */ lfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); bind(lfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); listen(lfd, LISTENQ); /** * 创建UDP并捆绑与TCP相同的端口,由于TCO是独立于UDP,故无需在调用bind之前设置SO_REUSEADDR * */ ufd = socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(ufd, (struct sockaddr*)&servaddr, sizeof(servaddr)); /* 由于TCP连接将有子进程处理 */ signal(SIGCHLD, sig_chld); FD_ZERO(&rset); maxfdp1 = max(lfd, ufd) + 1; while (1) { FD_SET(lfd, &rset); FD_SET(ufd, &rset); /* * 等待监听TCP套接字的可读条件或UDP套接字的可读条件 * */ if((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) { /* 由于sig_chld可能中断对select调用,故需要处理该错误 */ if(errno == EINTR) continue; else err_sys("select error"); }/* 连接客户端 */ if(FD_ISSET(lfd, &rset)) { len = sizeof(cliaddr); cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len); if((childpid = fork()) == 0){ close(lfd); str_echo(cfd); exit(0); } close(cfd); }/* 接收并发送数据报 */ if(FD_ISSET(ufd, &rset)) { len = sizeof(cliaddr); n = recvfrom(ufd, mesg, MAXLINE, 0, (struct sockaddr*)&cliaddr, &len); sendto(ufd, mesg, n, 0, (struct sockaddr*)&cliaddr, len); } }return 0; }

    推荐阅读