nodejs-websocket介紹

websocket 是一種網(wǎng)絡(luò)通信協(xié)議,一般用來(lái)進(jìn)行實(shí)時(shí)通信會(huì)使用到

為什么要用 websocket

websocket 協(xié)議和 http 協(xié)議類似,http 協(xié)議有一個(gè)缺陷,只能由客戶方端發(fā)起請(qǐng)求,服務(wù)端根據(jù)請(qǐng)求 url 和傳過(guò)去的參數(shù)返回對(duì)應(yīng)結(jié)果

websocket 是雙向通信的,只要 websocket 連接建立起來(lái),可以由客戶端給服務(wù)端發(fā)送數(shù)據(jù),也可以由服務(wù)端主動(dòng)給客戶端發(fā)送數(shù)據(jù)

websocket 適用場(chǎng)景:聊天室

簡(jiǎn)介

websocket 相關(guān)簡(jiǎn)介,可以看阮老師的文章

用法

服務(wù)端nodejs-websocket

nodejs 可以通過(guò)nodejs-websocket來(lái)實(shí)現(xiàn)創(chuàng)建一個(gè) websocket 的服務(wù)

// websocket.js
const ws = require('nodejs-websocket')

const createServer = () => {
  let server = ws.createServer(connection => {
    connection.on('text', function(result) {
      console.log('發(fā)送消息', result)
    })
    connection.on('connect', function(code) {
      console.log('開(kāi)啟連接', code)
    })
    connection.on('close', function(code) {
      console.log('關(guān)閉連接', code)
    })
    connection.on('error', function(code) {
      console.log('異常關(guān)閉', code)
    })
  })
  return server
}

module.exports = createServer()

nodejs-websocket用法

文檔地址:https://www.npmjs.com/package/nodejs-websocket

node 創(chuàng)建的 websocket 服務(wù),主要包含三個(gè)概念

WS: 引入nodejs-websocket后的主要對(duì)象

  • ws.createServer([options], [callback]):創(chuàng)建一個(gè) server 對(duì)象
  • ws.connect(URL, [options], [callback]):創(chuàng)建一個(gè) connect 對(duì)象,一般由客戶端鏈接服務(wù)端 websocket 服務(wù)時(shí)創(chuàng)建
  • ws.setBinaryFragmentation(bytes):設(shè)置傳輸二進(jìn)制文件的最小尺寸,默認(rèn) 512kb
  • setMaxBufferLength:設(shè)置傳輸二進(jìn)制文件的最大尺寸,默認(rèn) 2M

Server:通過(guò) ws.createServer 創(chuàng)建

Function

  • server.listen(port, [host], [callback]): 傳入端口和主機(jī)地址后,開(kāi)啟一個(gè) websocket 服務(wù)
  • server.close([callback]): 關(guān)閉 websocket 服務(wù)
  • server.connections: 返回包含所有 connection 的數(shù)組,可以用來(lái)廣播所有消息
// 服務(wù)端廣播
function broadcast(server, msg) {
  server.connections.forEach(function(conn) {
    conn.sendText(msg)
  })
}

Event

可以通過(guò)server.on('event', (res) => {console.log(res)})調(diào)用

  • Event: 'listening()':調(diào)用server.listen會(huì)觸發(fā)當(dāng)前事件
  • Event: 'close()': 當(dāng)服務(wù)關(guān)閉時(shí)觸發(fā)該事件,如果有任何一個(gè) connection 保持鏈接,都不會(huì)觸發(fā)該事件
  • Event: 'error(errObj)':發(fā)生錯(cuò)誤時(shí)觸發(fā),此事件后會(huì)直接調(diào)用 close 事件
  • Event: 'connection(conn)':建立新鏈接(完成握手后)觸發(fā),conn 是連接的實(shí)例對(duì)象

Connection:每一個(gè)客戶端創(chuàng)建連接時(shí)的實(shí)例

Function

  • connection.sendText(str, [callback]):發(fā)送字符串給另一側(cè),可以由服務(wù)端發(fā)送字符串?dāng)?shù)據(jù)給客戶端
  • connection.beginBinary():要求連接開(kāi)始傳輸二進(jìn)制,返回一個(gè)WritableStream
  • connection.sendBinary(data, [callback]): 發(fā)送一個(gè)二進(jìn)制塊,類似connection.beginBinary().end(data)
  • connection.send(data, [callback]): 發(fā)送一個(gè)字符串或者二進(jìn)制內(nèi)容到客戶端,如果發(fā)送的是文本,類似于sendText(),如果發(fā)送的是二進(jìn)制,類似于sendBinary(),
    callback將監(jiān)聽(tīng)發(fā)送完成的回調(diào)
  • connection.close([code, [reason]]):開(kāi)始關(guān)閉握手(發(fā)送一個(gè)關(guān)閉指令)
  • connection.server:如果服務(wù)是 nodejs 啟動(dòng),這里會(huì)保留 server 的引用
  • connection.readyState:一個(gè)常量,表示連接的當(dāng)前狀態(tài)

