HTTP

478 阅读1小时+

HTTP 的特点和缺点

特点无连接无状态灵活简单快速

  • 无连接:每一次请求都要连接一次,请求结束就会断掉,不会保持连接
  • 无状态:每一次请求都是独立的,请求结束不会记录连接的任何信息,减少了网络开销,这是优点也是缺点
  • 灵活:通过http协议中头部的Content-Type标记,可以传输任意数据类型的数据对象(文本、图片、视频等等),非常灵活
  • 简单快速:发送请求访问某个资源时,只需传送请求方法和URL就可以了,使用简单,正由于http协议简单,使得http服务器的程序规模小,因而通信速度很快

缺点无状态不安全明文传输队头阻塞

  • 无状态:请求不会记录任何连接信息,没有记忆,就无法区分多个请求发起者身份是不是同一个客户端的,意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大
  • 不安全明文传输可能被窃听不安全,缺少身份认证也可能遭遇伪装,还有缺少报文完整性验证可能遭到篡改
  • 明文传输:报文(header部分)使用的是明文,直接将信息暴露给了外界,WIFI陷阱就是复用明文传输的特点,诱导你连上热点,然后疯狂抓取你的流量,从而拿到你的敏感信息
  • 队头阻塞:开启长连接时,只建立一个TCP连接,同一时刻只能处理一个请求,那么当请求耗时过长时,其他请求就只能阻塞状态(如何解决下面有讲)

HTTP 报文结构?

对于 TCP 而言,在传输的时候分为两个部分:TCP头数据部分

而 HTTP 类似,也是header + body的结构,具体而言:

起始行 + 头部 + 空行 + 实体
复制代码

由于 http 请求报文响应报文是有一定区别,因此我们分开介绍。

起始行

对于请求报文来说,起始行类似下面这样:

GET /home HTTP/1.1
复制代码

也就是方法 + 路径 + http版本

对于响应报文来说,起始行一般张这个样:

HTTP/1.1 200 OK
复制代码

响应报文的起始行也叫做状态行。由http版本、状态码和原因三部分组成。

值得注意的是,在起始行中,每两个部分之间用空格隔开,最后一个部分后面应该接一个换行,严格遵循ABNF语法规范。

头部

展示一下请求头和响应头在报文中的位置:

不管是请求头还是响应头,其中的字段是相当多的,而且牵扯到http非常多的特性,这里就不一一列举的,重点看看这些头部字段的格式:

    1. 字段名不区分大小写
    1. 字段名不允许出现空格,不可以出现下划线_
    1. 字段名后面必须紧接着:

空行

很重要,用来区分开头部实体

问: 如果说在头部中间故意加一个空行会怎么样?

那么空行后的内容全部被视为实体。

实体

就是具体的数据了,也就是body部分。请求报文对应请求体, 响应报文对应响应体

HTTP 请求方法(9种)

HTTP1.0: GETPOSTHEAD

HTTP1.1: PUTPATCHDELETEOPTIONSTRACECONNECT

方法描述
GET获取资源
POST传输资源,通常会造成服务器资源的修改
HEAD获得报文首部
PUT更新资源
PATCH对PUT的补充,对已知资源部分更新 菜鸟
DELETE删除资源
OPTIONS列出请求资源支持的请求方法,用来跨域请求
TRACE追踪请求/响应路径,用于测试或诊断
CONNECT将连接改为管道方式用于代理服务器(隧道代理)

GET 和 POST 的区别

  • GET在浏览器回退时是无害的,而POST会再次发起请求
  • GET请求会被浏览器主动缓存,而POST不会,除非手动设置
  • GET请求参数会被安逗保留在浏览器历史记录里,而POST中的参数不会被保留
  • GET请求在URL中传递的参数有长度限制(浏览器限制大小不同),而POST没有限制
  • GET参数通过URL传递,POST放在Request body
  • GET产生的URL地址可以被收藏,而POST不可以
  • GET没有POST安全,因为GET请求参数直接暴露在URL上,所以不能用来传递敏感信息
  • GET请求只能进行URL编码,而POST支持多种编码方式
  • 对参数的数据类型,GET只接受ASCII字符,而POST没有限制
  • GET产生一个TCP数据包,POST产生两个数据包(Firefox只发一次)。GET浏览器把 http header和data一起发出去,响应成功200,POST先发送header,响应100 continue,再发送data,响应成功200

常见状态码

200("OK") 一切正常。实体主体中的文档(若存在的话)是某资源的表示。

400("Bad Request") 客户端方面的问题。实体主题中的文档(若存在的话)是一个错误消息。希望客户端能够理解此错误消息,并改正问题。

500("Internal Server Error") 服务期方面的问题。实体主体中的文档(如果存在的话)是一个错误消息。该错误消息通常无济于事,因为客户端无法修复服务器方面的问题。

301("Moved Permanently") 当客户端触发的动作引起了资源URI的变化时发送此响应代码。另外,当客户端向一个资源的旧URI发送请求时,也发送此响应代码。

404("Not Found") 和410("Gone") 当客户端所请求的URI不对应于任何资源时,发送此响应代码。404用于服务器端不知道客户端要请求哪个资源的情况;410用于服务器端知道客户端所请求的资源曾经存在,但现在已经不存在了的情况。

409("Conflict") 当客户端试图执行一个”会导致一个或多个资源处于不一致状态“的操作时,发送此响应代码。

SOAP Web服务只使用响应代码200("OK")和500("Internal Server Error")。无论是你发给SOAP服务器的数据有问题,还是服务器在处理数据的过程中出现问题,或者SOAP服务器出现内部问题,SOAP服务器均发送500("Internal Server Error")。客户端只有查看SOAP文档主体(body)(其中包含错误的描述)才能获知错误原因。客户端无法仅靠读取响应的前三个字节得知请求成功与否。

2、状态码系列。 1XX:通知 1XX系列响应代码仅在与HTTP服务器沟通时使用。

100("Continue") 重要程度:中等,但(写操作时)很少用。
这是对HTTP LBYL(look-before-you-leap)请求的一个可能的响应。该响应代码表明:客户端应重新发送初始请求,并在请求中附上第一次请求时未提供的(可能很大或者包含敏感信息的)表示。客户端这次发送的请求不会被拒绝。对LBYL请求的另一个可能的响应是417("Expectation Failed")。

请求报头:要做一个LBYL请求,客户端必须把Expect请求报头设为字符串"100-continue"。除此以外,客户端还需要设置其他一些报头,服务器将根据这些报头决定是响应100还是417。

101("Switching Protocols")
重要程度:非常低。
当客户端通过在请求里使用Upgrade报头,以通知服务器它想改用除HTTP协议之外的其他协议时,客户端将获得此响应代码。101响应代码表示“行,我现在改用另一个协议了”。通常HTTP客户端会在收到服务器发来的101响应后关闭与服务器的TCP连接。101响应代码意味着,该客户端不再是一个HTTP客户端,而将成为另一种客户端。
尽管可以通过Upgrade报头从HTTP切换到HTTPS,或者从HTTP1.1切换到某个未来的版本,但实际使用Upgrade报头的情况比较少。Upgrade报头也可用于HTTP切换到一个完全不同的协议(如IRC)上,但那需要在Web服务器切换为一个IRC服务器的同时,Web客户端切换为一个IRC的客户端,因为服务器将立刻在同一个TCP连接上开始使用新的协议。

请求报头:客户端把Upgrade报头设置为一组希望使用的协议。
响应报头:如果服务器同意切换协议,它就返回一个Upgrade报头,说明它将切换到那个协议,并附上一个空白行。服务器不用关闭TCP链接,而是直接在该TCP连接上开始使用新的协议。

2XX: 成功
2XX系列响应代码表明操作成功了。

200("OK")
重要程度:非常高。
一般来说,这是客户端希望看到的响应代码。它表示服务器成功执行了客户端所请求的动作,并且在2XX系列里没有其他更适合的响应代码了。

实体主体:对于GET请求,服务器应返回客户端所请求资源的一个表示。对于其他请求,服务器应返回当前所选资源的一个表示,或者刚刚执行的动作的一个描述。

-201("Created")
重要程度:高。

当服务器依照客户端的请求创建了一个新资源时,发送此响应代码。

响应报头:Location报头应包含指向新创建资源的规范URI。
实体主体:应该给出新创建资源的描述与链接。若已经在Location报头里给出了新资源的URI,那么可以用新资源的一个表示作为实体主体。

-202("Accepted")
重要程度:中等。

客户端的请求无法或将不被实时处理。请求稍后会被处理。请求看上去是合法的,但在实际处理它时有出现问题的可能。
若一个请求触发了一个异步操作,或者一个需要现实世界参与的动作,或者一个需要很长时间才能完成且没必要让Web客户端一直等待的动作时,这个相应代码是一个合适的选择。

响应报头:应该把未处理完的请求暴露为一个资源,以便客户端稍后查询其状态。Location报头可以包含指向该资源的URI。
实体主体:若无法让客户端稍后查询请求的状态,那么至少应该提供一个关于何时能处理该请求的估计。

203("Non-Authoritative Information")
重要程度:非常低。
这个响应代码跟200一样,只不过服务器想让客户端知道,有些响应报头并非来自该服务器--他们可能是从客户端先前发送的一个请求里复制的,或者从第三方得到的。

