轻松搞定技术面试中常见的网络通信问题

LIZHAO 编程技术
阅读量 0 评论量 0

TCP/IP 详解

TCP/IP 协议栈

什么是TCP:

TCP 是 面向连接的、可靠的、基于字节流的传输层通信协议。

什么是TCP连接:

简单来说就是, 用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。

三次握手

需要了解三次握手的过程,以及TCP状态迁移图。

三次握手过程(Client 主动与 Server发起连接):

  1. client端,发送SYN同步标志。
      告诉server想要与其建立TCP连接,此时client的TCP状态为 SYNC_SENT
  2. server端,回复ACK,并发送SYN标志,TCP状态为SYNC_RCVD
      server端在收到client端的同步标志SYN后,确认client发送能力OK
      同时发起SYN,表明可以建立连接。
  3. client端,收到sevrer端的SYN,回复ACK。
      client端收到server端的SYN,以及ACK后,确认server端的发送能力以及接收能力OK。(Client:TCP状态 ESTABLISHED)
      server端在收到客户端的ACK后,确认其client的接收能力OK。(Server:TCP状态 ESTABLISHED)

这里有个问题,为什么不能是2次握手呢?

比较片面的理解:TCP是双向的,既然是双向的那么就必须确保通信的双方的收发能力都OK。

假设没有最后的一次ACK答复,那么server端并不知道client端的接收能力是否OK,所有需要3次握手,确保双方的收发能力都OK。

  1. 首要原因是为了防止旧的重复连接初始化造成混乱.
    在网络拥堵的情况下,客户端连续发送了两个建立连接的SYN报文。

如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:

  • 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是 RST 报文,以此中止历史连接;
  • 如果不是历史连接,则第三次发送的报文是 ACK 报文,通信双方就会成功建立连接;
  1. 需要通过3次握手来同步双方的初始化序列号
      当客户端发送初始化序列号的时候,服务端会回复ACK。
      那么当服务端发送初始化序列号时,自然客户端也需要回复服务端ACK。
      这样才能保证双方的序列号的同步。
  2. 避免资源浪费
      倘若只有2次握手,当我们参数超时重发的时候,可能建立多个连接,浪费资源

问题小结:TCP 建立连接时,通过三次握手 能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。

一次三次握手抓包图:

三次握手图解:

四次挥手

需要了解四次挥手的过程,以及TCP状态迁移图。

理解四次挥手每一次挥手后两端处于什么状态。

四次挥手过程(Client 主动关闭,Server被动关闭):

  1. client 发送FIN标志,并进入FIN_WAIT_1状态。
  2. server端收到客户端的FIN后,回复ACK,并进入CLOSE_WAIT状态,client端收到ACK后,进入FIN_WAIT_2状态(半关闭状态,可接收数据,不可发送数据)。
  3. 当server端数据处理完毕时,发送FIN给客户端,并进入LAST_ACK状态。
  4. client端收到FIN后,发送ACK,进入TIME_WAIT状态(等待2MSL后关闭),server端收到ACK后关闭。

图解四次挥手:

问:

  1. 为什么需要等待2MSL时间
      为了确保被动关闭方能够关闭,当最后一个ACK发出后,由于主动方并不知道被动方是否收到,当被动方发出FIN后但没有收到主动方的ACK,会触发FIN重传,所以等待2MSL,以便于再次接收FIN。
       TCP 就设计出了这么一个机制,经过 2MSL 这个时间, 足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
  2. 为什么需要四次挥手而不是三次
      在收到Client端的FIN后,服务端回复ACK后,仅代表客户端半关闭,此时还可以接收数据,倘若服务端在收到FIN后还需要进行一些处理,还需要给客户端发送数据,这时如果只有3次握手的话,服务端在回复ACK的同时就发送了FIN标志,这样的话服务端没法在客户端想要关闭的时候再处理数据。

TCP与UDP的适用场景

实际上就是考察TCP与UDP的区别。

区别:

  1. TCP是面向流的,UDP是面向数据报的。
  2. TCP是可靠的有序的,UDP是不可靠的。
  3. TCP通信前需要建立连接,UDP不需要建立连接。

其他问题:

  1. TCP是可靠的,为什么是可靠的?
  2. UDP可是实现数据的有序吗?

常见的网络模型

