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

什么是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抄下来:

 




    
    群聊


群聊


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

 

客户端

 




    
    群聊


群聊

输入:


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

 

 

 

 

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

 

图片发送

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

 

 

客户端:

 




    
    群聊


群聊

输入:

 

 

 

通过ajax上传

上面使用的是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也重写吧

在客户端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的,你可以看到分组聊天成功了,并且一个人可以加入多个分组。

 

 

 

 

在服务端js中实现分组

服务端实现分组主要依靠两个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文件夹是 《聊天室入门实战》系列的文件夹

socket.io简易教程(群聊,发送图片,分组,私聊)_第1张图片加入QQ群一起交流讨论。

今日头条长期招聘前端等各个岗位啦   内推发送简历到[email protected]哦~

你可能感兴趣的:(chat聊天实战)