WebSocket

简介

WebSocket 是一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

优点:

  • 1)较少的控制开销:在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小;

  • 2)更强的实时性:由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于 HTTP 请求需要等待客户端发起请求服务端才能响应,延迟明显更少;

  • 3)保持连接状态:与 HTTP 不同的是,WebSocket 需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息;

  • 4)更好的二进制支持:WebSocket 定义了二进制帧,相对 HTTP,可以更轻松地处理二进制内容;

  • 5)可以支持扩展:WebSocket 定义了扩展,用户可以扩展协议、实现部分自定义的子协议。

前后端使用WebSocket

img

服务端利用SpringBoot启动一个WebSocket服务,同时暴露出该服务的应用路径,客户端则利用该应用路径进行连接。需要注意的是,在服务端只需要启动一个WebSocket服务,而每一个客户端就是一个WebSocket应用。就有点像:服务端是古老的电话接线员,而客户端就是打电话的人。假如用户A想要给用户B打电话就需要先打电话到接线员那,然后接线员再接通用户B。不过WebSocket可以实现广播和私聊。

前端初始化WebSocket对象

不需要引入第三方依赖包,直接使用js自带的WebSocket对象。

  1. 创建WebSocket对象

    
    const ws = new WebSocket('ws://localhost:8000/websocket/')
    

    ws://和jdbc://http://一样都是协议名,同样的,Websocket还支持更加安全的wss:///websocket即该服务的应用路径名

  2. onopen事件监听

    与服务端连接成功会触发。

    
    webSocketOnOpen(e){
    
        console.log('与服务端连接打开->',e)
    
    },
    
  3. onerror事件监听

    与服务端连接异常时触发。

    
    webSocketOnError(e){
    
        console.log('与服务端连接异常->',e)
    
    },
    
  4. onclose事件监听

    与服务端连接关闭时触发。

    
    webSocketOnClose(e){
    
        console.log('与服务端连接关闭->',e)
    
    },
    
  5. onmessage事件监听

    接收到来自服务端的消息时触发。

    
    webSocketOnMessage(e){
    
        console.log('来自服务端的消息->',e)
    
    },
    

一个完整的WebSocket对象应该具备以上属性,同时需要将以上属性跟WebSocket对象绑定。

使用原生JS初始化WebSocket对象演示


const ws = new WebSocket(webSocketUrl)

//onopen事件监听

ws.addEventListener('open',e=>{

    console.log('与服务端连接打开->',e)

},false)

//onclose事件监听

ws.addEventListener('close',e=>{

    console.log('与服务端连接关闭->',e)

},false)

//onmessage事件监听

ws.addEventListener('message',e=>{

    console.log('来自服务端的消息->',e)

},false)

//onerror事件监听

ws.addEventListener('error',e=>{

    console.log('与服务端连接异常->',e)

},false)

ws对象的addEventListener( )方法,为WebSocket绑定事件监听,从而在各个事件监听中处理事务。

使用Vue初始化WebSocket对象演示


export default {

  name: "Home",

  data() {

    return {

      webSocketObject: null,

    }

  },

  created() {

    //初始化WebSocket

    this.webSocketInit()

  },

  methods: {

    webSocketInit(){

      const webSocketUrl = 'ws://localhost:8000/websocket/'+this.username

      this.webSocketObject = new WebSocket(webSocketUrl);

      this.webSocketObject.onopen = this.webSocketOnOpen

      this.webSocketObject.onmessage = this.webSocketOnMessage

      this.webSocketObject.onerror = this.webSocketOnError

      this.webSocketObject.onclose = this.webSocketOnClose

    },

    webSocketOnOpen(e){

      console.log('与服务端连接打开->',e)

    },

    webSocketOnMessage(e){

      console.log('来自服务端的消息->',e)

    },

    webSocketOnError(e){

      console.log('与服务端连接异常->',e)

    },

    webSocketOnClose(e){

      console.log('与服务端连接关闭->',e)

    },

  },

}

</script>

同样的,利用methods分别定义好OnOpen、OnMessage、OnError、OnClose四个事件监听,然后进行初始化并且绑定就可以了。这样就完成了WebSocket对象以及事件监听的初始化。

