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