700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > 【Linux网络编程】UDP 套接字编程

【Linux网络编程】UDP 套接字编程

时间:2018-12-13 05:19:26

相关推荐

【Linux网络编程】UDP 套接字编程

【Linux网络编程】UDP 套接字编程

【1】用户数据报协议(UDP)

UDP是一个简单的传输层协议,不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数据报只到达一次,UDP提供无连接的服务即UDP客户与服务器之间不必存在任何长期的关系。

【2】基本UDP套接字编程

【2.1】UDP客户/服务器程序套接字执行流程

注:sendto函数必须指定目的地(即服务器)的地址作为参数;recvfrom函数将与所接收的数据一道返回客户的协议地址;

【2.2】基本UDP通信示例程序

服务器端示例代码

#include"unp.h"intmain(int argc, char **argv){/*** sockfd : socket 文件描述符;* servaddr : 服务器 socket 地址结构;* cliaddr : 客户端 socket 地址结构;*/intsockfd;struct sockaddr_inservaddr, cliaddr;/*** 新建套接字* AF_INET : IPv4协议;* SOCK_DGRAM : 数据包套接字;*/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, (SA *) &servaddr, sizeof(servaddr));/*** 调用服务器回射函数*/dg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr));}voiddg_echo(int sockfd, SA *pcliaddr, socklen_t clilen){/*** n : Recvfrom 函数调用的返回值,表示接收到的字节数;* len : 接收到的客户端地址信息的大小* mesg : 数据缓冲区*/intn;socklen_tlen;charmesg[MAXLINE];/*** 不断循环将接收到的数据回射给客户端*/for ( ; ; ) {len = clilen;/*** 从客户端接收数据到数据缓冲区* pcliaddr : 指向客户端地址结构的指针*/n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);/*** 将从客户端接收到的数据回射给客户端;*/Sendto(sockfd, mesg, n, 0, pcliaddr, len);}}

客户端示例代码

#include"unp.h"intmain(int argc, char **argv){/*** socket : 套接字文件描述符;* servaddr : 服务器 socket 地址结构;*/intsockfd;struct sockaddr_inservaddr;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);/*** 新建套接字* AF_INET : IPv4协议;* SOCK_DGRAM : 数据包套接字;*/sockfd = Socket(AF_INET, SOCK_DGRAM, 0);/*** 调用客户端回射函数*/dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));exit(0);}voiddg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){/*** n : Recvfrom 函数调用的返回值,表示接收到的字节数;* sendline : 数据发送缓冲区* recvline : 数据接收缓冲区* len : 接收到的服务器地址信息的大小* replyaddr : 发送数据的对端地址结构信息*/intn;charsendline[MAXLINE], recvline[MAXLINE + 1];socklen_tlen;struct sockaddr_in*replyaddr;// 针对服务器地址结构分配内存空间replyaddr = Malloc(servlen);// 从文件中获取数据到发送缓冲区while (Fgets(sendline, MAXLINE, fp) != NULL) {// 发送给服务器Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);len = servlen;// 从服务器接收回射数据n = Recvfrom(sockfd, recvline, MAXLINE, 0, (SA *) replyaddr, &len);// 打印数据发送端的地址与端口信息printf("received reply from %s, port %d\n",inet_ntoa(replyaddr->sin_addr), htons(replyaddr->sin_port));// 接收缓冲区的结束标志recvline[n] = 0;/* null terminate */// 将接收到的数据输出到标准输出Fputs(recvline, stdout);}}

示例程序说明:

数据报丢失 若一个客户端数据报丢失,客户将阻塞于dg_cli函数中的recvfrom调用一直等待一个永远不会到达的服务器应答;若客户端数据报到达服务器,但服务器的应答丢失,客户也将永远阻塞于recvfrom调用;解决方案:recvfrom调用设置超时;服务器进程未运行 客户将阻塞于recvfrom调用,等待一个永不会出现的服务器应答;客户主机能够往服务器发送UDP数据报之前,需要一次ARP请求和应答的交换;由于服务器进程关闭,服务器返回端口不可达ICMP消息,称为ICMP异步错误;基本规则:对于一个UDP套接字,由其引发的异步错误却并不返回给它,除非该UDP套接字已连接;解决方法:仅在进程已将其UDP套接字连接到恰恰一个对端后,才会返回异步错误;

【2.3】UDP的connect函数

UDP套接字调用connect,内核只是检查是否存在立即可知的错误,记录对端IP地址和端口号,然后立即返回到调用进程;

不用指定输出操作的IP地址和对端端口号,不能使用sendto,而改用write和send,写到已连接套接字对应的任何内容将会自动发送到connect的套接字上;不必使用recvfrom以获取数据报的发报者,而改用read,recv或recvmsg,在一个已连接的套接字上,内核的输入操作返回的数据报将会只来自connect指定的协议地址的数据报;已连接的UDP套接字中发生的ICMP错误将会返回给进程,而未连接的ICMP错误将不会返回的进程;

TCP,未连接UDP,已连接UDP套接字比较

使用场合:UDP客户进程或服务器进程在使用自己的UDP套接字与确定的唯一对端进行通信的场合;

一个UDP套接字多次调用connect:1. 指定新的IP地址和端口号;2. 断开套接字;

【2.4】UDP套接字编程注意事项

UDP缺乏流量控制,容易产生数据报的丢失 解决方案:使用SO_RCVBUF套接字选项修改套接字接收缓冲区大小,该方案只能缓解UDP缺乏流量控制而导致的问题;UDP中的外出接口的确定 对于已连接的UDP套接字可以用于确定某个特定目的地的外出接口;在UDP套接字上调用connect并不会给对端主机发送任何信息,只是保存了对端的IP地址和端口号,完全属于本地操作;

【2.5】使用select函数的TCP 和 UDP服务器程序示例

