LV045-网络属性
本文主要是网络编程——网络属性的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
一、网络属性
记得前边我们使用过setsockopt函数设置过允许地址重用,终于到了详细学习该函数的部分了。
1. getsockopt()函数
1.1 函数说明
在linux下可以使用man 2 getsockopt命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* 函数声明 */
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);【函数说明】该函数可以获取某个socket套接字的属性选项。
【函数参数】
sockfd:int类型,表示需要获取选项的套接字。level:int类型,选项所在的协议层。level 可取的值说明如下:
| SOL_SOCKET | 通用套接字选项 |
| IPPROTO_IP | IP选项 |
| IPPROTO_TCP | TCP选项 |
我们可以使用如下命令查看这些选项:
man 7 socket # 打开手册后查看 Socket options 部分
man 7 ip # 打开手册后查看 Socket options 部分
man 7 ipv6 # 打开手册后查看 Socket options 部分
man 7 tcp # 打开手册后查看 Socket options 部分常用选项如下:
| 选项名称 | 说明 | 数据类型 |
| SOL_SOCKET | ||
| SO_BROADCAST | 允许发送广播数据 | int |
| SO_DEBUG | 允许调试 | int |
| SO_DONTROUTE | 不查找路由 | int |
| SO_ERROR | 获得套接字错误 | int |
| SO_KEEPALIVE | 保持连接 | int |
| SO_LINGER | 延迟关闭连接 | struct linger |
| SO_OOBINLINE | 带外数据放入正常数据流 | int |
| SO_RCVBUF | 接收缓冲区大小 | int |
| SO_SNDBUF | 发送缓冲区大小 | int |
| SO_RCVLOWAT | 接收缓冲区下限 | int |
| SO_SNDLOWAT | 发送缓冲区下限 | int |
| SO_RCVTIMEO | 接收超时 | struct timeval |
| SO_SNDTIMEO | 发送超时 | struct timeval |
| SO_REUSEADDR | 允许重用本地地址和端口 | int |
| SO_TYPE | 获得套接字类型 | int |
| SO_BSDCOMPAT | 与BSD系统兼容 | int |
| IPPROTO_IP | ||
| IP_HDRINCL | 在数据包中包含IP首部 | int |
| IP_OPTINOS | IP首部选项 | int |
| IP_TOS | 服务类型 | |
| IP_TTL | 生存时间 | int |
| IPPRO_TCP | ||
| TCP_MAXSEG | TCP最大数据段的大小 | int |
| TCP_NODELAY | 不使用Nagle算法 | int |
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般使用格式如下:
/* 需要包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* 至少应该有的语句 */
int sendBuff;
socklen_t optLen = sizeof(int);
int socket_fd = socket_fd = socket(AF_INET, SOCK_STREAM, 0);
getsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, (int *)&sendBuff, &optLen);【注意事项】none
1.2 使用实例
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int main(int argc, char *argv[])
{
int sendBuff;
socklen_t optLen = sizeof(int);
int socket_fd = socket_fd = socket(AF_INET, SOCK_STREAM, 0);
getsockopt(socket_fd, SOL_SOCKET, SO_SNDBUF, (int *)&sendBuff, &optLen);
printf("sendBuff length: %dKB\n", sendBuff / 1024);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
sendBuff length: 16KB2. setsockopt()函数
2.1 函数说明
在linux下可以使用man 2 setsockopt命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* 函数声明 */
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);【函数说明】该函数可以设置某个socket套接字的属性选项。
【函数参数】
sockfd:int类型,表示需要设置选项的套接字。level:int类型,选项所在的协议层。 level 可取的值:
| SOL_SOCKET | 通用套接字选项 |
| IPPROTO_IP | IP选项 |
| IPPROTO_TCP | TCP选项 |
我们可以使用如下命令查看这些选项:
man 7 socket # 打开手册后查看 Socket options 部分
man 7 ip # 打开手册后查看 Socket options 部分
man 7 ipv6 # 打开手册后查看 Socket options 部分
man 7 tcp # 打开手册后查看 Socket options 部分常用选项如下:
| 选项名称 | 说明 | 数据类型 |
| SOL_SOCKET | ||
| SO_BROADCAST | 允许发送广播数据 | int |
| SO_DEBUG | 允许调试 | int |
| SO_DONTROUTE | 不查找路由 | int |
| SO_ERROR | 获得套接字错误 | int |
| SO_KEEPALIVE | 保持连接 | int |
| SO_LINGER | 延迟关闭连接 | struct linger |
| SO_OOBINLINE | 带外数据放入正常数据流 | int |
| SO_RCVBUF | 接收缓冲区大小 | int |
| SO_SNDBUF | 发送缓冲区大小 | int |
| SO_RCVLOWAT | 接收缓冲区下限 | int |
| SO_SNDLOWAT | 发送缓冲区下限 | int |
| SO_RCVTIMEO | 接收超时 | struct timeval |
| SO_SNDTIMEO | 发送超时 | struct timeval |
| SO_REUSEADDR | 允许重用本地地址和端口 | int |
| SO_TYPE | 获得套接字类型 | int |
| SO_BSDCOMPAT | 与BSD系统兼容 | int |
| IPPROTO_IP | ||
| IP_HDRINCL | 在数据包中包含IP首部 | int |
| IP_OPTINOS | IP首部选项 | int |
| IP_TOS | 服务类型 | |
| IP_TTL | 生存时间 | int |
| IPPRO_TCP | ||
| TCP_MAXSEG | TCP最大数据段的大小 | int |
| TCP_NODELAY | 不使用Nagle算法 | int |
optlen:socklen_t类型,表示optval的长度,要注意需要传入的是一个变量。
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般使用格式如下:
/* 需要包含的头文件 */
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
/* 至少应该有的语句 */
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
int b_reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));【注意事项】none
2.2 使用实例
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int main(int argc, char *argv[])
{
int b_reuse;
socklen_t optLen = sizeof(int);
int socket_fd = socket_fd = socket(AF_INET, SOCK_STREAM, 0);
getsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (int *)&b_reuse, &optLen);
printf("b_reuse: %d\n", b_reuse);
int a_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &a_reuse, sizeof (int));
getsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, (int *)&b_reuse, &optLen);
printf("b_reuse: %d\n", b_reuse);
return 0;
}在终端执行以下命令编译程序:
gcc test.c -Wall # 生成可执行文件 a.out
./a.out # 执行可执行程序然后,终端会有以下信息显示:
b_reuse: 0
b_reuse: 13. getsockname()函数
3.1 函数说明
在linux下可以使用man 2 getsockname命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/socket.h>
/* 函数声明 */
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);【函数说明】该函数可以用于获取与某个套接字关联的本地协议地址。
【函数参数】
sockfd:int类型,表示已经创建的socket套接字。addr:struct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边前边介绍bind函数的时候已经说过了。addrlen:socklen_t类型指针变量,用于指定addr所指向的结构体对应的字节长度。
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般使用格式如下:
/* 需要包含的头文件 */
#include <stdio.h> /* perror scanf printf */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
/* 至少应该有的语句 */
char ipv4_addr[16] = {};
int port = -1;
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in sin;
socklen_t addrlen = sizeof(sin);
if( getsockname(scoket_fd, (struct sockaddr *)&sin, &addrlen) < 0 )
{
perror("[error]getsockname");
return -1;
}
/* 获取本地IP */
if (!inet_ntop(AF_INET, (void *)&sin.sin_addr, ipv4_addr, sizeof(sin)))
{
perror("[error]inet_ntop");
return -1;
}
/* 获取本地端口 */
port = ntohs(sin.sin_port);【注意事项】none
3.2 使用实例
暂无
4. getpeername()函数
4.1 函数说明
在linux下可以使用man 2 getpeername命令查看该函数的帮助手册。
/* 需包含的头文件 */
#include <sys/socket.h>
/* 函数声明 */
int getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);【函数说明】该函数可以用于获取与某个套接字关联的对端协议地址(如果在服务器端调用的话,可以获取客户端的IP和端口,与accept()函数传出的信息相同)。
【函数参数】
sockfd:int类型,表示已经创建的socket套接字。addr:struct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边前边介绍bind函数的时候已经说过了。addrlen:socklen_t类型指针变量,用于指定addr所指向的结构体对应的字节长度。
【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误类型。
【使用格式】一般使用格式如下:
/* 需要包含的头文件 */
#include <stdio.h> /* perror scanf printf */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
/* 至少应该有的语句 */
char ipv4_addr[16] = {};
int port = -1;
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
if(getpeername(accept_fd, (struct sockaddr *)&cin, &addrlen) < 0)
{
perror("[error]getsockname");
return -1;
}
if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
{
perror("[error]inet_ntop");
return -1;
}
port = ntohs(cin.sin_port);【注意事项】none
4.2 使用实例
暂无
二、网络超时检测
1. 为什么要超时检测?
在网络通信中,很多操作会使得进程阻塞,比如前边TCP套接字中的accept()/connect()/recv(),UDP套接字中的recvfrom()等,进程不能无限制阻塞吧,所以我们就需要进行超时检测,进行超时检测的必要性如下:
避免进程在没有数据时无限制地阻塞。
当设定的时间到时,进程从原操作返回继续运行。
2. socket属性设置超时
2.1 选项与结构体
我们上边学习了socket选项的获取和设置,在里边我们可以设置超时时间,此时相关的选项如下:
| 选项名称 | 说明 | 数据类型 |
| SO_RCVTIMEO | 接收超时 | struct timeval |
| SO_SNDTIMEO | 发送超时 | struct timeval |
/struct timeval <enter>然后我们便会搜索到该结构体的成员说明:
struct timeval
{
time_t tv_sec; /* Seconds */
suseconds_t tv_usec; /* Microseconds */
};2.2 使用实例
2.2.1 server服务器端
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: server.c
* Author : fanhua
* Description: server服务器端——socket超时检测(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror */
#include <stdlib.h> /* exit atoi */
#include <errno.h> /* errno号 */
#include <sys/types.h> /* socket bind listen accept send */
#include <sys/socket.h> /* socket inet_addr bind listen accept send */
#include <strings.h> /* bzero */
#include <arpa/inet.h> /* htons inet_addr inet_pton */
#include <netinet/in.h> /* ntohs inet_addr inet_ntop*/
#include <unistd.h> /* close */
#include <string.h> /* strlen strcat*/
void usage(char *str); /* 提示信息打印函数 */
int main(int argc, char *argv[])
{
int count = 0;
/* 1.参数判断及端口号处理 */
int port = -1;
if (argc != 3)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
if (port < 5000)
{
usage(argv[0]);
exit(-1);
}
/* 2.创建套接字,得到socket描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)/* SOCK_STREAM,使用TCP协议 */
{
perror ("socket");
exit(-1);
}
/* 3.socket属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 3.2 设置超时时间 */
struct timeval tout;
tout.tv_sec = 2; /* 设置2秒时间超时 */
tout.tv_usec = 0;
setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &tout, sizeof(tout)); /* 设置接收超时 */
/* 4.将套接字与指定端口号和IP进行绑定 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族, IPv4 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* 填充IP地址,INADDR_ANY表示允许监听任意IP,但是它其实是(in_addr_t) 0x00000000 */
{
perror ("inet_pton");
exit(-1);
}
/* 4.2绑定 */
if(bind(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("bind");
exit(-1);
}
/*5.调用listen()把主动套接字变成被动套接字 */
if (listen(socket_fd, 5) < 0)
{
perror("listen");
exit(-1);
}
printf ("Server starting....OK!\n");
/*6.阻塞等待客户端连接请求 */
int newfd = -1;
struct sockaddr_in cin; /* 用于存储成功连接的客户端的IP信息 */
socklen_t addrlen = sizeof(cin);
if ((newfd = accept(socket_fd, (struct sockaddr *)&cin, &addrlen)) < 0)
{
perror("accept");
exit(-1);
}
/* 7.打印成功连接的客户端的信息(此处只能打印一个) */
char ipv4_addr[16];
if (!inet_ntop(AF_INET, (void *)&cin.sin_addr, ipv4_addr, sizeof(cin)))
{
perror ("inet_ntop");
exit(-1);
}
printf ("Clinet(%s:%d) is connected successfully![newfd=%d]\n", ipv4_addr, ntohs(cin.sin_port), newfd);
/* 8.数据读写 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
while(1)
{
printf("count=%d\n", ++count);
/* 8.1接收来自客户端的数据 */
bzero(buf, BUFSIZ);
bzero(replay, BUFSIZ);
do{
ret = read(newfd, buf, BUFSIZ - 1);
}while(ret < 0 && EINTR == errno);
if(ret < 0)
{
perror("read");
continue;
}
if(!ret) break; /* 对方已经关闭 */
printf("Receive data: %s", buf);
/* 8.2对客户端做出应答 */
strcat(replay, buf);
ret = send(newfd, replay, strlen(replay), 0);
if(ret < 0)
{
perror("send");
exit(-1);
}
/* 8.3判断是否需要退出 */
if(!strncasecmp(buf, "quit", strlen("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 9.关闭文件描述符 */
close(newfd);
close(socket_fd);
return 0;
}
/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n");
}2.2.2 client客户端
/** =====================================================
* Copyright © hk. 2022-2022. All rights reserved.
* File name: cilent.c
* Author : fanhua
* Description: client客户端程序——socket超时检测(TCP)
* ======================================================
*/
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <stdlib.h> /* exit atoi*/
#include <errno.h> /* errno号 */
#include <unistd.h> /* write close */
#include <sys/types.h> /* socket connect send */
#include <sys/socket.h>/* socket inet_addr connect send */
#include <netinet/in.h>/* inet_addr */
#include <arpa/inet.h> /* inet_addr inet_pton htonl*/
#include <string.h> /* bzero strncasecmp strlen */
void usage(char *str); /* 提示信息打印函数 */
/* 主函数 */
int main(int argc, char *argv[])
{
/* 1.参数判断及端口号处理 */
int port = -1;
int portClient = -1;
if (argc != 4)/* 参数数量不对时打印提示信息 */
{
usage(argv[0]);
exit(-1);
}
port = atoi(argv[2]);/* 字符串转数字 */
portClient = atoi(argv[3]);
if (port < 5000 || portClient < 5000 || (port == portClient))
{
usage(argv[0]);
exit(-1);
}
/* 2.打开套接字,得到套接字描述符 */
int socket_fd = -1; /* 接收服务器端socket描述符 */
if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("socket");
exit(-1);
}
/* 3.socket属性设置 */
/* 3.1允许绑定地址快速重用 */
int b_reuse = 1;
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int));
/* 4.连接服务器 */
/* 4.1填充struct sockaddr_in结构体变量 */
struct sockaddr_in sin;
bzero (&sin, sizeof (sin)); /* 将内存块(字符串)的前n个字节清零 */
sin.sin_family = AF_INET; /* 协议族 */
sin.sin_port = htons(port); /* 网络字节序的端口号 */
/* 5.设置客户端发送接收数据的端口 */
/* 5.1客户端的 argv[1] 需要与系统的IP一致 */
if(inet_pton(AF_INET, argv[1], (void *)&sin.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 5.2绑定固定的IP 和 端口号 */
struct sockaddr_in sinClient;
bzero(&sinClient, sizeof (sinClient)); /* 将内存块(字符串)的前n个字节清零 */
sinClient.sin_family = AF_INET; /* 协议族 */
sinClient.sin_port = htons(portClient); /* 网络字节序的端口号 */
if(inet_pton(AF_INET, argv[1], (void *)&sinClient.sin_addr) != 1)/* IP地址 */
{
perror ("inet_pton");
exit(-1);
}
/* 5.3绑定 */
if(bind(socket_fd, (struct sockaddr *)&sinClient, sizeof(sinClient)) < 0)
{
perror("bind");
exit(-1);
}
/* 6.连接 */
if(connect(socket_fd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
{
perror("connect");
exit(-1);
}
printf("Client staring...OK!\n");
/* 7.数据读写 */
int ret = -1;
char buf[BUFSIZ]; /* BUFSIZ是在stdio.h中定义的一个宏,值为8192 */
char replay[BUFSIZ];
while (1)
{
/* 7.1发送数据 */
bzero (buf, BUFSIZ);
bzero (replay, BUFSIZ);
printf(">");
if (fgets(buf, BUFSIZ - 1, stdin) == NULL)
{
continue;
}
do{
ret = write(socket_fd, buf, strlen(buf));
}while (ret < 0 && EINTR == errno);
/* 7.2接收服务器的回馈数据 */
ret = recv(socket_fd, replay, BUFSIZ, 0);
if(ret < 0)
{
perror("recv");
exit(-1);
}
printf("server replay:%s", replay);
/* 7.3判断是否需要退出 */
if (!strncasecmp(buf, "quit", strlen ("quit"))) //用户输入了quit字符
{
printf ("Client is exiting!\n");
break;
}
}
/* 8.关闭文件描述符 */
close(socket_fd);
return 0;
}
/**
* @Function: usage
* @Description: 用户提示信息打印函数
* @param str : 当前应用程序命令字符串,一般是./app
* @return : none
*/
void usage(char *str)
{
printf ("\n%s serv_ip serv_port", str);
printf ("\n\t serv_ip: server ip address");
printf ("\n\t serv_port: server port(>5000)\n\n");
printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n");
}2.2.3 Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make来进行。
## =====================================================
# Copyright © hk. 2022-2022. All rights reserved.
# File name: Makefile
# Author : fanhua
# Description: Makefile文件
## ======================================================
#
CC = gcc
DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)
# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}
all : $(TARGET_LIST)
%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: all clean clean_o clean_out
clean : clean_o clean_out
@rm -vf $(TARGET_LIST)
clean_o :
@rm -vf *.o
clean_out :
@rm -vf *.out2.2.4 测试结果
我们执行以下命令编译链接程序,生成两个可执行文件:
make然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client:
gcc -g -O2 -Wall client.c -o client
gcc -g -O2 -Wall server.c -o server对于服务器端,我们执行以下命令启动服务器进程:
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口对于客户端,我们执行以下命令启动客户端进程:
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003然后我们就会看到如下现象:

由于我们设置了2s超时,所以在2s内客户端没有连接的话,accept()就会返回,并且显示错误信息,当我们2s内连接客户端后,没有数据从客户端发送到服务器时,每2s就会超时一次,而原本会一直阻塞在recv()函数。设置了超时时候,依然可以正常接收数据。
3. 使用函数自身参数
一般来说,我们一般会使用select()、poll()或者epoll_wait()来查询某个socket文件描述符是否可读或者可写,而这函数一般是带有超时时间的,前边介绍这些函数的时候也有过说明,例如:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);4. 定时器中接收
还有一种方式就是在定时器中接收数据,因为定时器定时时间到了之后会产生SIGALRM信号,这样我们便可以自定义信号处理函数来接收数据,防止进程一直阻塞。
三、TCP客户端掉线了?
对于面向连接的TCP协议的 socket套接字,在实际应用中通常都要检测对端是否处于连接中,连接端口分两种情况:
- (1)连接正常关闭,调用
close()/shutdown()连接正常关闭,send()与recv()等函数立马返回错误,select()函数返回SOCK_ERR; - (2)连接的对端异常关闭,比如网络断掉、突然断电等。
对于第一种情况,正常关闭的,就没啥好说的了,关于第二条,我们如何判断呢?我们了解一个概念吗,就是心跳检测,就像心跳一样客户端每隔几秒钟发送一个数据包(心跳包)给服务器,告诉服务器,客户端还在线。如果服务器在规定时间内没有收到客户端发来的心跳包,则认为客户端已经掉线。
1. 客户端异常掉线的判断
1.1 应用层自定义心跳包程序
自己编写心跳包程序,就是自己的程序加入一条线程,定时向对端发送数据包,查看是否有ACK应答数据包发回,根据ACK的返回情况来管理连接,如果在一定时间内没有收到对方的回应,即认为对方已经掉线。。此方法比较通用,一般使用业务层心跳处理,灵活可控,但会改变现有的协议。
1.2 TCP中使用SO_KEEPALIVE套接字选项
使用TCP的keepalive机制,其实在UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测。keepalive基本原理如下:
TCP内嵌有心跳包,以服务端为例,当server检测到超过一定时间tcp_keepalive_time,一般在linux中是7200s,也就是2h,我们使用以下命令可以查该时间:
cat /proc/sys/net/ipv4/tcp_keepalive_time当没有数据传输的时候,会向client客户端发送一个keepalive packet,此时client端一般会有三种反应:
- (1)
client客户端连接正常,返回一个ACK。server端收到ACK后重置计时器,在2小时后再发送探测。如果2小时内连接上有数据传输,那么在该时间的基础上向后推延2小时发送探测包; - (2)
client客户端异常关闭,或网络断开。client无响应,server收不到ACK,在一定时间tcp_keepalive_intvl(一般默认是75s)后重发keepalive packet,并且重发一定次数tcp_keepalive_probes(一般默认是9);
# 查看 tcp_keepalive_intvl
cat /proc/sys/net/ipv4/tcp_keepalive_intvl
# 查看 tcp_keepalive_probes
cat /proc/sys/net/ipv4/tcp_keepalive_probes- (3)客户端曾异常关闭,但后来已经重启。
server收到的探测响应是一个复位,server会端终止连接。
我们还可以使用如下命令查看上边三个值的设置情况:
shellsudo sysctl -a | grep keepalive默认情况下应该会显示如下信息:
shell[sudo] hk 的密码: net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9 net.ipv4.tcp_keepalive_time = 7200
【注意事项】
(1)根据 MSDN 的文档,如果为 socket 设置了KEEPALIVE选项,TCP/IP 栈在检测到对方掉线后, 任何在该 socket上进行的调用(发送/接受调用)就会立刻返回,错误号是 WSAENETRESET ;同时,此后的任何在该socket句柄的调用会立刻失败,并返回WSAENOTCONN错误。
(2)SO_KEEPALIVE设置空闲2小时才发送一个“保持存活探测分节”,不能保证实时检测。对于判断网络断开时间太长,对于需要及时响应的程序不太适应。当然也可以修改时间间隔参数,但是会影响到所有打开此选项的套接口,关联完成端口的socket可能会忽略掉该套接字选项。
1.3 内核检测
在
2.6内核里面的网卡驱动中,使能1s的周期性检查定时器网卡硬件或者我们通过
GPIO,插拔网线时候产生中断,处理相应中断,这样的话可以立即检测到。
2. SO_KEEPALIVE的使用
一般来说,我们可以通过设置网络属性来使用SO_KEEPALIVE参数完成“心跳检测”:
/* 主函数中应有的部分 */
int keepAlive = 1; /* 设定KeepAlive */
int keepIdle = 5; /* 开始首次KeepAlive探测前的TCP空闭时间 */
int keepInterval = 5; /* 两次KeepAlive探测间的时间间隔 */
int keepCount = 3; /* 判定断开前的KeepAlive探测次数 */
setKeepAlive(socket_fd, keepAlive, keepIdle, keepInterval, keepCount); /* 设置参数 */
/* 自定义函数 */
void setKeepAlive(int socket_fd, int attr_on, socklen_t idle_time, socklen_t interval, socklen_t cnt)
{
setsockopt(socket_fd, SOL_SOCKET, SO_KEEPALIVE, (const char *) &attr_on, sizeof(attr_on));
setsockopt(socket_fd, SOL_TCP, TCP_KEEPIDLE, (const char *) &idle_time, sizeof(idle_time));
setsockopt(socket_fd, SOL_TCP, TCP_KEEPINTVL, (const char *) &interval, sizeof(interval));
setsockopt(socket_fd, SOL_TCP, TCP_KEEPCNT, (const char *) &cnt, sizeof(cnt));
}