响应报头:客户端应明白某些报头可能是不准确的,某些响应报头可能不是服务器自己生成的,所以服务器也不知道其含义。

204("No Content")
重要程度:高。
若服务器拒绝对PUT、POST或者DELETE请求返回任何状态信息或表示,那么通常采用此响应代码。服务器也可以对GET请求返回此响应代码,这表明“客户端请求的资源存在,但其表示是空的”。注意与304("Not Modified")的区别。204常常用在Ajax应用里。服务器通过这个响应代码告诉客户端:客户端的输入已被接受,但客户端不应该改变任何UI元素。

实体主体:不允许。

205("Reset Content")
重要程度:低。
它与204类似,但与204不同的是,它表明客户端应重置数据源的视图或数据结构。假如你在浏览器里提交一个HTML表单,并得到响应代码204,那么表单里的各个字段值不变,可以继续修改它们;但假如得到的响应代码205,那么表单里的各个字段将被重置为它们的初始值。从数据录入方面讲:204适合对单条记录做一系列编辑,而205适于连续输入一组记录。

206("Partial Content")
重要程度:对于支持部分GET(partial GET)的服务而言“非常高”,其他情况下“低”。
它跟200类似,但它用于对部分GET请求(即使用Range请求报头的GET请求)的响应。部分GET请求常用于大型二进制文件的断点续传。

请求报头:客户端为Range请求报头设置一个值。
响应报头:需要提供Date报头。ETag报头与Content-Location报头的值应该跟正常GET请求相同。

若实体主体是单个字节范围(byte range),那么HTTP响应里必须包含一个Content-Range报头,以说明本响应返回的是表示的哪个部分,若实体主体是一个多部分实体(multipart entity)(即该实体主体由多个字节范围构成),那么每一个部分都要有自己的Content-Range报头。
实体主体:不是整个表示,而是一个或者多个字节范围。

3XX 重定向
3XX系列响应代码表明:客户端需要做些额外工作才能得到所需要的资源。它们通常用于GET请求。他们通常告诉客户端需要向另一个URI发送GET请求,才能得到所需的表示。那个URI就包含在Location响应报头里。

300("Multiple Choices")
重要程度:低。
若被请求的资源在服务器端存在多个表示,而服务器不知道客户端想要的是哪一个表示时,发送这个响应代码。或者当客户端没有使用Accept-*报头来指定一个表示,或者客户端所请求的表示不存在时,也发送这个响应代码。在这种情况下,一种选择是,服务器返回一个首选表示,并把响应代码设置为200,不过它也可以返回一个包含该资源各个表示的URI列表,并把响应代码设为300。

响应报头:如果服务器有首选表示,那么它可以在Location响应报头中给出这个首选表示的URI。跟其他3XX响应代码一样,客户端可以自动跟随Location中的URI。
实体主体:一个包含该资源各个表示的URI的列表。可以在表示中提供一些信息,以便用户作出选择。

301("Moved Permanently")
重要程度:中等。
服务器知道客户端试图访问的是哪个资源,但它不喜欢客户端用当前URI来请求该资源。它希望客户端记住另一个URI,并在今后的请求中使用那个新的URI。你可以通过这个响应代码来防止由于URI变更而导致老URI失效。

响应报头:服务器应当把规范URI放在Location响应报头里。
实体主体:服务器可以发送一个包含新URI的信息,不过这不是必需的。

302("Found")
重要程度:应该了解,特别市编写客户端时。但我不推荐使用它。
这个响应代码市造成大多数重定向方面的混乱的最根本原因。它应该是像307那样被处理。实际上,在HTTP 1.0中,响应代码302的名称是”Moved Temporarily”,不幸的是,在实际生活中,绝大多数客户端拿它像303一样处理。它的不同之处在于当服务器为客户端的PUT,POST或者DELETE请求返回302响应代码时,客户端要怎么做。
为了消除这一混淆,在HTTP 1.1中,该响应代码被重命名为"Found",并新加了一个响应代码307。这个响应代码目前仍在广泛使用,但它的含义市混淆的,所以我建议你的服务发送307或者303,而不要发送302.除非你知道正在与一个不能理解303或307的HTTP 1.0客户端交互。

响应报头:把客户端应重新请求的那个URI放在Location报头里。
实体主体:一个包含指向新URI的链接的超文本文档(就像301一样)。

303("See Other")
重要程度:高。
请求已经被处理,但服务器不是直接返回一个响应文档,而是返回一个响应文档的URI。该响应文档可能是一个静态的状态信息,也可能是一个更有趣的资源。对于后一种情况,303是一种令服务器可以“发送一个资源的表示,而不强迫客户端下载其所有数据”的方式。客户端可以向Location报头里的URI发送GET请求,但它不是必须这么做。
303响应代码是一种规范化资源URI的好办法。一个资源可以有多个URIs,但每个资源的规范URI只有一个,该资源的所有其他URIs都通过303指向该资源的规范URI,例如:303可以把一个对www.example.com/software/cu…

响应报头:Location报头里包含资源的URI。
实体主体:一个包含指向新URI的链接的超文本文档。

304("Not Modified")
重要程度:高。
这个响应代码跟204("No Content")类似:响应实体主体都必须为空。但204用于没有主体数据的情况,而304用于有主体数据,但客户端已拥有该数据,没必要重复发送的情况。这个响应代码可用于条件HTTP请求(conditional HTTP request).如果客户端在发送GET请求时附上了一个值为Sunday的If-Modified-Since报头,而客户端所请求的表示在服务器端自星期日(Sunday)以来一直没有改变过,那么服务器可以返回一个304响应。服务器也可以返回一个200响应,但由于客户端已拥有该表示,因此重复发送该表示只会白白浪费宽带。

响应报头:需要提供Date报头。Etag与Content-Location报头的值,应该跟返回200响应时的一样。若Expires, Cache-Control及Vary报头的值自上次发送以来已经改变,那么就要提供这些报头
实体主体:不允许。

305("Use Proxy")
重要程度:低。
这个响应代码用于告诉客户端它需要再发一次请求,但这次要通过一个HTTP代理发送,而不是直接发送给服务器。这个响应代码使用的不多,因为服务器很少在意客户端是否使用某一特定代理。这个代码主要用于基于代理的镜像站点。现在,镜像站点(如www.example.com.mysite.com/) 包含跟原始站点(如 www.example.com/) 一样的内容,但具有不同的URI,原始站点可以通过307把客户端重新定向到镜像站点上。假如有基于代理的镜像站点,那么你可以通过把 proxy.mysite.com/ 设为代理,使用跟原始URI(www.example.com/) 一样的URI来访问镜像站点。这里,原始站点example.com可以通过305把客户端路由到一个地理上接近客户端的镜像代理。web浏览器一般不能正确处理这个响应代码,这是导致305响应代码用的不多的另一个原因。

响应报头:Location报头里包含代理的URI。

306 未使用
重要程度:无
306 响应代码没有在HTTP标准中定义过。

307("Temporary Redirect")
重要程度:高。
请求还没有被处理,因为所请求的资源不在本地:它在另一个URI处。客户端应该向那个URI重新发送请求。就GET请求来说,它只是请求得到一个表示,该响应代码跟303没有区别。当服务器希望把客户端重新定向到一个镜像站点时,可以用307来响应GET请求。但对于POST,PUT及DELETE请求,它们希望服务器执行一些操作,307和303有显著区别。对POST,PUT或者DELETE请求响应303表明:操作已经成功执行,但响应实体将不随本响应一起返回,若客户端想要获取响应实体主体,它需要向另一个URI发送GET请求。而307表明:服务器尚未执行操作,客户端需要向Location报头里的那个URI重新提交整个请求。

响应报头: 把客户端应重新请求的那个URI放在Location报头里。
实体主体:一个包含指向新URI的链接的超文本文档。

4XX:客户端错误
这些响应代码表明客户端出现错误。不是认证信息有问题,就是表示格式或HTTP库本身有问题。客户端需要自行改正。

400("Bad Request")
重要程度:高。
这是一个通用的客户端错误状态,当其他4XX响应代码不适用时,就采用400。此响应代码通常用于“服务器收到客户端通过PUT或者POST请求提交的表示,表示的格式正确,但服务器不懂它什么意思”的情况。

实体主体:可以包含一个错误的描述文档。

401("Unauthorized")
重要程度:高。
客户端试图对一个受保护的资源进行操作,却又没有提供正确的认证证书。客户端提供了错误的证书,或者根本没有提供证书。这里的证书(credential)可以是一个用户名/密码,也可以市一个API key,或者一个认证令牌。客户端常常通过向一个URI发送请求,并查看收到401响应,以获知应该发送哪种证书,以及证书的格式。如果服务器不想让未授权的用户获知某个资源的存在,那么它可以谎报一个404而不是401。这样做的缺点是:客户端需要事先知道服务器接受哪种认证--这将导致HTTP摘要认证无法工作。

响应报头:WWW-Authenticate报头描述服务器将接受哪种认证。
实体主体:一个错误的描述文档。假如最终用户可通过“在网站上注册”的方式得到证书,那么应提供一个指向该注册页面的链接。