【2.5.1】模型示意图

【2.5.2】示例程序

#include"unp.h"intmain(int argc, char **argv){/*** listenfd : 监听套接字文件描述符* connfd : 连接套接字文件描述符* udpfd : UDP socket 文件描述符* nready : 就绪描述符数目,select 函数返回值* maxfdp1 : 指定待测试的描述符个数,它的值是待测试的最大描述符加1* mesg : 数据缓冲区* childpid : 子进程 ID* rset : 读描述符集合* n : 接收到的字节数* len : socket 地址结构大小* on : 开关变量* cliaddr : 客户端地址结构* servaddr : 服务器地址结构* sig_chld : SIGCHLD 信号处理函数,处理僵死进程*/int listenfd, connfd, udpfd, nready, maxfdp1;charmesg[MAXLINE];pid_tchildpid;fd_setrset;ssize_tn;socklen_tlen;const inton = 1;struct sockaddr_incliaddr, servaddr;voidsig_chld(int);/*** 创建监听套接字* AF_INET : IPv4协议* SOCK_STREAM : 字节流套接字*/listenfd = 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);// 设置 socket 套接字属性,此处允许重用本地地址Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));// 绑定 TCP 监听套接字到服务器地址Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));// 监听客户端连接Listen(listenfd, LISTENQ);/*** 创建监听套接字* AF_INET : IPv4协议* SOCK_DGRAM : 数据包套接字*/udpfd = 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);// 绑定 UDP 套接字到服务器地址Bind(udpfd, (SA *) &servaddr, sizeof(servaddr));// 指定 SIGCHLD 信号对应的处理函数Signal(SIGCHLD, sig_chld);/* must call waitpid() *//*** 初始化读描述符集合 rset* 初始化待测试的描述符个数 maxfdp1*/FD_ZERO(&rset);maxfdp1 = max(listenfd, udpfd) + 1;for ( ; ; ) {// 注册 listenfd udpfd 到描述符集合中FD_SET(listenfd, &rset);FD_SET(udpfd, &rset);// 监听发生的事件if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) {if (errno == EINTR)continue;/* back to for() */elseerr_sys("select error");}// 监听套接字激活if (FD_ISSET(listenfd, &rset)) {len = sizeof(cliaddr);// 接收连接并创建连接套接字connfd = Accept(listenfd, (SA *) &cliaddr, &len);/*** 创建子进程并处理业务逻辑* str_echo 函数 : 用于处理客户端的请求*/if ( (childpid = Fork()) == 0) {/* child process */Close(listenfd);/* close listening socket */str_echo(connfd);/* process the request */exit(0);}Close(connfd);/* parent closes connected socket */}// UDP 套接字激活if (FD_ISSET(udpfd, &rset)) {len = sizeof(cliaddr);// UDP 回射处理逻辑n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len);Sendto(udpfd, mesg, n, 0, (SA *) &cliaddr, len);}}}voidsig_chld(int signo){pid_tpid;intstat;/*** 原型 : pid_t waitpid(pid_t pid,int * status,int options)* 功能 : 如果在调用 waitpid() 时子进程已经结束,* 则 waitpid() 会立即返回子进程结束状态值;* 子进程的结束状态值会由参数 status 返回, * 而子进程的进程识别码也会一起返回;* 如果不在意结束状态值,则参数 status 可以设成 NULL;* 参数 :* 参数 pid 为欲等待的子进程识别码,* 其数值意义如下:* pid<-1 等待进程组识别码为 pid 绝对值的任何子进程;* pid=-1 等待任何子进程,相当于 wait();* pid=0 等待进程组识别码与目前进程相同的任何子进程;* pid>0 等待任何子进程识别码为 pid 的子进程;* * 参数 options 提供了一些额外的选项来控制 waitpid* WNOHANG 若 pid 指定的子进程没有结束,则 waitpid() 函数返回0,不予以等待; * 若结束,则返回该子进程的ID;* WUNTRACED 若子进程进入暂停状态,则马上返回,但子进程的结束状态不予以理会;* WIFSTOPPED(status) 宏确定返回值是否对应于一个暂停子进程;* * 子进程的结束状态返回后存于 status* * 相关宏介绍* WIFEXITED(status) 如果若为正常结束子进程返回的状态,则为真; * 对于这种情况可执行 WEXITSTATUS(status),取子进程传给 exit 或 _eixt 的低8位;* WEXITSTATUS(status) 取得子进程 exit()返回的结束代码, * 一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏;* WIFSIGNALED(status) 若为异常结束子进程返回的状态,则为真; * 对于这种情况可执行 WTERMSIG(status),取使子进程结束的信号编号;* WTERMSIG(status) 取得子进程因信号而中止的信号代码, * 一般会先用 WIFSIGNALED 来判断后才使用此宏;* WIFSTOPPED(status) 若为当前暂停子进程返回的状态,则为真; * 对于这种情况可执行 WSTOPSIG(status),取使子进程暂停的信号编号;* WSTOPSIG(status) 取得引发子进程暂停的信号代码, * 一般会先用 WIFSTOPPED 来判断后才使用此宏;* * 返回值* 如果执行成功则返回子进程识别码 (PID) , 如果有错误发生则返回返回值-1 * 且失败原因存于 errno 中;*/// 此处等待子进程结束,防止出现僵死进程while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)printf("child %d terminated\n", pid);return;}

【3】高级UDP套接字编程

【3.1】接收标志、目的IP地址和接口索引

示例程序分析

