URL模块
先来看看Node.js中自带的url模块,下面这段代码利用url.parse方法将一个url地址解析为对象, 一个url对象的属性有很多,常用的有query和pathname等
let url = require('url');
//一个url路径
let u = 'http://LiMing:xxxx@www.baidu.com:80/abc/index.html?a=1&b=2#hash';
// 加true 可以将地址中的查询字符串query(a=1&b=2)转化成对象({ a: '1', b: '2' })
let urlObj = url.parse(u,true);
console.log(urlObj);
console.log(urlObj.query); // 查询字符串 { a: '1', b: '2' }
console.log(urlObj.pathname); // 路径
打印结果
Url {
protocol: 'http:',
slashes: true,
auth: 'LiMing:xxxx',
host: 'www.baidu.com:80',
port: '80',
hostname: 'www.baidu.com',
hash: '#hash',
search: '?a=1&b=2',
query: 'a=1&b=2',
pathname: '/abc/index.html',
path: '/abc/index.html?a=1&b=2',
href: 'http://LiMing:xxxx@www.baidu.com:80/abc/index.html?a=1&b=2#hash' }
{ a: '1', b: '2' }
/abc/index.html
http模块(基于TCP)
创建http服务可用如下两种方式
let http = require('http'); //继承了net包,所以和tcp用法基本相同
let server = http.createServer(function(req,res){
console.log('访问');
}).listen(8000);
或
let server = http.createServer();
server.on('request',function(req,res){})
server.listen(8000);
createServer方法其实本质上也是为http.Server对象添加了一个request事件监听,其回调函数会在客户请求到来时触发
上面代码启动了一个监听8000端口的服务器,当浏览器访问localhost:8000时,可以看到控制台打印出‘访问’
但这个服务是无意义的,它不会应答客户,不会做任何事,最后浏览器提示‘无法访问’
req和res分别代表请求对象(可读流)和响应对象(可写流)。其中req是http.IncomingMessage的实例,res是http.ServerResponse的实例。
浏览器访问时,看到Hello
下面写个能在浏览器请求服务时,得看到一个显示Hello的页面
let http = require('http');
let server = http.createServer(function(req,res){
console.log('访问');
res.writeHead(200,{'Content-Type':'text/plain'});
res.write('Hello');
res.end();
}).listen(8000);
-
res.writeHead(statusCode,[heasers]):向请求的客户端发送响应头,该函数在一个请求中最多调用一次,如果不调用,则会自动生成一个响应头
-
res.write(data,[encoding]):即可写流的write方法,向请求的客户端发送相应内容,data是一个buffer或者字符串,如果data是字符串,则需要制定编码方式,默认为utf-8,在res.end调用之前可以多次调用(write方法不能在end之后调用)
-
res.end([data],[encoding]):即可写流的end方法,结束响应,告知客户端所有发送已经结束,当所有要返回的内容发送完毕时,该函数必需被调用一次,两个可选参数与res.write()相同。如果不调用这个函数,客户端将用于处于等待状态。
-
res.setHeader('Content-Type','text/plain')设置响应头,调用write之前才能调用setHeader方法,setHeader可以调用多次,可以添加自己定义的Key-value,比如res.setHeader('name':'wz')
-
res.headerSent 响应头是否写给客户端,调用res.setHeader后,headerSent为false,当调用res.write、res.end、res.writeHead后,headerSent为true
-
res.getHeader('name');取出响应头字段
-
res.removeHeader('name');删除响应头字段
-
res.statusCode = 200; 设置状态码
-
res.sendDate = false; 响应头中不设置日期
注 : 当我们在服务器访问网页时,我们的服务器可能会输出两次“访问”。那是因为大部分浏览器都会在你访问 http://localhost:8000/时尝试读取 http://localhost:8000/favicon.ico )
一些API
let http = require('http');
let server = http.createServer();
// req是请求 是一个可读流
// res是响应 是一个可写流
server.on('request',function(req,res){
let method = req.method; //http请求方法,如GET POST PUT DELETE等
let httpVersion = req.httpVersion; //http协议版本
let url = req.url; //原始的请求路径
let headers = req.headers; //http请求头
console.log('url :' + url);
console.log('headers :');
console.log( headers);
console.log('httpVersion :' + httpVersion);
console.log('method :' + method);
//根据可读流 如果数据 大于64k data事件可能会触发多次
let buffers = [];
req.on('data',function(data){
console.log('data')
buffers.push(data);
});
req.on('end',function(){
console.log(Buffer.concat(buffers).toString());
res.write('hello');
res.end('world');
});
});
// 监听请求的到来
server.on('connection',function(socket){
console.log('建立连接');
});
server.on('close',function(){
console.log('服务端关闭')
})
server.on('error',function(err){
console.log(err);
});
server.listen(8080);
测试:我们可以用curl命令发送http请求
其中
-v是打印出来的详细信息
-d 'a=1' 是添加请求体 a=1
上面代码打印一些常用的req的属性,如请求头headers等, req.headers请求头中的名字都是小写的
请求req的事件
- data : 请求req是可读流,因此可以通过on('data',callback)监听请求发来的数据,注意,如果没有请求体,不会触发data事件,但是仍会触发end事件
- end : 当请求体数据传输完毕时,该事件会被触发,此后不会再有数据
- close:用户当前请求结束时,该事件被触发,不同于end,如果用户强制终止了传输,也是用close
http.server的事件
- request:当客户端请求到来时,该事件被触发,提供两个参数req和res,表示请求和响应信息
- connection:当TCP连接建立时,该事件被触发,提供一个参数socket,是net.Socket的实例
- close:当服务器关闭时,触发事件(注意不是在用户断开连接时)
至此,我们了解了一些http创建服务时,为我们提供的对及事件
由于http是基于tcp的,这里创建tcp服务,来模拟http服务接收请求并响应的过程
模拟的http服务,使用方式如下
let net = require('net'); //tcp是由net模块实现的
let server = net.createServer(function(socket){
//实现一个parser方法,解析出请求和响应,并模拟Http服务触发request事件
parser(socket,function(req,res){
server.emit('request',req,res); //server继承了EventEmitter,所以可以emit事件
});
});
server.on('request',function(req,res){
console.log(req.url);
console.log(req.headers);
console.log(req.httpVersion);
console.log(req.method);
req.on('data',function(data){
console.log('req data ',data.toString());
});
req.on('end',function(){
res.end(`
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5
hello`) //手写的响应头,不能有空格
});
})
server.on('connection',function(){
console.log('建立连接');
});
server.listen(8000);
我们要实现一个parser方法,读取socket的内容,写入sd中
let {StringDecoder} = require('string_decoder');
function parser(socket,callback){
let buffers = []; // 每次读取的数据放到数组中
let sd = new StringDecoder();//解决乱码问题
//不断读取socket中的内容,写入sd中
function fn(){
let content = socket.read(); // read默认将请缓存区内容读完,读完后如果还有数据会继续触发readable事件
buffers.push(content);
let str = sd.write(Buffer.concat(buffers));
console.log('从socket读出的内容 :',str);
}
socket.on('readable',fn);
}
此时启动服务,通过curl命令访问服务
curl -v -d 'a=1' http://localhost:3000/abc?a=1#aaa
可以看到输出结果如下:
建立连接
从socket读出的内容 :
POST /abc?a=1 HTTP/1.1
Host: localhost:3000
User-Agent: curl/7.59.0
Accept: */*
Content-Length: 5
Content-Type: application/x-www-form-urlencoded
'a=1'
从上面打印结果中可以看出,socket套接字接收了客户的请求信息,其中包含了请求头(空行之上)和请求体(空行之下),根据这些信息,我们实现一个将请求头解析成对象的方法
//一行一行解析
function parserHeader(head){
let lines = head.split(/\r\n/);
let start = lines.shift(); //取出第一行
let method = start.split(' ')[0];//这里解析出method为POST
let url = start.split(' ')[1];
let httpVersion = start.split(' ')[2].split('/')[1];
let headers = {};
//解析剩余行
lines.forEach(line => {
let row = line.split(': ');
headers[row[0]] = row[1];
});
return {url,method,httpVersion,headers}
}
这里就是根据上面的请求信息的打印结果,将字符串解析成key-value的对象
请求信息解析完了,需要进一步完善parser,模拟http服务触发request事件和其中的req的data事件
function parser(socket,callback){
let buffers = []; // 每次读取的数据放到数组中
let sd = new StringDecoder();
function fn(){
let content = socket.read(); // 默认将请缓存区内容读完,读完后如果还有数据会触发readable事件
buffers.push(content);
let str = sd.write(Buffer.concat(buffers));
console.log('从socket读出的内容 :',str);
//如果读取的内容有两个连续的\r\n,说明请求头已读完
if(str.match(/\r\n\r\n/)){
let result = str.split('\r\n\r\n');
let head = parserHeader(result[0]);//解析请求头
Object.assign(socket,head); //解析的请求头赋给socket
socket.removeListener('readable',fn); // 读完请求头,不再继续读,移除监听
socket.unshift(Buffer.from(result[1]));// 将读取多出的内容塞回流中
callback(socket);//执行callback,触发request事件
}
}
socket.on('readable',fn)
}
上面代码
1、str.match(/\r\n\r\n/) 当str中包含连续的换行回车时,说明读完请求头了,即result[0]是请求头
2、Object.assign(socket,head); 将解析的请求头对象赋给socket
3、当读出整个请求头内容时,就停止读取,即移除readable事件监听
4、http服务的req的data事件,是用来接收请求发来的数据(即请求体的数据),上面的代码,readable有可能将请求发来的请求头和请求体的数据都读完,那么data事件将无法触发,所以要将readable读取出的、除了请求头之外的数据(即result[1]),再添加回socket(socket.unshift)
5、按照我们的用法,执行callback( server.emit('request',req,res);)就会通过emit触发request事件;
在request中监听了
注:socket.on('end',callback)是客户端关闭时才调用,而http服务中的req.on('end',callback)是当请求体数据传输完毕时调用,所以上面代码中 callback(socket);可以触发data事件,但不能触发end事件
进一步完善parser,使我们的服务能自动触发end事件
let {Readable} = require('stream');
class IncomingMessage extends Readable{ //自定义可读流
_read(){}
}
function parser(socket,callback){
let buffers = [];
let sd = new StringDecoder();
let im = new IncomingMessage();//自定义可读流实例
function fn(){
//模拟res
let res = {write:socket.write.bind(socket),end:socket.end.bind(socket)}
let content = socket.read();
buffers.push(content);
let str = sd.write(Buffer.concat(buffers));
if(str.match(/\r\n\r\n/)){
let result = str.split('\r\n\r\n');
let head = parserHeader(result[0]);
// im = {...im,...head}
Object.assign(im,head); //将请求头对象给im可读流
socket.removeListener('readable',fn);
socket.unshift(Buffer.from(result[1]));
if(result[1]){ // 有请求体,把数据移到im可读流(data触发多次暂不考虑,认为只有一次)
socket.on('data',function(data){
im.push(data);
im.push(null); //null表示可读流结束,用于触发end事件
callback(im,res);
});
}else{ // 没请求体
im.push(null);
callback(im,res);
}
}
}
socket.on('readable',fn)
}
1.为了触发end事件,创建自定义可读流im,当可读流读到数据末尾时,就会自动触发end
2.这里req即im,当req触发data事件读取到流的末尾,即触发end
3.模拟res,res.end()中手写了一个响应头,在浏览器中访问localhost:8000可以看到输出‘hello’
完整代码如下(tcp服务模拟实现http服务)
let net = require('net');
let {StringDecoder} = require('string_decoder');
let {Readable} = require('stream');
class IncomingMessage extends Readable{
_read(){}
}
function parser(socket,callback){
let buffers = [];
let sd = new StringDecoder();
let im = new IncomingMessage();
function fn(){
let res = {write:socket.write.bind(socket),end:socket.end.bind(socket)}
let content = socket.read();
buffers.push(content);
let str = sd.write(Buffer.concat(buffers));
if(str.match(/\r\n\r\n/)){
let result = str.split('\r\n\r\n');
let head = parserHeader(result[0]);
// im = {...im,...head}
Object.assign(im,head);
socket.removeListener('readable',fn);
socket.unshift(Buffer.from(result[1]));
if(result[1]){
socket.on('data',function(data){
im.push(data);
im.push(null);
callback(im,res);
});
}else{
im.push(null);
callback(im,res);
}
}
}
socket.on('readable',fn)
}
function parserHeader(head){
let lines = head.split(/\r\n/);
let start = lines.shift();
let method = start.split(' ')[0];
let url = start.split(' ')[1];
let httpVersion = start.split(' ')[2].split('/')[1];
let headers = {};
lines.forEach(line => {
let row = line.split(': ');
headers[row[0]] = row[1];
});
return {url,method,httpVersion,headers}
}
let server = net.createServer(function(socket){
parser(socket,function(req,res){
server.emit('request',req,res);
});
});
server.on('request',function(req,res){
console.log(req.url);
console.log(req.headers);
console.log(req.httpVersion);
console.log(req.method);
req.on('data',function(data){
console.log('ok',data.toString());
});
req.on('end',function(){
res.end(`
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5
hello`)
});
})
server.on('connection',function(){
console.log('建立连接');
});
server.listen(3000);
http客户端
http客户端发送JSON数据
let http = require('http');
let options = {
hostname:'localhost', //访问主机ip
port:4000, //端口号
path: '/', // ‘/’是根路径
method:'get', //访问方式
// 请求头 客户端用来描述发送给服务端的数据
headers:{
'Content-Type':'application/json',
'Content-Length':13 //Content-Length为发送数据转为buffer的长度,如果没设置,数据发送不过去;设置长度和发送长度不符,会报错
}
}
let request = http.request(options); //调用http.request方法,并没有真正发送数据给服务端
request.on('response',function(res){
res.on('data',function(chunk){
console.log(chunk);
});
});
//调用request.end方法,发送数据给服务端,数据只能是buffer或字符串
request.end('{"name":"wz"}'); //发送json字符串
http客户端发送表单数据
let http = require('http');
let options = {
hostname:'localhost',
port:4000,
path: '/',
method:'get',
headers:{
'Content-Type':'application/x-www-form-urlencoded',
'Content-Length':15
}
}
let request = http.request(options);
//监听响应,当服务端给客户端发送响应时触发回调
request.on('response',function(res){
//res响应为可读流,读取数据
res.on('data',function(chunk){
console.log(chunk);
});
});
request.end('name=wz&&age=29');//表单发送数据
服务端解析发送的数据并响应
let http = require('http');
let queryString = require('querystring'); //解析query字符串
let server = http.createServer(function(req,res){
let contentType = req.headers['content-type'];
let buffers = [];
req.on('data',function(chunk){
buffers.push(chunk);
});
req.on('end',function(){
let content = Buffer.concat(buffers).toString();
console.log('content',content);
if(contentType === 'application/json'){
console.log(JSON.parse(content).name)
}else if(contentType === 'application/x-www-form-urlencoded'){
console.log(queryString.parse(content).name)
}
res.end('hello');
});
});
server.listen(4000);
通过node xxx方式启动客户端,在浏览器直接访问,看不到服务端的log
http服务如何支持多语言
从百度的请求头中可以看到字段Accept-Language表示支持的语言,其中q是指它前面这一语言的权重,没指定q的,权重为1,按照权重的优先级和服务支持的语言,匹配先使用哪种语言Node.js的服务是基于流的,有关Node.js Stream(流)的概念可参考
juejin.cn/post/684490…
有关Tcp的介绍可参考juejin.cn/post/684490…
参考资料
1、www.jianshu.com/p/ab2741f78…