当前位置: 首页 > article >正文

Linux C/C++编程-UDP套接字编程示例

【图书推荐】《Linux C与C++一线开发实践(第2版)》_linux c与c++一线开发实践pdf-CSDN博客


《Linux C与C++一线开发实践(第2版)(Linux技术丛书)》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

LinuxC\C++编程技术_夏天又到了的博客-CSDN博客

UDP套接字创建函数socket()、地址绑定函数bind()与TCP套接字的相同,具体用法可参考第13章。本节仅介绍消息发送函数sendto()和sendmsg,以及消息接收函数recofrom和recvmsg。

14.1  消息发送函数sendto和sendmsg

发送消息时,send只可用于基于连接的套接字,send和write唯一的不同是标志的存在,当标志为0时,send等同于write。sendto和sendmsg既可用于无连接的套接字,也可用于基于连接的套接字,但这两个函数一般不用在连接套接字上。

这两个函数用来发送消息,函数原型如下:

#include <sys/types.h>

#include <sys/socket.h>

ssize_t sendto(int sock, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

ssize_t sendmsg(int sock, const struct msghdr *msg, int flags);

其中,参数sock表示将要发送数据的套接字;buf指向将要发送数据的缓冲区;len是buf所指缓冲区的长度,以字节为单位;flags是以下零个或者多个标志的组合体,可以通过或操作连在一起。

  1. MSG_DONTROUTE:不要使用网关来发送封包,只发送到直接联网的主机。这个标志主要用于诊断或者路由程序。
  2. MSG_DONTWAIT:操作不会被阻塞。
  3. MSG_EOR:终止一个记录。
  4. MSG_MORE:调用者有更多的数据需要发送。
  5. MSG_NOSIGNAL:当另一端终止连接时,请求在基于流的错误套接字上不要发送SIGPIPE信号。
  6. MSG_OOB:发送out-of-band数据(需要优先处理的数据),同时现行协议必须支持这种操作。
  7. to:指向存放接收端地址的区域,可以为NULL。
  8. tolen:是结构体struct sockaddr的长度。
  9. msg:指向存放发送消息头的内存缓冲,定义如下:
struct msghdr {

    void *msg_name;    

    socklen_t msg_namelen; 

    struct iovec *msg_iov;     

    size_t msg_iovlen;  

    void *msg_control; 

    socklen_t msg_controllen;

    int msg_flags;   

};

如果函数执行成功,则返回已发送的字节数,失败则返回-1,错误码errno被设为以下某个值。

  1. EACCES:对于UNIX域套接字,不允许对目标套接字文件进行写操作,或者路径前驱的一个目录节点不可搜索。
  2. EAGAIN,EWOULDBLOCK:套接字已标记为非阻塞,而发送操作被阻塞。
  3. EBADF:sock不是有效的描述词。
  4. ECONNRESET:连接被用户重置。
  5. EDESTADDRREQ:套接字不处于连接模式,没有指定对端地址。
  6. EFAULT:内存空间访问出错。
  7. EINTR:操作被信号中断。
  8. EINVAL:参数无效。
  9. EISCONN:基于连接的套接字已被连接上,同时指定接收对象。
  10. EMSGSIZE:消息太大。
  11. ENOMEM:内存不足。
  12. ENOTCONN:套接字尚未连接,目标没有给出。
  13. ENOTSOCK:sock索引的不是套接字。
  14. EPIPE:本地连接已关闭。

14.2 消息接收函数recvfrom和recvmsg

这两个函数从套接字上接收一个消息。recvfrom和recvmsg可以同时应用于面向连接的和无连接的套接字。recv一般只用在面向连接的套接字,几乎等同于recvfrom,只需将recvfrom的第5个参数设置为NULL。按照习惯,recvfrom和recvmsg一般用于无连接套接字。

如果消息太大,无法完整存放在所提供的缓冲区中,则根据不同的套接字,多余的字节被丢弃。假如套接字上没有消息可以读取,除非套接字已被设置为非阻塞模式,否则这两个函数将会阻塞一直等到消息到来。

这两个函数声明如下:

#include <sys/types.h>

#include <sys/socket.h>

ssize_t  recvfrom(int sock, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

ssize_t  recvmsg(int sock, struct msghdr *msg, int flags);

其中,参数sock是将要接收数据的套接字;buf为存放消息的缓冲区;len为buf所指缓冲区的大小;flags是以下一个或者多个标志的组合体,可以通过或操作连在一起。

  1. MSG_DONTWAIT:操作不会被阻塞,非阻塞,立即返回,不等待。
  2. MSG_ERRQUEUE:指示应该从套接字的错误队列上接收错误值,依据不同的协议,错误值以某种辅助性消息的方式传递进来,使用者应该提供足够大的缓冲区。错误以sock_extended_err结构形态被使用,定义如下:
#define SO_EE_ORIGIN_NONE    0
#define SO_EE_ORIGIN_LOCAL   1
#define SO_EE_ORIGIN_ICMP    2
#define SO_EE_ORIGIN_ICMP6   3
struct sock_extended_err
{
    u_int32_t ee_errno;   /* error number */
    u_int8_t ee_origin; /* where the error originated */
    u_int8_t ee_type;    /* type */
    u_int8_t ee_code;    /* code */
    u_int8_t ee_pad;
    u_int32_t ee_info;    /* additional information */
    u_int32_t ee_data;    /* other data */
    /* More data may follow */
};
  1. MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
  2. MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长,只对packet套接字有效。
  3. MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号、错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
  4. MSG_EOR:指示记录的结束,返回的数据完成一个记录。
  5. MSG_TRUNC:指明报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
  6. MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
  7. MSG_OOB:指示接收到out-of-band数据(需要优先处理的数据)。
  8. MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其他数据。
  9. From:为指向存放对端地址的缓冲区指针,如果为NULL,不储存对端地址;fromlen是一个输入输出参数,作为输入参数,指向存放表示from所指缓冲区的最大长度,作为输出参数,指向存放表示from所指缓冲区的实际长度;msg指向存放进入消息头的内存缓冲,结构形态如下:
struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    socklen_t     msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

如果函数成功执行,则返回接收到的字节数;如果另一端已关闭,则返回0;如果函数执行失败,则返回-1,errno被设为以下的某个值。

  1. EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时。
  2. EBADF:sock不是有效的描述词。
  3. ECONNREFUSE:远程主机阻绝网络连接。
  4. EFAULT:内存空间访问出错。
  5. EINTR:操作被信号中断。
  6. EINVAL:参数无效。
  7. ENOMEM:内存不足。
  8. ENOTCONN:与面向连接关联的套接字尚未被连接上。
  9. ENOTSOCK:sock索引的不是套接字。

了解了基本的UDP收发函数后,接着进入实战环节。

【例14.1】获取网卡IP地址信息(UDP套接字版)

(1)打开Visual Studio Code,新建文本文件,输入代码如下:

#include <string.h>  
#include <sys/socket.h>  
#include <sys/ioctl.h>  
#include <net/if.h>  
#include <stdio.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
int main()  
{  
    int inet_sock;  
    struct ifreq ifr;  // 定义网口请求结构体
    inet_sock = socket(AF_INET, SOCK_DGRAM, 0);  
  
    strcpy(ifr.ifr_name, "eno16777736");  
    // SIOCGIFADDR标志代表获取接口地址  
    if (ioctl(inet_sock, SIOCGIFADDR, &ifr) <  0)  
        perror("ioctl");  
    printf("%s\n", inet_ntoa(((struct sockaddr_in*)&(ifr.ifr_addr))->sin_addr));  
    return 0;  
}

在代码中,首先创建一个UDP 套接字,然后把本机的一个网卡名字“eno16777736”赋值给ifr.ifr_name,接着调用ioctl函数获取SIOCGIFADDR信息,即网络接口的IP地址信息。

(2)保存代码为test.cpp,上传到Linux,然后编译并运行:

# g++ test.cpp -o test

# ./test

1.1.1.10

该IP地址是笔者Ubuntu的eno16777736网卡的IP地址。

【例14.2】服务器和客户端通信

(1)先创建服务器端的程序。打开Visual Studio Code,新建文本文件,输入代码如下:

#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <strings.h>  
#include <unistd.h>  
#include <errno.h>  
#include <sys/stat.h>  
#include <dirent.h>  
#include <sys/mman.h>  
#include <sys/wait.h>  
#include <signal.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <sys/msg.h>  
#include <sys/sem.h>  
#include <pthread.h>  
#include <semaphore.h>  
#include <poll.h>  
#include <sys/epoll.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <netinet/in.h>  

char rbuf[50];  
  
int main()  
{  
    int sockfd;  
    int size;  
    int ret;  
    int on =1;  
    struct sockaddr_in saddr;  
    struct sockaddr_in raddr;  
  
    // 设置地址信息,ip信息  
    size = sizeof(struct sockaddr_in);  
    bzero(&saddr,size);  
    saddr.sin_family = AF_INET;  
    saddr.sin_port = htons(8888);  
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);  
  
    // 创建UDP的套接字
    sockfd = socket(AF_INET,SOCK_DGRAM,0);  
    if(sockfd<0)  
    {  
        perror("socket failed");  
        return -1;  
    }  
  
    // 设置端口复用  
    setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));   
  
    // 绑定地址信息,IP信息  
    ret = bind(sockfd,(struct sockaddr*)&saddr,sizeof(struct sockaddr));  
    if(ret<0)  
    {  
        perror("sbind failed");  
        return -1;  
    }  
  
    socklen_t  val = sizeof(struct sockaddr);  
    // 循环接收客户端发来的消息  
    while(1)  
    {  
        puts("waiting data");  
        ret=recvfrom(sockfd,rbuf,50,0,(struct sockaddr*)&raddr,&val);  
        if(ret <0)  
        {  
            perror("recvfrom failed");  
        }  
  
        printf("the data :%s\n",rbuf);  
        bzero(rbuf,50);  
    }  
    // 关闭udp套接字,这里不可达  
    close(sockfd);  
    return 0;  
}  

