您的位置:首页 > 其它

socket.io简易教程(群聊,发送图片,分组,私聊)

2017-08-24 23:04 495 查看

什么是Socket.io?

过去:

由于http是无状态的协议,所以实现聊天等通信功能非常困难,当别人发送一条消息时,服务器并不知道当前有哪些用户等着收消息,所以以前实现聊天通信功能最普遍的就是轮询机制了,客户端定期发一个请求,看看有没有人发送消息到服务器上了,如果有,服务器就将消息发给该客户端。

缺点显而易见,那么多的请求消耗了大量资源,有大量的请求其实是浪费了。

现在:

现在,我们有了WebSocket,他是HTML5的新api。 WebSocket 连接本质上就是一个 TCP 连接,WebSocket会通过http请求建立,建立后的WebSocket会在客户端和服务器端建立一个持久的连接,直到有一方主动的关闭了该连接。所以现在服务器就知道有哪些用户正在连接了,这样通讯就变得相对容易了。

Socket.io:

Socket.io实际上是WebSocket的父集,Socket.io封装了WebSocket和轮询等方法,他会根据情况选择方法来进行通讯。

本篇博客主要是介绍各种功能的实现,完整的demo项目开发请看《聊天室入门实战》

看看我已经部署的聊天项目demo

实战项目源码和本博客的源码都已上传至了github https://github.com/neuqzxy/chat ,欢迎下载,觉得不错就给个星星吧。

入门Socket.io

简单入门

官方文档

首先下载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

从官网上把前端js抄下来:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>群聊</title>
</head>
<body>
<h1>群聊</h1>
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
let socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
</script>

</html>

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“的事件,服务器可以触发来实现客户端与服务器的交互。

后端js

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,他可以将消息广播给所有用户,这就实现了群聊的功能。

客户端

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>群聊</title>
</head>
<body>
<h1>群聊</h1>
<div style="width: 30%; float: left">
输入:<input type="text" id="msginput">
<button id="msgbtn">发送</button>
</div>
<div style="width: 65%; float: right" id="showbox">

</div>
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
let socket = io.connect('http://localhost:3000');
let btn = document.getElementById('msgbtn');
let msginput = document.getElementById('msginput');
let showbox = document.getElementById('showbox');
btn.addEventListener('click', (event) => {
let msg = msginput.value;
let data = {msg: msg};
socket.emit('sendMessage', data);
});
socket.on('receiveMessage', (data) => {
console.log('收到');
let message = document.createElement('div');
message.innerHTML = `${data.id}: ${data.msg}`;
showbox.appendChild(message);
})
</script>

</html>


客户端类似,点击按钮,触发发送消息事件,将消息发给服务器,消息是作为第二个参数传递的,然后监听服务器的收到消息事件(该事件就是实现群发的事件)。

开两个窗口,就能实现消息群发功能了。

图片发送

通过FileReader发送图片

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);
})
});


客户端:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>群聊</title>
</head>
<body>
<h1>群聊</h1>
<div style="width: 30%; float: left">
输入:<input type="text" id="msginput">
<button id="msgbtn">发送</button>
<hr>
<input type="file" id="tupian">
<button onclick="sendImg()">发送图片</button>
</div>
<div style="width: 65%; float: right" id="showbox">

</div>
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
let socket = io.connect('http://localhost:3000');
let btn = document.getElementById('msgbtn');
let msginput = document.getElementById('msginput');
let showbox = document.getElementById('showbox');
btn.addEventListener('click', (event) => {
let msg = msginput.value;
let data = {msg: msg};
socket.emit('sendMessage', data);
});
socket.on('receiveMessage', (data) => {
console.log('收到');
let message = document.createElement('div');
message.innerHTML = `${data.id}: ${data.msg}`;
showbox.appendChild(message);
});
let sendImg = () => {
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);
}
};
socket.on('receiveImg', (data) => {
let ImgDIV = document.createElement('div');
ImgDIV.innerHTML = `<div>${data.id}: <img src="${data.img}" /></div>`;
showbox.appendChild(ImgDIV);
})
</script>

