Back

网络

计算机网络、HTTP协议、TCP协议相关

[TOC]

基本

OSI七层模型 对应网络协议 作用
应用层 HTTP、TFTP、FTP、NFS、SMTP、Telnet 应用程序间通信的网络协议
表示层 Rlogin、SNMP、Gopher 数据格式化、加密、解密
会话层 SMTP、DNS 建立、维护、管理会话连接
传输层 TCP、UDP 建立、维护、管理端到端连接
网络层 IP、ICMP、ARP、RARP、AKP、UUCP IP寻址和路由选择
数据链路层 FDDI、Ethernet、Arpanet、PDN、SLIP、PPP 控制网络层与物理层间的通信
物理层 IEEE 802.1A、IEEE 802.2到802.11 比特流传输

数据链路层:

  • 数据包叫Frame,“帧”;
  • 由两部分组成:标头和数据,标头标明数据发送者、接收者、数据类型;
  • 用MAC地址定位数据包路径;
  • 相关设备是交换机;

网络层:

  • 数据包叫packet,“包”;

  • IPv4:32个二进制,4字节*8位;IPv6:1同一子网28个二进制,8字节*16位;

  • 子网掩码与IP的and运算判断是否为同一子网下;

  • 路由:把数据从原地址转发到目标地址,同一局域网内,通过广播的方式找到,不同局域网内,原主机先将包根据网关添加路由器/主机地址,通过交换机的广播方式发给目标主机,原主机将数据包传输给目标主机,再由目标主机根据MAC广播交给对应目标

  • ping ip,使用ICMP协议可以确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置;

  • ARP协议:IP与MAC地址的映射,ARP会在以太网中以广播的形式,获取IP对应的MAC地址;

    仅限IPv4,IPv6使用 Neighbor Discovery Protocol替代;

  • 相关设备是路由器,网关

传输层:

  • 数据包叫segment,“段”;

  • Socket、UDP、TCP见下

应用层使用TCP传输数据时,会先将数据打到TCP的Segment中,然后TCP的Segment会打到IP的Packet中,然后再打到以太网Ethernet的Frame中,传输到目标主机后,再一层层解析,往上传递。

从浏览器输入URL之后都发生了什么

浏览器输入URL,按回车,

  1. 浏览器根据输入内容,匹配对应的URL和关键词,校验URL的合法性,从历史记录、书签等地方,找到可能对应的URL,进行补全,使其符合通用URI的语法。

  2. 发起请求,从URL中解析出域名,首先查看本地hosts文件,判断是否有这个域名对应的ip,如果没有,请求本地DNS服务器,先查询本地DNS服务的缓存,如果没有再向上级DNS服务器发送请求,递归查询,直到找到对应的IP地址,然后本地DNS服务器就把对应关系保存在缓存中

    注意根DNS服务器没有记录具体的域名和IP的对应关系,而是告诉本地DNS服务器可以到域服务器上继续查询,给出的是域服务器的地址。

  3. 拿到域名对应的IP后,应用层程序准备好数据后,委托给操作系统,复制应用层数据到内核的内存空间中,交给网络协议栈(将其打包为tcp包(传输层),帧(数据链路层),并将数据从内核拷贝到网卡,后续由网卡负责数据的发送),与Web服务器建立 TCP/IP 连接(三次握手具体过程)。

  4. 建立连接后,发起一个HTTP 请求,经过路由器的转发,通过Web服务器(CDN、反向代理之类的)的防火墙,该 HTTP 请求到达了Web服务器。

  5. 服务器处理该 HTTP 请求,返回一个 HTML 文件。

  6. 浏览器解析该 HTML 文件,解析HTML文件后,构建dom树 -》构建render树 -》布局render树 -》绘制 render树,自上而下加载,边加载边解析渲染,显示在浏览器端,对于图片音频等则是异步加载。

本质上是OSI七层模型 + 相应协议、组件实现;建立一次TCP后,在HTTP 1.1请求头配置keep-alive=true后,默认保持两小时的连接,可以一直进行http请求,但同时只处理一个http请求,HTTP 2.0才允许并行多个;

HTTP方法

菜鸟HTTP教程/HTTP请求方法

Get和Post的区别

  • 语义上的区别,Get一般表示查询、获取,Post是更新,创建

  • Get具有幂等性,Post没有

  • 参数传递方面,Get一般参数接在Url上,对外暴露,有长度限制(1024个字节即256个字符),只接收ASCII字符,需要进行url编码

    Post参数放在request body里,支持多种编码

  • GET请求会被浏览器主动cache,而POST不会,除非手动设置

  • GET产生的URL地址可以加入书签,而POST不可以

  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留

  • GET在浏览器回退时是无害的,而POST会再次提交请求

其实本质都是一种协议的规范,规定参数的存放位置,参数长度大小等,当然也可以反着来,只要服务器能够理解即可

幂等性:同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的,每次返回的结果一样,不产生副作用;

根据语义,简单的把get看成查询,只要服务器的数据没变,每次查询得到的结果是一样的,而把post看成添加,每次post请求都会创建新资源,服务器状态改变

具有幂等性的方法:GET、HEAD、OPTIONS、DELETE、PUT

没有幂等性的方法:POST

安全性:安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。

常见状态码

参考HTTP状态码

  • 301:永久移动请求的网页已永久移动到新位置,即永久重定向;返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替,新的URI会放在响应header的Location字段中;搜索引擎会抓取新内容同时也将旧地址修改为新地址。301调整默认会被浏览器cache,用户后续多次访问该url时会,浏览器会直接请求跳转地址。
  • 302:临时移动请求的网页暂时跳转到其他页面,即暂时重定向;旧地址的资源还在,只是重定向到临时的新地址中,对SEO有利,搜索引擎会抓取新内容保存旧地址。如果在响应头中通过Cache-Control或Expires,也可以实现301中浏览器缓冲的效果。
  • 405:请求的Method被禁止,比如Post的接口用成了Get
  • 502:Bad Gateway,作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
  • 503:Service Unavailable,服务不可用
  • 504:Gateway Time-out,充当网关或代理的服务器,未及时从上游服务器收到请求

HTTP头

Connection: keep-alive

保持长连接,连接复用,避免频繁建立连接带来的性能损耗。HTTP1.0默认是关闭的,HTTP1.1默认是开启的。对应的key是Connection。可以设置参数Keep-Alive: max=5, timeout=120表示一次keep-alive可以发送的请求次数是5,连接保持时间是120s,默认是2小时,可以一直进行HTTP请求,但一个TCP连接同一时间只支持一个HTTP请求,即一次连接中,请求是一个个处理的,如果前一个没处理完,没办法处理下一个,也就是下一个不返回。

不同的浏览器对同一Host建立的TCP连接数量的限制取决于浏览器本身,像Chrome最多允许同时建立6个TCP连接,假如一个HTML页面有很多图片需要加载,且这些图片都是同一Host,且HTTPs,那浏览器在TLS 后会和服务器确认是否启用HTTP2,如果启用就可以同一连接下同时下载多个,如果不启用,就仍然用那几个连接,排队去等了。

另外多个HTTP连接,是可以建立在一个TCP连接上的。

与TCP的keep-alive不同:

  • TCP 的 keep-alive 是由操作系统内核来控制,存在于内核态,通过 keep-alive 报文来防止 TCP 连接被对端、防火墙或其他中间设备意外中断,主要的作用是保活,和上层应用没有任何关系,只负责维护单个 TCP 连接的状态,其上层应用可以复用该 TCP 长连接,也可以关闭该 TCP 长连接。
  • HTTP 的 keep-alive是由应用层控制,存在于用户态, 机制则是和自己的业务密切相关的,浏览器通过头部告知服务器要复用这个 TCP 连接,请不要随意关闭。只有到了 keep-alive 头部规定的 timeout 才会关闭该 TCP 连接,不过这具体依赖应用服务器,应用服务器也可以根据自己的设置在响应后主动关闭这个 TCP 连接,只要在响应的时候携带 Connection: Close 告知对方

