浅谈HTTP协议与前后端实现
HTTP协议概要
HTTP协议是计算机网络里的应用层协议,在网络层使用的是具有稳定传输的TCP协议,建立连接时依照TCP协议的三次握手,四次挥手进行连接和断开。本博客主要讲解顶层相关的,程序员比较关心的顶层编程实现,因此不细讲HTTP协议的底层实现。
HTTP协议的方法有很多种,例如POST,GET,PUT,DELETE等。这里本博客仅对两种常用的方法POST和GET进行分享。HTTP报文的结构主要有两个部分,一个是报文头,一个是报文体。
HTTP报文头
HTTP报文的报文头一般负责和通信对方协商一些信息,包括编码格式,连接方式等。一般情况下,请求报文头和回应报文头的字段略有不同。下面列举一些常见的报文头字段:
请求报文头
以访问百度发出的GET请求为例,浏览器发出的请求报文头的字段值如上图所示,其中常用字段的意义分别表示为:
Accept
:表示请求方可以接收的报文格式Accept_Encoding
:表示请求方的编码格式Accept_Language
:表示请求方使用的语言Connection
:表示连接方式,一般情况下默认开启Keep-Alive
,表示客户端和服务端保持长连接,即使本次HTTP请求结束后仍保持TCP连接,下一次请求时不必再进行TCP连接建立,直至客户端或者服务端程序关闭或者主动断开连接时,才断开TCP连接。如果该字段值为Closed
,则表示在本次请求结束后,服务端和客户端的连接将断开Cookie
:Cookie字段,保存一些服务端返回给客户端的浏览凭证,一般用于服务端验证客户端身份Host
:请求访问的主机域名或IP地址Sec-Fetch-xxx
:这些字段表示跨站请求模式以及一些相关参数User-Agent
:表示客户端使用的设备或浏览器,服务端通过过滤一些User-Agent非法的请求来防爬虫脚本(虽然User-Agent基本可以伪造,该防御手段已经过时)
以上展示的是GET请求的报文头,对于POST请求,一般还多出以下字段:
Content-Type
:报文体的内容格式,POST请求报文体一般有JSON,urlencoded,form-data格式等,其中JSON即字典格式,urlencoded即和GET请求的url格式一样传递参数,form-data是一种特殊的格式,参数之间以特定的字符串进行分隔,此时该字段会多一个子字段boundary以表示分隔的字符串Content-Length
:报文体的长度,供接收端检验报文以及读取报文。
这里已经可以体会到,GET和POST方法传递参数的模式有一定区别,GET请求通过在url中传递请求的参数,POST请求通过报文体传输请求的参数。一般情况下,url的长度不宜过长,因此GET请求的参数长度有一定限制。而POST请求在理论上参数可以无限大,但一般由于服务端设置不同,因此有不同的报文大小限制,但相较于GET请求POST请求可发送的参数大小还是大得多的。
应答报文头
应答报文头的字段一般和请求报文头很不相同,因为应答是服务端对客户端的应答,一般不再是传递一些沟通参数,而是传递一些结果参数,即对请求处理后返回的结果的一些参数。一些常用字段有:
Content-Length
:返回报文体长度Location
:重定向地址,一般要求客户端去向Location地址发送请求Server
:服务端使用的服务器框架Set-Cookie
:服务端向客户端分发的Cookie,一般有Cookie值,使用域,使用路径等Access-Control-Allow-Credentials
:要求客户端是否携带证书来访问Access-Control-Allow-Origin
:是否允许跨域,如果不允许,则在该字段值填写指定的前端网站的域名地址,若允许则可使用‘*’
使用HTTP协议通信
前端实现HTTP请求
这里以javascript为例,一般前端浏览器都有内置类XMLHttpRequest
用来实现HTTP请求的发送和处理,通过实例化该类的对象,就能够发送HTTP请求并对response进行处理。
发送请求需要实例化XMLHttpRequest
对象
var http = new XMLHttpRequest()
http.withCredentials = true
其中对象里有一个属性为withCredentials
表示是否携带Cookie,为true时则后续请求会自动携带该站点的相关Cookie,不需要程序员手动管控Cookie
发送请求:
// GET
http.open("GET", url, true)
http.send()
// POST
http.open("POST", url, true)
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded")
http.send(data)
这里的POST请求需要注意,Content-type
字段值根据data格式进行相应的设置。
open函数有三个参数,第一个参数代表请求方法,第二个参数代表请求的url,第三个参数为是否异步。若为true则为异步,即不阻塞后续程序;若为false则为同步,即请求过程阻塞整个程序,知道response到来才继续执行。一般情况下异步较为多,同步会导致页面阻塞,用户体验极差。
设置处理回调函数
http.onreadystatechange = function(data) {
if (http.readyStatus == 4 && http.status == 200) {
// OK
} else if (http.readyState == 4) {
// Fail
}
}
这里readyStatus有0-4五种状态,分别为:
- 0:初始化,XMLHttpRequest对象还没有完成初始化
- 1:载入,XMLHttpRequest对象开始发送请求
- 2:载入完成,XMLHttpRequest对象的请求发送完成
- 3:解析,XMLHttpRequest对象开始读取服务器的响应
- 4:完成,XMLHttpRequest对象读取服务器响应结束
status为请求返回的服务器回应码,200为OK,500为Server Error,404为NOT Found等
一些坑:
- 一般来讲,一个XMLHttpRequest对象可以发送若干个请求,也可以同时使用多个XMLHttpRequest发送多个请求,但是一个XMLHttpRequest在同一时刻(较短的时间内)只能发送一个请求,因此如果要连续同时发送多个请求,建议使用多个XMLHttpRequest对象进行发送。注意进行对象的销毁避免缓存占用过大
- 对于一些浏览器的跨域要求非常严格,对于Chrome浏览器要求报文返回头必须要有Access-Control-Allow-Credentials和Access-Control-Allow-Origin字段,否则报文会被浏览器拦截。对于Safari浏览器Access-Control-Allow-Origin字段必须限制跨域,否则可能会被Safari跨域设置所拦截
- 关于Cookie值的操作,前端脚本最好不要进行Cookie值操作,否则会有一系列不安全的隐患,Chrome浏览器也禁止了这一点。
后端构建HTTP服务器
HTTP服务器即处理HTTP请求的服务程序。这里的后端以nodejs为例,使用express框架实现http服务器搭建。
express框架+bodyParser插件搭建http服务器非常简洁,express框架为程序员准备好了一套http处理流程,程序员只需要设定相关回调函数即可,而bodyParser帮助程序员做好了http报文的解析,程序员可以直接拿到请求参数。构建http服务器:
const app = express()
app.use(bodyParser.json({limit:'100mb'}));
app.use(bodyParser.urlencoded({ limit:'100mb', extended: true }));
app.use(function (req, res, next) {
res.setTimeout(60*1000, function () {
console.log("Request has timed out.");
return res.status(408).send("请求超时")
});
next();
});
app是express实例,也是服务器实例,前两行使用bodyParser设定了请求参数的解析格式,这里设定了JSON和urlencoded参数的解析,当发送的请求为JSON格式或者urlencoded格式时,程序员不再需要进行参数解析,就可以直接获取各个参数。
第三行设定了服务器超时相应,这里设置的超时时间为60秒。
服务器监听端口:
app.listen(port, function() {
console.log('Listen at %d', port)
})
处理get请求:
app.get('/', function(req, res) {
let param = req.query
let path = req.path
res.statusCode = 200
res.end(message)
}
由于设置了bodyParser,因此这里可以直接调用req.query就可以获取请求参数,即将参数转化为JSON格式对象。
处理post请求:
app.post('/', function(req, res) {
let param = req.body
let path = req.path
res.statusCode = 200
res.end(message)
})
同理,调用req.body即可获取请求参数。
另外,可以使用app.all对所有请求进行处理:
app.all('*', function (req, res) {
switch (req.method) {
case 'POST':
switch (req.path) {
case '/':
break;
default:
break;
}
break;
case 'GET':
switch (req.path) {
case '/':
break;
default:
break;
}
break;
default:
break;
}
})
这样的写法在扩展时可以对一些代码进行复用,比如res的处理,拓展比较方便,但是在可读性上会稍差一些。
一些坑:
- 对于POST请求,最好设置最大接收报文体大小,否则一些较大的报文可能会被服务器拦截导致接收数据错误。
- 对于文件的传输,最好使用文件流的方式进行传输,可以结合fs模块的writeStream进行实现。
实战例子:实现北航云盘转储服务
- 需求:实时向北航云盘保存数据以及下载数据
- 功能:
- 向固定账户的北航云盘上传文件
- 下载固定账户的某个文件
- 实时保存登录状态
- 使用框架:nodejs+express+https模块