漫谈TCP(二)

继续谈一谈TCP当中的一些细节。

send()返回-1且errno=EAGAIN

此时唯一的原因就是sendbuffer已满,解决方法是在调用send()之前,加一个poll(fd)判断一下fd是否可写,如果fd就绪就发送,未就绪就下一次再发。这里poll也可以换成epoll或select。

多线程对同一fd操作

此时数据会混乱,解决方法是把fd加入到多路复用中,判断就绪再操作。比如,当其中一个线程对fd进行写操作时,将fd从epoll中删除,写完了再加入回去。这就和加锁是一样的效果,保证只有一个线程在操作fd。
这里提示一下,多个线程对同一个fd send()是有问题的设计,应当避免这样的设计。

延迟ACK可取消

延迟ACK可以设置取消,取消后就是收到数据立即回复ACK包。这种方案只适用于网络良好的情况下。

close()后网络中的剩余数据

主动方调用close()发送fin包后,还在网络中的数据怎么办?其实,被动方会继续接收这些数据。虽然主动方的fd被回收,但是tcb还在,还会继续完成剩下的任务。所以,这种情况下,该收包的收包,该回ack的回ack,该重传的重传,直到接收到了被动方的fin包。其实,可以理解为,主动方调用close(),依然是发出了一个正常的数据包,只不过这个包的FIN位置1了而已。

四次挥手

这是tcp断开连接的主要过程。
在这里插入图片描述

出现大量TIME_WAIT

通过netstat发现,大量连接处于TIME_WAIT状态。
原因是被动方没有及时调用close(),或是在close()前清除了客户端的信息。
解决方法,先判断被动方代码中有没有调用close(),再看代码中

if(recv()==0)
{
    close(fd);
}

这一部分代码有没有加入业务代码。如果有,把这些业务代码做成异步的,相当于客户端断开连接的信号,往异步的消息队列发,抛到线程池或是异步结构中。

出现FIN_WAIT_2

主要是因为对端没有及时调用close()导致,所以出现这种情况,先抛给对端处理,本端无解,只能kill掉相应进程。
站在客户端立场上,出现FIN_WAIT_2的影响并不大,可以再开一个连接开传输数据。但是,如果断开连接时又遇到这个情况,陷入了循环,FIN_WAIT_2太多,还是只能kill或是通知对端。

TIME_WAIT的作用

作用是保证最后一次ack对端接收到了。
如果被动方未接收到ack包,被动方会重发fin包,主动方如果收到了重发的fin包,会重新发ack包。TIME_WAIT的时间可以设置。
如果没有TIME_WAIT,被动方可能就会一直处于LAST_ACK状态而无法结束,陷入死锁。

CLOSING状态

出现CLOSING状态有两种情况。一是被动方发送的fin包比ack包先到(网络原因后发的先到)。此时主动方在FIN_WAIT_1的状态接收到了fin包,那么就会直接进入CLOSING状态。当然,最后也会进入TIME_WAIT状态。
另一种情况,是双方同时调用close(),情况与上面的差不多,也是从FIN_WAIT_1直接进入CLOSING,区别在于双方都会进入TIME_WAIT状态。

SO_REUSEADDR

端口复用,是把TIME_WAIT状态下的tcb拿来复用,不需要等待连接释放。当然这里也不是马上复用,而是等到主动方如果有分配socket的需要了才会复用,如果没有分配,还是TIME_WAIT。所以,有可能出现刚才讲的LAST_ACK死锁。但是,现在网络较为稳定,出现丢ack包的可能性较低,此外,这个死锁也是有时间限制的。

epoll

先说明一下,epoll不一定比select/poll效率高,特别是当连接数减少时。实际开发中,当连接数超过500(理论值1024)时使用epoll较好。
此外还要注意,epoll不是网络模块,而是文件系统的一部分。

数据结构

epoll中有两个数据结构。一个用来存储监听节点,另一个用来存储就绪节点。因为涉及到查找,所以这里分析一下用来做索引的数据结构。
hash:查找速度快,但不利于扩展。(hash表扩展不方便,添加节点会很麻烦。)
B/B+树:分支多,节点大,不适合做内存索引。
跳表:有多个层级,节点有很多浪费。
红黑树:方便扩展,且节点没有浪费。所以,监听的节点使用红黑树。

就绪队列

这里说明一下就绪节点用队列的原因。
一是就绪节点数量较少,二是就绪节点不需要查找,本身就需要遍历,所有节点都需要带到用户空间去执行,这样使用队列较为方便。
这里选用链式结构,方便扩展,不用考虑长度问题。选用队列不用栈,是因为一次可能读不完就绪节点,使用栈可能会导致饥饿。

线程安全

红黑树和队列中存的是struct epoll_event结构体。这里线程安全主要是加锁。
对于红黑树而言,有两种粒度的锁,一是锁整棵树,二是锁子树。虽然锁子树粒度较小,但是锁整棵树更容易实现且更为安全,所以这里选择的是锁住整棵树。这里的锁使用的是互斥锁,因为临界区的操作较多,而队列选用的是自旋锁,队列用CAS也可以。

需要epoll通知是否就绪的时机

1.三次握手完成时。全连接队列增加一个节点。
2.recv buffer有数据时。
3.send buffer有数据时。
4.接收到fin包时。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值