利用express+socket.io实现简易版聊天室

写在前面

最近由于利用node重构某个项目,项目中有一个实时聊天的功能,于是就研究了一下聊天室,在线demo|源码,欢迎大家反馈。这个聊天室的主要利用到了socket.ioexpress。这个聊天室支持群聊,私聊,支持发送图片(PS:大家在体验时最好开启两个浏览器,自问自答)。下面就来和大家分享下实现过程:

WebSocket

HTML5一种新的协议。它实现了浏览器与服务器全双工通信。

为了更好的理解WebSocket,需要了解一下在没有WebSocket阶段是如何写聊天室这种实时系统的:
基于http协议浏览器可以实现单向通信,只能由浏览器发起请求(Request),服务器进行响应(Response),一个请求对应一个响应。由于服务器不能主动向客户端推送消息,于是普遍采用的方式就是轮询(polling),轮询实现起来非常简单,就是定时的利用ajax向服务器端进行请求。如果服务器有新的数据就返回新的数据,如果没有数据就返回空响应。用代码来模拟下就是这个样子的:

// 前端请求代码
function update (fn) {
    var xhr = new XMLHttpRequest();
    xhr.open("get", "./update.php");
    xhr.onreadystatechange = function(){    
    if(xhr.readyState === 4){
      if(xhr.status == 200){    
        const res = JSON.parse(xhr.response);
        if (res.flag) {
            // 进行相应操作

            // fn为接到响应后的处理函数
            fn && fn(fn);
        }
      }
    }
    };
    xhr.send();
}
function polling () {
    update();
}
setInterval(polling, 2000);
// 后台响应代码
<?php
    // 利用随机数的大小来模拟是否有新数据
    if (rand(1, 100) < 35) {
        echo json_encode(array( 
            "flag" => true, 
            "data" => '有新数据来了'
        ));
    } else {
        echo json_encode(array(
            "flag" => false
        ));
    }
?>

这种定时请求的方式的关键在于间隔时间的选取,依据我在上面代码做的模拟,很少概率能拿到下真正的数据,多半的ajax请求是无效的,于是又有前辈基于轮询提出来了Comet(服务器推),这种技术可以通过长轮询(long polling)实现(还可以利用iframe),长轮询也是靠ajax实现客户端的请求,其流程为:客户端发起请求,服务器挂起请求,假若有新的数据返回,服务器响应客户端刚才的请求,客户端得到响应后继续请求服务器。用伪代码来模拟下长轮询的过程:

// 前端利用下面函数进行请求
function longPolling () {
    update(update);
}
longpolling();
// 后端代码做如下更改
<?php
    // 利用随机数的大小来模拟是否有新数据
    while (true) {
        if (rand(1, 100) < 5) {
            echo json_encode(array( 
                "flag" => true, 
                "data" => '有新数据来了'
            ));
            break;
        }
    }
?>

长轮询的确减少了请求的次数,但是它也有着很大的问题,那就是耗费服务器的资源
无论是轮询还是长轮询,还有着一个问题就是http并不是支持长连接很多人会说keep-alive不就是做到了长连接吗?然而并非如此,keep-alive是重用一个TCP连接,就是说http 1.1做到了一个TCP连接可以发送多个http请求,然而每个http请求还需要发送Request Header,每个请求的响应还会带着Response Header。对于轮询和长轮询来说伴随着真实数据的交换,还有进行的就是大量的http header的交换。
基于这些问题,WebSocket被提出,WebSocket可以理解为对http的一个补丁包,WebSocket使http变成了一个真正的长连接,握手阶段利用http协议,之后就不会再发起http请求了。下面来看下WebSocket握手的过程:

clipboard.png
客户端的请求头比一般的http请求多出来几个字段:

  • Upgrade: websocket,Connection: Upgrade,利用这两个字段来告诉服务器,我要将协议升级为websocket
  • Sec-WebSocket-Version: 13,来告诉服务器我想要使用的WebSocket的版本。
  • Sec-WebSocket-Key,其值采用base64编码的随机16字节长的字符序列,这个值会在响应头中回应。
  • Sec-WebSocket-Extensions,提供了一个客户端支持的协议扩展列表来供服务器选择,服务器只能选择一个,并且会将选择的扩展写入响应头的Sec-WebSocket-Extensions
  • Sec-WebSocket-Protocol,与Sec-WebSocket-Extensions原理相似,用于协商应用子协议。

再来看看响应头:

  • Status Code,值为101,表示已经升级到WebSocket协议
  • Sec-WebSocket-Extensions告诉客户端服务器选择的协议扩展
  • Sec-WebSocket-Protocol告诉客户端服务器选择的子协议
  • Sec-WebSocket-Accept经服务器确认并且加密后的Sec-WebSocket-Key

还有一点值得关注的就是协议头由http/https换成了ws/wss,也标识真http完成了其使命,接下来的事情由WebSocket来负责啦!

socket.io