402("Payment Required")
重要程度:无。
除了它的名字外,HTTP标准没有对该响应的其他方面作任何定义。因为目前还没有用于HTTP的微支付系统,所以它被留作将来使用。尽管如此,若存在一个用于HTTP的微支付系统,那么这些系统将首先出现在web服务领域。如果想按请求向用户收费,而且你与用户之间的关系允许这么做的话,那么或许用得上这个响应代码。 注:该书印于2008年

403("Forbidden")
重要程度:中等。
客户端请求的结构正确,但是服务器不想处理它。这跟证书不正确的情况不同--若证书不正确,应该发送响应代码401。该响应代码常用于一个资源只允许在特定时间段内访问, 或者允许特定IP地址的用户访问的情况。403暗示了所请求的资源确实存在。跟401一样,若服务器不想透露此信息,它可以谎报一个404。既然客户端请求的结构正确,那为什么还要把本响应代码放在4XX系列(客户端错误),而不是5XX系列(服务端错误)呢?因为服务器不是根据请求的结构,而是根据请求的其他方面(比如说发出请求的时间)作出的决定的。

实体主体:一个描述拒绝原因的文档(可选)。

404("Not Found")
重要程度:高。
这也许是最广为人知的HTTP响应代码了。404表明服务器无法把客户端请求的URI转换为一个资源。相比之下,410更有用一些。web服务可以通过404响应告诉客户端所请求的URI是空的,然后客户端就可以通过向该URI发送PUT请求来创建一个新资源了。但是404也有可能是用来掩饰403或者401.

405("Method Not Allowd")
重要程度:中等。
客户端试图使用一个本资源不支持的HTTP方法。例如:一个资源只支持GET方法,但是客户端使用PUT方法访问。

响应报头:Allow报头列出本资源支持哪些HTTP方法,例如:Allow:GET,POST

406("Not Acceptable")
重要程度:中等。
当客户端对表示有太多要求,以至于服务器无法提供满足要求的表示,服务器可以发送这个响应代码。例如:客户端通过Accept头指定媒体类型为application/json+hic,但是服务器只支持application/json。服务器的另一个选择是:忽略客户端挑剔的要求,返回首选表示,并把响应代码设为200。

实体主体:一个可选表示的链接列表。

407("Proxy Authentication Required")
重要程度:低。
只有HTTP代理会发送这个响应代码。它跟401类似,唯一区别在于:这里不是无权访问web服务,而是无权访问代理。跟401一样,可能是因为客户端没有提供证书,也可能是客户端提供的证书不正确或不充分。

请求报头:客户端通过使用Proxy-Authorization报头(而不是Authorization)把证书提供给代理。格式跟Authrization一样。
响应报头:代理通过Proxy-Authenticate报头(而不是WWW-Authenticate)告诉客户端它接受哪种认证。格式跟WWW-Authenticate一样。

408("Reqeust Timeout")
重要程度:低。
假如HTTP客户端与服务器建立链接后,却不发送任何请求(或从不发送表明请求结束的空白行),那么服务器最终应该发送一个408响应代码,并关闭此连接。

409("Conflict")
重要程度:高。
此响应代码表明:你请求的操作会导致服务器的资源处于一种不可能或不一致的状态。例如你试图修改某个用户的用户名,而修改后的用户名与其他存在的用户名冲突了。

响应报头:若冲突是因为某个其他资源的存在而引起的,那么应该在Location报头里给出那个资源的URI。 实体主体:一个描述冲突的文档,以便客户端可以解决冲突。

410("Gone")
重要程度:中等。
这个响应代码跟404类似,但它提供的有用信息更多一些。这个响应代码用于服务器知道被请求的URI过去曾指向一个资源,但该资源现在不存在了的情况。服务器不知道
该资源的新URI,服务器要是知道该URI的话,它就发送响应代码301.410和310一样,都有暗示客户端不应该再请求该URI的意思,不同之处在于:410只是指出该资源不存在,但没有给出该资源的新URI。RFC2616建议“为短期的推广服务,以及属于个人但不继续在服务端运行的资源”采用410.

411("Length Required")
重要程度:低到中等。
若HTTP请求包含表示,它应该把Content-Length请求报头的值设为该表示的长度(以字节为单位)。对客户端而言,有时这不太方便(例如,当表示是来自其他来源的字节流时)。
所以HTTP并不要求客户端在每个请求中都提供Content-Length报头。但HTTP服务器可以要求客户端必须设置该报头。服务器可以中断任何没有提供Content-Length报头的请求,并要求客户端重新提交包含Content-Length报头的请求。这个响应代码就是用于中断未提供Content-Lenght报头的请求的。假如客户端提供错误的长度,或发送超过长度的表示,服务器可以中断请求并关闭链接,并返回响应代码413。

412("Precondition Failed")
重要程度:中等。
客户端在请求报头里指定一些前提条件,并要求服务器只有在满足一定条件的情况下才能处理本请求。若服务器不满足这些条件,就返回此响应代码。If-Unmodified-Since是一个常见的前提条件。客户端可以通过PUT请求来修改一个资源,但它要求,仅在自客户端最后一次获取该资源后该资源未被别人修改过才能执行修改操作。若没有这一前提条件,客户端可能会无意识地覆盖别人做的修改,或者导致409的产生。

请求报头:若客户但设置了If-Match,If-None-Match或If-Unmodified-Since报头,那就有可能得到这个响应代码。If-None-Match稍微特别一些。若客户端在发送GET或HEAD请求时指定了If-None-Match,并且服务器不满足该前提条件的话,那么响应代码不是412而是304,这是实现条件HTTP GET的基础。若客户端在发送PUT,POST或DELETE请求时指定了If-None-Match,并且服务器不满足该前提条件的话,那么响应代码是412.另外,若客户端指定了If-Match或If-Unmodified-Since(无论采用什么HTTP方法),而服务器不满足该前提条件的话,响应代码也是412。

413("Request Entity Too Large")
重要程度:低到中等。
这个响应代码跟411类似,服务器可以用它来中断客户端的请求并关闭连接,而不需要等待请求完成。411用于客户端未指定长度的情况,而413用于客户端发送的表示太大,以至于服务器无法处理。客户端可以先做一个LBYL(look-before-you-leap)请求,以免请求被413中断。若LBYL请求获得响应代码为100,客户端再提交完整的表示。

响应报头:如果因为服务器方面临时遇到问题(比如资源不足),而不是因为客户端方面的问题而导致中断请求的话,服务器可以把Retry-After报头的值设为一个日期或一个间隔时间,以秒为单位,以便客户端可以过段时间重试。

414("Request-URI Too Long")
重要程度:低。
HTTP标准并没有对URI长度作出官方限制,但大部分现有的web服务器都对URI长度有一个上限,而web服务可能也一样。导致URI超长的最常见的原因是:表示数据明明是该放在实体主体里的,但客户端却把它放在了URI里。深度嵌套的数据结构也有可能引起URI过长。

415("Unsupported Media Type")
重要程度:中等。
当客户端在发送表示时采用了一种服务器无法理解的媒体类型,服务器发送此响应代码。比如说,服务器期望的是XML格式,而客户端发送的确实JSON格式。 如果客户端采用的媒体类型正确,但格式有问题,这时最好返回更通用的400。

416("Requestd Range Not Satisfiable")
重要程度:低。
当客户端所请求的字节范围超出表示的实际大小时,服务器发送此响应代码。例如:你请求一个表示的1-100字节,但该表示总共只用99字节大小。

请求报头:仅当原始请求里包含Range报头时,才有可能收到此响应代码。若原始请求提供的是If-Range报头,则不会收到此响应代码。
响应报头:服务器应当通过Content-Range报头告诉客户端表示的实际大小。

417("Expectation Failed")
重要程度:中等。
此响应代码跟100正好相反。当你用LBYL请求来考察服务器是否会接受你的表示时,如果服务器确认会接受你的表示,那么你将获得响应代码100,否则你将获得417。

5XX 服务端错误
这些响应代码表明服务器端出现错误。一般来说,这些代码意味着服务器处于不能执行客户端请求的状态,此时客户端应稍后重试。有时,服务器能够估计客户端应在多久之后重试。并把该信息放在Retry-After响应报头里。

5XX系列响应代码在数量上不如4XX系列多,这不是因为服务器错误的几率小,而是因为没有必要如此详细--对于服务器方面的问题,客户端是无能为力的。

500("Internal Server Error")
重要程度:高。
这是一个通用的服务器错误响应。对于大多数web框架,如果在执行请求处理代码时遇到了异常,它们就发送此响应代码。

501("Not Implemented")
重要程度:低。
客户端试图使用一个服务器不支持的HTTP特性。 最常见的例子是:客户端试图做一个采用了拓展HTTP方法的请求,而普通web服务器不支持此请求。它跟响应代码405比较相似,405表明客户端所用的方法是一个可识别的方法,但该资源不支持,而501表明服务器根本不能识别该方法。

502("Bad Gateway")
重要程度:低。
只有HTTP代理会发送这个响应代码。它表明代理方面出现问题,或者代理与上行服务器之间出现问题,而不是上行服务器本身有问题。若代理根本无法访问上行服务器,响应代码将是504。

