您当前的位置:首页 > 计算机 > 网络通信

一文带你深入了解HTTP

时间:11-24来源:作者:点击数:
图片

http的发展史

在学习网络之前,了解它的历史能够帮助我明白为何它会发展为如今这个样子,能让我有探究它的兴趣。下面的这张图片就展示了“互联网”诞生至今的发展历程

图片
图片

http是什么?

HyperTextTransferProtocol 直译为“超文本传输协议”。

1.超文本:指文字、图片、视频、音频等的混合体,比如最熟悉的html。

2.传输:http是一个“双向协议”,传输的是请求方和响应方之间的数据,不限制请求方和响应方之间的角色,传递的过程中可以存在任意“中间人”。

3.协议:协是两个或多个参与者之间的交流,议是指对参与者之间的约定和规范。所以,http协议可以理解为作用在计算机之间,使用计算机能够理解的语言确立计算机之间交流通信的规范,以及相关的各种控制和错误处理方式。

所以对于以上的问题可以有这样的总结:http是一个在计算机世界里专门在两点之间传递文字、图片、音频、视频等超文本数据的约定和规范。

图片

与http相关的一些概念

浏览器(web Browser):浏览器的本质是http中的请求方,使用http协议获得网络上的各种资源。在HTTP协议里,浏览器的角色被称为"User Agent"即用户代理,意思是作为访问者的”代理来发起HTTP请求。下图是一些主流浏览器及其内核。

图片

服务器(web Server):硬件含义就是物理形式或“云”形式的机器。软件含义的 Web 服务器就是提供 Web 服务的应用程序,通常会运行在硬件含义的服务器上。它利用强大的硬件能力响应海量的客户端 HTTP 请求,返回动态的信息。常见的web服务器有Apache、Nginx。

CDN(Content Delivery Network):CDN是为了解决长距离网络访问速度慢的问题而诞生的一种网络应用服务,全称为“内容分发网络”。CDN最核心的原则是“就近访问”,使用HTTP协议里的代理和缓存技术,用户在上网的时候不直接访问原网站,而是访问离他最近的一个CDN节点,节省了访问过程中的时间成本。(负载均衡,安全防护,边缘计算)。

爬虫(Crawler):“机器人”形式的用户代理,是一种可以自动访问Web资源的应用程序。

HTML(Hyper Text Markup Language):超文本标记语言,用于描述超文本页面,用标签定义图片、文字、排版布局,最终由浏览器渲染。

web Service:由W3C定义的应用服务开发规范,使用client-server主从架构。是一个基于Web(HTTP)的服务架构技术。

WAF:网络应用防火墙,位于Web服务器之前,专门检测http流量,是防护web应用安全的技术。可以阻止SQL注入,跨站脚本攻击,可以完全集成进Apache或Nginx。

TCP/IP:一系列网络通信协议的统称,其中最核心的是TCP和IP协议。其他的还有UDP,ICMP,ARP等,共同构成一个复杂但有层次的协议栈。IP(Internet Protocol)协议主要解决寻址和路由问题,以及如何在两点之间传输数据包。TCP(Transmission Control Protoco)协议位于IP协议之上,意思是“传输控制协议”,基于IP协议提供可靠的、字节流形式的通信,是HTTP协议实现的基础。互联网上的 HTTP 协议运行在 TCP/IP 上,HTTP 也就可以更准确地称为“HTTP over TCP/IP”。

DNS(Domain Name System):域名系统,用有意义的名字来作为 IP 地址的等价替代。在 DNS 中,“域名”(Domain Name)又称为“主机名”(Host)。域名用“.”分隔成多个单词,级别从左到右逐级升高,最右边的被称为“顶级域名”。但想要使用 TCP/IP 协议来通信仍然要使用 IP 地址,所以需要把域名做一个转换,“映射”到它的真实 IP,这就是所谓的“域名解析”。

URI/URL:URI(Uniform Resource Identifier)中文名称是统一资源标识符。DNS 和 IP 地址只是标记了互联网上的主机,URI能够唯一地标记互联网上资源。URI 另一个更常用的表现形式是 URL(Uniform Resource Locator), 统一资源定位符,也就是我们俗称的“网址”,它实际上是 URI 的一个子集,通常不会做严格的区分。

URI 主要有三个基本的部分构成:

1.协议名:即访问该资源应当使用的协议

2.主机名:即互联网上主机的标记,可以是域名或 IP 地址

3.路径:即资源在主机上的位置,使用“/”分隔多级目录

HTTPS:全称是“HTTP over SSL/TLS”,也就是运行在 SSL/TLS 协议上的 HTTP。它是一个负责加密通信的安全协议,建立在 TCP/IP 之上,所以也是个可靠的传输协议,可以被用作 HTTP 的下层,相当于“HTTP+SSL/TLS+TCP/IP”。

代理(Proxy): 是 HTTP 协议中请求方和应答方中间的一个环节,作为“中转站”,既可以转发客户端的请求,也可以转发服务器的应答。

代理有很多的种类,常见的有:

1.匿名代理:完全“隐匿”了被代理的机器,外界看到的只是代理服务器;

2.透明代理:顾名思义,它在传输过程中是“透明开放”的,外界既知道代理,也知道客户端;

3.正向代理:靠近客户端,代表客户端向服务器发送请求;

4.反向代理:靠近服务器端,代表服务器响应客户端的请求;

图片

网络的分层模型

图片

网络分层模型层级是从下往上数的,一般我们比较常接触到的是TCP/IP四层模型,也是比较早出现的分层模型。

第一层是链路层(link layer),负责在底层网络上发送原始数据包,工作在网卡这个层次,使用 MAC 地址来标记网络上的设备,所以有时候也叫 MAC 层。对应的是ISO模型的"数据链路层"。

第二层叫网络层(internet layer),IP协议就处在这一层。因为IP协议定义了"IP 地址"的概念,所以就可以在"链路层"的基础上,用IP地址取代MAC地址,在这个网络里找设备时只要把IP 地址再翻译成MAC地址就可以了。对应的是ISO模型的"网络层"。