但是可见,两种keep-alive都是为了连接复用

Content-Length

表示本次响应的数据长度,后面的字节就属于下一个响应的了,因为HTTP是基于TCP进行通信的,应用层需要解决粘包的问题,HTTP为了解决这个问题,有两种解决方案:1. 设置Content-Length作为响应的边界;2. 响应体设置回车符、换行符作为响应的边界,常见于Transfer-Encoding=chunked

Upgrade

一般是用于协议升级,比如WebSocket,浏览器与服务器使用WebSocket通信时,会先通过普通的HTTP建立连接,在请求头里带上Upgrade: WebSocket; Connection: Upgrade; Sec-WebSocket-Key: 随机生成的base64码,发送给服务器。

如果服务器支持WebSocket协议,就会走WebSocket握手流程,根据客户端生成的basae64码,使用公钥变成另一个字符串,放在Set-WebSocket-Accept响应头中,并带上101响应码,表示协议切换,要建立起WebSocket连接。

HTTP缓存

对于一些重复性的HTTP请求,比如每次请求得到的数据都是一样的,就可以把这对请求响应缓存在本地,从而提升性能。

强制缓存

由两个响应头字段实现:

  • Cache-Control: 值可以是max-age=x秒,或者 s-maxage=x秒(代理缓存,如CDN),或者public、private表示缓存是否共享,或者no-cache表明不缓存,或者no-store表示禁止缓存

  • Expires: 值可以是 max-age+请求时间,但需要配合Last-modified使用,或者直接是绝对时间,表示缓存什么时候到期

Cache-Control的优先级高于Expires

客户端接收到的响应头包括这两个字段中的一个时,在之后的时间里就不会去请求服务端获取数据,直接使用本地缓存,本地缓存一般是直接存在磁盘,响应码后面会直接标识 200(from dish cache)

协商缓存

客户端与服务端协商,根据结果判断是否使用本地缓存,有两种头部

  • 请求头中的If-Modified-Since,表示用来与响应资源比较,判断是否缓存是否被修改过,单位是秒配合响应头中的Last-Modified,表示响应资源的最后修改时间,单位是秒字段实现,如果缓存被修改过,返回HTTP 200,如果没被修改过,返回HTTP 304

  • 响应头中的ETag,表示缓存响应标识配合请求头中的If-None-Match,表示资源过期时,如果响应头里有Etag,则再次向服务器发起请求时,会设置If-None-Match的值位Etag,服务器会对比If-None-Match的值,如果有缓存有修改,返回304,否则返回200

    ETag的优先级高于Last-Modified,ETag可以解决那种文件内容不变,但文件修改时间改变的场景;

HTTPS

HTTPS主要解决HTTP的安全问题,如报文内容安全(加密解决),防篡改(签名解决),防冒充(CA认证解决)。

HTTPS = HTTP + SSL/TLS,SSL是介于HTTP之下TCP之上的协议层,提供 加密明文,验证身份,保证报文完整 的保障。TLS是升级版的SSL,作用类似,比SSL多了些其他功能,现在绝大多数浏览器都不支持SSL了,而是支持TLS。

HTTP 先和 TLS 通信,再由 TLS 和 TCP 通信。一般情况下,TLS需要先经过TCP三次握手,建立可靠连接之后,才能做TLS握手的事。

  • 加密

    使用 对称加密 加密 报文(私钥加密私钥解密)

    使用 非对称加密 加密 对称加密的密钥,保证该密钥的传输安全(客户端公钥加密,服务端私钥解密;或者 服务端私钥加密,客户端公钥解密)

  • 验证身份

    通过第三方(CA)发布TLS 证书,对通信方进行认证

    服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。

    所以这里涉及到两个公钥,一个是服务端用于 TLS 的公钥,一个是CA证书的公钥;

    所以,TLS 证书主要是表明了该域名归属,日期等信息,还包含了用于报文加密的公钥和私钥以及数字签名,TLS 证书被服务端持有

    进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了,通信时使用上述的加密机制保护报文;

    证书信任的方式:操作系统和浏览器内置;CA颁发;手动导入证书

  • 保护报文完整

    TLS 提供报文摘要功能(即签名)结合加密和认证来进行完整性保护

在HTTP的基础上,以TLS 1.2版本为例,RSA算法的握手流程:

  1. 客户端访问服务端的网页,首先经过浏览器内置的受信任的CA机构列表,查看该服务器是否向CA机构提供了证书;CA会对服务端提供的公钥和服务端的相关信息进行Hash,然后用CA的私钥进行签名,将签名的公钥和服务端提供的公钥和信息一起整成一份证书;

  2. 如果服务器证书中的信息与当前正在访问的网站(域名等)一致,那么浏览器就认为服务端是可信的,并从服务器证书中取得服务器公钥;

  3. 如果还没有取得服务器中的证书,则与服务器建立TCP连接,之后开始TLS的流程:

    1. 客户端向服务端发送请求,把自己支持的TLS版本,加密套件、一个随机数 发给服务端;
    2. 服务端收到请求后,确认客户端的TLS版本,加密套件,然后也生成一个随机数,与证书和公钥一起发给客户端;
    3. 客户端收到服务端的证书、公钥,需要验证该证书是否真实有效:先通过浏览器或操作系统内置的拿到CA证书,使用CA证书的公钥对CA证书进行解密,得到的CA证书的Hash值,然后与从服务端收到的证书的Hash值进行比较,判断是否一致,一致说明服务端可信;
    4. 客户端在验证服务端的证书是有效的之后,再生成一个随机数(也称预主密钥),使用刚刚收到的服务端公钥进行加密后发送出去;
    5. 服务端收到预主密钥后,用私钥进行解密,得到预主密钥的值;
    6. 最后,客户端用预主密钥,使用第1步和第2步的随机数计算出会话密钥;
  4. 建立会话密钥:客户端通过服务器公钥加密会话密钥发送给服务端,服务端用自己私钥解密得到会话密钥,用于接收和发送数据,之后传输的http数据都是经过加密的。

    每个会话会生成一个会话密钥,每个会话的会话密钥均不同。

总结:非对称加密的手段传递密钥,然后用密钥进行对称加密传递数据,对称加密的密钥由客户端生成,每次会话密钥都不一样,CA会保存服务端提供的非对称加密的公钥并签名;

过程中,客户端会产生两个随机数,第一个用于制作会话密钥(对报文做对称加密时使用),第二个用于验证公钥是否可用;服务端会生成一个随机数,给客户端制作会话密钥;

RSA非对称加密

例子:

公钥为(7,33)
假设源数据翻译成十进制为:3,1,15
对其求7次幂为:2187,1,170859375
对其求33的余:9,1,27
得到密文:9,1,27

私钥为(3,33)
得到密文:9,1,27
对其求3次幂为:729,1,19683
对其求33的余:3,1,15
得到明文:3,1,15

设公钥为(e,n),私钥为(d,n)
即 明文^e%n=密文,密文^d%n=明文

公钥和私钥的制作过程

  1. 生成两个质数:p和q
  2. 两个质数相乘:N = p * q
  3. 使用欧拉函数计算:T = (p-1) * (q-1)
  4. 选出公钥,条件:质数 && 1 < 公钥 < T,不是T的因子,即E
  5. 计算私钥,条件:(D * E)% T = 1

