前面几篇文章,我从纵向的空间到横向的时间,再到一个具体的小栗子,可以说是全方位,无死角的覆盖了HTTP的大部分基本框架,但是我聊的都太宽泛了,很多内容都是一笔带过,再加上一句后面再说就草草结束了。并且我还漏了一点东西,就是HTTP本身。
所以那,这一章,我们回到我们的核心论点,来聊一聊HTTP的特性以及起始行中的核心内容。
一、What is HTTP?
这个问题如果大家看过前面几篇文章,肯定能很轻易的回答:HTTP是应用层协议,用来传输超文本,或者可以说是用来传输超媒体的一种协议,HTTP是无状态的基于请求-响应模型的。你说的没错,接下来我也可能会聊到你想到的这些。但是还有呢?还有呢?下面,我们就来捋一捋HTTP的特点,来说一下这“还有”的部分是什么。
1)灵活且容易扩展
灵活易扩展,可以说是HTTP这个协议的最大的特点,也是最有价值的特点。为什么这么说呢?大家如果跟着这个系列阅读到了这里,肯定能理解,因为这个特点是HTTP可以发展至今经久不衰的原因。我们先来看一点点图片,要不然都是字挺无聊的。
上图,是RFC1945也就是HTTP/1.0的Additional Features部分,这部分增加了一些请求方法和头字段规定,注意我之前说过,HTTP/1.0还算不上是标准,只是个备忘录,但是在这个备忘录的最后,已经确定了一部分未来的标准的内容。
我们再来看张图:
这是RFC2616也就是HTTP/1.1标准的头字段的部分定义,是不是有些字段看上去很熟悉。没错,有些1945中的附加内容,在2616中就已经变成标准了。这也算是可以体现HTTP协议灵活且易扩展的一个方面了吧。
这些RFC文档,实际上更像是对于已有扩展的承认和标准化,实现了“从实践中来,到实践中去”的良性循环。这也是为啥HTTP可以经久不衰的原因了。
灵活且易扩展这个特性也会在接下来的文章中一再的强调,因为你会逐渐的发现除了极少的部分,HTTP几乎没有不能扩展的部分。
2)可靠传输:值得依赖的背后的人
其实说HTTP协议可靠协议并不是特别的准确,HTTP的可靠性一部分来自于HTTP协议自身,一部分来自于它所依赖的上层TCP协议。
TCP本身就是一个可靠的传输协议,它的可靠性其实就是来自于三次握手,通过客户端与服务器的沟通来确定是否建立连接,UDP就没这么麻烦,传过去就完事了,我管你收没收到。所以基于这样的因素,TCP就不可避免的要比UDP慢不少。牺牲了速度,确保了稳定。
而HTTP可靠的另外一部分,则是来自于它本身的数据包的形式,也就是它加上的那一层HTTP头。
我们说HTTP是可靠的协议,但是它的可靠,并不是绝对的纯粹的可靠,只是一种理论场景下的可靠。也就是说在正常网络环境和物理设备的情况下,HTTP会尽可能的把数据准确传递到另一端。只是尽可能。
比如,光纤断了,你啥玩意来了都传输不过去。哈哈哈哈
3)应用层协议
这个东西显而易见啦~但是我还得磨叽两句,如果你读了空间穿梭就会知道其实应用层协议有很多。为啥只有HTTP应用如此广泛和出名呢?嗯……因为它比较能打,怎么用都行,又因为具有灵活可扩展的特点,所以它几乎可以传输一切内容。
所以像比如FTP、SSH啥的,只能在特定的领域使用,固然就没有HTTP应用这么广泛,这么知名了。
4)请求-应答模型
这个我之前也说过。HTTP是基于请求-应答模型的应用层协议。简单来说就是一来一回,有来有回。
请求-应答模型还有一点就是:明确的规定了HTTP协议里双方的定位,永远是请求方先发起连接和请求,是主动的,应答方只能在接收到请求后进行答复,是被动的。注意我说的话,我说的是请求方和应答方,而没有绝对的说是浏览器方和服务器方,因为发起请求的一方不一定仅仅只是浏览器,服务器也可以作为请求的发起方,比如代理服务器。
另外,HTTP的请求-应答模式,也恰好契合了传统的 C/S(Client/Server)系统架构,请求方作为客户端、应答方作为服务器。所以,随着互联网的发展就出现了 B/S(Browser/Server)架构,用轻量级的浏览器代替笨重的客户端应用,实现零维护的“瘦”客户端,而服务器则摈弃私有通信协议转而使用 HTTP 协议。
最后,请求 - 应答模式也完全符合 RPC(Remote Procedure Call)的工作模式,可以把 HTTP 请求处理封装成远程函数调用,导致了 WebService、RESTful 和 gRPC 等的出现。
嗯,这段话是我抄的~
5)无状态
这个东西想必大家也很熟悉了吧,我印象中我之前也提到过这个无状态。那我想问你个问题,TCP是有状态还是无状态的呢?你猜一猜。答案我放在文末了。
HTTP的无状态主要体现在数据包的一次性上,我发完了就不管了,收到信息就处理,客户端和服务器都不会记录之前发送过的包的任何信息。HTTP报文本身没有互相的联系,这一次的包和上一次的包也没有任何关联,它仅有的联系在发送和响应的过程中也显得微不足道,嗯……我说的这个联系其实指的仅仅是发过去了,收到了,返回了,结束了,这样的联系(好像跟没联系也差不多~)。
当然,这种无状态也可以通过一点点小手段来解决,哈哈哈哈,我猜你猜到是啥了,嗯,就是cookie。
最后,要着重说的特点就这些,但是你发现一个问题没有,其实好多特点,都算是灵活可扩展的子特点,那么还可以说出有关灵活可扩展所延伸出的哪些特点呢?
二、HTTP的优缺点:其实我并没有你想象得那么好。
其实这一小节可以和上一小节合并一下,但是计划都已经写到这了,就这样吧。
HTTP的特点有很多,但是特点不代表优点,其中也有不少缺点,其实整个HTTP的历史,实际上在做的事情就是扬长避短,发挥它的长处,尽量弥补它的短处。唉……果然到哪里都要做到全方位多面体发展,就连一个协议都要这么卷,心累~。
1)HTTP的优点
我们先来稍微说下HTTP的优点吧。
灵活易扩展
这肯定没毛病吧,要是没有这个优点,你现在都不一定用学HTTP,可能在学另外一个应用层协议,幻想一下,可能叫做ZakingTTP。哈哈哈哈。
正是因为灵活,所以才容易扩展,而容易扩展正好验证了HTTP的灵活,所以其实灵活与易扩展是相辅相成的,互为表里的,不可拆分的一个优点。HTTP里几乎每一个核心要素都没有被硬性的要求一定要怎么样怎么样,给予了开发者极大的便利和*,这个我们再后面也会再次强调。
灵活易扩展的特性不仅仅应用其自身,它不限制具体的下层协议,你随便,只要你理论上是可靠的就行。
简单
简单这个优点可能有点让人无语,HTTP简单么?简单为什么我学了这么久感觉还是差点什么。
其实我个人理解,HTTP是简单的,但是正是因为它的简单,所以它才没有那么容易。简单的是它的设计,是它的学习门槛,夸张一点说,你甚至不用学习HTTP,仅仅看报文的名字都能猜到个一二。
也正是因为它的简单,所以才可以灵活易扩展,这两个优点,就意味着HTTP有无限的可能。无限的可能,就意味着它的内容会很多,很杂,甚至可以自行设计,对于初学者,就或许没那么友好了。
应用广泛、环境成熟
HTTP的另一大特点就是应用广泛,软硬件环境都非常成熟。你几乎可以在任意的互联网通信场景中找到HTTP的影子,比如Web页面,比如台式机的浏览器,手机上的APP等等,从你看到新闻、视频、手机上的游戏,都离不开HTTP。
不仅仅是在应用领域,在开发领域,HTTP协议也得到了广泛的支持。它并不限定某种编程语言或者操作系统,所以天然具有跨平台,跨语言的优越性。而且因为HTTP本身的简单性,几乎所有的编程语言都有HTTP的调用库和相关的测试工具。
所以你现在知道HTTP有多重要了吧。
2)HTTP的缺点
无状态
其实无状态并不算是确切的缺点,但是我把它归类到缺点里了。因为我个人觉得,无状态所带来的好处并不足以覆盖它所带来的问题。
无状态的好处是它减少了服务器因为需要“记忆”而产生的额外的存储需要和性能耗费。
另外一个好处就是,正是因为它是无状态的,所以它可以很容易的进行组合,让负载均衡把请求转发到任意一台服务器,不会因为状态不一致而导致出错。
那么我们继续说说无状态所带来的问题,没有状态就意味着我无法关联两次请求,无法支持多个相互关联且连续的“事务”。比如购物车买东西,你是要知道用户身份的,无状态我怎么能知道是谁买的呢?再比如网站的登录系统,也是一定要携带用户标识加以区分是“你”还是“我”的问题。
而正是因为这个问题,所以才有了cookie,这个小饼干。
明文
这肯定是实打实的缺点了,明文意味着谁都可以获取发送的报文,甚至随意修改和更换。它的明文虽然可以方便阅读和调试,但是所带来的安全性问题实在是无法被忽视的。
不安全
这个缺点,其实一部分来自于明文,而其它的不安全的地方则是由于HTTP自身的欠缺。明文只是机密性方面的一个缺点,而在身份认证和完整性校验上面,HTTP也是欠缺的。
身份认证,简单来说就是你怎么证明你是你。那在现实生活中你是怎么证明你是你得呢?身份证!没错,HTTP缺了一个类似身份证一样的东西。
而完整性校验则是说,HTTP的数据在传输的过程中如果被篡改了是无法被感知且无法验证其真伪的。虽然我们或许可以加上一些数字摘要,但是又由于它是明文的,还是可以被第三方获取和篡改,本质并没有什么区别。
所以,正是由于明文和不安全的问题,才出现了HTTPS,甚至是现在几乎要求所有的网站都使用HTTPS,无一例外。这个我们聊到HTTPS的时候再说哈。
性能一般
为什么HTTP的性能一般呢?实际上本质的问题就是队头阻塞,前面的卡住了,后面的就要一直等一直等。很好理解,对吧?
HTTP的发展,一直致力于解决这样的性能问题,换句话说就是解决队头阻塞的问题,虽然在HTTP/1.1,HTTP/2中一定程度上解决了HTTP的队头阻塞问题,但是却无法解决TCP的队头阻塞,所以才有了HTTP/3的终极方案,直接不用TCP了。
三、HTTP方法简介:最熟悉的陌生人
我相信你很熟悉HTTP的方法,天天都在用,怎么可能会不熟悉。但是你真的熟悉HTTP的方法了么?我觉得并没有,看完这一小节,我相信你就会真正的熟悉了你每天都要面对的最熟悉的陌生人。
首先,RFC1945的规定的方法只有三个:GET、HEAD、POST,而2616则在此基础上又多了五个:PUT、DELETE、TRACE、CONNECT、OPTIONS,还有安全和幂等。要注意,这些单词都必须是大写的。
我们简单来看下这些方法都代表了什么意思:
- GET:获取资源,主要的目的是从目标Request-URI获取资源。
- HEAD:获取资源的头信息。
- POST:就是向Request-URI上传数据,或者提交数据。
- PUT:类似于POST。
- DELETE:删除目标资源。
- TRACE:追踪请求-响应的路径。
- CONNECT:建立一个特殊的连接隧道。
- OPTIONS:列出允许对该资源使用的方法。
我们简单的罗列了一下HTTP所规定的请求方法。其中前四个比较常用,GET和POST这两个是最常用的。后面的四个用的就很少了,甚至有些实践中几乎没有使用。我们先看看这些方法,它都对资源做了哪些事情?增删改查!
没毛病,你的观察力很强。那我有个问题,我向服务器提交了一个DELETE请求,希望删除服务器的某个资源,服务器一定会按照我的请求删除该资源么?答案是你请求你的,我听不听的决定权在我。所以,客户端只有建议,无法要求。
举个小例子,你希望可以通过GET请求获取服务器的私密文件,这个文件保存了所有用户的账号和密码,地址是:https://www.zaking.com/users/password.txt。然后你开开心心的发出了你的请求,服务器接收到你的请求一看,卧槽,这逼要请求这个文件,这是来黑我服务器了吧?果断甩个500就不搭理你了。
我们简单的理解了下方法有哪些,方法能干啥。那么下面,我们就来详细的解释下这些方法。
1)GET
GET方法,不用说,是HTTP中最古老的方法,没有之一,从HTTP诞生一直辉煌至今,无人可以替代。
GET的含义就是从服务器获取资源,这个资源既可以是静态的文本、页面、图片、视频等媒体资源,也可以是由后端语言比如PHP、JAVA等动态生成的各种类型数据。
但是GET方法并不单纯,在规范中,还有两种类型的GET:conditional GET 和 partial GET。也就是有条件的GET请求和部分GET请求。什么意思呢,当GET请求与If-Modified-Since字段配合使用,就变成了conditional GET,仅当资源被修改的时候才会执行GET操作。而partial GET则是Range字段和GET一起使用的情况下产生的,只会返回整体数据的一部分。
这样做的目的,其实就是为了节省资源。
2)HEAD
HEAD方法与GET方法是完全相同的,也是从服务器获取资源,服务器的处理机制也与GET一样,只不过不会返回body,只会传回响应头,也就是资源的“元信息”。
HEAD作用是为了解决某些无需body的场景下使用GET请求造成的资源浪费,比如我只需要确定我是否可以对服务器上对应的资源做某些指定的操作,那我们直接使用HEAD方法就可以了。
3)POST
POST方法与GET正好相反,是向服务器传输数据,传输的数据就放在body里。
POST方法算是在日常工作实践中使用频率仅次于GET请求的HTTP方法,甚至在某些个性化约束下,项目中所有的请求都使用POST,连GET都不用。
4)PUT
PUT方法和POST方法十分类似,也可以向服务器提交数据。但是这两者其实有一点点微妙的不同,PUT方法目的在于“修改”,而POST则是“新建”。
好吧,你说我知道这个东西有啥用嘛~其实我个人觉得用处不大,因为大多数的实践用由于两者太过相似,压根就不适用PUT,一个POST解决所有的问题。
5)DELETE
DELETE方法用于删除服务器的资源,但是由于这个操作对于服务器来说太过危险,所以服务器往往都会忽略这个方法,友好一点的就做个假删除,给资源打个删除的标记。甚至强硬一点的,直接不搭理你。
6)TRACE
该方法,本意是用于对HTTP请求链路的测试和诊断,可以显示请求-响应的路径,出发点是好的,但是无奈存在漏洞,会泄漏网站的信息,所以也是会被服务器拒绝的。
7)CONNECT
这个方法比较特殊,要求服务器为客户端和另外一台服务器建立一条通道,这时候,被请求的服务器就充当了代理的角色。
8)OPTIONS
该方法要求服务器列出可对该资源使用的方法列表,会在响应头的Allow字段中返回。它的功能有限,用处不大,所以有些服务器根本就没有对他的实现,比如Nginx。
9)扩展
这八个方法我们大致了解了一下,总结来说就是常用的经常用,不常用的几乎没用,好吧~又废话了。
还记得我们之前说过,HTTP是灵活且易扩展的,所以,对于HTTP的方法来说,也是可以扩展的。只要你和服务器都做好了约定,你可以随意扩展你的HTTP方法。不知道大家还有没有印象,有一个著名的愚人节玩笑,官方发布了一个基于HTTP的协议,叫做HTCPCP协议,即超文本咖啡壶协议,为HTTP协议增加了用来煮咖啡的BREW方法,哈哈哈哈。大家有兴趣可以看一下,还有RFC文档呢,编号是2324。
除了HTCPCP对HTTP的玩笑性扩展,还有一些在实际应用中扩展的请求方法。比如PATCH、LOCK、UNLOCK等,如果有合适的场景,你也完全可以使用这些方法,当然,注意要获得服务器的支持。
10)安全与幂等
这两个概念,还是挺重要的,安全是指请求方法不会破坏服务器上的资源,幂等则是指多次执行相同请求,每次返回的结果也应该是相同的。
那么在HTTP的方法里,GET和HEAD方法是安全的,POST、PUT、DELETE则是不安全的。GET、HEAD、DELETE、PUT则是幂等的,POST则不是幂等的。为啥POST不是幂等的呢,因为多次提交数据会创建多个资源,还记得我们之前说过POST是创建,PUT是更新吧,而多次使用PUT更新一个资源,还是第一次更新的状态,所以PUT是幂等的。
好啦,方法我们大概都理解了。我们继续~
四、URI:爸爸和双胞胎兄弟
额,其实我在之前的一些文章中有过对URI的详细的讲解,比如浏览器原理之跨域?跨站?你真的不懂我!和真正“搞”懂HTTP协议01之背景故事中都聊过URI。我在这里简单的说说吧,更详细的可以去查看这两篇内容。
其实URI是一个总称,算是爸爸的角色,而URL和URN则是一个双胞胎兄弟,URI叫做统一资源标识符,说白了就是用来在网上找资源的。而URL则是统一资源定位符,用资源在互联网上的地址作为标识,URN呢则是叫做统一资源名称,通过文件名来定位网络上的资源。
但是大家再接触这些东西的时候为啥会觉得混乱呢?其实就是一个儿子太出名了,一个儿子不咋出名,所以渐渐的就忘了那个不咋出名的儿子,把出名的儿子和爸爸搞混了。嗯……就这样~
这里我要额外的强调一点,不知道大家在日常的工作中,针对拼接URL往往会有这样的规范:“/”加头不加尾。什么意思呢?比如我们要拼接一个URL字符串:
// ok /data-center/queryList // 不ok data-center/queryList/
为啥会这样要求呢。是因为关于URI的组成的规范。我们简单来了解下。
一个URI通常由五部分组成:
其实你也可以理解为四部分,"://"是个固定操作。我们分别来解释下。
1)scheme
其实就是协议名,标识资源应该使用哪种协议来访问。最常见的就是http,当然还有https、ftp等等。
紧随着scheme后面的就是“://”,它把协议和后面的部分分隔开。这个东东,其实并不是必要的,这个不必要是指从发明者的设计角度来说的,现在,你不得不接受,所以它在使用它是必须的。
2)authority
在“://”之后,就是authority部分,通常就是主机名加端口号的形式,主机名可以是ip或者域名的形式,必须要有。但是端口号可以省略,会有一个默认值,你知道这个默认值是啥么?
3)path
有了协议、主机名、端口号,再加上这个path,就可以访问互联网上的资源了。而这个path,就对应了我们最开始所说的那个实践中拼接URL字符串的规范的原因了。
URI里的path采用了类似文件系统的“目录”、“路径”的表示方式,因为早起的互联网上的计算机多是UNIX系统,所以就采用了UNIX的“/”风格。它与“://”后面的部分是一致的。这里要尤其注意的是,URI的path部分必须与“/”开始,也就是必须包含“/”,“/”是path的部分,不是authority的部分。这回你理解了为啥会这样制定开发规范了吧。
我们再来看个例子:
file:///D:/baidu_download/file
这是我编的啊~~怎么有三个“/”?这是file协议的特例,它中间省略了主机名,默认就是localhost。你访问本地的机器,自然可以在该协议下省略主机名,但是在互联网上的可绝对不行噢。
五、HTTP状态码:妈妈,他骗我!
这是本篇的最后一节了,我们来聊聊状态码。状态码是响应头中最为重要的一个概念,在RFC1945中就定义了一些,当然只有十来个,到了2616,随着头字段的扩展,状态码也变得多了起来,大概有四十多个,那~~我们需要记住所有的状态码么?甚至于那些在实践中几乎用不到的那些?显然,没必要,咱们只记住一些核心状态码的应用就好了,不是还有Reason呢嘛?如果你记不住数字含义,Reason还可以起到一定的辅助作用。
状态码有三十多个,但是在实际的开发应用中,真正最常见的,差不多就是200、500还有301、302、404这几个。甚至于刚入门的同学们只知道200和500,再甚至于服务器对一些场景响应的状态码,不管什么原因,直接抛个500就完事了。
状态码的核心作用,是告知客户端服务器对该次请求的处理结果,是表达服务器对数据的处理状态,客户端可以依据状态码来进行后续的处理。
状态码很多,但是好在它是有分类的。我们接下来就先来了解一下状态码的分类。
RFC标准把状态码分为了五类,从100到599,别问为啥,问就是标准规定的。这五类的具体含义,大概是这样的:
- 1xx:提示信息,表示请求处理的中间状态,还需要后续的操作。
- 2xx:成功,报文已经收到并被正确处理。
- 3xx:重定向,资源位置发生了变动,需要客户端重新发送请求。
- 4xx:客户端错误,请求报文有问题,服务器无法处理。
- 5xx:服务器错误,服务器在处理请求时内部发生了问题。
总共就这五种,但是其实在客户端与服务器的请求-应答的模型中,正确的理解这些状态码并不仅仅是单方的责任,是服务器和客户端都必须要达成一致的理解的。
客户端作为请求的发起方,在获取响应报文后,需要正确的理解状态码,才能知道后续如何操作,是发送新的请求还是抛出一个错误?是获取本地缓存资源还是拉去服务器数据?等等等等,都需要双方正确理解状态码的含义。
而服务器作为请求的接收方,也要正确的理解运用状态码,选择正确的状态码返回给客户端,指示客户端下一步要如何操作,特别是在出错的时候,返回正确的状态码就显得更为重要。
好啦,我们现在对状态码有了一个整体的认识和了解,接下来,我们来具体学习一下在实践中比较容易接触到的状态码,其它没有聊到的,大家可以自行查阅RFC标准或者来看我之前的翻译也可以,完全翻译自RFC标准。
1)1xx
在100这个分类下面,最常见的就是101了。101的含义是Switching Protocol,也就是选择协议。它需要配合Upgrade字段,要求在HTTP协议的基础上改成其它协议继续通信,比如WebSocket。如果服务器同意变更,就会发送101状态码,之后的数据传输就不会再使用HTTP协议了。
2)2xx
2xx这个分类表示请求成功,在1945的时候,只有200(OK)、201(Created)、202(Accept)和204(No Content)这四个,到了2616则又扩展了三个:203(Non-Authoritative Information)、205(Reset Content)、206(Partial Content),所以在HTTP/1.1的标准中,成功的状态码一共有七个。
我们挑两个重要的来看看。
首先就是200,200这个状态码表示服务器成功收到了客户端发送的消息,并且成功处理了客户端的请求,并且成功的返回了客户端想要的数据。反正就是一切顺利~
其次是204,它与200的含义基本上是一模一样的,也是一切顺利,没啥问题,但是与200的区别是,204不会返回body。对于服务器来说,正确的区分200和204是很有必要的。
最后是206,这个东西很重要,大家要额外注意一下,206是部分传输,是HTTP分块传输或者断点续传的基础,当客户端发送“范围请求”、要求获取整体资源的部分数据时会出现。它与200的内在含义并无区别,只是服务器会返回部分数据。206的状态码通常会伴随着“Content-Range”头字段,表示响应报文里数据的具体范围。
3)3xx
这个类别含义就是重定向,以前的资源找不到了,你得向新的地址重新发送请求。3xx的状态码一共也有八个,但是其中废弃了一个306,所以只剩下7个了,其中重要的如301、302、304。不那么重要的比如300——多种选择(Multiple Choices)、303——参见其他(See Other)、305——使用代理(Use Proxy)、307——临时重定向(Temporary Redirect)。
我们来着重了解下重要的三个3xx状态码。
首先是301,永久重定向,意味着你访问的资源已经不存在了,需要通过Location头字段指明后续要跳转到的URI。
与301相似的是302,临时重定向,它与301最终的结果是一样的,都会跳转到新的Location地址,但是两者在语义和使用场景上有着核心的区别。301是永久,302是临时。比如你网站的资源位置发生了永久性的变动,或者再比如从http升级到了https协议,但是你不能直接删除原来的地址,因为老客户还要用,为了可以直接使用新的https,你需要把请求到旧的地址的请求重定向到新的地址。而302,则意味着资源的位置只是临时变化,比如午夜的紧急维护,这个时候有睡不着的客户点进来之后,就可以让他临时跳转到一个新的维护页面,但是到了第二天维护好了,这个临时重定向也就不需要了。
304,这个东东稍微有点意思,它的含义是Not Modified,没有修改。它主要用于If-Modified-Since等条件请求,用于缓存控制。它通常不具有任何跳转含义,但是可以理解为跳转到了缓存,也即缓存重定向。
4)4xx
这个分类,意味着客户端错误,整个4xx下的状态码有18个之多,我们挑一些来稍微详细的聊一聊。
400:Bad Request,这个意味着客户端错误,但是也仅仅是客户端错误,服务器无法理解这次请求,但是具体是什么错误,并没有详细准确的说明,只是告诉你错了,但是却不告诉你哪里错了,实在是让人恼火,所以,在实践应用中,尽可能不要出现400这个状态码。
403:Forbidden,实际上不是客户端的请求出错,而是服务器拒绝了对该资源的访问。拒绝的原因可能有很多,完全由服务器控制,如果服务器友善一点,可能会给你返回原因,但是实践中大多数都懒得告诉你。
404:Not Found,它的本意是资源不存在,无法在服务器上找到,所以无法返回给客户端。但是,在实践中,不管啥错误都可能会被服务器返回个404,挺烦人的,一点不友好。
你发现了没有,其实大多数场景下的决定权都在服务器手里,客户端只能对之作出响应,要求,建议,而无法左右。好啦,稍稍跑了下题,我们继续。剩下的都比较好理解,Reason都把语义说的很清楚。
比如:
405:Method Not Allowed,请求行中指定的方法不允许用于请求URI中标识的资源。响应必须包含一个Allow头字段,其中包含对请求资源有效的方法列表。
406:Not Acceptable,资源无法满足客户端的条件,比如要请求一个中文的内容,但是目前只有英文的。
408:Request Timeout,请求超时,很好理解吧。
5)5xx
服务器错误意味着客户端的请求是没问题的,但是服务器在处理时内部发生了错误,无法返回应有的响应数据。在2616中,500的状态码有6个,我们来看下:
500:Internal Server Error,服务器错误,跟400类似,也是一个通用的服务器错误状态码,没有明确表明到底服务器到底出了什么错,也是我们最常见的服务器错误状态码。但是与400不同的是,500的状态码可以很有效的避免服务器泄漏可能的隐私信息,所以在实践中几乎只要报错就是500就完事了。
501:Not Implemented,大概意思就是客户端请求的功能还不支持,敬请期待,期待到啥时候,那不知道。
502:Bad Gateway,通常是服务器作为网关或者代理时出现的错误,表示服务器没问题,但是访问后端服务出了问题,具体啥问题不告诉你。
503:Service Unavailable,表示服务器当前正忙,暂时无法响应服务,503是一个临时的状态,很可能过一会儿就不忙了,可以通过Retry-After字段可以在多久后再次尝试。
6)DIY
最后,你猜599这个状态码是什么含义么?嗯……这个不能留做问题,我得告诉你答案,答案就是,这是我自己定义的状态码,Reason叫做Zaking Received。换句话说,HTTP协议是可扩展的,所以状态码也是可以扩展的,只要你自己定义的状态码没有和标准规定的冲突,那么你就可以使用,当然,还有个前提,我们之前聊方法的时候也说过,就是必须客户端和服务器都可以理解,要做好约定。
然后,最新的RFC9110其实新增了点状态码。
六、一个POST请求的小栗子
我们先来写个客户端的代码,就是个index.html,代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Zaking HTTP Demo -05</title> </head> <body> <button id="btn">点我发起请求</button> </body> <script> const btnDom = document.getElementById("btn"); function requestFn() { const xhr = new XMLHttpRequest(); const url = "http://www.zaking.com:8090"; xhr.open("POST", url); xhr.onreadystatechange = function () { if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { console.log(xhr); console.log(xhr.responseText); } }; xhr.send(); } btnDom.addEventListener("click", requestFn); </script> </html>
很简单,就是一个原生的ajax请求。然后我们需要提供了一个clinet.js服务,通过读取index.html返回到页面上:
const http = require("http"); const fs = require("fs"); const path = require("path"); const hostname = "127.0.0.1"; const port = 9000; const server = http.createServer((req, res) => { let sourceCode = fs.readFileSync( path.resolve(__dirname, "./index.html"), "utf8" ); res.end(sourceCode); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
也特别简单,就是读一下文件,返回文件。最后,我们稍稍修改下我们之前用过的那个server.js:
const http = require("http"); const hostname = "127.0.0.1"; const port = 8090; const server = http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.end("Hello Zaking World!This is Node"); }); server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`); });
因为两个端口号,所以会有跨域的问题,我们除了加了一个CORS的头,其他的跟上一小节的那个小小栗子的代码没有任何的区别。
当然,其实这块也可以通过一个服务来解决问题,通过path来返回不同的内容。一般情况下“/”这个path下就是我们的前端页面的静态文件,然后会通过一些特定的path,比如/api来作为接口的路径,这样就没有跨域的问题,只需要一个服务。只不过我们这里选择了另外一种方式作为示例,不重要哈。
然后,见证奇迹的时刻,我们需要打开两个命令行工具,分别输入这样的命令:
node ./05/server.js node ./05/client.js
当然,这个具体的命令可能跟你的文件所在位置的不同稍微有点不同,不重要,理解了就好了。然后,打开http://www.zaking.com:9000/这个地址,你会看到这样的界面:
然后,别忘了打开控制台的Network,点击发起请求的按钮:
这就是这次请求的所有内容,我们不关注它的字段都是干啥的哈。在Response中,我们也可以拿到正常返回的字符串:
或者,我们之前的index.html里也打印了:
所以,无论从什么角度来说,这次实验的请求都是完美成功!
但是,不知道到了这里你有没有什么疑问?我有~~~我server的代码明明什么都没改啊?怎么就POST请求也没啥问题呢。那是不是意味着我用啥请求都行?嗯……我们试试呗,反正代码都在这呢,随便我们怎么玩,我们把requestFn里的POST改成PUT:
xhr.open("PUT", url);
然后重新启动下客户端的服务,再点下发送请求:
报错了?额。。怪我怪我,这个报错跟我们的核心内容没关系,还是跨域的问题,我们再给server.js加一行代码:
const server = http.createServer((req, res) => { res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "*"); res.end("Hello Zaking World!This is Node"); });
再启动下server.js,然后点下按钮:
没问题~~~,再试下DELETE?竟然还是一样。那再试下HEAD、OPTIONS?你会发现HEAD没有返回body,OPTIONS跟前面几个的返回一样。最后既然都到这里了,我们就都试一下,也不差这两个了试下TRACE和CONNECT。我们可以看到TRACE方法报错了:
还记得我们之前说过的TRACE方法会暴露服务器数据,不安全,所以注意这里:是浏览器直接报错了,那这个请求发没发到服务器呢?我们在sever.js中打印一下:
const server = http.createServer((req, res) => { console.log("received"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "*"); res.end("Hello Zaking World!This is Node"); });
然后再重复下之前的步骤。我们发现服务器压根没有打印,所以这个请求直接被浏览器拦截,没有到达服务器。最后,试试CONNECT。竟然也报错了,跟TRACE的报错一样。好吧。
经过以上的测试,我们发现TRACE和CONNECT方法在浏览器中竟然都不支持,OPTIONS、GET、POST、PUT、DELETE等方法如果服务器没有去处理这些方法,从感知上来说,竟然没有区别。HEAD则按照规范要求,没有返回body。
那我们说TRACE方法,可能会造成问题,所以不被支持,这个还可以理解,但是CONNECT方法为啥也不支持啊?嗯……因为我们试验的场景是浏览器环境下的http请求,如果是服务器对服务器发起请求,是否就可以使用CONNECT了呢?再有,GET请求是不能发送body的,但是在服务器作为发起方的时候是否就可以发送body了呢?
我们先来看看GET能不能发body,我们创建一个request-server.js,代码如下,也是从官网复制下来的:
const http = require("http"); const postData = JSON.stringify({ msg: "Hello World!", }); const options = { hostname: "www.zaking.com", port: 8090, path: "/", method: "GET", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(postData), }, }; const req = http.request(options, (res) => { console.log(`STATUS: ${res.statusCode}`); console.log(`HEADERS: ${JSON.stringify(res.headers)}`); res.setEncoding("utf8"); res.on("data", (chunk) => { console.log(`BODY: ${chunk}`); }); res.on("end", () => { console.log("No more data in response."); }); }); req.on("error", (e) => { console.error(`problem with request: ${e.message}`); }); // Write data to request body req.write(postData); req.end();
啥也没动哈,就是官网的代码。然后,我们直接执行这个服务,怎么启动就不多说了。结果是这样的:
是可以的对吧?
CONNECT有点复杂,我也不会了,就这样吧。哈哈哈哈。我猜测CONNECT和TRACE其实都可以在服务器端得到支持,只是我不知道怎么搞这个例子。
快完事了~我们在来看看返回一些状态码是什么样的。就先看看500吧。回到之前的代码,我们稍微修改下server.js:
const server = http.createServer((req, res) => { console.log("received"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "*"); res.statusCode = 500; res.end("Hello Zaking World!This is Node"); });
然后把index.html中的请求方法改成GET。我们发现一个问题:
返回了500,没问题,但是~
结果还是返回到浏览器了,但是控制台却没有打印,只有个500的错误:
这说明数据还是从服务器返回到浏览器了,但是因为浏览器发现是500,就没有把body再传给javascript引擎去处理了。当然,还有其他的各种方法,这里就不再多说,大家有兴趣可以自己用代码试一下。还有一些特殊的状态码要配合头字段使用的,后面我还会详细的聊。
我们再试一个有趣的我之前说过的,叫做599的状态码。代码呢这样改:
const server = http.createServer((req, res) => { console.log("received"); res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Methods", "*"); res.statusCode = 599; res.statusMessage = "Zaking Not Know"; res.end("Hello Zaking World!This is Node"); });
结果是这样的:
是不是,很有趣?
终于~这一章结束啦~
七、总结
这一章我们聊的内容不少,需要大家好好消化一下,是我们后面学习的重要的基础。我们稍微回顾一下我们都学了什么。首先我们先学了下HTTP的优缺点,让大家对HTTP有一点稍微详细的认识。
再往后,我们学了下HTTP的方法,HTTP的方法不少,但是实际上我们在实际使用中的就那么几个,大家要注重学习,另外要额外关注下安全和幂等的含义。
然后,我们还简单学了下URI,这部分其实很重要,但是我之前写过了,所以不想再重复的写,大家要深入的了解可以再去看看我之前写的文章。
最后,理论的部分就只剩下状态码了,状态码很多,有40多个。我简单的讲解了其中一些常见的状态码,还有一部分,会在后续的学习中更加深入的去学习。
在理论结束之后,我们还写了个小例子,这个例子测试了浏览器发起的各种请求方法。还试了一下500状态码的样子。
最后,按照惯例,留点小问题给大家:
1、其实我在第四节所画出来的URI是简写形式,你知道它的完全体是什么样的么?为什么完全体很少使用呢?
2、URI后的query部分可以传输Object或者Array类型的数据么?或者说,GET方法可以传Object或者Array类型的数据么?
3、服务器发起的GET请求可以传送body么?浏览器呢?
4、HTTP的状态码一定符合它的描述么?
5、为什么在状态码的那一小节,我会起一个那样的标题呢?
本篇所有内容,只是限定在RFC2616,其实说起来有点过时了,但是重要的内容确实也还是这些,后续可能会因为讲解深入再涉及到,也可能会涉及不到,关于方法和状态码的最新的额外的内容,大家其实完全可以自行学习了。
填坑
- TCP是有状态的协议,他需要通过状态来维护通信双方的连接状态,可以理解为连接双方的一个标志,用来记录连接建立到了什么阶段,后续的情况如何处理等等。详情可以参考之前聊过的三次握手和四次挥手。顺带说一句,TCP是有连接有状态的,而UDP是无连接无状态的。
- HTTP关于灵活可扩展延伸的特点有:比如,压缩,国际化,缓存,身份认证也就是HTTPS啦,等等,等等。
- 最后,其实本篇留下了个connect的那个坑,但是那个坑我觉得不太重要,我又确实不会,所以就不填了。????????