目录
WebSocket协议初探
Socket连接的建立过程
聊天室:node.js端
聊天室:web端
小结
WebSocket协议初探
一个基于TCP的通信协议
- 复用HTTP的握手
- 基于TCP传输协议
101切换协议
WebSocket连接之后,传输的都是二进制数据了
Socket连接的建立过程
- 观察浏览器/node.js端WebSocket连接的建立过程
- 观察对握手的处理和协议转换的过程
<!DOCTYPE html>
<html>
<body>
<h2>Chatroom</h2>
<div></div>
<label>say</label>
<input />
<button>send</button>
<script>
const client = new WebSocket('ws://chat.svc:8080')
// wss -> WebSocket Secure
// https - tls/ssl - tcp/ip
// wss - tls - tcp/ip
client.onopen = () => {
console.log('connection open')
}
</script>
</body>
</html>
const express = require('express')
const app = express()
const path = require('path')
const parseHeader = require('parse-headers') // 引用一个解析headers的库
const crypto = require('crypto') // node加密
app.get('/', (req, res) => {// 运行网页
res.sendFile(path.resolve(__dirname, 'handshake.html'))
})
app.listen(3000) // 前端服务占用端口
/* -- websocket server -- */
const net = require('net')
const server = net.createServer()
server.on('connection', socket => { // 有请求进来
// 网络插槽
socket.on('data', (buffer) => {// 监听数据来了没
const str = buffer.toString() // 第一次握手过来的字符串
console.log('---message ---') // 看一下客户端发送了几次消息,建立连接发送了2次
console.log(str)
const headers = parseHeader(str)
const sha1 = crypto.createHash('sha1') // 生成一个hash
// 重新计算sha1 + 一个公开的串 '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'跟浏览器对一下
sha1.update(headers['sec-websocket-key'] + '')
const acceptKey = sha1.digest('base64') // 验签
const response = `HTTP/1.1 101 Switching Protocols // 同意你Switching Protocols
Upgrade: websocket // 帮你转换协议websocket
Connection: Upgrade // 升级
Sec-Websocket-Accept: ${acceptKey}// http协议换行前面不能有空格,不然会误以为也是头部字符串
// http协议这里需要空行,才会结束body
`// 返回值
socket.write(response) // 返回给客户端
})
})
server.listen(8080)// 服务端占用端口
解析封包,位运算找到用户发送的消息payload,处理payload
聊天室:node.js端
观察用node.js端实现过程
思考:下载全部聊天记录应该如何实现?用另一个TCP/IP连接,socket或者http都可以,不用登录的那个连接,避免全部聊天记录过大,造成阻塞
const express = require('express')
const app = express()
const path = require('path')
app.use(express.static('static'))
app.get('/', (req, res) => {
res.sendFile(path.resolve(__dirname, "chatroom.html"))
})
app.listen(3000)
// 聊天室
const io = require('socket.io')(8080)// 服务端socket.io提供自己的方式
// 创建socket成本并不高,只是一个对象;计算,存储,日志,这些成本会很高
const users = new Map() // 全局对象 以socket方式建立一个Map是不可行 的,只适用于demo
// 聊天室全体消息
function broadcast(type, message, sender) { // sender发送者,系统消息为空
for(let socket of users.keys()) {
// 聊天室内每个连接都发一个
socket.send({type, message, sender})
}
}
io.on('connect', socket => { // 底层都处理好了,不用握手什么的了
// {type, message}
// linux文件的i/o模型 epoll 宏位数
// i/o wait 也比较耗性能
socket.on('message', data => { // 等消息进来
console.log('here123123123', data) // json
switch(data.type) {
case 'LOGIN': // 登录
users.set(socket, {name : data.name})// set给Map加值key=socket;value={name : data.name}
// 实现一个方法,聊天室全体消息
broadcast('LOGIN', `${data.name}加入了聊天`)
break
case 'CHAT': // 聊天
// 通过socket可以拿到用户登录信息,socket区分用户仅限于demo,socket一段时间后是会换连接的
// 实际上:用户校验,用户传token
const user = users.get(socket)
broadcast('CHAT', data.message, user.name)
break
}
})
})
聊天室:web端
<!DOCTYPE html>
<html>
<head>
<style>
#content {
padding :10px;
border : 1px solid #343434;
}
</style>
</head>
<body>
<h2>Chatroom</h2>
<div id='content'></div>
<label>say</label>
<input id='ipt' />
<button onclick="send()">send</button>
<script src="/socket.io.js"></script>
<script>
// 客户端socket.io
const socket = io('ws://chat.svc:8080')// 连接socket
const name = "user" + new Date().getTime() // 用户昵称
socket.send({ // 一进聊天室
type : 'LOGIN',
name
})
const contentDiv = document.getElementById('content')
socket.on('message', data => { // 服务端传过来的广播消息
const {message, sender} = data
let senderName = sender
if(!sender) {
senderName = "系统"
}
else if(sender === name) {
senderName = "我"
}
const div = document.createElement('div')
div.className = data.type.toLowerCase() // class名
div.innerHTML = `${senderName}: ${message}` // 将展示的消息
contentDiv.append(div)
})
const ipt = document.getElementById('ipt')
ipt.addEventListener('keyup', e => { // 监听按键
if(e.key === 'Enter') { // 回车键发送
send()
}
})
function send(){
const val = ipt.value
if(val === '') {
return
}
console.log(val)
socket.send({ // 发送消息
type : "CHAT",
message : val
})
ipt.value = "" // 清空输入框
ipt.focus() // 重新聚焦
}
</script>
</body>
</html>
小结
- WebSocket握手和协议转换的过程很【自然】
- socket(网络插座)为客户端/服务端提供通信机制