下面先用node来实现一个普通的聊天室,这个聊天室不能实现信息的实时同步,只有当客户端访问服务器是才能够得到最新的信息。
服务端代码:
const Koa=require('koa');
const Router=require('koa-router');
const static=require('koa-static');
const render=require('koa-art-template');
const path=require('path');
const session=require('koa-session');
const bodyParser=require('koa-bodyparser');
let app=new Koa();
let router=new Router();
const msgs=[
{username:'Alan',content:'Hello'},
{username:'Bob',content:'Good morning!'},
{username:'Mary',content:'Have a good day!'},
];
app.keys = ["some secret hurr"];
render(app, {
//html页面目录
root: path.join(__dirname, "view"),
//设置后缀名
extname: ".html",
debug: process.env.NODE_ENV !== "production"
});
router.get('/',async (ctx)=>{
ctx.render('index');
})
.post('/login',async ctx=>{
//获取表单中提交过来的数据
let {username} =ctx.request.body;
//将用户名保存到session中
ctx.session.user={username};
//重定向到聊天室
ctx.redirect('/room');
})
.get('/room',async ctx=>{
//将聊天信息渲染到聊天室
ctx.render('room',{user:ctx.session.user,msgs});
})
.post('/sendMsg',async (ctx)=>{
let username=ctx.session.user.username;
let {content}=ctx.request.body;
//将客户端发送过来的数据保存到msgs中,并且将最新的msgs返回给客户端
msgs.push({username,content});
ctx.body=msgs;
})
let store={
myStore:{},
get(key){
return this.myStore[key];
},
set(key,session){
this.myStore[key]=session;
},
destroy(key){
delete this.myStore[key];
}
}
//处理静态资源
app.use(static(path.resolve('./public')));
//将session保存到服务器
app.use(session({store},app));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(8888);
登录页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>聊天室</title>
</head>
<body>
<form action="/login" method="POST">
<input type="text" name="username">
<input type="password" name="password">
<button>登录</button>
</form>
</body>
</html>
聊天室页面:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>聊天室</title>
</head>
<body>
<div class="msgList">
<ul>
{{each msgs}}
<li>
{{$value.username}}:
{{$value.content}}
</li>
{{/each}}
</ul>
</div>
<p>Hello, {{user.username}} ,you can chat with everyone</p>
<input type="text" class="sendMsg"><button class="sendBtn">send message</button>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
$('.sendBtn').click(function(){
var content=$('.sendMsg').val();
$('.sendMsg').val('');
$.ajax({
url: '/sendMsg',
method: 'post',
dataType: 'json',
data: {
content:content
},
success:function(data){
var contents='<ul>';
for(var i=0;i<data.length;i++){
contents+='<li>'+data[i].username+':'+data[i].content+'</li>'
}
contents+='</ul>';
$('.msgList').html(contents);
}
});
})
</script>
</body>
</html>
那我们要解决实时同步消息的问题要怎么做呢?
第一种方法我们可以让Ajax请求轮询(就是把Ajax请求写在setInterval里面,这样就一直向服务器发送请求)。缺点:不断询问服务器,浪费性能
第二种方法是使用H5中的eventSource来完成
第三种则是使用H5的websocket 。缺点:兼容性问题(IE11)
总结几种方式:
- 长轮询 : 客户端不停问,服务器不停回
- 长连接: 客户端一次,服务器多次(服务器向客户端单向输出)
- ws(握手) WebSocket
- 全双工(双向工作(客户端和服务器))通信
下面介绍主角socket.io
首先安装socket.io-client和koa-socket
npm i socket.io-client koa-socket -S
这里我们才有的思路是:
当登录成功后创建一个关于时间戳的id并且保存在session.user.id
中,并创建一个自己的session-->mySession
,将时间戳id做为key值,并把用户的username保存至mySession[id]
中,但重定向到聊天室页面时,把id渲染到页面上去,在聊天室页面初始化socket连接时,再次将id返回服务器中,并将当前socket的对应的唯一id保存到自定义的session中。
然后发送信息是通过socket的id在自定义的session查找到所对应的用户名即可。
const Koa=require('koa');
const Router=require('koa-router');
const static=require('koa-static');
const render=require('koa-art-template');
const path=require('path');
const session=require('koa-session');
const bodyParser=require('koa-bodyparser');
const IO=require('koa-socket');
let app=new Koa();
let router=new Router();
const io=new IO();
const msgs = [
{ username: "Alan", content: "Hello" },
{ username: "Bob", content: "Good morning!" },
{ username: "Mary", content: "Have a good day!" }
];
global.mySession={}; //
app.keys = ["some secret hurr"];
function findBySocketId(socketid) {
for (var tempstamp in global.mySession) {
let obj=global.mySession[tempstamp];
if(obj.socketid===socketid){
return obj;
}
}
}
io.attach(app); //附加到app上产生关联
io.on('connection',(ctx,data)=>{
console.log('连接上了一个');
});
//接收用户的消息
io.on('sendMsg',(ctx)=>{
// console.log('some new content:',ctx.data.content);
console.log('当前的socket的id为:',ctx.socket.socket.id);
//查找对象的用户名
let obj=findBySocketId(ctx.socket.socket.id);
//将信息广播给所有人
io.broadcast('allmessage',obj.username+':'+ctx.data.content);
//要想在此处拿到用户名是拿不到的
});
//处理登录同步消息
io.on('login',ctx=>{
let id=ctx.data.id;
//将当前的socket的Id保存到对应的session中
global.mySession[id].socketid=ctx.socket.socket.id;
})
render(app, {
//html页面目录
root: path.join(__dirname, "view"),
//设置后缀名
extname: ".html",
debug: process.env.NODE_ENV !== "production"
});
router.get('/',async (ctx)=>{
ctx.render('index');
})
.post('/login',async ctx=>{
//获取表单中提交过来的数据
let {username} =ctx.request.body;
//将用户名保存到session中
ctx.session.user={username};
//生成时间戳保存在session中
let id=Date.now();
ctx.session.user.id=id;
//保存到自己的session中
global.mySession[id]={
username:username
}
//重定向到聊天室
ctx.redirect('/room');
})
.get('/room',async ctx=>{
//将聊天信息渲染到聊天室
ctx.render('room',{
user:ctx.session.user,
msgs,
id:ctx.session.user.id
});
})
// .post('/sendMsg',async (ctx)=>{
// let username=ctx.session.user.username;
// let {content}=ctx.request.body;
// //将客户端发送过来的数据保存到msgs中,并且将最新的msgs返回给客户端
// msgs.push({username,content});
// ctx.body=msgs;
// })
let store={
myStore:{},
get(key){
return this.myStore[key];
},
set(key,session){
this.myStore[key]=session;
},
destroy(key){
delete this.myStore[key];
}
}
//处理静态资源
app.use(static(path.resolve('./public')));
//将session保存到服务器
app.use(session({store},app));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(8888);
html代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>聊天室</title>
</head>
<body>
<div class="msgList">
<ul>
{{each msgs}}
<li>
{{$value.username}}:
{{$value.content}}
</li>
{{/each}}
</ul>
</div>
<p>Hello, {{user.username}} ,you can chat with everyone</p>
<input type="text" class="sendMsg"><button class="sendBtn">send message</button>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io('http://localhost:8888');
//客户端登录,让服务器保存用户信息
socket.on('connect', function () {
console.log('connected!');
//登录,同步前后端信息
socket.emit('login',{
id:{{id}}
});
});
socket.on('allmessage',function (data) {
console.log(data);
})
socket.on('disconnect', function () {
console.log('connection is unable');
});
$('.sendBtn').click(function(){
var content=$('.sendMsg').val();
socket.emit('sendMsg',{
content:content
});
})
</script>
</body>
</html>
如果我们要实现私聊和群聊要怎么做呢?
私聊只需要记录下要私聊人的socketid即可
使用app._io.to(socketid).emit();
即可完成对相应客户端的emit
群聊则使用join(groupName)
来完成加入一个群组,
然后一样使用app._io.to(socketid).emit();
来完成群发信息
完整代码:
app.js
const Koa=require('koa');
const Router=require('koa-router');
const static=require('koa-static');
const render=require('koa-art-template');
const path=require('path');
const session=require('koa-session');
const bodyParser=require('koa-bodyparser');
const IO=require('koa-socket');
let app=new Koa();
let router=new Router();
const io=new IO();
const group = {
'department':'部门群',
'personal':'私人群'
}
global.mySession={};
app.keys = ["some secret hurr"];
function findBySocketId(socketid) {
for (var tempstamp in global.mySession) {
let obj=global.mySession[tempstamp];
if(obj.socketid===socketid){
return obj;
}
}
}
function findKeyBySocketId(socketid) {
for (var key in global.mySession) {
let obj=global.mySession[key];
if(obj.socketid===socketid){
return key;
}
}
}
io.attach(app); //附加到app上产生关联
io.on('connection',(ctx,data)=>{
console.log('连接上了一个');
});
//接收用户的消息
io.on('sendMsg',(ctx)=>{
// console.log('some new content:',ctx.data.content);
console.log('当前的socket的id为:',ctx.socket.socket.id);
//查找对象的用户名
let obj=findBySocketId(ctx.socket.socket.id);
//将信息广播给所有人
io.broadcast('allmessage',obj.username+':'+ctx.data.content);
//要想在此处拿到用户名是拿不到的
});
//处理登录同步消息
io.on('login',ctx=>{
let id=ctx.data.id;
//将当前的socket的Id保存到对应的session中
global.mySession[id].socketid=ctx.socket.socket.id;
io.broadcast('online',{
online:global.mySession
});
ctx.socket.on('disconnect',(ctx)=>{
//退出后删除掉mySession中对应的用户消息
let socketid=ctx.socket.socket.id;
let key=findKeyBySocketId(socketid);
delete global.mySession[key];
io.broadcast('online',{
online:global.mySession
});
console.log('一个用户退出了');
})
});
io.on("sendPrivateMsg",ctx=>{
//获取接收人的socketid和需要发送的消息
let {receiver,privateMsg}=ctx.data;
//获取发送人的socketid
let senderId=ctx.socket.socket.id;
let {username}=findBySocketId(senderId);
// 向指定客户端emit
app._io.to(receiver).emit("privateMsg", `${username}对你说:${privateMsg}`);
});
io.on("joinGroup",ctx=>{
let groupName=ctx.data;
ctx.socket.socket.join(groupName);
console.log('加入'+groupName);
});
io.on("sendGroupMsg", ctx => {
let { groupName, groupMsg } = ctx.data;
let sender=ctx.socket.socket.id;
let {username}=findBySocketId(sender);
console.log(username);
console.log(groupMsg);
app._io.to(groupName).emit('groupMsg',`
${group[groupName]}的${username}对大家说:${groupMsg}
`);
});
render(app, {
//html页面目录
root: path.join(__dirname, "view"),
//设置后缀名
extname: ".html",
debug: process.env.NODE_ENV !== "production"
});
router.get('/',async (ctx)=>{
ctx.render('index');
})
.post('/login',async ctx=>{
//获取表单中提交过来的数据
let {username} =ctx.request.body;
//将用户名保存到session中
ctx.session.user={username};
//生成时间戳保存在session中
let id=Date.now();
ctx.session.user.id=id;
//保存到自己的session中
global.mySession[id]={
username:username
}
//重定向到聊天室
ctx.redirect('/room');
})
.get('/room',async ctx=>{
//将聊天信息渲染到聊天室
ctx.render('room',{
user:ctx.session.user,
id:ctx.session.user.id
});
})
// .post('/sendMsg',async (ctx)=>{
// let username=ctx.session.user.username;
// let {content}=ctx.request.body;
// //将客户端发送过来的数据保存到msgs中,并且将最新的msgs返回给客户端
// msgs.push({username,content});
// ctx.body=msgs;
// })
let store={
myStore:{},
get(key){
return this.myStore[key];
},
set(key,session){
this.myStore[key]=session;
},
destroy(key){
delete this.myStore[key];
}
}
//处理静态资源
app.use(static(path.resolve('./public')));
//将session保存到服务器
app.use(session({store},app));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(8888);
room.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>聊天室</title>
<link rel="stylesheet" href="css/room.css">
</head>
<body>
<div class="roomDiv">
<div class="top">
当前在线人数:<span id="online"></span>
<div class="username">当前用户:{{user.username}}</div>
</div>
<div class="PrivateChatDiv">
私聊:<select id="receiver" onchange="receiverSocketid=this.value"></select>
<input type="text" id="privateMsg">
<button id="sendPrivateMsg">发送私聊信息</button>
<div class="PrivateChat">
<ul class="PrivateMsgList"></ul>
</div>
</div>
<div class="commonChat">
对所有人说:<input type="text" class="sendMsg">
<button class="sendBtn">发送消息</button>
<div class="msgList">
<ul></ul>
</div>
</div>
<div class="groupChat">
加入聊天群组:
<button id="department">部门群</button>
<button id="personal">私人群</button><br>
群聊:<input type="text" id="groupMsg">
<button id="sendGroupMsg">发送群聊消息</button>
<div class="groupChatMsg">
<ul class="groupChatMsgList"></ul>
</div>
</div>
</div>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io('http://192.168.254.57:8888/');
//用于保存用户的socketid以便进行私聊
var receiverSocketid;
//用来表示群的变量
var groupName;
//客户端登录,让服务器保存用户信息
socket.on('connect', function () {
console.log('connected!');
//登录,同步前后端信息
socket.emit('login',{
id:{{id}}
});
});
socket.on('allmessage',function (data) {
$('.msgList ul').append(`<li>${data}</li>`);
});
socket.on('privateMsg',function (data) {
$('.PrivateMsgList').append(`<li>${data}</li>`);
});
socket.on('groupMsg',function (data) {
$('.groupChatMsgList').append(`<li>${data}</li>`);
});
socket.on('disconnect', function () {
console.log('connection is unable');
});
//获取实时的在线人数列表
socket.on('online',function (data) {
console.log(data);
let users=Object.values(data.online);
$('#online').html(users.length);
var html;
for(var i=users.length-1;i>=0;i--){
var user=users[i];
html+=`
<option value="${user.socketid}">
${user.username}
</option>
`;
}
$('#receiver').html(html);
});
$('.sendBtn').click(function(){
var content=$('.sendMsg').val();
socket.emit('sendMsg',{
content:content
});
});
$('#sendPrivateMsg').click(function(){
var privateMsg=$('#privateMsg').val();
socket.emit('sendPrivateMsg',{
receiver:receiverSocketid,
privateMsg:privateMsg
});
});
$('#sendGroupMsg').click(function(){
var groupMsg=$('#groupMsg').val();
socket.emit('sendGroupMsg',{
groupName:groupName,
groupMsg:groupMsg
});
});
$('#department').click(function () {
socket.emit('joinGroup','department');
groupName='department';
alert('已经进入部门群,开始聊天吧!')
});
$('#personal').click(function () {
socket.emit('joinGroup','personal');
groupName='personal';
alert('已经进入私人群,开始聊天吧!')
});
</script>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>聊天室</title>
<style>
.loginBox{
width: 200px;
height: 200px;
position: absolute;
left: 50%;
top: 50%;
border: 1px solid black;
padding: 20px;
border-radius: 5px;
margin-left: -140px;
margin-top: -140px;
}
.title{
text-align: center;
}
form{
text-align: center;
}
input{
height: 30px;
border: 1px solid black;
border-radius: 2px;
}
.btn{
width: 150px;
height: 30px;
margin-top: 40px;
}
</style>
</head>
<body>
<div class="loginBox">
<p class="title">快进来聊天啊</p>
<form action="/login" method="POST">
<input type="text" name="username" placeholder="昵称">
<button class="btn">进入聊天室</button>
</form>
</div>
</body>
</html>