TCP和网络编程相关问题

一. 握手

1. 三次握手的过程?

客户端向服务端发SYN k,客户端进入SYN_SEND状态
服务端收到后向客户端发 ACK k+1, SYN j,服务端进入SYN_RECV状态。
客户端向服务端发ACK j+1, 客户端ESTABLISH。
服务端收到ACK后进入ESTABLISH状态。
双向连接建立。

2. SYN攻击是什么?解决方案?

SYN攻击:攻击者伪造大量IP向TCP端口发送SYN请求,服务器不停超时重传ACK,占用了未完成连接队列的大量位置,使正常的连接无法建立,拖慢服务器业务。
解决方案:SYN cookies,减少超时时间等。

3. 为什么是三次握手?

主要是TCP是全双工通信,二次握手肯定满足不了要求。
四次握手只是把第二次的ACK和SYN请求分开,但连接尚未建立,并不需要等待什么,所以合在一起是比较合理的。

二. 挥手

1. 四次挥手的过程?

客户端向服务端发送FIN。客户端进入FIN_WAIT_1状态
服务端回复ACK,此时客户端侧发送通道关闭。服务端进入CLOSE_WAIT状态,客户端进入FIN_WAIT_2状态。
服务端发送FIN,此时服务端进入LAST_ACK状态。
客户端收到FIN,发送ACK,此时客户端进入TIME_WAIT状态,服务端收到ACK进入CLOSED状态。
客户端等待2MSL后,进入CLOSED状态。

2. 为什么是四次挥手,三次行不行?

三次行不行?三次当然不行。
四次挥手和三次挥手的区别就是中间那次FIN和ACK要不要放在一个包里发,答案当然是否定的。TCP是全双工连接,服务端收到FIN后,只是保证不会再收到来自客户端的消息了,因为客户端要关闭连接了。但是服务端这边的发送缓冲区可能还有消息没有发出去,要发送完这些未尽的消息才能发送FIN,表示服务端也要关闭了。
当然在实现中,如果缓冲区确实没有数据,服务器确实会立刻发送FIN+ACK的组合包,看起来像是三次挥手,但这也只是实现的优化,并不代表协议设计成了这样。

3. 出现大量TIME_WAIT是为什么?如何解决?

一般服务端出现大量TIME_WAIT都是HTTP服务器的东西。这是因为一次HTTP请求过程中,如果是短连接,关闭连接的都是服务器,因此可能会出现比较多的TIME_WAIT。
修改/sbin/sysctl -p的数值,这是系统中TIME_WAIT的一些参数。

net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭;
net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间。
net.ipv4.tcp_max_tw_buckets TIMEWAIT最大连接数

此处应该注意,tcp_tw_recycle选项在公网打开会导致客户端连接出错率上升,谨慎打开。

4. 出现大量CLOSE_WAIT是为什么?如何解决?

服务端出现大量CLOSE_WAIT状态的连接,根据协议来看显然是服务端没有发送FIN所致,所以一般都是服务器代码写的有问题,或者发送缓冲区积压了太多东西,一直没有发完,轮不到发送FIN,一般也是程序写的有问题(对于服务端码农而言)。
解决方案:查代码吧。一般都是没有正确的处理连接关闭所致。

5. TIME_WAIT的作用是什么?

TIME_WAIT被设计为等待2MSL,也就是两倍的包最长的生命周期,这主要是为了两个目的:
(1) 防止迷路的FIN包最后又发回来,影响正常的连接。
如果不等2MSL,而是等待时间比较短,那可能一个对端发来的超时的FIN包被发送过来,此时可能已经建立了一个新的连接,那么这时候新的连接也会受到影响。
(2)可靠地关闭TCP连接
如果客户端最后发送的ACK超时,那么根据TCP超时重传的机制,服务端会重发FIN,此时如果客户端已经关闭连接,那么就会回复一个RST,这样就会被认为是一个错误。为了避免这种情况出现,客户端要等待2MSL,准备好迎接重发的FIN(如果有的话)。

6. tcp有了keepalive的机制,那么应用层的心跳机制是否还需要?

需要。
tcp的keepalive仅能保证tcp连接的正常,并不代表应用还是正常的,比如说应用在某处死锁了,然而内核对此并不知情,还会继续处理keepalive,这样看起来连接还是OK的,但是应用已经萎了。