503("Service Unavailable")
重要程度:中等到高。
此响应代码表明HTTP服务器正常,只是下层web服务服务不能正常工作。最可能的原因是资源不足:服务器突然收到太多请求,以至于无法全部处理。由于此问题多半由客户端反复发送请求造成,因此HTTP服务器可以选择拒绝接受客户端请求而不是接受它,并发送503响应代码。

响应报头:服务器可以通过Retry-After报头告知客户端何时可以重试。

504("Gateway Timeout")
重要程度:低。
跟502类似,只有HTTP代理会发送此响应代码。此响应代码表明代理无法连接上行服务器。

505("HTTP Version Not Supported")
重要程度: 非常低。
当服务器不支持客户端试图使用的HTTP版本时发送此响应代码。

实体主体:一个描述服务器支持哪些协议的文档。

如何理解 URI?

URI, 全称为(Uniform Resource Identifier), 也就是统一资源标识符,它的作用很简单,就是区分互联网上不同的资源。

但是,它并不是我们常说的网址, 网址指的是URL, 实际上URI包含了URNURL两个部分,由于 URL 过于普及,就默认将 URI 视为 URL 了。

URI 的结构

URI 真正最完整的结构是这样的。

可能你会有疑问,好像跟平时见到的不太一样啊!先别急,我们来一一拆解。

scheme 表示协议名,比如http, https, file等等。后面必须和://连在一起。

user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。

host:port表示主机名和端口。

path表示请求路径,标记资源所在位置。

query表示查询参数,为key=val这种形式,多个键值对之间用&隔开。

fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置。

举个例子:

https://www.baidu.com/s?wd=HTTP&rsv_spt=1
复制代码

这个 URI 中,httpsscheme部分,www.baidu.comhost:port部分(注意,http 和 https 的默认端口分别为80、443),/spath部分,而wd=HTTP&rsv_spt=1就是query部分。

URI 编码

URI 只能使用ASCII, ASCII 之外的字符是不支持显示的,而且还有一部分符号是界定符,如果不加以处理就会导致解析出错。

因此,URI 引入了编码机制,将所有非 ASCII 码字符界定符转为十六进制字节值,然后在前面加个%

如,空格被转义成了%20三元被转义成了%E4%B8%89%E5%85%83

什么是持久连接/长连接

http1.0协议采用的是"请求-应答"模式,当使用普通模式,每个请求/应答客户与服务器都要新建一个连接,完成之后立即断开连接(http协议为无连接的协议)

http1.1版本支持长连接,即请求头添加Connection: Keep-Alive,使用Keep-Alive模式(又称持久连接,连接复用)建立一个TCP连接后使客户端到服务端的连接持续有效,可以发送/接受多个http请求/响应,当出现对服务器的后续请求时,Keep-Alive功能避免了建立或者重新建立连接

1.png

如图:短连接极大的降低了传输效率

长连接优缺点

优点

  • 减少CPU及内存的使用,因为不需要经常建立和关闭连接
  • 支持管道化的请求及响应模式
  • 减少网络堵塞,因为减少了TCP请求
  • 减少了后续请求的响应时间,因为不需要等待建立TCP、握手、挥手、关闭TCP的过程
  • 发生错误时,也可在不关闭连接的情况下进行错误提示

缺点

一个长连接建立后,如果一直保持连接,对服务器来说是多么的浪费资源呀,而且长连接时间的长短,直接影响到服务器的并发数

还有就是可能造成队头堵塞(下面有讲),造成信息延迟

如何避免长连接资源浪费?

  • 客户端请求头声明Connection: close,本次通信后就关闭连接

  • 服务端配置:如Nginx,设置keepalive_timeout设置长连接超时时间,keepalive_requests设置长连接请求次数上限

  • 系统内核参数设置

    • net.ipv4.tcp_keepalive_time = 60,连接闲置60秒后,服务端尝试向客户端发送侦测包,判断TCP连接状态,如果没有收到ack反馈就在
    • net.ipv4.tcp_keepalive_intvl = 10,就在10秒后再次尝试发送侦测包,直到收到ack反馈,一共会
    • net.ipv4.tcp_keepalive_probes = 5,一共会尝试5次,要是都没有收到就关闭这个TCP连接了

什么是管线化(管道化)

http1.1在使用长连接的情况下,建立一个连接通道后,连接上消息的传递类似于

请求1 -> 响应1 -> 请求2 -> 响应2 -> 请求3 -> 响应3

管理化连接的消息就变成了类似这样

请求1 -> 请求2 -> 请求3 -> 响应1 -> 响应2 -> 响应3

管线化是在同一个TCP连接里发一个请求后不必等其回来就可以继续发请求出去,这可以减少整体的响应时间,但是服务器还是会按照请求的顺序响应请求,所以如果有许多请求,而前面的请求响应很慢,就产生一个著名的问题队头堵塞(下面有讲解决方法)

管线化的特点:

  • 管线化机制通过持久连接完成,在http1.1版本才支持
  • 只有GET请求和HEAD请求才可以进行管线化,而POST有所限制
  • 初次创建连接时不应启动管线化机制,因为服务器不一定支持http1.1版本的协议
  • 管线化不会影响响应到来的顺序,如上面的例子所示,响应返回的顺序就是请求的顺序
  • 要求客户端服务端都支持管线化,但并不要求服务端也对响应进行管线化处理,只是要求对于管线化的请求不失败即可
  • 由于上面提到的服务端问题,开户管线化很可能并不会带来大幅度的性能提升,而且很多服务端和代理程序对管线化的支持并不好,因为浏览器(Chrome/Firefox)默认并未开启管线化支持

如何解决 HTTP 的队头阻塞问题

http1.0协议采用的是请求-应答模式,报文必须是一发一收,就形成了一个先进先出的串行队列,没有轻重缓急的优先级,只有入队的先后顺序,排在最前面的请求最先处理,就导致如果队首的请求耗时过长,后面的请求就只能处于阻塞状态,这就是著名的队头阻塞问题。解决如下:

并发连接

因为一个域名允许分配多个长连接,就相当于增加了任务队列,不至于一个队列里的任务阻塞了其他全部任务。以前在RFC2616中规定过客户端最多只能并发2个连接,但是现实是很多浏览器不按套路出牌,就是遵守这个标准T_T,所以在RFC7230把这个规定取消掉了,现在的浏览器标准中一个域名并发连接可以有6~8个,记住是6~8个,不是6个(Chrome6个/Firefox8个)

如果这个还不能满足你

继续,不要停...

域名分片

一个域名最多可以并发6~8个,那咱就多来几个域名

比如a.baidu.com,b.baidu.com,c.baidu.com,多准备几个二级域名,当我们访问baidu.com时,可以让不同的资源从不同的二域名中获取,而它们都指向同一台服务器,这样能够并发更多的长连接了

而在HTTP2.0下,可以一瞬间加载出来很多资源,因为支持多路复用,可以在一个TCP连接中发送多个请求

HTTP 如何处理大文件的传输?

对于几百 M 甚至上 G 的大文件来说,如果要一口气全部传输过来显然是不现实的,会有大量的等待时间,严重影响用户体验。因此,HTTP 针对这一场景,采取了范围请求的解决方案,允许客户端仅仅请求一个资源的一部分。

如何支持

当然,前提是服务器要支持范围请求,要支持这个功能,就必须加上这样一个响应头:

Accept-Ranges: none
复制代码

用来告知客户端这边是支持范围请求的。

Range 字段拆解

而对于客户端而言,它需要指定请求哪一部分,通过Range这个请求头字段确定,格式为bytes=x-y。接下来就来讨论一下这个 Range 的书写格式:

  • 0-499表示从开始到第 499 个字节。
  • 500- 表示从第 500 字节到文件终点。
  • -100表示文件的最后100个字节。

服务器收到请求之后,首先验证范围是否合法,如果越界了那么返回416错误码,否则读取相应片段,返回206状态码。

同时,服务器需要添加Content-Range字段,这个字段的格式根据请求头中Range字段的不同而有所差异。

具体来说,请求单段数据和请求多段数据,响应头是不一样的。

举个例子:

// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39

复制代码

接下来我们就分别来讨论着两种情况。

单段数据

对于单段数据的请求,返回的响应如下:

HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100

i am xxxxx
复制代码

值得注意的是Content-Range字段,0-9表示请求的返回,100表示资源的总大小,很好理解。

多段数据

接下来我们看看多段请求的情况。得到的响应会是下面这个形式:

HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes


--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96

i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96

eex jspy e
--00000010101--
复制代码

这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:

  • 请求一定是多段数据请求
  • 响应体中的分隔符是 00000010101

因此,在响应体中各段数据之间会由这里指定的分隔符分开,而且在最后的分隔末尾添上--表示结束。

以上就是 http 针对大文件传输所采用的手段。

HTTP 中如何处理表单数据的提交?

在 http 中,有两种主要的表单提交的方式,体现在两种不同的Content-Type取值:

  • application/x-www-form-urlencoded
  • multipart/form-data

由于表单提交一般是POST请求,很少考虑GET,因此这里我们将默认提交的数据放在请求体中。

application/x-www-form-urlencoded

对于application/x-www-form-urlencoded格式的表单内容,有以下特点:

  • 其中的数据会被编码成以&分隔的键值对
  • 字符以URL编码方式编码。

如:

// 转换过程: {a: 1, b: 2} -> a=1&b=2 -> 如下(最终形式)
"a%3D1%26b%3D2"
复制代码