当p和q特别大时,生成的T和N都非常大,即使公开N,也很难暴力算出p和q,所以破解非常困难

HTTP 2.0

设备变好,内容形式多样,页面资源变多,实时性要求变高使得HTTP1.1延时变高,像在Chrome连接最大并发量是6个,且每一个连接都需要经过TCP和TLS握手耗时,HTTP1.1本身一个连接只能处理一个请求,才能继续处理,每次请求头部都巨大且重复,不支持服务端消息推送;

HTTP2.0建立在Https协议的基础上,支持二进制流而不是文本,支持多路复用而不是有序阻塞,支持数据压缩减少包大小,支持server push等特性,实现低延时,高性能。

头部压缩

http1.x的头带有大量信息,而且每次都要重复发送,字段是ASCII编码,效率较低。http/2使用HPACK算法来压缩header。

HPACK算法包含三个组成部分:静态字典、动态字典、哈夫曼编码(用于压缩);通过字典长度较小的索引表示对于的字段、再使用哈夫曼编码进行压缩,可高达50%~90%的压缩率。

静态字典表

存储高频的字段和对应的索引,比如一些常见的HTTP请求头字段和值以及对应的索引,索引从1开始自增,代表对应的字段;

HTTP2.0头部基于二进制编码(把索引值翻译成二进制数),不需要使用冒号、空格、或者\r\n作为分隔符,直接使用字符串长度来分割索引和值;

各自缓存之后,之后发送的请求如果不包含首部,就会自动使用之前请求发送的首部,如果首部发生变化,则只需将变化的部分加入到header帧中,改变的部分会加入到头部字段表中,首部表在HTTP2.0的连接存续期内始终存在,由客户端喝服务端共同渐进式更新。

动态字典表

静态表只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。

比如有新的不存在静态字典表的字段出现在双方的通信中,双方就会为这个新的字段更新一个对应的索引,记录到动态字典表中。动态字典表生效有一个前提是,必须在同一连接上,重复传输完成相同的HTTP头部,如果在一个连接上只发送了一次,或者重复传输时总是略有变化,动态字典表就无法充分利用了。

动态字典表会随着通信时间的累积,越变越大,占用的内存也越大,会影响服务器性能,因此一般服务器会提供类似http2_max_requests的配置,限制一个连接能传输的请求数量,避免动态字典表无限增大,当达到上限后,就会关闭连接来释放内存,等下次连接再重新建立动态字典表。

二进制分层帧

  • 帧类型有10种,分为数据帧和控制帧两大类;

  • 标志位用于携带简单的控制信息,比如END_HEADERS表示数据结束标志,相当于HTTP1.x里的空行或者/r/n;END_Stream表示单向数据发送结束,后续不会有数据帧;

  • 流标识符,用来标识帧属于哪个Stream,用于接收双方在乱序的帧里找到相同的StreamID,从而有序的组装信息;为了防止两端流ID冲突,客户端发起的流具有奇数ID,服务器端发起的流具有偶数ID;

    每个Stream是一个逻辑联系,一个独立的双向的frame存在于客户端和服务器端之间的HTTP2.0连接中。一个HTTP2.0连接上可包含多个并发打开的Stream,这个并发Stream的数量能够由客户端设置。

  • 帧数据:由HPACK算法压缩过的HTTP头和包体;

消息:一个完整的请求或响应,由一个或多个帧组成。

在二进制分帧层上,http2.0会将所有传输信息分割为更小的消息和帧,并对它们采用二进制格式的编码将其封装,新增的二进制分帧层同时也能够保证http的各种动词,方法,首部都不受影响,兼容上一代http标准。其中,http1.X中的首部信息header封装到Headers帧中,而request body将被封装到Data帧中。

多路复用

HTTP2.0通过多路复用实现并发传输,多个Stream复用同一条TCP连接,达到并发的效果,解决HTTP1.1队头阻塞问题,避免频繁建立TCP、TLS握手的时间,减少TCP慢启动对流量的影响,提高HTTP传输的吞吐量。

多个 Stream 跑在一条 TCP 连接,同一个 HTTP 请求与响应是跑在同一个 Stream 中,HTTP 消息可以由多个 Frame 构成, 一个 Frame 可以由多个 TCP 报文构成。不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的;即多个Stream传输时是并行交错的,但是同一个Stream的帧是有序的。

连接是持久的,客户端和服务器之间只需要一个连接,每个数据流可以拆分成很多不依赖的帧,这些帧可以乱序发送,也可以分优先级,多个流的数据包能够混合在一起通过同样的连接传输,服务端在根据不同帧首部的流标识进行区分和组装。

请求优先级

将HTTP消息分为很多独立帧之后,就可以通过优化这些帧的交错喝传输顺序进一步优化性能,服务端也可以根据流的优先级,优先将最高优先级的帧发送给客户端。

服务端推送

服务端可以对一个客户端请求发送多个响应,而无需客户端明确地请求,省去客户端重复请求的步骤。

服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

使用长连接需要考虑的点

  • 客户端和服务端的数量,因为要保持连接,如果客户端的数量远超服务端的数量,服务端与每个客户端都维持一个长连接,对服务端来说负担比较大,此时需要设置一个合理的超时时间,在空闲时间过长时断开连接,释放服务端资源;
  • 因为长连接的多路复用,连接一旦建立便不会断开,流量会被分到同一个服务端,会导致负载不均衡,需要连接池能分辨出服务端的多个实例,自己实现;

TCP(Transmission Control Protocol 传输控制协议)

特点

  • 面向连接的,提供可靠交付(只保证传输层可靠),丢包重传,有状态服务

  • 有流量控制,拥塞控制

  • 提供全双工通信

  • 面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块)

  • 头部20字节

  • 每一条 TCP 连接只能是点对点的(一对一)

  • 缺点:

    • TCP协议在内核中实现,升级起来比较麻烦,还需要client端和server端同时支持;
    • TCP建立连接有延时,启动时慢启动,拥塞避免,比如应用层HTTP要建立连接,需要先建立TCP连接,无法整合在一起;
    • TCP存在队头阻塞问题,因为要保证收到的字节数有序且完整,内核要保证收到连续的包才允许应用层读取,如果中间有一个包超时重传了,就会阻塞应用层;
    • IP变动,端口变动需要重新建立TCP连接,比如切换wifi和4G
  • 关于TCP的Keep-Alive:

    连接的双方在物理层面没有连接的概念,连接也不是一直存在数据交互,所以双方是感知不到对方的连接有没有关掉的,TCP keep-alive 也是TCP的保活机制,其基本原理是,在一段时间内没有数据传输,就会给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接,通过这种方式,来实现连接的概念。

应用场景

  • HTTP、FTP文件传输、SSH、SMTP

包头

TCP包头
TCP包头

  • 端口号:用于找到目标主机上的应用程序进程,TCP包是没有IP地址的,因为IP数据是网络层的事,所以这一层只有端口号;

  • 序号:让包能顺序发送和接收,解决乱序问题,也可以区分连接的阶段,区分不同的连接。三次握手除了确立双方建立连接,最重要的是确立双方包的序列号,每一次连接都要有不同的序列号用于区别,序列号的起始通常是随时间,如果每次连接的序列号相同,可能会导致前一次连接的包发送到了下一次连接里;序号的增加和传输的字节数相关;

  • 确认序列号:发出去的数据包时进行确认的标记,解决丢包问题,接收方回复的ACK包的确认序号 = 发送方数据包的序号 + TCP的数据载荷字节数,注意此时的载荷数据可能是这一次报文的完整数据,也有可能是包含了上一次报文的部分数据和此次报文的部分数据;

  • 状态位:客户端和服务端连接的状态,即包的类型,操控TCP的状态机;

  • 窗口大小解决流量控制问题,标识自己当前的处理能力;

