网络协议

基础

网路 四层、七层模型

四层: 网络接口层、网络层、传输层、应用层

七层:物理层、数据链路层、网络层、传输层、表示层、会话层、应用层

DNS域名解析过程

域名 -> 浏览器缓存 -> 本机缓存 -> hosts文件 -> 缓存服务器 -> 根域名服务器 ->顶级域名服务器 -> 权威域名服务器

TCP

TCP 头格式

image-20230327202829578
  • 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
  • 确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
  • 控制位:
    • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1
    • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
    • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
    • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

序列号和确认号计算公式:

公式一:序列号 = 上一次发送的序列号 + len(数据长度)。特殊情况,如果上一次发送的报文是 SYN 报文或者 FIN 报文,则改为 上一次发送的序列号 + 1。
公式二:确认号 = 上一次收到的报文中的序列号 + len(数据长度)。特殊情况,如果收到的是 SYN 报文或者 FIN 报文,则改为上一次收到的报文中的序列号 + 1。

TCP UDP 区别

  • UDP

    • 是面向无连接的通讯协议,数据包括目的端口号和源端口号信息。
    • 优点:UDP速度快、操作简单、需要系统资源较少,由于通讯不需要连接,可以实现广播发送。
    • 缺点:UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,也不用重发,不可靠。
  • TCP

    • 是面向连接的通信协议,通过三次握手建立连接,通讯完成时四次挥手。
    • 优点:TCP在数据传递时,有确认、编号、重传、流量控制、拥塞控制等机制,能保证数据正确性,较可靠。
    • 缺点:TCP相对于UDP速度慢一点、要求系统资源较多。

流量控制

流量控制就是让发送方的发送速率不要太快,要让接收方来的及接收。

原理是通过确认报文中窗口字段来控制发送方的发送速率,发送方的发送窗口大小不能超过接收方给出窗口大小。

拥塞控制

防止过多的数据注入到网络中,这样可以使网络中的路由器或链路过载。如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。

拥塞算法

慢启动、拥塞避免算法、拥塞发生算法(超时重传、快速重传)、快速恢复算法

三次握手、四次挥手

image-20230327204638017

SYN:同步序列编号(Synchronize Sequence Numbers)

三次握手

  • 握手前(服务器端为listen状态):服务器端首先客户端和服务端要创建socket,并且服务端绑定端口变为listen状态
  • 第一次握手(客户端SYN_SEND状态):客户端发起SYN包到服务器,进入SYN_SEND状态,进入服务器端连接等候列队,等待服务器的确认;
  • 第二次握手(服务器端SYN_RCVD状态):如果服务器端已经建立连接列队有空余,服务端会像客户端发起第二次握手,服务器会先确认客户端发过来的SYN包,并 同时也返回一个SYN包并回复一个ACK,服务器端变为SYN_RCVD状态;
  • 第三次握手(ESTABLISHEDED状态):客户端接收到服务端返回的确认包后,向服务器发送确认包,待服务器手到ACK包后,双方建立了TCP连接,双方切换到ESTABLISHED状态

四次挥手

  • 第一次挥手(主动端为FIN_WAIT_1状态):主动端关闭socket发起关闭连接消息,并且发送FIN数据段给被动端,进入FIN_WAIT_1状态;
  • 第二次挥手(被动端为CLOSE_WAIT状态;主动端FIN_WAIIT_2状态):被动端收到FIN段后,会返回主动端一个确认ACK包,证明收到了主动端的关闭通知,并关闭读通道,进入CLOSE_WAIT状态,既还可以进行写数据; 主动端接收到ACK确保包后,会关闭写通道,进入FIN_WAIT_2状态;
  • 第三次挥手(被动端LAST_ACK状态):被动端发送完数据之后,会发送一个FIN数据段,表示写入完成,并进入LAST_ACK状态;
  • 第四次挥手(主动端TIME_WAIT状态):主动端收到消息后会再返回给被动端一条ACK确认段,并关闭读通道,进入TIME_WAIT状态; 被动端接收到ACK确认段后会关闭写通道;