connection.CONNECTING:值為 0,表示正在連接
connection.OPEN:值為 1,表示連接成功,可以通信了
connection.CLOSING:值為 2,表示連接正在關(guān)閉。
connection.CLOSED:值為 3,表示連接已經(jīng)關(guān)閉,或者打開(kāi)連接失敗。

  • connection.outStream: 存儲(chǔ)connection.beginBinary()返回的OutStream對(duì)象,沒(méi)有則返回 null
  • connection.path:表示建立連接的路徑
  • connection.headers:只讀請(qǐng)求頭的 name 的 value 對(duì)應(yīng)的 object 對(duì)象
  • connection.protocols:客戶端請(qǐng)求的協(xié)議數(shù)組,沒(méi)有則返回空數(shù)組
  • connection.protocol:同意連接的協(xié)議,如果有這個(gè)協(xié)議,它會(huì)包含在connection.protocols數(shù)組里面

Event

  • Event: 'close(code, reason)': 連接關(guān)閉時(shí)觸發(fā)
  • Event: 'error(err)':發(fā)生錯(cuò)誤時(shí)觸發(fā),如果握手無(wú)效,也會(huì)發(fā)出響應(yīng)
  • Event: 'text(str)':收到文本時(shí)觸發(fā),str 時(shí)收到的文本字符串
  • Event: 'binary(inStream)':收到二進(jìn)制內(nèi)容時(shí)觸發(fā),inStream時(shí)一個(gè)ReadableStream
var server = ws
  .createServer(function(conn) {
    console.log('New connection')
    conn.on('binary', function(inStream) {
      // 創(chuàng)建空的buffer對(duì)象,收集二進(jìn)制數(shù)據(jù)
      var data = new Buffer(0)
      // 讀取二進(jìn)制數(shù)據(jù)的內(nèi)容并且添加到buffer中
      inStream.on('readable', function() {
        var newData = inStream.read()
        if (newData)
          data = Buffer.concat([data, newData], data.length + newData.length)
      })
      inStream.on('end', function() {
        // 讀取完成二進(jìn)制數(shù)據(jù)后,處理二進(jìn)制數(shù)據(jù)
        process_my_data(data)
      })
    })
    conn.on('close', function(code, reason) {
      console.log('Connection closed')
    })
  })
  .listen(8001)
  • Event: 'connect()':連接完全建立后發(fā)出

具體代碼實(shí)現(xiàn)

const ws = require('nodejs-websocket')
// 可以通過(guò)不同的code可以表示要后端實(shí)現(xiàn)的不同邏輯
const {
  RECEIEVE_MESSAGE,
  SAVE_USER_INFO,
  CLOSE_CONNECTION
} = require('../constants/config')

// 當(dāng)前聊天室的用戶
let chatUsers = []

// 廣播通知
const broadcast = (server, info) => {
  console.log('broadcast', info)
  server.connections.forEach(function(conn) {
    conn.sendText(JSON.stringify(info))
  })
}

// 服務(wù)端獲取到某個(gè)用戶的信息通知到所有用戶
const broadcastInfo = (server, info) => {
  let count = server.connections.length
  let result = {
    code: RECEIEVE_MESSAGE,
    count: count,
    ...info
  }
  broadcast(server, result)
}

// 返回當(dāng)前剩余的在線用戶
const sendChatUsers = (server, user) => {
  let chatIds = chatUsers.map(item => item.chatId)
  if (chatIds.indexOf(user.chatId) === -1) {
    chatUsers.push(user)
  }
  let result = {
    code: SAVE_USER_INFO,
    count: chatUsers.length,
    chatUsers: chatUsers
  }
  broadcast(server, result)
}

// 觸發(fā)關(guān)閉連接,在離開(kāi)頁(yè)面或者關(guān)閉頁(yè)面時(shí),需要主動(dòng)觸發(fā)關(guān)閉連接
const handleCloseConnect = (server, user) => {
  chatUsers = chatUsers.filter(item => item.chatId !== user.chatId)
  let result = {
    code: CLOSE_CONNECTION,
    count: chatUsers.length,
    chatUsers: chatUsers
  }
  console.log('handleCloseConnect', user)
  broadcast(server, result)
}

