Skip to content

LV045-网络属性

本文主要是网络编程——网络属性的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

一、网络属性

记得前边我们使用过setsockopt函数设置过允许地址重用,终于到了详细学习该函数的部分了。

1. getsockopt()函数

1.1 函数说明

linux下可以使用man 2 getsockopt命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h> 
/* 函数声明 */
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);

函数说明】该函数可以获取某个socket套接字的属性选项。

函数参数

  • sockfdint类型,表示需要获取选项的套接字。
  • levelint类型,选项所在的协议层。level 可取的值说明如下:
SOL_SOCKET通用套接字选项
IPPROTO_IPIP选项
IPPROTO_TCPTCP选项
- `optname`:`int`类型,表示要获取的选项名称。`optname`可取的值说明如下:

我们可以使用如下命令查看这些选项:

shell
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_OPTINOSIP首部选项int
IP_TOS服务类型
IP_TTL生存时间int
IPPRO_TCP
TCP_MAXSEGTCP最大数据段的大小int
TCP_NODELAY不使用Nagle算法int
- `optval`:`void`类型指针变量,获取的套接字选项,需要根据选项名称的数据类型进行强制类型转换。 - `optlen`:`socklen_t `类型指针变量,表示`optval`的长度,要注意需要传入的是一个地址。

返回值int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

使用格式】一般使用格式如下:

c
/* 需要包含的头文件 */
#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 使用实例

c
#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;
}

在终端执行以下命令编译程序:

shell
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

shell
sendBuff length: 16KB

2. setsockopt()函数

2.1 函数说明

linux下可以使用man 2 setsockopt命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#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套接字的属性选项。

函数参数

  • sockfdint类型,表示需要设置选项的套接字。
  • levelint类型,选项所在的协议层。 level 可取的值:
SOL_SOCKET通用套接字选项
IPPROTO_IPIP选项
IPPROTO_TCPTCP选项
- `optname`:`int`类型,表示要设置的选项名称。optname 常用可取的值说明如下:

我们可以使用如下命令查看这些选项:

shell
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_OPTINOSIP首部选项int
IP_TOS服务类型
IP_TTL生存时间int
IPPRO_TCP
TCP_MAXSEGTCP最大数据段的大小int
TCP_NODELAY不使用Nagle算法int
- `optval`:`void`类型指针变量,设置的套接字选项,需要根据选项名称的数据类型进行强制类型转换。
  • optlensocklen_t类型,表示optval的长度,要注意需要传入的是一个变量。

返回值int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

使用格式】一般使用格式如下:

c
/* 需要包含的头文件 */
#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 使用实例

c
#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;
}

在终端执行以下命令编译程序:

shell
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

shell
b_reuse: 0
b_reuse: 1

3. getsockname()函数

3.1 函数说明

linux下可以使用man 2 getsockname命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/socket.h>

/* 函数声明 */
int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函数说明】该函数可以用于获取与某个套接字关联的本地协议地址

函数参数

  • sockfdint类型,表示已经创建的socket套接字。

  • addrstruct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边前边介绍bind函数的时候已经说过了。

  • addrlensocklen_t类型指针变量,用于指定addr所指向的结构体对应的字节长度。

返回值int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

使用格式】一般使用格式如下:

c
/* 需要包含的头文件 */
#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命令查看该函数的帮助手册。

c
/* 需包含的头文件 */
#include <sys/socket.h>

/* 函数声明 */
int  getpeername(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

函数说明】该函数可以用于获取与某个套接字关联的对端协议地址(如果在服务器端调用的话,可以获取客户端的IP和端口,与accept()函数传出的信息相同)。

函数参数

  • sockfdint类型,表示已经创建的socket套接字。

  • addrstruct sockaddr类型的结构体指针变量,指向一个struct sockaddr类型变量,该结构体中含有要绑定的IP地址及端口号。但是呢,我们一般不使用这个类型,一般会使用struct sockaddr_in类型,具体原因后边前边介绍bind函数的时候已经说过了。

  • addrlensocklen_t类型指针变量,用于指定addr所指向的结构体对应的字节长度。

返回值int类型,成功返回0,失败返回-1,并设置errno表示错误类型。

使用格式】一般使用格式如下:

c
/* 需要包含的头文件 */
#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`结构体其实我们前边已经学习过了,我们可以使用`man struct timeval`命令查看帮助手册,使用该命令后输入以下搜索标记:
c
/struct timeval <enter>

然后我们便会搜索到该结构体的成员说明:

c
struct timeval
{
	time_t      tv_sec;  /* Seconds */
	suseconds_t tv_usec; /* Microseconds */
};

2.2 使用实例

2.2.1 server服务器端
c
/** =====================================================
 * 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客户端
c
/** =====================================================
 * 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来进行。

makefile
## =====================================================
# 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 *.out
2.2.4 测试结果

我们执行以下命令编译链接程序,生成两个可执行文件:

shell
make

然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server和客户端程序client

shell
gcc -g -O2 -Wall    client.c   -o client
gcc -g -O2 -Wall    server.c   -o server

对于服务器端,我们执行以下命令启动服务器进程:

shell
./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口

对于客户端,我们执行以下命令启动客户端进程:

shell
./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003

然后我们就会看到如下现象:

image-20220705141116835

由于我们设置了2s超时,所以在2s内客户端没有连接的话,accept()就会返回,并且显示错误信息,当我们2s内连接客户端后,没有数据从客户端发送到服务器时,每2s就会超时一次,而原本会一直阻塞在recv()函数。设置了超时时候,依然可以正常接收数据。

3. 使用函数自身参数

一般来说,我们一般会使用select()poll()或者epoll_wait()来查询某个socket文件描述符是否可读或者可写,而这函数一般是带有超时时间的,前边介绍这些函数的时候也有过说明,例如:

c
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套接字选项

使用TCPkeepalive机制,其实在UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测。keepalive基本原理如下:

TCP内嵌有心跳包,以服务端为例,当server检测到超过一定时间tcp_keepalive_time,一般在linux中是7200s,也就是2h,我们使用以下命令可以查该时间:

shell
cat /proc/sys/net/ipv4/tcp_keepalive_time

当没有数据传输的时候,会向client客户端发送一个keepalive packet,此时client端一般会有三种反应:

  • (1)client客户端连接正常,返回一个ACKserver端收到ACK后重置计时器,在2小时后再发送探测。如果2小时内连接上有数据传输,那么在该时间的基础上向后推延2小时发送探测包;
  • (2)client客户端异常关闭,或网络断开。client无响应,server收不到ACK,在一定时间tcp_keepalive_intvl(一般默认是75s)后重发keepalive packet,并且重发一定次数tcp_keepalive_probes(一般默认是9);
shell
# 查看 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会端终止连接。

我们还可以使用如下命令查看上边三个值的设置情况:

shell
sudo 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参数完成“心跳检测”:

c
/* 主函数中应有的部分 */
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));
}