第三层叫"传输层"(transport layer),这个层次协议的职责是保证数据在IP地址标记的两点之间可靠地传输,是TCP协议和UDP协议工作的层次。对应的是ISO模型的"传输层"。

第四层叫"应用层"(application layer),由于下面的三层把基础打得非常好,所以在这一层就"百花齐放"了,有各种面向具体应用的协议。例如 Telnet、SSH、FTP、SMTP 等等,当然还有我们的 HTTP。 对应的是ISO模型的"会话层","表示层","应用层"。

利用TCP/IP协议族进行网络通信时,会通过分层顺序与对方进行通信(发送端从应用层往下走,接收端从应用层往上走)。

图片

域名

域名是一个有层次的结构,是一串用“.”分隔的多个单词,最右边的被称为“顶级域名”,然后是“二级域名”,层级关系向左依次降低。最左边的是主机名,通常用来表明主机的用途,比如“www”表示提供万维网服务、“mail”表示提供邮件服务,不过这也不是绝对的。

可以通过下面的例子了解一下协议 主机 域名之间的层次关系。域名就像人的名字一样,名字的关键是要让我们容易记忆。除了标识身份之外,域名还可以代替ip地址。

图片
图片

DNS

我们经常会使用域名访问网站,但其实在网络查找的工程当中是使用ip定位资源的,域名必须解析为ip地址才可以正确的拿到资源。DNS就是用来将域名变为ip的协议。

DNS 的核心系统是一个三层的树状、分布式服务,基本对应域名的结构:

1.根域名服务器(Root DNS Server):管理顶级域名服务器,返回"com","net","cn"等顶级域名服务器的 IP 地址。

2.顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如 cn 顶级域名服务器可以返回 123.cn域名服务器的 IP 地址。

3.权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如 123.cn 权威域名服务器可以返回 www.123.cn 的 IP 地址。

图片

虽然DNS的服务,遍布全球,服务能力也很厉害,但是全世界的网民都在使用这个服务,也会对服务器造成很大的压力。在核心 DNS 系统之外,还有两种手段用来减轻域名解析的压力,并且能够更快地获取结果,基本思路就是“缓存”。

DNS的解析结果可以保存在大公司自己的DNS服务器里,或者操作系统缓存、hosts 文件当中,很多域名解析的工作就都不用请求根DNS服务器了,直接在本地或本机就能解决,不仅方便了用户,也减轻了各级 DNS 服务器的压力,效率就大大提升了。

基于域名和DNS服务器,我们可以实现重定向。因为域名代替了ip地址,所以可以对外域名不变,而主机IP可以任意变动。当主机有情况需要下线、迁移时,可以更改 DNS 记录,让域名指向其他的机器。

我们应该都听说过负载均衡吧,DNS在域名解析阶段就可以进行负载均衡的操作。

第一种方式,因为域名解析可以返回多个 IP 地址,所以一个域名可以对应多台主机,客户端收到多个 IP 地址后,就可以自己使用轮询算法依次向服务器发起请求,实现负载均衡。

第二种方式,域名解析可以配置内部的策略,返回离客户端最近的主机,或者返回当前服务质量最好的主机,这样在 DNS 端把请求分发到不同的服务器,实现负载均衡。

图片

HTTP/1.X

前面我们说了HTTP就是“超文本传输协议”,是一个在计算机世界里专门在两点之间传递文字、图片、音频、视频等超文本数据的约定和规范。在学习过网络的层次模型之后我们又了解了HTTP是一个应用层的协议。在这个环节我们开始正式深入HTTP的世界(基于http/1.1)。

(一)HTTP报文

HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成:

1.起始行(start line):描述请求或响应的基本信息;

2.头部字段集合(header):使用 key-value 形式更详细地说明报文;

3.消息正文(entity):实际传输的数据,它不一定是纯文本,可以是图片、视频等二进制数据。

请求行

请求行一般用来描述客户端要怎样操作服务端的资源,一般由三个部分组成。通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束。

图片
状态行

状态行一般用来描述服务端对于客户端的请求回复的状态,一般也是由三个部分组成。

图片

头部字段

请求行或状态行再加上头部字段集合就构成了 HTTP 报文里完整的请求头或响应头。除了起始行以外,请求头和响应头的结构基本相同。HTTP 头字段非常灵活,不仅可以使用标准里的 Host、Connection 等已有头,也可以任意添加自定义头。不过使用头字段需要注意下面几点:

1.字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好。

2.字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线“_”。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名。

3.字段名后面必须紧接着“:”,不能有空格,而“:”后的字段值前可以有多个空格。

4.字段的顺序是没有意义的,可以任意排列不影响语义。

5.字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。

图片

(二)HTTP请求方法

目前 HTTP/1.1 规定了八种方法,单词都必须是大写的形式,下面就来看看这些方法:

1.GET:获取资源,可以理解为读取或者下载数据。

2.HEAD:获取资源的元信息。

3.POST:向资源提交数据,相当于写入或上传数据。

4.PUT:类似 POST。

5.DELETE:删除资源。

6.CONNECT:建立特殊的连接隧道。

7.OPTIONS:列出可对资源实行的方法。

8.TRACE:追踪请求 - 响应的传输路径。

这几个是我们比较常用的方法,有必要好好学习一下。

GET和HEAD

  1. GET适用于向服务器请求资源,一般将数据携带于url上。
  2. HEAD类似于简化版的GET请求,服务端收到HEAD请求时只返回响应头并且响应头与GET完全一致。

POST和PUT

  1. POST 适用于向服务端发送数据,将数据携带在body当中,通常表示的是“create”的含义。
  2. PUT 类似于POST方法,也可以向服务器提交数据,是“update”的含义。

GET和POST的区别

在这里特别容易被问到的问题是GET和POST的区别,我也想在这块详细的写一下。以下是基于我个人的理解

