WebSocket原理及socket.io+vue+koa实现聊天功能

3,162

WebSocket是什么?

  1. WebSocket是一个基于TCP的全双工通讯协议,归属于IETF
  2. WebSocket API 是一个 Web API,归属于W3C
  3. 两个规范是独立发布的

WebSocket与HTTP有什么关系?

  • WebSocket和Http并没有关系他只是HTTP协议上的一个补充,为什么说是HTTP协议的补充呢,来看下面的两张图。
    在这里插入图片描述
    在这里插入图片描述
  • WebSocket会借用Http协议来完成一部分握手,首先浏览器发送HTTP的get请求,紧接着服务端会返回101状态码,101是Switching Protocols,当客户端通过在请求里使用Upgrade报头,以通知服务器它想改用除HTTP协议之外的其他协议时,客户端将获得此响应代码,通常HTTP客户端会在收到服务器发来的101响应后关闭与服务器的TCP连接,101响应代码意味着该客户端不再是一个HTTP客户端,而将成为另一种客户端,在发送这个响应后,将http升级到webSocket。
  • Connection:必须设置Upgrade
  • Upgrade:字段必须设置Websocket
  • Sec-WebSocket-Key:是一个Base64 encode的值,这个是浏览器随机生成的字符串,用于验证。
  • Origin:字段可选,通常用来表示在浏览器中发起此Websocket连接所在的页面,类似于Referer。但是,与Referer不同的是,Origin只包含了协议和主机名称。

WebSocket优点?

  1. WebSocket是一个持久化的协议,而HTTP是非持久的无状态协议
  2. HTTP是被动的,一个Request对应一个Response,Response不能主动发起
  3. Ajax轮询的原理是让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息,需要服务器有很快的处理速度和资源
  4. long pool的原理跟ajax轮询差不多,都是采用轮询的方式,不过采取的是阻塞模型,客户端发起连接后,如果没消息,就一直不返回Response给客户端,直到有消息才返回,返回完之后,客户端再次建立连接,不断循环,需要服务器有很高的并发
    在这里插入图片描述
    在这里插入图片描述

WebSocket API

ws.readyState 描述
0 正在连接
1 表示连接成功,可以通信
2 连接正在关闭
3 表示连接已关闭或打开连接失败
bufferedAmount
只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数
事件 事件处理程 描述
open ws.onopen 连接建立时触发
message ws.onmessage 接收数据时触发
error ws.onerror 通信错误时触发
close ws.onclose 连接关闭时触发
方法 描述
ws.send() 使用连接发送数据
ws.close() 关闭链接

WebSocket API 兼容性?

在这里插入图片描述
仍是存在不兼容的浏览器

WebSocket常用实现

为了解决兼容问题,websocket常用实现通常是通过socket.io。

vue+koa实战

chat.vue
<template>
  	<div class="chat">
        <!-- 导航 -->
        <van-nav-bar
            :title="navTitle"
            left-text=""
            left-arrow
            @click-left="onClickLeft"
            />
        <!-- 聊天区域 -->
        <div class="box">
            <div
                class="chat-list"
                v-for="(item,index) in chatmsgs"
                :key="item.id">
                <div v-if="item.from == myUserInfo.userId" class="chat-left">
                    <img class="user-img" :src='item.avatar'/>
                    <div>{{item.content}}</div>
                </div>
                <div v-else class="chat-right">
                    <div>{{item.content}}</div>
                    <img class="user-img" :src='item.avatar'/>
                </div>  
            </div>
        </div>
        <!-- 输入框 -->
        <div class="chat-foot">
            <input id="input" type="text" placeholder="请输入..." v-model="inputText"/>
            <span @click="send">发送</span>
        </div>
  	</div>
</template>

<script>
import axios from 'axios';
import url from '@/service.config.js';
import { mapState } from 'vuex';
import io from 'socket.io-client'

