socket
socket 工作流程
- 客户端连接服务器(TCP/IP),三次握手,建立了连接通道
- 客户端和服务器通过 socket 接口发送消息和接收消息(需要手动解析 http 协议,和流的操作),任何一端在任何时候,都可以向另一端发送任何消息
- 有一端断开了,通道销毁
http
http 工作流程
- 客户端连接服务器(TCP/IP),三次握手,建立了连接通道
- 客户端发送一个 http 格式的消息(消息头 消息体),服务器响应 http 格式的消息(消息头 消息体)
- 客户端或服务器断开,通道销毁
实时性的问题
轮询
长连接
WebSocket
HTML5 新增
专门用于解决实时传输的问题
WebSocket 工作流程
客户端连接服务器(TCP/IP),三次握手,建立了连接通道
客户端发送一个 http 格式的消息(特殊格式),服务器也响应一个 http 格式的消息(特殊格式),称之为 http 握手
双发自由通信,通信格式按照 WebSocket 的要求进行
客户端或服务器断开,通道销毁
服务端的握手响应
-
在 WebSocket 的 http 握手阶段,服务器响应头中需要包含如下内容:
# 升级成 websocket 协议 Upgrade: websocket # 连接方式:升级的方式 Connection: Upgrade # 从客户端发来的 Sec-WebSocket-Accept,服务端对它加密后再返还给客户端(用于通信) Sec-WebSocket-Accept: [key]
-
其中
Sec-WebSocket-Accept
的值来自于以下算法:// 对客户端传来的 Sec-WebSocket-Key 进行 sha1 加密,再对加密后的结果拼接固定字符串,最后进行 base64 编码 base64(sha1(Sec-WebSocket-Key) + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
-
在
node
中可以使用以下代码获得:const crypto = require("crypto"); const hash = crypto.createHash("sha1"); // requestKey 来自于请求头中的 Sec-WebSocket-Key hash.update(requestKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); const key = hash.digest("base64");
WebSocket 原理
-
index.html 客户端
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <button>发送数据到服务器</button> <script> // 客户端(浏览器)websocket const ws = new WebSocket("ws://localhost:5008"); // 创建一个websocket,同时,发送连接到服务器 ws.onopen = function () { // http握手完成 console.log("连接已建立"); }; ws.onmessage = function (e) { console.log("来自服务器的数据", e.data); }; ws.onclose = function () { console.log("通道关闭"); }; document.querySelector("button").onclick = function () { ws.send("123"); }; // ws.close(); //客户端主动断开连接 </script> </body> </html>
-
index.js 服务端
const net = require("net"); const server = net.createServer((socket) => { console.log("收到客户端的连接"); socket.once("data", (chunk) => { // 解析 http 协议的请求头信息 const httpContent = chunk.toString("utf-8"); let parts = httpContent.split("\r\n"); parts.shift(); parts = parts .filter((s) => s) .map((s) => { const i = s.indexOf(":"); return [s.substr(0, i), s.substr(i + 1).trim()]; }); // 加密 Sec-WebSocket-Key const headers = Object.fromEntries(parts); const crypto = require("crypto"); const hash = crypto.createHash("sha1"); hash.update( headers["Sec-WebSocket-Key"] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ); const key = hash.digest("base64"); /** * 设置了响应头 */ socket.write(`HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: ${key} 这是响应体内容 `); socket.on("data", (chunk) => { // 接收数据,流的方式 console.log(chunk); }); }); }); server.listen(5008);
socket.io
案例:在线聊天室
消息规格
# --- 客户端发送 ---
## 获取当前所有在线用户
消息名称:users
消息内容:无
## 登录
消息名称:login
消息内容:用户名
## 消息
消息名称:msg
消息内容:`{to:"目标用户名,null表示所有人", content:"消息内容"}`
# --- 服务器发送 ---
## 获取当前所有在线用户
消息名称:users
消息内容:用户数组
## 登录
消息名称:login
消息内容:true 或 false,true表示登录成功,false表示登录失败(昵称已存在)
## 新用户进入
消息名称:userin
消息内容:用户名
## 用户离开
消息名称:userout
消息内容:用户名
## 新消息来了
消息名称:new msg
消息内容:`{from:"用户名", content:"消息内容", to:"接收消息的人,如果是null,表示所有人"}`
客户端实现
-
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>在线聊天室</title> <link rel="stylesheet" href="./css/global.css" /> <link rel="stylesheet" href="./css/index.css" /> </head> <body> <div class="login"> <div class="form"> <h1>请输入你的昵称,回车后进入聊天室</h1> <input type="text" /> </div> </div> <div class="chat" style="display: none;"> <div class="user-list"> <div class="title">在线人数:<span>0</span></div> <ul class="users"> <li class="all">所有人</li> </ul> </div> <div class="main"> <ul class="chat-list"> <li class="log">欢迎来到聊天室</li> </ul> <div class="sendmsg"> <span class="gray">对</span> <span class="user">所有人</span> <span class="gray">说:</span> <input type="text" /> </div> </div> </div> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.0/jquery.min.js"></script> <script src="js/ui.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/socket.io/2.3.0/socket.io.js"></script> <script src="./js/chat.js"></script> </body> </html>
-
chat.js
const socket = io.connect(); // 客户端发送消息给服务器 page.onLogin = function (username) { socket.emit("login", username); }; page.onSendMsg = function (me, msg, to) { socket.emit("msg", { to, content: msg, }); page.addMsg(me, msg, to); page.clearInput(); }; // 客户端监听服务器消息 socket.on("login", (result) => { if (result) { page.intoChatRoom(); socket.emit("users", ""); } else { alert("昵称不可用,请更换昵称"); } }); socket.on("users", (users) => { page.initChatRoom(); for (const u of users) { page.addUser(u); } }); socket.on("userin", (username) => { page.addUser(username); }); socket.on("userout", (username) => { page.removeUser(username); }); socket.on("new msg", (result) => { page.addMsg(result.from, result.content, result.to); });
服务端实现
-
index.js
const express = require("express"); const http = require("http"); const path = require("path"); // express const app = express(); const server = http.createServer(app); app.use(express.static(path.resolve(__dirname, "public"))); // websocket require("./chatServer")(server); // 监听端口 server.listen(5008, () => { console.log("server listening on 5008"); });
-
chatServer.js
const socketIO = require("socket.io"); let users = []; module.exports = function (server) { const io = socketIO(server); io.on("connection", (socket) => { let curUser = ""; //当前用户名 // 监听客户端消息 socket.on("login", (data) => { if ( data === "所有人" || users.filter((u) => u.username === data).length > 0 ) { //昵称不可用 socket.emit("login", false); } else { // 昵称可用 users.push({ username: data, socket, }); curUser = data; socket.emit("login", true); // 新用户进入了 socket.broadcast.emit("userin", data); } }); socket.on("users", () => { const arr = users.map((u) => u.username); socket.emit("users", arr); }); socket.on("msg", (data) => { if (data.to) { // 发送给指定的用户 const us = users.filter((u) => u.username === data.to); const u = us[0]; u.socket.emit("new msg", { from: curUser, content: data.content, to: data.to, }); } else { // 发送给所有人 socket.broadcast.emit("new msg", { from: curUser, content: data.content, to: data.to, }); } }); socket.on("disconnect", () => { socket.broadcast.emit("userout", curUser); users = users.filter((u) => u.username !== curUser); }); }); };
TypeScript👉 声明文件
上一篇