#include"unp.h"/*** 相关宏介绍 : * CMSG_FIRSTHDR() 宏* 该宏用于返回 msg 所指向的 msghdr 结构体中的第一个 struct cmsghdr 结构体指针* * CMSG_NXTHDR() 宏* 该宏用于返回下一个附属数据对象的struct cmsghdr 指针* * CMSG_SPACE()宏* 该宏用于计算一个附属数据对象的所必需的空白* * CMSG_DATA 宏* 该宏根据传入的 cmsghdr 指针参数, 返回其后面数据部分的指针* * CMSG_LEN 宏* 该宏的入参是一个控制信息中的数据部分的大小* 返回值是这个根据入参大小, 需要配置的 cmsghdr 结构体中 cmsg_len 成员的值*/#include<sys/param.h>/* ALIGN macro for CMSG_NXTHDR() macro *//*** 函数入参说明 :* fd : socket 文件描述符* ptr : 数据缓冲区指针* nbytes : 数据缓冲区大小* flagsp : 值传递方式的标志位,用于传入控制信息* sa : 地址结构指针* salenptr : 地址结构大小指针* pktp : 函数 recvfrom_flags 返回信息结构体指针*//*** struct unp_in_pktinfo {* struct in_addripi_addr;// 保存 IP 地址信息* intipi_ifindex;// 保存系统分配的索引* };*//*** 接收数据并返回标志信息*/ssize_trecvfrom_flags(int fd, void *ptr, size_t nbytes, int *flagsp,SA *sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp){/*** struct msghdr {* void* msg_name;//协议地址* socklen_tmsg_namelen;//协议地址长度* struct lovec * msg_lov;//输入输出缓冲区数组* ssize_tmsg_lovlen;//输入输出数组中缓冲区的个数* void* msg_control;//辅助数据的起始位置指针* socklen_tmsg_controllen;//辅助数据大小* intmsg_flags; //引用传递,用于返回之前flags的控制信息* }*/struct msghdrmsg;/*** struct iovec{* void *iov_base; // 起始地址指针* size_t iov_len; // 区间长度*};*/struct ioveciov[1];/*** n : 接收到的字节数*/ssize_tn;#ifdefHAVE_MSGHDR_MSG_CONTROLstruct cmsghdr*cmptr;union {struct cmsghdrcm;charcontrol[CMSG_SPACE(sizeof(struct in_addr)) +CMSG_SPACE(sizeof(struct unp_in_pktinfo))];} control_un;msg.msg_control = control_un.control;msg.msg_controllen = sizeof(control_un.control);msg.msg_flags = 0;#else// 初始化 msgbzero(&msg, sizeof(msg));/* make certain msg_accrightslen = 0 */#endif// 初始化 msgmsg.msg_name = sa;msg.msg_namelen = *salenptr;iov[0].iov_base = ptr;iov[0].iov_len = nbytes;msg.msg_iov = iov;msg.msg_iovlen = 1;// 接收数据,flagsp : 值传递方式的标志位,用于保存控制信息;if ( (n = recvmsg(fd, &msg, *flagsp)) < 0)return(n);// 地址结构大小,用于回传结果信息;*salenptr = msg.msg_namelen;/* pass back results */// 回传信息的结构体if (pktp)bzero(pktp, sizeof(struct unp_in_pktinfo));/* 0.0.0.0, i/f = 0 */// 若本实现不支持 msg_control 成员,将待返回标记置为 0,并返回#ifndefHAVE_MSGHDR_MSG_CONTROL*flagsp = 0;/* pass back results */return(n);#else*flagsp = msg.msg_flags;/* pass back results *//*** 情况如下 :* 1. 没有控制信息;* 2. 控制信息被截断;* 3. 调用者不想返回一个 unp_in_pktinfo 结构;*/if (msg.msg_controllen < sizeof(struct cmsghdr) ||(msg.msg_flags & MSG_CTRUNC) || pktp == NULL)// MSG_CTRUNC : 本数据报的辅助数据被截断,// 即内核预备返回的辅助数据超过进程事先分配的空间return(n);/*** 遍历 msghdr 结构体,处理任意数目的辅助数据对象;*/for (cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL;cmptr = CMSG_NXTHDR(&msg, cmptr)) {/*** IP_RECVDSTADDR : 将目的 IP 地址作为控制信息返回;* IPPROTO_IP | IP_RECVDSTADDR : 随 UDP 数据报接收目的地址;*/#ifdefIP_RECVDSTADDRif (cmptr->cmsg_level == IPPROTO_IP &&cmptr->cmsg_type == IP_RECVDSTADDR) {memcpy(&pktp->ipi_addr, CMSG_DATA(cmptr),sizeof(struct in_addr));continue;}#endif/*** IP_RECVIF : 将接收接口的索引作为控制信息返回;* IPPROTO_IP | IP_RECVIF : 随 UDP 数据报接收接口索引;*/#ifdefIP_RECVIFif (cmptr->cmsg_level == IPPROTO_IP &&cmptr->cmsg_type == IP_RECVIF) {struct sockaddr_dl*sdl;sdl = (struct sockaddr_dl *) CMSG_DATA(cmptr);/*** struct sockaddr_dl : 数据链路套接字地址结构* sdl->sdl_index : 系统分配的索引*/pktp->ipi_ifindex = sdl->sdl_index;continue;}#endiferr_quit("unknown ancillary data, len = %d, level = %d, type = %d",cmptr->cmsg_len, cmptr->cmsg_level, cmptr->cmsg_type);}return(n);#endif/* HAVE_MSGHDR_MSG_CONTROL */}/*** recvfrom_flags 函数的包装函数* 当 recvfrom_flags 函数发生错误时打印错误日志,当前进程退出*/ssize_tRecvfrom_flags(int fd, void *ptr, size_t nbytes, int *flagsp,SA *sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp){ssize_tn;n = recvfrom_flags(fd, ptr, nbytes, flagsp, sa, salenptr, pktp);if (n < 0)err_quit("recvfrom_flags error");return(n);}

【3.2】数据报截断

