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

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

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

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

【2】基本UDP套接字编程

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

【Linux网络编程】UDP 套接字编程_第1张图片

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

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

服务器端示例代码

#include	"unp.h"

int
main(int argc, char **argv)
{
	/**
	 * sockfd : socket 文件描述符;
	 * servaddr : 服务器 socket 地址结构;
	 * cliaddr : 客户端 socket 地址结构;
	 */
	int					sockfd;
	struct sockaddr_in	servaddr, 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));
}

void
dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
{
	/**
	 * n : Recvfrom 函数调用的返回值,表示接收到的字节数;
	 * len : 接收到的客户端地址信息的大小
	 * mesg : 数据缓冲区
	 */
	int			n;
	socklen_t	len;
	char		mesg[MAXLINE];
	/**
	 * 不断循环将接收到的数据回射给客户端
	 */
	for ( ; ; ) {
		len = clilen;
		/**
		 * 从客户端接收数据到数据缓冲区
		 * pcliaddr : 指向客户端地址结构的指针
		 */
		n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
		/**
		 * 将从客户端接收到的数据回射给客户端;
		 */
		Sendto(sockfd, mesg, n, 0, pcliaddr, len);
	}
}

客户端示例代码

#include	"unp.h"

int
main(int argc, char **argv)
{
	/**
	 * socket : 套接字文件描述符;
	 * servaddr : 服务器 socket 地址结构;
	 */
	int					sockfd;
	struct sockaddr_in	servaddr;

	if (argc != 2)
		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.sin_addr);
	/**
	 * 新建套接字
	 * AF_INET : IPv4协议;
	 * SOCK_DGRAM : 数据包套接字;
	 */
	sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
	/**
	 * 调用客户端回射函数
	 */
	dg_cli(stdin, sockfd, (SA *) &servaddr, sizeof(servaddr));

	exit(0);
}

void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	/**
	 * n : Recvfrom 函数调用的返回值,表示接收到的字节数;
	 * sendline : 数据发送缓冲区
	 * recvline : 数据接收缓冲区
	 * len : 接收到的服务器地址信息的大小
	 * replyaddr : 发送数据的对端地址结构信息
	 */
	int				n;
	char			sendline[MAXLINE], recvline[MAXLINE + 1];
	socklen_t		len;
	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套接字比较

套接字类型 write或send 不指定目的地址的sendto 指定目的地址的sendto
TCP套接字 可以 可以 EISCONN
UDP套接字,已连接 可以 可以 EISCONN
UDP套接字,未连接 EDESTADDRREQ EDESTADDRREQ 可以

使用场合: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】模型示意图

【Linux网络编程】UDP 套接字编程_第2张图片

【2.5.2】示例程序

#include	"unp.h"

int
main(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;
	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);

	/**
	 * 创建监听套接字
	 * 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() */
			else
				err_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);
		}
	}
}

void
sig_chld(int signo)
{
	pid_t	pid;
	int		stat;
	/**
	 * 原型 : 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			/* ALIGN macro for CMSG_NXTHDR() macro */
/**
 * 函数入参说明 :
 * fd : socket 文件描述符
 * ptr : 数据缓冲区指针
 * nbytes : 数据缓冲区大小
 * flagsp : 值传递方式的标志位,用于传入控制信息
 * sa : 地址结构指针
 * salenptr : 地址结构大小指针
 * pktp : 函数 recvfrom_flags 返回信息结构体指针
 */
/**
 * struct unp_in_pktinfo {
 *   struct in_addr		ipi_addr;	// 保存 IP 地址信息
 *   int			ipi_ifindex;	// 保存系统分配的索引
 * };
 */
/**
 * 接收数据并返回标志信息
 */
ssize_t
recvfrom_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_t	msg_namelen;		//协议地址长度
     *   struct lovec   * msg_lov;		//输入输出缓冲区数组
     *   ssize_t	msg_lovlen;		//输入输出数组中缓冲区的个数
     *   void		* msg_control;		//辅助数据的起始位置指针
     *   socklen_t	msg_controllen;		//辅助数据大小
     *   int		msg_flags;	    	//引用传递,用于返回之前flags的控制信息
	 * }
	 */
	struct msghdr	msg;
	/**
	 * struct iovec{
	 *	 void *iov_base; // 起始地址指针
	 *	 size_t iov_len; // 区间长度
	 *};
	 */
	struct iovec	iov[1];
	/**
	 * n : 接收到的字节数
	 */
	ssize_t			n;

