nodejs网络通信

159 阅读10分钟

node提供了net、dgram、http、https4个模块,分别用于处理tcp、udp、http、https,适用于服务器端和客户端。

node网络通信概念

网络7层模型

image.png 也有人把7层模型简化为5层模型

image.png

物理层:

电缆、光纤等,传输0和1这些信号

链接层:

单纯的0和1没有意义,连接层是规定0和1的分组方式

链接层-以太网协议:

    一组电信号构成一个数据包,叫作"帧"(frame),每一帧分为两个部分:标头(head)和数据(data)。
    标头包含数据包的一些说明,比如发送者、接收者、数据类型。
    标头的长度固定为18字节,数据的长度,最短为46字节,最长为1500字节。
    因此整个帧最短为64字节,最长为1518字节,如果数据很长,就必须分割为多个包发送。

链接层-mac地址

以太网那个数据包的标头包含了发送者和接受者的信息,那么发送者和接受者是如何标识呢?
以太网规定,连入网络的所有设备,都必须具有“网卡”接口,数据包必须是从一块网卡,
传送到另一块网卡,网卡的地址就是数据包发送的地址和接收的地址。这叫做mac地址。
每块网卡出场的时候,都有一个全世界独一无二的mac地址,长度是48个二进制位,通常用12个16进制数表示。
比如:00-B0-D0-86-BB-F7
前6个16进制数是厂商编号,后6个是该厂商的网卡流水号,有了mac地址,就可以定位网卡和数据包的路径了。

链接层-广播

以太网数据包必须知道接收方的mac地址,然后才能发送。
首先,一块网卡怎么会知道另一块网卡的mac地址?
其次,就算有mac地址,系统怎么才能把数据包准确送到接收方。

回答是以太网采用一种很原始的方式,他不是把数据包准确送到接收方,
而是向本网络内所有计算机发送,让每台计算机自己判断,是否为接收方。

网络层-由来

以太网协议,依靠mac地址发送数据,理论上,单单依靠mac地址,上海的网卡就可以找到洛杉矶的网卡了,技术上是可以实现的。
但是这样做有一个重大的缺点,以太网采用广播的方式发送数据包,所有成员人手一包,不仅效率低,
而且局限在发送者所在的子网络。也就是说,如果两台计算机不在同一个子网络,广播是传不过去的。
这种设计是合理的,否则互联网上每台计算机都会收到所有的包,那会引起灾难。
互联网是无数子网络共同组成的一个巨型网络,很像想象上海和洛杉矶的电脑会在同一个子网络,这几乎是不可能的。
因此,必须找到一种方法,能够区分哪些mac地址属于同一个子网络,哪些不是,如果是同一个子网络,就采用广播方式发送,否则就采用路由方式发送,("路由"的意思,就是指如何向不同的子网络分发数据包),mac地址本身无法做到这一点,它只与厂商有关与所处网络无关。
这就导致了网络层的诞生,它的作用是引进一套新的地址,使得我们能够区分不同的计算机是否属于同一个子网络,这套地址就叫做网络地址。简称"网址"。
于是,网络层出现以后,每台计算机有了两种地址,一种是mac地址,另一种是网络地址,两种地址之间没有任何联系,mac地址是绑定在网卡上的,网络地址则是管理员分配的,它们只是随机组合到一起,网络地址帮助我们确定计算机所在的子网络,mac地址则将数据包送到该子网络的目标网卡,因此从逻辑上可以推断,必定是先处理网络地址,然后再处理mac地址。

网络层-ip协议

规定网络地址的协议,叫作ip协议,它所定位的地址,就称为ip地址。
目前广泛采用的是ip协议的第4版,简称ipv4,这个版本规定,网络地址由32个二进制组成。
比如:172.16.254.1
习惯上,我们用分成4段的十进制数表示ip地址,从0.0.0.0一直到255.255.255.255
ip协议的作用主要有两个,一个是为每一台计算机分配ip地址,另一个是确定哪些地址在同一个子网络。

传输层-端口号