Tcp 协议规定主动关闭乙方, 进入FIN_WAIT_2 -> TIME_WATE 状态, 必须等待2MSL。之后才能进行CLOSED。主要的目的是怕最后一个ACK包对方没有收到, 那么对方在超时后将重发第三次握手的FIN包。

什么是粘包和拆包?为什么会出现?

粘包就是两个或者多个以上的包粘在一起,拆包就是为什么解决粘包问题。

出现粘包原因有两个,一个是发送方发送不及时,导致多个包在发送端粘黏。另一个是接收方接受不及时,导致包在接收端堆叠导致。

怎么解决?

设计一个带包头的应用层报文结构就能解决。

  • 包头定长,以特定标志开头,里带着负载长度,这样接收侧只要以定长尝试读取包头,再按照包头里的负载长度读取负载就行了
  • 特殊字符作为边界
  • 自定义消息结构

TCP 的控制位

TCP 有多个控制位

  • SYNC是开启连接
  • FIN是结束连接(单向断开,优雅断开)
  • ACK是指回复包
  • RST是异常断开(双向都直接不能处理了)
  • PSH是类似于FLUSH,告诉对方可以将缓冲区的数据直接上报道应用层了(还是有序的)
  • URG(把这一个包上报应用层,数据包可能是无序的)

tcp 连接为什么是三次握手

  • 三次握手才可以同步双方的初始序列号
  • 三次握手才可以阻止重复历史连接的初始化(主要原因)
  • 三次握手才可以避免资源浪费, 服务端不确定客户端是否收到了ack。

tcp close 为什么需要四次挥手

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务端收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,因此是需要四次挥手。

当被动关闭方(上图的服务端)在 TCP 挥手过程中,「没有数据要发送」并且「开启了 TCP 延迟确认机制」,那么第二和第三次挥手就会合并传输,这样就出现了三次挥手。

为什么需要 TIME_WAIT 状态

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收(网络数据包延迟到达);
  • 保证「被动关闭连接」的一方,能被正确的关闭;

TIME WAIT 过多的危害

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。

客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。

如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP+ 目的 PORT」都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。具体可以看我这篇文章:客户端的端口可以重复使用吗?(opens new window)

因此,客户端(发起连接方)都是和「目的 IP+ 目的 PORT 」都一样的服务端建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP+ 目的 PORT」都一样的服务端建立连接了。

不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。

如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接

如果已经建立了连接,但是服务端的进程崩溃会发生什么?

TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。

我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况,发现**在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手**。

TCP重传机制

超时重传

重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传

TCP 会在以下两种情况发生超时重传:

  • 数据包丢失
  • 确认应答丢失

其中, RTO(超时重传时间), 是由TCP动态计算出来的

快速重传

TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传

image-20230327213817796

在上图,发送方发出了 1,2,3,4,5 份数据:

  • 第一份 Seq1 先送到了,于是就 Ack 回 2;
  • 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
  • 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
  • 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
  • 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

SACK 方法

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment), 选择性确认

这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

TCP 滑动窗口

我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。

那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:

image-20230327214213135

图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答

窗口大小由哪一方确定

TCP 头里有一个字段叫 Window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

所以,通常窗口的大小是由接收方的窗口大小来决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

程序如何表示发送方窗口

image-20230327214631377
  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNASend Unacknoleged):是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

那么可用窗口大小的计算就可以是:

可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)

接收方的滑动窗口

image-20230327214842113

其中三个接收部分,使用两个指针进行划分:

  • RCV.WND:表示接收窗口的大小,它会通告给发送方。
  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量,就可以指向 #4 的第一个字节了。

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。

TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

image-20230327220925169

根据上图的流量控制,说明下每个过程:

  1. 客户端发送 140 字节数据后,可用窗口变为 220 (360 - 140)。
  2. 服务端收到 140 字节数据,但是服务端非常繁忙,应用进程只读取了 40 个字节,还有 100 字节占用着缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通告给客户端。
  3. 客户端收到确认和窗口通告报文后,发送窗口减少为 260。
  4. 客户端发送 180 字节数据,此时可用窗口减少到 80。
  5. 服务端收到 180 字节数据,但是应用程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并在发送确认信息时,通过窗口大小给客户端。
  6. 客户端收到确认和窗口通告报文后,发送窗口减少为 80。
  7. 客户端发送 80 字节数据后,可用窗口耗尽。
  8. 服务端收到 80 字节数据,但是应用程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端。
  9. 客户端收到确认和窗口通告报文后,发送窗口减少为 0。

