WebSocket
随着 web 技术的发展,使用场景和需求也越来越复杂,客户端不再满足于简单的请求得到状态的需求
实时通讯越来越多应用于各个领域
HTTP 是最常用的客户端与服务端通信技术,但 HTTP 通信只能由客户端发起,无法及时获取服务端的数据改变
只能依靠定期轮询来获取最新的状态。时效性无法保证,同时更多的请求也会增加服务器的负担
WebSocket
技术应运而生
WebSocket
使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
在 WebSocket API
中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
Socket.IO
Socket.IO
是基于 WebSocket
的 Client-Server
实时通信库
Socket.IO
可以在客户端和服务器之间实现低延迟、双向和基于事件的通信
它建立在 WebSocket
协议之上,并提供额外的保证,例如回退到 HTTP 长轮询或自动重新连接
注意: Socket.IO
不是 WebSocket
实现。
也就是说我们不能通过 new WebSocket(URL)
的方式来连接服务端
必须使用其提供的客户端的 socket.io-client
来链接 socket.io
创建的服务
Socket.IO
屏蔽了所有底层细节,让顶层调用非常简单
当 Socket.IO
检测到当前环境不支持 WebSocket
时,能够自动地选择最佳的方式来实现网络的实时通信
Socket.IO
由两部分组成:
socket.io
socket.io-client
Socket.IO
的核心理念就是允许发送、接收任意事件和任意数据
任意能被编码为 JSON
的对象都可以用于传输,二进制数据也是支持的
Socket.IO
底层是基于 engine.io
这个库
engine.io
为 socket.io
提供跨浏览器 / 跨设备的双向通信的底层库
engine.io
使用了 Websocket
和 XHR
方式封装了一套 socket
协议
在低版本的浏览器中,不支持 Websocket
,为了兼容使用长轮询 (polling
) 替代
Socket.IO使用
在使用上,Socket
客户端首先创建一个 socket
对象
io()
的第一个参数是链接服务器的 URL
,默认情况下是 window.location
Socket
的客户端和服务端都有两个函数 on()
、emit()
实现客户端与服务端的双向通信:
Emit
:触发一个事件,第一个参数是事件名称,第二个参数是要发送到另一端的数据on
:注册一个事件,用来监听 emit
触发的事件broadcast
默认是向所有的 socket
连接进行广播,但是不包括发送者自身:
io.emit(foo);
会触发所有用户的foo事件socket.emit(foo);
只触发当前用户的foo事件socket.broadcast.emit(foo);
触发除了当前用户的其他用户的foo事件namespace
相当于建立新的频道,你可以在一个socket.io
服务上面隔离不同的连接、事件和中间件
默认的连接也是有namespace
的,就是 /
每一个 socket
连接都会有一个独一无二的标志,那就是 socket.id
socket.id
也是room
的标志,每个 socket
连接自身都拥有一间房room
那么我们就可以给这个room
发送消息,如果加入了房间room
,就能接受到room
里的广播信息
如果 socket
断开连接,也就是 disconnect
后,它会被自动移出 room
socket.join(rooms[, callback])
:加入房间socket.leave(room[, callback])
:离开房间socket.to(room)
: 给房间发送消息Socket.IO 聊天室
最后附上基于Socket.IO
和Express
的聊天室核心代码:
// server.js
var express = require('express');
var app = express();
var http = require('http').Server(app);
// 思考:socket.io作为一个函数,当前http作为参数传入生成一个io对象?
// io-server
var io = require("socket.io")(http);
var users = []; // 储存登录用户
var usersInfo = []; // 存储用户姓名和头像
// 开启静态资源服务器
// 路由为/默认www静态文件夹
// express.static是express提供的内置中间件,用来设置静态文件目录,这个文件夹里的文件 html、css、js 彼此可以用相对路径,可以直接访问图片等静态资源
app.use('/', express.static(__dirname + '/www'));
// 每个连接的用户都有专有的socket
// socket.io 的语法前后端通用,通过 socket.emit () 触发事件,通过 socket.on () 来监听和处理事件,通过传递的参数进行通信
/*
io.emit(foo); //会触发所有用户的foo事件
socket.emit(foo); //只触发当前用户的foo事件
socket.broadcast.emit(foo); //触发除了当前用户的其他用户的foo事件
*/
// 服务端通过 connection 监听客户端的连接,只要有客户端连接就执行回调函数
io.on('connection', (socket)=> {
// 渲染在线人员
io.emit('disUser', usersInfo);
// 登录,检测用户名是否已存在
socket.on('login', (user)=> {
if(users.indexOf(user.name) > -1) { // 昵称是否存在
socket.emit('loginError'); // 触发客户端的登录失败事件
} else {
users.push(user.name); //储存用户的昵称
usersInfo.push(user); // 储存用户的昵称和头像
socket.emit('loginSuc'); // 触发客户端的登录成功事件
socket.nickname = user.name;
io.emit('system', { // 向所有用户广播该用户进入房间
name: user.name,
status: '进入'
});
io.emit('disUser', usersInfo); // 渲染右侧在线人员信息
console.log(users.length + ' user connect.'); // 打印连接人数
}
});
// 发送窗口抖动
// 服务端会广播该事件使得每个客户端都会抖动窗口
socket.on('shake', ()=> {
socket.emit('shake', {
name: '您'
});
socket.broadcast.emit('shake', {
name: socket.nickname
});
});
// 发送消息事件
// 用户发送消息时触发服务器端的 sendMsg 事件,并将消息内容作为参数,服务器端监听到 sendMsg 事件之后向其他所有用户广播该消息,用的 socket.broadcast.emit (foo)
socket.on('sendMsg', (data)=> {
var img = '';
for(var i = 0; i < usersInfo.length; i++) {
if(usersInfo[i].name == socket.nickname) {
img = usersInfo[i].img;
}
}
socket.broadcast.emit('receiveMsg', { // 向除了发送者之外的其他用户广播
name: socket.nickname,
img: img,
msg: data.msg,
color: data.color,
type: data.type,
side: 'left'
});
socket.emit('receiveMsg', { // 向发送者发送消息,为什么分开发送?因为css样式不同
name: socket.nickname,
img: img,
msg: data.msg,
color: data.color,
type: data.type,
side: 'right'
});
});
// 断开连接时
socket.on('disconnect', ()=> {
var index = users.indexOf(socket.nickname);
if(index > -1 ) { // 避免是undefined
users.splice(index, 1); // 删除用户信息
usersInfo.splice(index, 1); // 删除用户信息
io.emit('system', { // 系统通知
name: socket.nickname,
status: '离开'
});
io.emit('disUser', usersInfo); // 重新渲染
console.log('a user left.');
}
});
});
http.listen(3000, function() {
console.log('listen 3000 port.');
});
// chat-client.js
$(function() {
// io-client
// 客户端通过 socket.io 模块的实例化对象 io 建立与服务端的连接
// 连接成功会触发服务器端的connection事件
var socket = io();
// 点击输入昵称,回车登录
$('#name').keyup((ev)=> {
if(ev.which == 13) {
inputName();
}
});
$('#nameBtn').click(inputName);
// 登录成功,隐藏登录层
socket.on('loginSuc', ()=> {
$('.name').hide();
})
socket.on('loginError', ()=> {
alert('用户名已存在,请重新输入!');
$('#name').val('');
});
function inputName() {
var imgN = Math.floor(Math.random()*4)+1; // 随机分配头像
if($('#name').val().trim()!=='')
socket.emit('login', { // 触发服务器端登录事件
name: $('#name').val(),
img: 'image/user' + imgN + '.jpg'
});
return false;
}
// 系统提示消息
socket.on('system', (user)=> {
var data = new Date().toTimeString().substr(0, 8);
$('#messages').append(`${data}
${user.name} ${user.status}了聊天室
`);
// 滚动条总是在最底部
$('#messages').scrollTop($('#messages')[0].scrollHeight);
});
// 监听抖动事件
socket.on('shake', (user)=> {
var data = new Date().toTimeString().substr(0, 8);
$('#messages').append(`${data}
${user.name}发送了一个窗口抖动
`);
shake();
// 滚动条总是在最底部
$('#messages').scrollTop($('#messages')[0].scrollHeight);
});
// 显示在线人员
socket.on('disUser', (usersInfo)=> {
displayUser(usersInfo);
});
// 发送消息
// 点击按钮或回车键发送消息
$('#sub').click(sendMsg);
$('#m').keyup((ev)=> {
if(ev.which == 13) {
sendMsg();
}
});
// 接收消息
// 服务器端接受到来自用户的消息后会触发客户端的 receiveMsg 事件,并将用户发送的消息作为参数传递,该事件会向聊天面板添加聊天内容
// 由于发送的是图片,所以对页面布局难免有影响,为了页面美观客户端在接收其他用户发送的消息的时候会先判断发送的是文本还是图片,根据不同的结果展示不同布局。判断的方法是在客户发送消息的时候传入一个 type,根据 type 的值来确实发送内容的类型。所以上面发送图片代码中触发了 sendMsg 事件,传入参数多了一个 type 属性。
socket.on('receiveMsg', (obj)=> { // 将接收到的消息渲染到面板上
// 发送为图片
if(obj.type == 'img') {
$('#messages').append(`
${obj.img} ">
${obj.name}
${obj.msg}
`);
// 滚动条总是在最底部
$('#messages').scrollTop($('#messages')[0].scrollHeight);
return;
}
// 提取文字中的表情加以渲染
var msg = obj.msg;
var content = '';
while(msg.indexOf('[') > -1) { // 其实更建议用正则将[]中的内容提取出来
var start = msg.indexOf('[');
var end = msg.indexOf(']');
content += ''+msg.substr(0, start)+'';
content += '+msg.substr(start+6, end-start-6)+').png">';
msg = msg.substr(end+1, msg.length);
}
content += ''+msg+'';
$('#messages').append(`
${obj.img} ">
${obj.name}
${obj.color};">${content}
`);
// 滚动条总是在最底部
$('#messages').scrollTop($('#messages')[0].scrollHeight);
});
// 发送消息
var color = '#000000';
function sendMsg() {
if($('#m').val() == '') { // 输入消息为空
alert('请输入内容!');
return false;
}
color = $('#color').val();
socket.emit('sendMsg', {
msg: $('#m').val(),
color: color,
type: 'text'
});
$('#m').val('');
return false;
}
var timer;
function shake() {
$('.main').addClass('shaking');
clearTimeout(timer);
timer = setTimeout(()=> {
$('.main').removeClass('shaking');
}, 500);
}
// 显示在线人员
function displayUser(users) {
$('#users').text(''); // 每次都要重新渲染
if(!users.length) {
$('.contacts p').show();
} else {
$('.contacts p').hide();
}
$('#num').text(users.length);
for(var i = 0; i < users.length; i++) {
var $html = `
${users[i].img} ">
${users[i].name}
`;
$('#users').append($html);
}
}
// 清空历史消息
$('#clear').click(()=> {
$('#messages').text('');
socket.emit('disconnect');
});
// 发送表情其实很简单,将表情图片放在 li 中,当用户点击 li 时就将表情的 src 中的序号解析出来,用 [emoji + 表情序号] 的格式存放在聊天框里,点击发送后再解析为 src。就是一个解析加还原的过程,这一过程中我们的服务器代码不变,需要改变的是客户端监听的 receiveMsg 事件
// 渲染表情
init();
function init() {
for(var i = 0; i < 141; i++) {
$('.emoji').append('+ (i+1)+').png">');
}
}
// 显示表情选择面板
$('#smile').click(()=> {
$('.selectBox').css('display', "block");
});
$('#smile').dblclick((ev)=> {
$('.selectBox').css('display', "none");
});
$('#m').click(()=> {
$('.selectBox').css('display', "none");
});
// 用户点击发送表情
$('.emoji li img').click((ev)=> {
ev = ev || window.event;
var src = ev.target.src;
var emoji = src.replace(/\D*/g, '').substr(6, 8); // 提取序号
var old = $('#m').val(); // 用户输入的其他内容
$('#m').val(old+'[emoji'+emoji+']');
$('.selectBox').css('display', "none");
});
// 当用户点击抖动按钮时会 emit 服务端的抖动事件,服务端会广播该事件使得每个客户端都会抖动窗口
// 用户发送抖动
$('.edit #shake').click(function() {
socket.emit('shake');
});
// 用户发送图片
// 用了 fileReader 对象,可以将我们选中的文件已 64 位输出,然后将结果存放在 reader.result 中,我们选中图片之后,reader.result 就存放的是图片的 src
$('#file').change(function() {
var file = this.files[0]; // 上传单张图片
var reader = new FileReader();
//文件读取出错的时候触发
reader.onerror = function(){
console.log('读取文件失败,请重试!');
};
// 读取成功后
reader.onload = function() {
var src = reader.result; // 读取结果
var img = '+src+'">';
socket.emit('sendMsg', { // 发送
msg: img,
color: color,
type: 'img' // 发送类型为img
});
};
reader.readAsDataURL(file); // 读取为64位
});
});