1. 大小:GET通常将数据带在URL当中而POST将数据放在body里(是RFC在语义上的要求,语法上GET也可以使用body传输数据而POST同样可以把参数放在URL里),因此由于浏览器对于URL长度的限制,GET请求能携带的数据大小一般不超过2KB。值得一提Chrome浏览器对URL的长度限制已经增加到2MB,但是我们考虑到兼容性,URL的长度应该以最大限制的最小标准为主(IE浏览器限制为2KB),除了浏览器的限制,还应该考虑到服务端的限制。

2. 安全:安全是指请求的方法是否会对服务器当中的资源造成影响,因为GET方法是只读的,只要服务器没有“曲解”客户端的请求,服务端上的数据就是安全的。而POST会对服务端的数据进行“增删改”的操作,因此是不安全的。

3. 幂等:幂等的意思是说多次重复执行操作,产生的效果是否相同。显然因为GET方法只对服务器上的资源做只读操作,因此是幂等的。POST在RFC中的定义是“新增或提交数据”,多次提交数据会创建多个资源,所以不是幂等的(而 PUT 是“替换或更新数据”,多次更新一个资源,所以是幂等的)。

4. 缓存:就是说这个方法的可缓存性,绝大多数的浏览器的实现里仅仅支持GET缓存。因为GET因为是读取,就可以对GET请求的数据做缓存。而POST不幂等也就意味着不能随意多次执行。因此也就不能缓存。

(三)URI是什么

URI,也就是统一资源标识符(Uniform Resource Identifier)。因为它经常出现在浏览器的地址栏里,所以俗称为“网络地址”,简称“网址”。URI 不完全等同于网址,它包含有 URL 和 URN 两个部分,在 HTTP 世界里用的网址实际上是 URL——统一资源定位符(Uniform Resource Locator)。但因为 URL 实在是太普及了,所以常常把这两者简单地视为相等。

URI 本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字。

图片

上面这个图片就是一个完整的URI,下面详细拆解一下它的结构。

scheme协议名,表示资源应该使用哪种协议来访问。最常见的当然就是“http”了,表示使用 HTTP 协议。另外还有“https”,表示使用经过加密、安全的 HTTPS 协议。此外还有其他不是很常见的 scheme,例如 ftp、ldap、file、news 等。

:// 分隔符,在 scheme 之后,必须是三个特定的字符“://”,它把 scheme 和后面的部分分离开。没有特定的意义。

user:passwd@ 身份信息,表示登录主机时的用户名和密码,但现在已经不推荐使用这种形式了,因为它把敏感信息以明文形式暴露出来,存在严重的安全隐患。

host:port主机名,表示资源所在的主机名,通常的形式是“host:port”,即主机名加端口号。

path路径,表示资源所在位置,采用了类似文件系统“目录”的表示方式,通常以‘/’开始

query查询参数,用一个“?”开始,但不包含“?”,表示对资源附加的额外要求。path是多个“key=value”的字符串,这些字符串用字符“&”连接,浏览器和服务器都可以按照这个格式把长串的查询参数解析成可理解的字典或关联数组形式。

#fragment片段标识符,它是 URI 所定位的资源内部的一个“锚点”,浏览器可以在获取资源后直接跳转到它指示的位置。但片段标识符仅能由浏览器这样的客户端使用,服务器是看不到的。

在 URI 里只能使用 ASCII 码,对于 ASCII 码以外的字符集和特殊字符做一个特殊的操作,把它们转换成与 URI 语义不冲突的形式。这在 RFC 规范里称为“escape”和“unescape”,俗称“转义”。URI 转义的规则有点“简单粗暴”,直接把非 ASCII 码或特殊字符转换成十六进制字节值,然后前面再加上一个“%”。

(四)状态码

在HTTP报文部分我们说了HTTP的状态行,我们在这个部分就来看看状态行中的状态码。

状态码是一个十进制的数字,RFC 标准把状态码分成了五类,用数字的第一位表示分类,而 0 ~ 99 不用,这样状态码的实际可用范围就变成了 100~599。这五类的具体含义是:

  • 1××:提示信息,表示目前是协议处理的中间状态,还需要后续的操作。
  • 2××:成功,报文已经收到并被正确处理。
  • 3××:重定向,资源位置发生变动,需要客户端重新发送请求。
  • 4××:客户端错误,请求报文有误,服务器无法处理。
  • 5××:服务器错误,服务器在处理请求时内部发生了错误。

1××

1×× 类状态码属于提示信息,是协议处理的中间状态,实际能够用到的时候很少。

"100 Continue"应该是比较常接触到的,会在POST请求发送大文件给服务器时询问服务器是否能够接受时使用,需要带上请求头Expect: 100-continue。这个过程也就是我们常说的POST发送两个TCP包给服务器的说法的来源,不过客户端不需要一直等待服务端的回应,在一定时间内没有收到否定的回答还是会将数据主体发送给服务器。

2××

2×× 类状态码表示服务器收到并成功处理了客户端的请求,这也是客户端最愿意看到的状态码。

“200 OK”是最常见的成功状态码,表示一切正常,服务器如客户端所期望的那样返回了处理结果,如果是非 HEAD 请求,通常在响应头后都会有 body 数据。

“204 No Content”是另一个很常见的成功状态码,它的含义与“200 OK”基本相同,但响应头后没有 body 数据。所以对于 Web 服务器来说,正确地区分 200 和 204 是很必要的。

“206 Partial Content”是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段Content-Range,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/2000”,意思是此次获取的是总计 2000 个字节的前 100 个字节。

3××

3××类状态码表示客户端请求的资源发生了变动,客户端必须用新的 URI 重新发送请求获取资源,也就是通常所说的“重定向”,包括著名的 301、302 跳转。

“301 Moved Permanently” 俗称“永久重定向”,含义是此次请求的资源已经不存在了,需要改用改用新的 URI 再次访问。

“302 Found”,曾经的描述短语是“Moved Temporarily”,俗称“临时重定向”,意思是请求的资源还在,但需要暂时用另一个 URI 来访问。