代码很简单,通过一个while循环等待客户端发来的消息。没有数据过来时,就在recvfrom函数上阻塞着。

(2)保存代码为test.cpp,上传到Linux,然后编译并运行:

# g++ test.cpp -o test

# ./test

waiting data

(3)下面创建客户端代码。打开Visual Studio Code,新建文本文件,输入代码如下:

#include <sys/types.h>  
#include <sys/stat.h>  
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <strings.h>  
#include <unistd.h>  
#include <errno.h>  
#include <sys/stat.h>  
#include <dirent.h>  
#include <sys/mman.h>  
#include <sys/wait.h>  
#include <signal.h>  
#include <sys/ipc.h>  
#include <sys/shm.h>  
#include <sys/msg.h>  
#include <sys/sem.h>  
#include <pthread.h>  
#include <semaphore.h>  
#include <poll.h>  
#include <sys/epoll.h>  
#include <sys/socket.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <netinet/in.h>  

char wbuf[50];  
  
int main()  
{  
    int sockfd;  
    int size,on = 1;  
    struct sockaddr_in saddr;  
    int ret;  
  
    size = sizeof(struct sockaddr_in);  
    bzero(&saddr,size);  
  
    // 设置地址信息、ip信息  
    saddr.sin_family = AF_INET;  
    saddr.sin_port = htons(8888);  
    saddr.sin_addr.s_addr=inet_addr("172.16.2.6");	// 172.16.2.6为服务端所在的IP地址  
    
    sockfd= socket(AF_INET,SOCK_DGRAM,0);  		// 创建UDP的套接字
    if(sockfd<0)  
    {  
        perror("failed socket");  
        return -1;  
    }  
    // 设置端口复用  
    setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on));  
  
    // 循环发送信息给服务端  
    while(1)  
    {  
        puts("please enter data:");  
        scanf("%s",wbuf);  
        ret=sendto(sockfd,wbuf,50,0,(struct sockaddr*)&saddr,  
            sizeof(struct sockaddr));  
        if(ret<0)  
        {  
            perror("sendto failed");  
        }  
  
        bzero(wbuf,50);  
    }  
    close(sockfd);  
    return 0;  
}  