multipart/form-data

对于multipart/form-data而言:

  • 请求头中的Content-Type字段会包含boundary,且boundary的值有浏览器默认指定。例: Content-Type: multipart/form-data;boundary=----WebkitFormBoundaryRRJKeWfHPGrS4LKe
  • 数据会分为多个部分,每两个部分之间通过分隔符来分隔,每部分表述均有 HTTP 头部描述子包体,如Content-Type,在最后的分隔符会加上--表示结束。

相应的请求体是下面这样:

Content-Disposition: form-data;name="data1";
Content-Type: text/plain
data1
----WebkitFormBoundaryRRJKeWfHPGrS4LKe
Content-Disposition: form-data;name="data2";
Content-Type: text/plain
data2
----WebkitFormBoundaryRRJKeWfHPGrS4LKe--
复制代码

小结

值得一提的是,multipart/form-data 格式最大的特点在于:每一个表单元素都是独立的资源表述。另外,你可能在写业务的过程中,并没有注意到其中还有boundary的存在,如果你打开抓包工具,确实可以看到不同的表单元素被拆分开了,之所以在平时感觉不到,是以为浏览器和 HTTP 给你封装了这一系列操作。

而且,在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因为没有必要做 URL 编码,带来巨大耗时的同时也占用了更多的空间。

HTTP 代理

常见的代理有两种:普通代理(中间人代理)隧道代理

普通代理(中间人代理)

1626169261669.jpg

如图:代理服务器相当于一个中间人,一直帮两边传递东西,好可怜~~

不过它可以在中间可以帮我们过滤、缓存、负载均衡(多台服务器共用一台代理情况下)等一些处理

注意,实际场景中客户端和服务器之间可能有多个代理服务器

隧道代理

客户端通过CONNECT方法请求隧道代理创建一个可以到任意目标服务器和端口号的TCP连接,创建成功之后隧道代理只做请求和响应数据的转发,中间它不会做任何处理

1626191263809.jpg

为什么需要隧道代理呢?

我们都知道https服务是需要网站有证书的,而代理服务器显然没有,所以浏览器和代理之间无法创建TLS,所以就有了隧道代理,它把浏览器的数据原样透传,这样就实现了通过中间代理和服务端进行TLS握手,然后进行加密传输

可能有人会问,那还要代理干嘛,直接请求服务器不是更好吗

代理服务器,到底有什么好处呢?

  • 突破访问限制:如访问一些单位或集团内部资源,或用国外代理服务器(翻墙),就可以上国外网站看片等
  • 安全性更高:上网者可以通过这种方式隐藏自己的IP,免受攻击。还可以对数据过滤,对非法IP限流等
  • 负载均衡:客户端请求先到代理服务器,而代理服务器后面有多少源服务器,IP是多少,客户端是不知道的。因此,代理服务器收到请求后,通过特定的算法(随机算法、轮询、一致性hash、LUR(最近最少使用) 算法这里不细说了)把请求分发给不同的源服务器,让各个源服务器负载尽量均衡
  • 缓存代理:将内容缓存到代理服务器(这个下面一节详细说)

代理最常见的请求头

Via

是一个能用首部,由代理服务器添加,适用于正向和反向代理,在请求和响应首部均可出现,这个消息首部可以用来追踪消息转发情况,防止循环请求,还可以识别在请求或响应传递链中消息发送者对于协议的支持能力,详情请看MDN

Via: 1.1 vegur
Via: HTTP/1.1 GWA
Via: 1.0 fred, 1.1 p.example.net
复制代码

X-Forwarded-For

记录客户端请求的来源IP,每经过一级代理(匿名代理除外),代理服务器都会把这次请求的来源IP追加进去

X-Forwarded-For: client,proxy1,proxy2
复制代码

注意:与服务器直连的代理服务器的IP不会被追加进去,该代理可能过TCP连接的Remote Address字段获取到与服务器直连的代理服务器IP

X-Real-IP

一般记录真实发出请求的客户端的IP,还有X-Forwarded-HostX-Forwarded-Proto分别记录真实发出请求的客户端的域名协议名

代理中客户端IP伪造问题以及如何预防?

X-Forwarded-For是可以伪造的,比如一些通过X-Forwarded-For获取到客户端IP来限制刷票的系统就可以通过伪造该请求头达到刷票的目的,如果客户端请求显示指定了

X-Forwarded-For:192.168.1.108
复制代码

那么服务端收到的这个请求头,第一个IP就是伪造的

预防

  1. 在对外Nginx服务器上配置
location / {
  proxy_set_header X-Forwarded-For $remote_addr
}
复制代码

这样第一个IP就是从TCP连接客户端的IP,不会读取伪造的

  1. 从右到左遍历X-Forwarded-For的IP,排除已知代理服务器IP和内网IP,获取到第一个符合条件的IP就可以了

正向代理和反向代理

正向代理

工作在客户端的代理为正向代理。使用正向代理的时候,需要在客户端配置需要使用的代理服务器,正向代理对服务端透明。比如抓包工具Fiddler、Charles以及访问一些外网网站的代理工具都是正向代理

1626238781500.jpg

正向代理通常用于

  • 缓存
  • 屏蔽某些不健康的网站
  • 通过代理访问原本无法访问的网站
  • 上网认证,对用户访问进行授权

反向代理

工作在服务端的代理称为反向代理。使用反向代理的时候,不需要在客户端进行设置,反向代理对客户端透明。如Nginx就是反向代理

1626239283393.jpg

反向代理通常用于负载均衡服务端缓存流量隔离日志金丝雀发布

代理中的长连接

在各个代理和服务器、客户端节点之间是一段一段的TCP连接,客户端通过代理访问目标服务器也叫逐段传输,用于逐段传输的请求头叫逐段传输头

逐段传输头会在每一段传输的中间代理中处理掉,不会传给下一个代理

标准的逐段传输头有:Keep-AliveTransfer-EncodingTEConnectionTrailerUpgradeProxy-AuthorizationProxy-Authenticate

Connection头决定当前事务完成后是否关闭连接,如果该值为keep-alive,则连接是持久连接不会关闭,使得对同一服务器的请求可以继续在该连接上完成

如何理解 HTTP 缓存及缓存代理?

关于强缓存协商缓存的内容,我已经在能不能说一说浏览器缓存做了详细分析,小结如下:

首先通过 Cache-Control 验证强缓存是否可用

  • 如果强缓存可用,直接使用

  • 否则进入协商缓存,即发送 HTTP 请求,服务器通过请求头中的If-Modified-Since或者If-None-Match这些条件请求字段检查资源是否更新

    • 若资源更新,返回资源和200状态码
    • 否则,返回304,告诉浏览器直接从缓存获取资源

这一节我们主要来说说另外一种缓存方式: 代理缓存

为什么产生代理缓存?

对于源服务器来说,它也是有缓存的,比如Redis, Memcache,但对于 HTTP 缓存来说,如果每次客户端缓存失效都要到源服务器获取,那给源服务器的压力是很大的。

由此引入了缓存代理的机制。让代理服务器接管一部分的服务端HTTP缓存,客户端缓存过期后就近到代理缓存中获取,代理缓存过期了才请求源服务器,这样流量巨大的时候能明显降低源服务器的压力。

那缓存代理究竟是如何做到的呢?

总的来说,缓存代理的控制分为两部分,一部分是源服务器端的控制,一部分是客户端的控制。

源服务器的缓存控制

private 和 public

在源服务器的响应头中,会加上Cache-Control这个字段进行缓存控制字段,那么它的值当中可以加入private或者public表示是否允许代理服务器缓存,前者禁止,后者为允许。

比如对于一些非常私密的数据,如果缓存到代理服务器,别人直接访问代理就可以拿到这些数据,是非常危险的,因此对于这些数据一般是不会允许代理服务器进行缓存的,将响应头部的Cache-Control设为private,而不是public

proxy-revalidate

must-revalidate的意思是客户端缓存过期就去源服务器获取,而proxy-revalidate则表示代理服务器的缓存过期后到源服务器获取。

s-maxage

sshare的意思,限定了缓存在代理服务器中可以存放多久,和限制客户端缓存时间的max-age并不冲突。

讲了这几个字段,我们不妨来举个小例子,源服务器在响应头中加入这样一个字段:

Cache-Control: public, max-age=1000, s-maxage=2000
复制代码

相当于源服务器说: 我这个响应是允许代理服务器缓存的,客户端缓存过期了到代理中拿,并且在客户端的缓存时间为 1000 秒,在代理服务器中的缓存时间为 2000 s。

客户端的缓存控制

max-stale 和 min-fresh

在客户端的请求头中,可以加入这两个字段,来对代理服务器上的缓存进行宽容限制操作。比如:

max-stale: 5
复制代码

表示客户端到代理服务器上拿缓存的时候,即使代理缓存过期了也不要紧,只要过期时间在5秒之内,还是可以从代理中获取的。

又比如:

min-fresh: 5
复制代码

表示代理缓存需要一定的新鲜度,不要等到缓存刚好到期再拿,一定要在到期前 5 秒之前的时间拿,否则拿不到。

only-if-cached

这个字段加上后表示客户端只会接受代理缓存,而不会接受源服务器的响应。如果代理缓存无效,则直接返回504(Gateway Timeout)

