实现一对一即时聊天应用,重要的一点就是消息可以实时的传递,一种方案就是熟知的使用 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 已经封装的很好了,使用它开发一个即时聊天应用更多工做须要咱们去接入本身的业务逻辑,本文也只是一个聊天系统的冰山一角,还有不少须要去作,感兴趣的朋友欢迎关注,后续会持续分享一些其它功能。