可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变。

拥塞控制

前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。

一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

为了在「发送方」调节所要发送数据的量,定义了一个叫做「拥塞窗口」的概念。

拥塞窗口

拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

那么怎么知道当前网络是否出现了拥塞呢?

其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。

拥塞控制主要是四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

慢启动

TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?

慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1(MSS)。

那慢启动涨到什么时候是个头呢?

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

  • cwnd < ssthresh 时,使用慢启动算法。
  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

拥塞避免算法

前面说道,当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。

一般来说 ssthresh 的大小是 65535 字节。

那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

拥塞避免算法的变化过程如下图:

image-20230327222023353

就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。

拥塞发生

当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:

  • 超时重传
  • 快速重传

这两种使用的拥塞发送算法是不同的,接下来分别来说说。

发生超时重传的拥塞发生算法

当发生了「超时重传」,则就会使用拥塞发生算法。

这个时候,ssthresh 和 cwnd 的值会发生变化:

  • ssthresh 设为 cwnd/2
  • cwnd 重置为 1 (是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)

拥塞发生算法的变化如下图:

image-20230327222446503

接着,就重新开始慢启动,慢启动是会突然减少数据流的。这真是一旦「超时重传」,马上回到解放前。但是这种方式太激进了,反应也很强烈,会造成网络卡顿。

发生快速重传的拥塞发生算法

还有更好的方式,前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthreshcwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
  • 进入快速恢复算法

快速恢复算法

快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。

正如前面所说,进入快速恢复之前,cwndssthresh 已被更新了:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;

然后,进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

快速恢复算法的变化过程如下图:

image-20230327223112630

也就是没有像「超时重传」一夜回到解放前,而是还在比较高的值,后续呈线性增长。

如何优化TCP 协议

优化三次握手的性能

  1. 调整SYN报文重传次数
  2. 调整SYN半连接队列长度
  3. 调整SYN+ACK报文里重传次数
  4. 调整accept队列长度
  5. 绕过三次握手

TCP四次挥手性能提升

  1. 调整FIN报文重传次数
  2. 调整FIN_WAIT2状态的时间
  3. 调整TIME_WAIT状态上限个数
  4. 复用TIME_WAIT状态的连接

TCP 传输性能提升

  1. 扩大窗口大小
  2. 调整发送缓冲区范围
  3. 调整接收缓冲区范围
  4. 接收缓冲区动态调节
  5. 调整内存范围

HTTP

浏览器输入url 发生了什么过程

<1> 客户端发送之前的准备工作
1)查询域名对应的ip (1)先查浏览器自身的缓存. (2)再查hosts文件. (3)查DNS服务器.
2)浏览器会构造出一个HTTP请求. 这个HTTP请求中就包含了刚才的这个域名信息(用户输入的URL信息).
3)与服务端进行三次握手建立连接
4)建立连接之后再进行数据传输.
5)数据经过传输层、网络层、数据链路层、物理层 层层封装转化

<2>数据传输路途中间的转发过程

电信号沿着路由器进行传播

<3>数据到达接收方
1)数据报经过物理层、数据链路层、网络层,层层解析,最后将数据放入到缓冲区
2)服务端进程从缓冲区获取到协议数据内容, 解析路径和参数,并进行处理
3)封装数据重新走数据发送步骤
4)客户端收到数据报,解析后将数据传递给浏览器, 浏览器解析协议后,渲染具体的响应内容

https 请求过程

1)客户端向服务器发起https的请求,连接到服务器的443端口;

2)服务器将非对称加密的公钥以证书的形式回传给客户端,证书⾥面包含了网站地址,加密公钥,以及证书的颁发机构。

3).客户端接受到该公钥后进行验证,就是验证步骤2中服务器返回的证书,如果有问题,则HTTPS请求无法继续;如果没有问题,则上述公钥是合格的(第一次HTTP请求)。

