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); // 单位: 毫秒
// 等待并接受客户端连接,如果没有客户端连接, 一直阻塞
// 检测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);
}
// 等待并对方发送数据到本地,如果对方没有发送数据, 一直阻塞
// 检测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;
}
// 将要发送的数据写到本地写缓冲区
// 本地写缓冲区, 一直阻塞
// 检测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;
}
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;
}