处理方式 丢弃超出部分的字节并向应用程序返回MSG_TRUNC标志;丢弃超出部分的字节但不告知应用进程;保留超出部分的字节并在同一套接字上后续的读操作中返回超出部分的字节;

【3.3】UDP代替TCP的时机

UDP的优势 UDP支持广播与多播;UDP没有连接建立和拆除;相关概念,最小事务处理时间 = RTT(客户与服务器之间往返时间) + SPT(客户请求的服务器处理时间);UDP不具备的TCP特性 正面确认,丢失分组重传,重复分组检测,给被网络打乱次序的分组排序;窗口式流量控制;慢启动和拥塞避免;相关建议 对于广播与多播应用程序必须使用UDP;对于简单的请求-应答应用程序可以使用UDP,但需要在应用程序中添加错误检测功能;对于海量数据传输不应该使用UDP;

【3.4】给UDP应用添加可靠性

请求-应答式应用程序中使用UDP添加的特性 序列号:提供客户验证一个应答是否匹配相应的请求; 序列号实现方式:客户为每个请求冠以一个序列号,服务器必须在返送给客户的应答中回射该序列号;超时和重传:用于处理丢失的数据报; 超时和重传实现方式影响往返时间(RTT)的因素:距离、网络速度、拥塞;方法1:计算RTO,当重传超时期满时,必须对下一个RTO应用某个指数回退,需要解决重传二义性问题;方法2:对于每个请求,服务器必须回复一个序列号以及时间戳,当客户端收到一个应答时,从当前时间减去服务器在应答中回复的时间戳从而得到RTT;

计算公式

示例代码分析

#include"unp.h"ssize_tDg_send_recv(int, const void *, size_t, void *, size_t,const SA *, socklen_t);// UDP 客户端程序voiddg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){ssize_tn;charsendline[MAXLINE], recvline[MAXLINE + 1];// 从文件读取数据while (Fgets(sendline, MAXLINE, fp) != NULL) {// 收发数据处理函数n = Dg_send_recv(sockfd, sendline, strlen(sendline),recvline, MAXLINE, pservaddr, servlen);recvline[n] = 0;// 打印收到的数据Fputs(recvline, stdout);}}

#include"unprtt.h"#include<setjmp.h>#defineRTT_DEBUG/*** rtt_info 数据结构 :* * struct rtt_info {*floatrtt_rtt;// most recent measured RTT, seconds*floatrtt_srtt;// smoothed RTT estimator, seconds*floatrtt_rttvar;// smoothed mean deviation, seconds*floatrtt_rto;// current RTO to use, seconds*intrtt_nrexmt;// #times retransmitted: 0, 1, 2, ...*uint32_trtt_base;// #sec since 1/1/1970 at start*};** struct msghdr { * * // 消息的协议地址 协议地址和套接口信息,在非连接的 UDP 中,发送者要指定对方地址端口,* 接受方用于的到数据来源,如果不需要的话可以设置为 NULL* (在 TCP 或者连接的 UDP 中,一般设置为 NULL)* void * msg_name ; * socklen_t msg_namelen ;//地址的长度 * struct iovec* msg_iov ; //多 io 缓冲区的地址* int msg_iovlen ; //缓冲区的个数* void * msg_control ; //辅助数据的地址* socklen_t msg_controllen ; //辅助数据的长度* int msg_flags ; //接收消息的标识* } * * #include <sys/uio.h>* struct iovec{*void *iov_base; // Pointer to data*size_t iov_len; // Length of data* }*/static struct rtt_info rttinfo;static intrttinit = 0;static struct msghdrmsgsend, msgrecv;static struct hdr {uint32_tseq;uint32_tts;} sendhdr, recvhdr;// 定时器信号处理函数,处理 SIGALRM 信号static voidsig_alrm(int signo);static sigjmp_bufjmpbuf;ssize_tdg_send_recv(int fd, const void *outbuff, size_t outbytes,void *inbuff, size_t inbytes,const SA *destaddr, socklen_t destlen){ssize_tn;/*** iovsend[ 0 ] : 存放信息头; iovsend[ 1 ] : 存放信息体;* iovrecv[ 0 ] : 存放信息头; iovrecv[ 1 ] : 存放信息体;*/struct ioveciovsend[2], iovrecv[2];if (rttinit == 0) {// 初始化 rtt_info 结构体变量rtt_init(&rttinfo);rttinit = 1;rtt_d_flag = 1;}// 初始化收发信息缓冲区结构体sendhdr.seq++;msgsend.msg_name = destaddr;msgsend.msg_namelen = destlen;msgsend.msg_iov = iovsend;msgsend.msg_iovlen = 2;iovsend[0].iov_base = &sendhdr;iovsend[0].iov_len = sizeof(struct hdr);iovsend[1].iov_base = outbuff;iovsend[1].iov_len = outbytes;msgrecv.msg_name = NULL;msgrecv.msg_namelen = 0;msgrecv.msg_iov = iovrecv;msgrecv.msg_iovlen = 2;iovrecv[0].iov_base = &recvhdr;iovrecv[0].iov_len = sizeof(struct hdr);iovrecv[1].iov_base = inbuff;iovrecv[1].iov_len = inbytes;// 指定 SIGALRM 信号的处理函数Signal(SIGALRM, sig_alrm);// 重置 rtt_nrexmt(重传次数) rtt_newpack(&rttinfo);// 定时时间到达 RTO 时,重新发送数据sendagain:#ifdefRTT_DEBUGfprintf(stderr, "send %4d: ", sendhdr.seq);#endif// 更新 rtt_basesendhdr.ts = rtt_ts(&rttinfo);// 发送数据Sendmsg(fd, &msgsend, 0);// 启动定时器,定时时间为 RTO 的值alarm(rtt_start(&rttinfo));#ifdefRTT_DEBUG// 打印调试信息rtt_debug(&rttinfo);#endif/*** sigsetjmp() 会保存目前堆栈环境,然后将目前的地址作一个记号,* 而在程序其他地方调用 siglongjmp() 时便会直接跳到这个记号位置,* 然后还原堆栈,继续执行程序;* * 调用 sigsetjmp 为信号处理函数建立了一个跳转缓冲区,* 若 sigsetjmp 的返回不是由长跳转引起则调用 recvmsg 等待下一个数据报到达,* 若 alarm 定时期满,sigsetjmp 便由长跳转返回1;*/if (sigsetjmp(jmpbuf, 1) != 0) {// 判断是否发生了 timeoutif (rtt_timeout(&rttinfo) < 0) {err_msg("dg_send_recv: no response from server, giving up");rttinit = 0;errno = ETIMEDOUT;return(-1);}#ifdefRTT_DEBUG// 若是 DEBUG 模式,则打印调试信息err_msg("dg_send_recv: timeout, retransmitting");#endifgoto sendagain;}do {n = Recvmsg(fd, &msgrecv, 0);#ifdefRTT_DEBUGfprintf(stderr, "recv %4d\n", recvhdr.seq);#endif} while (n < sizeof(struct hdr) || recvhdr.seq != sendhdr.seq);// 接收到正确的应答数据后,关闭定时器alarm(0);// 更新 RTT 的值rtt_stop(&rttinfo, rtt_ts(&rttinfo) - recvhdr.ts);// 返回数据体的字节数return(n - sizeof(struct hdr));}static voidsig_alrm(int signo){siglongjmp(jmpbuf, 1);}// 封装 dg_send_recv 函数,在出错时打印日志信息ssize_tDg_send_recv(int fd, const void *outbuff, size_t outbytes,void *inbuff, size_t inbytes,const SA *destaddr, socklen_t destlen){ssize_tn;n = dg_send_recv(fd, outbuff, outbytes, inbuff, inbytes,destaddr, destlen);if (n < 0)err_quit("dg_send_recv error");return(n);}

