银行安全传输平台(三)解决套接字超时和TCP粘包问题


前言

本篇笔记主要解决在网络传输过程中遇到的套接字超时问题处理和TCP粘包问题。

一、套接字超时是什么

套接字通信过程中默认使用的是阻塞函数,如果条件不满足则程序一直阻塞在这里,所以我们想要设置一个固定的时间,一旦阻塞的时间超过我们设定的这个时间,就会强制线程或者进程处理其他任务。
例如:

// 等待并接受客户端连接
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.1 定时器(不可用)

例如sleep(1),让线程等待一定的时长,(不可用原因:不能马上醒过来,在指定时间之内,如果满足条件,不能直接解除阻塞)。例如接受数据read/recv,只要读缓冲区没有数据,我们就要阻塞在这里,但是假如我们让程序睡眠十秒后退出,在这十秒之后读缓冲区有数据了,不能马上退出,直至睡眠完成,所以就是说程序大部分时间都在睡觉,所以该思路不可用。

2.2 select

IO多路转接函数最后一个参数是设置阻塞时长,所以这些函数可以帮助我们检测内核fd状态(读/写/异常),例如accept检测读缓冲区,检测读缓冲区有没有数据就行,我们在这里设置一个阻塞时长,如果一直没有检测到数据并且超时,那么IO转接函数就会直接退出。如果在阻塞过程中,如果检测到状态变化则直接返回。
例如:

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);// 单位: 毫秒

三、超时处理select实现

3.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监听的文件描述符
int ret = select(sockfd+1, &rdset, NULL, NULL, &val);
if(ret == 0)
{
 	// 超时了, 最后一个参数等待时长用完了   
}
else if(ret == 1)
{
    // 有新连接
    accept();	// 绝对不阻塞
}
else
{
    // 异常, select调用失败, 返回值为 -1
}

在传统的网络编程里,服务器端调用 accept 函数时,如果没有客户端连接到来,该函数会一直阻塞,所以我们使用 select 函数对监听套接字进行监控,同时设置一个超时时间。在这个超时时间内,若有新的客户端连接到来,select 函数会返回,接着就可以调用 accept 函数去接受连接;若超时时间用完仍没有新连接,select 函数也会返回,这样程序就能继续执行其他任务。

3.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
}

当调用read函数从套接字读取数据时,如果没有数据到达,函数会一直阻塞,借助select在规定时间内监控指定套接字的可读状态,若超时仍无数据,程序可继续执行其他操作;若有数据到达,再调用read函数读取数据,此时read函数不会阻塞。

3.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
}

3.4 connect超时

Posix 定义了与 select/epoll 和 非阻塞 connect 相关的规定:
连接过程中写缓冲区不可用;连接建立成功时,socket 文件描述符变为可写。(连接建立时,写缓冲区空闲,所以可写);连接建立失败时,socket 文件描述符既可读又可写。 (由于有未决的错误,从而可读又可写)
连接失败, 错误判定方式:
当用select检测连接时,socket既可读又可写,只能在可读的集合通过getsockopt获取错误码。
处理流程:

  • 设置connect函数操作的文件描述符为非阻塞
    • 调用connect
    • 使用select检测
      • 需要getsockopt进行判断
    • 设置connect函数操作的文件描述符为阻塞 -> 状态还原
// 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)
{
	//这里就是检测了val时间,还是没返回,就是超时了
 	// 超时了, 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状态还原 -> 阻塞

connect的超时处理跟前面几个不太一样,首先要将connect设置为非阻塞,无论连接成功与否直接返回(不用在这儿阻塞,如果这里不设置非阻塞,就可能在这里一直阻塞,因为有可能三次握手由于某种一直连不上)。可以理解为直接获取三个状态(连接过程中,连接成功,连接失败,我们需要用select来获取连接过程中这个状态,所以要设置为非阻塞)
默认函数是有一个超时处理的,但是如果这个时间不能满足我们,我们就需要自己处理,不能直接用select的原因就是没有意义,因为我们不能检测写缓冲区,无论写缓冲区可不可用都不能代表connect是否成功。

四、TCP粘包问题

4.1 原因

  • 想象一个场景,客户端每隔1s给服务器发送一条数据, 每条数据长度 100字节 , 服务器每隔2s接收一次数据,服务器就会接收一个数据得到200字节 -> 粘包(不能区分两个包)
  • 怎么造成的?
    • 发送的时候, 内核进行了优化, 数据到达一定量发送一次
    • 网络环境不好, 有延时
    • 接收方接收数据频率低, 一次性读到了多条客户端发送的数据
  • 解决方案:
    • 发送的时候, 强制缓冲区数据被发送出去 - > flush
    • 在发送数据的时候每个数据包添加包头 包头: 一块内存,
      • 存储了当前这条消息的属性信息 ,属于谁 -> char[12] ,有多大 -> int……

4.2 解决逻辑

  • 发送端:
    • 将数据长度转换为网络字节序,作为头部发送。
    • 再发送实际数据内容。
  • 接收端:
    • 先读取 4 字节长度字段,转换为主机字节序。
    • 根据长度读取完整数据。

伪代码如下:

// 发送逻辑
int dataLen = sendData.size() + 1; // +1 包含 '\0'
unsigned char *netdata = (unsigned char*)malloc(dataLen + 4);
int netlen = htonl(dataLen);
memcpy(netdata, &netlen, 4);          // 写入长度
memcpy(netdata + 4, sendData.data(), dataLen); // 写入数据

// 接收逻辑
int netdatalen = 0;
readn(&netdatalen, 4);                // 读取长度
int n = ntohl(netdatalen);
char *tmpBuf = (char*)malloc(n);
readn(tmpBuf, n);                     // 读取数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值