HTTP协议必知必会详解
系列文章目录文章目录系列文章目录摘要一、开篇你真的分得清 HTTP 和 HTML 吗二、HTTP 的本质浏览器与服务器的 约定语言三、一次完整的 HTTP 请求到底经历了什么四、拆解 HTTP 报文请求与响应的内部结构4.1 HTTP 请求报文4.2 HTTP 响应报文五、无状态协议的 破局者Cookie 与 Session5.1 Cookie存在客户端的 身份小纸条5.2 Session存在服务端的 用户储物柜5.3 Session 的创建与管理六、解惑长连接和无状态真的矛盾吗总结摘要在学习 Web 容器、后端开发的过程中HTTP 协议是绕不开的基础。很多开发者虽然日常天天用 HTTP却对其核心机制一知半解HTTP 和 HTML 到底有什么区别无状态协议是怎么实现用户登录状态保持的长连接和无状态为什么不矛盾本文将从 HTTP 的本质出发带你拆解一次完整的 HTTP 请求过程分析请求与响应的报文结构深入讲解 Cookie 与 Session 的工作原理同时解答很多开发者容易混淆的长连接与无状态的关系帮你彻底搞懂 HTTP 协议的核心知识点为深入学习 Tomcat、Jetty 等 Web 容器打下坚实基础。一、开篇你真的分得清 HTTP 和 HTML 吗在深入学习 Tomcat、Jetty 这类 Web 容器之前我想先问你一个问题HTTP 和 HTML 有什么区别这其实是一个很好的入门测试能帮你快速检验自己对 HTTP 协议的理解程度。因为 Tomcat 和 Jetty 本质上就是HTTP 服务器 Servlet 容器如果连最基础的 HTTP 协议都没搞懂想要深入理解 Web 容器的工作原理无异于空中楼阁。如果你对这个问题还有些迟疑没关系跟着本文一起我们重新把 HTTP 协议的核心知识点梳理一遍。二、HTTP 的本质浏览器与服务器的 “约定语言”很多人都知道HTTP 是应用层协议基于 TCP/IP 协议来传输数据比如 HTML 文件、图片、接口数据等等。但很多人忽略了一点HTTP 协议本身不关心数据包在网络里是怎么传输的它核心要解决的是客户端和服务器之间的通信格式问题。举个很简单的例子当你在浏览器里输入网址想要从服务器获取一个 HTML 页面的时候浏览器要做两件事和服务器建立 Socket 连接这是网络通信的基础有了这个连接双方才能收发数据。把自己的需求打包成数据通过这个连接发给服务器。第一步很好理解那第二步里浏览器要怎么告诉服务器我想要什么我是要获取数据还是要提交表单我想要的是哪个资源是首页还是用户列表我能接收什么格式的返回数据能不能支持压缩这些信息如果没有一个统一的格式服务器根本没办法解析。比如浏览器发了一堆乱码一样的字符服务器怎么知道你要干嘛三、一次完整的 HTTP 请求到底经历了什么很多开发者天天发 HTTP 请求却从来没认真想过从你点击链接到页面展示出来这中间到底发生了多少步我们来完整走一遍这个流程触发请求用户在浏览器里输入网址回车或者点击了一个超链接浏览器捕获到这个操作知道要发起网络请求了。发起 TCP 连接请求浏览器根据域名解析出服务器的 IP 地址然后向服务器的对应端口默认 80发起 TCP 连接请求。TCP 三次握手建立连接服务器收到连接请求后双方经过 TCP 三次握手确认彼此的收发能力都正常正式建立起 TCP 连接。打包请求数据浏览器把用户的请求按照 HTTP 协议的格式打包成一个 HTTP 请求报文。网络传输请求把这个报文通过之前建立的 TCP 连接发送给服务器经过网络路由最终到达服务器的应用程序。服务器解析请求服务器的 HTTP 服务程序比如 Tomcat拿到这个报文按照 HTTP 协议的格式解包解析出客户端的需求你要访问哪个路径用的什么方法传了什么参数处理业务逻辑服务器根据解析出来的请求处理对应的业务如果是静态资源就直接读文件如果是动态接口就调用对应的后端程序拿到处理结果。打包响应数据服务器把处理结果再按照 HTTP 协议的格式打包成响应报文。网络传输响应把响应报文通过 TCP 连接发回给浏览器经过网络传输到达客户端。浏览器解析响应浏览器拿到响应报文按照 HTTP 协议解包拿到里面的内容如果是 HTML就开始解析渲染页面。页面渲染展示浏览器把解析好的 HTML、CSS、JS 处理完最终把页面展示给用户。而我们常说的 Tomcat、Jetty 这类 Web 容器在这个过程中负责的就是接受连接、解析请求、处理请求、发送响应这几个核心步骤。要注意的是一台服务器可能要同时处理成千上万的浏览器请求如果一个个串行处理效率会极低。所以 Tomcat 这类容器都会用多线程技术把这些步骤并行化来提升并发处理能力这也是我们后续深入学习 Web 容器的核心重点之一。四、拆解 HTTP 报文请求与响应的内部结构说了这么多格式那 HTTP 的报文到底长什么样我们拿一个真实的登录请求来拆解一下你一看就懂了。4.1 HTTP 请求报文这是极客时间登录接口的真实请求我们把它整理成标准格式POST /account/ticket/login HTTP/1.1 Accept: application/json, text/plain, */* Accept-Encoding: gzip, deflate, br Accept-Language: en,de;q0.9,zh-CN;q0.8,zh;q0.7,en-US;q0.6 Connection: keep-alive Content-Length: 115 Content-Type: application/json Host: account.geekbang.org User-Agent: Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.81 Safari/537.36 {country:86,cellphone:139********,password:*******,captcha:,remember:1,platform:web}你可以看到一个完整的 HTTP 请求报文分为三个部分请求行就是第一行包含三个信息请求方法这里是POST表示这是一个提交数据的请求除此之外还有 GET、PUT、DELETE 等常用方法。请求路径/account/ticket/login表示要访问的服务器接口路径。HTTP 版本HTTP/1.1表示用的是 1.1 版本的 HTTP 协议。请求头从第二行开始到空行之前的部分都是请求头这是一系列的键值对用来传递一些附加的信息Accept告诉服务器我客户端能接收什么类型的返回数据。Accept-Encoding我支持的压缩方式这样服务器可以返回压缩后的数据减少传输体积。Content-Type我这次请求的正文是什么格式这里是application/json说明正文是 JSON 格式的数据。Host要访问的主机域名因为一台服务器可能部署了多个网站靠这个来区分。User-Agent客户端的标识告诉服务器我是什么浏览器、什么版本服务器可以根据这个做不同的适配。请求正文空行之后的部分就是真正要传输的业务数据这里就是我们提交的登录账号密码信息。当这个请求到达 Tomcat 之后Tomcat 会把这些字节流解析成一个Request对象把请求行、请求头、正文这些信息都封装进去然后交给 Web 应用去处理。4.2 HTTP 响应报文处理完之后服务器会返回响应同样的响应报文也分为三个部分我们还是看这个登录请求的响应HTTP/1.1 200 OK Connection: keep-alive Content-Type: application/json; charsetUTF-8 Date: Sun, 24 Feb 2019 11:50:20 GMT Set-Cookie: expiresWed, 06-Mar-2019 GMT; Max-Age864000; path/; domain.geekbang.org; HttpOnly {code:0,msg:登录成功,data:{token:xxxxxx}}状态行第一行同样三个信息HTTP 版本HTTP/1.1状态码200表示请求处理成功除此之外还有 404找不到资源、500服务器错误、302重定向等常用状态码。状态描述OK对状态码的文本描述。响应头同样是键值对传递附加信息比如这里的Set-Cookie就是服务器告诉浏览器要把这个 Cookie 存起来下次请求带上。响应正文空行之后的部分就是服务器返回的业务数据这里是登录的结果信息。Web 应用处理完请求之后会生成一个Response对象Tomcat 再把这个对象转换成标准的 HTTP 响应报文发给浏览器一次请求就完成了。五、无状态协议的 “破局者”Cookie 与 SessionHTTP 协议有一个很重要的特点无状态。什么意思就是协议本身请求和请求之间是没有任何关系的服务器不会记得你上一次请求了什么也不会记得你是谁。每一次请求对服务器来说都是一次全新的、独立的请求。这就带来了一个问题比如你登录淘宝把商品加入购物车然后刷新页面服务器如果不记得你是谁那它就会以为你是个新用户提示你未登录购物车也空了这显然是不能接受的。所以为了让服务器能识别用户在无状态的 HTTP 协议之上就诞生了 Cookie 和 Session 这两个技术。5.1 Cookie存在客户端的 “身份小纸条”Cookie 本质上就是服务器让浏览器存在本地的一份小数据。当你第一次登录服务器的时候服务器在响应头里通过Set-Cookie告诉浏览器“你把这个用户标识存起来下次给我发请求的时候记得把这个信息带上”。然后浏览器就会把这个 Cookie 存在本地之后每次给这个服务器发请求的时候都会自动在请求头里带上这个 Cookie。这样服务器拿到 Cookie就能识别出你是谁了。简单来说Cookie 就是一份存在用户本地的 “小纸条”每次请求都带着它让服务器能认出你。5.2 Session存在服务端的 “用户储物柜”但是 Cookie 有个问题它是存在用户本地的而且是明文传输的如果我们把用户的账号、密码这些敏感信息都存在 Cookie 里很容易被窃取有很大的安全隐患。所以 Session 就出现了。Session 可以理解为服务器在自己的内存里给每个用户开辟了一个 “储物柜”用来存用户的状态信息比如用户 ID、登录状态这些。那服务器怎么把请求和这个储物柜对应起来呢很简单服务器给每个储物柜生成一个唯一的编号也就是Session ID然后把这个编号通过 Cookie 发给浏览器让浏览器存起来。之后每次请求浏览器带着这个 Session ID 的 Cookie 过来服务器拿到这个 ID就能找到对应的那个储物柜拿到用户的状态信息了。这样一来敏感的用户信息都存在服务器端传到客户端的只有一个没有意义的 Session ID既安全又减少了网络传输的体积因为不用每次都传一大堆用户信息了。5.3 Session 的创建与管理那 Session 是什么时候创建的呢是在服务器端创建的。以 Java Web 为例当你的 Web 应用调用HttpServletRequest.getSession()方法的时候Tomcat 这类 Web 容器就会检查请求里有没有 Session ID如果没有就会创建一个新的 Session生成唯一的 Session ID然后把这个 ID 通过 Cookie 返回给浏览器。为了保证 Session 的可靠性Tomcat 还提供了多种持久化方案不会把 Session 只存在单机内存里不然服务器重启用户的登录状态就没了。通常会把 Session 存在 Redis 这类高性能的中间件里这样就算服务器集群部署所有节点都能共享 Session不会出现用户在不同节点之间切换就掉线的问题。同时 Session 有过期时间Tomcat 会开一个后台线程定期清理过期的 Session释放服务器的资源。六、解惑长连接和无状态真的矛盾吗很多人学到这里都会有一个疑问HTTP 是无状态的多个请求之间没有关系但是 HTTP/1.1 里又引入了长连接多个请求可以共用同一个 TCP 连接这不是矛盾了吗其实这完全不矛盾因为这两个东西根本就不是一个层面的东西。我们先回顾一下在 HTTP/1.0 的时候每次请求都要新建一个 TCP 连接请求完了就把连接关掉。就好比你每次寄信都要新修一条路寄完就把路拆了下次再寄再修这显然效率很低修路的开销太大了。所以 HTTP/1.1 引入了长连接通过Connection: keep-alive来开启默认就是开启的。意思是这个 TCP 连接一次请求完了不关掉下次你还要给这个服务器发请求就直接用这个已经修好的路不用再重新修路了省下了 TCP 三次握手的开销。但是长连接是 TCP 层面的连接复用而 HTTP 的无状态是应用层的协议特性。也就是说就算多个请求共用同一个 TCP 连接这些请求本身还是独立的每个请求都包含了服务器处理这个请求需要的所有信息服务器不需要记住上一个请求的状态就能处理当前的请求。就好比路是同一条路但是你寄的每一封信都是独立的每封信里都写好了完整的收件人、内容收信人不需要记得你上一封信寄了什么拿到这封信就能处理。这两者完全不冲突长连接只是优化了传输层的效率并没有改变 HTTP 协议本身无状态的特性。而且要注意HTTP/1.1 的长连接虽然解决了连接复用的问题但是带来了一个新的问题队头阻塞。因为同一个连接里的请求是要排队处理的前面的请求没处理完后面的请求就得等着。这个问题直到 HTTP/2.0通过二进制分帧的方式才彻底解决。总结到这里我们把 HTTP 协议的核心知识点都梳理了一遍总结一下HTTP 的本质它是浏览器和服务器之间约定的通信格式HTTP 是 “信封”用来规定怎么传输数据而 HTML、JSON 这些是 “信的内容”是传输的数据本身。请求流程一次完整的 HTTP 请求要经过 TCP 连接建立、请求打包传输、服务器处理、响应打包返回、浏览器渲染这一系列步骤。报文结构HTTP 的请求和响应都分为行、头、正文三个部分通过标准化的格式让双方能够互相解析。状态保持HTTP 本身是无状态的所以我们用 Cookie 来在客户端存标识用 Session 来在服务端存用户状态实现了用户登录这类有状态的功能。长连接与无状态两者并不矛盾长连接是 TCP 层的连接复用用来提升传输效率而无状态是 HTTP 应用层的特性两者互不影响。搞懂了这些你就已经掌握了 HTTP 协议的核心必知必会的知识点这也是你深入学习 Web 容器、后端开发的重要基础。不知道你有没有搞懂这些知识点你在日常开发中有没有遇到过和 HTTP 协议相关的有趣问题欢迎在评论区留言讨论。