</html>


通过ajax上传

上面使用的是FileReader来实现图片传输的,很简单,下面我们使用ajax来上传图片,较FileReader复杂一些,我们使用formData这个对象实现上传,该对象的好处是不必明确的在xhr对象上设置请求头,XHR会自动的识别数据类型是formData,并配置相关头部信息,我们要做的只是将它直接传给send方法。

客户端:

新建一个按钮,用于ajax传输。

<button onclick="sendImg1()">ajax</button>


关于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 = `<div>${data.id}: <img src="${data.imgUrl}" /></div>`;
showbox.appendChild(ImgDIV);
});


至此,图片发送功能就完成了,还有其他的方法,大家都可以尝试一下。

分组群聊

这里我使用了sea.js,是淘宝团队的加载js的一个工具,非常简单好用。为了节省篇幅,我就不介绍了。大家最好先学习一下用法。

为了实验方便,我们新建02.html, group1.js , group2.js,后端js也重写吧

在客户端js中实现分组

客户端

02.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>socket.io</title>
</head>
<body>
<div style="float: left; width: 30%">
<button onclick="group1()">Group1</button>
<button onclick="group2()">Group2</button>
<hr>
群聊<input type="text" id="msginput">
<button id="sendmsg">发送</button>
</div>
<div style="float: left; width: 65%" id="chatbox">

</div>
</body>
<script src="js/sea.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
let group1 = () => {
console.log('一组');
seajs.use(['./js/group1.js'], (socket) => {
chat(socket);
})
};

let group2 = () => {
console.log('二组');
seajs.use(['./js/group2.js'], (socket) => {
chat(socket);
})
};

/**
* 下面写两个分组的共同的方法
*/
let chat = (socket) => {
let btn = document.getElementById('sendmsg');
let msgInput = document.getElementById('msginput');
btn.addEventListener('click', () => {
let msg = msgInput.value;
let data = {msg: msg};
socket.emit('sendMsg', data);
});
socket.on('receiveMsg', (data) => {
let chatBox = document.getElementById('chatbox');
let div = document.createElement('div');
div.innerHTML = `${data.id}: ${data.msg}`;
chatBox.appendChild(div);
})
}
</script>
</html>
可以看到,这里有两个按钮,分别代表两个不同的分组,每一个按钮绑定了一个事件。代表加入哪一个分组中。在事件函数中我们通过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的,你可以看到分组聊天成功了,并且一个人可以加入多个分组。

在服务端js中实现分组

服务端实现分组主要依靠两个api:

socket.join()

socket.leave()

一个负责添加用户,一个负责删除。

socket.to负责找到该组别

03.html:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div style="float: left; width: 30%">
<button onclick="group1()">Group1</button>
<button onclick="group2()">Group2</button>
<hr>
群聊<input type="text" id="msginput">
<button id="sendmsg">发送</button>
<button id="sendtoourgroup">发给本组用户</button>
</div>
<div style="float: left; width: 65%" id="chatbox">
</div>
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
let socket = io.connect('http://localhost:3000');
let group1 = () => {
socket.emit('addgroup1');
};
let group2 = () => {
socket.emit('addgroup2');
};
let btn = document.getElementById('sendmsg');
let msgInput = document.getElementById('msginput');
btn.addEventListener('click', () => {
let msg = msgInput.value;
let data = {msg: msg};
socket.emit('sendMsg', data);
});
socket.on('receiveMsg', (data) => {
let chatBox = document.getElementById('chatbox');
let div = document.createElement('div');
div.innerHTML = `${data.id}: ${data.msg}`;
chatBox.appendChild(div);
});
let btn2 = document.getElementById('sendtoourgroup');
btn2.addEventListener('click', () => {
let msg = msgInput.value;
let data = {msg: msg};
socket.emit('sendToOurGroup', data);
})
</script>
</html>


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文件夹是 《聊天室入门实战》系列的文件夹
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: