Openssl数据安全传输平台005:Socket-超时的处理-代码框架及实现

文章目录

  • 0. 代码仓库
  • 1. 服务端accept超时 -- 等待客户端连接
  • 2. read超时
  • 3. write超时
  • 4. connect超时

0. 代码仓库

https://github.com/Chufeng-Jiang/OpenSSL_Secure_Data_Transmission_Platform

使用到的函数如下

// 套接字通信过程中默认的阻塞函数 -> 条件不满足, 一直阻塞
// 等待并接受客户端连接
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);

// 设置超时处理的原因:
    - 不想让线程/进程一直在对应的函数(上边的函数)的位置阻塞
    - 设置一个阻塞的时间, 当时间到了之后强制线程/进程处理别的任务
    - 
// 超时处理的思路:
    - 定时器 
        - linux中可以发信号, 中断休眠

    - sleep(10)
        - 不可用, 在指定时间之内如果阻塞函数满足条件, 直接接触阻塞, 进行业务处理
        - 上述两种方式, 不能在程序休眠过程中解除休眠, 进行业务处理
        
    - IO多路转接函数:
        - 帮助我们委托内核检测fd的状态://异常
        - 这些函数最后一个参数设置函数阻塞时长, 在阻塞过程中, 如果有fd状态发生变化, 函数直接返回
		int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); // 单位: s
		int poll(struct pollfd *fds, nfds_t nfds, int timeout); // 单位: 毫秒
		int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 单位: 毫秒

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监听的文件描述符

// 最多阻塞3s
int ret = select(sockfd+1, &rdset, NULL, NULL, &val);
if(ret == 0)
{
     // 超时了, 最后一个参数等待时长用完了   
}
else if(ret = 1)
{
    // 有新连接
    accept(); // 绝对不阻塞
}
else
{
    // 异常, select调用失败, 返回值为 -1
}
TcpSocket* TcpServer::acceptConn(int wait_seconds)
{
	int ret;
	if (wait_seconds > 0)
	{
		fd_set accept_fdset;
		struct timeval timeout;
		FD_ZERO(&accept_fdset);
		FD_SET(m_lfd, &accept_fdset);
		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;
		do
		{
			// 检测读集合
			ret = select(m_lfd + 1, &accept_fdset, NULL, NULL, &timeout);
		} while (ret < 0 && errno == EINTR);	// 被信号中断, 再次进入循环
		if (ret <= 0)
		{
			return NULL;
		}
	}

	// 一但检测出 有select事件发生,表示对等方完成了三次握手,客户端有新连接建立
	// 此时再调用accept将不会堵塞
	struct sockaddr_in addrCli;
	socklen_t addrlen = sizeof(struct sockaddr_in);
	int connfd = accept(m_lfd, (struct sockaddr*)&addrCli, &addrlen); //返回已连接套接字
	if (connfd == -1)
	{
		return NULL;
	}

	return new TcpSocket(connfd);
}

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通信的文件描述符

int ret = select(fd+1, &rdset, NULL, NULL, &val);
if(ret == 0)
{
     // 超时了, 最后一个参数等待时长用完了   
}
else if(ret == 1)
{
    // 有新数据达到-> 对方发送来的通信数据
    read()/recv();    // 绝对不阻塞
}
else
{
    // 异常, select调用失败, 返回值为 -1
}
int TcpSocket::readTimeout(unsigned int wait_seconds)
{
	int ret = 0;
	if (wait_seconds > 0)
	{
		fd_set read_fdset;
		struct timeval timeout;

		FD_ZERO(&read_fdset);
		FD_SET(m_socket, &read_fdset);

		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;

		//select返回值三态
		//1 若timeout时间到(超时),没有检测到读事件 ret返回=0
		//2 若ret返回<0 &&  errno == EINTR 说明select的过程中被别的信号中断(可中断睡眠原理)
		//2-1 若返回-1,select出错
		//3 若ret返回值>0 表示有read事件发生,返回事件发生的个数

		do
		{
			ret = select(m_socket + 1, &read_fdset, NULL, NULL, &timeout);

		} while (ret < 0 && errno == EINTR);

		if (ret == 0)
		{
			ret = -1;
			errno = ETIMEDOUT;
		}
		else if (ret == 1)
		{
			ret = 0;
		}
	}

	return ret;
}

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通信的文件描述符

int ret = select(fd+1, NULL, &wrset, NULL, &val);
if(ret == 0)
{
     // 超时了, 最后一个参数等待时长用完了   
}
else if(ret == 1)
{
    // 写缓冲区可写
    write()/send();    // 绝对不阻塞
}
else
{
    // 异常, select调用失败, 返回值为 -1
}
int TcpSocket::writeTimeout(unsigned int wait_seconds)
{
	int ret = 0;
	if (wait_seconds > 0)
	{
		fd_set write_fdset;
		struct timeval timeout;

		FD_ZERO(&write_fdset);
		FD_SET(m_socket, &write_fdset);
		timeout.tv_sec = wait_seconds;
		timeout.tv_usec = 0;
		do
		{
			ret = select(m_socket + 1, NULL, &write_fdset, NULL, &timeout);
		} while (ret < 0 && errno == EINTR);

		// 超时
		if (ret == 0)
		{
			ret = -1;
			errno = ETIMEDOUT;
		}
		else if (ret == 1)
		{
			ret = 0;	// 没超时
		}
	}

	return ret;
}

4. connect超时

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

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

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

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

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

    • 当用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(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);

// 连接服务器 -> 不阻塞了
connect(connfd, &serveraddress, &addlen);

// 通过select检测
struct timeval val = {3, 0};    // 3s

// 通信的connfd放到写集合中进行检测
fd_set wrset;
FD_ZERO(&wrset);
FD_SET(connfd, &wrset);    // fd通信的文件描述符

// 函数返回了, connect有结果了, 成功/失败 -> 过程走完了, 得到了结果
int ret = select(fd+1, NULL, &wrset, NULL, &val);
if(ret == 0)
{
     // 超时了, connect还在连接过程中
}
else if(ret == 1)
{
    // 写缓冲区可写
    // 连接过程完成了, 得到了结果
    int opt;
    getsockopt(connfd, SOL_SOCKET, SO_ERROR, &opt, sizeof(opt));
    if(opt > 0)
    {
        // connect失败了
    }
    else if(opt == 0)
    {
        // connect连接成功了
    }
}
else
{
    // 异常, select调用失败, 返回值为 -1
}
// 将connfd状态还原 -> 阻塞
int TcpSocket::connectTimeout(sockaddr_in *addr, unsigned int wait_seconds)
{
	int ret;
	socklen_t addrlen = sizeof(struct sockaddr_in);

	if (wait_seconds > 0)
	{
		setNonBlock(m_socket);	// 设置非阻塞IO
	}

	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;
			}
		}
	}
	if (wait_seconds > 0)
	{
		setBlock(m_socket);	// 套接字设置回阻塞模式
	}
	return ret;
}

你可能感兴趣的:(数据安全传输基础设置平台项目,socket)