socket
- socket.io一個是基於Nodejs架構體系的,支持websocket的協議用於實時通信的一個軟件包。
- socket.io 給跨瀏覽器構建實時應用提供了完整的封裝,socket.io完全由javascript實現
依賴的外部包
express、socket.io
安裝
- npm install --save-dev express
- npm install --save-dev socket.io
- 默認會在項目下新建一個node_module文件,引入express和socket.io的外部包
服務器server:
var express = require('express'); var app = express(); var http = require('http'); //創建一個服務器 var server = http.createServer(app); //監聽端口 var port = normalizePort(process.env.PORT || '3000'); server.listen(port); app.set('views', path.join(__dirname, 'views')); //服務器端引入socket.io var io = require('socket.io').listen(server); io.on('connection', function(socket){ socket.on('message', function () { }); socket.on('disconnect', function(){...}); });
客戶端client
//客戶端引入socket var socket = io(); socket.on('connect', function () { socket.send('hi'); socket.on('message', function (msg) { // my msg }); });
原理
- 服務器保存好所有的 Client->Server 的 Socket 連接,
- Client A 發送消息給 Client B 的實質是:Client A -> Server -> Client B。
- 即 Client A 發送類似
{from:'Client A', to:'Client B', body: 'hello'}的數據給 Server。 - Server 接收數據根據 to值找到 Client B 的 Socket 連接並將消息轉發給 Client B
使用
- 使用socket.io,其前后端句法是一致的。
- 即通過socket.emit() 來激發一個事件;
- 通過socket.on() 來監聽和處理對應事件;
- 這兩個事件通過傳遞的參數進行通信。
服務器信息傳輸基本語法
- 所有客戶端
// send to current request socket client // 發送一個請求的當前請求的socket客戶端 socket.emit('message', "this is a test"); // sending to all clients except sender // 廣播消息,不包括當前的發送者 socket.broadcast.emit('message', "this is a test"); // sending to all clients, include sender // 發送消息給所有客戶端,包括發送者 io.sockets.emit('hi', 'everyone'); io.emit('hi', 'everyone'); // 寫的簡單點:
- 房間內發送
// sending to all clients in 'room1' room except sender // 給房間room1的所有客戶端發送消息,不包括發送者 socket.broadcast.to('room1').emit('message', 'hello'); // sending to all clients in 'room1' room(channel), include sender // 給房間room1的所有客戶端發送消息,包括發送者 io.sockets.in('room1').emit('message', 'hello');
- 指定發送給單個用戶
// sending to individual socketid // 給單個用戶socketId發送消息 io.sockets.socket(socketId).emit('message', 'for your eyes only');
socket.set和socket.get方法分為用於設置和獲取變量。
io.sockets.on('connection', function (socket) { socket.on('set nickname', function (name) { socket.set('nickname', name, function () { socket.emit('ready'); }); }); socket.on('msg', function () { socket.get('nickname', function (err, name) { console.log('Chat message by ', name); }); }); });
socket.join()加入房間 && socket.leave()離開房間
io.on('connection', function(socket){ //加入房間 socket.join('some room'); //用to或者in是一樣的,用emit來給房間激發一個事件 io.to('some room').emit('some event'): //socket.leave('some room'); }); io.on('disconnection', function(socket){ //一旦disconneted,那么會自動離開房間 ... });
socket.send()和socket.recv()消息的發送和接收
- socket.emit()和socket.send()的區別
- socket.emit allows you to emit custom events on the server and client
- socket.send sends messages which are received with the 'message' event
數組操作
新建一個數組
var onlineList = [];
添加元素到數組
onlineList.push(uid);
判斷元素是不是在數組
onlineList.indexOf(uid)
- 返回值:
- -1:不在數組中
- 其他數值:對應的下標
刪除數據
index = onlineList.indexOf(uid) //找到對應的下標 onlineList.splice(index,1) //刪除index到index+1的數據,也就是刪除下標為index的數據
- 請注意,splice() 方法與 slice() 方法的作用是不同的,splice() 方法會直接對數組進行修改。
數據庫設計和學習:
用戶數據結構:包含用戶名,密碼和圖片
var userSchema = new Schema({ username: String, password: String, imgUrl: String, meta: { updateAt: {type:Date, default: Date.now()}, createAt: {type:Date, default: Date.now()} } });
朋友數據結構
- 包含uid--自身的id值,fid--朋友的id值
- mongodb數據庫每次新建一個對象,都會默認給這個對象一個唯一的_id值,作為這個對象的唯一標識符
- 將uid的類型定義為
ObjectId,設置引用ref為User - 在查詢消息的時候可以同時查詢兩張表,而默認的_id值也就是他查詢的鍵
var mongoose = require('../db'); var Schema = mongoose.Schema; var ObjectId = Schema.Types.ObjectId; var friendSchema = new Schema({ uid: {type:ObjectId, ref:'User'}, fid: {type:ObjectId, ref:'User'}, meta: { updateAt: {type:Date, default: Date.now()}, createAt: {type:Date, default: Date.now()} } });
消息數據結構
- 消息是兩個用戶之間的通信,因此需要
from和to兩個對象 - 同時也需要
uid
var messageSchema = new Schema({ uid: {type:ObjectId, ref:'User'},//用戶 from: {type:ObjectId, ref:'User'},//發送給誰 to: {type:ObjectId, ref:'User'},//誰接收 msg: String, type: Number,//已讀1 or 未讀0 meta: { updateAt: {type:Date, default: Date.now()}, createAt: {type:Date, default: Date.now()} } });
新建一個用戶數據
$("body").on('click', '#registerBtn', doRegister);- 點擊body中的id為
registerBtn的按鈕,執行doRegister函數
-
ajax是一種異步的請求,當用戶每次輸入一定的值,服務器都會把這個值傳遞過來
-
$("#usr").val()--用jquery的方式獲取id為usr的表單的值 -
$("#userThumb").attr("src")--用jquery的方式獲取id為userThumb的屬性src的值,獲取圖片的路徑 -
JSON.stringify是將傳遞過來的數據轉換為JSON格式 -
如果成功,那么success,執行后面的
function(); -
$.cookie('username', result.data.username, {expires:30});是利用jquery.cookie.js將數據存放到cookie里面
function doRegister() { $.ajax({ type: "POST", //方式post url: "/register", //路徑register contentType: "application/json", dataType: "json", //數據類型json格式 data: JSON.stringify({ 'usr': $("#usr").val(), //用戶名 'pwd': $("#pwd").val(), //密碼 'imgUrl': $("#userThumb").attr("src") //圖片 }), success: function(result) { if (result.code == 99) { //失敗彈出錯誤信息 console.log("注冊失敗") } else { //成功就將輸入的數據作為cookies存入 console.log("注冊成功"); console.log(result.data); $.cookie('username', result.data.username, {expires:30}); $.cookie('password', result.data.password, {expires:30}); $.cookie('imgUrl', result.data.imgUrl, {expires:30}); $.cookie('id', result.data._id, {expires:30}); location.href = "/webchat"; //跳轉到聊天界面 } } }) }
我們可以通過拆分的方法,將上面的代碼拆解為幾個版塊
- 將路由定義為一個變量
var urlRegister = "/register";
2.將上面的一段代碼提煉出骨干
function postData(url, data, cb) { var promise = $.ajax({ type: "post", url: url, //傳遞過來的post路徑 dataType: "json", contentType: "application/json", data:data //傳遞過來的data }); promise.done(cb); //執行cb回調函數 }
3.將數據轉換為JSON格式,傳遞參數到postData(),執行函數
var jsonData = JSON.stringify({ 'usr': $("#usr").val(), //用戶名 'pwd': $("#pwd").val(), //密碼 'imgUrl': $("#userThumb").attr("src") }); postData(urlRegister, jsonData, cbRegister);
4.cbRegster()函數
function cbRegister(result) { console.log(result); if (result.code == 99) { //失敗彈出錯誤信息 console.log("注冊失敗") } else { //成功就將輸入的數據作為cookies存入 console.log("注冊成功"); console.log(result.data); $.cookie('username', result.data.username, {expires:30}); $.cookie('password', result.data.password, {expires:30}); $.cookie('imgUrl', result.data.imgUrl, {expires:30}); $.cookie('id', result.data._id, {expires:30}); location.href = "/webchat"; //跳轉到聊天界面 } }
頭像上傳
$("body").on('change', '#uploadFile', preUpload); $("body").on('click', '#UploadBtn', doUpload);
- 表單傳遞的方式設置為
POST post路徑設置為/uploadImage- 傳遞過來的數據類型為
form - 最后如果上傳成功,那么將id為
userThumb的src屬性設置為傳遞過來的data
function doUpload() { //取出上傳過來的文件 var file = $("#uploadFile")[0].files[0]; //與普通的Ajax相比,使用FormData的最大優點就是可以異步上傳二進制文件。 var form = new FormData(); form.append("file", file); $.ajax({ url: "/uploadImg", //路徑設置為uploadImage type: "POST", data: form, //數據格式為form async: true, processData: false, contentType: false, success: function(result) { startReq = false; if (result.code == 0) { //將id為userThumb的src屬性設置為傳遞過來的data $("#userThumb").attr("src", result.data); } } }); }
通過formidable這個npm包來實現圖片的上傳
- 安裝:
npm install --save-dev formidable - 引入:
var formidable = require('formidable'); - 圖片
post到/uploadImg var form = new formidable.IncomingForm();新建一個formform.uploadDir = "./public/thumb";設置文件存放的位置,自己事先定義好用來存放圖片的文件夾- 這邊有一個問題是上傳圖片之后,圖片的路徑是window的路徑'/',而我們在瀏覽器渲染要手動修改為''
router.post('/uploadImg', function(req, res, next) { var form = new formidable.IncomingForm(); var path = ""; var fields = []; form.encoding = 'utf-8'; form.uploadDir = "./public/thumb";//存放文件的位置 form.keepExtensions = true; form.maxFieldsSize = 30000 * 1024 * 1024; var uploadprogress = 0; console.log("start:upload----"+uploadprogress); //開始上傳 form.parse(req); form.on('field', function(field, value) { console.log(field + ":" + value); }) .on('file', function(field, file) { path = '\\' + file.path; //獲取文件的本地路徑 }) .on('progress', function(bytesReceived, bytesExpected) { uploadprogress = (bytesReceived / bytesExpected * 100).toFixed(0); console.log("upload----"+ uploadprogress); //上傳中 }) .on('end', function() { console.log('-> upload done\n'); //上傳結束 entries.code = 0; entries.data = path; //將路徑賦給data res.writeHead(200, { 'content-type': 'text/json' }); res.end(JSON.stringify(entries)); //將entries轉換為JSON格式 }) .on("err",function(err){ //發生錯誤 var callback="<script>alert('"+err+"');</script>"; res.end(callback); }) .on("abort",function(){ //中斷 var callback="<script>alert('"+ttt+"');</script>"; res.end(callback); }); });
- 最后用post過來的user創建一個新的user數據對象
router.post('/register', function(req, res, next) { //添加用戶 dbHelper.addUser(req.body, function (success, doc) { res.send(doc); }) });
exports.addUser = function(data, cb) { var user = new User({ username: data.usr, password: data.pwd, imgUrl: data.imgUrl }); user.save(function(err, doc) { if (err) { cb(false, err); } else { cb(true, entries); } }) };
這樣整個上傳的邏輯就已經寫完了,接下來是添加一個朋友,和上面的做法一致。
唯一不同的是,我們在添加朋友的時候,一般都是相互之間都成為朋友的,所以在新建的時候要同時新建兩個user
var friend_me = new Friend({ uid: data.uid,//自己的id fid: data.fid }); var friend_frd= new Friend({ uid: data.fid,//朋友的id fid: data.uid });
保存也需要同時保存兩個新的對象
這里采用的是async的並行parallel操作,async的引入是通過var async = require('async');
async.parallel({ one: function(callback) { //保存自己 friend_me.save(function(err, doc) { callback(null, doc); }) }, two: function(callback) { //保存朋友 friend_frd.save(function(err, doc) { callback(null, doc); }) } }, function(err, results) { // results is now equals to: {one: 1, two: 2} cb(true, entries); });
消息的傳遞也需要同時創建兩個消息,一個用來發給自己,另一個是發給朋友,保存的方式和朋友一致
var message_me = new Message({ uid: data.uid, //自己 from: data.from, to: data.to, type: config.site.ONLINE,//在線 message: data.msg }); var message_friend = new Message({ uid: data.to, //朋友,data.to中保存的是朋友的fid from: data.from, to: data.to, type: data.type,//朋友需要判斷是否在線 message: data.msg });
數據表的查詢
方式一,findOne
User.findOne({username: data.usr }, function(err, doc) { ...... } })
方式2. find()+ exec(函數體), 其中exec是execute執行下一個函數的意思
User.find()
.exec(function(err, docs) { ...... })
方式3.
- 兩張表之間的查詢,mongodb提供了
populate方法用來查詢兩張表 - 索引號也就是_id
- populate()函數可以帶兩個參數,第一個參數是查詢的外鍵對應的數據表,第二個可以規定需要查詢的字段,比如'username'。
Friend.find({'uid': uid}) //找到uid對應的uid .populate('fid') //查找fid對應的user表 .exec(function(err, docs){ .... })
點對點聊天的實現
-
首先用戶加入到一個唯一的sessionId的房間
socket.emit('join', sessionId); -
用戶發送消息給socket
socket.send(_id,fid,msg); -
socket給uid發送消息msg
io.to(uid).emit('msg', uid,fid,msg); -
socket給fid發送消息msg
io.to(fid).emit('msg', uid,fid,msg); -
服務器端監聽消息
socket.on('message', function(uid,fid,msg){ var type;//在線還是不在線 if(onlineList.indexOf(fid) === -1){//判斷朋友是不是在線 type= config.site.OFFLINE;//用戶不在線 //socket給自己發送消息不在線 io.to(uid).emit('msg', uid,fid,msg); }else { type=config.site.ONLINE;//在線 io.to(fid).emit('msg', uid,fid,msg);//socket給朋友發送消息 io.to(uid).emit('msg', uid,fid,msg);//socket給自己發送消息 } //構建一個data的json數據 var data = { "uid": uid, "from": uid,//自己 "to": fid,//朋友 "type": type, "msg": msg }; //調用dbHelper中的addMessage函數來將消息存放到數據庫 dbHelper.addMessage(data, function(success,data){ ... }); });
- 客戶端socket.on('msg')來監聽消息的發送
socket.on('msg', function(uid, fid, msg) { fromID = (_id == fid)?uid:fid; //接受到的消息的發送人id if (_id == fid) { fImg = $('#'+uid).children('img').attr('src');//獲取到圖片路徑 message = $.format(TO_MSG, fImg, msg)//格式化為發送的消息 } else { message = $.format(FROM_MSG, _img, msg); //格式化為收到的消息 } $("#v"+fromID).append(message); //將消息append添加到前端 $("#v"+fromID).scrollTop($("#v"+fromID)[0].scrollHeight); });
如何使session唯一
如果用戶與用戶之間的聊天不是在同一個聊天室的話,那么他們的聊天消息會出錯
所以我們要為用戶指定一個唯一的聊天室id
- A先加入,A-a_id,B加入,b_id,
- A->B: sid=a_id+b_id;
- B->A: sid=a_id+b_id;
- 這樣session的值就唯一了
roomId = (uid>fid)?(uid+fid):(fid+uid);
歷史消息的處理
存放消息
- 首先在存放歷史消息的時候,給歷史消息一個屬性
type,表示朋友是否在線 - 如果朋友在線,type設置為1
- 如果朋友不在線,type設置為0
- 把消息存放到數據庫里面
取出離線消息
- 用find()方法指定需要取出type為1的消息
- 從form對應的表中取出響應的字段,添加到messageList數組
exports.getOfflineMsg = function (data, cb) { var uid = data.uid; Message.find({'uid':uid, 'type':'1'}) .populate('from') .exec(function(err, docs) { var messageList=new Array(); for(var i=0;i<docs.length;i++) { messageList.push(docs[i].toObject()); } cb(true, messageList); }); }
將取出的消息渲染到前端的頁面
var msg = $.format(TO_MSG, result[i].from.imgUrl, result[i].msg); ... $("#v"+fid).append(msg);
設置離線消息為已讀狀態
var conditions = {'uid':uid, 'from':fid, 'type':'0'};- 按照條件查詢數據庫里面type為0的數據的每一條數據
var update = {$set :{ 'type' : '1'}};- 將數據庫里面的數據的type類型設置為1,表示為已讀狀態
var options = { multi: true };- 使用multi:true`的屬性將數據庫里面全部的數據一次性更新
var uid = data.uid; var fid = data.fid; var conditions = {'uid':uid, 'from':fid, 'type':'0'}; var update = {$set :{ 'type' : '1'}}; var options = { multi: true }; Message.update(conditions,update,options, function(error,data){ if(error) { console.log(error); }else { data.id = fid; cb(true, data); } })
小禮物走一走,來簡書關注我
