《网络编程》高级 UDP 套接字编程

概述

UDP 是一个无连接、不可靠的数据报协议,任何可靠传输都需由应用程序提供,例如:超时重传、序列号应答机制,但是它在某些场合使用效率高,方便。它支持广播和多播。有关《基本 UDP 套接字编程》参照该文,这里只是在那个基础上,记录一些在 UDP 编程中容易出现的问题。

辅助数据

辅助数据(也称为控制信息)可通过调用 recvmsg 和 sendmsg 函数使用,这里两个函数的定义可参考文章《高级 I/O》,使用 msghdr 结构体中的 msg_control 和 msg_controllen 成员发送和接收辅助数据。

以下是辅助数据的各种用途:

辅助数据由一个或多个辅助数据对象构成,每个对象以一个结构体 cmsghdr 开头。结构体定义如下:

   /* 结构 cmsghdr*/
   struct cmsghdr {
       socklen_t cmsg_len;    /* data byte count, including header */
       int       cmsg_level;  /* originating protocol */
       int       cmsg_type;   /* protocol-specific type */
       /* followed by unsigned char cmsg_data[]; */
   };

而 msg_control 指向第一个辅助数据对象,辅助数据的总长度则有 msg_controllen 指定。每个对象开头都是一个描述该对象的 cmsghdr 结构。在 cmsg_type 成员和实际数据之间可以有填充字节,从数据结尾处到下一个辅助数据对象之前也可以有填充字节。如下图所示:

以下是处理辅助数据的宏定义:

 #include <sys/socket.h>

    /* 宏定义 */
   struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
   struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
   size_t CMSG_ALIGN(size_t length);
   size_t CMSG_SPACE(size_t length);
   size_t CMSG_LEN(size_t length);
   unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

   /* 结构 cmsghdr*/
   struct cmsghdr {
       socklen_t cmsg_len;    /* data byte count, including header */
       int       cmsg_level;  /* originating protocol */
       int       cmsg_type;   /* protocol-specific type */
       /* followed by unsigned char cmsg_data[]; */
   };
/* 这些宏定义用来创建和访问不是套接字负载部分的control控制信息(也称为辅助数据);
 * 这些控制信息可能包含所接收数据报的接口和各种很少使用的描述头等信息;
 */
 CMSG_FIRSTHDR(); /* 返回指向辅助数据缓冲区的第一个cmsghdr结构的指针,若无辅助数据则返回NULL; */

 CMSG_NXTHDR();/* 返回指向下一个cmsghdr结构的指针,若不再有辅助对象则返回NULL;*/

 CMSG_ALIGN();/* given a length, returns it including the required  align‐
       ment.  This is a constant expression. */

 CMSG_SPACE();/* 返回给定数据量的一个辅助数据对象的大小 */

 CMSG_DATA();/* 返回指向与cmsghdr结构关联的数据的第一个字节的指针; */

 CMSG_LEN();/* 返回给定数据量的存放到cmsg_len中的值;*/

CMSG_LEN 和 CMSG_SPACE 的区别在于,前者不计辅助数据对象中数据部分之后可能的填充字节,因而返回的是用于存放在 cmsg_len 成员中的值,后者是计上尾处可能的填充字节,因而返回的是为辅助数据对象动态分配空间的大小。上面这些宏定义的使用方式如下:

    struct msghdr msg;
    struct cmsghdr *cmptr;

    for(cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL; cmptr = CMSG_NXTHDR(&msg, cmptr))
    {
        if(cmptr->cmsg_level == ... &&
                cmptr->cmsg_type == ...)
        {
            u_char *ptr;

            ptr = CMSG_DATA(cmptr);
            /* process data pointer to by ptr */
        }
    }

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

下面利用辅助数据实现一个功能类似于 recvfrom 函数的编程,该函数返回三个值:返回 msg_flags 接收标志、接收数据报的目的 IP 地址、接收数据报的接口索引。其中头文件 un.h 只是定义了返回结构 目的 IP 地址和 接口索引。

#ifndef UN_H
#define UN_H
#include <netinet/in.h>