代码也很简单,使用一个while循环等待用户输入信息,输入后就把信息发送出去。

(4)保存代码为client.cpp,上传到Linux,重新开一个终端,然后编译并运行:

[root@localhost client]# g++ client.cpp -o client

[root@localhost client]# ./client

please enter data:

hi,server

please enter data:

发送消息“hi,server”后,服务端就变为:

waiting data

the data :hi,server

waiting data

【例14.3】实现简单的ifconfig查询功能

(1)打开Visual Studio Code,新建文本文件,输入代码如下:

#include <net/if.h>       /* for ifconf */  
#include <linux/sockios.h>    /* for net status mask */  
#include <netinet/in.h>       /* for sockaddr_in */  
#include <sys/socket.h>  
#include <sys/types.h>  
#include <sys/ioctl.h>  
#include <stdio.h>  
#include <unistd.h> // for close
#include <arpa/inet.h>
#include <string.h>
#define MAX_INTERFACE   (16)  
      
void port_status(unsigned int flags);  
      
/* set == 0: do clean , set == 1: do set! */  
int set_if_flags(char *pif_name, int sock, int status, int set)  
{  
	struct ifreq ifr;  
	int ret = 0;  
          
	strncpy(ifr.ifr_name, pif_name, strlen(pif_name) + 1);  
	ret = ioctl(sock, SIOCGIFFLAGS, &ifr);  
	if (ret)  
		return -1;  
	/* set or clean */    
	if (set)  
		ifr.ifr_flags |= status;  
	else   
		ifr.ifr_flags &= ~status;  
	/* set flags */  
	ret = ioctl(sock, SIOCSIFFLAGS, &ifr);  
	if (ret)  
		return -1;  
          
	return 0;  
}  
      