#include"unprtt.h"intrtt_d_flag = 0;/* debug flag; can be set by caller *//** Calculate the RTO value based on current estimators:*smoothed RTT plus four times the deviation*/#defineRTT_RTOCALC(ptr) ((ptr)->rtt_srtt + (4.0 * (ptr)->rtt_rttvar))/*** 将入参规范化到一定的范围内*/static floatrtt_minmax(float rto){if (rto < RTT_RXTMIN)rto = RTT_RXTMIN;else if (rto > RTT_RXTMAX)rto = RTT_RXTMAX;return(rto);}// 初始化 rtt_info 结构体voidrtt_init(struct rtt_info *ptr){struct timevaltv;Gettimeofday(&tv, NULL);ptr->rtt_base = tv.tv_sec;/* # sec since 1/1/1970 at start */ptr->rtt_rtt = 0;ptr->rtt_srtt = 0;ptr->rtt_rttvar = 0.75;ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));}// 重置 rtt_base uint32_trtt_ts(struct rtt_info *ptr){uint32_tts;struct timevaltv;Gettimeofday(&tv, NULL);ts = ((tv.tv_sec - ptr->rtt_base) * 1000) + (tv.tv_usec / 1000);return(ts);}voidrtt_newpack(struct rtt_info *ptr){ptr->rtt_nrexmt = 0;}// 返回 RTO 的值intrtt_start(struct rtt_info *ptr){return((int) (ptr->rtt_rto + 0.5));}/** A response was received.* Stop the timer and update the appropriate values in the structure* based on this packet's RTT. We calculate the RTT, then update the* estimators of the RTT and its mean deviation.* This function should be called right after turning off the* timer with alarm(0), or right after a timeout occurs.*/voidrtt_stop(struct rtt_info *ptr, uint32_t ms){doubledelta;// 以秒为单位,估计 RTT 的大小ptr->rtt_rtt = ms / 1000.0;/*** 更新 RTT* * Update our estimators of RTT and mean deviation of RTT.* See Jacobson's SIGCOMM '88 paper, Appendix A, for the details.* We use floating point here for simplicity.*/delta = ptr->rtt_rtt - ptr->rtt_srtt;ptr->rtt_srtt += delta / 8;/* g = 1/8 */if (delta < 0.0)delta = -delta;/* |delta| */ptr->rtt_rttvar += (delta - ptr->rtt_rttvar) / 4;/* h = 1/4 */ptr->rtt_rto = rtt_minmax(RTT_RTOCALC(ptr));}/*** 判断是否发生了 timeout 事件* * A timeout has occurred.* Return -1 if it's time to give up, else return 0.*/intrtt_timeout(struct rtt_info *ptr){ptr->rtt_rto *= 2;/* next RTO */if (++ptr->rtt_nrexmt > RTT_MAXNREXMT)return(-1);/* time to give up for this packet */return(0);}// 打印 rtt_info 的信息以方便调试voidrtt_debug(struct rtt_info *ptr){if (rtt_d_flag == 0)return;fprintf(stderr, "rtt = %.3f, srtt = %.3f, rttvar = %.3f, rto = %.3f\n",ptr->rtt_rtt, ptr->rtt_srtt, ptr->rtt_rttvar, ptr->rtt_rto);fflush(stderr);}

【3.5】绑定接口地址

get_ifi_info函数用途:监视本地主机所有接口以便获悉某个数据报在何时以及哪个端口到达UDP应用程序;可通过get_ifi_info函数获取UDP数据报的目的地址;

示例程序分析

