HTTP 协议
1. 引入
HTTP 是应用层的重要协议,是基于 TCP 实现的。
java 程序员,适用 java 的主要场景就是 “做网站” ,网站 = 后端(HTTP 服务器 ) + 前端(浏览器),因此,HTTP协议也是很重要的。
HTTP 是 超文本传输协议。文本 => 字符串(能在码表上找到的合法字符);超文本 => 不仅是字符串,还可以是携带 一些图片,特殊格式什么的 (eg:HTML);富文本:eg:word 文档。
HTTP 诞生于 1991 年,最新版本是 HTTP 3.0,当前主要还是使用 HTTP 1.1 和 HTTP 2.0。2.0 和 3.0 引入了很多新的特性 1)提高了传输效率;2)提高了传输安全性; HTTP 3.0 之前,传输层是基于 TCP 的,到了 3.0,则是基于 UDP 的(其基于 UDP 实现了一系列更复杂的机制,可以确保可靠性,也不怕大数据包)。
HTTP 协议最主要的应用场景, 就是网站。浏览器和服务器之间,传输数据。客户端(手机, PC), 和服务器之间的数据传输, 也很可能是 HTTP。
所谓网页,是通过 HTML 来构建的。HTML 也是一个“编程语言”,和 Java, C++ 画风差异是非常大的。C++, Java, 表达的是“逻辑”,做什么,HTML, 则是描述的“内容”,有什么。一个的网页,光有 HTML 是不够的,还需要 CSS 和 JavaScript。
HTTP 协议的交互过程,是典型的 “一问一答”。对于 一般网站开发,基本够用。对于一些特殊情况,如像 QQ,微信那样的窗口交流,只 HTTP 是不够的,像这种。
这种场景,需要服务器主动给浏览器发送消息(消息推送),应用层这里还提供了一个和 HTTP 搭配的协议,websocket (HTTP 的跟班,对 HTTP 的能力进行补充)
2. HTTP 报文格式
要研究报文格式,先要能看到 HTTP 报文。这里我们使用抓包工具,抓包工具本质上是一个“代理程序”,能够获取到网络上传输的数据,并显示出来,从而给程序员提供一些参考。接下来的学习中,实际工作中都会非常常用。
工具一:wireshark 高大全,可以抓各种协议数据包。 TCP、 IP、 UDP、 以太网…(使用起来更复杂一点)
工具二:fiddler 专注于 HTTP 的抓包。
这里使用 Fiddler。下载地址:Fiddler 经典版,这个是免费的。下载过程是一路 next。
下载后,在进行抓包前,要进行简单的设置
除了上述操作之外,另外,还需要关闭电脑上本身的代理程序。
有的进行 fq,电脑上本身就有代理(单独的程序/浏览器插件),fiddler 也是一个代理程序,代理之间可能会冲突,造成 fiddler 无法使用。
代理,是一个程序,而不是设备,它工作在 应用层。客户端和服务器之间的请求和响应,它都会经手,因此就能显示出请求和响应的具体细节。代理分成两种:正向代理(客户端的代言人);反向代理(服务器的代言人)。
系统上有任何一个程序(不一定是浏览器),使用了 HTTP/HTTPS 此时,就都能被 fiddler 给获取到。电脑上的很多程序会在你感知不到的情况下在后台偷偷的做很多事情,和服务器进行交互。
打开一个网站,其实浏览器和服务器之间进行的 HTTP 交互不是只有一次,通常有很多次。第一次交互是拿到这个页面的 html。 html 还会依赖其他的 css 和 js, 图片等。 html 被浏览器加载之后, 又会触发一些其他的 http 请求, 获取到 CSS, js 等。当执行 js 的时候, js 代码里可能又要触发很多的 http 请求, 获取到一些数据…,经过多次的反复拉扯。
蓝色的表示返回的是一个 html,往往是访问一个网站的入口请求。双击他,就能看到明细。
上半部分是请求明细,下半部分是响应明细。当前响应是被压缩的,因为网络传输中,带宽是较贵的硬件资源,为了省带宽,就把响应进行压缩(一般是压缩响应,请求不太需要),压缩和解压缩的过程,是需要消耗 时间 和 cpu 的。
点击请求和响应中的 RAW 就能看到原始数据。
点击 View in Notepad 就可以在记事本中看。
3. HTTP请求
包含四个部分
- 首行
GET https://www.sogou.com/ HTTP/1.1
三个部分, GET 部分:方法 ,HTTP/1.1 部分:HTTP的版本号,和夹在二者之间的 URL 。这三个部分使用空格进行分割。 - 请求头
从第二行,一直到后面都是请求头。类似于 TCP 报头/ IP 报头,重要的属性信息。这里是文本的方式组织的。
报头中包含了很多的键值对:每个键值对占一行。键和值之间使用**” :“**、 空格 来分割。此处的键值对都有哪些,都是什么含义?都是 HTTP 协议规定的。
-
空行 请求头最下面会有一个空行。这个空行就可以表示结束标记。
-
正文 http 的载荷部分。有的 http 请求有 正文,有的就没有。
4. HTTP响应
基本格式也是四个部分。
-
首行
HTTP/1.1 200 OK
版本号: HTTP/1.1; 状态码:200;状态码描述:OK。这里也是含有 3 个部分。 -
响应头
同样是键值对
-
空行
-
响应正文(body)
载荷部分是 html
这里只截图了一部分
5. 认识 URL
非常重要的概念,并不仅仅是在 HTTP 中使用。
JDBC的时候,也是用过 url ,
jdbc:mysql://127.0.0.1:3306/java110?characterEncoding=utf8&useSSL=false
URL:描述一个网络上的资源位置,唯一资源定位符。
URI:唯一资源标识符 严格来说,uri 的范围比 url 更大。
下面是一个完整结构:
查询字符串,也是键值对格式,使用 ” & “ 分割键值对;使用 ” = “ 分割键和值。查询字符串具体什么意思,外人无从得知,写这个代码的程序员才知道。
6. URL encode
query string 里是自定义的键值对。在 URL 中,本身有些特殊符号具有特定的含义。eg:
/ : ? @ …
如果 url 的 query string 中也包含同样的符号,咋办?如果直接写进去,可能就会使服务器/浏览器解析失败。靠谱的方法就是对上述符号进行"转义"。例如:” + “ 转义,+ => ascii 拿出来, 使用 16 进制表示, 并且加上 %。
对于汉字,也是要进行转义的,汉字的 utf8 / gbk 等编码值其中可能某个字节就恰好和某个符号的 ascii 一致。网络上有现成的转换工具,搜索一下就能用。
url 中的 query string 中包含中文时,必须进行编码
7. 报文字段
7.1 方法
首行:方法 URL 版本号
方法描述了本次请求,干什么。
虽然有语义,但事实上,这些方法的语义,都是"标准文档作者"的一厢情愿,开发者在实际开发程序的时候,才不管这一套,完全可以用 POST 来从服务器拿数据,也完全可以用 GET 往服务器放数据。甚至有的甚至,全用 POST。
两个典型的使用 POST 的场景:1. 登录;2.上传。以登录为例,一些网站时的密码是明文传输的(在 body 中),一抓包就知道了。
从使用习惯上说,GET 通常没有 body,POST 通常有 body。GET 会把需要给服务器的补充信息放到 query string 中(url中),POST 会把这些信息放到 body 中。
经典面试题: GET 和 POST 有啥区别?——GET 和 POST 本质上没有区别。
使用 GET 的场景,也可以替换成 POST,使用 POST 的场景,也能替换成 GET。
取决于代码,尤其是服务器和客户端都是自己实现的情况下。(部分服务器/部分浏览器
某些情况下 GET 和 POST 不能完美替换,但是 大部分情况 相互替换问题一般不大。)
但是 GET 和 POST 在使用习惯上还是有区别的。
-
GET 习惯于把数据放到 url 的 query string 中。POST 习惯于把数据放到 body 中。
GET 也可以把数据放到 body(有的服务器/浏览器
可能不支持), POST 也可以把数据放到 query string (对于绝大部分的服务
器和浏览器都适用) -
语义上的区别。标准文档中,GET 的语义是用来获取数据。POST 的语义是给服务器传输数据。实际使用并不一定非得拘泥于上述要求。
-
幂等性。标准文档中,建议 GET 请求实现成幂等的。POST 则无要求。GET 在实际开发中也不一定非得实现成幂等。标准是这么建议的,不一定采纳。确实很多时候是按照幂等的方式走的。计算机幂等,也是挺关键的一件事情,如果某个操作本身是幂等的,此时就可以进行缓存了。
每次输入的内容一定,输出的结果也一定。称为幂等
每次输入的内容一定,输出的结果不一定。不是幂等。
- GET 请求是可以被浏览器收藏夹收藏的。
POST 请求不可以。
网上部分错误说法
-
POST 比 GET 更安全
论据:登录的时候,如果使用 GET,用户名密码就会显示在 url 上,此时就会被别人直接看到。所以就不安全。
即使是 POST,数据没有显示在 URL,也是可以被黑客通过抓包获取到的。真正保证安全性关键在于加密。如果你数据加密了,就算放到 url 显示又有什么问题? -
GET 传输的数据量小(存在上限),POST 传输的数据量更大。
描述的是以前。实际上 HTTP 标准文档上明确说了,对于 GET 的 URL 的长度不做限制。之前老版本的 IE 浏览器在实现的时候,URL 的长度有限制。目前比较长的 URL 也是很常见。 -
GET 只能携带文本数据,POST 则可以携带二进制数据。
这个说法不能说是完全错误,确实有一定的局限性的。
URL 通过 query string 来携带数据。query string 是只能包含文本的。但是可以对二进制数据进行 urlencode,自然就成了文本了。到了服务器自然进行 urldecode,就能把数据还原成二进制。
POST 请求 body 中也经常不是直接携带二进制 (可以),也有很多时候是对二进制数据进行 urlencode / base64 等方式进行转码。
7.2 Host
Host:表示服务器主机的地址和端口。Host: www.sogou.com
URL 已经有 Host 了,这里的 Host 和 URL 中的 ip 地址 端口 之类的,绝大部分情况下都是一样的,少数情况可能不同。
7.3 Content-Length
表示 body 中的数据长度。
通过这个长度来处理粘包问题。HTTP 底层也是基于 TCP。连续传输多个 HTTP 数据,此时接收方这边的接收缓冲区里就会积累多个包的数据,应用程序在读取这些数据的时侯就需要明确
包之间的边界。
如果是没有 body 的请求/响应,直接使用空行作为分隔符了。
如果有 body,空行就不是结束标记了——从空行开始来读取 body,body 要读多长就取决于 Content-Length。读完之后,这个包就结束了。
7.4 Content-Type
表示请求的 body 中的数据格式。
body 可以传输很多种格式的。包括程序员也可以自己约定任意的格式。
但是有些格式是非常常见的:
请求中:
- application/json body 就是 json
- application/x-www-form-urlencoded 称为 form 菜单,通过 HTML 中的 form 标签构造出来的一种格式。这个格式的特点,认为是把 query string 放到 body 里了。(也可以上传文件)
- multipart/form-data 上传文件时使用。
响应中:
- text/plain 纯文本
- text/html html
- text/css css
- application/javascript js
- application/json
- image/png
- image/jpg
…
这两种字段和 body 密切相关。如果你这个数据包,没有 body,也就不会有这两个字段。
由于浏览器和服务器之间要进行多次网络交互,整体的过程是比较低效的。为了提升效率,就会把一些固定不变的内容在浏览器本地的机器硬盘上进行缓存,如:css, 图片, js, 很少发生改变……,保存到硬盘上之后,后续再请求,就可以直接从硬盘上读取数据,减少了网络交互的开销。
使用 ctrl + F5 强制刷新。就可以不读取缓存,直接读取服务器数据。
正常来说,响应只要有body,都得有 Content-Type。实际上如果响应确实没有 Content-Type,也没有 body。这个时候,有的浏览器(像 chrome 这种)也能自己猜一猜。浏览器容错能力很强的程序,即使你返回的数据有问题,也能尽可能的帮你正确显示出来(鲁棒性,你对他越粗鲁,他表现的越棒)
Content-Type: text/html; charset=UTF-8,后面自己写服务器程序返回网页,如果发现网页乱码,就可以检查一下是否是这里的,编码方式没有设置或者是设置的不对。
7.5 User-Agent(简称 UA)
主要包含 操作系统的信息,浏览器的信息,描述了用户使用什么样的设备上网。
上古时期,UA 非常关键的部分。不同用户使用的上网的设备,差异很大。(计算机发展速度日新月异),同一个时间段内,新的和旧的会同时存在。以浏览器为例:同一时刻这些浏览器
都会存在于市场上:1) 最老的浏览器, 只能显示文 2) 之后能显示图片 3) 能支持 js 4) 支持 flash 等动画效果 5) ……
如果你要写一个网站,你写的网站是否要使用新的特性?使用新特性,老的设备就无法正常打开。不使用新特性,你这个网站就打不过竞争对手。借助 UA 就能解决上述问题。
服务器就可以针对此时的 UA 的信息进行判定。如果用户用的是很老的设备,返回的页面就不包含新特性,确保这个页面能够正确访问出来;如果用户用的是新的设备,返回的页面就包含新特性,确保这个页面体验足够好。
随着时间的推移,浏览器现在都差不多了,看似 UA 没了用武之地,但仍然有用。 PC 端 屏幕大,更宽。手机 屏幕小,更窄。 对应的返回页面布局就应该有差异。
UA 里包含了系统信息, 可以判定系统是 PC端系统 还是 移动端的系统,此时就可以根据这个信息来返回不同的页面。
使用于机浏览器的时候,一些手机浏览器都有一个功能——手动修改 UA,手动把 UA 改成 PC 的 UA, 就能访问 电脑版 的网页了。
但是给 PC 维护一套代码,给移动端维护一套代码,搞两套代码,也有点麻烦。于是有了响应式布局(前端后未提出的解决上述问题的技术方案),通过一套代码,适应不同尺寸的显示器。
CSS3 提供了一个特性:“媒体查询”,可以感知到当前屏幕的尺寸,根据不同的尺寸,应用不同的样式。
现在也有越来越多的网站不再依赖 UA 来进行区分了。
7.6. Referer
referer 描述了当前页面从哪里来。
浏览器中,直接输入 url / 或点击收藏夹 打卡的网页,此时是没有 referer的。如果跳转了,就有了。如图:
搜索广告中,都是按照点击计费。以搜狗为例,用户每次点击广告,搜狗都能赚钱。
因此,需要记录某个广告在某个时间段内被点击多少次,及是否是从 搜狗搜索 跳转过去的。
搜狗统计,广告主统计,双方都要统计。对上了就直接结算,没对上就需要程序员排查一下哪边的统计出问题了(这类问题之前经常要处理)
referer 是否会被篡改呢?答案是会的。这种情况在10 年左右时非常常见,通过篡改referer,就可以将从其他地方跳转到广告的,该从己方跳转,从而赚黑心钱。
而 HTTPS 就能够很好的解决上述问题,(S 指 SSL (网络中用于加密的协议) )加密就能把 header 和 body 都进行加密。网络上传输的就是密文了。要想修改,就得先破解。就算能解密,也篡改不了(一旦修改就能被用户的浏览器感知到)。
所有广告平台也就纷纷升级成了 https = > 后续越来越多的网站都引入了 https。直到今天,现在网络上 https 的网站是绝对的主流了,纯 http 的网站很少见了。
7.7 Cookie
非常重要的报头中的属性。Cookie 本质上是一个,浏览器这边本地持久化存储数据的机制。
浏览器作为电脑上的一个程序,能否直接读写本地磁盘文件呢?
答:当然可以,系统提供了 API 操作文件。作为一个程序当然可以调用这些 api 进行操作
浏览器上运行的网页,能否通过浏览器提供的 api 来读写本地磁盘文件呢?
答:理论上完全可行,但是浏览器禁止了这种做法。(浏览器并没有给网页提供这样的 api)。一个网页不能直接的读写你的硬盘文件。这样是为了安全性,网站一点就打开了,万一打开的是一个黑客搞的恶意网站,此时,他通过网页直接操作你电脑上所有的文件,很危险。
但是有些网站,是需要把一些信息保存到浏览器这边的。比如当前登录的用户的身份信息。所以,浏览器退而求其次,给网页提供了这样的 API,能够有限度的存储数据(按照键值对的格式)。而不能随意的访问文件系统。
这三个都是浏览器提供的网页可以存储数据的机制:
1)Cookie
2)LocalStorage
3)IndexDB
1)Cookie是 比较经典,最广泛使用的机制;2)、3)更新一些,使用逐渐增加,目前 Cookie 仍然是主流。
HTTP 请求中的 Cookie 字段,就是把本地存储的 Cookie 信息发送到服务器这边,HTTP 响应中会有一个 Set-Cookie 字段,就是服务器告诉浏览器你要在本地保存哪些信息,都是键值对(程序员自定义的)。
关于 Cookie 几个重要结论:
-
Cookie 从哪里来?
答:服务器返回给浏览器的。通常都是首次访问/登录成功之后。 -
Cookie 到哪里去?
答:Cookie 会存储在浏览器本地主机的硬盘上,后续每次访问服务器都会带上 Cookie 不同的客户端。保存的 Cookie 是不同的。即使是同一个主机,使用不同浏览器,Cookie 大概率也不同。 -
Cookie 中存什么?
答:键值对格式的数据。这里的内容都是程序员自定义的。和 query string 一样外人无从理解。
eg:Cookie: lid=%E7%86%A8%E6%88%98%E5%A3%A8%E7%9A%A8%E8%B7%B3%E5%88%80; cna=myzCHTqTOwMCAXul.pWbmf4Yt; isg=BDg4XSZogaAhKcU
且不同网站的 Cookie 不一样。
旧版本的 chrome 可以看到每个键值对, edge 浏览器还是能看到的。 -
Cookie 在浏览器这边如何组织?
答:在硬盘本地保存,是按照不同的域名为维度分别存储。你的浏览器访问百度,有一组 cookie; 访问搜狗,也有一组 cookie。 -
Cookie 的用途是什么?
答: 用来在客户端保存数据。其中最主要的是保存用户的身份标识,服务器就可以通过标识来区分用户了。一些其他的业务数据一般不会存到 cookie 中,cookie 随时可以删除掉,把业务数据存储在服务器, 通过 cookie 中的身份标识找到对应的数据。
一般账号密码不会在 cookie 中保存,cookie 是要传输给服务器的,一般浏览器保存的密码都是明文密码。明文密码放到 cookie 当然不合适。 虽然有 https 能加密。https 侧重于是"不能被篡改", 而不是"不能被解密"。
7.8 状态码
状态码 用于响应中的。表示响应的结果如何(是正确还是错误,如果是错误,是什么原因)
就像C 语言中的 strerror 把错误码翻译成一个错误原因的字符串。当我们出现问题的时候,原因有很多种,就可以使用不同的数字来区分原因,就可以对症下药了。
这里讲几个常见的:
- 200
成功了,一切顺利 - 404
访问的资源(URL 中的路径)没有找到。 - 403
请求的资源没有访问权限。 - 405
你的服务器只支持 GET 请求,但你发了一个 POST - 500
服务器内部错误(服务器挂了),现实中较难遇到,但自己写的就容易出现了。 - 504
访问服务器超时了,可能是服务器挂了,也可能是网挂了。 - 302
重定向(临时重定向),明明访问的是 网站 A,但 A 让你去访问 B,浏览器自动访问 B。
例如:一个网站的域名改了,但是人们不知道,就会按原域名访问。为了让这些人访问不受影响,就要将访问老域名的请求重定向到新域名上。 - 301
重定向(永久重定向)。这就影响到浏览器的缓存。如果是永久重定向,浏览器就会把重定向结果记录下来,后续再次访问,就直接访问重定向的目标地址即可,不必多一次跳转,如果是临时重定向,就不方便缓存。
在这种重定向报文的响应中,会有一个特殊的 header 叫做 Location ,描述了要重定向到的目标地址,重定向是要跳转到哪里
HTTP/1.1 302 Found
Location: https://gitee.com/HGt22222/blog_python
浏览器收到重定向响应之后,就会自动发起新的请求。
状态码中, 还有一个特殊的, 418 。 418 在 HTTP 协议的标准文档上是存在的 (虽然没有啥实际的含义) “I am a teapot” 我是个茶壶 (我是个杯具) 设计 HTTP 协议的大佬, 弄的 “彩蛋”
8. 构造 HTTP 请求
两种方法:1)通过代码构造,有点难度。
通过代码来构造 HTTP 请求。
任何一种编程语言,只要能够操作网络,都可以构造 HTTP 请求。
对于 Java 来说,需要使用 ServerSocket(Socket)(Tcp 的 socket api 来编程)
本质上就是基于 Socket 写一个 TCP 的客户端,然后在 socket 中按照 HTTP 协议的格式写入字符串即可。
Java - OkHttp OkHttpClient (比较知名的 Java 的 HTTP 客户端库)
1 OkHttpClient client = new OkHttpClient().newBuilder()
2 .build();
3 MediaType mediaType = MediaType.parse("text/plain");
4 RequestBody body = RequestBody.create(mediaType, "");
5 Request request = new Request.Builder().
6 .url("https://www.sogou.com?a=11&b=22").
7 .method("GET", body)
8 .addHeader("Cookie", "IFLOC=CINGID:SUID=64A1B9308530A40A00000006370954A;cuid=AAFLTF10wAAAAdH5J17FgTAEAW=ABTEST=5117984318021v17")
9 .build();
10 Response response = client.newcall(request).execute();
上述列出的代码只是给大家演示了,HTTP 客户端程序基本的写法,实际开发中,都是有一些第三方库,封装好了上述操作,不需要手动写 socket。
后续如果确实需要用到 OkHttpClient,代码不知道该咋写。在 postman 中构造好,其能自动生成对应代码。
2)通过第三方工具。
这里使用第三方工具构造。Postman
。
点击加号,新开一页。
接着,对请求进行构造。
使用这个生成构造出的对应代码。
日常开发中,还会涉及到,网页中构造 HTTP 请求。(特别常见的情况)需要通过 HTML / JS 来构造出 HTTP 请求了,比较经典的方式:1)form 表单;2)ajax