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是以下零个或者多个标志的组合体,可以通过或操作连在一起。
- MSG_DONTROUTE:不要使用网关来发送封包,只发送到直接联网的主机。这个标志主要用于诊断或者路由程序。
- MSG_DONTWAIT:操作不会被阻塞。
- MSG_EOR:终止一个记录。
- MSG_MORE:调用者有更多的数据需要发送。
- MSG_NOSIGNAL:当另一端终止连接时,请求在基于流的错误套接字上不要发送SIGPIPE信号。
- MSG_OOB:发送out-of-band数据(需要优先处理的数据),同时现行协议必须支持这种操作。
- to:指向存放接收端地址的区域,可以为NULL。
- tolen:是结构体struct sockaddr的长度。
- 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被设为以下某个值。
- EACCES:对于UNIX域套接字,不允许对目标套接字文件进行写操作,或者路径前驱的一个目录节点不可搜索。
- EAGAIN,EWOULDBLOCK:套接字已标记为非阻塞,而发送操作被阻塞。
- EBADF:sock不是有效的描述词。
- ECONNRESET:连接被用户重置。
- EDESTADDRREQ:套接字不处于连接模式,没有指定对端地址。
- EFAULT:内存空间访问出错。
- EINTR:操作被信号中断。
- EINVAL:参数无效。
- EISCONN:基于连接的套接字已被连接上,同时指定接收对象。
- EMSGSIZE:消息太大。
- ENOMEM:内存不足。
- ENOTCONN:套接字尚未连接,目标没有给出。
- ENOTSOCK:sock索引的不是套接字。
- 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是以下一个或者多个标志的组合体,可以通过或操作连在一起。
- MSG_DONTWAIT:操作不会被阻塞,非阻塞,立即返回,不等待。
- 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 */
};
- MSG_PEEK:指示数据接收后,在接收队列中保留原数据,不将其删除,随后的读操作还可以接收相同的数据。
- MSG_TRUNC:返回封包的实际长度,即使它比所提供的缓冲区更长,只对packet套接字有效。
- MSG_WAITALL:要求阻塞操作,直到请求得到完整的满足。然而,如果捕捉到信号、错误或者连接断开发生,或者下次被接收的数据类型不同,仍会返回少于请求量的数据。
- MSG_EOR:指示记录的结束,返回的数据完成一个记录。
- MSG_TRUNC:指明报尾部数据已被丢弃,因为它比所提供的缓冲区需要更多的空间。
- MSG_CTRUNC:指明由于缓冲区空间不足,一些控制数据已被丢弃。
- MSG_OOB:指示接收到out-of-band数据(需要优先处理的数据)。
- MSG_ERRQUEUE:指示除了来自套接字错误队列的错误外,没有接收到其他数据。
- 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被设为以下的某个值。
- EAGAIN:套接字已标记为非阻塞,而接收操作被阻塞或者接收超时。
- EBADF:sock不是有效的描述词。
- ECONNREFUSE:远程主机阻绝网络连接。
- EFAULT:内存空间访问出错。
- EINTR:操作被信号中断。
- EINVAL:参数无效。
- ENOMEM:内存不足。
- ENOTCONN:与面向连接关联的套接字尚未被连接上。
- 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命令类似。