TCP通过四元组可以唯一的确定一个连接,四元组:源地址、源端口、目的地址、目的端口,其中,源地址、目标地址在IP头部中,告诉IP协议发送报文给目标主机,源端口和目标端口在TCP头部中,告诉TCP协议要把报文发给哪个进程。

TCP和UDP可以使用同一个端口,因为TCP和UDP在内核中是两个独立的模块,主机收到数据包后,可以在IP包头的协议号里判断数据包是TCP还是UDP

在客户端中,针对同一个端口,可以与多个不同的服务端建立TCP连接,原因是TCP是通过四元组确定一个唯一连接的,对客户端来说,只要多个服务端的IP不同,就可以复用同一个端口,不会导致连接冲突的问题。

同理,在服务端中,只要绑定的IP不是同一个,就可以复用同一个端口,一般针对服务端绑定IP来说,有几种可能,比如本身机器的IP,或者 127.0.0.1 或者 0.0.0.0(比较特殊,在没设置SO_REUSEPORT情况下,表示绑定该主机上的所有IP,就会出现冲突)

理论上,服务端单机TCP的最大连接数 = 客户端IP数 x 客户端端口数,即 2^32 * 2^16 = 2^48,但会受操作系统资源影响,所以有一定的上限,以Linux为例,影响因素有:文件描述符限制、内存限制;

术语

SYN:发起一个新连接,并在其序号进行初始值的设定

ACK:回复,确认序号有效

ack:回复,确认序号,=发送方seq+1

FIN:释放一个连接

RST:复位标志,表示TCP连接中出现异常,需要重新连接

FIN:表示今后不再有数据发送,希望断开连接

MTU:一个网络包的最大长度,一般是1500字节

MSS:除去IP和TCP头部后,一个网络包能容纳的TCP数据的最大长度

建立连接 - 三次握手

三次握手
三次握手

  1. 第一次握手:Client将标志位SYN置为1,随机产生一个值seq=j,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。 每一端先发出去的都会有SYN,收到之后会发出ACK。

    如果client发送失败,会周期性进行超时重传,直到收到server的确认。

  2. 第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=j+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

    如果server发送响应失败,会周期性进行超时重传,直到收到client的确认。

  3. 第三次握手:Client收到确认后,检查ack是否为j+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=k+1,并将该数据包发送给Server,Server检查ack是否为k+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,可以开始传输数据了。

    第三次握手可以顺便携带数据,前两次则不行。

    client第三次握手,此时client会认为自己已经ESTABLISHED,server还未收到,此时仍然为active状态:如果server一直没收到连接请求,server会重复第二次握手,直到自己收到第三次握手的请求,此时server才是ESTABLISHED;如果此时client发送了data数据且加上了ACK,server也会切换为established;如果server有数据发送却发送不出,也会重复第二次握手。

关于半连接队列、全连接队列,如果队列满了,后续的连接会被丢掉。

半连接队列是一个哈希表,因为队列里都是不完整的连接,便于在第三次握手时通过哈希表,快速取出对应的连接;

全连接队列是一个链表,因为存在此队列里的连接已经是可用的了,直接从头节点里获取即可;

作用

三次握手后初始化socket,序号和窗口大小,并建立TCP连接。

  • 确定双方TCP包的起始序号,保证连接后的可靠有序传输:比如接收方可以去除重复的数据、接收方可以按序号进行顺序接收、或者用于标识已发送的数据包中,哪些已被对方接收。

  • 确定了序号就可以区分新老连接,防止旧的重复连接初始化造成混乱:由于可能出现client与server建立连接并进行通信后断开重连,如果每次连接没有新的起始序号,会导致server分辨不出收到的包是这次连接的还是上次连接的,这个号会作为以后的数据通信序号,以保证应用层接收到的数据不会因网络传输问题而乱序。

    另外,每个连接都会有不同的序号,序号的起始号随时间变化,每4微秒加一,如果有重复,需要4.55个小时后才会出现,因为IP包头里有TTL(生存时间),超过4小时早过期了。

    序号有回绕问题,所以只能最大程度的避免,比如Linux使用PAWS机制来解决序号回绕的问题(需要开启tcp_timestamps),其实就是包序号配合上时间戳,判断时间戳是否是递增的,来解决序号回绕问题,比如回绕时,新老序号不一样,新序号从0开始,老序号是最大值,此时配合上时间戳,就能检查出来包是否可以被接收。

  • server端多出一个状态,用来检查client端是否接收到自己的报文,从而避免客户端重复建立连接,造成不必要的资源浪费

不使用两次握手进行连接的原因

  • 因为两次握手,只保证一方的初始序号能被对方成功接收,双方无法确认对方收到了自己的序号

    如果两次握手确定连接,client发送连接请求给server,server接收后发送响应给client,此时连接建立,client可以正常的发送和接收,但是server并不知道自己发送的请求client有没有收到,此时server端无法确认自己的序号。

  • 因为两次握手,server端没有中间状态给客户端来阻止历史连接

    如果client发送连接请求给server,server收到连接请求,响应回去后就建立连接,意味着这时双方可以互发数据,但如果此时客户端没有进入ESTABLISHED状态,重复发起连接,server端无法分辨是数据包还是连接包。(因为此时server端已经认为双方可以互发数据包了,server端只有在收到RST报文才会断开连接,本质上还是没有确认包序号导致的问题)

  • 因为两次握手,如果server端每收到SYN就建立连接,会导致建立多个冗余的无效连接,造成资源浪费

    因为server端没有中间状态来检测client端是否接收到自己回复的ACK,如果client端重复发送多个SYN报文,而server端每次收到SYN就建立连接,就会建立多个冗余的无效连接,造成资源浪费。

另外,三次握手是因为第二次握手的时候,server收到client的请求和自己的请求一次性响应回了client,也可以把这一步拆出来,变成四次握手也是可以的,三次是至少的要求。四次挥手的第二和三次不能合并是因为此前连接已建立,贸然关闭会导致部分报文没有接受完成。

传输时,TCP层使用MSS分包的原因

MSS用于TCP分片,描述一个网络包能容纳的TCP数据的最大长度,一般来说,TCP建立连接时,会协商双方的MSS值,如果数据包超过MSS值,就需要进行分片,由它形成的IP包长度也不会大于MTU(1500字节),以MSS为单位进行分片传输,这样的好处是重传时只需重传特定分片,不用重传所有分片,增加重传的效率。虽然本身IP层也能分包,但是由于IP层并没有超时重传机制,如果一个IP分片丢失,会导致整个IP报文的所有分片都需要重传。

连接过程中超时或者握手丢失怎么办?

  • 第一次握手超时或丢失,此时server端没有响应SYN-ACK回去,client端苦苦等待,就会触client端的超时重传机制,重传SYN报文,重传的SYN报文序号跟之前一样。Linux下重试次数是5次,第一次超时重传是1秒后,第二次两秒,每次超时的时间是上一次的 2 倍,如果5次都超时,总共要等等63s,TCP才会断开连接;
  • 第二次握手超时或丢失,虽然server端响应了SYN-ACK给客户端了,但是client端收不到,此时client会跟上面的一样,进行超时重传SYN报文(继续第一次握手);而server端因为迟迟收不到client端的ACK,也会触发超时重传,重传SYN-ACK报文(继续第二次握手)。Linux下也是重试5次,机制跟上面一样;
  • 第三次握手超时或丢失,由于ACK不会重传,此时是server端触发超时重传,重传SYN-ACK,直到收到第三次握手或达到最大重传次数(继续第二次握手)。

