實現一對一即時聊天應用,重要的一點就是消息可以實時的傳遞,一種方案就是熟知的使用 Websocket 協議,本文中咱們使用 Node.js 中的一個框架 Socket.io 來實現。前端
先看下,咱們實現的最終效果,以下所示:git
你也能夠在瀏覽器分別輸入如下兩個 URL 地址進行體驗:github
聊天頁面的 HTML 佈局是不復雜的,大致分爲 3 層,以下所示:web
<div class="container">
<div class="chat-header row"> <span class="col-xs-2 chat-header-left glyphicon glyphicon-menu-left"></span> <span class="col-xs-8 chat-header-center" id="chatHeaderCenter"></span> <span class="col-xs-2 chat-header-right glyphicon glyphicon-option-horizontal"></span> </div> <div class="chat-content" id="chatContent"></div> <div class="chat-bottom row"> <span class="col-xs-10 col-md-11 input-text"><input type="text" class="form-control " id="inputText" placeholder="請輸入要發送的內容..."></span> <span class="col-xs-2 col-md-1 span-submit"> <input class="btn btn-default btn-primary input-submit" id="sendBtn" data-dismiss="alert" type="submit" value="發送"> </span> </div> </div> <script src="/socket.io/socket.io.js"></script> <script src="./js/chat.js"></script> 複製代碼
客戶端首先建立一個 socket 對象,io() 的第一個參數是連接服務器的 URL,默認狀況下是 window.location。
Socket 的客戶端和服務端都有兩個函數 on()、emit() 這也是核心,經過這兩個函數能夠輕鬆的實現客戶端與服務端的雙向通訊。docker
// js/chat.js
const socket = io(); socket.on('connect', () => { socket.emit('online', query.sender); }); socket.on('reply_private_chat', replyPrivateMessage); ... 複製代碼
在客戶端發送消息,則是監聽發送按鈕的 onclick 事件或回車事件,對消息作一些處理經過 socket.emit 發送到服務端,由服務端轉接到另外一客戶端。express
前端部分更多細節代碼,這裏再也不列舉,可在 Github 上 Clone 下來自行查看,文末有代碼示例地址。json
const chatHeaderCenter = document.getElementById('chatHeaderCenter');
const inputText = document.getElementById('inputText'); const sendBtn = document.getElementById('sendBtn'); chatHeaderCenter.innerText = query.receiver; sendBtn.onclick = sendMsg; inputText.onkeydown = sendMsgByEnter; function sendMsg() { const value = inputText.value; if (!value) return alert('Message is required!'); const message = { sender: query.sender, receiver: query.receiver, text: value }; socket.emit('private_chat', message, data => { renderMessage(data, true); }); inputText.value = ''; } ... 複製代碼
使用 Express 搭建咱們的後端服務,建立一個 app.js 裏面監聽 30010 端口,加載咱們的客戶端頁面。後端
// app.js
const express = require('express'); const app = express(); const path = require('path'); const server = require('http').createServer(app); const PORT = 30010; app.use(express.static(path.join(__dirname, '../', 'public'))); server.listen(PORT, () => console.log(`Server is listening on ${PORT}`)); 複製代碼
上面咱們已經搭建了一個簡單的 Express 服務,如今引入咱們自定義的 io.js。瀏覽器
// app.js
require('./io.js')(server); 複製代碼
建立 io.js 在加載 socket.io 時傳入 server 對象,這時會拿到一個服務端的 io 對象,同步的註冊 connection 事件,若是有新的客戶端進來會被觸發,connection 回調函數的 socket 是指當前客戶端與服務端創建的連接。服務器
還有 online、private_chat、disconnect 這些事件有些是系統提供的,有些是咱們自定義的,下文還會在介紹。
const _ = require('underscore');
const moment = require('moment'); const userData = require('./users.json'); const USER_STATUS = ['ONLINE', 'OFFLINE']; const users = {}; module.exports = server => { const io = require('socket.io')(server); io.on('connection', socket => { socket.on('online', ...) socket.on('private_chat', ...); socket.on('disconnect', ...); }); } 複製代碼
on('online') 是咱們自定義的事件,由客戶端上線後觸發告訴咱們當前客戶端的用戶信息,保存 socket.id 創建用戶與 socket.id 的映射關係,用於後續私聊。這裏的 socket.id 每一次客戶端斷開重鏈都是會變的。
socket.on('online', username => {
socket.username = username; users[username] = { socketId: socket.id, status: USER_STATUS[0] }; }) 複製代碼
on('private_chat') 也是咱們自定義的事件,收到客戶端發送的消息後對消息作處理,判斷接收方是否在線,若是在線經過 socket.id 找到對應的 socket 向接收方推送消息,若是用戶不在線,能夠作些離線消息推送處理。這裏私聊轉發關鍵的一點是 socket.to().emit()。
socket.on('private_chat', (params, fn) => {
const receiver = users[params.receiver]; params.createTime = moment().format('YYYY-MM-DD HH:mm:ss'); const senderData = _.findWhere(userData, { username: params.sender }); params.senderPhoto = (senderData || {}).photo; if (!params.senderPhoto) { const senderLen = params.sender.length; params.senderPhotoNickname = params.sender.substr(senderLen - 2) } fn(params); if (receiver && receiver.status === USER_STATUS[0]) { socket.to(users[params.receiver].socketId).emit('reply_private_chat', params); } else { console.log(`${params.receiver} 不在線`); // 能夠在作些離線消息推送處理 } }); 複製代碼
斷開連接時觸發,reason 表示客戶端或服務端斷開連接的緣由。在這個事件裏咱們也會更改斷開連接的緣由。
socket.on('disconnect', reason => {
if (users[socket.username]) users[socket.username].status = USER_STATUS[1]; }); 複製代碼
我將以上示例打包爲了一個 Docker 鏡像,感興趣的能夠執行如下命令拉取,自行部署運行。
docker pull docker.io/qufei1993/private-chat-socketio
複製代碼
代碼示例:
Demo 在線體驗:
Socket.io 已經封裝的很好了,使用它開發一個即時聊天應用更多工做須要咱們去接入本身的業務邏輯,本文也只是一個聊天系統的冰山一角,還有不少須要去作,感興趣的朋友歡迎關注,後續會持續分享一些其它功能。