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抄下來:

<!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文件夾是 《聊天室入門實戰》系列的文件夾

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章