socket
socket,中文名称套接字,可以将其理解成一个抽象层,用于在网络上执行进程间的通信。它为应用程序提供了发送和接收数据的机制,通过ip和接口来标识网络中唯一的位置
套接字拥有三种类型:
- 流套接字
使用 TCP进行面向连接的可靠通信。它保证数据的顺序性和完整性 - 数据报套接字
使用UDP进行面向无连接的通信。它不保证数据的顺序和完整性,但效率较高 - 原始套接字
允许直接访问底层协议,如 IP 层或以太网层。通常用于开发新的协议或对现有协议进行测试
我们平时最常用的就是基于TCP的流套接字,websocket也是基于此
socket最大的特点就是服务器和客户端可以任意进行通信,通信内容不限制于格式,也不限于发送和接收方,除非某一方主动断开,否则socket会一直保持连接状态
创建scoket
我们可以使用不同的语言来创建和监听socket,如在 C 语言中使用socket() 函数,在Python 中使用socket.socket()方法,以下是在node中创建一个scoket的示例
const net = require('net');
const server = net.createServer((socket) => {console.log('客户端已连接');socket.on('data', (data) => {console.log('收到客户端消息:', data.toString());socket.write('服务器已收到你的消息: ' + data.toString());});socket.on('end', () => {console.log('客户端已断开连接');});
});
const PORT = 65432;
const HOST = '127.0.0.1';
server.listen(PORT, HOST, () => {console.log(`服务器正在监听 ${HOST}:${PORT}`);
});
我们使用Node.js的net模块创建一个简单的TCP服务器,通过这个TCP服务器我们就能实现scoket的功能
服务器与客户端通信
轮询与长连接
在web早期,如果我们想要实现客户端定时向服务器获取某些数据时,客户端最常用的几个方法分别是轮询和长连接
-
轮询
轮询是指客户端以固定的时间间隔向服务器发送请求,询问是否有新数据或状态更新,服务器每次接收到请求后都会返回当前的状态或数据,相比于其他实现,轮询有以下特点- 轮询机制相对简单,易于实现和理解。
- 由于请求是按固定时间间隔发送的,所以数据存在一定的延迟
- 服务器的数据更新的可能并没有那么频繁,频繁的通过轮询来请求数据可能会增加服务器负担
以下是一个通过js实现的简单轮询机制
function pollServer() {setInterval(() => {fetch('http://test.com/api/test').then(response => response.json()).then(data => {console.log(data);})}, 5000) } pollServer();
-
长连接
长连接是一种保持客户端和服务器之间的连接在一段时间内持续有效的通信方式。在请求响应期间,服务器和客户端会一直保持连接,直到服务器将响应结果返回
相比于轮询,长连接的链接次数大大小于轮询,但每开启一个长连接,服务器就会开启一个相应的线程,这个线程会一直处于阻塞状态,直到服务器返回了相应数据
这两种方法要么数据的实时性无法保证,要么占用服务器资源过多,所以在很长一段时间内都没有一个很好的解决方案
websocket
我们上面介绍过了scoket,事实上浏览器本身就支持socket通信,浏览器并没有对应的api来实现,直到html5推出,与其一齐推出的还有webscoketAPI
webscoket协议本身出现时间比html还要早,但直到html5推出之后才有相关实现,webscoket基于scoket,所以也能提供和scoket一样的服务器和客户端相互通信,但和scoket区别在于,webscoket发送数据时必须遵守websocket协议规范
如果想要开启一个webscoket通信,浏览器首先将会发送一个特定格式的http请求,服务器接受后会返回一个特定格式的http响应
以下是一个简单的webscoket通信示例
服务器
const net = require('net');
const crypto = require('crypto');
function generateAcceptValue(key) {return crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', 'binary').digest('base64');
}
const server = net.createServer((socket) => {console.log('客户端已连接');socket.once('data', (data) => {let parts = data.toString().split("\r\n")parts.shift()parts = parts.filter((s) => s).map((s) => {const i = s.indexOf(":")return [s.slice(0, i), s.slice(i + 1).trim()]})const head = Object.fromEntries(parts)const key = generateAcceptValue(head['Sec-WebSocket-Key']);socket.write(`HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: ${key}`);socket.on("data", (chunk) => {console.log(chunk);});});
});
const PORT = 65432;
const HOST = '127.0.0.1';
server.listen(PORT, HOST, () => {console.log(`服务器正在监听 ${HOST}:${PORT}`);
});
客户端
<!DOCTYPE html>
<html><head><title>WebSocket 客户端</title>
</head><body><button id="sendButton">发送消息</button><div id="messages"></div><script>const ws = new WebSocket('ws://localhost:65432');const sendButton = document.getElementById('sendButton');const messagesDiv = document.getElementById('messages');ws.onopen = () => {console.log('已连接到服务器');};ws.onclose = () => {console.log('已断开与服务器的连接');};sendButton.addEventListener('click', () => {const message = '你好,服务器!';console.log('向服务器发送消息:', message);ws.send(message);});</script>
</body></html>
这是当我们在客户端发送了四次消息给服务器之后的界面
可以看到服务器接受到了消息,但消息是二进制形式的,因为webscoket对于数据通信是有着严格要求的,我们必须对数据进行处理才行
具体实现代码如下
function parseMessage(buffer) {const secondByte = buffer[1];const length = secondByte & 127;let offset = 2;if (length === 126) {offset += 2;} else if (length === 127) {offset += 8;}const masks = buffer.slice(offset, offset + 4);offset += 4;const message = buffer.slice(offset).map((byte, i) => byte ^ masks[i % 4]).toString();return message;
}
我们将得到的chunk传入这个函数,这个函数就会将数据进行处理并返回
最终结果如下