1. 套接字选项 (socket options)
每一个套接字 (socket)在不同的协议层次 (级别)上面有不同的行为属性 (选项)
我们可以设置 / 获取指定的套接字选项
getsockopt:获取套接字的选项
setsockopt:设置套接字的选项NAME getsockopt, setsockopt - get and set options on sockets SYNOPSIS #include <sys/types.h> #include <sys/socket.h> // 获取套接字的选项 int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen); // 设置套接字的选项,0:禁用 非0:使能(一般设置为1) int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); sockfd:你要设置或者获取属性的套接字描述符 level:你要设置或者获取属性位于什么级别 optname:选项名称 optval:指针,指向一块空间 get:指向的空间用来保存获取到的值 set:指向的空间用来保存需要设置的值,把指定的值设置到socket上面去 optlen: get:指针,调用前用来保存optval指向的空间的长度,调用后保存的是获取到的选项的实 际大小(防止内存越界) set:变量,用来指定设定选项的选项值的长度 返回值: 成功返回0, 失败返回-1,同时errno被设置
常用的几种套接字选项:
// Leve(级别):SOL_SOCKET // optname(选项名):SO_BROADCAST 说明:允许发送 / 接收广播数据报 // 数据类型:int // Leve(级别):SOL_SOCKET // optname(选项名):SO_RCVBUF 说明:接收缓冲区大小 // 数据类型:int // Leve(级别):SOL_SOCKET // optname(选项名):SO_SNDBUF 说明:发送缓冲区大小 // 数据类型:int // Leve(级别):SOL_SOCKET // optname(选项名):SO_REUSEADDR 说明:允许重用本地地址 // 数据类型:int // Leve(级别):SOL_SOCKET // optname(选项名):SO_REUSEPORT 说明:允许重用本地端口 // 数据类型:int // Leve(级别):IPPROTO_IP // optname(选项名):IP_ADD_MEMBERSHIP 说明:加入多播组 // 数据类型:ip_mreq{} // Leve(级别):IPPROTO_IP // optname(选项名):IP_DROP_MEMBERSHIP 说明:离开多播组 // 数据类型:ip_mreq{}
练习一:获取指定套接字的接收缓冲区大小,并重新设置该套接字的接收缓冲区大小
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> int main() { // 申请一个套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket failed"); return -1; } int buflen; socklen_t len = sizeof(buflen); // 获取接收缓冲区大小,每台机器接收缓冲区的大小都不一样 int r = getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void *)&buflen, &len); if (r == 0) { printf("buflen = %d\n", buflen); } // 设置接收缓冲区大小 // 注意:接收缓存区大小是你要设置的那个值的2倍,并且最小大小为2304 // buflen = 1000; // 2000 < 2304,设置为2304 // buflen = 2000; // 设置为4000 buflen = 6600; // 设置为13200 setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void *)&buflen, sizeof(buflen)); // 获取 len = sizeof(buflen); r = getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (void *)&buflen, &len); if (r == 0) { printf("buflen = %d\n", buflen); } close(sockfd); return 0; }
练习二:设置套接字选项,允许端口号重用
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <string.h> int main() { // 申请一个套接字 int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket failed"); return -1; } // 获取该套接字是否允许端口号重用 int on; socklen_t len = sizeof(on); int r = getsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&on, &len); if (r == 0) { printf("on = %d\n", on); } // 设置该套接字允许端口号重用 on = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&on, sizeof(on)); printf("on = %d\n", on); close(sockfd); return 0; }
2. 广播 (boardcast)
一对多的通信
a. 只有当传输层协议为UDP (SOCK_DGRAM)时,才支持广播功能
TCP是端对端的连接,通信前需要进行三次握手建立连接
广播是一对多的通信
b. 广播的地址问题,如果发送广播,网络地址是哪里呢?
广播是向局域网中所有的主机发送信息
广播的地址是将IP地址中所有的主机号设置为1 (子网掩码取反 再和 IP地址 按位或运算)
即: xxx.xxx.xxx.255
例子:
你的IP:192.168.1.103
哪些是主机号你还不知道?
netmask:255.255.255.0 (前面一定是连续的1,后面是连续的0)
这个IP的局域网的广播地址是:192.168.1.255
你的IP:192.168.1.103
哪些是主机号你还不知道?
netmask:255.255.0.0
这个IP的局域网的广播地址是:192.168.255.255
你的IP:192.168.1.103
哪些是主机号你还不知道?
netmask:255.255.128.0
这个IP的局域网的广播地址是:192.168.127.255
全网广播地址:255.255.255.255但是这个没有意义,一般的路由器都不会转发这个数据包
你把数据发送到广播地址上面,局域网内所有的用户都可以收到这个数据包
广播的编程思路 (流程):广播发送者:
1. 创建一个套接字 (SOCK_DGRAM)
socket
2. 设置套接字选项,使能发送广播
setsockopt
3. 向一个局域网发送广播数据
广播地址 + 端口号
sendto
4. 接收外界的消息 ----> 可以不要
recvfrom
5. 关闭套接字
close
shutdown
广播接收者:1. 创建一个套接字 (SOCK_DGRAM)
socket
2. 设置套接字选项,使能接收广播
setsockopt
3. 绑定地址
bind
绑定广播地址 (从哪一个局域网接收广播)
地址 + 端口号
4. 接收广播数据
recvfrom() ---> 没有数据会阻塞
5. 向发送者发送数据 ---> 可以不要
sendto
6. 关闭套接字
close
shutdown
代码实现:boardcast_send.c
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { if (argc != 3) { printf("argc num error\n"); return -1; } // 1. 创建一个套接字 (SOCK_DGRAM) int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("boardcast_send socket failed"); return -1; } // 2. 设置套接字选项,使能发送广播 int enable= 1; setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (void *)&enable, sizeof(enable)); // 需要一个网络地址,指定发给哪一个接收者 struct sockaddr_in recvAddr; memset(&recvAddr, 0, sizeof(recvAddr)); recvAddr.sin_family = AF_INET; // 协议族 recvAddr.sin_port = htons(atoi(argv[2])); // 端口号 recvAddr.sin_addr.s_addr = inet_addr(argv[1]); // IPV4地址 // 当接收者发信息给我时,保存该接收者的网络地址 struct sockaddr_in recv_addr; socklen_t addrlen = sizeof(recv_addr); while (1) { // 3. 向一个局域网发送广播数据 char buf[250] = {0}; scanf("%s", buf); int w = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&recvAddr, sizeof(recvAddr)); printf("w = %d\n", w); if (buf[0] == '#') { // 退出条件 break; } // 4. 可以接收外界的消息 ----> 可以不要 // char buff[250] = {0}; // int r = recvfrom(sockfd, buff, 250, 0, (struct sockaddr*)&recv_addr, &addrlen); // if (r > 0) { // printf("r = %d, buff = %s\n", r, buff); // printf("接收者 IP:%s, port:%d\n", inet_ntoa(recv_addr.sin_addr), ntohs(recv_addr.sin_port)); // } } // 5. 关闭套接字 close(sockfd); return 0; }
boardcast_recv.c
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int main(int argc, char *argv[]) { if (argc != 3) { printf("argc num error\n"); return -1; } // 1. 创建一个套接字 (SOCK_DGRAM) int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("boardcast_send socket failed"); return -1; } // 2. 设置套接字选项,使能接收广播 int enable = 1; setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, (void *)&enable, sizeof(enable)); // 3. 绑定地址 struct sockaddr_in recvAddr; memset(&recvAddr, 0, sizeof(recvAddr)); recvAddr.sin_family = AF_INET; // 协议族 recvAddr.sin_port = htons(atoi(argv[2])); // 端口号 recvAddr.sin_addr.s_addr = inet_addr(argv[1]); // IPV4地址 int res = bind(sockfd, (struct sockaddr *)&recvAddr, sizeof(recvAddr)); if (res == -1) { perror("boardcast_send bind failed"); close(sockfd); return -1; } // 当发送者发信息给我时,保存该发送者的网络地址 struct sockaddr_in send_addr; socklen_t addrlen = sizeof(send_addr); while (1) { // 4. 接收广播数据 char buf[250] = {0}; int r = recvfrom(sockfd, buf, 250, 0, (struct sockaddr*)&send_addr, &addrlen); if (r > 0) { printf("r = %d, buf = %s\n", r, buf); printf("发送者 IP:%s,port:%d\n", inet_ntoa(send_addr.sin_addr), ntohs(send_addr.sin_port)); } // 5. 向发送者发送数据 ---> 可以不要 } // 6. 关闭套接字 close(sockfd); return 0; }
3. 多播 / 组播 (multicast)
单播用于两个主机之间端对端的通信,广播用于一个主机对整个局域网上所有的主机进行数据通信
单播和广播是两个极端有时候,我们需要对一组特定的主机进行通信
=====>多播
a. 多播也只有传输层协议为 UDP 时,才支持多播 (组播)功能
b. 多播地址 ---> IPV4的D类地址
224.0.0.0 ~ 239.255.255.255 之间的 IP地址
局部多播地址:224.0.0.0 ~ 224.0.0.255
===> (仅用于局域网通信),路由器不会转发此地址之间的数据包
预留的多播地址:224.0.1.0 ~ 238.255.255.255
管理权限多播地址:239.0.0.0 ~ 239.255.255.255 类似于私有IP
相对于单播和广播,多播是属于一种折中的方式,只有某些加入多播组的主机才能收到数据
多播的编程思路:多播发送者
1. 创建一个套接字 (SOCK_DGRAM)
2. 发送消息到一个多播组地址
IP + 端口号
sendto
3. 接收消息 ---> 可以不要
4. 关闭套接字 close
多播接收者
1. 创建一个套接字 (SOCK_DGRAM)
2. 加入一个多播组 (发送者发送的组号)
3. 绑定地址 IP + 端口号
IP:多播组的地址
端口号
4. 接收多播组的信息
recvfrom
5. 发送信息到多播组 -----> 可以不要
6. 关闭套接字
如何加入多播组示例代码:struct ip_mreq { // 多播组的IP地址(D类地址)---> 类似于QQ群号 struct in_addr imr_multiaddr; // 接口地址,多播数据包实际上经过哪一个网卡发送/接收 struct in_addr imr_address } struct in_addr { uint32_t s_addr; /* 32位IP地址 */ }; // 例子:把本机IP加入多播组 224.0.0.1 struct ip_mreq mreq; memset(&mreq, 0, sizeof(mreq)); mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1"); // 多播组的IP地址mreq.imr_address.s_addr = inet_addr("172.4.1.5"); // 本地的IP地址 或者 mreq.imr_interface.s_addr = htonl(INADDR_ANY); // INADDR_ANY 让内核帮你随便选择一个网卡 // 使用setsockopt,加入指定的多播组中 setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&mreq,sizeof(mreq));
注意:要支持多播,需要设置路由表,让数据包从正确的网卡出去,而不是从默认网卡出去
首先 ifconfig 查看本机 IP 和网卡名字 (第一行第一个,如:ens33,eth0,eth1 ......)
sudo route add -net 224.0.0.0 netmask 240.0.0.0 ens33 // 加入路由表sudo route add default gw 172.4.1.1 dev ens33 // 设置默认网关
查看内核的 IP 路由标
route -n
代码实现:
multicast_send.c#include <stdio.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> // 多播组号的IP和端口号通过命令行参数传递 int main(int argc, char *argv[]) { if (argc != 3) { printf("argc num error\n"); return -1; } // 1.创建一个套接字(SOCK_DGRAM) int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socked failed"); return -1; } // 2.发送信息到一个多播组地址 // IP+端口号 // sendto struct sockaddr_in recvAddr; memset(&recvAddr, 0, sizeof(struct sockaddr_in)); recvAddr.sin_family = AF_INET; recvAddr.sin_port = htons(atoi(argv[2])); recvAddr.sin_addr.s_addr = inet_addr(argv[1]); // 当接收者发信息给我时,保存该接收者的网络地址 struct sockaddr_in recv_addr; socklen_t addrlen = sizeof(recv_addr); while (1) { char buf[250] = {0}; scanf("%s", buf); int w = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&recvAddr, sizeof(recvAddr)); printf("w = %d\n", w); if (buf[0] == '#') { // 退出条件 break; } // 3. 接收消息 ---> 可以不要 char buff[250] = {0}; int r = recvfrom(sockfd, buff, 250, 0, (struct sockaddr *)&recv_addr, &addrlen); printf("发送者 IP:%s, port:%d\n", inet_ntoa(recv_addr.sin_addr), ntohs(recv_addr.sin_port)); printf("r = %d, buff = %s\n", r, buff); } // 4.关闭套接字 close(sockfd); return 0; }
multicast_recv.c
#include <stdio.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <arpa/inet.h> #include <stdlib.h> #include <unistd.h> // 多播组号的IP和端口号通过命令行参数传递 int main(int argc, char *argv[]) { if (argc != 3) { printf("arg num error\n"); return -1; } // 1.创建一个套接字(SOCK_DGRAM) int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if (sockfd == -1) { perror("socket failed\n"); return -1; } // 2.加入一个多播组(发送者发送的组号) struct ip_mreq mreq; memset(&mreq, 0, sizeof(mreq)); mreq.imr_multiaddr.s_addr = inet_addr(argv[1]); // 多播组的IP地址 mreq.imr_interface.s_addr = htonl(INADDR_ANY); // 让内核随便选择一个网卡 // mreq.imr_address.s_addr = inet_addr("172.4.1.5"); // 本地的IP地址 int ret = setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void *)&mreq, sizeof(mreq)); if (ret == -1) { perror("setsockopt failed"); close(sockfd); return -1; } // 3.绑定地址 IP + 端口号 struct sockaddr_in local; memset(&local, 0, sizeof(local)); local.sin_family = AF_INET; local.sin_port = htons(atoi(argv[2])); // 端口号 local.sin_addr.s_addr = inet_addr(argv[1]); // 多播组地址 ret = bind(sockfd, (struct sockaddr *)&local, sizeof(local)); if (ret == -1) { perror("bind failed"); close(sockfd); return -1; } // 保存发送者网络地址 struct sockaddr_in send_addr; socklen_t addrlen = sizeof(send_addr); while (1) { // 4.接收多播组的信息 char buf[250] = {0}; int r = recvfrom(sockfd, buf, 250, 0, (struct sockaddr *)&send_addr, &addrlen); printf("发送者 IP:%s, port:%d\n", inet_ntoa(send_addr.sin_addr), ntohs(send_addr.sin_port)); printf("r = %d, buf:%s\n",r, buf); // 私发消息给发送者 int w = sendto(sockfd, "received", strlen("received"), 0, (struct sockaddr *)&send_addr, sizeof(send_addr)); printf("w = %d\n", w); sleep(3); // 5. 发送信息到多播组 ---> 可以不要 w = sendto(sockfd, "okokok", strlen("okokok"), 0, (struct sockaddr *)&local, sizeof(local)); printf("w = %d\n", w); } // 关闭套接字 close(sockfd); return 0; }