nodejs-websocket介紹

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

為什么要用 websocket

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

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

websocket 適用場景:聊天室

簡介

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

用法

服務(wù)端nodejs-websocket

nodejs 可以通過nodejs-websocket來實現(xiàn)創(chuàng)建一個 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('開啟連接', 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ù)玩讳,主要包含三個概念

WS: 引入nodejs-websocket后的主要對象

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

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

Function

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

Event

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

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

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

Function

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

connection.CONNECTING:值為 0,表示正在連接
connection.OPEN:值為 1牌柄,表示連接成功畸悬,可以通信了
connection.CLOSING:值為 2,表示連接正在關(guān)閉珊佣。
connection.CLOSED:值為 3傻昙,表示連接已經(jīng)關(guān)閉,或者打開連接失敗彩扔。

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

Event

  • Event: 'close(code, reason)': 連接關(guān)閉時觸發(fā)
  • Event: 'error(err)':發(fā)生錯誤時觸發(fā),如果握手無效,也會發(fā)出響應(yīng)
  • Event: 'text(str)':收到文本時觸發(fā)须板,str 時收到的文本字符串
  • Event: 'binary(inStream)':收到二進(jìn)制內(nèi)容時觸發(fā)碰镜,inStream時一個ReadableStream
var server = ws
  .createServer(function(conn) {
    console.log('New connection')
    conn.on('binary', function(inStream) {
      // 創(chuàng)建空的buffer對象,收集二進(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ā)出

具體代碼實現(xiàn)

const ws = require('nodejs-websocket')
// 可以通過不同的code可以表示要后端實現(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ù)端獲取到某個用戶的信息通知到所有用戶
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)閉連接绪颖,在離開頁面或者關(guān)閉頁面時,需要主動觸發(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)閉甜奄,會導(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('開啟連接', code)
    })
    connection.on('close', function(code) {
      console.log('關(guān)閉連接', code)
    })
    connection.on('error', function(code) {
      // 某些情況如果客戶端多次觸發(fā)連接關(guān)閉,會導(dǎo)致connection.close()出現(xiàn)異常课兄,這里try/catch一下
      try {
        connection.close()
      } catch (error) {
        console.log('close異常', error)
      }
      console.log('異常關(guān)閉', code)
    })
  })
  // 所有連接釋放時牍氛,清空聊天室用戶
  server.on('close', () => {
    chatUsers = []
  })
  return server
}

const server = createServer()

module.exports = server

部分前端代碼

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

let websocket = new WebSocket(wsConfig.WS_ROOT_PATH)
websocket.onopen = () => {
  console.log('websocket連接開啟...')
  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))
  }
}

問題

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

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

總結(jié)

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末勒魔,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子菇曲,更是在濱河造成了極大的恐慌冠绢,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件常潮,死亡現(xiàn)場離奇詭異弟胀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門孵户,熙熙樓的掌柜王于貴愁眉苦臉地迎上來萧朝,“玉大人,你說我怎么就攤上這事夏哭〖旒恚” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵竖配,是天一觀的道長何址。 經(jīng)常有香客問我,道長械念,這世上最難降的妖魔是什么头朱? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮龄减,結(jié)果婚禮上项钮,老公的妹妹穿的比我還像新娘。我一直安慰自己希停,他們只是感情好烁巫,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著宠能,像睡著了一般亚隙。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上违崇,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天阿弃,我揣著相機(jī)與錄音,去河邊找鬼羞延。 笑死渣淳,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伴箩。 我是一名探鬼主播入愧,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嗤谚!你這毒婦竟也來了棺蛛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤巩步,失蹤者是張志新(化名)和其女友劉穎旁赊,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體椅野,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡彤恶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年钞钙,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片声离。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡芒炼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出术徊,到底是詐尸還是另有隱情本刽,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布赠涮,位于F島的核電站子寓,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏笋除。R本人自食惡果不足惜斜友,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垃它。 院中可真熱鬧鲜屏,春花似錦、人聲如沸国拇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽酱吝。三九已至也殖,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間务热,已是汗流浹背忆嗜。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留崎岂,地道東北人捆毫。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像该镣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子响谓,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容