提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
UDP可分为单播,组播,广播 使用UDP协议进行信息的传输之前不需要建议连接。换句话说就是客户端向服务器发送信息,客户端只需要给出服务器的ip地址和端口号,然后将信息封装到一个待发送的报文中并且发送出去。至于服务器端是否存在,或者能否收到该报文,客户端根本不用管。
单播: 用于两个主机之间的端对端通信, 广播: 用于一个主机对整个局域网上所有主机上的数据通信,广播地址255.255.255.255 多播: 对一组特定的主机进行通信,而不是整个局域网上的所有主机
socket系统调用过程(UDP):
服务端:
1、socket()创建一个socket
2、bind()将一个socket绑定到一个地址
3、recvform() / sendto() 进行信息交互
4、close()关闭socket
客户端:
1、socket()创建一个socket
2、 recvfrom() / sendto() 进行信息交互
3、close()关闭socket
注意: udp客户端也可使用connect函数,但它仅仅用于表示确定了另一方的地址,并没有其他含义。当使用connect,双方的收发recvform() / sendto() 可变为read()/write(),不需要服务端的ip信息。
一、使用的API
头文件: #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h>
1、socket()创建
int socket(int domain, int type, int protocol);
success:返回文件描述符 error: -1 错误代码存入errno中
domain: 一般把它赋为AF_INET
AF_INET:使用IPv4 TCP/IP协议
AF_INET6:使用IPv6 TCP/IP协议
AF_UNIX:创建只在本机内进行通信的套接字
type:
SOCK_STREAM:创建TCP流套接字
SOCK_DGRAM:创建UDP数据报套接字
SOCK_RAW:创建原始套接字
protocol:通常设置为0
2、bind()绑定地址
①、bind()
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
success:0 error: -1 错误代码存入errno中
sockfd:socket()返回的描述符
addr:指向sockaddr 的的指针,保存着本地套接字的地址(即端口和IP地址)
信息。不过由于系统兼容性的问题,一般不使用这个结构,而使用另外一个
结构(struct sockaddr_in)来代替
addrlen:sockaddr结构的长度
对于IPV4使用以下结构体:
struct sockaddr_in{
unsigned short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
struct in_addr {
uint32_t s_addr;
};
eg: (struct sockaddr_in srv_addr;)
bzero(&srv_addr,sizeof(struct sockaddr_in));
srv_addr.sin_family = AF_INET;
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
srv_addr.sin_port = htons(8000);
INADDR_ANY:让服务器端计算机上的所有网卡的IP地址都可以作为服务器IP地址,也即监听外部客户端程序发送到服务器端所有网卡的网络请求。
如果需要使用特定的IP地址则需要使用inet_addr 和inet_ntoa完成字符串和in_addr结构体的互换
②、网络字节序转换:
整型与整型转换:
uint32_t htonl(uint32_t hostlong);
将一个32位数从主机字节顺序转换成网络字节顺序
uint16_t htons(uint16_t hostshort);
将一个无符号短整型数值转换为网络字节序
uint32_t ntohl(uint32_t netlong);
将网络字节序转为一个32位数主机字节用于打印显示
uint16_t ntohs(uint16_t netshort);
将网络字节序转为一个16位数主机字节用于打印显示
字符与整型转换:
int inet_aton(const char *cp, struct in_addr *inp);
将字符串型的ip地址(192.168.136.18)转成网络字节序的sockaddr_in.in_addr
in_addr_t inet_addr(const char *cp);
将字符串型ip转成网络字节序 对255.255.255.255 无效
in_addr_t inet_network(const char *cp);
将字符串型ip转成主机字节序
char* inet_ntoa(struct in_addr inaddr);
将网络字节序转成字符型ip 用于打印
3、connect()请求连接服务端
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
success:0 error: -1 错误代码存入errno中
sockfd:客户端socket返回的描述符
addr: 想连接服务端的ip(ip地址和端口 类似bind时的addr)
addrlen: sizeof(struct sockaddr)
4、sendto() 发送 与 recvfrom()接收
可阻塞等待和通过fcntl设为非阻塞
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
success: 返回实际发送的数据大小 error:-1,错误原因存于errno
sockfd: 为socket返回的描述符
buf: 待发送的数据 和 接收的缓冲区
len: 待发送的字节数 和 接收的最大字节数(如接收超过只会收到max)
flags: 置为 0
dest_addr/src_addr:待要发送目的的sockaddr结构体/待接收时对方的sockaddr结构体
addrlen: 发送时: sockaddr结构体的长度,
接收时:调用前为sockaddr结构体的长度,调用后为实际收到sockaddr结构体的长度
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
5、close()关闭socket
int close(int fd);
success: 0 error: -1
服务端需要关闭 socket返回的 和 accept返回的
客户端 只要关闭socket返回的
6、setsockopt()设置socket属性
int setsockopt(int sockfd, int level, int optname, const void *optval,
socklen_t optlen);
success:0 error:-1
sockfd:socket文件描述符
level:
SOL_SOCKET 对应有不同选择
IPPROTO_IP 对应有不同选择
optname:根据level来选
SOL_SOCKET:
SO_REUSEADDR,打开或关闭地址复用地址功能
SO_REUSEPORT,打开或关闭地址复用端口功能
SO_BROADCAST,允许或禁止发送广播数据
SO_SNDBUF,设置发送缓冲区的大小
SO_RCVBUF,设置接收缓冲区的大小
IPPROTO_IP:
IP_ADD_MEMBERSHIP 加入多播组
IP_DROP_MEMBERSHIP 离开多播组
optval:指向存放选项值的缓冲区 打开1 关闭0
optlen:optval缓冲区长度
更多参数参考:https:
二、案例
1、单播
一边发送一边接收
①、单播server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(int argc,char *argv[])
{
int client_socket_addr_len;
char message[256];
int ret;
int portnumber;
int socket_descriptor;
struct sockaddr_in server_socket_addr, client_socket_addr;
if(2 != argc || 0 > (portnumber=atoi(argv[1])))
{
printf("Usage:%s port\n",argv[0]);
exit(1);
}
socket_descriptor=socket(AF_INET,SOCK_DGRAM,0);
memset(&server_socket_addr, 0, sizeof(server_socket_addr));
server_socket_addr.sin_family=AF_INET;
server_socket_addr.sin_addr.s_addr=inet_addr("192.168.117.128");
server_socket_addr.sin_port=htons(portnumber);
bind(socket_descriptor,(struct sockaddr *)&server_socket_addr,sizeof(server_socket_addr));
client_socket_addr_len=sizeof(server_socket_addr);
while(1)
{
printf("Waiting for data form client\n");
memset(message, 0, sizeof(message));
memset(&client_socket_addr, 0, sizeof(client_socket_addr));
ret = recvfrom(socket_descriptor,message,sizeof(message),0,(struct sockaddr *)&client_socket_addr,&client_socket_addr_len);
printf("Response from client:%s,%s,%d,ret=%d\n",message,
inet_ntoa(client_socket_addr.sin_addr),ntohs(client_socket_addr.sin_port),ret);
if(strncmp(message,"stop",4) == 0)
{
printf("client has told me to end the connection\n");
break;
}
memset(message, 0, sizeof(message));
printf("please input msg to client:");
scanf("%s",message);
ret = sendto(socket_descriptor,message, strlen(message)+1, 0, (struct sockaddr *)&client_socket_addr, client_socket_addr_len);
printf("sendto=%d\n",ret);
}
close(socket_descriptor);
return 0;
}
②、单播client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
int main(int argc,char *argv[])
{
int socket_descriptor;
int iter=0;
char buf[80];
struct sockaddr_in server_sin;
int len,ret,portnumber;
int addr_len=sizeof(server_sin);
if(2 != argc || 0 > (portnumber=atoi(argv[1])))
{
printf("Usage:%s port\n",argv[0]);
exit(1);
}
socket_descriptor=socket(AF_INET,SOCK_DGRAM,0);
memset(&server_sin, 0, sizeof(server_sin));
server_sin.sin_family=AF_INET;
server_sin.sin_addr.s_addr=inet_addr("192.168.117.128");
server_sin.sin_port=htons(portnumber);
for(iter=0;iter<=20;iter++)
{
memset(buf,0,sizeof(buf));
sprintf(buf,"data packet with ID %d",iter);
sleep(1);
ret = sendto(socket_descriptor,buf,sizeof(buf),0,(struct sockaddr *)&server_sin,sizeof(server_sin));
printf("send %s,ret=%d\r\n",buf,ret);
memset(buf,0,sizeof(buf));
memset(&server_sin, 0, sizeof(server_sin));
len = recvfrom(socket_descriptor, buf, sizeof(buf), 0,(struct sockaddr *)&server_sin, &addr_len);
printf("Receive from server: %s,%d, %d, %s,%d\r\n", buf, len,
server_sin.sin_addr.s_addr,inet_ntoa(server_sin.sin_addr),ntohs(server_sin.sin_port));
}
sprintf(buf,"stop\n");
sendto(socket_descriptor,buf,sizeof(buf),0,(struct sockaddr *)&server_sin,sizeof(server_sin));
close(socket_descriptor);
printf("client send,terminating\n");
return 0;
}
2、多播(组播)
发送组播消息的一端需要将数据发送到组播地址和固定的端口上,想要接收组播消息的终端需要绑定对应的固定端口然后加入到组播的群组,最终就可以实现数据的共享。
多播地址: 1、224.0.0.0~224.0.0.255为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用; 2、224.0.1.0~224.0.1.255是公用组播地址,可以用于Internet; 3、224.0.2.0~238.255.255.255为用户可用的组播地址(临时组地址),全网范围内有效; 4、239.0.0.0~239.255.255.255为本地管理组播地址,仅在特定的本地范围内有效。
设置组播属性:
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
success:0 error:-1
sockfd:用于 UDP 通信的套接字
level:套接字级别,设置组播属性需要将该参数指定为:IPPTOTO_IP
optname: 设置组播属性,:发送端:IP_MULTICAST_IF 接收端:IP_ADD_MEMBERSHIP
optval:
发送端:这个指针应该指向一个 struct ip_mreqn{} 类型的结构体地址
接收端:这个指针需要指向一个struct ip_mreqn{}类型的结构体地址
optlen:optval 指针指向的内存大小,即: sizeof(struct in_addr) /sizeof(struct ip_mreqn)
struct in_addr
{
in_addr_t s_addr;
};
struct ip_mreqn
{
struct in_addr imr_multiaddr;
struct in_addr imr_address;
int imr_ifindex;
};
#include <net/if.h>
unsigned int if_nametoindex(const char *ifname);
转至:https:
%E5%8F%91%E9%80%81%E7%AB%AF
①、组播发送端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
char buf[1024];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(8888);
inet_pton(AF_INET, "239.0.3.10", &cliaddr.sin_addr.s_addr);
int num = 0;
while(1)
{
sprintf(buf, "hello, client...%d\n", num++);
sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, len);
printf("发送的组播的数据: %s\n", buf);
sleep(1);
}
close(fd);
return 0;
}
②、组播接收端
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <net/if.h>
int main()
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
int reuse = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
perror("setsockopet error\n");
return -1;
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
struct ip_mreqn opt;
opt.imr_multiaddr.s_addr=inet_addr("239.0.3.10");
opt.imr_address.s_addr = htonl(INADDR_ANY);
opt.imr_ifindex = if_nametoindex("ens33");
setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &opt, sizeof(opt));
char buf[1024];
while(1)
{
memset(buf, 0, sizeof(buf));
recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("接收到的组播消息: %s\n", buf);
}
close(fd);
return 0;
}
注意事项:在组播数据的发送端,需要先设置组播属性,发送的数据是通过 sendto () 函数发送到某一个组播地址上,并且在程序中数据发送到了接收端的 9999 端口,因此接收端程序必须要绑定这个端口才能收到组播消息。
3、广播
想要发送广播的才需要setsocket为广播,在同一广播域下都能接收到。
想要进行广播通信,必须知道广播地址才可以。在IP地址中,如果最后一位数组是255,则该地址一定是广播地址,形如192.168.0.255,常见的广播地址有以下两种:
1、受限广播地址 以255.255.255.255组成的广播地址,我们在代码中可以使用htonl(INADDR_BROADCAST)获取该地址;在当前路由器均不转发此类广播地址。
2、子网广播地址 子网广播地址是一种常用的广播方式,它是指在一个具体的子网内进行广播. 比如192.168是网络ID,则192.168.1.255是子网:192.168.1的广播地址,
标准: 广播域:广播域是网络中能接收任一台主机发出的广播帧的所有主机集合。也就是说,如果广播域内的其中一台主机发出一个广播帧,同一广播域内所有的其它主机都可以收到该广播帧。计算:用主机的IP地址与子网掩码进行与运算即可知道该主机属于哪一个广播域 广播地址:要想相同广播域内的其它主机能收到的广播帧,还需要在发送广播包的时候指定当前所属广播域内的广播地址。广播地址的计算方法为子网掩码取反再与广播域进行或运算。 使用UDP进行跨网段广播:只要在相同广播域下,设置子网掩码来实现。 更详细:https://blog.csdn.net/localhostcom/article/details/104511879
设置为广播发送
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
返回值:函数调用成功返回 0,失败返回 - 1
sockfd:进行 UDP 通信的文件描述符
level: 套接字级别,需要设置为 SOL_SOCKET
optname:选项名,此处要设置 udp 的广播属性,该参数需要指定为:SO_BROADCAST
optval:如果是设置广播属性,该指针实际指向一块 int 类型的内存
该整型值为 0:关闭广播属性
该整形值为 1:打开广播属性
optlen:optval 指针指向的内存大小,即:sizeof(int)
eg:
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
转至: https:
%E8%AE%BE%E7%BD%AE%E5%B9%BF%E6%92%AD%E5%B1%9E%E6%80%A7
①、广播发送方
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
char buf[1024];
struct sockaddr_in cliaddr;
int len = sizeof(cliaddr);
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof(opt));
cliaddr.sin_family = AF_INET;
cliaddr.sin_port = htons(8888);
inet_pton(AF_INET, "192.168.117.255", &cliaddr.sin_addr.s_addr);
int num = 0;
while(1)
{
sprintf(buf, "hello, client...%d\n", num++);
sendto(fd, buf, strlen(buf)+1, 0, (struct sockaddr*)&cliaddr, len);
printf("发送的广播的数据: %s\n", buf);
sleep(1);
}
close(fd);
return 0;
}
②、广播接收方
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
int main()
{
struct sockaddr_in addr;
int reuse = 1;
char buf[1024];
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0)
{
perror("setsockopet error\n");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons(8888);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
while(1)
{
memset(buf, 0, sizeof(buf));
recvfrom(fd, buf, sizeof(buf), 0, NULL, NULL);
printf("接收到的广播消息: %s\n", buf);
}
close(fd);
return 0;
}
参考:https://subingwen.cn/linux/multicast/#4-1-%E5%8F%91%E9%80%81%E7%AB%AF
三、UDP丢包问题
UDP丢包: 发送方: 1、发送包太大 ①、发送的包比64K大会导致UDP协议sendto返回错误。 ②、发送的包比MTU大,UDP包在接收端容易丢包,可查看接收端的网卡统计。可考虑把包切成小包 2、发包速度太快: ①、接收端来不及接收导致接收端丢包。 ②、发送端网卡处理不过来 接收方: ①、接收缓冲区小于发送客户端的包的大小, ②、接收客户端recvfrom速度太慢,导致接收缓冲区满丢弃数据。 前一种问题,可以考虑增大接收缓冲区。后一种问题,可以考虑将接收操作和业务处理操作分离到不同的线程来处理。
|