SYN报文被丢弃可能的原因:

  • PAWS机制 + NAT环境下,因为client端A和B通过NAT网关与server端建立连接,在server端看来,client端其实是同一个,此时如果client端A和B都开启了PAWS机制,数据包带有时间戳有先后顺序,就会导致server端丢弃client的数据包了;
  • 半连接队列 / 全连接队列 满了,此时后来的SYN包都会被丢弃;

已建立连接但重复接收SYN包会怎么样

当client端与server端建立连接后,client端宕机重启,再次发起SYN连接(四元组相同),但server端仍然处于上一个连接的ESTABLISHED状态,因为报文序号的原因,此时server端能知道此报文号不是自己期望的,那就会回复一个携带了正确序号和确认号的ACK报文(Challenge ACK,携带了server端下一次想要接收的序号),client端收到这个Challenge ACK后,发现确认号不是自己期望的,就会回复RST报文,server端收到后就会释放掉该连接。

Challenge ACK的作用是告诉发送方报文的序号是错误的,并给发送方正确的包序号。

如何关闭连接

server端关闭连接时,如果贸然关闭整个server端,会导致所有连接到此server端的client端都断开,最佳实践是可以单独关闭每一条TCP连接,原理就是伪造一个SYN包发送给server端,获取Challenge ACK,反向制作出RST报文,分别发送给client端和server端,让双方断开连接。

TCP如果发现到达的报文序号对于当前的连接是不正确的,就会发送一个RST重置报文,从而使得连接断开,因此,TCP对RST重置报文的序号要求很严格,一定要等于下一个预期接收包的序号(可以利用challenge ACK实现),才能进行重置,断开连接。TCP重置攻击的原理,就是不断向TCP发送RST包,让正常的TCP连接断开,解决方案是IP层使用IPsec协议,通过加密和认证的方式判断数据来源。

建立连接后出故障了怎么办?

  • TCP设有一个保活计时器,每收到一次请求都会复位这个计时器,server端在一段时间内没有进行数据交互时,就会触发这套keep-alive机制,如果规定时间(2小时)内没收到,则发送探测报文测试对方是否出现故障,连续10次/75分钟,仍没反应,说明对方故障;

    如果没有开启这个机制,server端就还是一直保持在ESTABLESHED状态;

    这个机制主要是探测对端主机是否宕机重启的场景,如果是对端应用进程崩溃的,因为TCP连接是由操作系统内核维护的,能感知到并进行资源回收,进入四次挥手流程断开连接;

  • 如果client端宕机后重启,还收到了之前TCP连接的报文,就会回复RST报文,断开连接;

  • 如果client端宕机后没有重启,server端发送数据收不到ACK,会进行超时重传,直至达到上限,断开连接;

SYN Flood攻击的关系

TCP三次握手时,客户端发送SYN到服务端,服务端收到之后,便回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了半连接队列。

当客户端回复ACK, 服务端接收后,三次握手就完成了。这时连接会等待被具体的应用取走,在被取走之前,它被推入ACCEPT队列,即全连接队列。

SYN Flood攻击,在短时间内,伪造不存在的IP地址,向服务器大量发起SYN报文。当服务器回复SYN+ACK报文后,不会收到ACK回应报文,导致服务器上建立大量的半连接半连接队列满了,此时服务器就无法处理正常的TCP请求。

应对方案:

  • 调大netdev_max_baklog,该参数用于当网卡接收数据包的速度大于内核处理速度时,用一个队列保存这些数据包,默认是1000。

  • 增大TCP半连接队列的长度

  • 减少SYN+ACK重传的次数:受到SYN Flood攻击,会有大量处于SYN_REVC状态的TCP连接,处于这个状态会重传SYN+ACK,通过减少其重试次数上限,来快速关闭连接。

  • 开启net.ipv4.tcp_syncookies:在收到SYN包后,服务器根据一定的方法,以数据包的源地址、端口等信息为参数计算出一个cookie值作为自己的SYNACK包的序列号,回复SYN+ACK后,服务器并不立即分配资源进行处理,等收到发送方的ACK包后,重新根据数据包的源地址、端口计算该包中的确认序列号是否正确,如果正确则建立连接,否则丢弃该包。

  • SYN Proxy防火墙:服务器防火墙会对收到的每一个SYN报文进行代理和回应,并保持半连接。等发送方将ACK包返回后,再重新构造SYN包发到服务器,建立真正的TCP连接。

关闭连接 - 四次挥手

四次挥手
四次挥手

  1. 第一次挥手:Client发送一个FIN、一个seq,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
  2. 第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号ack为收到seq+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
  3. 第三次挥手:Server发送一个FIN、一个ACK、ack为上面的seq+1、一个seq,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
  4. 第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态(等待时间设为2MSL,即报文最大生存时间),接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手,Server会比Client早一点关闭TCP连接。

由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭。这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到新数据了(但仍然可能收到在途的数据),但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

关闭连接需要四次挥手的原因

  • 四次挥手:第一次挥手Client发送消息确定Client想关闭连接,第二次挥手Server发送消息确定Client可以关闭连接,第三次挥手Server发送消息确定Server想关闭连接,第四次挥手Client发送消息确定Server可以关闭连接,少了哪一次都可能导致没有完全关闭,造成一方可发送或者接收。

  • 之所以要四次,是因为server收到FIN后,不能同时发送ACK确认信号和FIN,有可能此时client有一些报文还没收完,如果client没有收到,server才有能重发,所以第二、三次挥手不能合并。

  • 关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送新数据,但对方还能发送旧数据或接收数据,己方也未必把全部数据都发送给对方了,所以己方先发送ACK,告知对方我知道你不会再发送新数据,之后己方还仍然可以发送一些数据给对方,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方的ACK和FIN一般都会分开发送。

正常情况下,是否发送第三次挥手的控制权不在内核,而在被动关闭方的应用层,取决于其什么时候调用关闭函数,调用了关闭函数,才会发第二个FIN,在关闭之前,还可以继续发送数据,所以二三次挥手的ACK和FIN是分开发的。

关闭函数有两种:

  • close()函数,会使得socket不再有发送和接收的能力,当使用close()函数关闭连接时,由于不再具有发送和接收的能力,会直接返回RST报文给对方,然后内核释放连接,属于粗暴关闭。当客户端调用了close函数,服务端继续读操作,就会报Connection reset by peer,如果是写操作,则程序会产生SIGPIPE信号,交由应用层程序的信号处理器处理,默认是进程终止退出。

  • shutdown()函数,会使socket只关闭发送方向而不关闭读取方向,此时会经历完整的四次挥手,属于优雅关闭。

只有在特定场景下,才会出现三次挥手:当被动关闭方在TCP挥手过程中,没有数据要发送,同时没有开启TCP_QUICKACK, 即开启了TCP延时确认机制(该机制可以使得ACK和数据报文一起发送,提升效率),那么第二三次挥手可以合并传输,此时是三次挥手。

断开连接过程中超时或者挥手丢失怎么办?

  • 第一次挥手时超时或丢失,当重试到达上限后,client端会直接断开连接;
  • 第二次挥手时超时或丢失,由于ACK不会重传,所以是client端进行第一次挥手的超时重传,当重试到达上限后,client端会直接断开连接;
  • 第三次挥手时超时或丢失,此时client端处于FIN_WAIT2状态,如果client端是调用close函数,则该状态最多持续60s,到时直接关闭;如果client端调用的是shutdown函数,则只关闭了发送方向,此时仍然可以接受数据,但状态会一直处于FIN_WAIT2;而server端会重传FIN,直到到达上限,server断开连接;
  • 第四次挥手时超时或丢失,server端会进行超时重传,当重试到达上限后,server端会直接断开连接;client此时的状态是TIME_WAIT,在定时2MSL后(每次收到第三次挥手会进行重置),超时client端就会断开连接;