#include"unpifi.h"voidmydg_echo(int, SA *, socklen_t, SA *);intmain(int argc, char **argv){intsockfd;const inton = 1;pid_tpid;struct ifi_info*ifi, *ifihead;struct sockaddr_in*sa, cliaddr, wildaddr;/*** 调用 Get_ifi_info 获取所有 IPv4 地址包括别名地址*/for (ifihead = ifi = Get_ifi_info(AF_INET, 1);ifi != NULL; ifi = ifi->ifi_next) {/*** 创建 UDP 套接字,并在其上绑定单播地址* SO_REUSEADDR : */sockfd = Socket(AF_INET, SOCK_DGRAM, 0);/*** 1. SO_REUSEADDR 允许启动一个监听服务器并捆绑其众所周知的端口,即使以前建立的将该端口用作他们的本地端口的连接仍存在;* 2. 允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地 IP 地址即可;* 3. SO_REUSEADDR 允许单个进程捆绑同一端口到多个套接字上,只要每次捆绑指定不同的本地 IP 地址即可;* 4. SO_REUSEADDR 允许完全重复的捆绑:当一个 IP 地址和端口号已绑定到某个套接字上时,如果传输协议支持,* 同样的 IP 地址和端口还可以捆绑到另一个套接字上,一般来说本特性仅支持UDP套接字;*/Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));sa = (struct sockaddr_in *) ifi->ifi_addr;sa->sin_family = AF_INET;sa->sin_port = htons(SERV_PORT);Bind(sockfd, (SA *) sa, sizeof(*sa));printf("bound %s\n", Sock_ntop((SA *) sa, sizeof(*sa)));// 创建一个子进程,等待数据报到达并回射给发送者if ( (pid = Fork()) == 0) {mydg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr), (SA *) sa);exit(0);}/*** 若当前接口支持广播,则绑定广播地址;*/if (ifi->ifi_flags & IFF_BROADCAST) {sockfd = Socket(AF_INET, SOCK_DGRAM, 0);Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));sa = (struct sockaddr_in *) ifi->ifi_brdaddr;sa->sin_family = AF_INET;sa->sin_port = htons(SERV_PORT);if (bind(sockfd, (SA *) sa, sizeof(*sa)) < 0) {/*** 允许 bind 调用以 EADDRINUSE 错误返回失败的结果;* 原因 : 若某个接口有多个处于同一个子网的地址(别名),* 则这些单播地址需要对应同一个广播地址; */if (errno == EADDRINUSE) {printf("EADDRINUSE: %s\n",Sock_ntop((SA *) sa, sizeof(*sa)));Close(sockfd);continue;} elseerr_sys("bind error for %s",Sock_ntop((SA *) sa, sizeof(*sa)));}printf("bound %s\n", Sock_ntop((SA *) sa, sizeof(*sa)));// 创建一个子进程,等待数据报到达并回射给发送者if ( (pid = Fork()) == 0) {mydg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr),(SA *) sa);exit(0);}}}sockfd = Socket(AF_INET, SOCK_DGRAM, 0);Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));bzero(&wildaddr, sizeof(wildaddr));wildaddr.sin_family = AF_INET;wildaddr.sin_addr.s_addr = htonl(INADDR_ANY);wildaddr.sin_port = htons(SERV_PORT);Bind(sockfd, (SA *) &wildaddr, sizeof(wildaddr));printf("bound %s\n", Sock_ntop((SA *) &wildaddr, sizeof(wildaddr)));// 创建一个子进程,等待数据报到达并回射给发送者if ( (pid = Fork()) == 0) {mydg_echo(sockfd, (SA *) &cliaddr, sizeof(cliaddr), (SA *) sa);exit(0);}// 主进程终止,派生出的所有子进程继续运行exit(0);}voidmydg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, SA *myaddr){intn;charmesg[MAXLINE];socklen_tlen;// 无限循环for ( ; ; ) {len = clilen;// 接收数据n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);printf("child %d, datagram from %s", getpid(),Sock_ntop(pcliaddr, len));printf(", to %s\n", Sock_ntop(myaddr, clilen));// 发送接收到的数据,回射Sendto(sockfd, mesg, n, 0, pcliaddr, len);}}