后端初始化WebSocket对象

  • SpringBoot自带的WebSocket有以下5个常用注解:

@ServerEndpoint

暴露出的ws应用的路径,支持RESTful风格传参,类似/websocket/{username}

@OnOpen

与当前客户端连接成功,有入参Session对象(当前连接对象),同时可以利用@PathParam()获取上述应用路径中传递的参数,比如@PathParam("username") String username。

@OnClose

与当前客户端连接失败,有入参Session对象(当前连接对象),同时也可以利用@PathParam()获取上述应用路径中传递的参数。

@OnError

与当前客户端连接异常,有入参Session对象(当前连接对象)、Throwable对象(异常对象),同时也可以利用@PathParam()获取上述应用路径中传递的参数。

@OnMessage

当前客户端发送消息,有入参Session对象(当前连接对象)、String message对象(当前客户端传递过来的字符串消息)

  • 利用SpringBoot创建项目,需要引入依赖:

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-websocket</artifactId>

</dependency>
  • 在application.yaml中定义好该服务的端口号:

server:

  port: 8000
  • 利用自定义配置类开启WebSocket:

package cn.wqk.serverwebsocket.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.socket.config.annotation.EnableWebSocket;

import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration

@EnableWebSocket

public class WebSocketConfig {

    @Bean

    public ServerEndpointExporter serverEndpointExporter(){

        return new ServerEndpointExporter();

    }

}
  • 定义Websocket主业务类:

package cn.wqk.serverwebsocket.socket;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Component;

import javax.websocket.*;

import javax.websocket.server.PathParam;

import javax.websocket.server.ServerEndpoint;

import java.io.IOException;

import java.util.Date;

import java.util.Map;

import java.util.Set;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ConcurrentMap;

import java.util.concurrent.CopyOnWriteArraySet;

import java.util.concurrent.atomic.AtomicInteger;

@Component

@Slf4j

@ServerEndpoint("/websocket/{username}") //暴露的ws应用的路径

public class WebSocket {

    /**

     * 客户端与服务端连接成功

     * @param session

     * @param username

     */

    @OnOpen

    public void onOpen(Session session,@PathParam("username") String username){

        /*

            do something for onOpen

            与当前客户端连接成功时

         */

    }

    /**

     * 客户端与服务端连接关闭

     * @param session

     * @param username

     */

    @OnClose

    public void onClose(Session session,@PathParam("username") String username){

        /*

            do something for onClose

            与当前客户端连接关闭时

         */

    }

    /**

     * 客户端与服务端连接异常

     * @param error

     * @param session

     * @param username

     */

    @OnError

    public void onError(Throwable error,Session session,@PathParam("username") String username) {

    }

    /**

     * 客户端向服务端发送消息

     * @param message

     * @param username

     * @throws IOException

     */

    @OnMessage

    public void onMsg(Session session,String message,@PathParam("username") String username) throws IOException {

        /*

            do something for onMessage

            收到来自当前客户端的消息时

         */

    }

}

前后端联动实现简单聊天室

前端在OnMessage事件监听中接收到来自后端的消息,然后进行处理(展示在页面上);同样的,后端也是在OnMessage中接收到来自前端的消息,然后进行处理(发送到所有客户端)。

前端

  1. 定义一个输入框,再定义一个按钮:接收消息并且发送

<input

       type="text"

       v-model="sendMessage"

       placeholder="请输入你要发送的消息">

<button @click="handleSendButton()">发送</button>

注意:直接利用websocket对象的send()方法发送消息,前后端数据传输利用JSON字符串,所以发送的时候需要将对象转为JSON字符串。

  1. 定义一个列表:用于展示聊天信息

<table>

    <thead>

        <tr>

            <th>消息编号</th>

            <th>发送者</th>

            <th>发送时间</th>

            <th>发送内容</th>

        </tr>

    </thead>

    <tbody>

        <tr v-for="item in messageList" :key="item.time">

            <td>{{ item.id }}</td>

            <td>{{ item.username }}</td>

            <td>{{ new Date(item.time).toLocaleTimeString() }}</td>

            <td>{{ item.message }}</td>

        </tr>

    </tbody>