“304 Not Modified” 是一个比较有意思的状态码,它用于 If-Modified-Since 等条件请求,表示资源未修改,用于缓存控制。它不具有通常的跳转含义,但可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。

4××

4××类状态码表示客户端发送的请求报文有误,服务器无法处理,它就是真正的“错误码”含义了。

“400 Bad Request” 是一个通用的错误码,表示请求报文有错误,只是一个笼统的错误,没有明确含义的状态码。

“403 Forbidden” 实际上不是客户端的请求出错,而是表示服务器禁止访问资源。

“404 Not Found” 原意是资源在本服务器上未找到,所以无法提供给客户端。但现在已经被“用滥了”,只要服务器“不高兴”就可以给出个 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,某种程度上它比 403 还要令人讨厌。

5××

5××类状态码表示客户端请求报文正确,但服务器在处理时内部发生了错误,无法返回应有的响应数据,是服务器端的“错误码”。

“500 Internal Server Error” 与 400 类似,也是一个通用的错误码,服务器究竟发生了什么错误我们是不知道的。不过对于服务器来说这应该算是好事,通常不应该把服务器内部的详细信息,例如出错的函数调用栈告诉外界。虽然不利于调试,但能够防止黑客的窥探或者分析。

“501 Not Implemented” 表示客户端请求的功能还不支持,这个错误码比 500 要“温和”一些,和“即将开业,敬请期待”的意思差不多,不过具体什么时候“开业”就不好说了。

“502 Bad Gateway” 通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。

“503 Service Unavailable” 表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。503 是一个“临时”的状态,很可能过几秒钟后服务器就不那么忙了,可以继续提供服务,所以 503 响应报文里通常还会有一个“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。

(五)HTTP的特点

1.灵活可扩展:HTTP在诞生之初只规定了报文的基本格式,比如用空格分隔单词,用换行分隔字段,“header+body”等,报文里的各个组成部分都没有做严格的语法语义限制,可以由开发者任意定制。而那些 RFC 文档,实际上也可以理解为是对已有扩展的“承认和标准化”,实现了“从实践中来,到实践中去”的良性循环。

2.可靠传输: 因为 HTTP 协议是基于 TCP/IP 的,而 TCP 本身是一个“可靠”的传输协议,所以 HTTP 自然也就继承了这个特性,能够在请求方和应答方之间“可靠”地传输数据。

3.应用层的协议: HTTP 凭借着可携带任意头字段和实体数据的报文结构,以及连接控制、缓存代理等方便易用的特性,只要不太苛求性能,HTTP 几乎可以传递一切东西,满足各种需求,称得上是一个“万能”的协议。

4.请求 - 应答:请求 - 应答模式是 HTTP 协议最根本的通信模型,通俗来讲就是“一发一收”。请求 - 应答模式也明确了 HTTP 协议里通信双方的定位,永远是请求方先发起连接和请求,是主动的,而应答方只有在收到请求后才能答复,是被动的,如果没有请求时不会有任何动作。

5.无状态: “状态”其实就是客户端或者服务器里保存的一些数据或者标志,记录了通信过程中的一些变化信息。HTTP在整个协议里没有规定任何的“状态”,但不要忘了 HTTP 是“灵活可扩展”的,虽然标准里没有规定“状态”,但完全能够在协议的框架里给它“打个补丁”,增加这个特性(cookie)。

6.明文传输: “明文”意思就是协议里的报文(准确地说是 header 部分)不使用二进制数据,而是用简单可阅读的文本形式。

7.不安全: 安全有很多的方面,明文只是“机密”方面的一个缺点,在“身份认证”和“完整性校验”这两方面 HTTP 也是欠缺的。

(六)HTTP的实体数据

数据类型
Accept

在TCP/IP协议栈里,数据的传输都是Header+body的形式。在传输层协议中,不需要关心数据是什么,但在应用层必须要告诉上层数据的类型,否则上层就不知该如何处理。最早的HTTP协议中,并没有附加的数据类型信息,所有传送的数据都被客户程序解释为HTML文档,而为了支持多媒体数据类型,HTTP协议中就使用了附加在文档之前的MIME(Multipurpose Internet Mail Extensions 多用途互联网邮件扩展类型)指定的数据类型信息来标识数据类型。MINE将数据分为七大类(video、image、application、text、audio、multipart、message),再以type/subtype的格式细分出其下的子类。例如我们常用到的text/html 、text/css 、image/jpeg 、 applaction/json等。

Accept-encoding

此外HTTP协议还制定了数据的压缩格式:

gzip:GNU zip 压缩格式,也是互联网上最流行的压缩格式;

deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;

br:一种专门为 HTTP 优化的新压缩算法(Brotli)。

Accept-Language

标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型,例如:Accept-Language: zh-CN, zh, en

数据类型在请求头中的表现

在 HTTP 协议里用 Accept、Accept-Encoding、Accept-Language 等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级,这里的“q”是“quality factor”的意思。权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。服务器会在响应头里多加一个 Vary 字段,记录服务器在内容协商时参考的请求头字段。

图片

(七)HTTP如何传输大文件

1.数据压缩

前面提到的accept-encoding请求头可以算是是一种传输大文件的解决方式,服务器可以选择一种浏览器支持的数据压缩方式放进content-encoding响应头里,再把原数据压缩后返回给客户端。缺点是这种方式只对文本有较好地压缩率,对于图片音频等本身就已经高度压缩的多媒体数据束手无策。

2.分块传输

在HTTP头部表示为Transfer-Encoding: chunked,指报文里的body部分不是一次性发过来的,而是分为许多chunked分块发送。Transfer-Encoding: chunked和Content-Length这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文的传输要么是长度已知,要么是长度未知(chunked),这一点你一定要记住。

3.范围请求

