由于http是无状态的协议,所以实现聊天等通信功能非常困难,当别人发送一条消息时,服务器并不知道当前有哪些用户等着收消息,所以以前实现聊天通信功能最普遍的就是轮询机制了,客户端定期发一个请求,看看有没有人发送消息到服务器上了,如果有,服务器就将消息发给该客户端。
缺点显而易见,那么多的请求消耗了大量资源,有大量的请求其实是浪费了。
现在,我们有了WebSocket,他是HTML5的新api。 WebSocket 连接本质上就是一个 TCP 连接,WebSocket会通过http请求建立,建立后的WebSocket会在客户端和服务器端建立一个持久的连接,直到有一方主动的关闭了该连接。所以现在服务器就知道有哪些用户正在连接了,这样通讯就变得相对容易了。
Socket.io实际上是WebSocket的父集,Socket.io封装了WebSocket和轮询等方法,他会根据情况选择方法来进行通讯。
本篇博客主要是介绍各种功能的实现,完整的demo项目开发请看《聊天室入门实战》
看看我已经部署的聊天项目demo
实战项目源码和本博客的源码都已上传至了github https://github.com/neuqzxy/chat ,欢迎下载,觉得不错就给个星星吧。
官方文档
首先下载express和socket.io:
npm init
npm install --save express socket.io
然后新建一个app.js的文件,引包:
/**
* Created by zhouxinyu on 2017/8/24.
*/
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000, () => {
console.log('server running at 127.0.0.1:3000');
});
上面这一段和我们平时的写法不太一样,因为socket.io是tcp连接,大家将它当做一个公式记住就可以了,下面的内容以前用app.get现在还是app.get,不受影响。
然后新建public文件夹用于存放前端静态资源,在public里新建一个01.html文件,在app.js中使用express将public文件夹静态出来。
app.use(express.static('./public'));
接下来就是socket.io了
从官网上把前端js抄下来:
群聊
群聊
src:这里的src就是这样的,这个socket.io.js代码不是放在静态资源目录中的,现在实际请求的路径是127.0.0.1:3000/socket.io/socket.io.js, 你访问一下就会看到js代码了,当公式记住就行,没什么特别的。
io.connect: 这里面的路径实际上代表的是命名空间,这里是默认的命名空间“/”,如果URL是“http://localhost/abc"那就代表http请求连接到localhost下的abc命名空间中,如果不理解暂时当公式记住,到命名空间那一节再详细讲解。
socket.on:这个会jquery和node的应该就能猜出来了,这就是一个注册事件的api,实际上,我们通过socket.io进行通讯主要就是操作各种事件,这里注册了一个叫”new“的事件,服务器可以触发来实现客户端与服务器的交互。
io.on('connection', (socket) => {
});
和前端代码类似,这里一来就监听连接事件,之后的代码都在回调里写,因为必须要保持连接才能和响应事件。该回调里的参数socket就是这次连接中服务器和该客户端的socket,具有唯一性。
/**
* Created by zhouxinyu on 2017/8/24.
*/
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000, () => {
console.log('server running at 127.0.0.1:3000');
});
app.use(express.static('./public'));
/* socket.io 逻辑 */
io.on('connection', (socket) => {
socket.on('sendMessage', (data) => {
data.id = socket.id;
io.emit('receiveMessage', data);
})
});
就是监听客户端的发送消息事件然后通过io.emit触发群发事件。逻辑很简单没什么好说的,主要注意两点:
1. socket.id是socket的一个属性,存着这次socket连接的id,是唯一标识的,我们实现私聊就可以通过该id找到用户。
2. io.emit是触发广播的一个api,他可以将消息广播给所有用户,这就实现了群聊的功能。
群聊
群聊
输入:
客户端类似,点击按钮,触发发送消息事件,将消息发给服务器,消息是作为第二个参数传递的,然后监听服务器的收到消息事件(该事件就是实现群发的事件)。
开两个窗口,就能实现消息群发功能了。
FileReader是HTML5的新特性,用于读取文件。这里是介绍
我们使用readAsDataURL来读取图片,这样读取出来的内容是base64格式的直接放在图片的src中就可以被解析了,非常方便
下面是主要的代码:
let Imginput = document.getElementById('tupian');
let file = Imginput.files[0]; //得到该图片
let reader = new FileReader(); //创建一个FileReader对象,进行下一步的操作
reader.readAsDataURL(file); //通过readAsDataURL读取图片
reader.onload =function () { //读取完毕会自动触发,读取结果保存在result中
let data = {img: this.result};
socket.emit('sendImg', data);
}
我们先实例化一个reader对象,然后通过指定格式读取文件,读取完毕后就将结果发送给服务器,由服务器广播给所有用户。
下面是实现的代码:
服务端:
/**
* Created by zhouxinyu on 2017/8/24.
*/
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000, () => {
console.log('server running at 127.0.0.1:3000');
});
app.use(express.static('./public'));
/* socket.io 逻辑 */
io.on('connection', (socket) => {
socket.on('sendMessage', (data) => {
data.id = socket.id;
io.emit('receiveMessage', data);
});
socket.on('sendImg', (data) => {
data.id = socket.id;
io.emit('receiveImg', data);
})
});
客户端:
群聊
群聊
输入:
上面使用的是FileReader来实现图片传输的,很简单,下面我们使用ajax来上传图片,较FileReader复杂一些,我们使用formData这个对象实现上传,该对象的好处是不必明确的在xhr对象上设置请求头,XHR会自动的识别数据类型是formData,并配置相关头部信息,我们要做的只是将它直接传给send方法。
客户端:
新建一个按钮,用于ajax传输。
关于formData的使用很简单,只需要两步:
1. 实例化一个formData对象
let formData = new FormData();
2. 传入文件
formData.append(file.name, file);
完毕,我们只需要将formData传给send方法就行了。
关于ajax的用法这里不再赘述,大家可以使用jquery封装的ajax $.post。
let sendImg1 = () => {
let formData = new FormData();
let Imginput = document.getElementById('tupian');
let file = Imginput.files[0];
formData.append(file.name, file);
//ajax
let xhr = new XMLHttpRequest();
xhr.open('POST', '/sendimg', true);
xhr.send(formData);
xhr.onreadystatechange = () => {
if(xhr.readyState === 4) {
if((xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304)) {
console.log('success');
let data = {imgName: xhr.responseText};
socket.emit('ajaxImgSendSuccess', data);
}
else {
console.log(xhr.readyState,xhr.status)
}
} else {
console.log(xhr.readyState);
}
};
}
我们在传输完成之后,就得到服务器的返回值,我们需要让服务器返回刚刚上传的图片的名字(也可以是路径)
服务端
服务端使用了formidable。用法也不再赘述。
app.post('/sendimg', (req, res, next) => {
let imgname = null;
let form = new formidable.IncomingForm();
form.uploadDir = './static/images';
form.parse(req, (err, fields, files) => {
res.send(imgname);
});
form.on('fileBegin', (name, file) => {
file.path = path.join(__dirname, `./static/images/${file.name}`);
imgname = file.name;
});
});
我们新建一个static文件夹,里面的images文件夹放传上来的图片。传输完成之后,将图片名称发给客户端,这时,客户端就可以填写图片URL访问我们的图片了。
所以,我们必须要将static文件夹静态出来:
app.use('/static', express.static(path.join(__dirname, './static')));
当客户端接收到名称后,就触发事件,然后由服务器广播:
触发事件
if((xhr.status >= 200 && xhr.status < 300) || (xhr.status === 304)) {
console.log('success');
let data = {imgName: xhr.responseText};
socket.emit('ajaxImgSendSuccess', data); //触发事件
}
服务器广播:
socket.on('ajaxImgSendSuccess', (data) => {
data.id = socket.id;
data.imgUrl = `/static/images/${data.imgName}`;
io.emit('receiveAjaxImgSend', data);
})
客户端接收广播,显示图片:
socket.on('receiveAjaxImgSend', (data) => {
let ImgDIV = document.createElement('div');
ImgDIV.innerHTML = `${data.id}: `;
showbox.appendChild(ImgDIV);
});
至此,图片发送功能就完成了,还有其他的方法,大家都可以尝试一下。
这里我使用了sea.js,是淘宝团队的加载js的一个工具,非常简单好用。为了节省篇幅,我就不介绍了。大家最好先学习一下用法。
为了实验方便,我们新建02.html, group1.js , group2.js,后端js也重写吧
客户端
02.html:
socket.io
群聊
可以看到,这里有两个按钮,分别代表两个不同的分组,每一个按钮绑定了一个事件。代表加入哪一个分组中。在事件函数中我们通过sea.js加载该分组的js文件。
group1.js:
/**
* Created by zhouxinyu on 2017/8/24.
*/
define(function (require, exports, module) {
const socket = io.connect('http://localhost:3000/group1');
module.exports = socket;
});
group2.js
/**
* Created by zhouxinyu on 2017/8/24.
*/
define(function (require, exports, module) {
const socket = io.connect('http://localhost:3000/group2');
module.exports = socket;
});
很简单的两个js文件,和node.js的module.exports一样,将socket导出,在前端使用
seajs.use(['./js/group2.js'], (socket) => {
chat(socket);
})
socket被传入回调中。
其实分组的代码只有一句:
const socket = io.connect('http://localhost:3000/group1');
const socket = io.connect('http://localhost:3000/group2');
服务端:
服务端分组的代码只有两个
let group1 = io.of('/group1');
let group2 = io.of('/group2');
分别代表加入group1和group2组
然后再group1和group2上写监听就可以了,其余没什么特别的:
/**
* Created by zhouxinyu on 2017/8/24.
*/
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000, () => {
console.log('server running at 127.0.0.1:3000');
});
app.use(express.static('./public'));
/* socket.io 逻辑 */
let group1 = io.of('/group1');
let group2 = io.of('/group2');
group1.on('connection', (socket) => {
socket.on('sendMsg', (data) => {
data.id = socket.id;
group1.emit('receiveMsg', data);
})
});
group2.on('connection', (socket) => {
socket.on('sendMsg', (data) => {
data.id = socket.id;
group2.emit('receiveMsg', data);
})
});
到了这里代码就写完了,你可以开4个窗口连接127.0.0.1:3000/02.html,然后两个group1的两个group2的,你可以看到分组聊天成功了,并且一个人可以加入多个分组。
服务端实现分组主要依靠两个api:
socket.join()
socket.leave()
一个负责添加用户,一个负责删除。
socket.to负责找到该组别
03.html:
Title
群聊
fenzu.js:
/**
* Created by zhouxinyu on 2017/8/24.
*/
const express = require('express');
const app = express();
const server = require('http').Server(app);
const io = require('socket.io')(server);
server.listen(3000, () => {
console.log('server running at 127.0.0.1:3000');
});
app.use(express.static('./public'));
/* socket.io 逻辑 */
io.on('connection', (socket) => {
socket.on('addgroup1', () => {
socket.join('group1', () => {
let data = {id: '系统', msg: '新用户加入'};
socket.to('group1').emit('receiveMsg', data);
console.log(Object.keys(socket.rooms));
})
});
socket.on('addgroup2', () => {
socket.join('group2', () => {
let data = {id: '系统', msg: '新用户加入'};
socket.to('group2').emit('receiveMsg', data);
console.log(Object.keys(socket.rooms));
})
});
socket.on('sendMsg', (data) => {
data.id = socket.id;
io.emit('receiveMsg', data);
});
socket.on('sendToOurGroup', (data) => {
data.id = socket.id;
let groups = Object.keys(socket.rooms);
for(let i = 1; i <= groups.length; i++) {
socket.to(groups[i]).emit('receiveMsg', data);
}
socket.emit('receiveMsg', data);
})
});
私聊其实就是找到该用户的socket然后触发socket就行。所以有两个方法:
1. 直接将所有用户的socket保存到一个数组中,以用户名为键,要发给谁直接从数组中找。
2. 还是以用户名为键,但是以socket.id为值,找到id后,再通过id找到该socket。
我们使用第二种方法,第一种比较浪费资源。我的一个部署的项目实际上用的是第一种方法www.mycollagelife.com
第二种方法实际上也是socket.to(id)这个api发送的,具体就不在详细写了,大家那么聪明,看了分组之后一定能够举一反三吧。
项目已经上传至github https://github.com/neuqzxy/chat 其中socket是该博客的文件夹,还有两个文件夹,chat文件夹是 《聊天室入门实战》系列的文件夹