由于写原生的WebSocket在处理低版本浏览器的兼容性上的困难,所以一般在写实时交互的这种项目时一般会利用到socket.iosocket.io并不仅仅是WebSocket,还包含着AJAX long pollingAJAX multipart streamingJSONP Polling等。socket.io可以看做是基于engine.io的二次开发。通过emiton可以轻松地实现服务器与客户端之间的双向通信,emit来发布事件,on来订阅事件。

用户登录/登出

下面开始来写代码,我利用的构建工具是gulp,模板语言是jade,css预处理语言是less,假若也需要使用到这些,可以关注下我所在团队搭建的一个小的脚手架,先从app.js开始:

const users = {}, 
    app = express(),
    server = require("http").createServer(app),
    io = require("socket.io").listen(server); 
// 将socket.io绑定到服务器上,使得任何连接到服务器的客户端都具有实时通信的功能

// 服务器来监听客户端
io.on("connection", (socket) => {
    // socket是返回的连接对象,两端的交互就是通过这个对象
});

需要创建一个对象(users)来存储在线用户,键值为用户昵称,为用户登录来订阅个事件:

socket.on("login", (nickname) => {
        if (users[nickname] || nickname === "system") {
            socket.emit("repeat");          
        } else {
            socket.nickname = nickname;
            users[nickname] = {
                name: nickname,
                socket: socket,
                lastSpeakTime: nowSecond()
            };
            socket.emit("loginSuccess");            
            UsersChange(nickname, true);
        }
});
socket.on("disconnect", () => {
    if (socket.nickname && users[socket.nickname]) {
        delete users[socket.nickname];
        UsersChange(socket.nickname, false);
    }
});
function UsersChange (nickname, flag) {
    io.sockets.emit("system", {
        nickname: nickname,
        size: Object.keys(users).length,
        flag: flag
    });
}
function nowSecond () {
    return Math.floor(new Date() / 1000);
}

用户登录时需要验证其昵称是否含有,假若函数,则触发在客户端的js代码中注册的repeat事件,反之触发loginSuccess事件并且登录成功后需要向所有的客户端来广播,所以利用了io.sockets.emitrepeatloginSuccesssystem,在src/js/index.js中进行注册,主要用于页面的显示,也就是一些dom操作,所以在这里没有什么好讲的。用户退出,直接调用默认事件disconnect就好,并将该用户从用户对象中移除。

心跳检测

在用户的状态上的坑还是不少的,因为WebSocket中间过程比较复杂,经常会出现一些异常的情况,所以需要进行心跳检测,我采用的方式是服务端定时遍历用户列表,假若用户最后的发言时间与现在相比超过了5分钟,就将其视为掉线,从而避免了”用户undefined退出群聊”的这种情况。

function pong () {
    const now = nowSecond();
    for (let k in users) {
        if (users[k].lastSpeakTime + MAX_LEAVE_TIME < now) {
            var socket = users[k].socket;
            users[k].socket.emit("disconnect");
            socket.emit("nouser", "由于长时间未说话,您已经掉线,请重新刷新页面");
            socket = null;
        } 
    }
}
// 心跳检测
setInterval(pong, PONG_TIME);
function UsersChange (nickname, flag) {
    io.sockets.emit("system", {
        nickname: nickname,
        size: Object.keys(users).length,
        flag: flag
    });
}

写在最后

其实socket.io的使用真的非常简单,很容易就会上手,所以其余功能不再一一演示,大家可以看代码的实现(写的比较差,还请见谅),客户端代码中大量用到了L,相当于zepto$,特别需要处理的是在私信和发送图片的处理上,私信需要处理不同消息框,到底把消息添加到那个消息框中,我利用了一个对象来存储这些信息(cache),cache的键名为用户的昵称(因为在注册时判断了其是否唯一,所以可以将其视为唯一的);键值为对象,对象属性如下图所示:

clipboard.png
具体实现大家还是到源码中去看吧!