4)客户端生成一个随机数(对称加密公钥),并将随机数使用公钥加密发送给服务端

5)服务端使用私钥进行解密,获取到随机数

6)服务端将对称加密后的数据传递给客户端,客户端收到后使用对称解密, 得到服务端的数据,完成二次http请求

http 1.0/ 1.1/ 2.0 区别

HTTP/1.1 相比 HTTP/1.0 性能上的改进:

  • 使用长连接的方式改善了 HTTP/1.0 短连接造成的性能开销。
  • 支持管道(pipeline)网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。

HTTP/2 做的优化

  • 头部压缩

    HTTP 2.0使用 encoder 来减少需要传输的 header 大小,通讯双方各自 cache 一份 header fields 表,既避免了重复 header 的传输,又减小了需要传输的大小。

  • 数据格式

    HTTP/2 不再像 HTTP/1.1 里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(Headers Frame)和数据帧(Data Frame)

  • 多路复用

    我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。

    http/2 做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.0大了好几个数量级。

  • 服务端推送

TCP Keep Alive 与 HTTP Keep-Alive 比较

TCP的keepalive是侧重在保持客户端和服务端的连接,一方会不定期发送心跳包给另一方,没有断掉一方的定时发送几次心跳包。如果间隔发送几次,对方都返回的是RST,而不是ACK,那么就释放当前连接。

HTTP的keep-alive一般我们都会带上中间的横杠,普通的HTTP连接是客户端连接上服务端,然后结束请求后,由客户端或者服务端进行http连接的关闭。下次再发送请求的时候,客户端再发起一个连接,传送数据,关闭连接。这么个流程反复。但是一旦客户端发送connection: keep-alive头给服务端,且服务端也接受这个keep-alive的话,这个连接就可以复用了。一个HTTP处理完之后,另外一个HTTP数据包也直接从这个连接发送。减少新建和断开TCP连接的消耗。

二者的作用简单来说:

HTTP协议的keep-alive意图在于短时间内连接复用,希望可以短时间内在同一个连接上进行多次请求/响应。

TCP的KeepAlive机制意图在于保活、心跳、检测连接错误。当一个TCP连接两端长时间没有数据传输时(通常默认配置是2小时),发送keepalive探针,探测链接是否存活。

CSRF 和 XSS的区别与解决

XSS:跨站脚本攻击

提交内容中,包含不速之客(影响页面功能的js脚本)

其特点是不对服务器端造成任何伤害,而是通过一些正常的站内交互途径,例如发布评论,提交含有 JavaScript 的内容文本。这时服务器端如果没有过滤或转义掉这些脚本,作为内容发布到了页面上,其他用户访问这个页面的时候就会运行这些脚本。

怎么解决

  1. 输入:在输入方面对所有用户提交内容进行可靠的输入验证,提交内容包括URL、查询关键字、http头、post数据等
  2. 输出:html 编码,js编码,在输出方面,在用户输内容中使用标签。标签内的内容不会解释,直接显示。
  3. 严格执行字符输入字数控制。
  4. http only cookie, 防止脚本获取cookie;

CSRF:跨站请求伪造

冒充用户之手,伪造危险请求

CSRF 的全称是“跨站请求伪造”,而 XSS 的全称是“跨站脚本”。看起来有点相似,它们都是属于跨站攻击——不攻击服务器端而攻击正常访问网站的用户,但前面说了,它们的攻击类型是不同维度上的分类。CSRF 顾名思义,是伪造请求,冒充用户在站内的正常操作。我们知道,绝大多数网站是通过 cookie 等方式辨识用户身份(包括使用服务器端 Session 的网站,因为 Session ID 也是大多保存在 cookie 里面的),再予以授权的。所以要伪造用户的正常操作,最好的方法是通过 XSS 或链接欺骗等途径,让用户在本机(即拥有身份 cookie 的浏览器端)发起用户所不知道的请求。

严格意义上来说,CSRF 不能分类为注入攻击,因为 CSRF 的实现途径远远不止 XSS 注入这一条。通过 XSS 来实现 CSRF 易如反掌,但对于设计不佳的网站,一条正常的链接都能造成 CSRF。