如果想获取某个大文件其中的片段,分块传输就没办法满足这样的需求。HTTP协议提出了范围请求这样的概念,允许客户端只获取文件的某一部分。客户端先发个HEAD请求看看服务器是否支持范围请求,服务器必须在Accept-Ranges响应头中告知客户端是否具有范围请求的能力。请求头Ranges是HTTP范围请求的专用字段,值的格式是bytes=x-y表示x ~ y之间的范围。服务端在收到 Ranges请求头时,首先验证x-y的范围是否合法(x和y可以省略,省略x则表示从后往前,省略y则表示从前往后),其次计算读取偏移量,返回206状态码和所读取的文件 ,最后在响应头加上Content-Range表示实际返回的偏移量和总数,格式为bytes x-y/length。

范围请求还支持在一个头里定义多个x-y,这种情况需要一种特殊的MIME类型multipart/byteranges,表示报文是有多段组成。

(八)HTTP连接管理

http的通信过程采取请求/应答模式,在http0.9/1.0时期,每次发起请求都需要建立连接->发送数据->断开连接,由于整个请求的过程非常短暂,早起的http也称为短链接无链接的协议。由于TCP简历连接要经过三次握手四次挥手,整个过程需要3个RTT,而HTTP的一次简单请求通常只需要2个RTT,那么被浪费掉的时间有60%。

Connection:keep-alive

HTTP1.1提出了长连接的概念,也就是Keep-alive。在长连接上建立一次TCP连接可以发送多个HTTP请求。但因为连接是alive的,如果一直不关闭,就会占用大量的服务器资源,导致服务无法及时响应真正的请求,所以我们也需要及时关闭连接。可以通过在客户端请求头添加Connection: close字段主动关闭连接。服务端通常不会主动关闭连接,但我们也可以通过设置时长、请求数等方式约定断开连接的条件。

队头阻塞

基于请求-应答模式的http协议,形成了串行的请求队列(http1.1还提出了管道机制,即在同一个TCP连接上不用等待上一个请求的响应即可发出下个请求,不过客户端还是按照正常顺序接受响应,这种做法并没带来任何性能上的改善,所以默认保持关闭),如果队首的请求处于阻塞状态,那么后面的请求也无法正常响应结果就是更长时间的性能浪费。

并发连接和域名分片是对队头阻塞的针对性优化策略,浏览器限制每个客户端可以并发建立6~8个连接,又可以将多个域名指向同一个服务器,这样实际的连接数量就更多了,是一种用数量解决质量的思路。

重定向

当我们在浏览器输入一个url再按下回车,页面跳转到我们输入的地址中,这种行为就是主动跳转。浏览器还支持被动跳转,也就是HTTP的重定向。

状态码

在前面了解过HTTP状态码,3XX即表示为重定向。下面详细介绍下各个状态码的含义。

301指永久重定向,可能是域名下线,域名迁移等原因,原地址不再维护。此时浏览器在重定向的同时记录重定向后的地址,下次访问该域名就自动访问新的URI了。

302指临时重定向,可能是服务器维护、临时关闭等原因,临时跳转到新的地址上,此时浏览器不会记录重定向的地址,认为原地址还是有效的,下次访问时还是优先访问原地址。

303类似 302,但要求重定向后的请求改为 GET 方法,访问一个结果页面,避免 POST/PUT 重复操作。

307类似 302,但重定向后请求里的方法和实体不允许变动,含义比 302 更明确。

308类似 307,不允许重定向后的请求变动,但它是 301“永久重定向”的含义。

可以在地址栏输入bing.com,浏览器控制台中的状态如下图所示:

客户端是如何处理重定向的

在浏览器地址栏输入bing.con我们可以看到,状态码如下图所示:

我们浏览器收到响应之后根据响应头中的Location字段判断重定向的地址,然后进行被动跳转。

虽然重定向的用途很广,但是随之而来的也有更多问题。

第一个问题是“性能损耗”。很明显,重定向的机制决定了一个跳转会有两次请求 - 应答,比正常的访问多了一次。虽然 301/302 报文很小,但大量的跳转对服务器的影响也是不可忽视的。站内重定向可以长连接复用,站外重定向就要开两个连接。

第二的问题是循环重定向,比如 A->B->C->A,当我们访问A时就会发生无限跳转。所以HTTP协议特别规定,浏览器必须具有检测“循环跳转”的能力,在发现这种情况时应当停止发送请求并给出错误提示。

(九)cookie

HTTP 是“无状态”的,这既是优点也是缺点。优点是服务器没有状态差异,可以很容易地组成集群,而缺点就是无法支持需要记录状态的事务操作。好在 HTTP 协议是可扩展的,后来发明的 Cookie 技术,给 HTTP 增加了“记忆能力”。

图片

cookie同样存在于HTTP头部字段里。服务端可以使用set-cookie标识客户端身份,客户端则在请求时携带cookie告诉服务端自己的信息。cookie字段以key=value的格式保存,浏览器在一个cookie字段里可以存放多对数据,用;分割。

Cookie 主要用于以下三个方面:

1.会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)

2.个性化设置(如用户自定义设置、主题等)

3.浏览器行为跟踪(如跟踪分析用户行为等)

相关属性
生存周期

Expires俗称“过期时间”,用的是绝对时间点,可以理解为“截止日期”(deadline)。

Max-Age用的是相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。

Expires 和 Max-Age 可以同时出现,两者的失效时间不一致时浏览器会优先采用Max-Age计算失效期。如果服务器不设置Max-Age、Expries或者字段值为0指不能缓存cookie,但在会话期间是可用的,浏览器会话关闭之前可以用cookie记录用户的信息。

作用域

Domain和Path指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。通常 Path 就用一个“/”或者直接省略,表示域名下的任意路径都允许使用 Cookie。

安全性

HttpOnly表示此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问。这也是预防“跨站脚本”(XSS)攻击的有效手段。

SameSite可以防范“跨站请求伪造”(XSRF)攻击,SameSite = strict表示禁止cookie在跳转链接时跨域传输。SameSite = lax稍微宽松一点,允许在GET、HEAD等安全请求方式中跨域携带。默认值为none,表示不限制cookie的携带和传输。