有了mac地址和ip地址,我们已经可以在互联网上任意两台主机上建立通信。接下来的问题是,同一台主机上有许多程序都需要用到网络,比如,你一边浏览网页,一边与朋友线上聊天,当一个数据包从互联网上发来的时候,你怎么知道,他是表示网页的内容,还是表示在线聊天的内容?
也就是说,我们还需要一个参数,表示这个数据包到底供哪个程序(进程)使用,这个参数就叫做端口(port),它其实是每一个使用网卡的程序的编号,每个数据包都发送到主机的特定端口,所以不同的程序就能取到自己所需要的数据。
端口是0-65535之间的一个整数,正好16个二进制位,0-1023的端口被系统占用,用户只能选用大于1023的端口,不管是浏览网页还是在线聊天,应用程序会随机选用一个端口,然后与服务器的相应端口联系。
传输层的功能就是建立端口到端口的通信,相比之下,网络层的功能是建立主机到主机的通信,只要确定主机和端口,我们就能实现程序之间的交流,因此,unix就把主机+端口,叫作"套接字"(socket),有了它,就可以进行网络应用程序开发了。

传输层-udp协议

现在我们必须在数据包中加入端口信息,这就是需要新的协议,最简单的实现叫做udp协议,他的格式几乎就是在数据前面加上端口号。
udp数据包也就是由标头和数据两部分组成。
标头部分主要定义了发出端口和接收端口,数据部分就是具体的内容,然后,把整个udp数据包放入ip数据包的数据部分,而前面说过,ip数据包又是放在以太网数据包中的,
udp包非常简单,标头部分一共只有8个字节,总长度不超过65535字节,正好放进一个ip数据包。
udp协议本身不太可靠,数据发出去了,怎么知道对方有没有收到,对方把数据发出来了,对方怎么知道我收到了没有。所以保证不了数据传输的完整和一致性。

传输层-tcp协议

tcp:transmission control protocol传输控制协议
释义:是一种面向连接的、可靠的、基于字节流的传输层通讯协议
udp是不可靠的
tcp保证了数据的可靠性

开发人员主要接触到的是传输层,tcp可靠,那么什么时候哟个tcp什么时候用udp,需要根据场景,比如网页需要可靠的数据,用tcp比较好,比如视频直播、语音通话就可以用udp协议,它不会因为数据的丢失再去做连接,做额外的处理,顶多是卡顿一下。tcp有一个重要的特性就是一旦数据发送失败了就会重新发送。

应用层

应用程序收到传输层的数据,接下来就要进行解读,由于互联网是开放架构,数据来源五花八门,必须事先规定好格式,否则根本无法解读。
应用层的作用,就是规定应用程序的数据格式。
举例说:tcp协议可以为各种各样的程序传递数据,比如email、www、ftp等,那么必须有不同协议规定,电子邮件,网页、ftp数据的格式,这些应用程序协议就构成了应用层。
这是最高的一层,直接面对用户,他的数据就放在tcp数据包的数据部分,

构建tcp服务

image.png 简单的服务端:

const net = require('net')
const server = net.createServer()

server.on('connection', (clientSocket) => {
  console.log('有新的连接进来了')
  // 监听clientSocket的data事件
  clientSocket.on('data', data => {
    console.log('客户端说:', data.toString())
  })

  // 通过clientSocket给当前连接的客户端发送数据
  clientSocket.write('hello')
})

server.listen(3000, () => {
  console.log('server running 127.0.0.1:3000')
})

简单的客户端:

const net = require('net')

const client = net.createConnection({
  host: '127.0.0.1',
  port: 3000,
})

client.on('connect', () => {
  console.log('成功连接到服务器')
  // 给服务端发送数据了
  client.write('world')

  // 获取终端输入,发送给服务端
  process.stdin.on('data', data => {
    client.write(data)
  })
})

client.on('data', data => {
  console.log('服务端说:', data.toString())
})

构建udp服务

  • user Datagram protocol,简称udp,又称数据用户报协议
  • 和tcp一样,位于网络传输层用于传输数据包
  • udp的最大特点是无连接
  • udp的传输速度快
  • udp数据传输不可靠
    • 不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
    • 可靠性由应用层负责
  • 支持一对一通信,也支持一对多通信
  • 许多关键的互联网应用程序使用UDP
    • 如DNS域名系统服务,TFTP简单文件传输协议,DHCP动态主机设置协议等
  • udp适用于对速度要求比较高,对数据质量要求不严谨的应用
    • 例如流媒体、实时多人游戏、实时音视频

image.png

image.png

image.png

单播

server.js

const dgram = require('dgram');
const server = dgram.createSocket('udp4');

server.on('listening', () => {
  const address = server.address();
  console.log(`server running ${address.address} ${address.port}`)
})

server.on('message', (msg, remoteInfo) => {
  console.log(`server get ${msg} from ${remoteInfo.address}:${remoteInfo.port}`)
  // 给客户端回复一条消息
  server.send('word', remoteInfo.port, remoteInfo.address)
})