以上便是缓存代理的内容,涉及的字段比较多,希望能好好回顾一下,加深理解。

TLS1.2 握手的过程是怎样的?

之前谈到了 HTTP 是明文传输的协议,传输保文对外完全透明,非常不安全,那如何进一步保证安全性呢?

由此产生了 HTTPS,其实它并不是一个新的协议,而是在 HTTP 下面增加了一层 SSL/TLS 协议,简单的讲,HTTPS = HTTP + SSL/TLS

那什么是 SSL/TLS 呢?

SSL 即安全套接层(Secure Sockets Layer),在 OSI 七层模型中处于会话层(第 5 层)。之前 SSL 出过三个大版本,当它发展到第三个大版本的时候才被标准化,成为 TLS(传输层安全,Transport Layer Security),并被当做 TLS1.0 的版本,准确地说,TLS1.0 = SSL3.1

现在主流的版本是 TLS/1.2, 之前的 TLS1.0、TLS1.1 都被认为是不安全的,在不久的将来会被完全淘汰。因此我们接下来主要讨论的是 TLS1.2, 当然在 2018 年推出了更加优秀的 TLS1.3,大大优化了 TLS 握手过程,这个我们放在下一节再去说。

TLS 握手的过程比较复杂,写文章之前我查阅了大量的资料,发现对 TLS 初学者非常不友好,也有很多知识点说的含糊不清,可以说这个整理的过程是相当痛苦了。希望我下面的拆解能够帮你理解得更顺畅些吧 : )

传统 RSA 握手

先来说说传统的 TLS 握手,也是大家在网上经常看到的。我之前也写过这样的文章,(传统RSA版本)HTTPS为什么让数据传输更安全,其中也介绍到了对称加密非对称加密的概念,建议大家去读一读,不再赘述。之所以称它为 RSA 版本,是因为它在加解密pre_random的时候采用的是 RSA 算法。

TLS 1.2 握手过程

现在我们来讲讲主流的 TLS 1.2 版本所采用的方式。

刚开始你可能会比较懵,先别着急,过一遍下面的流程再来看会豁然开朗。

step 1: Client Hello

首先,浏览器发送 client_random、TLS版本、加密套件列表。

client_random 是什么?用来最终 secret 的一个参数。

加密套件列表是什么?我举个例子,加密套件列表一般张这样:

TLS_ECDHE_WITH_AES_128_GCM_SHA256
复制代码

意思是TLS握手过程中,使用ECDHE算法生成pre_random(这个数后面会介绍),128位的AES算法进行对称加密,在对称加密的过程中使用主流的GCM分组模式,因为对称加密中很重要的一个问题就是如何分组。最后一个是哈希摘要算法,采用SHA256算法。

其中值得解释一下的是这个哈希摘要算法,试想一个这样的场景,服务端现在给客户端发消息来了,客户端并不知道此时的消息到底是服务端发的,还是中间人伪造的消息呢?现在引入这个哈希摘要算法,将服务端的证书信息通过这个算法生成一个摘要(可以理解为比较短的字符串),用来标识这个服务端的身份,用私钥加密后把加密后的标识自己的公钥传给客户端。客户端拿到这个公钥来解密,生成另外一份摘要。两个摘要进行对比,如果相同则能确认服务端的身份。这也就是所谓数字签名的原理。其中除了哈希算法,最重要的过程是私钥加密,公钥解密

step 2: Server Hello

可以看到服务器一口气给客户端回复了非常多的内容。

server_random也是最后生成secret的一个参数, 同时确认 TLS 版本、需要使用的加密套件和自己的证书,这都不难理解。那剩下的server_params是干嘛的呢?

我们先埋个伏笔,现在你只需要知道,server_random到达了客户端。

step 3: Client 验证证书,生成secret

客户端验证服务端传来的证书签名是否通过,如果验证通过,则传递client_params这个参数给服务器。

接着客户端通过ECDHE算法计算出pre_random,其中传入两个参数:server_paramsclient_params。现在你应该清楚这个两个参数的作用了吧,由于ECDHE基于椭圆曲线离散对数,这两个参数也称作椭圆曲线的公钥

客户端现在拥有了client_randomserver_randompre_random,接下来将这三个数通过一个伪随机数函数来计算出最终的secret

step4: Server 生成 secret

刚刚客户端不是传了client_params过来了吗?

现在服务端开始用ECDHE算法生成pre_random,接着用和客户端同样的伪随机数函数生成最后的secret

注意事项

TLS的过程基本上讲完了,但还有两点需要注意。

第一、实际上 TLS 握手是一个双向认证的过程,从 step1 中可以看到,客户端有能力验证服务器的身份,那服务器能不能验证客户端的身份呢?

当然是可以的。具体来说,在 step3中,客户端传送client_params,实际上给服务器传一个验证消息,让服务器将相同的验证流程(哈希摘要 + 私钥加密 + 公钥解密)走一遍,确认客户端的身份。

第二、当客户端生成secret后,会给服务端发送一个收尾的消息,告诉服务器之后的都用对称加密,对称加密的算法就用第一次约定的。服务器生成完secret也会向客户端发送一个收尾的消息,告诉客户端以后就直接用对称加密来通信。

这个收尾的消息包括两部分,一部分是Change Cipher Spec,意味着后面加密传输了,另一个是Finished消息,这个消息是对之前所有发送的数据做的摘要,对摘要进行加密,让对方验证一下。

当双方都验证通过之后,握手才正式结束。后面的 HTTP 正式开始传输加密报文。

RSA 和 ECDHE 握手过程的区别

  1. ECDHE 握手,也就是主流的 TLS1.2 握手中,使用ECDHE实现pre_random的加密解密,没有用到 RSA。
  2. 使用 ECDHE 还有一个特点,就是客户端发送完收尾消息后可以提前抢跑,直接发送 HTTP 报文,节省了一个 RTT,不必等到收尾消息到达服务器,然后等服务器返回收尾消息给自己,直接开始发请求。这也叫TLS False Start

TLS 1.3 做了哪些改进?

TLS 1.2 虽然存在了 10 多年,经历了无数的考验,但历史的车轮总是不断向前的,为了获得更强的安全、更优秀的性能,在2018年就推出了 TLS1.3,对于TLS1.2做了一系列的改进,主要分为这几个部分:强化安全提高性能

强化安全

在 TLS1.3 中废除了非常多的加密算法,最后只保留五个加密套件:

  • TLS_AES_128_GCM_SHA256
  • TLS_AES_256_GCM_SHA384
  • TLS_CHACHA20_POLY1305_SHA256
  • TLS_AES_128_GCM_SHA256
  • TLS_AES_128_GCM_8_SHA256

可以看到,最后剩下的对称加密算法只有 AESCHACHA20,之前主流的也会这两种。分组模式也只剩下 GCMPOLY1305, 哈希摘要算法只剩下了 SHA256SHA384 了。

那你可能会问了, 之前RSA这么重要的非对称加密算法怎么不在了?

我觉得有两方面的原因:

第一、2015年发现了FREAK攻击,即已经有人发现了 RSA 的漏洞,能够进行破解了。

第二、一旦私钥泄露,那么中间人可以通过私钥计算出之前所有报文的secret,破解之前所有的密文。

为什么?回到 RSA 握手的过程中,客户端拿到服务器的证书后,提取出服务器的公钥,然后生成pre_random并用公钥加密传给服务器,服务器通过私钥解密,从而拿到真实的pre_random。当中间人拿到了服务器私钥,并且截获之前所有报文的时候,那么就能拿到pre_randomserver_randomclient_random并根据对应的随机数函数生成secret,也就是拿到了 TLS 最终的会话密钥,每一个历史报文都能通过这样的方式进行破解。

ECDHE在每次握手时都会生成临时的密钥对,即使私钥被破解,之前的历史消息并不会收到影响。这种一次破解并不影响历史信息的性质也叫前向安全性

RSA 算法不具备前向安全性,而 ECDHE 具备,因此在 TLS1.3 中彻底取代了RSA

提升性能

要算法

主要用于保证信息的完整性。常见的MD5算法散列函数哈希函数都属于这类算法,其特点就是单向性无法反推原文

假如信息被截取,并重新生成了摘要,这时候就判断不出来是否被篡改了,所以需要给摘要也通过会话密钥进行加密,这样就看不到明文信息,保证了安全性,同时也保证了完整性

如何保证数据不被篡改?签名原理和证书?

数字证书(数字签名)

它可以帮我们验证服务器身份。因为如果没有验证的话,就可能被中间人劫持,假如请求被中间人截获,中间人把他自己的公钥给了客户端,客户端收到公钥就把信息发给中间人了,中间人解密拿到数据后,再请求实际服务器,拿到服务器公钥,再把信息发给服务器

这样不知不觉间信息就被人窃取了,所以在结合对称和非对称加密的基础上,又添加了数字证书认证的步骤,让服务器证明自己的身份

数字证书需要向有权威的认证机构(CA)获取授权给服务器。首先,服务器CA机构分别有一对密钥(公钥和私钥),然后是如何生成数字证书的呢?

  • CA机构通过摘要算法生成服务器公钥的摘要(哈希摘要)
  • CA机构通过CA私钥及特定的签名算法加密摘要,生成签名
  • 签名服务器公钥等信息打包放入数字证书,并返回给服务器