Secure表示这个cookie仅能用HTTPS协议加密传输,明文的HTTP协议会禁止发送。但Cookie本身不是加密的,浏览器里还是以明文的形式存在。

(十)HTTP缓存控制

服务器的缓存控制

浏览器在访问页面资源时首先会查找缓存数据,如果没有再发送请求,向服务器获取资源;服务器响应请求,返回资源,同时标记资源的有效期;浏览器缓存资源,等待下次重用。这就是客户端缓存。服务器标记资源有效期使用的头字段是Cache-Control,里面的值max-age=xxx就是资源的有效时间(与cookie的max-age不同,这里的max-age时间的计算起点是响应报文的创建时刻)。

此外在响应报文里还可以用其他的值来更精确地指示浏览器应该如何使用缓存:

no-store: 不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面。

no-cache: 可以缓存,但在使用之前必须要去服务器验证是否过期。

must-revalidate: 如果缓存不过期就可以继续使用,但过期了就必须去服务器验证。

客户端的缓存控制

浏览器也可以发Cache-Control,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。在浏览器前进、后退、重定向时cache-control就生效了,响应头里有from disk cache字样,就说明浏览器未发送请求,而是直接使用了本地缓存。

条件请求

浏览器在刷新页面时相当于在请求头中添加了Cache-Control:no-cache,这样在刷新页面时,还是向服务端发送了请求,并没有很好的利用到缓存。所以HTTP协议又定义了一系列“If”开头的“条件请求”字段,专门用来检查验证资源是否过期。

条件请求一共有 5 个头字段,我们最常用的是if-Modified-Since和If-None-Match这两个。需要第一次的响应报文预先提供Last-modified(最后修改时间)和ETag(资源唯一标识),然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。

代理缓存
代理服务器
代理服务器就是客户端和服务端之间的中间商,在中间的位置转发上游的请求和下游的响应。代理服务器在计算机领域有非常重要的功能:

1.负载均衡:面向客户端时屏蔽原服务器,代理服务器可以通过轮询、哈希等算法将流量分发,提高整体的性能。

2.健康检查:使用‘心跳’等机制监控服务器,保证服务器的可用性。

3.安全防护:保护被代理服务端的IP和流量,防止网络攻击或负载问题。

4.加密卸载:对外和对内使用不同的加密策略,节省加密成本

5.内容缓存:暂存/复位服务器的响应。

缓存代理

HTTP的服务端缓存主要由代理服务器来实现,代理服务器收到源服务器的响应之后将报文转发给客户端的同时也存入自己的cache里,下次再有相同的请求就可以直接发送304或者缓存数据,节省源服务器的成本。

因为代理服务器既是服务端,又是客户端的特性,有一些特殊的cache-control属性:

1.服务端

private: 表示只能客户端缓存,不允许代理服务器上缓存。

punlic:表示完全公开,客户端和代理服务器都可以缓存。

proxy-revalidate:要求代理服务器缓存过期后必须回源验证。

s-maxage: 代理服务器缓存的有效期

no-transform: 不允许代理服务器转换数据格式。

2.客户端

max-stale: 如果代理上的缓存过期了也可以接受,但不能过期太多,超过 x 秒也会不要。

min-flash: 表示缓存少于x有效期就不要了。

only-if-cached:表示只接受代理缓存的数据,不接受源服务器的响应。如果代理上没有缓存或者缓存过期,就应该给客户端返回一个 504。

图片
图片

HTTPS

由于 HTTP 天生“明文”的特点,整个传输过程完全透明,任何人都能够在链路中截获、修改或者伪造请求 / 响应报文,数据不具有可信性。只有具有机密性、完整性、身份认证和不可否认性,我们才认为这个请求是安全的。HTTPS为HTTP增加了以上四个特性。

HTTPS实际上就指的是HTTP over TLS/SSl。是在原本的HTTP协议上加了一层TLS/SSL协议。

图片

(一)SSL/TLS

SSL 即安全套接层(Secure Sockets Layer),在 OSI 模型中处于第 5 层(会话层),由网景公司于 1994 年发明。SSL 发展到 v3 时已经证明了它自身是一个非常好的安全通信协议,于是在 1999 年它改名为 TLS(传输层安全, Transport Layer Security),目前应用的最广泛的 TLS 是 1.2,而之前的协议(TLS1.1/1.0、SSLv3/v2)都已经被认为是不安全的。

机密性(基于TLS1.2)

SSL/TLS通过加密(encrypt)来传输密文(cipher text)保证数据传输的安全性,只有拥有密钥(key)的人才能够通过解密(decrypt)获得明文(plain text/clear text),加密解密的操作过程就是加密算法。所以“密钥”是一长串的数字,约定俗成的度量单位是“位”(bit)。比如,说密钥长度是 128,就是 16 字节的二进制串,密钥长度 1024,就是 128 字节的二进制串。按照密钥的使用方式,加密可以分为两大类:对称加密和非对称加密。

对称加密

顾名思义,加密解密都使用相同的密钥就叫做对称加密。TLS里目前常用的有 AES 和 ChaCha20。

AES 的意思是“高级加密标准”(Advanced Encryption Standard),密钥长度可以是 128、192 或 256。它是 DES 算法的替代者,安全强度很高,性能也很好,而且有的硬件还会做特殊优化,所以非常流行,是应用最广泛的对称加密算法。

ChaCha20 是 Google 设计的另一种加密算法,密钥长度固定为 256 位,纯软件运行性能要超过 AES,曾经在移动客户端上比较流行,但 ARMv8 之后也加入了 AES 硬件优化,所以现在不再具有明显的优势。

非对称加密

对称加密看上去很好的实现了机密性,但是还有一个问题就是如何安全的传输密钥。因为在加密算法中,只要拥有密钥就可以解密,如果密钥在传输过程中被窃取,也就无机密性可言。为了解决这个问题,又有了非对称加密算法。他拥有两个密钥,分别是公钥(public key)和私钥(private key),公钥是公开的,而私钥是严格保密的。公钥和私钥有个特别的“单向”性,虽然都可以用来加密解密,但公钥加密后只能用私钥解密,反过来,私钥加密后也只能用公钥解密。非对称加密可以解决密钥交换的问题。网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文。

