【NJ05】NodeJS基础篇—05 Node开发方向之Server—网络

423 阅读9分钟

前言

前篇NodeJS基础篇—01从全局开始讲了NodeJS的全局模块等基础知识,那NodeJS还有什么呢?比如,网络(UDP、TCP、HTTP)、线程&进程管理等等;

本篇主要讲一下网络这部分,毕竟 API I/O 是一个服务的关键部分。话不多说,开始吧,有任何表述不准确的地方,欢迎各位大大们指正。

共涉及到的模块

  • dgram
  • net
    • socket对象
  • http模块:
    • Reqeust对象:获取请求数据信息
    • Response对象:响应反馈
  • url模块:分析处理 url
  • queryString模块:处理提交数据
  • URL
  • URLSearchParam
  • stream模块:处理body提交数据
  • path模块:本地文件路径处理
  • fs模块:文件资源处理模块

一、dgram模块(UDP 数据报

dgram模块提供了 UDP 数据包 socket 的实现

socket又称"套接字",应用程序通常通过"套接字" 向网络发出请求或者应答网络请求,其本质上就是一套用于实现网络数据交换的接口(API)

搭建简单的dgram服务器

【1】获取核心模块dgram

# Code

const dgram = require('dgram')

【2】取 dgram.Socket 类,并创建dgram.socket服务

创建一个特定 type 的dgram.Socket 对象,用来处理网 络数据的一个标准 API 对象,可以对网络数据进行读取和输出。

# 法1:
const serverSocket = new dgram.Socket()

# 法2:
const serverSocket = dgram.createSocket(type[, callback])
        # type: 'udp4''udp6' //udp4 => ipv4
# Code 

const serverSocket = dgram.createSocket('udp4');

【3】dgram(UDP)监听事件 .on()

  • close
  • error
  • listening
# 服务器开启成功,等待数据
# Code

serverSocket.on('listening', () => {
    console.log('服务器开启成功,等待数据:'); 
});
  • message
# 当接收到数据的时候触发 
# Code

serverSocket.on('message', data => {
    console.log('接收到了数据:', data.toString()); 
})

【4】绑定(监听)指定的地址及端口 .bind()

绑定(监听)指定的地址及端口

serverSocket.bind([port][, address][, callback])
    - port: 未指定则由系统分配
    - address: 默认 0.0.0.0,表示所有地址
    - IP callback: 绑定成功后的回调
# Code

serverSocket.bind(12345, '127.0.0.1');

【5】关闭服务 .close()

# Code

serverSocket.close()

【6】发送数据 .send()

发送数据:UDP,无连接协议,不需要连接到服务器,然后再发 数据。

serverSocket.send(msg, port, [address])
    - msg: 发送的数据(字符串/Buffer)

直接发送数据触到 前面指定ip:port的serverSocket接收信息

# Code

clientSocket.send('hello', 12345, '127.0.0.1');

二、net模块(TCP

在node中,tcp 协议,基于 net 模块来实现的 net 模块提供了创建基于流的 TCP 或 IPC 服务器。

(net.createServer())和客户端(net.createConnection()) 的异步 网络 API

  • 服务端:提供服务,被连接,被请求的一方
  • 客户端:获取服务,发起连接,请求的一方

搭建简单的net服务器 【net.Server类】

注: 接收到的数据是buffer,二进制 流数据传输

【1】获取核心模块net

# Code

const net = require('net')

【2】取 net.Server 类,并创建服务实例

# 法1:
const serverNet = new net.Server()

# 法2:
const serverNet = net.createServer([port[, host]])
      
# Code 

const server = net.createServer( 
    () => { 
        // 这个函数其实就是connection事件绑定的函数
    }
);

【3】监听端口 .listen()

监听端口,处理请求

    server.listen(端口, [ip]) 

端口:
ip:默认为0.0.0.0,表示所有通配

【4】net事件 .on()

  • close
  • error
  • connection
当有客户端连接的时候触发
回调函数的第一个参数是一个net.Socket实例对象,
数据的传输就是通过socket对象来实现,回调函数参数是传送的数据

# !!!socket对象中包含所有的socket实例方法,
# 比如socket.write向客户端发送数据

server.on('connection', socket => {
    // socket => 当前连接的 socket 对象
})

搭建简单的客户端 【net.Socket类】

TCP:先连接connection,后传输

【1】取 net.Socket 类,创建客户端socket对象

# 法一

const socketNetClient = new net.Socket()
socketNetClient.connect(options[, connectListener])

# 法二

const socketNetClient = net.createConnection(port[, host][, connectListener])
// connectListener连接回调
// 参数是 要连接的目标主机的地址以及端口号
# Code

const clientSocket = net.createConnection(12345, '10.220.17.238');

【2】在套接字上发送数据 .write()

在 socket 上发送数据。第二个参数指定了字符串的编码

.write(data[, encoding][, callback])

如果整个数据被成功刷新到内核缓冲区,则返回 true。 如果所有或部分数据在用户内存中排队,则返回 false。 当缓冲区再次空闲时,将触发 'drain'

可选的 callback 参数将在数据最终写完时执行(可能不会立即执行)。

【3】半关闭套接字 .end()

半关闭 socket。即,它发送一个 FIN 数据包。 服务器可能仍会发送一些数据。

如果end()中指定了 data,则相当于调用 socket.write(data, encoding) 之后再调用 socket.end()

【4】net.Socket类 事件

end和data主要是基于stream,才触发的

  • data 回调函数参数是buffer数据
  • end 需要另一方socket.end( )来触发
  • connect 已连接到服务器
  • error

数据包

在数据传输过程中不仅仅只有主体数据(你要发送的主要内容),还包括了一些其他的数据信息(一层一层包装),比如发送端的IP、端口等,以方便接受者对数据进行处理与回复反馈信息给对方。

如果发送的数据比较大的话,还会按照一定的规则对发送的数据进行分包,每一个包中包含有一部分主体数据以及 上面提到的额外信息,接收方在接收到数据以后会数据包进行整合等一系列操作。

这种传输规则就是数据传输协议中的规定,不同的协议对传输规则有不同的规定。

  • socket.remoteAddress //对方发过来的数据的地址
  • socket.remotePort //对方发过来数据的端口

net服务及客户端代码实例

# Server.js

const server = net.createServer();

server.on('connection', socket=>{  // !!!!net.Socket类!!!!
    //有client连接时触发 console.log('有client连接')
    socket.on('data',data=>{ //data事件接收数据
        console.log(`接收到了客户端发来的消息${data}`)
        socket.write('hello client')
        // 向客户端发送数据 
    }) 
})

server.listen(12345,'127.0.0.1')
# Client.js

const socketNetClient = new net.Socket()
socketNetClient.connect(12345,'127.0.0.1')
// const clientSocket = net.createConnection(12345,'127.0.0.1')

socketNetClient.on('connect',()=>{ 
    //已连接到server
    console.log('已连接上服务器')

    socketNetClient.write('hello,server') 
    socketNetClient.on('data',data=>{ 
        //二进制数据 
        console.log('接收到了服务器发来消息'+data) 
    })
})

三、HTTP模块

搭建简单的HTTP服务器

【1】获取核心模块http

# Code

const http = require('http')

【2】取 http.server 类,http.server服务

该类继承自 net.server,http基于tcp,request本质是 net.socket+http协议增加的一些内容。

request.socket 基本等价 net.socket

console.log(request.socket.remoteAddress );

# 法1:
const server = new http.server()

# 法2:
const server = http.createServer([options][, requestListener])

# Code 

const server = http.createServer( 
    () => { 
        // 这个函数其实就是connection事件绑定的函数
    }
);

【3】监听端口 .listen()

监听端口,处理请求

    server.listen(端口, [ip]) 

端口:
ip:默认为0.0.0.0,表示所有通配

【4】http事件 .on()

  • close
  • error
  • connection
当有客户端连接的时候触发
回调函数的第一个参数是一个http.Socket实例对象,
数据的传输就是通过socket对象来实现,回调函数参数是传送的数据

# !!!socket对象中包含所有的socket实例方法,
# 比如socket.write向客户端发送数据

server.on('connection', socket => {
    // socket => 当前连接的 socket 对象
    socket.write('xxxx')
    
})
  • request HTTP对Request和Response的数据格式进行规定以及包装。
    • request
      • request.setHeader(name, value)
      • request.getHeader(name)
      • request.path
      • request.protocal
      • request.host
      • request.url
    • response
      • response.setHeader(name, value)
      • response.getHeader(name)
      • response.end([data[, encoding]][, callback]) //表明所有响应头和正文都已发送,如果指定了 data,则其效果类似于调用 response.write(data, encoding)后跟 response.end(call back)
      • response.write(chunk[, encoding][, callback])//向客户端返回响应内容
      • response.statusCode
      • response.writeHead(statusCode[, statusMessage][, headers])向请求指定状态码发送响应头。
server.on('request', (request, response) => {
     # request: 客户端请求对象,
     # 保存了与当前这次请求的客户端相关的信息http.IncomingMessage 类
     # response: 服务器输出对象,提
     # 供了服务端输出(响应) 有关的一些方法http.ServerResponse 类
}

搭建简单的HTTP客户端

也可以是一个服务器向另一个服务器请求 http.clientRequest - options

  • protocol : 使用的协议。默认为 http:
  • host : 请求发送至的服务器的域名或 IP 地址。默认为 localhost
  • family : 当解析 host 和 hostname 时使用的 IP地址族时有效,值是 4 或 6,未指定时,则同时使用 IP v4 和 v6 port : 远程服务器的端口。默认为 80
  • method : 指定 HTTP 请求方法的字符串。默认为'GET'
  • path : 请求的路径。默认为 '/'。 应包括查询字符 串(如有的话)。如 '/index.html?page=12'
  • headers : 包含请求头的对象

【1】http.ClientRequest类,创建客户端实例

# 法一
const client = new http.ClientRequest()

# 法二
const client = http.request(options[, callback])
# Code

const client = http.request( { 
        //创建一个客户端(能发http请求)的对象
        // tcp/ip
        host: 'www.baidu.com', port: 80,

        // http
        protocol: 'http:', 
        method: 'get', 
        path: '/img/bd_logo1.png'
    }, res => { 
        // 这个回调函数会在服务器响应的时候触发 
        // !!!!! res => socket !!!!!
        let content = Buffer.alloc(0); 
        res.on('data', data => {
            content = Buffer.concat([content, data], content.length + data.length); 
        });

        res.on('end', () => { 
            fs.writeFileSync('./baidu.png', content); }); 
        }
);

http服务器代码实例

# server.js

  const http = require('http');
  const path = require('path');
  const fs = require('fs');
  const mime = require('./mime.json');

  http.createServer((req,res)=>{
      fs.readFile(path.join(__dirname,'www',req.url), (err,fileContent)=>{
         if(err){
            // 没有找到对应文件 404Notfind 
            res.writeHead(404,{
              'Content‐Type':'text/plain; charset=utf8'
            });
            res.end('页面被狗狗叼走了'); 
         }else{
            let dtype = 'text/html'; //默认类型
            // 获取请求文件的后缀\
            let ext = path.extname(req.url);
            
            // 如果请求的文件后缀合理,就从 json文件 里 获取到标准的响应格式 
            if(mime[ext]){
              dtype = mime[ext];
            }

            // 如果响应的内容是文本,就设置成utf8编码 
            if(dtype.startsWith('text')){
              dtype += '; 
              charset=utf8'
            }

            // 设置响应头信息 
            res.writeHead(200,{
              'Content‐Type':dtype
            });
            res.end(fileContent);
         }
     });
  }).listen(3000, ()=>{
       console.log('running...');
  });
 

四、参数传递与获取

核心模块url

node:url 模块提供用于网址处理和解析的实用工具

┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                              href                                              │
├──────────┬──┬─────────────────────┬────────────────────────┬───────────────────────────┬───────┤
│ protocol │  │        auth         │          host          │           path            │ hash  │
│          │  │                     ├─────────────────┬──────┼──────────┬────────────────┤       │
│          │  │                     │    hostname     │ port │ pathname │     search     │       │
│          │  │                     │                 │      │          ├─┬──────────────┤       │
│          │  │                     │                 │      │          │ │    query     │       │
"  https:   //    user   :   pass   @ sub.example.com : 8080   /p/a/t/h  ?  query=string   #hash "
│          │  │          │          │    hostname     │ port │          │                │       │
│          │  │          │          ├─────────────────┴──────┤          │                │       │
│ protocol │  │ username │ password │          host          │          │                │       │
├──────────┴──┼──────────┴──────────┼────────────────────────┤          │                │       │
│   origin    │                     │         origin         │ pathname │     search     │ hash  │
├─────────────┴─────────────────────┴────────────────────────┴──────────┴────────────────┴───────┤
│                                              href                                              │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
("" 行中的所有空格都应被忽略。它们纯粹是为了格式化。)

旧版

const url = require('url');
// 用于 URL 处理与解析
const querystring = require('querystring');
// querystring 模块提供了一些实用函数,用于解析与格式化 URL 查询字符串

const myURL =
  url.parse('https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash');
  
  • .parse('url',flag) 把URL字符串转化为对象,加flag = true之后, 其中的query分割成一个对象,querystring处理
  • .format() 就是把对象转化为标准的URL字符串
  • .search获取和设置网址的序列化的查询部分。
    • querystring.stringify()
  • .query 解析的query对象
    • querystring.parse()

新版

const myURL =
  new URL('https://user:pass@sub.example.com:8080/p/a/t/h?query=string#hash');
  • new URL(input[, base])
  • .format() 就是把对象转化为标准的URL字符串
  • .search获取和设置网址的序列化的查询部分。
  • .searchParams获取表示网址查询参数的 URLSearchParams对象。 此属性是只读的。
    • new URLSearchParams()参数可以是search也可以是queryObj,类似querystring核心库
    • .toString()序列化
    • .append(name, value)
    • .delete(name)
    • .set(name, value)
    • .get(name)

五、发起HTTP请求的方法

  • HTTP 标准库

    • 无需安装外部依赖
    • 需要以块为单位接受数据,自己监听 end 事件
    • HTTP 和 HTTPS 是两个模块,需要区分使用
  • Request 库

    • 使用方便
    • 有 promise 版本 request-promise
  • Axios

    • 既可以用在浏览器又可以用在 NodeJS
    • 可以使用 axios.all 并发多个请求
  • SuperAgent

    • 可以链式使用
  • node-fetch

    • 浏览器的 fetch 移植过来的