主动关闭连接的那一方需要TIME_WAIT状态的原因

  • 保证被动关闭连接的那一方能正确的关闭连接:client端发送的最后一个ACK报文给server端,这个ACK报文是可能丢失,此时server接收不到,那他就会重新发送FIN报文,client端就能在这2MSL内收到这个重传的报文,再次进入2MSL等待时间,发送ACK给server端,直到两者都进入closed状态。如果没有2MSL等待时间,而是client端发送完报文直接关闭,就会出现server端无法接收而导致无法进入closed状态;
  • 如果server超过了2MSL时间依然没收到client的ACK,会再次重发第三次挥手,但此时client会发送RST标志,表示异常关闭连接;
  • 防止历史连接中的数据,被后面相同的四元组连接错误的接收:client端在发送完最后一个ACK报文段后,在经过2MSL,可以使本连接持续的时间内所产生的所有报文都从网络消失,用这个时间让这个连接不会和后面的连接混在一起,使得下一个新的连接不会出现旧的连接请求报文;

TIME_WAIT等待时间是2MSL的原因

MSL(Maximum Segment Lifetime,报文最大生存时间,单位是时间),与IP头中的TTL的区别,TTL是指IP数据包可以经过的最大路由数,每经过一次处理它的路由,次数就减一,值为0时会进行丢弃,同时发送ICMP报文通知源主机,而MSL会设置一个大于等于TTL被消耗为0的时间,确保报文已被自然消亡。

TTL值一般是64,Linux将MSL设置为30秒,TIME_WAIT设置为2MSL,表示一来一回的时间,相当于至少允许报文丢失一次,即server端可以在client端处于TIME_WAIT状态下重试两次。

2MSL的时间从client端收到FIN后发送ACK开始计时,重复收到FIN报文则会进行重置。

TIME_WAIT状态下,client端收到相同四元组的SYN包,会怎么样

此时要看SYN包是否合法(合法的SYN包,其序号和时间戳会比期望下一个收到的序号或时间戳要大):

  • 如果是合法的SYN包,就会重用此四元组连接,跳过2MSL进入SYN_RECV状态,进入三次握手建立连接;
  • 如果收到非法的SYN包,就会再回复一个第四次握手的ACK报文(Challenge ACK),server端收到后,发现不是自己期望收到的ACK,就会回复RST报文给client端;

高并发下存在大量TIME_WAIT状态的连接的原因 及 解决办法

TIME_WAIT状态是 主动发起连接关闭的那一方 才会存在,可以是客户端,也可以是服务端

可能产生的原因:

  • 短连接场景下,无论是客户端还是服务端,请求头里带Connection: close,都是服务端在处理完这次请求后,主动关闭连接;因为如果由客户端来关闭,服务端还得处理一次这次关闭的socket;
  • 长连接超时,双方建立长连接之后,在一段时间没有数据通信,服务端就会关闭连接;
  • HTTP长连接的请求数量达到上限,服务端就会关闭连接,比如在一些QPS比较高的场景

可能产生的影响:

  • 如果在客户端,存在大量TIME_WAIT状态的连接,可能导致端口占用(此时无法对同一四元组建立连接,对其他四元组建立连接还是没问题),连接未能及时回收,导致无法创建新连接;

  • 如果在服务端,存在大量TIME_WAIT状态的连接,可能导致系统资源占用,如文件描述符、内存资源、CPU资源、线程资源等。端口资源倒是不会受限,因为服务端只监听一个端口,此时只会导致无法与客户端同一四元组建立连接而已,对其他客户端连接没有影响,只要同一个客户端使用新端口,也不会有影响,所以更多是对系统资源的占用;

解决办法:

TIME_WAIT的状态是必定存在的,所以一般来说,只能尽量减少(降低等待时间,降低出现的次数)TIME_WAIT带来的危害,比如:

  • 可以设置tcp_tw_reuse(连接复用)tcp_timestamps(报文带上 时间戳,防止回绕)参数解决,该参数的作用是让client端能快速复用处于TIME_WAIT的端口,如果在TIME_WAIT期间,client端以相同四元组再次建立连接,会判断TIME_WAIT是否超过一秒,超过则可以正常使用这个端口,但是可能会误接收上次连接的RST,导致连接断开,如果第四次挥手的ACK丢了,此时被动关闭的那一方可能不能被正常关闭;

    这种方案有个风险点,快速复用 TIME_WAIT 状态的端口,导致新连接可能被回绕序号的RST报文断开,因为RST报文比较特殊,TCP在处理的时候,发生即使RST报文的时间戳过期了,序号正确,还是能被接收;如果没打开tcp_tw_reuse,停留2MSL,那这个RST报文就不会出现在这个新连接里了;

    另外,此方案只适用于客户端,即服务发起端;

  • 修改tcp_max_tw_buckets参数,调小TIME_WAIT的等待时间;

  • 将短链接改成长连接,比如HTTP,把响应头的Connection: close改成Connection: keep-alive,告诉服务端不要关闭,尽量复用链接;

  • 尽量不在服务端关闭连接;

  • 增加可用端口的范围;

服务端出现大量CLOSE_WAIT状态的原因

  • CLOSE_WAIT状态是 被动关闭连接的那一方 才会存在,一般是在服务端,如果 被动关闭方 没有及时关闭连接,那就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT状态的连接转变为 LAST_ACK 状态。

  • 发生这种情况通常是代码问题,没有及时调用close方法关闭连接导致;

滑动窗口

TCP协议规定在建立连接后,会确定包的序号的起始ID,按照ID一个个发送,只有上一个数据包收到应答,才能发送下一个,这样就会导致效率比较低,为了解决这个问题,引入了滑动窗口,通过窗口来实现累计确认或累计应答的目的,应答某个ID的包就表示在这个ID之前的包都收到了,才允许发送下一个(可以理解成批量批量的ACK)。

窗口大小即自己的数据接收缓冲池的大小,由接收端决定,窗口标识了无需等待确认应答,还可以继续发送数据的最大值。

发送窗口和接收窗口所存放的字节数,都是放在操作系统内存缓冲区的,会受操作系统影响,如果接收端不能及时接收数据,可能导致窗口变小,发送端无法发送数据;如果先发生减少缓存,再收缩窗口,则可能出现丢包问题,因此TCP规定只能先收缩窗口,再减少缓存来避免丢包问题。

由于发送缓冲区大小决定了发送窗口的上限,而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此,发送缓冲区不能超过「带宽时延积」,尽量靠近最好。带宽时延积 = RTT * 带宽,如果发送缓冲区超过带宽时延积,此时网络过载,容易丢包,如果小于带宽时延积,带宽用不满,比较浪费。

在发送端和接收端会分别使用缓存来保存这些包的记录,一般分为四个:1. 已发送并确认的、2. 已发送未确认的、3. 没有发送但准备发送的,且大小在接收方处理范围内的、4. 没有发送且暂时不会发送的,滑动窗口就是处理第二、三部分的数据包

  • 由发送方和接收方在三次握手阶段,互相通知自己的最大可接收的字节数。

  • 当发送方窗口左部字节已发送且收到确认,窗口右滑直到左部第一个字节不是已发送并且已确认的状态,接受方窗口移动同理,此时可用窗口变大,表示可以继续发送数据;

  • 接收窗口只会对窗口内最后一个按序到达的字节进行确认,确认之后表示之前的所有字节都接收到了;

  • 发送窗口为0,表明可用窗口耗尽,在未收到ACK确认之前无法继续发送数据;

在处理过程中,当接收缓冲池的大小发生变化时,会给对方发送更新窗口大小的通知。

粘包问题