export default {
  	name: 'chat',
  	components: {
  	},
  	data() {
  		return {
            myUserInfo: {},
            navTitle: '',
            userId: '',
            inputText: '',
            socket: '',
            msgsList: [],
            avatar: require('../assets/default_img.jpeg'),
            flag: false,
            socketData: {},
            chatmsgs:[]
  		};
  	},
  	computed: {
      	...mapState(['userInfo']),
    },
  	mounted() {
        this.islogin = this.userInfo.isLogin;
        this.myUserInfo = this.userInfo;
        this.userId = this.$route.query.userid;
        // 发生跨域,需要手动链接
        this.socket = io('ws://localhost:3000');
        this.socket.on('recvmsg', data=>{
            console.log(data,"监听全局消息");
            this.flag = true;
            this.socketData = data;
            this.userInto(data.from);
        })
        setTimeout(()=>{
            this.userInto(this.userId);
            this.getMsgList();
            document.addEventListener("keydown", this.keyDownEvent);
        });
        console.log("我的信息=======", this.myUserInfo)
  	},
  	methods: {
        onClickLeft() {
            this.flag = false;
            window.history.go(-1);
        },
        userInto(user) {
            axios({
                url: url.userInto,
                method: 'post',
                data: {
                    userid: user,
                }
            }).then(res=>{
                if(res.data.code == 200) {
                    const data = res.data.data;
                    if(!this.flag) {
                        this.navTitle = data.userName;
                    } else {
                        this.avatar = res.data.data.userHead == '' 
                                    ? require('../assets/default_img.jpeg')
                                    : res.data.data.userHead;

                        this.$set(this.socketData,'avatar',this.avatar);
                        this.msgsList=[...this.msgsList,this.socketData];

                        const chatid = [this.userId,this.myUserInfo.userId].sort().join('_');
                        this.chatmsgs = this.msgsList.filter(v=>v.chatid == chatid);
                        console.log("this.msgsList,this.chatmsgs=============", this.msgsList, this.chatmsgs);
                    }
                }
            }).catch(err=>{
                console.log(err);
            });
        },
        getMsgList() {
            axios({
                url: url.getMsgList,
                method: 'post',
                data: {
                    from: this.myUserInfo.userId,
                }
            }).then(res=>{
                if(res.data.code == 200) {
                    this.msgsList = res.data.data.msgs;
                    this.msgsList.forEach((item,index) => {
                        let avatar = res.data.data.users[item.from].avatar;
                        let userHead = avatar=='' ? require('../assets/default_img.jpeg') :avatar
                        this.$set(item,'avatar',userHead);
                    });
                    const chatid = [this.userId,this.myUserInfo.userId].sort().join('_');
                    this.chatmsgs = this.msgsList.filter(v=>v.chatid == chatid);
                }
            })
        },
        send() {
            let text = document.getElementById("input").value
            const from = this.myUserInfo.userId;
            const to = this.userId;
            const msg = text;
            this.socket.emit('sendmsg',{from,to,msg})
            this.inputText='';
        },
        keyDownEvent(event) {
            if (event.keyCode === 13) this.send();
        },
  	}
}
</script>
koa index.js
const Koa = require('koa');
const app = new Koa();
// 关联koa
const server = require('http').Server(app.callback());
const io = require('socket.io')(server);
const chatModel = require('./model/Chat');
const Chat = chatModel.getModel('Chat');
io.on('connection',function(socket){
	// 传进来的socket变量为当前本次链接,而io是全局链接
	console.log('socket connection');
	socket.on('sendmsg', function(data){
		const {from,to,msg} = data;
		// 两个用户id排序变成字符串每次查询这个字符串
		const chatid = [from,to].sort().join('_')
		Chat.create({chatid,from,to,content:msg},function(err,doc){
			console.log(doc,"创建消息");
			io.emit('recvmsg',Object.assign({},doc._doc))
		})
	})
})

// 解决跨域问题(放在router下面也会造成跨域)
const cors = require('koa2-cors');
app.use(cors({
	origin: ['xxx:8080'],
	credentials: true
}));

// 解析前端post请求(放在router下面可能会造成解析不到post)
const bodyParser = require('koa-bodyparser');
app.use(bodyParser());

// controller引入路由
const Router = require('koa-router');
let chat = require('./controller/chat.js');

let router = new Router();
router.use('/chat', chat.routes());

app.use(router.routes());
app.use(router.allowedMethods());

app.use(async ctx => {
	ctx.body = 'hello word';
});

// "listener" argument must be a function
server.listen(3000, ()=>{
	console.log('Server is running at port 3000...');
});
koa model/Chat.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const chatSchema = new Schema({
    chatid:{'type':String,'require':true},
    from: {'type':String, 'require':true},
    to: {'type':String, 'require':true},
    content: {'type':String, 'require':true, default:''});
mongoose.model('Chat', chatSchema);
module.exports = {
	getModel:function(name){
		return mongoose.model(name)
	}
}
koa controller/chat.js
const Router = require('koa-router');
let router = new Router();
const mongoose = require('mongoose');
router.post('/getMsgList', async ctx => {
	const Chat = mongoose.model('Chat');
    let newChat = new Chat(ctx.request.body);
    const user = newChat.from;
    const User = mongoose.model('User');
    let users = {}
    await User.find({}).exec().then(async(res)=>{
		res.forEach(v=>{
			users[v._id] = {name:v.userName,avatar:v.userHead}
        })
        await Chat.find({'$or':[{from:user},{to:user}]}).exec().then((res)=>{
            ctx.body = {
                code: 200,
                message: '成功',
                data: {
                    msgs:res,
                    users:users
                }
            };
        }).catch(err=>{
            ctx.body = {
                code: 500,
                message: err
            };
        });
	})
});
module.exports = router;