/* 自定义结构体,成员包含目的地址、接口索引 */
struct unp_in_pktinfo{
    struct in_addr  ipi_addr;   /* destination IPv4 address */
    int             ipi_ifindex;/* received interface index */
};
#endif
/* 函数功能:类似于recvfrom函数;
 * 返回值:
 * 1、返回msg_flags值;
 * 2、返回所接收数据报的目的地址(由IP_RECVDSTADDR套接字选项获取);
 * 3、返回所接收数据报接口的索引(由IP_RECVIF套接字选项获取);
 */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include "un.h"

ssize_t recvfrom_flag(int fd, void *ptr, size_t nbytes, int *flags,
        struct sockaddr * sa, socklen_t *salenptr, struct unp_in_pktinfo *pktp)
{
    struct msghdr msg;/* 需要调用recvmsg函数,所以必须定义该结构 */
    struct iovec iov[1];/* 非连续缓冲区,在这里只定义一个缓冲区 */
    ssize_t n;

#ifdef  HAVE_MSGHDR_MSG_CONTROL
    /* 若支持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_control控制信息,则直接初始化为0 */
    bzero(&msg, sizeof(msg));
#endif
    /* 赋值初始化msghdr结构 */
    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;

    if( (n = recvmsg(fd, &msg, *flags)) < 0)
        return(n);/* 出错返回 */
    /* 若recvmsg调用成功返回,则执行以下程序 */
    *salenptr = msg.msg_namelen;/* 值-结果参数必须返回 */
    if(pktp)
        /* 初始化unp_in_pktinfo结构,置地址为0.0.0.0,置接口索引为0 */
        bzero(pktp, sizeof(struct unp_in_pktinfo));/* 0.0.0.0, i/f = 0 */
#ifndef HAVE_MSGHDR_MSG_CONTROL
    /* 若不支持msg_control控制信息,则把待返回标志置为0,并返回 */
    *flags = 0;/* 值-结果参数返回 */
    return(n);
    /* 以下程序都是处理支持msg_control控制信息的部分 */
#else
    /* 返回标志信息 */
    *flags = msg.msg_flags;/* 值-结果参数返回 */
    if(msg.msg_controllen < sizeof(struct cmsghdr) ||
            (msg.msg_flags & MSG_CTRUNC) || pktp == NULL)
        return(n);
    /* 处理辅助数据 */
    for(cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL; cmptr = CMSG_NXTHDR(&msg, cmptr))
    {
#ifdef IP_RECVDSTADDR
        /* 处理IP_RECVDSTADDR,返回接收数据报的目的地址 */
        /* 其中IPPROTO_IP表示IPv4域 */
        if(cmptr->cmsg_level == IPPROTO_IP &&
                cmptr->cmsg_type == IP_RECVDSTADDR)
        {
            memcpy(&pktp->ipi_addr, CMSG_DATA(cmptr), sizeof(struct in_addr));
            continue;
        }
#endif
#ifdef IP_RECVIF
        /* 处理IP_RECVIF,返回接收数据报的接口索引 */
        if(cmptr->cmsg_level == IPPROTO_IP &&
                cmptr->cmsg_type == IP_RECVIF)
        {
            struct sockaddr_dl *sdl;/* 数据链路地址结构中包含有接口索引成员 */
            sdl = (struct sockaddr_dl *)CMSG_DATA(cmptr);
            pktp->ipi_ifindex = sdl->sdl_index;
            continue;
        }
#endif
        err_quit("unknown ancillary data");
    }
    return(n);
#endif
}

下面我们利用该函数来实现前面所记录《基本 UDP 套接字编程》的程序,其中服务器的处理程序变为以下实现:

#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <netinet/in.h>
#include "un.h"
#include <net/if.h>
#include <arpa/inet.h>

