Node.js HTTP介绍 tcp模拟http服务

3,478 阅读10分钟

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…