重学HTTP协议
URL 基本格式
1 | scheme://host[:port]/[abs_path] |
一个 URL 就已经包含了寻找一个资源足够的信息。其协议非常简单,即用一个 URL 表达资源目的,如果需要额外的信息就通过 http 协议规定的报文格式的体带上。
port 为空则默认表示 80 端口,abs_path 不写,则作为请求 URL 时必须在 host 后面带一个 /
(通常浏览器会帮我们带上)
http 协议是简单的,但是他又是灵活的,因为所有功能、数据,都能通过一个 url 和一个 http 请求头来完成。传输的内容类型可以通过报文头中的 Content-Type
实现。 http 协议是无状态的协议,所以服务端不保存任何上一次的信息,每次服务端的接收请求和响应都是新的。
请求格式
http 请求有 3 部分构成:
- 请求行
- http 消息头
- http 请求正文(可选的)
请求行,为: method 请求 URL http 版本, 然后加一个 CRLF(回车和换行)。
例如:
1 | GET /page/maintainable-nodejs/getting-started-with-eslint.html HTTP/1.1 |
请求方法,有很多: GET, POST, PUT, DELETE, HEAD, TRACE, CONNECT, OPTIONS.
其中,POST 是在目标 URL 资源上附加新的数据(有添加的意思);PUT 表示对标示的资源存储一个东西,有更新的意思;TRACE 是请求服务器回显收到的消息,用于测试诊断;OPTIONS 查询服务器性能或查询相关资源的选项;HEAD 用于获取某个资源的响应报头(所以一般对 http 响应内容不感兴趣);CONNECT 保留将来备用。 GET 用来获取资源信息,所以应该是安全和幂等的。
GET 与 POST 的区别
GET 用来获取信息,post 用来更新、添加信息;
GET 可以通过 URL 后携带 queryString 的方式提交数据。但受浏览器 URL 长度的限制,而 POST 在 http 报文体中携带,则不受限制。
提交隐私数据时,POST 更安全。因为 GET 提交浏览器可能缓存 URL,易被别人通过历史记录找到;另外 GET 更容易造成 CSRF 跨站请求伪造漏洞,因为黑客在浏览器里向另一个网址发起 get 请求更容易且更隐蔽。
请求消息头
介绍几个容易混淆的
- Accept:表示客户端希望接受的资源类型,例如 image/gif
- Accept-Language: 客户端希望接受的语言,如 zh-ch
- Accept-charset: 客户端希望接受的字符集。例如 gb2312 或者 utf-8
- Accept-Encoding: 客户端希望接收的内容编码算法类型。如 gzip,deflate
- Authorization: 当访问一个资源收到 401 未授权,则可以发送带这个头的请求,要求服务器对其进行认证。
响应格式
- 状态行
- 响应报头
- 响应正文
例如: HTTP/1.1 200 OK
再加一个 CRLF 回车换行。
状态码有 3 位数字,组成,其含义如下图:
服务端解析 HTTP 请求
服务端的 http webserver 本质上是一个网络程序。这个网络程序能够接受 TCP 的请求,并且将 TCP 的包内容按照 http 协议的约定格式解析为 http 格式的数据结构。所以其接收到每个 TCP 请求后必然会经历 http-decode
的过程。其给客户端发送 TCP 响应内容的时候,必然会经历 http-encode
的过程。 对于 http 的 webserver 来说,每发送完一个 http 响应,则主动关闭本 socket 连接。(当然,http2 协议要求实现链路复用,那么 webserver 就不会关闭这个 TCP 连接)
在使用 Netty 这类的底层异步 IO 的网络通信框架时,你启动一个 HTTP Server,就是启动一个 Server 端的网络程序,只是需要自己指定上对请求的 http-decode 处理器即可。
而在 Node.js 中,创建一个 http server,它自身就已经完成了对 TCP 请求的 Http Decode,你的 requestListener 回调函数执行时,已经可以收到封装好的 InComingMessage 数据结构,这个数据结构就包含了 Http-Decode 后的相关信息。 而且使用 Netty 你也需要自己判断 http-decode 有没有成功,而 Node.js 中的 http 模块创建 server 后,你的回调函数拿到的 request 必然是已经成功了的请求对象。
一个创建 HTTP Server 的例子
1 | let http = require("http"); |
对于返回状态码的情形,其实在 Node.js 中你只需要设置好状态码,则 http 响应的状态行会自动被设置。比如上方我们设置了 405 状态码,则客户端收到的响应状态行就是:
1 | HTTP/1.1 405 Method Not Allowed |
也就是说,状态码的描述,以及当前 server 端支持的 http 协议版本,都是 Node.js 的 httpserver 模块内置给你处理了。(就如同该模块可以把 TCP 请求自动解析出 http 相应的协议信息一样)。
设置响应 keep-alive
Node.js 的 http server,默认就是 keep-alive 的,所以即使你不设置这个头,客户端也会收到 keep-alive
的头。如果你非要设置,则可以这样干:
1 | if (req.headers.connection === "keep-alive") { |
字符解码
如果使用 Netty 之类的网络程序,对于解析后的 http 报文,你拿到之后,比如从中取出 request URL,你依然需要对他进行 URL 解码。(比如客户端浏览器请求时 URL 后面有特殊字符(如回车换行空格之类的)或中文,则浏览器肯定进行 URLEncode 处理)。 然而你又不知道客户端用哪种字符集的 URL 编码方式将特殊字符进行的编码,所以只能从 UTF8 尝试,出现异常后再尝试另外一种字符集。是的,用 Netty 启动一个 HTTP 服务器时就是在做这样的事情。
另外你需要做的一件事情就是,客户端请求资源时一般采用 /
斜线的写法作为路径分隔符。而你要将这个 URI 转换为自己服务端操作系统上的本地路径,则要根据系统对分隔符进行转换。
而 Node.js 的 Http server 创建后,server 内部把这个 decode 的事情给做了。 你通过 req.url 拿到这个请求 URL 之后,就是 http 报文状态行里的那个 path。 当然转换成本地操作系统分隔符的事情,你还是要自己做的。
HTTP 响应
Node.js 中做 http 响应时,只需要 res.end()即可,但是要注意在 Node 中即使你调用了该函数,程序流程还是会往下执行的,因为 res.end 表示将缓冲区内容 flush 到客户端,但不代表你代码执行流停止了。所以一般要在此时执行 return 从而退出程序函数执行流。
在 Node.js 中你设置响应内容时,如果是字符串,则默认是 utf-8 编码后再发送给客户端的。server 已经帮你做了这件事情,如果是用 Netty,你需要自己将字符串转换为 Buffer,而转换 Buffer 时你需要指定字符编码(在 node 中也是的,只是 node.js 的 http 允许你直接传递一个字符串给 res.end,其实 server 内部给你进行了 utf8 字符集编码)