#ifdef	HAVE_MSGHDR_MSG_CONTROL
	struct cmsghdr	*cmptr;
	union {
	  struct cmsghdr	cm;
	  char			control[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
	// 初始化 msg
	bzero(&msg, sizeof(msg));	/* make certain msg_accrightslen = 0 */
#endif
	// 初始化 msg
	msg.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,并返回
#ifndef	HAVE_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 数据报接收目的地址;
 */
#ifdef	IP_RECVDSTADDR
		if (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 数据报接收接口索引;
 */
#ifdef	IP_RECVIF
		if (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;
		}
#endif
		err_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_t
Recvfrom_flags(int fd, void *ptr, size_t nbytes, int *flagsp,
			   SA *sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp)
{
	ssize_t		n;

	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;

计算公式

【Linux网络编程】UDP 套接字编程_第3张图片

【Linux网络编程】UDP 套接字编程_第4张图片

示例代码分析

#include	"unp.h"

ssize_t	Dg_send_recv(int, const void *, size_t, void *, size_t,
				   const SA *, socklen_t);
// UDP 客户端程序
void
dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
{
	ssize_t	n;
	char	sendline[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	

#define	RTT_DEBUG
/**
 * rtt_info 数据结构 :
 * 
 * struct rtt_info {
 *		float		rtt_rtt;	// most recent measured RTT, seconds
 *		float		rtt_srtt;	// smoothed RTT estimator, seconds
 *		float		rtt_rttvar;	// smoothed mean deviation, seconds
 *		float		rtt_rto;	// current RTO to use, seconds
 *		int		rtt_nrexmt;	// #times retransmitted: 0, 1, 2, ...
 *		uint32_t	rtt_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 
 * struct iovec{
 *      void *iov_base; // Pointer to data
 *      size_t iov_len; // Length of data
 * }
 */
static struct rtt_info   rttinfo;
static int	rttinit = 0;
static struct msghdr	msgsend, msgrecv;
static struct hdr {
  uint32_t	seq;
  uint32_t	ts;
} sendhdr, recvhdr;

// 定时器信号处理函数,处理 SIGALRM 信号
static void	sig_alrm(int signo);
static sigjmp_buf	jmpbuf;

ssize_t
dg_send_recv(int fd, const void *outbuff, size_t outbytes,
			 void *inbuff, size_t inbytes,
			 const SA *destaddr, socklen_t destlen)
{
	ssize_t			n;
	/**
	 * iovsend[ 0 ] : 存放信息头; iovsend[ 1 ] : 存放信息体;
	 * iovrecv[ 0 ] : 存放信息头; iovrecv[ 1 ] : 存放信息体;
	 */
	struct iovec	iovsend[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:
#ifdef	RTT_DEBUG
	fprintf(stderr, "send %4d: ", sendhdr.seq);
#endif
	// 更新 rtt_base
	sendhdr.ts = rtt_ts(&rttinfo);
	// 发送数据
	Sendmsg(fd, &msgsend, 0);
	// 启动定时器,定时时间为 RTO 的值
	alarm(rtt_start(&rttinfo));
#ifdef	RTT_DEBUG
	// 打印调试信息
	rtt_debug(&rttinfo);
#endif
	/**
	 * sigsetjmp() 会保存目前堆栈环境,然后将目前的地址作一个记号,
	 * 而在程序其他地方调用 siglongjmp() 时便会直接跳到这个记号位置,
	 * 然后还原堆栈,继续执行程序;
	 * 
	 * 调用 sigsetjmp 为信号处理函数建立了一个跳转缓冲区,
	 * 若 sigsetjmp 的返回不是由长跳转引起则调用 recvmsg 等待下一个数据报到达,
	 * 若 alarm 定时期满,sigsetjmp 便由长跳转返回1;
	 */
	if (sigsetjmp(jmpbuf, 1) != 0) {
		// 判断是否发生了 timeout
		if (rtt_timeout(&rttinfo) < 0) {
			err_msg("dg_send_recv: no response from server, giving up");
			rttinit = 0;
			errno = ETIMEDOUT;
			return(-1);
		}
#ifdef	RTT_DEBUG
		// 若是 DEBUG 模式,则打印调试信息
		err_msg("dg_send_recv: timeout, retransmitting");
#endif
		goto sendagain;
	}

	do {
		n = Recvmsg(fd, &msgrecv, 0);
#ifdef	RTT_DEBUG
		fprintf(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 void
sig_alrm(int signo)
{
	siglongjmp(jmpbuf, 1);
}

// 封装 dg_send_recv 函数,在出错时打印日志信息
ssize_t
Dg_send_recv(int fd, const void *outbuff, size_t outbytes,
			 void *inbuff, size_t inbytes,
			 const SA *destaddr, socklen_t destlen)
{
	ssize_t	n;

	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"

int		rtt_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
 */
#define	RTT_RTOCALC(ptr) ((ptr)->rtt_srtt + (4.0 * (ptr)->rtt_rttvar))

/**
 * 将入参规范化到一定的范围内
 */
static float
rtt_minmax(float rto)
{
	if (rto < RTT_RXTMIN)
		rto = RTT_RXTMIN;
	else if (rto > RTT_RXTMAX)
		rto = RTT_RXTMAX;
	return(rto);
}
// 初始化 rtt_info 结构体
void
rtt_init(struct rtt_info *ptr)
{
	struct timeval	tv;

	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_t
rtt_ts(struct rtt_info *ptr)
{
	uint32_t		ts;
	struct timeval	tv;

	Gettimeofday(&tv, NULL);
	ts = ((tv.tv_sec - ptr->rtt_base) * 1000) + (tv.tv_usec / 1000);
	return(ts);
}

void
rtt_newpack(struct rtt_info *ptr)
{
	ptr->rtt_nrexmt = 0;
}

// 返回 RTO 的值
int
rtt_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.
 */

void
rtt_stop(struct rtt_info *ptr, uint32_t ms)
{
	double		delta;
	// 以秒为单位,估计 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.
 */
int
rtt_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 的信息以方便调试
void
rtt_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"

void	mydg_echo(int, SA *, socklen_t, SA *);

int
main(int argc, char **argv)
{
	int					sockfd;
	const int			on = 1;
	pid_t				pid;
	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;
				} else
					err_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);
}

void
mydg_echo(int sockfd, SA *pcliaddr, socklen_t clilen, SA *myaddr)
{
	int			n;
	char		mesg[MAXLINE];
	socklen_t	len;
	// 无限循环
	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_char	ifm_type;			// message type
 *		
 *		int 	ifm_addrs;			// bitmask identifying sockaddrs in msg
 *		int		ifm_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_char	ifam_version;		// future binary compatability
 *		u_char	ifam_type;			// message type
 *		
 *		int		ifam_addrs;			// bitmask identifying sockaddrs in msg
 *		int		ifam_flags;			// value of ifa_flags
 *		u_short ifam_index;			// index for associated ifp
 *		int		ifam_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 
 *		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_t				len;
	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]);
					else
						snprintf(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);
			}

		} else
			err_quit("unexpected message type %d", ifm->ifm_type);
	}
	return(ifihead);
}

/**
 * 释放 ifi_info 结构内存
 */
void
free_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)
{
	int		mib[6];
	char	*buf;

	/**
	 * #include
	 * #include
	 * 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路由表	  返回接口清单
	 *	0	CTL_NET		CTL_NET				CTL_NET			CTL_NET
	 *	1	AF_ROUTE	AF_ROUTE			AF_ROUTE		AF_ROUTE
	 *	2	0		0				0			0
	 *	3	AF_INET		AF_INET				AF_INET6		0
	 *	4	NET_RT_DUMP	NET_RT_FLAGS		        NET_RT_DUMP		NET_RT_IFLIST
	 *	5	0	        RTF_LLINFO			0			0
	 *
	 * 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 中
 */
void
get_rtaddrs(int addrs, SA *sa, SA **rti_info)
{
	int		i;

	for (i = 0; i < RTAX_MAX; i++) {
		if (addrs & (1 << i)) {
			rti_info[i] = sa;
			NEXT_SA(sa);
		} else
			rti_info[i] = NULL;
	}
}

【3.6】并发UDP服务器

  • 情形一:服务器读入客户请求并发送一个应答之后,与该客户便不再相关了;
    • 解决方式:服务器创建子进程处理请求,当子进程处理完毕后,将应答直接发送给客户端;
    • 处理示意图

【Linux网络编程】UDP 套接字编程_第5张图片

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

【Linux网络编程】UDP 套接字编程_第6张图片

 

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

【1】UNIX网络编程

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

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

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

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

【6】sysctl 函数

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

【8】网卡接口相关定义

 

你可能感兴趣的:(网络编程系列,--,Linux,网络应用【套接字编程基础】)