三. 拥塞控制,流量控制

拥塞控制是为了解决全网流量不至于太过拥堵的问题,对自身连接未必是最优的。
流量控制就是为了防止一方发太快,接收方没有缓存放。

1.慢启动算法?

发送方一开始便向网络发送多个报文段,直至达到接收方通告的窗口大小为止。当发送方和接收方处于同一个局域网时,这种方式是可以的。但是如果在发送方和接收方之间存在多个路由器和速率较慢的链路时,就有可能出现一些问题。
一些中间路由器必须缓存分组,并有可能耗尽存储器的空间。
现在,TCP需要支持一种被称为“慢启动(slow start)”的算法。该算法通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。
慢启动为发送方的TCP增加了另一个窗口:拥塞窗口(congestion window),记为cwnd。当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为 1个报文段(即另一端通告的报文
段大小)。每收到一个ACK,拥塞窗口就增加一个报文段( cwnd以字节为单位,但是慢启动以报文段大小为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥
塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。发送方开始时发送一个报文段,然后等待 ACK。当收到该ACK时,拥塞窗口从1增加为2,即可以发送两个报文段。当收到这两个报文段的ACK时,拥塞窗口就增加为4。这是一种指数增加的关系。
总的来说,就是指数增加。那为啥叫慢启动呢,可能是因为启动时的窗口比较小吧……

2. 拥塞避免算法?

拥塞避免在慢启动增长到ssthresh后开始执行,开始线性增加发送窗口长度,而不再指数增加,并且一旦发现有丢包(发现ACK没收到)就要把ssthresh置为发生拥塞时接收窗口的一半,然后把cwnd设为1,重新开始一遍慢启动过程。

3. 快速重传?

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方)而不要等到自己发送数据时才进行捎带确认。这样让发送方很快能知道发生了丢包,马上去重传丢掉的包。

4. 快速恢复?

当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢开始门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢开始算法,而是开始执行拥塞避免算法。
由于发送方现在认为网络很可能没有发生拥塞,因此与慢开始不同之处是现在不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为 慢开始门限ssthresh减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

5. 流量控制

流量控制就是滑动窗口机制,收发双方通过报文同步接收缓冲区长度,以避免过快发送者的出现。
如果控制流量到0窗口,那么就设置一个零窗口定时器,时间到了就发一个窗口探测报文来更新窗口。

6. BBR算法有什么改进?

BBR算法的改进主要就是对拥塞避免算法的改进,主要思路就是通过计算时延带宽积来计算网络带宽,而不是像传统的拥塞避免算法那样,一旦出现超时就认为是网络拥塞,减少退避窗口。
基本思路就是分别估计带宽和延时,交替测量带宽和延迟;用一段时间内的带宽极大值和延迟极小值作为估计值。
具体可以参考知乎的回答。

三. TCP的那些状态

  • CLOSED:初始状态。

  • LISTEN:服务器处于监听状态。

  • SYN_SEND:客户端socket执行CONNECT连接,发送SYN包,进入此状态。

  • SYN_RECV:服务端收到SYN包并发送服务端SYN包,进入此状态。

  • ESTABLISH:表示连接建立。客户端发送了最后一个ACK包后进入此状态,服务端接收到ACK包后进入此状态。

  • FIN_WAIT_1:终止连接的一方(通常是客户机)发送了FIN报文后进入。等待对方FIN。

  • CLOSE_WAIT:(假设服务器)接收到客户机FIN包之后等待关闭的阶段。在接收到对方的FIN包之后,自然是需要立即回复ACK包的,表示已经知道断开请求。但是本方是否立即断开连接(发送FIN包)取决于是否还有数据需要发送给客户端,若有,则在发送FIN包之前均为此状态。

  • FIN_WAIT_2:此时是半连接状态,即有一方要求关闭连接,等待另一方关闭。客户端接收到服务器的ACK包,但并没有立即接收到服务端的FIN包,进入FIN_WAIT_2状态。

  • LAST_ACK:服务端发动最后的FIN包,等待最后的客户端ACK响应,进入此状态。

  • TIME_WAIT:客户端收到服务端的FIN包,并立即发出ACK包做最后的确认,在此之后的2MSL时间称为TIME_WAIT状态。

四. 网络编程

1. IO多路复用技术

