node提供了net、dgram、http、https4个模块,分别用于处理tcp、udp、http、https,适用于服务器端和客户端。
node网络通信概念
网络7层模型
也有人把7层模型简化为5层模型
物理层:
电缆、光纤等,传输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服务
简单的服务端:
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适用于对速度要求比较高,对数据质量要求不严谨的应用
-
- 例如流媒体、实时多人游戏、实时音视频
单播
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的封装。
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}`)
})