非对称加密算法的设计要比对称算法难得多,在 TLS 里只有很少的几种,比如 DH、DSA、RSA、ECC 等。

RSA 可能是其中最著名的一个,几乎可以说是非对称加密的代名词,它的安全性基于“整数分解”的数学难题,使用两个超大素数的乘积作为生成密钥的材料,想要从公钥推算出私钥是非常困难的。

ECC是非对称加密里的“后起之秀”,它基于“椭圆曲线离散对数”的数学难题,使用特定的曲线方程和基点生成公钥和私钥,子算法 ECDHE 用于密钥交换,ECDSA 用于数字签名。相对RSA,ECC在安全和性能上都有更明显的优势,160位的ECC相当于1024位的RSA,260位的ECC相当于2048位的RSA。

混合加密
虽然非对称加密没有密钥交换的难题,但因为它们都是基于复杂的数学难题,运算速度很慢,即使是 ECC 也要比 AES 差上好几个数量级。所以目前TLS使用混合加密,使二者取长补短,既能高效加密解密,又能安全的进行数据传输。

在建立连接之初先使用非对称加密的形式传递密钥,然后用随机数产生对称算法使用的“会话密钥”(session key),再用公钥加密。因为会话密钥很短,通常只有 16 字节或 32 字节,所以慢一点也无所谓。对方拿到密文后用私钥解密,取出会话密钥。这样,双方就实现了对称密钥的安全交换,后续就不再使用非对称加密,全都使用对称加密。

完整性
摘要算法