服务器配置好证书,以后客户端连接服务器,都先把证书发给客户端验证并获取服务器的公钥。

证书验证流程

  • 使用CA公钥和声明的签名算法对CA中的签名进行解密,得到服务器公钥的摘要内容
  • 再用摘要算法对证书里的服务器公钥生成摘要,再把这个摘要和上一步得到的摘要对比,如果一致说明证书合法,里面的公钥也是正确的,否则就是非法的

证书认证又分为单向认证双向认证

单向认证:服务器发送证书,客户端验证证书
双向认证:服务器和客户端分别提供证书给对方,并互相验证对方的证书

不过大多数https服务器都是单向认证,如果服务器需要验证客户端的身份,一般通过用户名、密码、手机验证码等之类的凭证来验证。只有更高级别的要求的系统,比如大额网银转账等,就会提供双向认证的场景,来确保对客户身份提供认证性

握手改进

流程如下:

大体的方式和 TLS1.2 差不多,不过和 TLS 1.2 相比少了一个 RTT, 服务端不必等待对方验证证书之后才拿到client_params,而是直接在第一次握手的时候就能够拿到, 拿到之后立即计算secret,节省了之前不必要的等待时间。同时,这也意味着在第一次握手的时候客户端需要传送更多的信息,一口气给传完。

这种 TLS 1.3 握手方式也被叫做1-RTT握手。但其实这种1-RTT的握手方式还是有一些优化的空间的,接下来我们来一一介绍这些优化方式。

会话复用

会话复用有两种方式: Session IDSession Ticket

先说说最早出现的Seesion ID,具体做法是客户端和服务器首次连接后各自保存会话的 ID,并存储会话密钥,当再次连接时,客户端发送ID过来,服务器查找这个 ID 是否存在,如果找到了就直接复用之前的会话状态,会话密钥不用重新生成,直接用原来的那份。

但这种方式也存在一个弊端,就是当客户端数量庞大的时候,对服务端的存储压力非常大。

因而出现了第二种方式——Session Ticket。它的思路就是: 服务端的压力大,那就把压力分摊给客户端呗。具体来说,双方连接成功后,服务器加密会话信息,用Session Ticket消息发给客户端,让客户端保存下来。下次重连的时候,就把这个 Ticket 进行解密,验证它过没过期,如果没过期那就直接恢复之前的会话状态。

这种方式虽然减小了服务端的存储压力,但与带来了安全问题,即每次用一个固定的密钥来解密 Ticket 数据,一旦黑客拿到这个密钥,之前所有的历史记录也被破解了。因此为了尽量避免这样的问题,密钥需要定期进行更换。

总的来说,这些会话复用的技术在保证1-RTT的同时,也节省了生成会话密钥这些算法所消耗的时间,是一笔可观的性能提升。

PSK

刚刚说的都是1-RTT情况下的优化,那能不能优化到0-RTT呢?

答案是可以的。做法其实也很简单,在发送Session Ticket的同时带上应用数据,不用等到服务端确认,这种方式被称为Pre-Shared Key,即 PSK。

这种方式虽然方便,但也带来了安全问题。中间人截获PSK的数据,不断向服务器重复发,类似于 TCP 第一次握手携带数据,增加了服务器被攻击的风险。

HTTP/2 有哪些改进?

由于 HTTPS 在安全方面已经做的非常好了,HTTP 改进的关注点放在了性能方面。对于 HTTP/2 而言,它对于性能的提升主要在于两点:

  • 头部压缩
  • 多路复用

当然还有一些颠覆性的功能实现:

  • 设置请求优先级
  • 服务器推送

这些重大的提升本质上也是为了解决 HTTP 本身的问题而产生的。接下来我们来看看 HTTP/2 解决了哪些问题,以及解决方式具体是如何的。

头部压缩

在 HTTP/1.1 及之前的时代,请求体一般会有响应的压缩编码过程,通过Content-Encoding头部字段来指定,但你有没有想过头部字段本身的压缩呢?当请求字段非常复杂的时候,尤其对于 GET 请求,请求报文几乎全是请求头,这个时候还是存在非常大的优化空间的。HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK,对请求头进行压缩。

HPACK 算法是专门为 HTTP/2 服务的,它主要的亮点有两个:

  • 首先是在服务器和客户端之间建立哈希表,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只需要把索引(比如0,1,2,...)传给对方即可,对方拿到索引查表就行了。这种传索引的方式,可以说让请求头字段得到极大程度的精简和复用。

HTTP/2 当中废除了起始行的概念,将起始行中的请求方法、URI、状态码转换成了头字段,不过这些字段都有一个":"前缀,用来和其它请求头区分开。

  • 其次是对于整数和字符串进行哈夫曼编码,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。

多路复用

HTTP 队头阻塞

我们之前讨论了 HTTP 队头阻塞的问题,其根本原因在于HTTP 基于请求-响应的模型,在同一个 TCP 长连接中,前面的请求没有得到响应,后面的请求就会被阻塞。

后面我们又讨论到用并发连接域名分片的方式来解决这个问题,但这并没有真正从 HTTP 本身的层面解决问题,只是增加了 TCP 连接,分摊风险而已。而且这么做也有弊端,多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。

而 HTTP/2 便从 HTTP 协议本身解决了队头阻塞问题。注意,这里并不是指的TCP队头阻塞,而是HTTP队头阻塞,两者并不是一回事。TCP 的队头阻塞是在数据包层面,单位是数据包,前一个报文没有收到便不会将后面收到的报文上传给 HTTP,而HTTP 的队头阻塞是在 HTTP 请求-响应层面,前一个请求没处理完,后面的请求就要阻塞住。两者所在的层次不一样。

那么 HTTP/2 如何来解决所谓的队头阻塞呢?

二进制分帧

首先,HTTP/2 认为明文传输对机器而言太麻烦了,不方便计算机的解析,因为对于文本而言会有多义性的字符,比如回车换行到底是内容还是分隔符,在内部需要用到状态机去识别,效率比较低。于是 HTTP/2 干脆把报文全部换成二进制格式,全部传输01串,方便了机器的解析。

原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。分帧之后,服务器看到的不再是一个个完整的 HTTP 请求报文,而是一堆乱序的二进制帧。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。

通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做(Stream)。HTTP/2 用来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。

可能你会有一个疑问,既然是乱序首发,那最后如何来处理这些乱序的数据帧呢?

首先要声明的是,所谓的乱序,指的是不同 ID 的 Stream 是乱序的,但同一个 Stream ID 的帧一定是按顺序传输的。二进制帧到达后对方会将 Stream ID 相同的二进制帧组装成完整的请求报文响应报文。当然,在二进制帧当中还有其他的一些字段,实现了优先级流量控制等功能,我们放到下一节再来介绍。

HTTP/2 中的二进制帧是如何设计的?

帧结构

HTTP/2 中传输的帧结构如下图所示:

每个帧分为帧头帧体。先是三个字节的帧长度,这个长度表示的是帧体的长度。

然后是帧类型,大概可以分为数据帧控制帧两种。数据帧用来存放 HTTP 报文,控制帧用来管理的传输。

接下来的一个字节是帧标志,里面一共有 8 个标志位,常用的有 END_HEADERS表示头数据结束,END_STREAM表示单方向数据发送结束。

后 4 个字节是Stream ID, 也就是流标识符,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。

流的状态变化

从前面可以知道,在 HTTP/2 中,所谓的,其实就是二进制帧的双向传输的序列。那么在 HTTP/2 请求和响应的过程中,流的状态是如何变化的呢?

HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里我们以一个普通的请求-响应过程为例来说明:

最开始两者都是空闲状态,当客户端发送Headers帧后,开始分配Stream ID, 此时客户端的打开, 服务端接收之后服务端的也打开,两端的都打开之后,就可以互相传递数据帧和控制帧了。

当客户端要关闭时,向服务端发送END_STREAM帧,进入半关闭状态, 这个时候客户端只能接收数据,而不能发送数据。

服务端收到这个END_STREAM帧后也进入半关闭状态,不过此时服务端的情况是只能发送数据,而不能接收数据。随后服务端也向客户端发送END_STREAM帧,表示数据发送完毕,双方进入关闭状态

如果下次要开启新的,流 ID 需要自增,直到上限为止,到达上限后开一个新的 TCP 连接重头开始计数。由于流 ID 字段长度为 4 个字节,最高位又被保留,因此范围是 0 ~ 2的 31 次方,大约 21 亿个。

流的特性

刚刚谈到了流的状态变化过程,这里顺便就来总结一下传输的特性:

  • 并发性。一个 HTTP/2 连接上可以同时发多个帧,这一点和 HTTP/1 不同。这也是实现多路复用的基础。
  • 自增性。流 ID 是不可重用的,而是会按顺序递增,达到上限之后又新开 TCP 连接从头开始。
  • 双向性。客户端和服务端都可以创建流,互不干扰,双方都可以作为发送方或者接收方
  • 可设置优先级。可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。

HTTP 版本

1991年HTTP 0.9版,只有一个GET,而且只支持纯文本内容,早已过时就不讲了

