如何在 express 中建立 websocket 介面以及一些相關問題的處理
大多數文章裡的方法是直接安裝 express-ws
然後進行使用:
const express = require(`express`)
const server = express()
const expressWs = require(`express-ws`)
expressWs(server)
server.ws(`/aaa`, (ws, req) => {
console.log(`socket`, req.testing)
ws.send(`aaa`)
ws.on(`message`, function(msg) {
console.log(msg)
})
})
server.get(`/bbb`, (req, res) => {
res.json(`bbb`)
})
server.listen(3040 )
可以看到在上面建立了一個 ws://127.0.0.1:3040/aaa
這樣的 websoket 介面, 然而當我們嘗試 new WebSocket("ws://127.0.0.1:3040/ccc")
時, 也能連線上, 這雖然不會影響我們的業務, 但沒有建立的介面能連上, 顯然是很不好的.
在 express-ws 的 github 倉庫和問題列表中看了很久沒有找到解決方法, 然後就去看它的程式碼. 發現程式碼不多, 卻拆分了很多檔案, 然而程式碼沒多少我卻很難看懂.
整理 express-ws 原始碼
然後先合併一些分散在各個檔案裡的程式碼和註釋:
function expressWs(app, httpServer, options = {}) {
addWsMethod(app)
const server = http.createServer(app)
app.listen = function serverListen(...args) {
return server.listen(...args)
}
const wsServer = new ws.Server({server})
wsServer.on(`upgrade`, async (req, socket, upgradeHead) => {
const res = new http.ServerResponse(req)
return server(req, res)
})
wsServer.on(`connection`, (socket, request) => {
if (`upgradeReq` in socket) {
request = socket.upgradeReq
}
request.ws = socket
request.wsHandled = false
request.url = websocketUrl(request.url)
const dummyResponse = new http.ServerResponse(request)
dummyResponse.writeHead = function writeHead(statusCode) {
if (statusCode > 200) {
dummyResponse._header = `` // eslint-disable-line no-underscore-dangle
socket.close()
}
}
app.handle(request, dummyResponse, () => {
if (!request.wsHandled) {
socket.close()
}
})
})
}
function trailingSlash(string) {
let suffixed = string
if (suffixed.charAt(suffixed.length - 1) !== `/`) {
suffixed = `${suffixed}/`
}
return suffixed
}
function websocketUrl(url) {
if (url.indexOf(`?`) !== -1) {
const [baseUrl, query] = url.split(`?`)
return `${trailingSlash(baseUrl)}.websocket?${query}`
}
return `${trailingSlash(url)}.websocket`
}
function addWsMethod(target) {
if (!target.ws) {
target.ws = function (route, ...middlewares) {
const wrappedMiddlewares = middlewares.map((function (middleware) {
return (req, res, next) => {
if (req.ws) {
req.wsHandled = true
try {
middleware(req.ws, req, next)
} catch (err) {
next(err)
}
} else {
next()
}
}
}))
const wsRoute = websocketUrl(route)
this.get(...[wsRoute].concat(wrappedMiddlewares))
return this
}
}
}
把原始碼整理出來, 看起來確實沒多少. 然而還是有些難看懂, 並且沒有解決上面所說的沒有建立的介面也會連線的問題, 那麼我整理原始碼有何用?
拿著原始碼改了半天, 還是沒有解決. 所以, 再去一下層看看能不能找到方法.
於是就來到了 express-ws
使用的 ws
, 看 ws
的使用量和活躍度都十分可觀, 應該能解決我的問題, 如果不能解決, 就是我的問題~~~
摒棄 express-ws
先脫離 express-ws
自身的那個思想, 仔細看了半天文件, 看到 readme 裡有有個示例: 演示了從 ws 連線時如何根據路由分發到不同的 ws 服務.
import { createServer } from 'http';
import { parse } from 'url';
import { WebSocketServer } from 'ws';
const server = createServer();
const wss1 = new WebSocketServer({ noServer: true });
const wss2 = new WebSocketServer({ noServer: true });
wss1.on('connection', function connection(ws) {
// ...
});
wss2.on('connection', function connection(ws) {
// ...
});
server.on('upgrade', function upgrade(request, socket, head) {
const { pathname } = parse(request.url);
if (pathname === '/foo') {
wss1.handleUpgrade(request, socket, head, function done(ws) {
wss1.emit('connection', ws, request);
});
} else if (pathname === '/bar') {
wss2.handleUpgrade(request, socket, head, function done(ws) {
wss2.emit('connection', ws, request);
});
} else {
socket.destroy();
}
});
server.listen(8080);
分發服務例子裡有了演示, 但是能不能做到 express 那樣通過 app.ws('/a')
app.ws('/b')
的形式進行分發呢? 能不能實現像 express 一樣支援路徑引數呢?
當前 express-ws
是沒有支援路徑引數的, 並且已經有一年沒有活躍了.
自己思考實現 express-ws 並解決它存在的問題
首先 app.ws 可以直接在 express 例項是通過 app.ws = fn
的實現. 然後 ws 對應的不同路由對應到不同的處理方法如何實現呢?
那就先以路由為 key, 然後處理方法為 value 儲存起來, 到時候根據訪問的 url 查詢對應的 key/value 執行就行.
如何根據 url 查詢 key? 我們在 ws 提供的示例中發現在 server.on('upgrade')
這一層可以進行查詢和分發, 然後好像思路上沒什麼大問題, 測試了一下確實可以.
以下是程式碼:
function expressWs(app) {
const server = http.createServer(app)
server.on(`upgrade`, (req, socket, head) => {
const obj = app.wsRoute[req.url]
obj ? obj.wss.handleUpgrade(req, socket, head, ws => obj.mid(ws, req)) : socket.destroy()
})
app.listen = (...arg) => server.listen(...arg)
app.ws = (route, mid) => {
app.wsRoute = app.wsRoute || {}
app.wsRoute[route] = {
wss: new Server({ noServer: true }),
mid,
}
}
}
使用方式依然是:
const express = require(`express`)
const server = express()
expressWs(server)
server.ws(`/aaa`, (ws, req) => {
console.log(`socket`, req.testing)
ws.send(`aaa`)
ws.on(`message`, function(msg) {
console.log(msg)
})
})
server.get(`/bbb`, (req, res) => {
res.json(`bbb`)
})
server.listen(3040)
測試了一下連線不存在的 ws 介面, 能按預期工作, 不存在就連線不上, 存在的能連線上, http 介面也能正常工作.
但是, 我的程式碼好像更少呢~~~
接下來要實現其他的功能就很簡單了: 例如
- 支援 express 的路由模式, 例如
/ws/:id
- 支援讀取 params, query 引數