实现完整性的手段主要是摘要算法(Digest Algorithm),也就是常说的散列函数、哈希函数(Hash Function)。可以把摘要算法近似地理解成一种特殊的加密算法,它能够把任意长度的数据加密成固定长度、而且独一无二的“摘要”字符串,且不能从压缩后的密文中推导出原文。MD5(Message-Digest 5)、SHA-1(Secure Hash Algorithm 1就是最常用的两个摘要算法,能够生成 16 字节和 20 字节长度的数字摘要。但这两个算法的安全强度比较低,不够安全,在 TLS 里已经被禁止使用了。目前TLS使用的是SLA-2。摘要算法保证了“数字摘要”和原文是完全等价的。所以,我们只要在原文后附上它的摘要,就能够保证数据的完整性。不过摘要算法不具有机密性,所以真正的完整性还是需要建立在机密性之上。

身份认证&不可否认
数字签名
数字签名的原理其实很简单,就是把公钥私钥的用法反过来,之前是公钥加密、私钥解密,现在是私钥加密、公钥解密。但又因为非对称加密效率太低,所以私钥只加密原文的摘要,这样运算量就小的多,而且得到的数字签名也很小,方便保管和传输。
数字证书和CA

因为公钥是任何人都可以发布的,所以我们需要引入第三方来保证公钥的可信度,这个“第三方”就是我们常说的 CA(Certificate Authority,证书认证机构),CA 对公钥的签名认证也是有格式的,要包含公钥的序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书”(Certificate)。小一点的 CA 可以让大 CA 签名认证,但链条的最后,也就是 Root CA,就只能自己证明自己了,这个就叫“自签名证书”(Self-Signed Certificate)或者“根证书”(Root Certificate)。你必须相信,否则整个证书信任链就走不下去了。

(二)TLS1.2建立连接的过程

TLS协议的组成

记录协议(Record Protocol): 规定了 TLS 收发数据的基本单位:记录(record)。所有的其他子协议都需要通过记录协议发出,但多个记录数据可以在一个 TCP 包里一次性发出。

警报协议(Alert Protocol): 的职责是向对方发出警报信息,有点像是 HTTP 协议里的状态码。比如,protocol_version 就是不支持旧版本,bad_certificate 就是证书有问题,收到警报后另一方可以选择继续,也可以立即终止连接。

握手协议(Handshake Protocol): 是 TLS 里最复杂的子协议,要比 TCP 的 SYN/ACK 复杂的多,浏览器和服务器会在握手过程中协商 TLS 版本号、随机数、密码套件等信息,然后交换证书和密钥参数,最终双方协商得到会话密钥,用于后续的混合加密系统。

变更密码规范协议(Change Cipher Spec Protocol): 是一个“通知”,告诉对方,后续的数据都将使用加密保护。那么反过来,在它之前,数据都是明文的。

基于ECDHE 的 TLS1.2 握手
图片

(三)TLS1.3

图片
图片

HTTP/2

HTTP1.X引入了Cookie解决了无状态的问题、通过引入TLS/SSL解决了明文传输不安全的问题。那接下来HTTP2的发力点就放在性能层面了。Google首先发明了SPDY协议,随后互联网标准化组织IETF以SPDY 为基础发布了HTTP2。HTTP2对于性能上的优化主要由以下几点出发:1. 包头过大 2. 队头阻塞 。

(一)性能优化

包头过大(头部压缩)

在HTTP1.x时期,很多请求请求体和响应体的大小远远小于头部字段的大小,比如GET请求,301/302/204响应。而且很多头部字段是重复的,HTTP/1.x浪费了大量的带宽在传输重复的头字段上,所以,HTTP/2 把“头部压缩”作为性能改进的一个重点。

HPACK算法是专门为压缩 HTTP 头部定制的算法,与 gzip、zlib 等压缩算法不同,它是一个“有状态”的算法,需要客户端和服务器各自维护一份“索引表”,压缩和解压缩就是查表和更新表的操作。为了方便管理和压缩,HTTP/2 废除了原有的起始行概念,把起始行里面的请求方法、URI、状态码等统一转换成了头字段的形式, 为了与“真头字段”区分开来,这些“伪头字段”会在名字前加一个“:”,比如“:authority” “:method” “:status”,分别表示的是域名、请求方法和状态码。废除了起始行里的版本号和错误原因短语。用索引号表示重复的字符串,还釆用哈夫曼编码来压缩整数和字符串,可以达到 50%~90% 的高压缩率。

下面的这个表格列出了“静态表”的一部分,这样只要查表就可以知道字段名和对应的值,比如数字“2”代表“GET”,数字“8”代表状态码 200。

图片

新增的头字段或者值保存在动态表(Dynamic Table)里,它添加在静态表后面,结构相同,但会在编码解码的时候随时更新。比如说,第一次发送请求时的“user-agent”字段长是一百多个字节,用哈夫曼压缩编码发送之后,客户端和服务器都更新自己的动态表,添加一个新的索引号“65”。那么下一次发送的时候就不用再重复发那么多字节了,只要用一个字节发送编号就好。

图片

队头阻塞(二进制分帧、流式传输)

基于请求-应答模式的http协议存在队头阻塞的问题,前面提到的并发连接和域名分片都是牺牲数量解决质量的思路。而HTTP2采用了二进制分帧➕流式传输的方式来解决这个问题。

二进制分帧

HTTP/2把原来的Header+Body的消息“打散”为数个小片的二进制“帧”(Frame),用HEADER帧存放头数据、DATA帧存放实体数据。

流式传输

HTTP/2还定义了一个“流”(Stream)的概念,它是二进制帧的双向传输序列,同一个消息往返的帧会分配一个唯一的流 ID。你可以把它想象成是一个虚拟的“数据流”,在里面流动的是一串有先后顺序的数据帧,这些数据帧按照次序组装起来就是HTTP/1里的请求报文和响应报文。HTTP/2 可以在一个 TCP 连接上用“流”同时发送多个“碎片化”的消息,这就是常说的“多路复用”( Multiplexing),多个往返通信都复用一个连接来处理。在“流”的层面上看,消息是一些有序的“帧”序列,而在“连接”的层面上看,消息却是乱序收发的“帧”。多个请求 / 响应之间没有了顺序关系,不需要排队等待,也就不会再出现“队头阻塞”问题,降低了延迟,大幅度提高了连接的利用率。

图片

帧开头是帧长度(不包含报文头的9个字节),默认上限是2^14,最大是2^24,也就是说 HTTP/2的帧通常不超过16K,最大是 16M。

后面的一个字节是帧类型,大致可以分成数据帧和控制帧两类,HEADERS帧和DATA帧属于数据帧,存放的是 HTTP 报文,而 SETTINGS、PING、PRIORITY 等则是用来管理流的控制帧。

第 5 个字节是非常重要的帧标志信息,可以保存 8 个标志位,携带简单的控制信息。常用的标志位有 END_HEADERS 表示头数据结束,END_STREAM 表示单方向数据发送结束(即 EOS,End of Stream)。

报文头里最后 4 个字节是流标识符,也就是帧所属的“流”,接收方使用它就可以从乱序的帧里识别出具有相同流 ID 的帧序列(在 HTTP/2 连接上,虽然帧是乱序收发的,但只要它们都拥有相同的流 ID,就都属于一个流,而且在这个流里帧不是无序的,而是有着严格的先后顺序。),按顺序组装起来就实现了虚拟的“流”。流标识符虽然有 4 个字节,但最高位被保留不用,所以只有 31 位可以使用,也就是说,流标识符的上限是 2^31,大约是 21 亿。

流的特点

1.流是可并发的,一个 HTTP/2 连接上可以同时发出多个流传输数据,也就是并发多请求,实现“多路复用”。

2.客户端和服务器都可以创建流,双方互不干扰。

3.流是双向的,一个流里面客户端和服务器都可以发送或接收数据帧,也就是一个“请求 - 应答”来回。

4.流之间没有固定关系,彼此独立,但流内部的帧是有严格顺序的。

5.流可以设置优先级,让服务器优先处理,比如先传 HTML/CSS,后传图片,优化用户体验。

6.流 ID 不能重用,只能顺序递增,客户端发起的 ID 是奇数,服务器端发起的 ID 是偶数。

7.在流上发送“RST_STREAM”帧可以随时终止流,取消接收或发送。

8.第 0 号流比较特殊,不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。

协议栈

图片
图片

HTTP/3

HTTP/2 虽然使用“帧”、“流”、“多路复用”,没有了“队头阻塞”,但这些手段都是在应用层里,而在 TCP 协议里,还是会发生“队头阻塞”。Google 在推 SPDY 的时候就已经意识到了这个问题,于是就又发明了一个新的QUIC协议,让 HTTP 跑在 QUIC 上而不是 TCP 上。而这个HTTP over QUIC就是 HTTP 协议的下一个大版本,HTTP/3。它在 HTTP/2 的基础上又实现了质的飞跃,真正完美地解决了队头阻塞问题。

图片

(一)QUICK

QUIC 基于 UDP,而 UDP 是“无连接”的,不需要“握手”和“挥手”,所以天生就要比 TCP 快。QUIC 全面采用加密通信,它使用自己的帧“接管”了 TLS 里的“记录”,握手消息、警报消息都不使用 TLS 记录,直接封装成 QUIC 的帧发送,省掉了一次开销。QUIC 的基本数据传输单位是包(packet)和帧(frame),一个包由多个帧组成,包面向的是“连接”,帧面向的是“流”。

QUIC 使用不透明的“连接 ID”来标记通信的两个端点,客户端和服务器可以自行选择一组 ID 来标记自己,这样就解除了 TCP 里连接对“IP 地址 + 端口”(即常说的四元组)的强绑定,支持“连接迁移”(Connection Migration)。

图片

(二)HTTP/3

因为 QUIC 本身就已经支持了加密、流和多路复用,所以HTTP/3不需要定义流,而是直接使用 QUIC 的流。由于流管理被“下放”到了 QUIC,所以 HTTP/3 里帧的结构也变简单了。帧头只有两个字段:类型和长度,而且同样都采用变长编码,最小只需要两个字节。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门