怎么解决

  1. referer校验
  2. 增加隐藏 csrf_token令牌, 但是可以被破解
  3. 重要场景增加验证码

Http 的缓存技术

  • 强制缓存: 

    强缓存指的是只要浏览器判断缓存没有过期,则直接使用浏览器的本地缓存,决定是否使用缓存的主动性在于浏览器这边。 

    返回的是 200 状态码,但在 size 项中标识的是 from disk cache,就是使用了强制缓存

    强缓存是利用下面这两个 HTTP 响应头部(Response Header)字段实现的,它们都用来表示资源在客户端缓存的有效期:

    • Cache-Control, 是一个相对时间;
    • Expires,是一个绝对时间;

    如果 HTTP 响应头部同时有 Cache-Control 和 Expires 字段的话,Cache-Control 的优先级高于 Expires

  • 协商缓存:

    当我们在浏览器使用开发者工具的时候,你可能会看到过某些请求的响应码是 304,这个是告诉浏览器可以使用本地缓存的资源,通常这种通过服务端告知客户端是否可以使用缓存的方式被称为协商缓存。

    协商缓存可以基于两种头部来实现。

    第一种:请求头部中的 If-Modified-Since 字段与响应头部中的 Last-Modified 字段实现,这两个字段的意思是:

    • 响应头部中的 Last-Modified:标示这个响应资源的最后修改时间;
    • 请求头部中的 If-Modified-Since:当资源过期了,发现响应头中具有 Last-Modified 声明,则再次发起请求的时候带上 Last-Modified 的时间,服务器收到请求后发现有 If-Modified-Since 则与被请求资源的最后修改时间进行对比(Last-Modified),如果最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;如果最后修改时间较旧(小),说明资源无新修改,响应 HTTP 304 走缓存。

    第二种:请求头部中的 If-None-Match 字段与响应头部中的 ETag 字段,这两个字段的意思是:

    • 响应头部中 Etag:唯一标识响应资源;
    • 请求头部中的 If-None-Match:当资源过期时,浏览器发现响应头里有 Etag,则再次向服务器发起请求时,会将请求头 If-None-Match 值设置为 Etag 的值。服务器收到请求后进行比对,如果资源没有变化返回 304,如果资源变化了返回 200。

    如果在第一次请求资源的时候,服务端返回的 HTTP 响应头部同时有 Etag 和 Last-Modified 字段,那么客户端再下一次请求的时候,如果带上了 ETag 和 Last-Modified 字段信息给服务端,这时 Etag 的优先级更高,也就是服务端先会判断 Etag 是否变化了,如果 Etag 有变化就不用在判断 Last-Modified 了,如果 Etag 没有变化,然后再看 Last-Modified。

    注意,协商缓存这两个字段都需要配合强制缓存中 Cache-Control 字段来使用,只有在未能命中强制缓存的时候,才能发起带有协商缓存字段的请求。 总结下来就是如果response 中有 last-modified or etag , 就使用协商缓存, 否则走强制缓存.

​ 总结: response: Cache-Control,Last-Modified/ Etag; request: If-Modified-Since/If-None-Match,

HTTP与RPC的区别

  • 服务发现

    • http采用的是DNS服务的方式实现的
    • rpc有专门的中间服务去保存服务名和IP信息,比如 Consul 或者 Etcd,甚至是 Redis
  • 序列化方式:

    • RPC服务序列化是针对二进制协议(0/1)来做序列化和反序列化,所以性能高。
    • Http服务是基于文本的序列化和反序列化,需要读一行一行的文本(比如json格式的),进行序列化和反序列化,所以性能低。
  • 报文长度:

    • RPC服务是自定义的传输协议,传输的报文都是干货。
    • Http服务里面包括很多没用的报文内容,比如Http Header里面的accept,referer等等

TCP 与 RPC 的总结

  • 纯裸 TCP 是能收发数据,但它是个无边界的数据流,上层需要定义消息格式用于定义消息边界。于是就有了各种协议,HTTP 和各类 RPC 协议就是在 TCP 之上定义的应用层协议。
  • RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 Thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,不一定非得基于 TCP 协议
  • 从发展历史来说,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。
  • RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP/1.1 性能要更好,所以大部分公司内部都还在使用 RPC。
  • HTTP/2.0HTTP/1.1 的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。