int get_if_info(int fd)  
{  
	struct ifreq buf[MAX_INTERFACE];      
	struct ifconf ifc;  
	int ret = 0;  
	int if_num = 0;  
 	ifc.ifc_len = sizeof(buf);  
	ifc.ifc_buf = (caddr_t) buf;  
          
	ret = ioctl(fd, SIOCGIFCONF, (char*)&ifc);  
	if (ret)  
	{  
		printf("get if config info failed");  
		return -1;  
	}  
	/* 网口总数ifc.ifc_len应该是一个输入参数 */      
	if_num = ifc.ifc_len / sizeof(struct ifreq);  
	printf("interface num is interface = %d\n", if_num);  
	while (if_num-- > 0)  
	{  
		printf("net device: %s\n", buf[if_num].ifr_name);     
		/* 获取第n个网口信息 */  
		ret = ioctl(fd, SIOCGIFFLAGS, (char*)&buf[if_num]);  
		if (ret)  
			continue;  
      
		/* 获取网口状态 */  
		port_status(buf[if_num].ifr_flags);  
              
		/* 获取当前网卡的IP地址 */  
		ret = ioctl(fd, SIOCGIFADDR, (char*)&buf[if_num]);  
		if (ret)  
			continue;  
		printf("IP address is: \n%s\n", inet_ntoa(((struct sockaddr_in *)(&buf[if_num].ifr_addr))->sin_addr));  
      
        /* 获取当前网卡的MAC */  
		ret = ioctl(fd, SIOCGIFHWADDR, (char*)&buf[if_num]);  
		if (ret)  
			continue;  
      
		printf("%02x:%02x:%02x:%02x:%02x:%02x\n\n",  
			(unsigned char)buf[if_num].ifr_hwaddr.sa_data[0],  
			(unsigned char)buf[if_num].ifr_hwaddr.sa_data[1],  
			(unsigned char)buf[if_num].ifr_hwaddr.sa_data[2],  
			(unsigned char)buf[if_num].ifr_hwaddr.sa_data[3],  
			(unsigned char)buf[if_num].ifr_hwaddr.sa_data[4],  
			(unsigned char)buf[if_num].ifr_hwaddr.sa_data[5]);  
	}  
}  
      
