一、原理
WebSocket是一种在单个TCP连接上实现全双工通信的协议,其原理可以分为握手过程和数据传输两个主要方面。
在握手过程中,首先由客户端发起WebSocket连接请求,这个请求通过HTTP协议发送,请求头部包含Upgrade(升级协议)、Connection(连接类型)和Sec-Websocket-Key(用于安全性验证的随机值)等特殊字段。当服务器收到客户端的请求后便会进行安全性验证,验证通过后便返回HTTP101状态码来切换协议。握手成功后,连接由HTTP协议升级为WebSocket协议,此后数据的传输就使用WebSocket协议。如下图所示:

WebSocket使用帧来传输数据,同时其连接是持久的,允许在同一连接上连续传输多个数据帧,避免了在每次通信中都要重新建立连接的开销。一旦WebSocket连接建立,客户端和服务端就可以随时相互发送数据,无需等待对方的响应,这就使得WebSecket适用于实时应用,如在线聊天,实时游戏等。
总体来说,WebSocket通过握手过程建立持久的全双工连接,通过帧来传输数据,实现了更为高效的实时通信。由于它的轻量级和低延迟特性,WebSocket在许多实时应用中被广泛使用。
本项目基于WebSocket的原理利用Socket.IO库完成了在线聊天室的开发。Socket.IO是一个实现了实时、双向通信的JavaScript库,它建立在WebSocket之上,并提供了更高层次的抽象,以简化实时应用程序的制作。
二、聊天室功能
- 用户登录:用户可以输入用户名和密码并选择头像来进入聊天室,如果用户名重复则会报错。
- 在线群聊:进入聊天室后可以看到当前聊天室人数和用户列表,当用户发送消息后所有其他用户都会在聊天界面看到带有用户名和头像的消息内容。
- 发送图片:用户可以选择存储在自己电脑上的图片进行发送。
- 系统通知:当有用户进入或者离开聊天室时,会有相应的系统消息对所有用户进行通知。
- 历史消息:当新用户进入聊天室时,可以看到自己登录之前聊天室已有的消息,方便了解聊天情况。
三、前端设计
3.1初始设置
var socket = io('http://localhost:7000')
sessionStorage.setItem('username', '')
sessionStorage.setItem('avatar', '')
连接socketio服务并建立两个会话存储用于记录用户名和头像数据。
3.2用户登录
$('#loginButton').on('click', function() {
//获取用户名
var username = $('#username').val().trim()
if (!username) {
alert('请输入用户名')
return
}
//获取密码
var password = $('#password').val().trim()
if (!password) {
alert('请输入密码')
return
}
//获取头像
var avatar = $('#loginAvatar li.now img').attr('src')
// 发送登录事件
socket.emit('login', {
username: username,
avatar: avatar,
password: password
})
})
// 登录失败
socket.on('loginError', data => {
alert('用户名已经存在')
})
// 登录成功
socket.on('loginSuccess', data => {
$('.loginBox').fadeOut()
$('.chatBox').fadeIn()
console.log(data)
$('.avatarUrl').attr('src', data.avatar)
$('.systemMessage .username').text(data.username)
sessionStorage.setItem('username', data.username)
sessionStorage.setItem('avatar', data.avatar)
})
点击登录按钮时,会发送"login"事件给服务端,并传递用户名,密码,头像数据。如果未输入用户名和密码则会报错以此提醒用户输入。如果用户名已存在,则登陆失败,返回”loginError”事件,如果不存在则登陆成功,返回”loginSuccess”事件,展现聊天界面并将用户名和头像保存起来。
3.3发送与获取聊天信息
// 发送聊天消息
$('.sendButton').on('click', () => {
// 获取到聊天的内容
var content = $('#content').html()
$('#content').html('')
if (!content) return alert('请输入聊天消息')
// 把消息传给服务器
socket.emit('sendMessage', {
msg: content,
username:sessionStorage.getItem('username'),
avatar:sessionStorage.getItem('avatar')
})
})
// 获取聊天消息
socket.on('receiveMessage', data => {
if (data.username === sessionStorage.getItem('username')) {
// 自己的消息
$('.boxMiddle').append(`
<div class="messageBox">
<div class="my message">
<img class="avatar" src="${data.avatar}" alt="" />
<div class="content">
<div class="bubble">
<div class="bubbleContent">${data.msg}</div>
</div>
</div>
</div>
</div>
`)
} else {
// 别人的消息
$('.boxMiddle').append(`
<div class="messageBox">
<div class="other message">
<img class="avatar" src="${data.avatar}" alt="" />
<div class="content">
<div class="nickname">${data.username}</div>
<div class="bubble">
<div class="bubbleContent">${data.msg}</div>
</div>
</div>
</div>
</div>
`)
}
scrollIntoView()
})
function scrollIntoView() {
// 当前元素的底部滚动到可视区
$('.boxMiddle').children(':last').get(0).scrollIntoView(false)
}
// 发送图片
$('#file').on('change', function() {
var file = this.files[0]
var fr = new FileReader()
fr.readAsDataURL(file)
fr.onload = function() {
socket.emit('sendImage', {
username: sessionStorage.getItem('username'),
avatar: sessionStorage.getItem('avatar'),
img: fr.result
})
}
})
// 获取图片聊天信息
socket.on('receiveImage', data => {
if (data.username === sessionStorage.getItem('username')) {
// 自己的消息
$('.boxMiddle').append(`
<div class="messageBox">
<div class="my message">
<img class="avatar" src="${data.avatar}" alt="" />
<div class="content">
<div class="bubble">
<div class="bubbleContent">
<img src="${data.img}">
</div>
</div>
</div>
</div>
</div>
`)
} else {
// 别人的消息
$('.boxMiddle').append(`
<div class="messageBox">
<div class="other message">
<img class="avatar" src="${data.avatar}" alt="" />
<div class="content">
<div class="nickname">${data.username}</div>
<div class="bubble">
<div class="bubbleContent">
<img src="${data.img}">
</div>
</div>
</div>
</div>
</div>
`)
}
$('.boxMiddle img:last').on('load', function() {
scrollIntoView()
})
})
点击发送按钮时,会发送”sendMessage”事件给服务端,并传递消息内容,用户名和头像数据,若有消息发送,服务端便会给所有用户返回”receiveMessage”事件,并传递消息数据,如果发消息的用户与会话存储的用户名不同,则说明是其他用户发的消息,内容显示在聊天框的左边,颜色为灰色。如果用户名相同则说明是本人发的消息,内容显示在聊天框的右边,颜色为绿色,发送图片同理。
3.4获取系统消息
// 新用户加入
socket.on('addUser', data => {
$('.systemMessage').append(`
<div class="system">
<p class="messageSystem">
<span class="content">${data.username}加入了聊天室</span>
</p>
</div>
`)
scrollIntoView()
})
// 更新用户列表
socket.on('userList', data => {
$('.userList ul').html('')
data.forEach(item => {
$('.userList ul').append(`
<li class="user">
<div class="avatar"><img src="${item.avatar}" alt="" /></div>
<div class="name">${item.username}</div>
</li>
`)
})
$('#userCount').text(data.length)
})
// 用户离开
socket.on('delUser', data => {
$('.systemMessage').append(`
<div class="system">
<p class="messageSystem">
<span class="content">${data.username}离开了聊天室</span>
</p>
</div>
`)
scrollIntoView()
})
当有新用户加入进来时,服务端会发送”addUser”事件使客户端产生系统消息,告诉所有用户有人加入聊天室,并发送”userList”事件同步更新用户列表,用户离开时服务端则会发送”delUser”和”userList”事件。
3.5获取历史消息
socket.on('messageList',data=>{
// 添加历史消息
$('.boxMiddle').html('')
data.forEach(item => {
$('.boxMiddle').append(`
<div class="messageBox">
<div class="other message">
<img class="avatar" src="${item.avatar}" alt="" />
<div class="content">
<div class="nickname">${item.username}</div>
<div class="bubble">
<div class="bubbleContent">${item.msg}</div>
</div>
</div>
</div>
</div>
`)
})
})
用户登录时,服务端会检测历史消息,如果存在则会发送”messageList”事件让客户端在聊天框中展现用户登陆前的消息记录。
四、后端设计
4.1初始设置
//启动聊天服务端
var app = require('express')()
var server = require('http').Server(app)
var io = require('socket.io')(server)
// 记录历史消息和已登录用户
const messages = []
const users = []
server.listen(7000, () => {
console.log('服务器成功启动')
})
app.use(require('express').static('public'))
app.get('/', function(req, res) {
res.redirect('/index.html')
})
用express框架启动聊天服务端监听7000号端口,并建立messages和users两个数组,messages数组用来存储历史聊天记录,users数组用来存储当前所有用户。
4.2用户登录
socket.on('login', data => {
let user = users.find(item => item.username === data.username)
if (user) {
// 用户已存在,登陆失败
socket.emit('loginError', { msg: '登录失败,用户在聊天室' })
} else {
// 用户不存在, 登录成功
console.log(data)
users.push(data)
socket.emit('loginSuccess', data))
// 广播系统消息
io.emit('addUser', data)
// 更新用户列表
io.emit('userList', users)
//获取历史消息
socket.emit('messageList', messages)
// 存储用户名和头像信息
socket.username = data.username
socket.avatar = data.avatar
}
})
当客户端发送”login”事件给服务端时,服务端会对传递过来的用户数据进行判断,如果当前用户名已存在,则发送”loginError”事件表明登陆失败,如果用户名不存在,则发送”loginSuccess”事件表明登陆成功,同时发送”addUser” ”uerList” “messageList”事件来广播系统消息,更新用户列表以及获取历史消息。
4.3用户离开
// 用户断开连接
socket.on('disconnect', () => {
// 删除用户信息
let idx = users.findIndex(item => item.username === socket.username)
users.splice(idx, 1)
// 广播系统消息
io.emit('delUser', {
username: socket.username,
avatar: socket.avatar
})
// 更新用户列表
io.emit('userList', users)
})
用户断开连接时会收到”disconnect”事件,服务端会将当前用户从users数组中删除,并发送”delUser”和”userList”事件来广播系统消息并更新用户列表。
4.4消息广播
// 接收消息
socket.on('sendMessage', data => {
console.log(data)
// 广播给所有用户
io.emit('receiveMessage', data)
messages.push(data)
})
// 接收图片
socket.on('sendImage', data => {
// 广播给所有用户
io.emit('receiveImage', data)
})
服务端收到”sendMessage”和”sendImage”事件时会通过”receiveMessage”和”receiveImage”事件将收到的消息和图片发送给所有在线用户。
五、项目展示
5.1 登陆界面

5.2 聊天界面



六、总结
本项目基于WebSocket的原理利用express框架开发了一个可以实现多人实时聊天功能的在线聊天室。聊天室支持用户选择合适的头像并保证用户名的唯一性,用户可以通过用户列表来查看当前在线用户,有用户加入或者退出时系统也会发出消息提醒,可发送内容包括文字和本地存储的图片,新登录用户还可以查看历史消息记录。聊天室运行时,WebSocket的应用保证了消息传输的实时性,同时尽可能地减小了服务器的开销,提高了效率。
在编写聊天室的过程中,我对于WebSocket有了更深的理解,了解了实时通信技术的相关原理,同时也掌握了如何用JavaScript语言进行前后端工作的开发。以后的学习过程中我将会探索更多高级技术应用,并将其运用到自己的项目中,在锻炼实践能力的同时给用户更好的使用体验。
在学习网络程序设计这门课的过程中,我获益匪浅。这门课与其他课最大的不同点在于这门课更加看重你的动手实践能力而不仅仅局限于理论知识。网络程序设计是一门理论与实践高度结合的学科,只有把学到的内容运用到项目中,才能说明你真正掌握了它。在课上孟老师也会举一些生动形象的例子让相关知识更容易理解。
最后感谢孟老师的细心教导。