#include"unpifi.h"#include"unproute.h"/*** 结构体结构 :* * struct if_msghdr {*u_short ifm_msglen;// to skip over non-understood messages*u_char ifm_version;// future binary compatability*u_charifm_type;// message type**int ifm_addrs;// bitmask identifying sockaddrs in msg*intifm_flags;// value of if_flags*u_short ifm_index;// index for associated ifp*struct if_data ifm_data;// statistics and other data about if*}**struct ifa_msghdr {*u_short ifam_msglen;// to skip over non-understood messages*u_charifam_version;// future binary compatability*u_charifam_type;// message type**intifam_addrs;// bitmask identifying sockaddrs in msg*intifam_flags;// value of ifa_flags*u_short ifam_index;// index for associated ifp*intifam_metric;// value of ifa_metric*}* struct ifi_info {* char ifi_name[IFI_NAME];// interface name, null-terminated*short ifi_index;// interface index*short ifi_mtu;// interface MTU*u_char ifi_haddr[IFI_HADDR];// hardware address*u_short ifi_hlen;// # bytes in hardware address: 0, 6, 8*short ifi_flags;// IFF_xxx constants from <net/if.h>*short ifi_myflags;// our own IFI_xxx flags*struct sockaddr *ifi_addr;// primary address*struct sockaddr *ifi_brdaddr;// broadcast address*struct sockaddr *ifi_dstaddr;// destination address*struct ifi_info *ifi_next;// next of these structures* }*/struct ifi_info *get_ifi_info(int family, int doaliases){int flags;char*buf, *next, *lim;size_tlen;struct if_msghdr*ifm;struct ifa_msghdr*ifam;struct sockaddr*sa, *rti_info[RTAX_MAX];struct sockaddr_dl*sdl;struct ifi_info*ifi, *ifisave, *ifihead, **ifipnext;/*** 检查路由表或接口清单*/buf = Net_rt_iflist(family, 0, &len);ifihead = NULL;ifipnext = &ifihead;// lim : 返回信息的尾部lim = buf + len;for (next = buf; next < lim; next += ifm->ifm_msglen) {ifm = (struct if_msghdr *) next;/*** RTM_IFINFO: 接口正在开工、停工等* RTM_NEWADDR: 多播地址正在被增至接口* * IFF_POINTOPOINT : Interface is point-to-point link* IFF_BROADCAST : Broadcast address valid* IFF_UP: Interface is up*/if (ifm->ifm_type == RTM_IFINFO) {if ( ((flags = ifm->ifm_flags) & IFF_UP) == 0)continue;sa = (struct sockaddr *) (ifm + 1);/*** 根据 ifm->ifm_addrs 的掩码将 sa 地址信息存储到 rti_info 中*/get_rtaddrs(ifm->ifm_addrs, sa, rti_info);if ( (sa = rti_info[RTAX_IFP]) != NULL) {/*** 函数名 :calloc* 函数原型 :void* calloc(unsigned int num,unsigned int size);* 功能:在内存的动态存储区中分配 num 个长度为 size 的连续空间,函数返回一个指向分配起始地址的指针;* 如果分配不成功,返回 NULL;*/ifi = Calloc(1, sizeof(struct ifi_info));/*** *ifipnext = ifi,ifipnext 的值为 ifi* ifipnext = &ifi->ifi_next,ifipnext 指针指向 ifi->ifi_next 的地址,* 即将 ifi 连接到了队列中;*/*ifipnext = ifi;ifipnext = &ifi->ifi_next;ifi->ifi_flags = flags;/*** 对于数据链路的套接字* sockaddr_dl : 数据链路套接字地址结构*/if (sa->sa_family == AF_LINK) {sdl = (struct sockaddr_dl *) sa;ifi->ifi_index = sdl->sdl_index;if (sdl->sdl_nlen > 0)snprintf(ifi->ifi_name, IFI_NAME, "%*s",sdl->sdl_nlen, &sdl->sdl_data[0]);elsesnprintf(ifi->ifi_name, IFI_NAME, "index %d",sdl->sdl_index);if ( (ifi->ifi_hlen = sdl->sdl_alen) > 0)memcpy(ifi->ifi_haddr, LLADDR(sdl),min(IFI_HADDR, sdl->sdl_alen));}}} else if (ifm->ifm_type == RTM_NEWADDR) {if (ifi->ifi_addr) {if (doaliases == 0)/*** 对于多播地址若不需要记录别名,* 则仅仅记录一个地址信息即可*/continue;ifisave = ifi;ifi = Calloc(1, sizeof(struct ifi_info));*ifipnext = ifi;ifipnext = &ifi->ifi_next;ifi->ifi_flags = ifisave->ifi_flags;ifi->ifi_index = ifisave->ifi_index;ifi->ifi_hlen = ifisave->ifi_hlen;memcpy(ifi->ifi_name, ifisave->ifi_name, IFI_NAME);memcpy(ifi->ifi_haddr, ifisave->ifi_haddr, IFI_HADDR);}ifam = (struct ifa_msghdr *) next;sa = (struct sockaddr *) (ifam + 1);/*** 根据 ifam->ifam_addrs 的掩码将 sa 地址信息存储到 rti_info 中*/get_rtaddrs(ifam->ifam_addrs, sa, rti_info);if ( (sa = rti_info[RTAX_IFA]) != NULL) {ifi->ifi_addr = Calloc(1, sa->sa_len);memcpy(ifi->ifi_addr, sa, sa->sa_len);}if ((flags & IFF_BROADCAST) &&(sa = rti_info[RTAX_BRD]) != NULL) {ifi->ifi_brdaddr = Calloc(1, sa->sa_len);memcpy(ifi->ifi_brdaddr, sa, sa->sa_len);}if ((flags & IFF_POINTOPOINT) &&(sa = rti_info[RTAX_BRD]) != NULL) {ifi->ifi_dstaddr = Calloc(1, sa->sa_len);memcpy(ifi->ifi_dstaddr, sa, sa->sa_len);}} elseerr_quit("unexpected message type %d", ifm->ifm_type);}return(ifihead);}/*** 释放 ifi_info 结构内存*/voidfree_ifi_info(struct ifi_info *ifihead){struct ifi_info*ifi, *ifinext;for (ifi = ifihead; ifi != NULL; ifi = ifinext) {if (ifi->ifi_addr != NULL)free(ifi->ifi_addr);if (ifi->ifi_brdaddr != NULL)free(ifi->ifi_brdaddr);if (ifi->ifi_dstaddr != NULL)free(ifi->ifi_dstaddr);ifinext = ifi->ifi_next;free(ifi);}}/*** 包装 get_ifi_info 函数,在发生错误时打印错误日志*/struct ifi_info *Get_ifi_info(int family, int doaliases){struct ifi_info*ifi;if ( (ifi = get_ifi_info(family, doaliases)) == NULL)err_quit("get_ifi_info error");return(ifi);}