/* 限定数据报的大小为 20 字节 */
#undef	MAXLINE
#define	MAXLINE	20		/* to see datagram truncation */
extern ssize_t recvfrom_flag(int , void *, size_t, int *, struct sockaddr*, socklen_t *, struct unp_in_pktinfo *);
extern char * Sock_ntop(const struct sockaddr *, socklen_t );
void
dg_echo(int sockfd, struct sockaddr *pcliaddr, socklen_t clilen)
{
	int						flags;
	const int				on = 1;
	socklen_t				len;
	ssize_t					n;
	char					mesg[MAXLINE], str[INET6_ADDRSTRLEN],
							ifname[IFNAMSIZ];
	struct in_addr			in_zero;
	struct unp_in_pktinfo	pktinfo;
/* 若支持IP_RECVDSATDDR和IP_RECVIF套接字选项,则设置它们 */
#ifdef	IP_RECVDSTADDR
	if (setsockopt(sockfd, IPPROTO_IP, IP_RECVDSTADDR, &on, sizeof(on)) < 0)
		err_ret("setsockopt of IP_RECVDSTADDR");
#endif
#ifdef	IP_RECVIF
	if (setsockopt(sockfd, IPPROTO_IP, IP_RECVIF, &on, sizeof(on)) < 0)
		err_ret("setsockopt of IP_RECVIF");
#endif
	bzero(&in_zero, sizeof(struct in_addr));	/* all 0 IPv4 address */

	for ( ; ; ) {
		len = clilen;
		flags = 0;
        /* 读取来自套接字的数据报 */
		n = recvfrom_flag(sockfd, mesg, MAXLINE, &flags,
						   pcliaddr, &len, &pktinfo);
        /* 把所读取的字节数显示,最大字节数不能超过20字节,若超过,则发生截断情况;
         * 调用sock_ntop把源IP地址和端口号转换为表达格式并输出 */
		printf("%d-byte datagram from %s", n, Sock_ntop(pcliaddr, len));
        /* 若返回的IP地址不为0,则调用inet_ntop转化目的IP地址格式并输出 */
		if (memcmp(&pktinfo.ipi_addr, &in_zero, sizeof(in_zero)) != 0)
			printf(", to %s", inet_ntop(AF_INET, &pktinfo.ipi_addr,
										str, sizeof(str)));
        printf(", index = %d", pktinfo.ipi_ifindex);
        /* 若返回的接口索引不为0,则调用if_indextoname获取接口名字并显示 */
		if (pktinfo.ipi_ifindex > 0)
			printf(", recv i/f = %s",
				   if_indextoname(pktinfo.ipi_ifindex, ifname));
        printf(", flags = %d",flags);
        /* 以下是测试4个标志 */
#ifdef	MSG_TRUNC
		if (flags & MSG_TRUNC)	printf(" (datagram truncated)");
#endif
#ifdef	MSG_CTRUNC
		if (flags & MSG_CTRUNC)	printf(" (control info truncated)");
#endif
#ifdef	MSG_BCAST
		if (flags & MSG_BCAST)	printf(" (broadcast)");
#endif
#ifdef	MSG_MCAST
		if (flags & MSG_MCAST)	printf(" (multicast)");
#endif
		printf("\n");
        /* 回射文本字符串给客户端 */
		sendto(sockfd, mesg, n, 0, pcliaddr, len);
	}
}

使用 UDP 协议

UDP 协议的特性:支持广播和多播;不需要建立连接和拆除;

TCP 协议的特性:确认应答,超时重传,重复分组检测,排序乱序的分组;窗口式流量控制;慢启动和拥塞避免;

所以在以下情况必须使用 UDP 协议:广播或多播;简单的请求-应答应用程序;注意:对于海量数据传输应避免使用 UDP 协议。

由于 UDP 是不可靠传输协议,所以应用程序必须提供:超时重传、序列号确认应答机制;

并发 UDP 服务器

对于 TCP 并发服务器只需 fork 创建一个新子进程即可,然而对于 UDP 必须应对两种不同类型的服务器:

  1. UDP 简单服务器:即读入一个客户请求并发送一个确认应答后,与这个客户就不再相关了。这种情况下,读入客户请求的服务器可以 fork 创建一个新的子进程并让子进程去处理请求。
  2. UDP 复杂服务器:即 UDP 服务器与客户交换多个数据报。问题是客户知道服务器端口号只有服务器的一个众所周知端口。一个客户发送其请求的第一个数据报到达这个端口,但是服务器如何区分这是来自客户的同一个请求的后续数据报还是来自其他客户的请求数据报,该问题解决办法是让服务器为每个客户创建一个新的套接字,在其上bind 绑定一个临时端口,然后使用该套接字发送对客户的所有应答。

参考资料:

《Unix 网络编程》

时间: 2024-11-09 01:40:42

《网络编程》高级 UDP 套接字编程的相关文章

《网络编程》基本 UDP 套接字编程