void port_status(unsigned int flags)  
{  
	if (flags & IFF_UP)    
	{  
		printf("is up\n");        
	}  
	if (flags & IFF_BROADCAST)     
	{  
		printf("is broadcast\n");     
	}  
	if (flags & IFF_LOOPBACK)      
	{  
		printf("is loop back\n");     
	}  
	if (flags & IFF_POINTOPOINT)   
	{  
		printf("is point to point\n");    
	}  
	if (flags & IFF_RUNNING)   
	{  
		printf("is running\n");   
	}  
	if (flags & IFF_PROMISC)   
	{  
		printf("is promisc\n");   
	}  
}  
      
int main()  
{  
	int fd;  
      
	fd = socket(AF_INET, SOCK_DGRAM, 0);  
	if (fd > 0)  
	{  
		get_if_info(fd);  
		close(fd);  
	}  
	return 0;  
}  

在代码中,首先创建UDP套接字,然后通过ioctl函数和内核进行交互,获得所需要的信息。ioctl函数在驱动编程中经常会碰到,它是用户态和内核态打交道的重要途径。结构体ifconf通常用来保存所有接口信息,其定义如下:

// if.h
struct ifconf
{
    int ifc_len; /* size of buffer */
    union
    {
        char *ifcu_buf; /* input from user->kernel*/
        struct ifreq *ifcu_req; /* return from kernel->user*/
    } ifc_ifcu;
};
#define ifc_buf ifc_ifcu.ifcu_buf /* buffer address */
#define ifc_req ifc_ifcu.ifcu_req /* array of structures */

(2)保存代码为test.cpp,上传到Linux,然后编译并运行:

# g++ test.cpp -o test
# ./test
interface num is interface = 6
net device: virbr0
is up
is broadcast
IP address is: 
192.168.122.1
00:00:00:00:00:00

net device: eno67109432
is up
is broadcast
is running
IP address is: 
1.1.1.11
00:0c:29:3d:94:31

net device: eno50332208
is up
is broadcast
is running
IP address is: 
192.168.0.2
00:0c:29:3d:94:27

net device: eno33554984
is up
is broadcast
is running
IP address is: 
10.1.0.2
00:0c:29:3d:94:1d

net device: eno16777736
is up
is broadcast
is running
is promisc
IP address is: 
1.1.1.10
00:0c:29:3d:94:13

net device: lo
is up
is loop back
is running
IP address is: 
127.0.0.1
00:00:00:00:00:00

可以看到,本机所有网卡信息都列举出来了,效果和ifconfig命令类似。


http://www.kler.cn/a/474304.html

相关文章:

  • Linux 文件的特殊权限—ACL项目练习
  • 生成模型:变分自编码器-VAE
  • Qt 界面外观
  • Linux下文件重定向
  • 基于YOLO11的无人机视角下羊群检测系统
  • 【Qt】QtConcurrent
  • 微软人工智能研究院推出OLA-VLM:一种以视觉为中心的方法来优化多模态大型语言模型
  • Redis Stream
  • Git指令
  • 一文读懂单片机的串口
  • 基于R语言的DICE模型实践技术应用;评估气候变化对经济的影响以及不同减排政策的经济成本和效益
  • Dify进阶:使用FastAPI和Selenium构建远程浏览器控制与录屏服务
  • 蓝桥杯python省赛备战day2--数组枚举--845数组中的最长山脉-枚举算法刷题学习笔记3--leetcode
  • NoSQL 基础知识总结
  • python【数据结构】
  • RabbitMQ中的配置文件advanced.config
  • 配置嵌入式服务器
  • Vue3 监听属性
  • [豆包MarCode AI 刷题] 算法题解 Java 青训入营考核 五题打卡第一天
  • git commit冲突,需输入提交信息合并提交
  • 服务器端QTcpSocket如何判断客户端是否在线
  • linux MySQL Percona Toolkit 使用指南
  • 【Pandas】pandas Series truediv
  • 系统架构设计师考点—数据库技术基础
  • pytest 参数介绍
  • CSS 变量:让你的样式更灵活、更易维护