libin1991 / libin_Blog

爬虫-博客大全
https://libin.netlify.com/
124 stars 17 forks source link

Node HTTP简单使用 tcp模拟http服务 #512

Open libin1991 opened 6 years ago

libin1991 commented 6 years ago

URL模块

let url = require('url');
//一个url路径
let u = 'http://LiMing:xxxx@www.baidu.com:80/abc/index.html?a=1&b=2#hash';
let urlObj = url.parse(u);
console.log(urlObj);

可以打印出

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' }

我们常用的有

// 加true 可以将地址中的查询字符串转化成对象
let urlObj = url.parse(u,true);
console.log(urlObj.query); // 查询字符串 { a: '1', b: '2' }
console.log(urlObj.pathname); //  路径

http模块(基于TCP)

创建http服务,并监听请求可用如下两种方式

let http = require('http'); //继承了net包,所以和tcp用法基本相同
let server = http.createServer(function(req,res){})

let server = http.createServer();
server.on('request',function(req,res){})

一些API

let http = require('http'); 
// 请求头
// > Host: www.baidu.com
// > User-Agent: curl/7.46.0
// > Accept: */*
// > Content-Length: 11
// > Content-Type: application/x-www-form-urlencoded

let server = http.createServer();
// req是请求 是一个可读流
// res是响应 是一个可写流
server.on('request',function(req,res){
    let method = req.method;
    let httpVersion  = req.httpVersion;
    let url = req.url;
    let headers = req.headers; 
    console.log(method,httpVersion,url,headers);
    // 如果数据 大于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());
        // socket.write socket.end
        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);

测试
执行启动http服务
可以用curl命令发送http请求


也可以直接在浏览器中访问localhost:8080 其中
-v是打印出来的详细信息
-d 'a=1' 是添加请求体 a=1
如果没有请求体不会走on('data'),但还是会触发end事件
req.headers可以打印请求头信息,请求头中的名字都是小写的

由于http是基于tcp的,这里用tcp服务模拟http服务

使用方式如下

let server = net.createServer(function(socket){
    //实现一个parser方法,解析出请求和响应,并模拟Http服务触发request事件
    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('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(3000);

接下来我们要实现一个parser方法,读取socket中的内容

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);
    }
    socket.on('readable',fn);
}

上面parser拿到了socket中传来的内容
启动服务
通过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'

上面打印结果中,空行之上是请求头,实现一个将请求头解析成对象的方法

//一行一行解析
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}
}

进一步完善parser,使能触发http的request事件和其中的data事件

function parser(socket,callback){
    let buffers = []; // 每次读取的数据放到数组中
    let sd = new StringDecoder();
    function fn(){
        let res = {write:socket.write.bind(socket),end:socket.end.bind(socket)}
        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,就会emit触发request事件

进一步完善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:3000可以看到输出‘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);