关键词:
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();
新建一个form
form.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
‘); //上传结束
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);
)
小礼物走一走,来简书关注我
socket.io中文手册socket.io中文文档(代码片段)
服务端io.on(‘connection’,function(socket));//监听客户端连接,回调函数会传递本次连接的socketio.sockets.emit(‘String’,data);//给所有客户端广播消息io.sockets.socket(socketid).emit(‘String’,data) 查看详情
textexpress4和socket.io:将socket.io传递给路由。(代码片段)
使用socket.io实现简单的聊天功能(代码片段)
Socket.io实际上是WebSocket的父集,Socket.io封装了WebSocket和轮询等方法首先得在你的项目中安装socket.io$npminstallsocket.io服务端代码:varapp=require(‘http‘).createServer()vario=require(‘socket.io‘)(app)varPORT=5555;varclientCount=0;app 查看详情
text#socket.io#react(代码片段)
text#socket.io#axios(代码片段)
text#socket.io#react(代码片段)
text#socket.io#express(代码片段)
聊天功能插件socket.io(代码片段)
一、Socket.io是什么 是基于时间的实时双向通讯库 基于websocket协议的 前后端通过时间进行双向通讯 配合express快速开发实时应用二、Socket.io和ajax区别 基于不同的网络协议 ajax基于http协议,单向,实时获取... 查看详情
感谢有“你”-------socket.io(代码片段)
我此前曾发过一遍文章有关于如何利用node.js+websocket搭建一个简单的多人聊天室有兴趣的朋友可以关注我的技术客栈---涛涛技术客栈。今天学习了websocket的一个框架---socket.io后瞬间感觉神清气爽,顿感从无尽的回调函数中解脱出... 查看详情
内部socket.io客户端列表位置(代码片段)
我想知道Socket.io是否会在内部进行簿记并允许用户检索客户列表,或者我们是否需要手动跟踪连接客户端列表,如下所示:varServer=require('socket.io');vario=newServer(3980,);constclients=[];io.on('connection',function(socket)clients.push(socket);socket.on('d... 查看详情
javascript#socket.io#react#server.js(代码片段)
socket.io简单总结(代码片段)
1.服务端:io.on(‘connection‘,function(socket));监听客户端连接,回调函数会传递本次连接的socket,一般的代码就写在这个回调里io.sockets.emit(‘String‘,data);给所有客户端广播消息,String就是自定义的事件名称,data是传给客户端的数据... 查看详情
node.js笔记-使用socket.io构建websocket聊天室(代码片段)
先安装socket.ionpminstallsocket.io服务端代码:letapp=require('http').createServer();letio=require('socket.io')(app,cors:true);letport=3000;letclientCount=0;app.listen(port);io.on('connection',function(socket)clientCount++;socket.nickname&... 查看详情
javascriptexpress.js和socket.io使用相同的端口(代码片段)
记一次socket.io的debug记录(代码片段)
背景: 项目开发客服聊天系统,使用socket.io进行开发,前端采用vue-element-admin,后端语言php,项目在本地运行功能正常,但是发布到测试环境的时候,socket的连接一直不成功,可以成功返回socketid,但是请求时并没有将sid作... 查看详情
pythonflask-socket.io向特定客户端发出(代码片段)
使用socket.io将消息发送到多个房间?(代码片段)
是否可以使用socket.io将消息发送到多个房间?发送到1个房间:io.sockets.in(room).emit("id",)发送到N个房间:io.sockets.in(room1,room2,roomN).emit("id",)答案sockets.in方法只接受一个房间作为参数,因此要广播到多个房间,你必须在两个房间之间... 查看详情