previously:html
TCP是HTTP的基石,对tcp还不是灰常清楚的小伙伴能够看看个人这篇NodeJS和TCP:一本通node
本文主要用于我的知识梳理,可能篇幅较长,So专门在开头copy了一份目录以便配合右下方链接点击分段阅读(づ ̄ 3 ̄)づgit
首先,HTTP
是基于 TCP
协议的,只有当tcp链接顺利创建时,浏览器客户端才能向服务器发送http请求。(详见TCP三次握手)github
当TCP
让让一台pc端对端的链接上另外一台pc后,两台机器之间能够互通数据,但这个数据并无通过什么额外的加工,是纯粹的数据,即用户输入什么数据,服务器就会拿到什么数据。web
而 HTTP
有些许不同, 一个http请求会将用户的输入通过浏览器包装后再发送给服务端,而包装后的数据便是咱们说的 http请求报文
。对应的,服务器要向浏览器回复响应,也须要通过一层包装,包装成 http响应报文
再响应给客户端。npm
从传输层面上来说,http仅仅是tcp的一项子集,一种再封装。编程
HTTP报文格式 是对 http协议最直观的阐释,也是咱们学习http协议最有效率的手段。json
HTTP报文主要分文两大类,请求报文
和 响应报文
,请求报文和响应报文又都分为行
、 头
和 体
三部分,而且头和体这两部分之间有 空行
隔开。跨域
如下图片出自于Android网络编程随想录(2) 浏览器
请求行分为如下三部分,每一个部分之间用空格隔开
请求头和请求行不同,它是多行的,每一行都是一组键值对,键和值之间用:
和空格
隔开。
Connection:keep-alive
Content-
开头的正经的,用户想要传给服务器的数据
如下图片出自于Android网络编程随想录(2)
同请求头
同请求体
客户端封装请求报文时会将url中的hash给去掉(query是会保留的)
GET 获取资源
POST 想服务器端发送数据,传输实体主体
PUT 传输文件 , RESTful中是更新修改操做
HEAD 获取报文首部
DELETE 删除文件
OPTIONS 询问支持的方法 ,试探方法,好比跨域,会先询问服务端可否跨域
TRACE 追踪路径
当提交的表单只包含一条数据时,且表单类型为默认时,请求报文长这样
若是是多条数据,会用空行隔开
但若是是multipart/form-data
编码时,请求体中多端数据间则是用特殊的分隔符来隔开的
即便只有一段数据也会用特殊的分隔符包裹住
多段数据时
状态码主要分为五大类
console.log(req.method); //请求方法
console.log(req.url); //url地址
console.log(req.httpVersion); //http协议版本
console.log(req.headers); //请求头
复制代码
// 获取请求体
req.on('data',function(data){
console.log(data.toString());
})
复制代码
let http = require('http');
let server = http.createServer();
server.on('request',function(req,res){
res.end('ok');
});
server.listen(8080);
复制代码
你能够能够这样简写
let http = require('http');
let server = http.createServer(function(req,res){
res.end('ok');
});
server.listen(8080);
复制代码
let http = require('http');
let options = {
host:'localhost'
,port:8080
,method:'POST'
,headers:{
'Content-Type':'application/x-www-form-urlencoded'
// ,'Content-Length':15 //通常来讲这个数值会自动计算
}
}
let req = http.request(options);
req.write('id=999');
// 只有调用end才会真正向服务器发送请求
req.end();
// 当客户端收到服务器响应的时候触发
req.on('response',function(res){ //只有一个参数
console.log(res.statusCode);
console.log(res.headers);
let result = [];
res.on('data',function(data){
result.push(data);
})
res.on('end',function(data){
let str = Buffer.concat(result);
console.log(str.toString());
})
})
复制代码
还能够把request()
和on('response')
合在一块儿写,不过此时没法像服务端主动发送头之外的数据(只有调用http.request(opt)
才会返回req,才能调用write()
)。
http.get(options,function(res){
...
res.on('data',function(chunk){
...
});
res.on('end',function(){
...
})
})
复制代码
end后没法继续写入(可写流规定)
res.write()
res.end()
<<<
Erorr::write after end!
复制代码
设置状态码之后 会自动补全状态码文本描述
res.statusCode = 200; //默认
复制代码
咱们不只能够设置,也能够删除一个准备发送给客户端的响应头
res.setHeader('Content-Type','text/plain');
res.setHeader('name','ahhh');
res.removeHeader('name'); //删除一个准备设置的头
复制代码
writeHead
相较于setHead
能同时设置多个头,而且连状态码一块儿设置。但它和setHeader最大的不一样在于,writeHeader一旦调用会马上发送。
console.log(res.headersSent) //false
res.writeHead(200,{'Content-Type':'text/plain'}); //writeHead设置完后不能再调用res.setHeader,由于调用writeHead会直接把头发送出去
// res.setHeader('name','zfpx'); //Can't set headers after they are sent. console.log(res.headersSent) //true 复制代码
而setHeader
设置的头是在调用write方法以后才会发送,另外须要注意的一点是头必须在write以前设置。
console.log('--- --- ---')
console.log(res.headersSent); //false
res.setHeader('name','ahhh');
console.log(res.headersSent) //false
res.write('ok');
console.log(res.headersSent) //true
res.end('end');
console.log(res.headersSent) //true
console.log('--- --- ---')
复制代码
客户端发送请求和服务端回以响应时都须要设置这个Content-Type
头,
对于服务端来讲,它须要拿这个头解析客户端发送过来的实体数据,(纵然很多状况下,请求都没有实体部分,好比get请求)。
let buffers = [];
req.on('data',function(chunk){
buffers.push(chunk);
})
req.on('end',function(){
let content = Buffer.concat(buffers).toString();
if(contentType === 'application/json'){
console.log(JSON.parse(content).name);
}else if(contentType === 'application/x-www-form-urlencoded'){
let queryString = require('querystring');
console.log(queryString.parse(content).name);
}
})
复制代码
实际状况下,若是有请求体(实体数据),可能会很复杂。(前面的请求体部分)
而且服务端响应客户端数据时也须要发给它这么一个头以便客户端解析数据,而这个Content-Type
每每和要返回给客户端的资源文件的后缀名是相关联的,So咱们通常使用一个npm包帮咱们进行转换,
...
let mime = require('mime');
...
res.setHeader('Content-type',mime.getType(filepath)+';charset=utf-8');
复制代码
http服务器相较于tcp服务器其实就多作了一件事,即解析请求头,剩下的请求体部分该on data
仍是同样on data监听便可。
但须要注意的是,data,即请求体是何时 发射
的呢?嗯,是在分离出请求头并解析完毕请求头后发射的。
// http.createServer(function(req,res){})
server.on('request',function(req,res){
//do somtheing like you are doing at tcp
}
复制代码
let server = net.createServer(function(socket){
parser(socket,function(req,res){
server.emit('request',req,res);
});
});
server.listen(3000);
复制代码
这里分离请求报文是指将 请求体 与其它两部分(请求行,请求头)分红两块,怎么分?嗯,前面说过,请求体和请求头之间有一行空行做为分隔,即 \r\n\r\n
或则说 0x0d 0x0a 0x0d 0x0a
。
嗯。。原理就是这么个原理咯,但有一个坑。
socket做为一个双工流,在读取客户端发来的数据时和普通的可读流同样有一个默认读取值,So,这可能致使你要读不少下才摸获得\r\n\r\n
这个分隔符,
而且可能最终读到\r\n\r\n
时还会多读出一些不属于"请求头"部分数据,咱们还须要将这部分多余的属于请求体的数据按回去,以便发射requset
事件时咱们能拿取到完整的 请求体
数据。
嗯。。坑比较多,这里就不献丑贴代码了,有兴趣的小伙伴能够本身去实现如下,有两点须要注意
读取时使用readable暂停模式来读取(以便把多余的数据按回去)
推荐用0x0d 0x0a
这种buffer级别的来判断而不是\r\n
这种字符级别,由于字符可能会致使乱码很差判断,须要处理的状况就更多了。
这里的请求头 包括 请求行与请求头
function parseHeader(head){
let lines = head.split(/\r\n/);
let start = lines.shift();
let lr = start.split(' ');
let method = lr[0];
let url = lr[1];
let httpVersion = lr[2].split('/')[1];
let headers = {};
lines.forEach(line=>{
let col = line.split(': '); //注意这里的空格
headers[col[0]] = col[1];
});
return {url,method,httpVersion,headers};
}
复制代码
仓库:点我点我!
虽然http不想tcp同样能够一直保持长链接,但咱们说过它毕竟是基于tcp的,因此也具备保持链接的能力。
在响应头中每每会包含 Connection:keep-alive
字样的字段,就是让浏览器保持链接不要中断,即便接受完响应信息,这个链接通常也能保持必定的时间(大概,嗯,2min?)
http发送请求时若是包含多个,能够不用等待就能直接发送下一个请求。
Chrome 并发量约为6个,Firefox 4个。
To be Continue...