server.on('error', err => {
  console.log('server error', err)
})

server.bind(3000)

client.js

const dgram = require('dgram');
const client = dgram.createSocket('udp4');

// 客户端给localhost主机的3000端口发一条消息
// 如果绑定了端口,就不能直接在这里发消息了,需要在listening回调里发送
client.send('hello', 3000, 'localhost')

client.on('listening', () => {
  const address = client.address();
  console.log(`client running ${address.address} ${address.port}`)
})

client.on('message', (msg, remoteInfo) => {
  console.log(`client get ${msg} from ${remoteInfo.address}:${remoteInfo.port}`)
})

client.on('error', err => {
  console.log('client error', err)
})

// 不绑定端口号的时候,会自动分配一个(如果需要固定一个端口号,就绑定一个端口号)
// client.bind(3000)

广播

server.js

const dgram = require('dgram');
const server = dgram.createSocket('udp4');

server.on('listening', () => {
  const address = server.address();
  console.log(`server running ${address.address} ${address.port}`)
  server.send('hello', 8000, '255.255.255.255')
  // 开启广播模式
  server.setBroadcast(true)
  // 每隔两秒,发送一条广播消息
  setInterval(() => {
    // 直接地址, 192.168.10.255 可以经过路由器转发,直接广播更精确,范围更小,在指定网段内广播
    // 受限地址 255.255.255.255  受限地址不会经过路由转发,在当前路由器局域网中进行传播(在当前局域网中的所有电脑,只要监听8000端口,就会收到消息)
    server.send('hello', 8000, '255.255.255.255')
  }, 2000);
})

server.on('message', (msg, remoteInfo) => {
  console.log(`server get ${msg} from ${remoteInfo.address}:${remoteInfo.port}`)
  // 给客户端回复一条消息
  server.send('word', remoteInfo.port, remoteInfo.address)
})

server.on('error', err => {
  console.log('server error', err)
})

server.bind(3000)

client.js

const dgram = require('dgram');
const client = dgram.createSocket('udp4');

client.on('message', (msg, remoteInfo) => {
  console.log(`client get ${msg} from ${remoteInfo.address}:${remoteInfo.port}`)
})

client.on('error', err => {
  console.log('client error', err)
})
// 绑定8000端口
client.bind(8000)

组播

server.js

const dgram = require('dgram');
const server = dgram.createSocket('udp4');

server.on('listening', () => {
  const address = server.address();
  console.log(`server running ${address.address} ${address.port}`)
  setInterval(() => {
    // 组播地址 224.0.1.100
    server.send('hello', 8000, '224.0.1.100')
  }, 2000);
})

server.on('message', (msg, remoteInfo) => {
  console.log(`server get ${msg} from ${remoteInfo.address}:${remoteInfo.port}`)
  // 给客户端回复一条消息
  server.send('word', remoteInfo.port, remoteInfo.address)
})

server.on('error', err => {
  console.log('server error', err)
})

server.bind(3000)

client.js

const dgram = require('dgram');
const client = dgram.createSocket('udp4');

client.on('listening', () => {
  const address = client.address();
  console.log(`client running ${address.address} ${address.port}`)
  // 加入组播组
  client.addMembership('224.0.1.100')
})

client.on('message', (msg, remoteInfo) => {
  console.log(`client get ${msg} from ${remoteInfo.address}:${remoteInfo.port}`)
})

client.on('error', err => {
  console.log('client error', err)
})

// 绑定8000端口
client.bind(8000)

构建http服务

tcp和udp都属于网络传输层协议,如果要构建高效的网络应用,就应该从传输层进行着手,但是对于经典的浏览器网页和服务器端通信场景,如果单纯的使用更底层的传输层协议则会变得麻烦。

所以对于经典的BS(browser server)通信,基于传输层之上,专门制定了更上一层的通信协议: http,用于浏览器和服务器端进行通信,由于http协议本身并不考虑数据如何传输及其他细节问题,所以属于应用层协议。

node提供了基本的http和https模块,用于http和https的封装。

image.png

image.png

image.png server.js

const http = require('http')
const fs = require('fs');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  fs.readFile('./index.html',(err, data) => {
    if(err) {
      res.statusCode = 404;
      res.end('404 not found')
      return;
    }
    res.statusCode = 200;
    res.end(data)
  })
})

server.listen(port,hostname,() => {
  console.log(`server running at http://${hostname}:${port}`)
})