HTTP 1.0(1996年)

  • 任意数据类型都可以发送
  • 有GET、POST、HEAD三种方法
  • 无法复用TCP连接(长连接)
  • 有丰富的请求响应头信息。以header中的Last-Modified/If-Modified-SinceExpires作为缓存标识

HTTP 1.1(1997年)

  • 引入更多的请求方法类型PUTPATCHDELETEOPTIONSTRACECONNECT
  • 引入长连接,就是TCP连接默认不关闭,可以被多个请求复用,通过请求头connection:keep-alive设置
  • 引入管道连接机制,可以在同一TCP连接里,同时发送多个请求
  • 强化了缓存管理和控制Cache-ControlETag/If-None-Match
  • 支持分块响应,断点续传,利于大文件传输,能过请求头中的Range实现
  • 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机,并且共享一个IP地址

缺点:主要是连接缓慢,服务器只能按顺序响应,如果某个请求花了很长时间,就会出现请求队头阻塞

虽然出了很多优化技巧:为了增加并发请求,做域名拆分、资源合并、精灵图、资源预取...等等

最终为了推进从协议上进行优化,Google跳出来,推出SPDY协议

SPDY(2009年)

SPDY(读作“SPeeDY”)是Google开发的基于TCP的会话层协议

主要通过帧、多路复用、请求优先级、HTTP报头压缩、服务器推送以最小化网络延迟,提升网络速度,优化用户的网络使用体验

原理是在SSL层上增加一个SPDY会话层,以在一个TCP连接中实现并发流。通常的HTTP GET和POST格式仍然是一样的,然而SPDY为编码和传输数据设计了一个新的帧格式。因为流是双向的,所以可以在客户端和服务端启动

虽然诞生后很快被所有主流浏览器所采用,并且服务器和代理也提供了支持,但是SPDY核心人员后来都参加到HTTP 2.0开发中去了,自HTTP2.0开发完成就不再支持SPDY协议了,并在Chrome 51中删掉了SPDY的支持

HTTP 2.0(2015年)

说出http2中至少三个新特性?

  • 使用新的二进制协议,不再是纯文本,避免文本歧义,缩小了请求体积
  • 多路复用,同域名下所有通信都是在单链接(双向数据流)完成,提高连接的复用率,在拥塞控制方面有更好的能力提升
  • 使用HPACK算法将头部压缩,用哈夫曼编码建立索表,传送索引大大节约了带宽
  • 允许服务端主动推送数据给客户端
  • 增加了安全性,使用HTTP 2.0,要求必须至少TLS 1.2
  • 使用虚拟的流传输消息,解决了应用层的队头阻塞问题

缺点

  • TCP以及TCP+TLS建立连接的延时,HTTP2使用TCP协议来传输的,而如果使用HTTPS的话,还需要TLS协议进行安全传输,而使用TLS也需要一个握手过程,在传输数据之前,导致我们花掉3~4个RTT
  • TCP的队头阻塞并没有彻底解决。在HTTP2中,多个请求跑在一个TCP管道中,但当HTTP2出现丢包时,整个TCP都要开始等待重传,那么就会阻塞该TCP连接中的所有请求

SPDY 和 HTTP2 的区别

  • 头部压缩算法,SPDY是通用的deflate算法,HTTP2是专门为压缩头部设计的HPACK算法
  • SPDY必须在TLS上运行,HTTP2可在TCP上直接使用,因为增加了HTTP1.1的Upgrade机制
  • SPDY更加完善的协议商讨和确认流程
  • SPDY更加完善的Server Push流程
  • SPDY增加控制帧的种类,并对帧的格式考虑的更细致

HTTP1 和 HTTP2

  • HTTP2是一个二进制协议,HTTP1是超文本协议,传输的内容都不是一样的
  • HTTP2报头压缩,可以使用HPACK进行头部压缩,HTTP1则不论什么请求都会发送
  • HTTP2服务端推送(Server push),允许服务器预先将网页所需要的资源push到浏览器的内存当中
  • HTTP2遵循多路复用,代替同一域名下的内容,只建立一次连接,HTTP1.x不是,对域名有6~8个连接限制
  • HTTP2引入二进制数据帧的概念,其中帧对数据进行顺序标识,这样浏览器收到数据之后,就可以按照序列对数据进行合并,而不会出现合并后数据错乱的情况,同样是因为有了序列,服务器就可以并行的传输数据,这就是流所做的事情。HTTP2对同一域名下所有请求都是基于流的,也就是说同一域名下不管访问多少文件,只建立一次连接

HTTP 3.0/QUIC

由于HTTP 2.0依赖于TCP,TCP有什么问题那HTTP2就会有什么问题。最主要的还是队头阻塞,在应用层的问题解决了,可是在TCP协议层的队头阻塞还没有解决。

TCP在丢包的时候会进行重传,前面有一个包没收到,就只能把后面的包放到缓冲区,应用层是无法取数据的,也就是说HTTP2的多路复用并行性对于TCP的丢失恢复机制不管用,因此丢失或重新排序的数据都会导致交互挂掉

为了解决这个问题,Google又发明了QUIC协议

并在2018年11月将QUIC正式改名为HTTP 3.0

特点

  • 在传输层直接干掉TCP,用UDP替代
  • 实现了一套新的拥塞控制算法,彻底解决TCP中队头阻塞的问题
  • 实现了类似TCP的流量控制、传输可靠性的功能。虽然UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些TCP中存在的特性
  • 实现了快速握手功能。由于QUIC是基于UDP的,所以QUIC可以实现使用0-RTT或者1-RTT来建立连接,这意味着QUIC可以用最快的速度来发送和接收数据。
  • 集成了TLS加密功能。目前QUIC使用的是TLS1.3

三次握手

由于在面试中,三次握手是被问的最频繁的面试题,所以本次我们从面试的角度来讲解三次握手

当面试官问你为什么需要有三次握手、三次握手的作用、讲讲三次三次握手的时候,我想很多人会这样回答:

首先很多人会先讲下握手的过程:

1、第一次握手:客户端给服务器发送一个 SYN 报文。

2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。

3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。

4、服务器收到 ACK 报文之后,三次握手建立完成。

作用是为了确认双方的接收与发送能力是否正常。

这里我顺便解释一下为啥只有三次握手才能确认双方的接受与发送能力是否正常,而两次却不可以

第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。

第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。

第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

因此,需要三次握手才能确认双方的接收与发送能力是否正常。

这样回答其实也是可以的,但我觉得,这个过程的我们应该要描述的更详细一点,因为三次握手的过程中,双方是由很多状态的改变的,而这些状态,也是面试官可能会问的点。所以我觉得在回答三次握手的时候,我们应该要描述的详细一点,而且描述的详细一点意味着可以扯久一点。加分的描述我觉得应该是这样:

刚开始客户端处于 closed 的状态,服务端处于 listen 状态。然后

1、第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 SN(c) 。此时客户端处于 SYN_Send 状态。

2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s),同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 *SYN_REVD *的状态。

3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 establised 状态。

4、服务器收到 ACK 报文之后,也处于 establised 状态,此时,双方以建立起了链接。

三次握手的作用

三次握手的作用也是有好多的,多记住几个,保证不亏。例如:

1、确认双方的接受能力、发送能力是否正常。

2、指定自己的初始化序列号,为后面的可靠传送做准备。

单单这样还不足以应付三次握手,面试官可能还会问一些其他的问题,例如:

1、(ISN)是固定的吗

三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。

如果ISN是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。

2、什么是半连接队列

服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。

这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超 过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, ....

3、三次握手过程中可以携带数据吗

很多人可能会认为三次握手都不能携带数据,其实第三次握手的时候,是可以携带数据的。也就是说,第一次、第二次握手不可以携带数据,而第三次握手是可以携带数据的。

为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据,因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。也就是说,第一次握手可以放数据的话,其中一个简单的原因就是会让服务器更加容易受到攻击了。

而对于第三次的话,此时客户端已经处于 established 状态,也就是说,对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据页没啥毛病。

四次挥手

由于在面试中,三次握手是被问的最频繁的面试题,所以本次我们从面试的角度来讲解三次握手

四次挥手也一样,千万不要对方一个 FIN 报文,我方一个 ACK 报文,再我方一个 FIN 报文,我方一个 ACK 报文。然后结束,最好是说的详细一点,例如想下面这样就差不多了,要把每个阶段的状态记好,我上次面试就被问了几个了,呵呵。我答错了,还以为自己答对了,当时还解释的头头是道,呵呵。

刚开始双方都处于 establised 状态,假如是客户端先发起关闭请求,则:

1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。

2、第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。

3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态

5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

这里特别需要主要的就是TIME_WAIT这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。

至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。

这里我给出每个状态所包含的含义,有兴趣的可以看看。

LISTEN - 侦听来自远方TCP端口的连接请求;

SYN-SENT -在发送连接请求后等待匹配的连接请求;

SYN-RECEIVED - 在收到和发送一个连接请求后等待对连接请求的确认;

ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;

FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;

FIN-WAIT-2 - 从远程TCP等待连接中断请求;

CLOSE-WAIT - 等待从本地用户发来的连接中断请求;

CLOSING -等待远程TCP对连接中断的确认;

LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;

TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;

CLOSED - 没有任何连接状态;

最后,在放在三次握手与四次挥手的图

参考文章: 其实是整合了众多文章的。