感谢王哇勇大神的HiChat小胡子哥的blogChat
由于本人水平有限,如有错误,欢迎大家指出!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,要使用Node.js和Socket.io搭建一个基本的聊天室,可以参考以下步骤: 1. 安装Node.js和npm 2. 创建一个新的Node.js项目并在项目文件夹里打开命令行 3. 在命令行中输入以下命令来安装Socket.io: ``` npm install socket.io ``` 4. 创建一个新的文件夹用于存放服务器端代码,我们称之为“server” 5. 在“server”文件夹中创建一个新的JavaScript文件,例如“server.js” 6. 在“server.js”中添加以下代码: ``` const io = require('socket.io')(); io.on('connection', (socket) => { console.log('a user connected'); socket.on('disconnect', () => { console.log('user disconnected'); }); }); io.listen(3000); ``` 7. 在命令行中进入“server”文件夹并运行“server.js”文件: ``` node server.js ``` 8. 创建一个新的文件夹用于存放客户端代码,我们称之为“client” 9. 在“client”文件夹中创建一个新的HTML文件,例如“index.html” 10. 在“index.html”中添加以下代码: ``` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Socket.io Chat Room</title> </head> <body> <h1>Socket.io Chat Room</h1> <div id="messages"></div> <form id="message-form"> <input type="text" id="input-message" placeholder="Type your message here..."> <button type="submit">Send</button> </form> <script src="https://cdn.socket.io/socket.io-3.0.1.min.js"></script> <script src="client.js"></script> </body> </html> ``` 11. 在“client”文件夹中创建一个新的JavaScript文件,例如“client.js” 12. 在“client.js”中添加以下代码: ``` const socket = io('http://localhost:3000'); const messageForm = document.getElementById('message-form'); const inputMessage = document.getElementById('input-message'); const messages = document.getElementById('messages'); messageForm.addEventListener('submit', (event) => { event.preventDefault(); const message = inputMessage.value; if (message) { socket.emit('chat message', message); inputMessage.value = ''; } }); socket.on('chat message', (message) => { const li = document.createElement('li'); li.textContent = message; messages.appendChild(li); }); ``` 13. 在命令行中进入“client”文件夹并运行以下命令: ``` npm install express ``` 14. 在“client”文件夹中创建一个新的JavaScript文件,例如“server.js” 15. 在“server.js”中添加以下代码: ``` const express = require('express'); const app = express(); app.use(express.static('public')); app.listen(8080, () => { console.log('Server running on http://localhost:8080'); }); ``` 16. 在命令行中进入“client”文件夹并运行以下命令: ``` node server.js ``` 现在你就可以在浏览器中打开“http://localhost:8080”并开始聊天了。 要实现私聊和创建群聊的功能,你需要在服务器端和客户端中添加一些额外的代码。 对于私聊功能,你可以创建一个“私聊房间”,只有被邀请的用户才能加入该房间,并在该房间中进行私人对话。以下是一些可能有用的代码: 1. 服务器端: ``` const users = {}; io.on('connection', (socket) => { console.log('a user connected'); socket.on('login', (username) => { users[username] = socket.id; console.log(`${username} logged in`); }); socket.on('invite', (username) => { const roomId = `${socket.id}:${users[username]}`; socket.join(roomId); io.to(users[username]).emit('invited', roomId); }); socket.on('chat message', (data) => { const { message, roomId } = data; io.to(roomId).emit('chat message', message); }); socket.on('disconnect', () => { console.log('user disconnected'); }); }); ``` 2. 客户端: ``` const socket = io('http://localhost:3000'); const username = prompt('What is your name?'); socket.emit('login', username); let roomId = null; const inviteForm = document.getElementById('invite-form'); const inviteInput = document.getElementById('invite-input'); inviteForm.addEventListener('submit', (event) => { event.preventDefault(); const username = inviteInput.value; if (username) { socket.emit('invite', username); inviteInput.value = ''; } }); socket.on('invited', (id) => { roomId = id; messages.innerHTML = ''; messageForm.style.display = 'block'; }); messageForm.addEventListener('submit', (event) => { event.preventDefault(); const message = inputMessage.value; if (message) { socket.emit('chat message', { message, roomId }); inputMessage.value = ''; } }); socket.on('chat message', (message) => { const li = document.createElement('li'); li.textContent = message; messages.appendChild(li); }); ``` 对于创建群聊功能,你可以创建一个“群聊房间”,所有用户都可以加入该房间,并在该房间中进行群聊。以下是一些可能有用的代码: 1. 服务器端: ``` const rooms = {}; io.on('connection', (socket) => { console.log('a user connected'); socket.on('create room', () => { const roomId = socket.id; socket.join(roomId); rooms[roomId] = { name: `Room ${Object.keys(rooms).length + 1}` }; io.to(roomId).emit('room created', roomId); console.log(`Room ${roomId} created`); }); socket.on('join room', (roomId) => { socket.join(roomId); io.to(roomId).emit('user joined', { username: 'anonymous', message: 'joined the chat' }); console.log(`User ${socket.id} joined room ${roomId}`); }); socket.on('chat message', (data) => { const { message, roomId } = data; io.to(roomId).emit('chat message', message); }); socket.on('disconnect', () => { console.log('user disconnected'); }); }); ``` 2. 客户端: ``` const socket = io('http://localhost:3000'); const createRoomButton = document.getElementById('create-room-button'); const joinRoomForm = document.getElementById('join-room-form'); const joinRoomInput = document.getElementById('join-room-input'); createRoomButton.addEventListener('click', () => { socket.emit('create room'); }); joinRoomForm.addEventListener('submit', (event) => { event.preventDefault(); const roomId = joinRoomInput.value; if (roomId) { socket.emit('join room', roomId); joinRoomInput.value = ''; messages.innerHTML = ''; messageForm.style.display = 'block'; } }); socket.on('room created', (roomId) => { const option = document.createElement('option'); option.value = roomId; option.textContent = `Room ${Object.keys(rooms).length}`; selectRoom.appendChild(option); }); socket.on('user joined', (data) => { const { username, message } = data; const li = document.createElement('li'); li.textContent = `${username} ${message}`; messages.appendChild(li); }); socket.on('chat message', (message) => { const li = document.createElement('li'); li.textContent = message; messages.appendChild(li); }); ``` 以上代码只是一个简单的示例,你可以根据自己的需求进行更改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值