04月07, 2018

node深入笔记(4)

第四篇。。。

网络

协议的概念

  • 为了让计算机能够通信,计算机需要定义通信规则,这些规则就是协议
  • 规则是多种,协议也有多种
  • 协议就是数据封装格式+传输

OSI七层模型

Open System Interconnection,适用于所有的网络。

alt

TCP/IP参考模型

  • TCP/IP是传输控制协议/网络互联协议的简称
  • 早期的TCP/IP模型是一个四层结构,从下往上依次是网络接口层、互联网层、传输层和应用层
  • 后来在使用过程中,借鉴OSI七层参考模型,将网络接口层划分为了物理层和数据链路层,形成五层结构

alt

这里简单说明一下各层所做的事:

  • 物理层,为数据端设备提供传送数据的通路,传输数据
  • 数据链路层,用来向网络层提供数据,就是把源计算机网络层传过来的信息传递给目标主机
  • 网络层,路由和选址
  • 传输层,提供建立、维护和取消传输连接功能,负责可靠地传输数据(PC)
  • 应用层,提供网络与用户应用软件之间的接口服务

发送方是从高层到低层封装数据

比如我们要发email给对方,流程大致如下:

  • 在应用层要把各式各样的数据如字母、数字、汉字、图片等转换成二进制
  • 在TCP传输层中,上层的数据被分割成小的数据段,并为每个分段后的数据封装TCP报文头部
  • 在TCP头部有一个关键的字段信息端口号,它用于标识上层的协议或应用程序,确保上层数据的正常通信 计算机可以多进程并发运行,例如在发邮件的同时也可以通过浏览器浏览网页,这两种应用通过端口号进行区分
  • 在网络层,上层数据被封装上亲的报文头部(IP头部),上层的数据是包括TCP头部的。IP地址包括的最关键字段信息就是IP地址,用于标识网络的逻辑地址。
  • 数据链路径层,上层数据成一个MAC头部,内部有最关键的是MAC地址。MAC地址就是固化在硬件设备内部的全球唯一的物理地址。
  • 在物理层,无论在之前哪一层封装的报文头和还是上层数据都是由二进制组成的,物理将这些二进制数字比特流转换成电信号在网络中传输

alt

接收方是从低层到高层解封装

作为email的接收方,流程大致如下:

  • 数据封装完毕传输到接收方后,将数据要进行解封装
  • 在物理层,先把电信号转成二进制数据,并将数据传送至数据链路层
  • 在数据链路层,把MAC头部拆掉,并将剩余的数据传送至上一层
  • 在网络层,数据的IP头部被拆掉,并将剩余的数据送至上一层
  • 在传输层,把TCP头部拆掉,将真实的数据传送至应用层

alt

真实网络环境

  • 发送方和接收方中间可能会有多个硬件中转设备 中间可能会增加交换机和路由器
  • 数据在传输过程中不断地进行封装和解封装的过程,每层设备只能处理哪一层的数据
    • 交换机属于数据链路层
    • 路由器属于网络层

TCP三次握手及四次挥手

三次握手

TCP是面向连接的,无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP 协议提供可靠的连接服务,连接是通过三次握手进行初始化的。三次握手的目的是同步连接双方的序列号和确认号 并交换 TCP窗口大小信息。

  • 第一次握手: 建立连接。客户端发送连接请求,发送SYN报文。然后,客户端进入SYN_SEND状态,等待服务器的确认。
  • 第二次握手: 服务器收到客户端的SYN报文段。需要对这个SYN报文段进行确认,发送ACK报文。同时,自己还要发送SYN请求信息。服务器端将上述所有信息一并发送给客户端,此时服务器进入SYN_RECV状态。
  • 第三次握手: 客户端收到服务器的ACK和SYN报文后,进行确认,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

简单地说,就是确定彼此能通信,需要三步。

四次挥手

  • 第一次挥手:客户端向服务器发送一个FIN报文段;此时,客户端进入 FIN_WAIT_1状态,这表示客户端没有数据要发送服务器了,请求关闭连接;
  • 第二次挥手:服务器收到了客户端发送的FIN报文段,向客户端回一个ACK报文段;服务器进入了CLOSE_WAIT状态,客户端收到服务器返回的ACK报文后,进入FIN_WAIT_2状态; 第三次挥手:服务器会观察自己是否还有数据没有发送给客户端,如果有,先把数据发送给客户端,再发送FIN报文;如果没有,那么服务器直接发送FIN报文给客户端。请求关闭连接,同时服务器进入LAST_ACK状态; 第四次挥手:客户端收到服务器发送的FIN报文段,向服务器发送ACK报文段,然后客户端进入TIME_WAIT状态;服务器收到客户端的ACK报文段以后,就关闭连接;此时,客户端等待2MSL后依然没有收到回复,则证明Server端已正常关闭,客户端也可以关闭连接了。

图解说明

我们可以通过wireshark来证明上面的说法。

可以写一个简单的node服务,如:

