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超时
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(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);