</table>
  1. 当客户端的onMessage接收到消息后就把消息展示到列表中

webSocketOnMessage(e){

    console.log('来自服务端的消息->',e)

    const receiveMessage = JSON.parse(e.data);

    this.messageList.push(receiveMessage)

},

注意:通过console.log(e)不难发现,来自服务端的消息是存储在e.data中的,并且是JSON字符串,所以我们需要将它转为JSON对象。

此时已经完成了前端发送消息并且接收消息且展示消息了。

后端

接收消息并且群发消息:


@OnMessage

public void onMsg(Session session,String message,@PathParam("username") String username) throws IOException {

    /*

            do something for onMessage

            收到来自当前客户端的消息时

         */

    sendAllMessage(message);

}

//向所有客户端发送消息(广播)

private void sendAllMessage(String message){

    Set<String> sessionIdSet = onlineClientMap.keySet(); //获得Map的Key的集合

    for (String sessionId : sessionIdSet) { //迭代Key集合

        Session session = onlineClientMap.get(sessionId); //根据Key得到value

        session.getAsyncRemote().sendText(message); //发送消息给客户端

    }

}

前端完整代码


<template>

  <div>

    <table>

      <thead>

      <tr>

        <th>消息编号</th>

        <th>发送者</th>

        <th>发送时间</th>

        <th>发送内容</th>

      </tr>

      </thead>

      <tbody>

      <tr v-for="item in messageList" :key="item.time">

        <td>{{ item.id }}</td>

        <td>{{ item.username }}</td>

        <td>{{ new Date(item.time).toLocaleTimeString() }}</td>

        <td>{{ item.message }}</td>

      </tr>

      </tbody>

    </table>

    <input

        type="text"

        v-model="sendMessage"

        placeholder="请输入你要发送的消息">

    <button @click="handleSendButton()">发送</button>

    <button @click="handleLogoutButton()">退出</button>

  </div>

</template>

<script>

import {

  getUsername,

  removeUsername

} from "@/utils/username";

export default {

  name: "Home",

  data() {

    return {

      webSocketObject: null,

      username: '',

      messageList: [

      ],

      sendMessage: ''

    }

  },

  created() {

    //从localStorage中获得username

    this.username = getUsername()

    //如果username不存在返回到登录页面

    if (!this.username){

      this.$router.push({

        name: 'Login'

      })

    }

    //初始化WebSocket

    this.webSocketInit()

  },

  beforeDestroy() {

    this.webSocketObject.close();//在该组件销毁时关闭该连接以节约资源

  },

  methods: {

    webSocketInit(){

      const webSocketUrl = 'ws://localhost:8000/websocket/'+this.username

      this.webSocketObject = new WebSocket(webSocketUrl);

      this.webSocketObject.onopen = this.webSocketOnOpen

      this.webSocketObject.onmessage = this.webSocketOnMessage

      this.webSocketObject.onerror = this.webSocketOnError

      this.webSocketObject.onclose = this.webSocketOnClose

    },

    webSocketOnOpen(e){

      console.log('与服务端连接打开->',e)

    },

    webSocketOnMessage(e){

      console.log('来自服务端的消息->',e)

      const receiveMessage = JSON.parse(e.data);

      this.messageList.push(receiveMessage)

    },

    webSocketOnError(e){

      console.log('与服务端连接异常->',e)

    },

    webSocketOnClose(e){

      console.log('与服务端连接关闭->',e)

    },

    handleSendButton() {

      const username = this.username

      const message = this.sendMessage

      this.webSocketObject.send(JSON.stringify({

        id: 1,

        message,

        username,

        time: new Date().getTime()

      }))

      this.sendMessage = ''

    },

    handleLogoutButton(){

      removeUsername() //清除username然后断开连接

      this.webSocketObject.close();

      this.$router.push({

        name: 'Login'

      })

    }

  },

}

</script>

这里采用的是在上一个页面获取到用户的用户名然后存储到LocalStorage中。

后端完整代码