select、poll、epoll

  1. 如果网络出错的话,select会检测的到吗?
      不能,用来检测socke连接断开,当对端关闭时,select会有可读事件,但read返回0。
  2. send函数返回100,表明这100的数据已经到对方了吗。
      不是,表明send已经将数据从应用层数据发送到本地缓冲区,什么时候把数据再发出去时间上是操作系统的行为,或者说是网络协议栈的行为。
  3. TCP_NODELAY,用于是否开启Nagle算法,默认情况下,操作系统是不会将一个很小的数据包立刻发出去的,会放在缓冲区,当到达一定的大小
      的时候才一起发出去,使用TCP_NODELAY可以禁用该算法,将数据包立即发出去。
Nagle 算法 该算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到来之前,不能发送其他小分组。
  1. 用select 可实现异步connect

poll,和select 类似。

epoll(最高效的网络模型):

  1. 在监测socket 是否可读下,存在两种模式 ET、LT
      默认情况下,epoll采用的是LT。
      LT:只要socket上有数据可读,就会一直触发。
      ET:只有到socket上的数据从无到有的情况下,才会触发,也就是说,在ET模式下,如果你这个scoket上的数据你没有读完,
      那么当这个socket即便是再有数据来也不会触发可读事件,也就是说,在该模式下我们必须循环的去读数据,直到socket上的数据读完。
  2. 那么是否需要监测socket的可写,也存在这两种模式吗?
      理论上是有这两种模式存在的,但是我们一般不监测socket的可写,因为只要双方在正常的收发数据,那么socket就是一直可写的。
      一般情况下,只有当我们调用send或者write返回-1(不代表出错,也可能是TCP窗口太小了)时,才去监测socket的可写事件,并且一旦发送完成后
      就需要取消对该socket的可写事件的监听。

如何将socket设置为非阻塞的:

Linux:

  1. 创建socket时,可以设置为非阻塞的。
  2. 调用fcntl,可设置为非阻塞的。
  3. 可用accept4,设置为非阻塞的。

优雅关闭socket:

正常情况下,我们在关闭连接的情况下,我们不知道最后一个数据包是否被发出去。

  1. shutdown:
      可用shutdown,实现半关闭。
  2. socket选项:SO_LINGER
      等待数据发完后,再关闭socket。

错误码

系统API分为可重试的,和不可重试的,在调用API的过程中可能被信号中断。

EINTR

SIGPIPE、EPIPE 为什么需要屏蔽这两个信号:

当一个连接已经关闭了,再对连接调用send 或者write时,操作系统会发送这个信号。

如果不对这两个信号处理,那么就会导致进程退出。

gethostbyname:

函数默认是阻塞的,并且当函数出错时,没办法通过lasterror获取错误码,必须用herrpr()函数去获取错误码。

定时器的实现(实际上考察数据结构):

  1. 服务器端为什么需要定时器?
      服务器端可能产生大量的死链(在3次握手或者4次挥手过程中,对端关机啊什么的,处于TIME_WAIT 或CLOSE_WAIT),需要处理。
      链接保活机制,心跳包,服务器端需要定时的处理没有发心跳包的链接。
      还有一些定时任务需要处理。
  2. libevent,libuv(中的计时器)。

TCP如何解决丢包粘包问题:

首先TCP是可靠的,不存在丢包的问题。

粘包:由于TCP是流的协议,所以发送100字节的数据,收到数据的情况可能很多种。

  1. 通过特定的结束符
  2. 包头中添加包的长度(最广泛的使用)
  3. 规定每个包的长度。

断线重连机制的设计:

  1. 客户端发现自己断开,然后重连。
  2. 分布式服务中的下游服务重连上游服务。

什么时间重连是核心:

  1. 固定时间重连
  2. 使用依次时间增加的方式(断线后立即重连,还是连不上的话就 等待2s 4s 8s ……),但需要监听网络变化,一旦网络连通后,立即重连。

HTTP协议

HTTP请求头 和 请求Body 中间通过 \r\n分割

GET /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
User-Agent: Mozilla/5.0\r\n
\r\n



POST /index.php HTTP/1.1\r\n
Host: www.hootina.org\r\n
Connection: keep-alive\r\n
Cache-Control: max-age=0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\n
content-length: 8
User-Agent: Mozilla/5.0\r\n
\r\n
abcdefgh

GET、POST的区别:

GET的参数放在网址后面,POST的参数放在包体。

GET有长度的限制

keep-alive:只是建议服务器不要断开这个链接,但服务器不一定要接受你的建议。

喵~