select和epoll都是IO多路复用技术的一种。
先说IO多路复用吧,在传统的网络编程中,socket的readwrite调用都是同步阻塞的,即调用之后,如果内核的缓冲区没有处理完,当前线程就会被阻塞住,无法响应其他的调用。这显然是动辄上万连接的服务器所无法接受的,于是早期的服务器都是采用多线程的方式,一个线程持有一个socket,来响应连接。这样显然也是靠不住的,在连接增加之后,线程调度本身所占用的CPU资源就变得非常巨大,这是不可忍受的。然后就有了IO多路复用技术,来让单线程也能有效的处理多个SOCKET的读写请求。
原理也很简单,就是内核提供这么一个接口,你可以告诉他你关心哪些socket,然后由他轮询这些socket,一旦其中有一个或者多个socket变得可用(可读或者可写),就返回一个队列,告诉你这些是可以用的,你一个一个处理吧。
当然这时候socket还是同步的,如果你不设置成非阻塞模式,那他也还是会阻塞你的调用,但我们不用担心:第一是因为我们可以轻易地把socket设置成非阻塞模式,这样就算缓冲区没有内容他也不会阻塞我的线程,其次既然是被返回说可以用了,那就说明还是有点东西的。这样线程可以一直不停地查询哪些socket是可以用的,有可以用的就可以处理一下,处理完继续监听;而不会被扯淡地阻塞在一个read调用上脱不开身,达到CPU的高效利用。
select就是这个机制的一种实现,它提供了三个传入参数,可以让你告诉他哪些socket是你要轮询的,每个传入参数都用一个FDSET,也就是socketfd的位掩码来表示,返回值也是用这个东西。然后它的内部会把你传进来的这个fd放到一个双向列表里,接着CPU就会真的去轮询这些socketfd的状态,轮询完给你返回。

2. epoll的原理,相对于select的优势,select有没有相对于epoll的优势场景?

epoll原理简述

epoll提供了三个接口,epoll_create, epoll_ctl, epoll_wait。
epoll_create会创建一个epoll的描述符,本质也是一个fd,(你会发现linux到处都是fd),表示我要创建一个集合啦,被加进来的你一个也跑不了。
epoll_ctl是控制集合成员的增加,删除和变动,可以增加一个要监听的fd,也可以删除。其内部是维护了一颗红黑树来表示这个集合,以支持快速的增加、查询、删除。如果你要增加一个fd进去,除了这个fd会被加到这颗红黑树上之外,还会把要监听的事件放到网络栈的回调中,也就是说,当有读写事件发生的时候,会主动通知到epoll,而不需要CPU像select那样傻傻的轮询了。
epoll_wait就是等待相应集合中的事件发生,有事件发生的话,对应的fd会被挂到一个双向链表上,然后挂到作为参数传入的数组中,这个数组指针已经被mmap过了,所以又免除了一次复制。
然后你就可以开心的处理这些准备好的socket了。

epoll相对于select的优势

  1. 没有select只能支持2048个文件描述符的限制。
  2. 拆分3个API,分开处理文件描述符集合的增删改查和等待时间的过程,免除了每次调用都要拷贝一次文件描述符到内核中的开销。
  3. 使用红黑树处理文件描述符,比位映射这种线性表结构复杂度低。
  4. 返回时的mmap,使得返回事件时内核和用户空间的拷贝开销也降低了很多。

epoll在哪些场景比select差

  1. 在文件描述符数量比较少的情况下,红黑树和双向链表这种结构就显得比较重了。
  2. 在文件描述符活跃概率比较高的情况下,回调机制反而可能不如select的轮询来的快。

3. epoll ET和LT的区别?ET模式如何使用?

ET: 边缘触发,如果有事件来,仅在由无到有的瞬间会通知用户,如果这次没有处理,不会再额外通知。优点:效率高;缺点:容易丢失这次事件。
LT:水平触发,只要文件描述符是就绪状态就会一直通知用户。优点:容易操作;缺点:效率相比ET低一些,但一般也够用。
ET模式的使用:
为了防止事件丢失,就要把read放在循环里调用,就像这样:

int len = 0;
while(len >= 0 && errno != E_WOULDBLOCK) {
  len = read(fd, buf, len);
}

一直读到出错为止。

你可能感兴趣的:(TCP和网络编程相关问题)