package cn.wqk.serverwebsocket.socket;

import lombok.extern.slf4j.Slf4j;

import org.springframework.stereotype.Component;

import javax.websocket.*;

import javax.websocket.server.PathParam;

import javax.websocket.server.ServerEndpoint;

import java.io.IOException;

import java.util.Date;

import java.util.Map;

import java.util.Set;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.ConcurrentMap;

import java.util.concurrent.CopyOnWriteArraySet;

import java.util.concurrent.atomic.AtomicInteger;

@Component

@Slf4j

@ServerEndpoint("/websocket/{username}") //暴露的ws应用的路径

public class WebSocket {

    /** 当前在线客户端数量(线程安全的) */

    private static AtomicInteger onlineClientNumber = new AtomicInteger(0);

    /** 当前在线客户端集合(线程安全的):以键值对方式存储,key是连接的编号,value是连接的对象 */

    private static Map<String ,Session> onlineClientMap = new ConcurrentHashMap<>();

    /**

     * 客户端与服务端连接成功

     * @param session

     * @param username

     */

    @OnOpen

    public void onOpen(Session session,@PathParam("username") String username){

        /*

            do something for onOpen

            与当前客户端连接成功时

         */

        onlineClientNumber.incrementAndGet();//在线数+1

        onlineClientMap.put(session.getId(),session);//添加当前连接的session

        log.info("时间[{}]:与用户[{}]的连接成功,当前连接编号[{}],当前连接总数[{}]",

                new Date().toLocaleString(),

                username,

                session.getId(),

                onlineClientNumber);

    }

    /**

     * 客户端与服务端连接关闭

     * @param session

     * @param username

     */

    @OnClose

    public void onClose(Session session,@PathParam("username") String username){

        /*

            do something for onClose

            与当前客户端连接关闭时

         */

        onlineClientNumber.decrementAndGet();//在线数-1

        onlineClientMap.remove(session.getId());//移除当前连接的session

        log.info("时间[{}]:与用户[{}]的连接关闭,当前连接编号[{}],当前连接总数[{}]",

                new Date().toLocaleString(),

                username,

                session.getId(),

                onlineClientNumber);

    }

    /**

     * 客户端与服务端连接异常

     * @param error

     * @param session

     * @param username

     */

    @OnError

    public void onError(Throwable error,Session session,@PathParam("username") String username) {

        /*

            do something for onError

            与当前客户端连接异常时

         */

        error.printStackTrace();

    }

    /**

     * 客户端向服务端发送消息

     * @param message

     * @param username

     * @throws IOException

     */

    @OnMessage

    public void onMsg(Session session,String message,@PathParam("username") String username) throws IOException {

        /*

            do something for onMessage

            收到来自当前客户端的消息时

         */

        log.info("时间[{}]:来自连接编号为[{}]的消息:[{}]",

                new Date().toLocaleString(),

                session.getId(),

                message);

        sendAllMessage(message);

    }

    //向所有客户端发送消息(广播)

    private void sendAllMessage(String message){

        Set<String> sessionIdSet = onlineClientMap.keySet(); //获得Map的Key的集合

        for (String sessionId : sessionIdSet) { //迭代Key集合

            Session session = onlineClientMap.get(sessionId); //根据Key得到value

            session.getAsyncRemote().sendText(message); //发送消息给客户端

        }

    }

}

流程图

img

总结

服务端启动一个WebSocket服务,初始化应用路径、连接打开OnOpen、连接关闭OnClose、连接异常OnError、收到消息OnMessage。OnMessage中需要对来自客户端的消息进行对应的处理,比如广播或者私聊给具体某人。

客户端需要利用WebSocket对象(定义好url),然后初始化OnOpen、OnClose、OnError、OnMessage。OnMessage中需要对来自服务端的消息进行处理,如展示到页面上等。同时还可以需用WebSocket对象的send()方法来给服务端发送数据,并且切记在页面关闭时需要将该连接关闭(利用WebSocket对象的close()方法)。

项目地址

文章作者: JAT
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Strive
三方技术 WebSocket WebSocket Java Vue
喜欢就支持一下吧