跨域的常见解决办法

  1. 使用 ajax 或者 jquery 的jsonp

  2. 服务端添加cors响应头

    1. 需要注意当需要进行cookie验证的场景, 服务端需要另外设置, credentials响应头, web端需要将withCredentials设置为true

    2. 响应头示例:

      header(‘Access-Control-Allow-Origin:’.$_SERVER[‘HTTP_ORIGIN’]);

      header(“Access-Control-Allow-Credentials:true”);

      header(“Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept,Authorization”);

      header(‘Access-Control-Allow-Methods:GET,POST,PUT,DELETE,OPTIONS,PATCH’);

关于为什么要有websocket 协议的问题

怎么样让服务器主动给客户端发送消息

  • http短轮训: 客户端不断的请求, 去尝试获取数据
  • http长轮训: 使用超时时间很长的请求, 去获取数据, 如果服务端数据未到达, 会等到数据到达之后, 在返回响应
  • websocket(全双工): 我们知道 TCP 连接的两端,同一时间里双方都可以主动向对方发送数据。这就是所谓的全双工

怎么建立websocket 连接

我们平时刷网页,一般都是在浏览器上刷的,一会刷刷图文,这时候用的是 HTTP 协议,一会打开网页游戏,这时候就得切换成我们新介绍的 WebSocket 协议

为了兼容这些使用场景。浏览器在 TCP 三次握手建立连接之后,都统一使用 HTTP 协议先进行一次通信。

  • 如果此时是普通的 HTTP 请求,那后续双方就还是老样子继续用普通 HTTP 协议进行交互,这点没啥疑问。
  • 如果这时候是想建立 WebSocket 连接,就会在 HTTP 请求里带上一些特殊的header 头,如下:
1
2
3
Connection: Upgrade
Upgrade: WebSocket
Sec-WebSocket-Key: T2a6wZlAwhgQNqruZ2YUyg==\r\n

这些 header 头的意思是,浏览器想升级协议(Connection: Upgrade),并且想升级成 WebSocket 协议(Upgrade: WebSocket)。同时带上一段随机生成的 base64 码(Sec-WebSocket-Key),发给服务器。

如果服务器正好支持升级成 WebSocket 协议。就会走 WebSocket 握手流程,同时根据客户端生成的 base64 码,用某个公开的算法变成另一段字符串,放在 HTTP 响应的 Sec-WebSocket-Accept 头里,同时带上101状态码,发回给浏览器。HTTP 的响应如下:

1
2
3
4
HTTP/1.1 101 Switching Protocols\r\n
Sec-WebSocket-Accept: iBJKv/ALIW2DobfoA4dmr3JHBCY=\r\n
Upgrade: WebSocket\r\n
Connection: Upgrade\r\n

image-20230323213511929

就这样经历了一来一回两次 HTTP 握手,WebSocket就建立完成了,后续双方就可以使用 webscoket 的数据格式进行通信了。

你在网上可能会看到一种说法:”WebSocket 是基于HTTP的新协议”,其实这并不对,因为WebSocket只有在建立连接时才用到了HTTP,升级完成之后就跟HTTP没有任何关系了

websocket 格式和字段

image-20230323214214242

数据包在WebSocket中被叫做,我们来看下它的数据格式长什么样子。

1. opcode字段

这个是用来标志这是个什么类型的数据帧。比如。

  • 等于 1 ,是指text类型(string)的数据包
  • 等于 2 ,是二进制数据类型([]byte)的数据包
  • 等于 8 ,是关闭连接的信号
2. payload字段

存放的是我们真正想要传输的数据的长度,单位是字节。比如你要发送的数据是字符串"111",那它的长度就是3

3. payload data字段

这里存放的就是真正要传输的数据,在知道了上面的payload长度后,就可以根据这个值去截取对应的数据。

WebSocket的数据格式也是数据头(内含payload长度) + payload data 的形式。

刘小恺(Kyle) wechat
如有疑问可联系博主