实际上因为TCP是面向字节流,而字节流本身没有粘包/拆包的概念,TCP层只管提供可靠性消息传输,不然为什么会对网络分层呢,粘包是应用层没有处理好导致

发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

只有TCP会(通过窗口大小来接收数据,窗口大小又是动态的),而UDP因为有消息边界(头部有规定报文的大小),所以不会

  • 产生的原因

    • 发送端粘包:发送端等到缓冲区满了才发送出去,造成粘包;

    有时为了提高发送数据的效率,服务端会把多个数据块合并成一个大的数据块后封包发送,但由于面向流的通信是无消息保护边界的,接收端就很难分辨出完成的数据包。

    当要发送的数据大于TCP发送缓冲区剩余大小,将会发生拆包;当待发送数据大于(最大报文长度),TCP在传输前将进行拆包。即TCP报文长度-TCP头部长度>MSS。

    • 接收端粘包:接收端不及时接收缓冲区的包,造成多个包接收;
  • 解决方法

    粘包主要就是没有处理好数据包的边界,因此我们可以

    • 发送固定长度的消息;消息尺寸和消息一块发送;特殊标记标记消息区间;
    • 通信双方规定好协议 + 编解码器(比如规定定长的请求头、请求体),按协议的格式进行解析;比如将数据分成两部分,一部分是头部,一部分是内容体,其中头部结构大小固定,且有一个字段声明内容体的大小。
    • 程序控制发送和接收频率;

TCP粘包问题分析和解决(全)

流量控制

TCP需要解决可靠传输和包的乱序问题,就需要知道网络实际的数据处理带宽或数据处理速度,才不会引起网络拥塞,导致丢包,so需要做流量控制。在TCP的包头中通过Window字段控制,这个字段是接收端每次ACK时会告诉发送端自己还有多少缓冲区可以接收数据,于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来,从而实现流量控制。

实现滑动窗口,发送端和接收端在进行数据交互时,会商定滑动窗口的大小,在对于包的确认时会携带一个窗口的大小(通过ACK通知),通过该窗口大小来实现流量控制(或者时不时发送一个窗口探测的数据段来确认双方的窗口大小),当发送方和接收方的滑动窗口大小为0时,发送方会定时发送窗口探测数据包,来更新窗口大小。

  • 窗口关闭(Zero Window):当滑动窗口的大小变成0,意味着发送端不发数据了,当接收方的滑动窗口可以更新时,会通过ACK通知发送端,但是这个窗口大小的ACK报文是可能丢失的,丢失会导致双方互相等待,导致死锁。

    所以收到Zero Window的那一方(即滑动窗口变为0),会启动一个持续计时器,如果计时器超时,就会发送Zero Window Probe报文进行探测,更新自己的窗口大小,这个值会被设置成3次,每次大约30-60s,如果3次过后还是0,可能就会断开连接了。

  • 糊涂窗口综合征(Silly Window Syndrome):如果接收方太忙,来不及取走滑动窗口里的数据,就会导致发送方可发送的数据越来越小,如果每次发送的数据太小,带宽没有占用,实际上是很亏的,这种现象就叫Silly Window Syndrome。

    解决方法有多种,一种是如果这个问题是接收端引起的,当收到的数据导致滑动窗口的大小小于某个值,就直接回滑动窗口为0的ACK给发送端,把滑动窗口关闭,等接收方的滑动窗口大于某个值是,才把滑动窗口打开,让发送端发数据过来;另一种是如果这个问题是发送端引起的,就延时处理,攒多点数据一块发。

重传机制

**丢包问题:**发送方按顺序发送一系列的包,接收方接收这些包是中间一部分包没收到

  • 产生原因
    • 中间这些包经过其他链路导致延时接收方还没收到;
    • 接收方收到且发送了ACK给发送方,但是发送方没有收到ACK;
    • 网卡丢包,数据链路有点问题,或者网卡性能不足,数据包真的丢了;
    • 因为顺序发送和接收问题,接收方后面的包收到了,但是前面的包还没收到,导致后面的包收到了也不能发送ACK给发送端;
    • 建立连接时,半连接队列 或者 全连接队列 满了,新来的包就会被丢掉;
    • 流量控制机制产生的丢包;
    • 接收数据时,数据会先暂存在内核缓冲区,如果缓冲区过小,发送速度又过快,产生溢出而丢包;

所以,为了保证可靠传输,解决丢包问题,就需要保证当报文丢失,在一定次数内持续重试,直到成功,又要确保如果重传都失败了,及时放弃传输,避免死循环的问题。

  • 解决方法

    • 超时重传(Retransmission Timeout,RTO),超时重传有一个计时器,在报文发送出去后开始计时,如果在时限内收到回复的ACK,计时器就清零,如果在时限内还没收到ACK,就触发重传。

      超时时间由自适应重传算法来决定,另外是每次重试的时间间隔会加倍,超过两次认为网络环境差,取消重传。超时重传主要解决丢包时干等回复ACK的问题

      RTO初始值是1秒,建立连接后,RTO会被动态计算,上限是2min,下限是200ms。RTO的设置尤为关键,设置过长,重发就慢,效率低性能差;设置过短,就会导致没有丢就重发,重发过快,就会增加网络拥塞,导致更多的超时,从而又导致更多的重发,因此无法设置成一个写死的值,而是通过RTT算法动态调整。一般超时重传时间RTO会略大于报文往返时间RTT的值。

      由于ACK本身没有ACK了,如果ACK发生丢失,ACK会视情况重传,比如,如果发送方收到了后续的ACK,就说明前面的内容都接收到了,就不会重传;如果没收到后续的ACK,则触发超时重传机制。

    • 快速重传机制,因为包是有顺序的,所以客户端可以检测出缺失的包,然后发送对应的冗余ACK(称为DupAck)给服务端,通过在服务端设置收到规定通过发送/接收冗余的ACK包的次数(一般是3个),如果达到了规定次数就把序号等于这个ACK号的包进行重传,不等超时时间,so 快速重传主要是解决超时等待过久的问题

      快速重传的触发条件是:收到3个或以上的重复ACK,即DupACK

      比如:接收端中间漏收了Seq2,后面又接收到了Seq3、4、5,那就会在接收时重复发送ACK2,发送端收到重复的ACK后就会重新发送Seq2了。

      但是快速重传有一个问题,ACK只向发送端告知最大的有序报文段,并不确定是哪一个报文丢失了,此时发送端不知道要重传哪些包,是重传一个,还是重传所有。

      以上图为例,由于报文的顺序性,接收端收不到Seq2,但Seq3、Seq4、Seq5都接收到了,但是ACK的时候只重复了ACK2,此时发送端只知道必定要重传Seq2,但是并不清楚是否要重传Seq3、Seq4、Seq5。

      SACK就是为了解决这个问题:

      在Linux中的SACK机制,它会在TCP头里加一个SACK(Selective ACK)的东西,SACK还是基于快速重传的ACK,只是SACK会把接收端收到的所有包的序列,都反馈给发送端,发送端根据遗漏的ACK序号,进行重传。SACK部分最多只能容纳4个块。

      比如在上面的场景中,接收端在发送ACK时,带上SACK,SACK上带有Seq3、4、5的信息,这样发送端就知道只需重传Seq2了。

      另外,还有个D-SACK(Duplicate SACK),在SACK上做扩展,DSACK使用SACK的第一个段作为标识,这个段表示接收范围,通过比较这个段的值和SACK的回复范围,进行判断,让发送方知道是发出去的包丢了,还是接收方回应的ACK包丢了;还是发送方的超时太短,导致重传;还是先发出去的包后到的情况,还是数据包被复制了。

      SACK和D-SACK的区别在于次数和目的,SACK通过重复重传来告诉发送方要重传什么数据,D-SACK用来告诉发送方哪些数据被重复接收。