const http = require('http');
const server = http.createServer((req, res) => {
   res.end('hello world');
});
server.listen(4444, () => {
    console.log('server started');
});

alt

图中也可以关注一下ACK及SYN的值。

其他

网络这一块,需要掌握的,肯定不止上面说的这些,还有像IP相关的(如头、子网掩码等),TCP深入点的知识。

可以通过阅读《TCP/IP详解》或《图解TCP》来提升。我前几天刚花了30大洋买了图解的电子书,看了一大半,表示略蛋疼(主要是概念的东西比较多)。

TCP代码实战

在Node.js中,net模块实现了基于TCP的数据通信。

API地址

const net = require('net');
// 创建一个tcp服务 里面放的是回调函数 监听函数,当连接到来时才会执行
// socket 套接字 是一个duplex 可以支持读操作和写操作
let server = net.createServer(function(socket){
    // 最大连接数2个
    // 希望每次请求到来时都一个提示 当前连接了多少个 一共连接多少个
    server.maxConnections = 2;
    server.getConnections(function(err,count){
        // socket每次连接时都会产生一个新的socket
        socket.write(`当前最大容纳${server.maxConnections},现在${count}人`)
    });
    socket.setEncoding('utf8');
    socket.on('data',function(data){
        console.log(data);
        // socket.end(); // 触发客户端的关闭事件
        // close事件表示服务端不在接收新的请求了,当前的还能继续使用,当客户端全部关闭后会执行close事件
        // server.close();
        // 如果所有客户端都关闭了,服务端就关闭,如果有人进来仍然可以
        server.unref();
    });
    socket.on('end',function(){
        console.log('客户端关闭');
    });
    // 请求到来时会触发这个函数
    // socket时一个可读可写
});
let port = 8080;
server.listen(port,'localhost',function(){
    console.log(`server start ${port}`)
});
// 当服务端发生错误时,close事件只有调用close方法才会触发
server.on('close',function(){
    console.log('服务端关闭');
})
server.on('error',function(err){
    // 如果端口号被占用的话,+1
    if(err.code === 'EADDRINUSE'){
        server.listen(++port)
    }
});

简单聊天室

let net = require('net');
// 当客户端连接服务端时 会触发回调函数 默认提示 输入用户名,就可以通信了
// 自己的说的话 不应该通知自己 应该通知别人
let clients = {};
function broadcast(nickname,chunk){
    Object.keys(clients).forEach(key=>{
        if(key!=nickname){
            clients[key].write(`${nickname}:${chunk} \r\n`);
        }
    })
}
let server = net.createServer(function(socket){
    server.maxConnections = 3;
    server.getConnections((err,count)=>{
        socket.write(`欢迎来到聊天室 当前用户数${count}个,请输入用户名\r\n`);
    });
    let nickname;
    socket.setEncoding('utf8'); 
    socket.on('end',function(){
        clients[nickname] &&clients[nickname].destroy();
        delete clients[nickname]; // 删除用户
    });
    socket.on('data',function(chunk){
        chunk = chunk.replace(/\r\n/,'')
        if(nickname){
            // 发言 broadcast
            broadcast(nickname,chunk);
        }else{
            nickname = chunk;
            clients[nickname] = socket;
            socket.write(`您的新用户名是${nickname} \r\n`);
        }
    });
});

server.listen(8080);

再强大一些,我们可以实现一个类似命令行的聊天室,功能如下:

  • r:新昵称 (修改昵称)
  • l: (显示在线的用户列表)
  • b:消息内容 (将消息内容广播给其他用户)
  • s:用户昵称:消息内容 (给指定用户发送消息内容,即私聊)

这个代码并不是太很难,唯一的可能需要考虑一点,就是匿名情况下,需要有一个key来做区别,可以使用:

const key = socket.remoteAddress + socket.remotePort; // 来保证唯一性

客户端的tcp代码

一个简单的示例如下:

const net = require('net');
net.createConnection({port:8080},function(){
    socket.write('hello');
    socket.on('data',function(data){
        console.log(data);
    });
});

当然,客户端连接的前提必须要有一个8080的localhost server端。

const net = require('net');
const server = net.createServer(function(socket){
    socket.setEncoding('utf8');
    socket.on('data',function(data){
        console.log(data.toString());
        socket.write('你好');
    });
});
server.on('connection',function(){
    console.log('客户端连接')
})
server.listen(8080);

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(); // 默认将请缓存区内容读干,读完后如果还有会触发readable事件
        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);
            }
            //callback(socket);
            // 先默认socket 是req对象 (内部又封装了一个可读流 IncomingMessage)
        }
    }
    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);

上面代码只是简单的实现了req的一些属性,原理就是读取socket流,就能得到类似下面的字符串:

GET / HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID.ef106639=1dvju3tfe9r67kverrzweazu8; screenResolution=1280x800

a=1&b=2

然后上面的部分是header的,下面的是数据。header部分通过parseHeader方法来处理。

本文链接:www.my-fe.pub/post/node-deep-note-4.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。