通讯的必要条件
- 主机之间需要有传输介质。光纤、蓝牙、wify
- 主机上必须有网卡设备。把二进制信息转为高低电压的过程就是数据的调制过程。把电信号转为二进制信息的过程为解调制。
- 主机之间需要协商网络速率。
网路的通讯方式
日常生活中,我们通讯的方式不可能只有两台主机,如何建立多台主机互连,假如要与十台电脑通讯,那么不可能去买一台有十个网卡的主机,所以这个时候就可以通过交换机
和路由器
完成这个事情。
如何建立多台主机互连?如何定位局域网中的其它主机?
通过交换机我们可以将多台电脑进行连接。
a主机将消息发送给交换机,交换机将消息广播给其他主机,在这个过程中其他机器都会收到这条消息,然后b主机就通过MAC地址发现a想找的是自己,而其他的主机就会将这条消息当作垃圾丢弃。这个过程就是通过交换机来完成局域网通信的操作。
通过交换机来完成局域网通信有哪些问题?
通过交换机来完成局域网通信是无法满足互联网的需求。比如说我们不可能将所有电脑都放在一个局域网内,因为交换机的接口数量是有上限的,而且如果我们把所有机器放在一个局域网里,那么这个时候广播风暴就会尤为明显!因为任意一条消息的发送都会被其他主机所接收,然后还要确认是否有效。基于这种情况,网路就被分为许多较小的局域网,然后就是城域网,最后是互联网。
如何解决?
我们现在能访问百度,但是百度并不在局域网内,所以我们与百度的主机分属于不同的局域网,那这两个局域网之间该如何建立通讯呢?这里我们就需要用来路由器
来进行数据的交换了。首先我们的电脑要去访问百度的主机,我们不仅需要知道百度的网卡地址
,还需要知道它在哪个网络里,这块就需要用到ip地址
了。这里要说明的是,我们数据在发送的时候不仅只有程序代码所接收到的数据,还有原mac地址、原ip地址和目标mac地址、目标ip地址
等等。这些内容呢首先会被组装成一条大的数据,之后再发送到路由器上,路由器是可以识别当前要找的主机它在不在这个局域网内。如果说它不在当前的局域网内,这个时候他会按照路由表上的信息去帮助我们通过网络路由来找到对应的网络,之后再去把数据发送过去。发送到对应的网络之后,由那边的交换机完成定位功能。那这样的话我们就通过了路由器实现了不同网络之间的主机通讯。
网络层次模型
对于网络通信来说它是一个复杂的工程,它需要有很多的基础硬件设备,而这些设备又有很多的厂商在进行生产,所以为了方便网络的实施与管理维护就会有相应的组织来推出相应的标准规范 。目前最常见的就是OSI七层模型和TCP、IP的四层模型。TCP、IP是建立在OSI之上的另外四种模型。无论我们采用的哪一种模型,它其实都是对通信的过程进行分层,然后每层当中也存在很多的协议。例如我们常用的http就属于应用层的协议,而tcp和udp就属于传输层的协议。下面就以OSI为例来介绍它的七层分层以及每一次做了哪些事情。它的作用就是更加清晰规范的完成网络通讯。
这里主要掌握分层的目的,七层的名称和作用,还有就是一些常见的协议名称
OSI七层模型
- 应用层: 用户与网络的接口
- 利用http协议完成网站服务
- 利用ftp协议完成文件的传输服务
- 利用ssh协议完成远程登陆服务
- 表示层: 数据加密、转换、压缩
- 会话层: 控制网络连接建立与终止
- 传输层:保证数据传输的可靠性
-
- 在封装的时候还会携带目标占用的端口号,例如访问网站的时候会携带80端口
- 网络层: 通过路由来找到目标网络
- 常见的就是ip协议,让我们根据ip地址来确定源和目标的网络
- 数据链路层:确定目标主机
- 这一层就是确定了目标网路并进入了某一个局域网内
- 物理层: 各种网络物理设备和标准
数据从A至B,先封装再解封
TCP IP四层模型
- 应用层
- 就是将OSI的应用层、表示层、会话层合并
- 传输层
- 就是OSI的传输层
- 主机层
- 就是OSI的网络层
- 接入层
- 就是将OSI的数据链路层、物理层合并
数据从A到B
- 按照分层自上向下一层一层封装
- 到了B主机的网卡协调之后,再按照自下向上的顺序进行拆解
- 最后在应用层里拿到A主机所发送过来的数据
数据封装与解封的过程
- 第一步在应用层产出需要使用的数据data
- 然后data数据就传向了传输层。在这一层对于我们来说最熟悉的就是TCP与UDP协议,这两个协议都是基于端口的,而端口的作用就是在主机上用于确定唯一一个应用进程。所以数据在这层就会包裹上目标应用端口和应用在当前主机上的源端口
- 数据再传到网络层,因为我们的主机是在不同的网络里的,所以需要通过IP协议来确定目标主机所在的网络。因此数据在这一层中会包裹目标主机的mac地址、目标主机的ip地址和当前主机的源ip地址。有了这些地址之后我们只是能确定某一个网络,并不能确定在这个网络里边那一台机器是我们想要的,所以接下来数据就到达了链路层
- 在这一层主要是通过mac地址来完成寻址操作,所以数据在这一层会被包裹目标主句的mac地址与当前主机的mac地址。至此一条具备完整信息的数据就封装ok了。
- 这个时候就要传递给网络了,而我们知道网线是不能识别二进制的,所以经过物理层(网卡)的调制之后就会变成高低电压,而我们这里任然是已二进制的数据来表示转换之后的数据,有了高低电压之后经过网络路由器的分配和传输介质的运输,最终就到达了目标主机的网卡。
- 这个时候它首先要去做数据的解调,将电信号变成二进制,然后再向上层传递至链路层
- 到达链路层之后它就去分析一下,目标的mac地址是不是当前主机的mac地址,如果是则继续向上传递至网络层
- 在这层就要去校验目标的ip是不是当前自己的ip,如果是,则继续拆包。向上传输至传输层
- 这里再次确定当前的端口是不是自己,如果是的话,则再次拆解数据,向上传递至应用层
- 到了应用层之后,当前网络里的目标应用就拿到了另外一个网络当中的某一台主机上的某一个应用所传递过来的数据。 这个过程就是数据在通讯过程中的封装与解封的步骤。
TCP三次握手的建立与四次挥手的过程
TCP协议
- TCP 用于处理实时通信
- TCP是基于端口,面向连接的传输层协议
- TCP的握手和挥手本质上都是四次,只不过握手合并成三次。因为主机之间要想通信需要先建立双向数据通道
- TCP的主要特点是传输稳定性高,但是在传输效率上相对于UDP要低
创建控制字段
- SYN = 1 表示当前主机请求建立连接
- FIN = 1 表示当前主机请求断开连接
- ACK = 1 表示数据信息确认
TCP三次握手
- 这里以CS网络架构为例,第一次客户端向服务端发送一个建立连接的请求(用SYN=1表示)
- 服务端接收到这个请求之后回送一条消息表示接收到了这个请求(用ACK=1表示)。到这一步就建立了一条由客户端向服务端的连接通道,而服务端要向客户端建立通道,同样需要建立连接。
- 服务端发送一个请求给客户端,表示它也想建立一个连接(同样用SYN=1来表示)
- 同样客户端也需要回送一个消息给服务端,表示接收到了这个请求(同样用ACK=1来表示)这四次连接发生之后就有了一个客户端与服务端进行数据通信的双向通道。
不过这里看起来是四次握手,不是三次握手。本身来说应该是四次握手,只不过在实际处理的时候,服务端在回复客户端ACk=1的时候,同时再发送一个SYN=1.也就是将这两次握手合并,这就得到了最终的三次握手。
TCP四次挥手
- 首先客户端发送一个断开连接的请求给服务端
- 然后服务端回复一个消息确认。这个时候就相当于断开了客户端到服务端的通道
- 接着服务端发送一个断开请求给客户端
- 客户端也回复一个确认消息给服务端。这样就断开了服务端到客户端的请求。 那么为什么不跟握手一样将服务端的确认客户端的断开回复与服务端请求与客户端断开请求合并呢?道理也很简单,
一个服务端会服务于多个客户端,我们不能保证某一个客户端将请求发送给服务端之后服务端就能立即的将结果数据全部传输给客户端。
也就是说客户端的确已经把所有的数据都发给了服务端,但是服务端还没有将客户端想要的数据都全部传回,这个时候掐断连接自然不合理。所以就需要服务端在确认自己已经把数据发送完成之后再主动发起一次断开连接的请求给客户端
。因此挥手必须是要有四次,而握手就可以合并为三次。
创建TCP通信
Net模块实现了底层通信接口
通信过程
- 创建服务端:接收和回写客户端数据
- 创建客户端:发送和接收服务端数据
- 数据传输: 内置服务事件和方法读写数据
通信事件
- listing事件:调用server.listen方法之后触发
- connection事件:新的连接建立时触发
- close事件: 当server关闭时触发
- error事件: 当错误出现时触发
通信事件&方法
- data事件:当接收到数据时触发该事件
- 每当某一端调用write方法来发送数据的时候,那么另一端就可以通过on来监听data事件去消耗数据。其实也就是从可读流里拿数据的操作
- write方法:在socket上发送数据,默认ut8编码
- 它和data方法是相对的,net模块所创建的都是基于流的操作,所以他本身就是可读流和可写流的集合。data可以去消费数据,write就可以用来写入数据。
- end操作:当socket的一端发送FIN包时触发,结束可读端
基于net创建服务端和客户端的TCP通信
自此我们就知道了,在node中通过net模块可以创建一个基于流操作的tcp通信,依据内置的方法可以创建一个服务端和一个客户端然后再去监听具体的事件,当某些事件被触发的时候,我们就可以去利用相应的方法来生产和消费数据。
- server.js
const net = require('net')// 创建服务端实例
const server = net.createServer()const PORT = 1234
const HOST = 'localhost'server.listen(PORT, HOST)server.on('listening', () => {console.log(`服务端已经开启在 ${HOST}: ${PORT}`)
})// 接收消息 回写消息
server.on('connection', (socket) => {socket.on('data', (chunk) => {const msg = chunk.toString()console.log(msg)// 回数据socket.write(Buffer.from('您好' + msg))})
})server.on('close', () => {console.log('服务端关闭了')
})server.on('error', (err) => {if (err.code == 'EADDRINUSE') {console.log('地址正在被使用')}else{console.log(err)}
})
- client.js
const net = require('net')const client = net.createConnection({port: 1234, host: '127.0.0.1'
})client.on('connect', () => {client.write('小星星')
})client.on('data', (chunk) => {console.log(chunk.toString())
})client.on('error', (err) => {console.log(err)
})client.on('close', () => {console.log('客户端断开连接')
})
TCP数据粘包
通信包含数据发送端和接收端
发送端累积数据统一发送.
接收端缓冲数据之后再消费
TCP拥塞机制决定发送时机
数据的封包与拆包
数据传输的过程
- 进行数据编码,获取二进制数据包
- 按规则拆解数据,获取指定长度的数据
Buffer数据读写
- writeInt16BE:将value从指定位置写入
- readInt16BE:从指定位置开始读取数据