概述
什么是选项的级别:
socket中可以设置的属性种类很多,比如socke的选项、传输层TCP/UDP的选项、数据链路层的选项。这些选项在不同的层级,这就是选项的级别。常用级别及含义如下:
级别 | 含义 |
SOL_SOCKET | 作用于套接字本身 |
IPPROTO_IP | 作用于IPv4协议 |
IPPROTO_TCP | 作用于流式套接字 |
IPPROTO_UDP | 作用于数据报套接字 |
SOL_SOCKET级别的常用选项:
IPPROTO_IP级别的常用选项:
相关函数
//获取套接字选项
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
//设置套接字选项
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
返回值:成功返回0,失败返回-1
sockfd:套接字的文件描述符
level:选项的级别
optname:要获取/设置哪一个选项
optval:获取/设置的选项存放在哪
optlen:选项存放的空间的大小
相关实验
1、保活连接实验
什么是保活链接:
保活链接指的是在TCP通信中,为了防止长时间不交互而连接被关闭的情况,服务器会向客户端发送一些数据,以确保连接正常。当到达设置的间隔时,会发出第一个保活数据,如果服务器没有收到客户端的回应,之后会继续发出。当到达设置的发送次数依旧没有得到回应,那么服务器就会中断与客户端的连接。
保活连接包括:是否开启、未通讯多久发送第一次保活数据、之后发送的间隔、发送几次未响应中断连接。与之相对应的选项如下:
含义 | 选项级别 | 选项 | 设置值 |
是否开启保活连接 | SOL_SOCKET | SO_KEEPALIVE | 1开启0关闭 |
未通讯多久发送第一次保活数据 | SOL_TCP | TCP_KEEPIDLE | 单位s 默认7200s(2h) |
之后发送的间隔 | SOL_TCP | TCP_KEEPINTVL | 单位s 默认75s |
发送几次未响应中断连接 | SOL_TCP | TCP_KEEPCNT | 默认9次 |
server.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <netinet/tcp.h>
#define BACKLOG 5 //最大接入客户端数量
int socket_init(char** argv);
void getKeepAlive(int fd);
void setKeepAlive(int fd);
int main(int argc ,char** argv){
int fd;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//初始化socket:TCP,IPv4,本地回环测试
fd = socket_init(argv);
//设置保活链接
getKeepAlive(fd);
setKeepAlive(fd);
getKeepAlive(fd);
//接受客户端链接
int newFd;
struct sockaddr_in newAddr;
socklen_t newAddrlen;
char buf[100] = {0};
fd_set readfds,readfdsTmp;
int i,nfds;
//1.清空可读可写集合,并将服务器的fd添加进可读集合
FD_ZERO(&readfds);
FD_ZERO(&readfdsTmp);
FD_SET(fd,&readfds);
nfds = fd + BACKLOG + 1;
while(1){
readfdsTmp = readfds;
//2.以阻塞方式监听全部文件描述符,如果有文件描述符可以写入,则返回
if(select(nfds,&readfdsTmp,NULL,NULL,NULL) == -1){
perror("select");
exit(-1);
}
//3.根据监听到的文件描述符进行相应的操作
if(FD_ISSET(fd,&readfdsTmp)){//监听到了服务器的fd,代表有新的客户端接入
if((newFd = accept(fd,(struct sockaddr*)&newAddr,&newAddrlen)) < 0){
perror("accept");
return -1;
}
printf("[%s,%d]connect\n",inet_ntoa(newAddr.sin_addr),ntohs(newAddr.sin_port));
printf("newFd = %d\n",newFd);
FD_SET(newFd,&readfds);//将新的socket加入监听列表
}
else{
//printf("Debug:fd = %d\n",fd);
for(i=fd+1;i<nfds;i++){//监听到了客户端的fd,处理相应客户端信息,这里的难点是客户端fd的范围如何确定
//printf("Debug:i=%d\n",i);
if(FD_ISSET(i,&readfdsTmp)){
if(read(i,buf,sizeof(buf)) <= 0){
close(i);
FD_CLR(i,&readfds);
printf("fd=%d closed\n",i);
}else{
if(getpeername(i,(struct sockaddr*)&newAddr,&newAddrlen) == -1){//与客户端链接存在问题
perror("getpeername");
}else{
printf("[%s,%d]data:%s",inet_ntoa(newAddr.sin_addr),ntohs(newAddr.sin_port),buf);
write(i,"server\n",strlen("server\n"));
memset(buf,0,sizeof(buf));
}
}
}
}
}
}
close(fd);
return 0;
}
void setKeepAlive(int fd){
int optval;
optval = 1;
if(setsockopt(fd,SOL_SOCKET,SO_KEEPALIVE,&optval,sizeof(int)) == -1){
perror("SO_KEEPALIVE");
}
optval = 5;
if(setsockopt(fd,SOL_TCP,TCP_KEEPIDLE,&optval,sizeof(socklen_t))){
perror("TCP_KEEPIDLE");
}
optval = 2;
if(setsockopt(fd,SOL_TCP,TCP_KEEPINTVL,&optval,sizeof(socklen_t))){
perror("TCP_KEEPINTVL");
}
optval = 3;
if(setsockopt(fd,SOL_TCP,TCP_KEEPCNT,&optval,sizeof(socklen_t))){
perror("TCP_KEEPCNT");
}
}
void getKeepAlive(int fd){
int optval;
socklen_t optlen;
if(getsockopt(fd,SOL_SOCKET,SO_KEEPALIVE,&optval,&optlen) == -1){
perror("get SO_KEEPALIVE");
}else{
printf("是否开启保活链接 = %d\n",optval);
}
if(getsockopt(fd,SOL_TCP,TCP_KEEPIDLE,&optval,&optlen) == -1){
perror("get TCP_KEEPIDLE");
}else{
printf("未通讯多久发送第一次保活数据 = %ds\n",optval);
}
if(getsockopt(fd,SOL_TCP,TCP_KEEPINTVL,&optval,&optlen) == -1){
perror("get TCP_KEEPINTVL");
}else{
printf("之后发送的间隔 = %d\n",optval);
}
if(getsockopt(fd,SOL_TCP,TCP_KEEPCNT,&optval,&optlen) == -1){
perror("get TCP_KEEPCNT");
}else{
printf("发送几次未响应中断连接 = %d\n",optval);
}
}
int socket_init(char** argv){
int fd;
struct sockaddr_in addr;
//1.创建socket
if((fd=socket(AF_INET,SOCK_STREAM,0))<0){//IPv4,TCP协议
perror("socket");
exit(-1);
}
//2.绑定IP、端口号
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址:0表示在本网络上的本主机,即:自己
if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("bind");
exit(-1);
}
//3.监听socket
if(listen(fd,BACKLOG) == -1){ //允许最多接入5个客户端
perror("listen");
exit(-1);
}
return fd;
}
代码执行结果如下:
2、广播实验
什么是广播:
如果数据包发送方式只有一个接收方,这称为单播。如果同时发给网络中的所有主机,这就是广播。只有UDP能够实现广播。
广播地址特点:
一个网络内主机号全为1的IP地址为广播地址。发到广播地址的数据包会被所有的主机接收。
IP:255.255.255.255在所有网段中都代表广播地址。
Linux下查询广播地址:
ifconfig查询到的广播地址为192.168.129.255,掩码为255.255.255.0
这代表如果向该广播地址发送数据,那么IP为192.168.129.xxx的主机都可以接收到数据。
客户端广播服务器接收实验:
设置允许广播:
//设置可以允许广播
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
client.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_DGRAM,0))<0){//IPv4,UDP协议
perror("socket");
return -1;
}
//设置可以允许广播
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
//2.设置要发送到的服务器信息
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //服务器端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //服务器IP地址:在本网络上的本主机,即:自己
//3.数据交互
while(1){
sendto(fd,"cilent",strlen("cilent"),0,(struct sockaddr*)&addr,sizeof(addr));
sleep(1);
}
close(fd);
return 0;
}
代码运行结果如下:
3、组播实验
组播是什么:
组播是向多个网络的不同主机同时发送消息。组播的IP是D类地址(多播地址),每一个IP代表一个多播组。多播地址只能用于 "目的地址" ,不能用作 "源地址" 。
组播地址范围:224.0.0.0 ~ 239.255.255.255,一般选择239的网络号
组播如何通讯:
接收方(目的地址)先加入指定的多播组,发送方(源地址)向多播组发送数据,则加入该组的全部主机都可以收到数据。
服务器加入多播组客户端发数据实验:
注意事项:
- 进行该实验时需要保证本机已经联网,因为组播的数据是在网络上传输的。
- 在设置服务器IP时,不能设置127.0.0.1,因为这是本地回环地址,是不涉及真正的网络传输的。应该设置为0,这代表本网络上的本主机的IP地址,这样才能真正的在网络中通信。
加入多播组:
//设置多播组的结构体
struct ip_mreq {
struct in_addr imr_multiaddr; /*IP 组播组地址*/
struct in_addr imr_interface; /*本地接口的IP地址*/
};
//绑定组播IP
struct ip_mreq mreq;
memset(&mreq,0,sizeof(mreq));
if(!inet_aton("239.2.2.100",&mreq.imr_multiaddr)){//组播IP
perror("inet_aton");
return -1;
}
printf("组播ip = %s\n",inet_ntoa(mreq.imr_multiaddr));//setsockopt设置套接字属性,加入多播组
if(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0){
perror("setsockopt");
return -1;
}
server.c代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(int argc,char** argv){
int fd;
struct sockaddr_in addr;
//判断参数有效性
if(argc != 3){
printf("param err\n");
printf("%s<ip><port>\n",argv[0]);
return -1;
}
printf("ip = %s\n",argv[1]);
printf("port = %s\n",argv[2]);
//1.创建socket
if((fd=socket(AF_INET,SOCK_DGRAM,0))<0){//IPv4,UDP协议
perror("socket");
return -1;
}
//绑定组播IP
struct ip_mreq mreq;
memset(&mreq,0,sizeof(mreq));
//if(!inet_aton("0.0.0.0",&mreq.imr_interface)){//本地IP
// perror("inet_aton");
// return -1;
//}
if(!inet_aton("239.2.2.100",&mreq.imr_multiaddr)){//组播IP
perror("inet_aton");
return -1;
}
printf("组播ip = %s\n",inet_ntoa(mreq.imr_multiaddr));
if(setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0){
perror("setsockopt");
return -1;
}
//2.绑定IP、端口号
addr.sin_family = AF_INET; //IPv4
addr.sin_port = htons(atoi(argv[2])); //端口号,要转化为大端子节序
addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址:0表示在本网络上的本主机,即:自己
if(bind(fd,(struct sockaddr*)&addr,sizeof(struct sockaddr_in)) == -1){
perror("bind");
return -1;
}
//3.数据交互
char buf[100] = {0};
struct sockaddr_in src_addr;
socklen_t src_addrlen;
while(1){
memset(buf,0,sizeof(buf));
if(recvfrom(fd,buf,sizeof(buf)-1,0,(struct sockaddr*)&src_addr,&src_addrlen) > 0){
printf("client port = %d\n",ntohs(src_addr.sin_port));
printf("client ip = %s\n",inet_ntoa(src_addr.sin_addr));
printf("read:%s\n",buf);
}
}
close(fd);
return 0;
}
代码运行结果如下: