2

一文读懂WebSocket

Websocket是一个持久化的网络通信协议,可以在单个 TCP 连接上进行全双工通讯,没有了Request和Response的概念,两者地位完全平等,允许服务端主动向客户端发送数据,一旦建立连接,客户端和服务端之间即可实时进行双向数据传输。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)没有同源限制,客户端可以与任意服务器通信。
(3)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

客户端

由于websocket是HTML5开始提供的,因此客户端可以新建ws对象用于建立通信。

let ws = new WebSocket('ws://localhost:3000');

ws.onopen = () => {
    console.log('open connection');
}

ws.onmessage = (event) => {
    //客户端接收到服务端数据的回调
    console.log(event,'onmessage event');
}

ws.onclose = () => {
    console.log('close connection')
}

以上是websocket的三个事件,还有一个是error事件,明显是在通信发生错误时触发的事件。

另外websocket还有两个常用方法,在后续的例子中将会使用它们:

ws.send();        //使用连接发送数据
ws.close();        //关闭连接

服务端

node中,使用最广泛的是通过ws模块来创建websocket服务,使用前需要先安装这个模块:

const express = require('express');

const SocketServer = require('ws').Server;

const port = 3000;

const server = express().listen(port, () => {
    console.log(`listening to ${port}`);
})

const wss = new SocketServer({server});

wss.on('connection', (ws) => {
    console.log('Client connected.');

    ws.on('message', (data) => {
        console.log(data);
        //服务端发送数据到客户端
        ws.send(data);
    })

    ws.on('close', () => {
        console.log('Close connected.')
    })
})

启动服务,运行:

node server.js

能看到服务启动:

listening to 3000

此时访问客户端页面,在控制台能看到:

open connection

输入ws,然后回车显示ws对象信息:

WebSocket{
    binaryType: "blob"
    bufferedAmount: 0
    extensions: ""
    onclose: () => { console.log('close connection') }
    onerror: null
    onmessage: (event) => { console.log(event,'onmessage event'); }
    onopen: () => { console.log('open connection'); }
    protocol: ""
    readyState: 1
    url: "ws://localhost:3000/"
}

这里提一下readyState,他是返回实例对象的当前状态,共有四种:

状态代码含义
CONNECTING0表示正在连接
OPEN1表示连接成功,可以通信
CLOSING2表示连接正在关闭
CLOSED3表示连接已经关闭或打开连接失败

这时在控制台手动调用send发送消息:

ws.send('hello')

实例对象的send()方法用于向服务端发送数据,触发客户端ws.onmessage事件,同时服务端的ws.on('message')事件也被触发,服务端接收客户端发送过来的消息。

客户端打印event回传信息:

image

在data属性中可以看到”hello“信息。

同时,服务端可以主动连续向客户端发送消息:

wss.on('connection', (ws) => {
    console.log('Client connected.');
    const sendNowTime = setInterval(() => {
         ws.send(String(new Date()))
    },1000)
});
//服务端每秒向客户端发送实时消息

image

客户端连接后就会开始定时接收服务端发来的数据,直至手动关闭连接或服务。

ws.close()

模拟多人聊天

下面提供一个完整的例子用于模仿两人通过websocket进行实时聊天:

客户端代码

<div id="app">
    <div class="box">
        <ul>
            <li v-for="(item,index) in news" :class="item.nickname == nickname ? 'me' : 'other'" :key="index">
                ![](item.nickname == nickname ? )
                <p>{{item.message}}</p>
            </li>
        </ul>
        <div>
            <input type="text" v-model="send">
            <button @click="sendMsg">发送</button>
        </div>
    </div>
</div>
*{
    margin: 0 ;
    padding: 0;
}
ul,li{
    list-style: none;
}
.box{
    width: 500px;
    margin: 0 auto;
}
.box ul li{
    display: flex;
    
    align-items: flex-start;
    margin-bottom: 10px;
}
.box ul li img{
    width: 40px;
    height: 40px;
    margin-right: 8px;
}
.box ul li p{
    font-size: 14px;
    line-height: 20px;
    max-width: 300px;
    background: #f1f1f1;
    border-radius: 4px;
    padding: 10px 20px;
}
.me{
    justify-content: flex-start;
    flex-direction: row;
}
.other{
    flex-direction: row-reverse;
    order:1
}
let app = new Vue({
    el: '#app',
    data(){
        return {
            socketUrl: 'ws://localhost:8000?userName=',
            nickname: '',
            news: [],
            send: '',
            userList: []
        }
    },
    mounted(){
        this.initChatting();
        let url = window.location.href;
        this.nickname = url.split('=')[1]
    },
    methods: {
        initChatting(){
            if(window.WebSocket){
                this.client = new WebSocket(this.socketUrl + this.nickname);

                this.client.onopen = (e) => {
                    if(e.type == 'open'){
                        console.log('客户端连接')
                    }
                }
                //客户端接收到服务端数据的回调
                this.client.onmessage = (e) => {
                    let data = JSON.parse(e.data);
                    if(data instanceof Array == true){
                        this.userList = data;       //在线用户数变化
                    }else{
                        //聊天信息
                        this.news.push(data);
                    }
                    console.log(this.news)
                }
                this.client.onclose = (e) => {
                    this.client = null;
                }
                this.client.onerror = () => {
                    if(!this.client){
                        console.log("服务连接失败")
                    }
                }
            }else{
                alert("浏览器版本过低,不支持websocket.")
            }
        },
        sendMsg(){
            let data = {
                message: this.send,
                uid: new Date().getTime(),
                nickname: this.nickname,
                date: new Date()
            }
            this.client.send(JSON.stringify(data))
            this.send = "";
        }
    }
})

服务端代码

const WebSocket = require('ws').Server;
const moment = require('moment');
const url = require('url');
const querystring = require('querystring');

let wss = new WebSocket({
    url: 'localhost',
    port: 8000
})

let id = 0;
let onlineMemberList = [];
let defaultUser = 'user';

wss.on('connection', (ws,req) => {
    console.log('connected.')
    id++;
    ws.id = id;
    let arg = url.parse(req.url).query;
    let nameObj = querystring.parse(arg);
    let userName;
    if(nameObj.username){
        userName = decodeURIComponent(username);
    }else{
        userName = defaultUser + id
    }
    
    let userInfo = {
        userName,
        socketId: id,
        date: moment().format('MMMM Do YYYY, hh:mm:ss a')
    }
    
    for(let i=0;i<onlineMemberList.length;i++){
        if(userInfo.userName === onlineMemberList[i].userName){
            onlineMemberList[i] = userInfo;
            wss.clients.forEach(item => {
                item.send(JSON.stringify(onlineMemberList))
            })
            return;
        }
    }
    onlineMemberList.push(userInfo);
    wss.clients.forEach(item => {
        item.send(JSON.stringify(onlineMemberList));
    })
    ws.on('message',(data) => {
        let newData = JSON.parse(data);
        console.log(data,'data')
        newData.serveDate = moment().format('MMMM Do YYYY, h:mm:ss a');
        wss.clients.forEach(item => {
            // 监听客户端发来的数据,直接将信息原封不动,全部返回回去
            item.send(JSON.stringify(newData));
        })
    })

    ws.on('close',(e) => {
        onlineMemberList = onlineMemberList.filter(item => {
            return item.socketId != ws.id;
        })
        wss.clients.forEach(item => {
            item.send(JSON.stringify(onlineMemberList));
        })
    })
    ws.on('error',(e) => {
        console.log('客户端异常',e)
    })
})

实现效果

image

跨域发送接收信息

由于websocket可以建立客户端和服务端点对点的链接,因此不受跨域问题影响,也可用于解决客户端跨域问题:

let ws = new WebSocket("ws://localhost:8200"); //建立连接
ws.onopen = function () { //打开协议
    console.log("连接成功");
}
ws.onmessage = function (mes) { //发送数据到服务端
    console.log(mes);
}
// ws.addEventListener("message", function (e) {});

document.querySelector(".btn").onclick = function () {
    let input = document.querySelector(".input").value;
    console.log("客户端发送给服务端的信息:" + input);
    ws.send(input); //如果服务端关闭协议后,即执行ws.close()后,此时会报错: WebSocket is already in CLOSING or CLOSED state.
};

看完这篇文章,希望读者能对websocket能有一个初步的了解,在工作、练习中可以更快地上手应用。

image


Wen前端严选
876 声望99 粉丝

精选前端前沿技术,涵盖前端完整技术体系!进阶高级前端!