拥塞控制

  • 产生原因:对资源的需求超过了可用的资源,网络吞吐量下降,如果网络出现拥塞,数据包将会丢失或延迟到达,发送方以为发送失败又会继续重传,从而导致网络拥塞程度更高。

  • 判定方式:只要发送端没有在规定时间收到ACK应答报文,就会发生超时重传,此时会认定网络出现拥塞。

    TCP通过数据包发送和确认的往返时间RTT,丢包率来判断是否拥塞,使用滑动窗口来进行拥塞控制,控制发送方的发送速率,避免包丢失和超时重传。

  • 解决方法:

    • 慢开始 + 拥塞避免,一开始慢慢的发送,逐渐增大发送速率(线性上升直至网络最佳值),再慢下来依次重复。

      慢开始阶段,TCP连接每收到一个数据的ACK(不包括重复的ACK),拥塞窗口就翻倍增加一个MSS(最大报文大小),当达到了慢开始的阈值,增长速度就会放缓,进入拥塞避免阶段,变成了每过一个RTT,拥塞窗口就会增长一个MSS;

      每一个TCP连接独立维护自己的拥塞窗口,拥塞窗口一般比MSS大,一般是MSS的某个倍数,MSS的上限一般是1460字节,一个窗口就是n个MSS;发送窗口大小 = min(拥塞窗口大小,接收窗口大小);

      拥塞窗口由发送方维护,不放在TCP头部中;

      拥塞避免时会重复慢开始,每次收到一个数据的ACK,拥塞窗口增加量会进行减半,在再次进入慢开始+拥塞避免,所以,慢开始不止在TCP连接启动时发生,也有可能在传输过程中反复发生。

      慢开始+拥塞避免
      慢开始+拥塞避免

    • 快重传 + 快恢复,当拥塞发生时,减少超时重传的使用,而是使用快速重传机制

      TCP每发送一个报文,就会启动一个超时计时器,如果在限定时间内没有收到这个报文的ACK,发送方就会认为报文丢失,此时就会进行超时重传,一般最小的超时重传时间是200ms,为了解决每次丢包都要等待200ms或者更长的时间才会重传的问题,TCP就会采用快速重传,一旦发送方收到3次重复确认,就不用等超时计时器了,会直接重传这个报文。

      在Reno拥塞控制算法中,TCP在遇到拥塞点后,不会反复进行慢开始+拥塞避免,而是拥塞窗口减半,直接进入快速重传,不进入慢开始,保持跟拥塞避免一样的线性增长,直到下一个拥塞点。

      网络上的限速,原理其实就是设置拥塞窗口的上限,当超过限制,就会丢弃这些报文,主动进入拥塞避免阶段,确保传输速度。

      快恢复
      快恢复

    为了实现上面两种机制,TCP使用BBR拥塞算法,来达到高带宽和低延时的平衡

流量控制和拥塞控制的区别:

流量控制针对接收方,为了让接收方能来得及接收,根据接收方自己的能力,控制发送速度,防止分组丢失;

而拥塞控制是为了防止过多的数据包注入到网络中,降低整个网络的拥塞程度,避免出现网络负载过载;

优化

参考:https://xiaolincoding.com/network/3_tcp/tcp_optimize.html

UDP(User Datagram Protocol 用户数据报协议)

特点

  • 不可靠的、无连接的,尽最大可能交付,只负责发送数据,无状态服务
  • 没有拥塞控制,流量控制
  • 面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),一个一个地发,一个一个地收
  • 头部只有8字节
  • 支持一对一、一对多、多对一和多对多的交互通信。

应用场景

  • 针对网络资源少,对丢包不敏感的应用,比如应用层的DHCP,在获取IP地址和子网掩码的使用,DNS、SNMP等
  • 需要广播的应用,比如DHCP、VXLAN
  • 需要处理速度快,时延低,容忍丢包、网络拥塞的应用,比如直播、视频,允许丢包,虽然丢包会导致丢帧,但影响不会很大;实时游戏、物联网终端的数据收集,其实大多数会基于UDP做一定的改进,减少UDP劣势的影响

包头

UDP包头
UDP包头

UDP包头比较简单,两端通信时,通过网络层里的IP,将数据包发送给对应的机器,IP头里有个8位协议,表明该数据是UDP协议的,解析到传输层,通过UDP包头提供端口号,让目标机器监听该端口号的应用程序进行处理

使用场景

  • 网络环境较好,如内网应用,或者对于丢包不敏感的应用
  • 需要广播或多播
  • 需要处理速度快,时延低,容忍丢包和网络拥塞

UDP如何实现TCP

建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,通过这样的数据结构来保证面向连接的特性。

UDP属于传输层,协议已经定死了,要实现TCP的功能只能在应用层实现,模拟TCP有的那些功能:确认机制、重传机制、窗口确认机制、流量控制、拥塞控制等那些功能。

UDP实现可靠性,可以简单理解成,将TCP的三次握手发送数据全程用UDP去发送,模拟TCP的包头

  • seq/ack机制,确保数据发送到对端
  • 数据包 + 序号,确保有序性
  • 数据包 + 确认序号,确保不丢包
  • 添加发送和接收缓冲区,主要是用户超时重传。
  • 定时任务实现超时重传机制。

发送端发送数据时,生成一个随机seq=x,然后每一片按照数据大小分配seq。数据到达接收端后接收端放入缓存,并发送一个ack=x的包,表示对方已经收到了数据。发送端收到了ack包后,删除缓冲区对应的数据。时间到后,定时任务检查是否需要重传数据

如何实现UDP的可靠传输

比如有QUIC协议,就是基于UDP实现的可靠传输协议,也是在应用层实现的。

套接字Socket

Socket是对TCP或UDP协议的封装,本质上是一个调用接口,而非协议,工作在OIS模型的第五层(会话层)

基于TCP协议的Socket

基于TCP的Socket
基于TCP的Socket

  • 服务端只调用bind()方法,不调用listen()方法,如果客户端直接根据这个ip和端口进行连接,此时无法联通,服务端会直接返回RST报文;
  • 但是不调用listen(),是可以建立TCP连接的,而socket不行而已。在TCP中,存在自连接,即客户端自己连自己,不能有服务端参与,此时是可以建立连接的。虽然不调用listen()方法,不会创建半连接队列和全连接队列,但内核有个全局的hash表,可以存放socket连接的信息,连接信息通过回环地址从这个全局hash表中取出,最后成功建立连接;
  • 不调用accept()方法,也能进行三次握手,建立连接,甚至,服务端在执行accept()方法前,如果客户端发送消息给服务端,服务端能正常回复ACK确认包的。因为accept()方法本身不参与握手,执行accept()只是为了从全连接队列里取出一条可用连接而已。

基于UDP协议的Socket

基于UDP的Socket
基于UDP的Socket

服务端如何管理这些连接和资源?

  • 父进程使用子进程来管理连接和资源,子进程在完成连接和数据通信后告诉父进程进行回收
  • 线程池 + 连接池,每个线程管理一个socket,连接池实现socket复用
  • IO多路复用,一个线程维护多个socket,如Java NIO、Netty的网络模型

连接池实现

写得真不错,清晰易懂 https://yusank.space/posts/conn-pool/

参考

TCP/IP参考1

TCP/IP参考2

极客时间 - 趣谈网络协议

TCP 的那些事儿

面试必备!TCP协议经典十五连问!

小林coding - 图解网络

https://coolshell.cn/articles/11564.html

https://coolshell.cn/articles/11609.html

Built with Hugo
Theme Stack designed by Jimmy