// 創(chuàng)建websocket服務(wù)
const createServer = () => {
  let server = ws.createServer(connection => {
    connection.on('text', function(result) {
      let info = JSON.parse(result)
      let code = info.code
      if (code === CLOSE_CONNECTION) {
        handleCloseConnect(server, info)
        // 某些情況如果客戶端多次觸發(fā)連接關(guān)閉,會(huì)導(dǎo)致connection.close()出現(xiàn)異常,這里try/catch一下
        try {
          connection.close()
        } catch (error) {
          console.log('close異常', error)
        }
      } else if (code === SAVE_USER_INFO) {
        sendChatUsers(server, info)
      } else {
        broadcastInfo(server, info)
      }
    })
    connection.on('connect', function(code) {
      console.log('開(kāi)啟連接', code)
    })
    connection.on('close', function(code) {
      console.log('關(guān)閉連接', code)
    })
    connection.on('error', function(code) {
      // 某些情況如果客戶端多次觸發(fā)連接關(guān)閉,會(huì)導(dǎo)致connection.close()出現(xiàn)異常,這里try/catch一下
      try {
        connection.close()
      } catch (error) {
        console.log('close異常', error)
      }
      console.log('異常關(guān)閉', code)
    })
  })
  // 所有連接釋放時(shí),清空聊天室用戶
  server.on('close', () => {
    chatUsers = []
  })
  return server
}

const server = createServer()

module.exports = server

部分前端代碼

前端主要是創(chuàng)建WebSocket連接后,在onopen事件觸發(fā)時(shí),初始化用戶的一些信息,比如每個(gè)用戶包含唯一的chatId之類的,以及保持用戶昵稱,用戶頭像啥的
再就是監(jiān)聽(tīng)onmessage事件,通過(guò)后端返回的 message 信息執(zhí)行對(duì)應(yīng)的操作,建議前后端約定一些 code 來(lái)表示某一種類似的 message 信息
然后就是監(jiān)聽(tīng)頁(yè)面的一些觸發(fā)事件,將信息通過(guò)send方法發(fā)送給服務(wù)端

let websocket = new WebSocket(wsConfig.WS_ROOT_PATH)
websocket.onopen = () => {
  console.log('websocket連接開(kāi)啟...')
  if (!this.chatId) {
    this.initChatId()
  }
  this.sendUserName()
}
websocket.onmessage = event => {
  let data = event.data
  let result = JSON.parse(data)
  let code = result.code
  let count = result.count
  this.updateChatCount(count)
  if (code === RECEIEVE_MESSAGE) {
    this.pushMessage(result)
    this.onMessageScroll()
  } else if (code === SAVE_USER_INFO || code === CLOSE_CONNECTION) {
    this.updateChatUser(result.chatUsers)
  }
  console.log('數(shù)據(jù)已接收...', code, result)
}
websocket.onclose = this.onWebsocketClose
websocket.onerror = this.onWebsocketError

// 發(fā)送message
sendMessage(info) {
  if (this.websocket && typeof this.websocket.send === 'function') {
    this.websocket.send(JSON.stringify(info))
  }
}

問(wèn)題

如果瀏覽器進(jìn)入其它頁(yè)面或者關(guān)閉瀏覽器,鏈接會(huì)異常關(guān)閉,經(jīng)常會(huì)導(dǎo)致后端出現(xiàn)異常報(bào)錯(cuò)

// 前端代碼監(jiān)聽(tīng)頁(yè)面關(guān)閉或者刷新
window.onunload = () => {
  this.closeConnect()
}
// vue里跳轉(zhuǎn)到其它頁(yè)面
beforeRouteLeave(to, from, next) {
  this.closeConnect()
  next()
}

總結(jié)

這次使用 websocket 實(shí)現(xiàn)一個(gè)基本的聊天室功能,個(gè)人感覺(jué)還比較簡(jiǎn)單,只是中間會(huì)出現(xiàn)一些由于鏈接異常斷開(kāi),導(dǎo)致后端服務(wù)拋出異常掛掉的情況
記住前端關(guān)閉頁(yè)面或者刷新頁(yè)面時(shí),先把連接關(guān)掉,每次進(jìn)入頁(yè)面時(shí)創(chuàng)建連接,然后后端將由于異常關(guān)閉導(dǎo)致的出錯(cuò) try/catch 一下,避免拋出異常,阻塞進(jìn)程

websocket 對(duì)于實(shí)現(xiàn)聊天室這樣的功能,真的很方便,其實(shí)還能擴(kuò)展到多人合作或者網(wǎng)絡(luò)游戲等功能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測(cè)試 ...
    KeKeMars閱讀 6,609評(píng)論 0 6
  • 原文地址:http://www.ibm.com/developerworks/cn/java/j-lo-WebSo...
    敢夢(mèng)敢當(dāng)閱讀 9,032評(píng)論 0 50
  • WebSocket 機(jī)制 WebSocket 是 HTML5 一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更...
    勇敢的_心_閱讀 2,380評(píng)論 0 4
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,639評(píng)論 19 139
  • 今日感恩(四期96--99) 2017/12/10日415 感恩早睡早起第148天。22:30睡覺(jué),6:00起床。...
    喜羊羊_43e1閱讀 174評(píng)論 0 2

友情鏈接更多精彩內(nèi)容