#include"unproute.h"char *net_rt_iflist(int family, int flags, size_t *lenp){intmib[6];char*buf;/*** #include<sys/param.h>* #include<sys/sysctl.h>* int sysctl( int *name, u_int namelen, void *oldp, size_t *oldenp, void *newp, size_t newlen );* 返回 0:成功 -1:失败* * name 参数是指定名字的一个整数数组,namelen 参数指定了该数组中的元素数目;* 该数组中的第一个元素指定本请求定向到内核的哪个子系统;*第二个及其后元素依次细化指定该系统的某个部分;* 获取某个值 : * oldp 参数指向一个供内核存放该值的缓冲区,oldlenp 则是一个值-结果参数:函数被调用时,oldlenp 指向的值指定该缓冲区的大小;* 函数返回时,该值给出内核存放在该缓冲区中的数据量,如果这个缓冲不够大,函数就返回 ENOMEM 错误;* 作为特例,oldp 可以是一个空指针,而 oldlenp 却是一个非空指针,内核确定这样的调用应该返回的数据量,并通过 oldlenp 返回这个大小;* 设置某个新值 : * newp 参数指向一个大小为 newlen 参数值的缓冲区,如果不准备指定一个新值,那么 newp 应为一个空指针,newlen 应为 0;* * mib[0] = CTL_NET : 获取网络子系统的信息;* mib[1] = AF_ROUTE : * AF_INET:获取或者设置影响网际协议的变量,下一级为使用某个 IPPROTO_XXX 常值指定的具体协议;* AF_LINK:获取或设置链路层信息;* AF_ROUTE : 返回路由表或接口清单的信息;*AF_UNSPEC: 获取或设置一些套接口层变量;** name[]返回IPv4路由表返回IPv4ARP高速缓存返回IPv6路由表 返回接口清单*0CTL_NETCTL_NETCTL_NETCTL_NET*1AF_ROUTEAF_ROUTEAF_ROUTEAF_ROUTE*20000*3AF_INETAF_INETAF_INET60*4NET_RT_DUMPNET_RT_FLAGS NET_RT_DUMPNET_RT_IFLIST*50 RTF_LLINFO00** 1. NET_RT_DUMP 返回由 name[3] 指定的地址族的路由表,如果所指定的地址族为 0,那么返回所有地址族的路由表;* 路由表作为可变数目的 RTM_GET 消息返回,每个消息后跟最多 4 个套接口地址结构:本路由表项目的地址,网关,网络掩码和克隆掩码;* 相比直接读写路由套接口操作,sysctl 操作所有改动仅仅体现在内核通过后者返回一个或者多个 RTM_GET 消息;* 2. NET_RT_FLAGS 返回由 name[3] 指定的地址族的路由表,但是仅仅限于那些所带标志(若干个 RTF_XXX 常值的逻辑或)* 与由 name[5] 指定的标志匹配的路由表项,路由表中所有 ARP 高速缓存均设置了 RTF_LLINFO 标志位,* 这种操作的信息返回格式和上一种操作的一致;* 3. NET_RT_IFLIST 返回所有已配置接口的信息,如果 name[5] 不为 0,它就是某个接口的索引,于是仅仅返回该接口的信息,* 已赋予每个接口的所有地址也同时返回;不过如果 name[3] 不为 0,那么仅限于返回指定地址族的地址;*/mib[0] = CTL_NET;mib[1] = AF_ROUTE;mib[2] = 0;mib[3] = family;/* only addresses of this family */mib[4] = NET_RT_IFLIST;mib[5] = flags;/* interface index or 0 */if (sysctl(mib, 6, NULL, lenp, NULL, 0) < 0)return(NULL);if ( (buf = malloc(*lenp)) == NULL)return(NULL);if (sysctl(mib, 6, buf, lenp, NULL, 0) < 0) {free(buf);return(NULL);}return(buf);}/*** 包装 net_rt_iflist 函数,在发生错误时打印错误日志*/char *Net_rt_iflist(int family, int flags, size_t *lenp){char*ptr;if ( (ptr = net_rt_iflist(family, flags, lenp)) == NULL)err_sys("net_rt_iflist error");return(ptr);}

#include"unproute.h"/** Round up 'a' to next multiple of 'size', which must be a power of 2* a & (2^n-1) 表示 a 的低 n 位是否有值* a | (2^n - 1) 表示 a 的低 n 位赋值为1* 1 + a | (2^n -1) 表示离 a 最近的下一个 2^n 倍值*/#define ROUNDUP(a, size) (((a) & ((size)-1)) ? (1 + ((a) | ((size)-1))) : (a))/** Step to next socket address structure;* if sa_len is 0, assume it is sizeof(u_long).* * sizeof(u_long)* 您的电脑是16位的比特数:* unsigned long的比特数是16,数的范围是0~(2的16方 -1);* 您的电脑是32位的比特数:* unsigned long的比特数是32,数的范围是0~(2的32方 -1);*/#define NEXT_SA(ap)ap = (SA *) \((caddr_t) ap + (ap->sa_len ? ROUNDUP(ap->sa_len, sizeof (u_long)) : \sizeof(u_long)))/*** 根据 addrs 的掩码将 sa 地址信息存储到 rti_info 中*/voidget_rtaddrs(int addrs, SA *sa, SA **rti_info){inti;for (i = 0; i < RTAX_MAX; i++) {if (addrs & (1 << i)) {rti_info[i] = sa;NEXT_SA(sa);} elserti_info[i] = NULL;}}

【3.6】并发UDP服务器

情形一:服务器读入客户请求并发送一个应答之后,与该客户便不再相关了; 解决方式:服务器创建子进程处理请求,当子进程处理完毕后,将应答直接发送给客户端;处理示意图情形二:UDP服务器与客户交换多个数据报; 待解决的问题:如何区分接收到的数据报是来自该客户同一个请求的后续数据报还是来自其他客户请求的数据报;解决方式:服务器为每个用户创建新的套接字,在其上绑定临时端口,然后使用该套接字发送对该客户的所有应答;客户端解析接收到的第一个应答中的源端口号,并将请求的后续数据报发送到该端口;处理示意图

参考

本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】UNIX网络编程

【2】Linux高性能服务器编程

【3】select函数在TCP和UDP回射服务器中的应用

【4】信号处理函数的返回sigsetjmp/siglongjmp

【5】《TCP/IP详解卷2:实现》笔记--选路请求和选路消息

【6】sysctl 函数

【7】UNP学习笔记(第十八章 路由套接字)

【8】网卡接口相关定义

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。