1、概念
是一种在单个TCP连接上进行全双工通信的协议。websocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
- 单工通信:数据传输只允许在一个方向上传输,只能一方发送数据,另一方接收数据并发送。
- 半双工:数据传输允许两个方向上的传输,但在同一时间内,只可以有一方发送或接收数据。
- 全双工:同时可进行双向数据传输。
2、背景
客户端发送请求,服务器响应后即断开连接,HTTP 协议无法实现服务器主动向客户端发起消息,因此无法支持实时性要求高的应用场景,比如在线游戏、即时聊天、股票行情等。
一般都是使用轮询技术,轮询是在特定的时间间隔,由浏览器对客户端发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然后HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的宽带等资源。
在这种情况下,HTML5定义了websocket协议,能更好的节省服务器资源和宽带,而且能够更实时地进行通讯。
轮询技术:
短轮询:setInterval,每隔一段时间客户端向服务端请求一次,一次请求就是一次http请求,都会伴随一次tcp连接,即三次握手,四次挥手。这样的弊端一个是会产生大量无意义的请求。还有就是频繁地打开关闭连接,还有实时性并不高。
长轮询:客户端向服务端发起请求,如果没有消息服务端就不响应,等有了消息在响应,然后客户端再次请求。这样的话请求都变得有意义了。弊端:客户端长时间得不到响应会导致超时,从而主动断开和服务器的连接(可以解决:ajax若因超时而结束,立即重新发送请求到服务器)
3、实现原理
WebSocket 协议(还是基于tcp协议,所以还会有三次握手,四次挥手),在三次握手和四次挥手之间会有一个websocket握手的过程,在这个websocket握手之后,服务器可以任意的给客户端发消息,客户端也可以随意给服务器发消息。工作原理如下:
握手阶段(Handshake):(先得发个http请求(固定以ws开头,还会有wss),但是是告诉服务器我们之后通信就不用http了,用websocket了)
- 客户端发起一个普通的 HTTP 或 HTTPS 请求到服务器,请求包含一些特殊的头部信息,如
Upgrade: websocket
,Connection: Upgrade,Sec-Websocket-Version: 13,Sec-Websocket-Key:(随机生成,会变化)
。 - 服务器收到请求后,如果支持 WebSocket,会返回一个 HTTP 101 状态码(切换协议),表示协议升级成功,升级成websocket,同时响应中也包含一些特殊的头部信息,如
Upgrade: websocket
,Connection: Upgrade,Sec-Websocket-Accept:(对应那个key生成的)
。 - 客户端和服务器之间的连接升级为 WebSocket 连接,之后所有的通信都是基于 WebSocket 协议进行。
- 客户端发起一个普通的 HTTP 或 HTTPS 请求到服务器,请求包含一些特殊的头部信息,如
数据传输阶段(Data Transfer):
- 一旦 WebSocket 连接建立成功,客户端和服务器之间可以随时相互发送数据。
- 数据以帧(Frame)的形式进行传输,每个帧包含一个或多个数据块,可以是文本或二进制数据。
- 客户端和服务器可以在任何时候发送数据,并且可以同时发送和接收数据,实现了真正的全双工通信。
连接关闭阶段(Connection Close):
- 当客户端或服务器决定关闭连接时,它们可以发送一个关闭帧来结束连接。
- 收到关闭帧的一方会响应一个关闭帧,然后双方都关闭连接。
4、优点
1)创建连接后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。
2)由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少。
缺点:
1)兼容性,h5才有
2)维持tcp连接需要耗费资源,消息比较少,那维持这个连接比较浪费资源
5、与HTTP相比
相同:
WebSocket和HTTP都是基于TCP/IP协议栈的应用层协议。
WebSocket和HTTP都是用于客户端和服务器之间的通信
不同:
1)通信方式:
HTTP:HTTP是一种单向的、无状态的通信协议,半双工。通常是客户端向服务器发起请求,服务器返回响应的模式。
WebSocket:WebSocket是一种全双工的通信协议,允许客户端和服务器之间同时发送和接收数据,实现了双向通信。
2)连接方式:
HTTP:HTTP是一种短连接的协议,每次通信都需要建立一个新的连接,通信结束后立即关闭连接。
WebSocket:WebSocket是一种长连接的协议,一旦建立连接,客户端和服务器之间可以保持长时间的通信,不需要频繁地建立和断开连接。
3)数据格式:
HTTP:HTTP通常使用文本格式来传输数据,如HTML、JSON等。
WebSocket:WebSocket支持传输任意格式的数据,可以是文本、二进制等。
4)实时性和效率:
HTTP:HTTP通常是基于请求-响应模式,因此在实时性和效率上有一定的限制,适用于普通的网页浏览和数据传输。
WebSocket:WebSocket是专门设计用于实时通信的协议,实时性和效率较高,适用于需要实时通信或推送消息的场景。
5)适用场景:
HTTP:适用于普通的网页浏览、数据传输等场景。
WebSocket:适用于实时聊天应用、在线游戏、实时数据展示等需要实时通信的场景
6、例子
1)使用原生websocket API
新建项目
新建两个文件夹
server
打开终端,执行npm init -y
安装npm i ws
新建server.js
终端执行node server.js
project
import React, { useState } from 'react';
const App = () => {
const [serverUrl, setServerUrl] = useState('');
const [messageInput, setMessageInput] = useState('');
const [messageLog, setMessageLog] = useState('');
const [ws, setWs] = useState();
const [imageInput, setImageInput] = useState(null);
const [imagePreview, setImagePreview] = useState(null);
/**
* onopen 连接建立时触发
onmessage 客户端接收服务端数据时触发
onerror 通信发生错误时触发
onclose 连接关闭时触发
onbeforeunload 当浏览器窗口关闭或者刷新时触发
*/
const connect = () => {
const nws = new WebSocket(serverUrl);
setWs(nws);
nws.onopen = () => {
logMessage('客户端连接服务端');
};
nws.onmessage = (event) => {
logMessage(`客户端接受消息: ${event.data}`);
};
nws.onclose = () => {
logMessage('Connection closed');
};
nws.onerror = (error) => {
logMessage(`WebSocket error: ${error}`);
};
};
const sendMessage = () => {
console.log(ws);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(messageInput);
setMessageInput('');
logMessage(`Sent: ${messageInput}`);
} else {
logMessage('WebSocket connection not open');
}
};
const logMessage = (message) => {
setMessageLog(prevLog => prevLog + message + '\n');
};
const sendMessageImage = () => {
//ws.readyState 0-正在连接中 1-已连接 2-正在关闭中 3-已关闭
if (ws && ws.readyState === WebSocket.OPEN) {
if (imageInput) {
const reader = new FileReader();
reader.onload = (event) => {
console.log(event.target);
ws.send(event.target.result);
setImagePreview(event.target.result);
logMessage(`Sent: ${event.target.result}`);
};
reader.readAsDataURL(imageInput);
} else {
setMessageLog((prevLog) => prevLog + 'Please select an image\n');
}
} else {
setMessageLog((prevLog) => prevLog + 'WebSocket connection not established\n');
}
};
const handleImageChange = (e) => {
const file = e.target.files[0];
setImageInput(file);
};
return (
<div>
<h1>WebSocket Test</h1>
<div>
<label htmlFor="serverUrl">WebSocket Server URL:</label>
<input
type="text"
id="serverUrl"
placeholder="ws://example.com:8080"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
<button onClick={connect}>Connect</button>
</div>
<div>
客户<textarea
id="messageInput"
placeholder="Type a message..."
value={messageInput}
onChange={(e) => setMessageInput(e.target.value)}
></textarea>
<button onClick={sendMessage}>Send Message</button>
</div>
<input
type="file"
id="imageInput"
onChange={handleImageChange}
/>
<button onClick={sendMessageImage}>Send Image</button>
<div>
<pre>{messageLog}</pre>
{imagePreview && <img src={imagePreview} alt="Image Preview" style={{ maxWidth: '100px' }} />}
</div>
</div>
);
};
export default App;
运行该项目
有一些问题:链接双方可以任何时候发送任何消息,不知道消息的含义是啥
在http中有path,固定那个接口获取的就是哪个数据
解决这个问题就是使用第三方库socket.io
2)socket.io
客户端和服务器双方事先约定好不同的事件,事件由谁监听,由谁触发,就可以有序管理消息了
const express = require('express');
const http = require('http');
const app = express();
const server = http.createServer(app);
const io = require('socket.io')(server, {
cors: {
origin: "http://localhost:3001",
methods: ["GET", "POST"]
}
});
// 用于存储当前在线用户的信息
const onlineUsers = {};
// 监听客户端连接事件
io.on('connection', (socket) => {
console.log('有新的连接');
// 监听客户端发送的用户名事件
socket.on('add user', (username) => {
// 将用户名与对应的 socket.id 存储起来
onlineUsers[socket.id] = username;
// 向所有客户端广播新用户加入的消息
io.emit('user joined', { username: username, numUsers: Object.keys(onlineUsers).length });
console.log(username + ' 加入了聊天室');
});
// 监听客户端发送的消息事件
socket.on('chat message', (msg) => {
console.log('收到消息:', msg);
// 将消息发送给所有连接的客户端(广播)
io.emit('chat message', { username: onlineUsers[socket.id], message: msg });
});
// 监听客户端断开连接事件
socket.on('disconnect', () => {
// 如果用户在加入聊天室后断开连接,则发送断开连接消息
if (onlineUsers[socket.id]) {
const username = onlineUsers[socket.id];
delete onlineUsers[socket.id];
// 向所有客户端广播用户离开的消息
io.emit('user left', { username: username, numUsers: Object.keys(onlineUsers).length });
console.log(username + ' 离开了聊天室');
}
});
});
// 服务启动
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
import React, { useState, useEffect } from 'react';
import io from 'socket.io-client';
const App = () => {
const [socket, setSocket] = useState(null);
const [username, setUsername] = useState('');
const [message, setMessage] = useState('');
const [messages, setMessages] = useState([]);
const [numUsers, setNumUsers] = useState(0);
useEffect(() => {
// 连接到服务器
const newSocket = io('http://localhost:3000');
setSocket(newSocket);
// 当组件卸载时断开连接
return () => newSocket.disconnect();
}, []);
// 监听新用户加入的消息
useEffect(() => {
if (socket) {
socket.on('user joined', ({ username, numUsers }) => {
setNumUsers(numUsers);
addMessage(`${username} 加入了聊天室`);
});
}
}, [socket]);
// 监听用户离开的消息
useEffect(() => {
if (socket) {
socket.on('user left', ({ username, numUsers }) => {
setNumUsers(numUsers);
addMessage(`${username} 离开了聊天室`);
});
}
}, [socket]);
// 添加一条新消息到消息列表
const addMessage = (msg) => {
console.log(msg);
setMessages(prevMessages => [...prevMessages, msg]);
};
// 发送消息
const sendMessage = () => {
if (message.trim() !== '') {
socket.emit('chat message', message);
setMessage('');
}
socket.emit('chat message', message);
setMessage('');
};
// 加入聊天室
const joinChatRoom = () => {
if (username.trim() !== '') {
socket.emit('add user', username);
}
};
return (
<div>
<h1>聊天室</h1>
<div>当前在线用户: {numUsers}</div>
<div>
{messages.map((msg, index) => {
console.log(msg);
return (
<div key={index}>{msg}</div>
)})}
</div>
<div>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} placeholder="输入用户名" />
<button onClick={joinChatRoom}>加入聊天室</button>
</div>
<div>
<input type="text" value={message} onChange={(e) => setMessage(e.target.value)} placeholder="输入消息" />
<button onClick={sendMessage}>发送消息</button>
</div>
</div>
);
};
export default App;