c++如何实现套接字超时处理

1 简介

在socket通信中,往往在以下情况会遇到阻塞

// 套接字通信过程中默认的阻塞函数 -> 条件不满足, 一直阻塞
// 等待并接受客户端连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
// 通信
// 接收数据
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
// 发送数据
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
// 连接服务器的时候
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2 为什么要超时处理

  • 不想让线程/进程一直在对应的函数(上边的函数)的位置阻塞。
  • 设置一个阻塞的时间, 当时间到了之后强制线程/进程处理别的任务。

3 超时处理的思路

IO多路转接函数(select)

  • 帮助我们委托内核检测fd的状态: 读/写/异常。
  • 这些函数最后一个参数设置函数阻塞时长, 在阻塞过程中, 如果有fd状态发生变化, 函数直接返回。

(1)accept超时

// 等待并接受客户端连接
// 如果没有客户端连接, 一直阻塞
// 检测accept函数对应的fd(监听的文件描述法)的读缓冲区就可以了
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

// 使用select检测状态
struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};
struct timeval val = {3, 0};	// 3s

// 监听的sockfd放到读集合中进行检测
fd_set rdset;
FD_ZERO(&rdset);
FD_SET(sockfd, &rdset);	// sockfd监听的文件描述符
//当sockfd有连接请求时,读集合就会有反应
int ret;
do
{
		ret = select(m_socket + 1, &rdset, NULL, NULL, &timeout);
} while (ret < 0 && errno == EINTR);//有中断就继续
// 超时
if (ret == 0)
{
	ret = -1;
	errno = ETIMEDOUT;
}
else if (ret == 1)
{
	// 有新连接
    accept();	// 绝对不阻塞
}
else if (ret == -1)
{
    // 异常, select调用失败, 返回值为 -1
}

(2)read超时

// 等待并对方发送数据到本地
// 如果对方没有发送数据, 一直阻塞
// 检测read函数对应的fd(通信的文件描述符)的读缓冲区就可以了
ssize_t read(int fd, void *buf, size_t count);

// 使用select检测状态
struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};
struct timeval val = {3, 0};	// 3s
// 通信的fd放到读集合中进行检测
fd_set rdset;
FD_ZERO(&rdset);
FD_SET(fd, &rdset);	// fd通信的文件描述符

//当fd有数据发过来时,读集合就会有反应
int ret;
do
{
		ret = select(m_socket + 1, &fd, NULL, NULL, &timeout);
} while (ret < 0 && errno == EINTR);//有中断就继续
// 超时
if (ret == 0)
{
	ret = -1;
	errno = ETIMEDOUT;
}
else if (ret == 1)
{
	// 有新数据达到-> 对方发送来的通信数据
    read()/recv();	// 绝对不阻塞
}
else if (ret == -1)
{
    // 异常, select调用失败, 返回值为 -1
}

(3)write超时

/ 将要发送的数据写到本地写缓冲区
// 本地写缓冲区, 一直阻塞
// 检测write函数对应的fd(通信的文件描述符)的写缓冲区就可以了
ssize_t write(int fd, const void *buf, size_t count);

// 使用select检测状态
struct timeval {
	time_t      tv_sec;         /* seconds */
	suseconds_t tv_usec;        /* microseconds */
};
struct timeval val = {3, 0};	// 3s
// 通信的fd放到写集合中进行检测
fd_set wrset;
FD_ZERO(&wrset);
FD_SET(fd, &wrset);	// fd通信的文件描述符

//当fd的写缓存是空,wrset就有反应
int ret;
do
{
		ret = select(m_socket + 1, NULL, &wrset, NULL, &timeout);
} while (ret < 0 && errno == EINTR);//有中断就继续
// 超时
if (ret == 0)
{
	ret = -1;
	errno = ETIMEDOUT;
}
else if (ret == 1)
{
	write()/send();	// 绝对不阻塞
}
else if (ret == -1)
{
    // 异常, select调用失败, 返回值为 -1
}

**(4)connect超时

  1. Posix 定义了与 select/epoll 和 非阻塞 connect 相关的规定:

    • 连接过程中写缓冲区不可用

    • 连接建立成功时,socket 文件描述符变为可写。(连接建立时,写缓冲区空闲,所以可写)

    • 连接建立失败时,socket 文件描述符既可读又可写。 (由于有未决的错误,从而可读又可写)

  2. 连接失败, 错误判定方式:

    • 当用select检测连接时,socket既可读又可写,只能在可读的集合通过getsockopt获取错误码。
// 连接服务器 -> 如果连接过程中, 函数不返回-> 程序阻塞在这个函数上, 通过返回值判断函数是不是调用成功了
// 返回值: 0 -> 连接成功, -1: 连接失败
// 默认该函数有一个超时处理: 75s, 175s
// 如果上述不能满足, 需要自己设置超时处理
// 设置超时连接处理过程:
	- 设置connect函数操作的文件描述符为非阻塞
	- 调用connect
	- 使用select检测
		- 需要getsockopt进行判断
	- 设置connect函数操作的文件描述符为阻塞 -> 状态还原
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 获取文件描述符的状态是否有错误
int getsockopt(int sockfd, int level, int optname,
                      void *optval, socklen_t *optlen);
// 判断错误
sockfd: 文件描述符
level: SOL_SOCKET
optname: SO_ERROR
optval: int 类型, 存储错误状态
	- 没有问题: 0
    - 有问题: 保存了错误码(错误编号 > 0)
optlen: optval大小对一个的以地址
// connect超时处理
// 设置非阻塞
int flag = fcntl(m_socket, F_GETFL);
flag |= O_NONBLOCK;
fcntl(m_socket, F_SETFL, flag);

ret = connect(m_socket, (struct sockaddr*)addr, addrlen);
// 非阻塞模式连接, 返回-1, 并且errno为EINPROGRESS, 表示连接正在进行中
if (ret < 0 && errno == EINPROGRESS)
{
	fd_set connect_fdset;
	struct timeval timeout;
	FD_ZERO(&connect_fdset);
	FD_SET(m_socket, &connect_fdset);
	timeout.tv_sec = wait_seconds;
	timeout.tv_usec = 0;
	do
	{
		// 一但连接建立,则套接字就可写 所以connect_fdset放在了写集合中
		ret = select(m_socket + 1, NULL, &connect_fdset, NULL, &timeout);
	} while (ret < 0 && errno == EINTR);

	if (ret == 0)
	{
		// 超时
		ret = -1;
		errno = ETIMEDOUT;
	}
	else if (ret < 0)
	{
		return -1;
	}
	else if (ret == 1)
	{
		/* ret返回为1(表示套接字可写),可能有两种情况,一种是连接建立成功,一种是套接字产生错误,*/
		/* 此时错误信息不会保存至errno变量中,因此,需要调用getsockopt来获取。 */
		int err;
		socklen_t sockLen = sizeof(err);
		int sockoptret = getsockopt(m_socket, SOL_SOCKET, SO_ERROR, &err, &sockLen);
		if (sockoptret == -1)
		{
			return -1;
		}
		if (err == 0)
		{
			ret = 0;	// 成功建立连接
		}
		else
		{
			// 连接失败
			errno = err;
			ret = -1;
		}
	}
}

//设置回阻塞
int flags = fcntl(m_socket, F_GETFL);
flags &= ~O_NONBLOCK;
ret = fcntl(m_socket, F_SETFL, flags);
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值