在前面文章中介绍了<UDP 协议>和<套接字数据传输>.UDP 协议和 TCP 协议不同,它是一种面向无连接.不可靠的传输层协议.在基于 UDP 套接字编程中,数据传输可用函数 sendto 和 recvfrom.以下是基本 UDP 套接字编程过程: sendto 与 recvfrom 函数 这两个函数的功能类似于 write 和 read 函数,可用无连接的套接字编程.其定义如下: /* 函数功能:发送数据: * 返回值:若成功则返回已发送的字节数,若出错则返回-1: * 函数原

【Python网络编程】利用Python进行TCP、UDP套接字编程

之前实现了Java版本的TCP和UDP套接字编程的例子,于是决定结合Python的学习做一个Python版本的套接字编程实验. 流程如下: 1.一台客户机从其标准输入(键盘)读入一行字符,并通过其套接字将该行发送到服务器. 2.服务器从其连接套接字读取一行字符. 3.服务器将该行字符转换成大写. 4.服务器将修改后的字符串(行)通过连接套接字再发回给客户机. 5.客户机从其套接字中读取修改后的行,然后将该行在其标准输出(监视器)上打印出来. [TCP]服务器端代码: import socket

探索UDP套接字编程

UDP和TCP处于同一层网络模型中,也就是运输层,基于二者之上的应用有很多,常见的基于TCP的有HTTP.Telnet等,基于UDP有DNS.NFS.SNMP等.UDP是无连接,不可靠的数据协议服务,而TCP提供面向流.提供可靠数据服务.注意,UDP和TCP没有好坏之分,只是二者的适用场景不同罢了. 典型的UDP套接字编程模型是客户端不予服务端建立连接,而只是调用sendto函数来向服务端发送数据,其中必须要指定服务端的信息,包括IP和端口等:服务端不接收来自客户端的连接,而只是调用recvfr

【转】 探索UDP套接字编程

UDP和TCP处于同一层网络模型中,也就是运输层,基于二者之上的应用有很多,常见的基于TCP的有HTTP.Telnet等,基于UDP有DNS.NFS.SNMP等.UDP是无连接,不可靠的数据协议服务,而TCP提供面向流.提供可靠数据服务.注意,UDP和TCP没有好坏之分,只是二者的适用场景不同罢了. 典型的UDP套接字编程模型是客户端不予服务端建立连接,而只是调用sendto函数来向服务端发送数据,其中必须要指定服务端的信息,包括IP和端口等:服务端不接收来自客户端的连接,而只是调用recvfr

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 so

TCP和UDP套接字编程 (java实现)

在了解网络编程之前,我们先了解一下什么叫套接字 套接字即指同一台主机内应用层和运输层之间的接口 由于这个套接字是建立在网络上建立网络应用的可编程接口 因此也将套接字称为应用程序和网络之间的应用程序编程接口! 关于TCP和UDP这里就不作太多介绍了,我们知道TCP是面向连接的,UDP是不面向连接的,TCP可靠,UDP不可靠即可! 我们来设计一个应用来示范一下,流程: 客户机从键盘读取一行字符串,并通过套接字发送到服务器. 服务器从连接的套接字获取这行字符串,并对其进行修改(将小写转为大写),最后再

linux下的UDP套接字编程

一.相关过程以及知识请详见我的另一篇博客<winsock套接字编程>,这里不再累述. 二.相关代码: server.c:  1 /****************************************                                                                                                    2     > File Name:server.c   3     > A

linux网络环境下socket套接字编程(UDP文件传输)

今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中,如果我们使用TCP传输,会造成传输速度较慢的情况,所以我们在进行文件传输的过程中,最好要使用UDP传输. 在其中,我们需要写两个程序,一个客户端,一个服务端,在一个终端中,先运行服务端,在运行客户端,在服务端和客户端都输入IP地址和端口号,注意服务端和客户端的端口号要相同,然后选择功能,在linux

linux网络编程-(socket套接字编程UDP传输)

今天我们来介绍一下在linux网络环境下使用socket套接字实现两个进程下文件的上传,下载,和退出操作! 在socket套接字编程中,我们当然可以基于TCP的传输协议来进行传输,但是在文件的传输中,如果我们使用TCP传输,会造成传输速度较慢的情况,所以我们在进行文件传输的过程中,最好要使用UDP传输. 在其中,我们需要写两个程序,一个客户端,一个服务端,在一个终端中,先运行服务端,在运行客户端,在服务端和客户端都输入IP地址和端口号,注意服务端和客户端的端口号要相同,然后选择功能,在linux