WebSocket开发

news2024/11/18 3:41:40

目录

前言

1.介绍

2.原理解析 

3.简单的聊天室搭建

4.点到点消息传输 

总结


前言

WebSocket 是互联网项目中画龙点睛的应用,可以用于消息推送、站内信、在线聊天等业务。


1.介绍

WebSocket 是一种基于 TCP 的新网络协议,它是一种持久化的协议,实现了全双工通信,可以让服务器主动发送消息给客户端。

在 WebSocket 出现之前,要保持消息更新和推送一般采用轮询的方式,例如,开启一个服务进程每隔一段时间去发送请求给另外一个服务,以此获取最新的资源信息。这里都很阻塞请求,性能非常差,也会占用资源。所以考虑使用 WebSocket 来实现,使用连接实现传输,性能很高,整个客户端和服务之间交互的请求过程如图。

由图可知,最开始客户端也需要发起一次 http 请求,然后 WebSocket 协议需要通过已建立的 TCP 连接来传输数据,可见 WebSocket 和 HTTP 请求也有一些交集。但是 WebSocket 只用发起一次 HTTP 请求之后就可以通过回调机制不断地获取数据并进行交互。 

整合 WebSocket 到 SpringBoot 

在 pom.xml 中添加如下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2.原理解析 

通信协议转换过程: 

服务端 API 

Tomcat的7.0.5 版本开始支持WebSocket,并且实现了Java WebSocket规范。

Java WebSocket应用由一系列的Endpoint组成。Endpoint 是一个java对象,代表WebSocket链接的一端,对于服务端,我们可以视为处理具体WebSocket消息的接口。

我们可以通过两种方式定义Endpoint:

  • 第一种是编程式, 即继承类 javax.websocket.Endpoint并实现其方法。
  • 第二种是注解式, 即定义一个POJO, 并添加 @ServerEndpoint相关注解。

Endpoint实例在WebSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法, 规范实现者确保生命周期的各个阶段调用实例的相关方法。 

服务端接受客户端发送的数据:

编程式注解式
通过添加 MessageHandler 消息处理器来接收消息在定义Endpoint时,通过@OnMessage注解指定接收消息的方法

服务端推送消息给客户端:

发送消息则由 RemoteEndpoint 完成, 其实例由 Session 维护。

发送消息有2种方式发送消息

  • 通过session.getBasicRemote 获取同步消息发送的实例 , 然后调用其 sendXxx()方法发送消息。
  • 通过session.getAsyncRemote 获取异步消息发送实例,然后调用其 sendXxx() 方法发送消息。

3.简单的聊天室搭建

WebSocket 使用 ws 或 wss 作为通信协议,和 HTTPS 协议类似,其中 wss 表示在 TLS 之上的 WebSocket。一个完整的 URL 如下:ws://example.com/api。实现聊天室的思路可分成以下两种:

  1. 利用 websocket 依赖中的 HandshakeInterceptor 和 WebSocketHandler,实现对应的方法。
  2. 利用注解实现对话的连接、断开、信息发送触发等功能。为了更好地理解 WebSocket 的流程,我们选择第二种方式来实现聊天室的功能。

编写一个配置文件,配置类代码:

package org.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    /**
     * 配置 WebSocketEndpointServer
     *
     * 注入 ServerEndpointExporter 到web启动流程中
     * @return
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

创建 WebSocket 服务端,需要通过注解来实现如下方法,见如下:

事件类型

WebSocket 注解

事件描述

open

@OnOpen

当打开连接后触发

message

@OnMessage

当接收客户端信息时触发

error

@OnError

当通信异常时触发

close

@OnClose

当连接关闭时触发

下面正式编写 WebSocket 服务端代码,代码如下:

package org.example.componment;


import com.alibaba.fastjson.JSON;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.chatobject.Message;
import org.example.config.WebSocketConfig;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint(value = "/chat"
        ,configurator = GetHttpSessionConfig.class)//标记此类为服务端
public class WebSocketChatSerever {

    /*
    使用线程安全的 Map存储会话
     */
    private static Map<String, Session> onlineSession = new ConcurrentHashMap<>();
    private HttpSession httpSession;
    /*
    当打开连接的时候,添加会话和更新在线人数
     */
    @OnOpen
    public void onOpen(Session session) {//WebSocket Session,不是httpSession
        //1.将 session进行保存
        onlineSession.put(session.getId(), session);
        //2.广播消息推送给所有用户
        sendMessageToAll(Message.toJsonResult(Message.ENTER, "", "", onlineSession.size()));
    }
/*    @OnOpen
    public void onOpen(Session session,EndpointConfig endpointConfig) {//和 http ServerEndpointConfig 是同一个对象
        //1.将 session进行保存
        this.httpSession = (HttpSession) endpointConfig.getUserProperties().get(HttpSession.class.getName());
        String user = (String) this.httpSession.getAttribute("user");
        onlineSession.put(user, session);
        //2.广播消息推送给所有用户
        sendMessageToAll(Message.toJsonResult(Message.ENTER, "", "", onlineSession.size()));
    }*/

    /**
     * 发送消息给所有人
     * @param msg
     */
    private static void sendMessageToAll(String msg) {
        onlineSession.forEach((id, session) -> {
            try {
                //发送同步消息的方法 getBasicRemote,getAsyncRemote发送异步的消息。
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
    /*    @OnMessage
    public static void sendMessageToAll(String msg, Session session, @PathParam("nickname")String nickname) {
        log.info("来自客户端:{} 发来的消息:{}",nickname,msg);
        SocketConfig socketConfig;
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            socketConfig = objectMapper.readValue(msg, SocketConfig.class);
            if (socketConfig.getType() == 1){//私聊
                socketConfig.setFormUser(session.getId());
                Session fromSession = map.get(socketConfig.getFormUser());
                Session toSession = map.get(socketConfig.getToUser());
                if (toSession != null){// 接收者存在,发送以下消息给接受者和发送者
                    fromSession.getAsyncRemote().sendText(nickname+": "+socketConfig.getMsg());
                    toSession.getAsyncRemote().sendText(nickname+": "+socketConfig.getMsg());
                }else {
                    fromSession.getAsyncRemote().sendText("频道号不存在或者对方不在线");
                }
            }else {//群聊
                broadcast(nickname+": "+socketConfig.getMsg());
            }
        } catch (Exception e) {
            log.error("发送消息出错");
            e.printStackTrace();
        }
    }*/

    /**
     * 浏览器发送消息给服务端,该方法会被调用
     * 张三 ---> 李四
     */
    @OnMessage
    public void OnMessage(String msg){
        //1.消息推送给指定的用户
        Message message = JSON.parseObject(msg, Message.class);
        //2.获取接收方的用户名

        //3.获取接收方用户对象的session对象

        //4.发送消息  session.getAsyncRemote().sendText(msg);
        
        //未实现私聊,先这样
        onlineSession.forEach((id, session) -> {
            try {
                //发送同步消息的方法 getBasicRemote,getAsyncRemote发送异步的消息。
                session.getBasicRemote().sendText(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
    }
    /*
    当关闭连接,移除会话并减少在线人数。
     */
    @OnClose
    public void onClose(Session session) {
        //1.从 onlineSession 中剔除当前的 session 对象
        onlineSession.remove(session.getId());
        //2.通知所有用户,当前用户下线了。
        sendMessageToAll(Message.toJsonResult(Message.QUIT, "", "下线啦!", onlineSession.size()));
    }
    /*
    当通信发生异常时,打印错误日志
     */
    @OnError
    public void OnError(Session session,Throwable error){
        error.printStackTrace();
    }
}

上面的代码就是对 onError、onMessage、onOpen 注解进行编写接口,使用线程安全的 Map 来存储对话信息,根据不同的操作对在线人数和当前会话用户进行删改。sessionid 对应每一个连接的用户,由于没有注册,所有保存用户信息用处不大。其中 Message 对象是对聊天消息进行封装的数据类,具体实现代码如下:

package org.example.chatobject;

import com.alibaba.fastjson.JSON;

/**
 * 聊天对象
 */
public class Message {
    //进入聊天
    public static final String ENTER = "ENTER";
    //聊天
    public static final String TALK = "TALK";
    //退出聊天
    public static final String QUIT = "QUIT";
    //消息类型
    private String type;
    //发送人
    private String username;
    //发送消息
    private String message;
    //在线人数
    private int onlineCount;
    //返回处理后的 json 结果

    public Message(String type, String username, String message, int onlineCount) {
        this.type = type;
        this.username = username;
        this.message = message;
        this.onlineCount = onlineCount;
    }

    public static String toJsonResult(String type, String username, String message, int onlineCount) {
        return JSON.toJSONString(new Message(type, username, message, onlineCount));
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public int getOnlineCount() {
        return onlineCount;
    }

    public void setOnlineCount(int onlineCount) {
        this.onlineCount = onlineCount;
    }
}

为了更好地编写前端页面和处理参数(JSON 格式为主)传递,添加如下配置到 pom.xml:

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.25</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

然后编写一个 Controller 文件来实现简单地路由和逻辑,具体实现代码如下:

package org.example.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;

@RestController
public class ChatController {
    @GetMapping("/login")
    public ModelAndView login() {
        return new ModelAndView("/login");
    }
    @GetMapping("/chat")
    public ModelAndView index(String username, String password, HttpServletRequest request) {
        return new ModelAndView("/chat");
    }

}

编写前端页面,运行项目,访问登录页面 http://localhost:8080/login,如图所示:

错误页面:

​ 

聊天页面,如图:

​在前端中也需要编写 WebSocket 的客户端操作,核心代码如下:

<script>

    /**
     * WebSocket客户端
     *
     */
    function createWebSocket() {
        /**
         * WebSocket客户端
         */
        var serviceUri = 'ws://localhost:8080/chat';
        var webSocket = new WebSocket(serviceUri);
        /**
         * 打开连接的时候
         */
        webSocket.onopen = function (event) {
            console.log('websocket打开连接....');
        };

        /**
         * 接受服务端消息
         */
        webSocket.onmessage = function (event) {
            console.log('websocket:%c' + event.data);
            // 获取服务端消息
            var message = JSON.parse(event.data) || {};

            var $messageContainer = $('.message_container');
            if (message.type === 'TALK') {
                var insertOneHtml = '<div class="mdui-card" style="margin: 10px 0;">' +
                    '<div class="some-class">' +
                    '<div class="message_content">' + message.username + ":" + message.message + '</div>' +
                    '</div></div>';
                $messageContainer.append(insertOneHtml);
            }
            // 更新在线人数
            $('#chat_num').text(message.onlineCount);
            //防止刷屏
            var $cards = $messageContainer.children('.mdui-card:visible').toArray();
            if ($cards.length > 5) {
                $cards.forEach(function (item, index) {
                    index < $cards.length - 5 && $(item).slideUp('fast');
                });
            }
        };

        /**
         * 关闭连接
         */
        webSocket.onclose = function (event) {
            console.log('WebSocket关闭连接');
        };

        /**
         * 通信失败
         */
        webSocket.onerror = function (event) {
            console.log('WebSocket发生异常');

        };
        return webSocket;
    }

    var webSocket = createWebSocket();


    /**
     * 通过WebSocket对象发送消息给服务端
     */
    function sendMsgToServer() {
        var $message = $('#msg');
        if ($message.val()) {
            webSocket.send(JSON.stringify({username: $('#username').text(), msg: $message.val()}));
            $message.val(null);
        }

    }

    /**
     * 清屏
     */
    function clearMsg() {
        $(".message_container").empty();
    }

    /**
     * 使用ENTER发送消息
     */
    document.onkeydown = function (event) {
        var e = event || window.event || arguments.callee.caller.arguments[0];
        e.keyCode === 13 && sendMsgToServer();
    };


</script>

其中,createWebSocket 方法是专门用来创建 WebSocket 对象的,并且封装了处理服务器的消息、错误处理、连接服务器等操作。

解析:

websocket对象创建:

let  ws  =  new WebSocket(URL);

websocket对象相关事件 :

事件

事件处理程序

描述

open

ws.onopen

连接建立时触发

message

ws.onmessage

客户端接收到服务器发送的数据时触发

close

ws.onclose

连接关闭时触发

websocket对象提供的方法 :

方法名称

描述

send()

通过websocket对象调用该方法发送数据给服务端

4.点到点消息传输 

所谓点到点消息传输,其实就是我们常说的私聊功能。一个用户相当于一个连接,两个用户之间在一个通道中进行消息传输,私密地聊天。

在之前项目地基础上,进一步引入一个新的概念:频道号。就像听广播一样,必须要在相同的频道上才能听到消息,同样地,两个用户必须在相同的频道上才能接受到对方发来的消息。

于是改写 WebSocket 类中的 OnMessage 方法,具体代码如下:

    @OnMessage
    public static void sendMessageToAll(String msg, Session session, @PathParam("nickname")String nickname) {
        log.info("来自客户端:{} 发来的消息:{}",nickname,msg);
        SocketConfig socketConfig;
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            socketConfig = objectMapper.readValue(msg, SocketConfig.class);
            if (socketConfig.getType() == 1){//私聊
                socketConfig.setFormUser(session.getId());
                Session fromSession = map.get(socketConfig.getFormUser());
                Session toSession = map.get(socketConfig.getToUser());
                if (toSession != null){// 接收者存在,发送以下消息给接受者和发送者
                    fromSession.getAsyncRemote().sendText(nickname+": "+socketConfig.getMsg());
                    toSession.getAsyncRemote().sendText(nickname+": "+socketConfig.getMsg());
                }else {
                    fromSession.getAsyncRemote().sendText("频道号不存在或者对方不在线");
                }
            }else {//群聊
                broadcast(nickname+": "+socketConfig.getMsg());
            }
        } catch (Exception e) {
            log.error("发送消息出错");
            e.printStackTrace();
        }
    }

总结

了解了 WebSocket 在Spring Boot 中的整合和实践,以群聊和私聊两大功能为例,具体讲解了相应的实现方法。对 WebSocket 的常用方法的封装,进行了详细的介绍。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1314014.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mysql 的ROW_NUMBER() 和分区函数的使用 PARTITION BY的使用

Mysql 的ROW_NUMBER() 和分区函数的使用 PARTITION BY的使用 描述&#xff1a; 遇到了一个需求&#xff0c;需要查询用户id和计划id&#xff0c;但是人员id的是重复&#xff0c;我想把人员id去重&#xff0c;支取一个。自然而然的就想到了 SELECT DISTINCT prj_plan.last_mon…

Unity inspector绘制按钮与Editor下生成与销毁物体的方法 反射 协程 Editor

应美术要求&#xff0c;实现一个在编辑环境下&#xff0c;不运行&#xff0c;可以实例化预制体的脚本 效果如上图所示 1.去实现一个简单的 行、列实例化物体脚本 2.在Inspector下提供按钮 3.将方法暴露出来&#xff08;通过自定义标签实现&#xff09; 需求一 using System.C…

Springboot+vue的公寓报修管理系统(有报告)。Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的公寓报修管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的公寓报修管理系统&#xff0c;采用M&#xff08;model&…

SpringBoot接入企微机器人

1、企业微信创建机器人&#xff08;如何创建不懂的请自行百度&#xff0c;很简单的&#xff09;&#xff0c;成功后能获取到一个Webhook地址&#xff1a;https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa 2、创建一个SpringBoot项…

Linux 内存池源码剖析

1 传统的分配与释放内存的函数缺点: void *malloc(size_t size); void *calloc(size_t nmemb,size_t size);void *realloc(void *ptr, size_t size);void free(void *ptr);缺点1: 高并发时较小内存块使用导致系统调用频繁,降低了系统的执行效率 缺点2: 频繁使用时增加了系统…

java导出excel通用工具(POI,类注解形式)

通过给类定义注解(设置名称&#xff0c;设置kv转换值)&#xff0c;然后利用设置的名称和传入的数据进行导出。 只需要在项目添加两个工具类就可以实现excel导出功能。 1、单sheet 步骤&#xff1a;1、根据业务需求定义导出的类&#xff0c;并设置表头名称。 …

计算机网络快速刷题

自用//奈奎斯特定理和香农定理计算题 参考博客&#xff1a;UDP协议是什么&#xff1f;作用是什么&#xff1f; 肝了&#xff0c;整理了8张图详解ARP原理 【网络协议详解】——FTP系统协议&#xff08;学习笔记&#xff09; 在OSI参考模型中&am…

论文自己改过后怎么降重 papergpt

大家好&#xff0c;今天来聊聊论文自己改过后怎么降重&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff0c;可以借助此类工具&#xff1a; 论文自己改过后如何降重 在论文修改过程中&#xff0c;我们往往会对…

力扣785.判断二分图

存在一个 无向图 &#xff0c;图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。给你一个二维数组 graph &#xff0c;其中 graph[u] 是一个节点数组&#xff0c;由节点 u 的邻接节点组成。形式上&#xff0c;对于 graph[u] 中的每个 v &#xff0c;都存…

[蓝桥杯刷题]合并区间、最长不连续子序列、最长不重复数组长度

前言 ⭐Hello!这里是欧_aita的博客。 ⭐今日语录: 成功的关键在于对目标的持久追求。 ⭐个人主页&#xff1a;欧_aita ψ(._. )>⭐个人专栏&#xff1a; 数据结构与算法 数据库 文章目录 前言合并区间问题&#x1f4d5;现实应用大致思路代码实现代码讲解 最长不连续子序列&a…

Python封装ADB获取Android设备wifi地址的方法

一、代码实现 import subprocessimport re import subprocessfrom common.logger import loggerdef get_device_wifi_address(udid):ip_command fadb -s {udid} shell ip routeresult subprocess.check_output(ip_command, shellTrue, textTrue)# 提取 IP 地址ip_address r…

Python多态原理及实现

对于弱类型的语言来说&#xff0c;变量并没有声明类型&#xff0c;因此同一个变量完全可以在不同的时间引用不同的对象。当同一个变量在调用同一个方法时&#xff0c;完全可能呈现出多种行为&#xff08;具体呈现出哪种行为由该变量所引用的对象来决定&#xff09;&#xff0c;…

跟随鼠标动态显示线上点的值(基于Qt的开源绘图控件QCustomPlot进行二次开发)

本文为转载 原文链接&#xff1a; 采用Qt快速绘制多条曲线&#xff08;折线&#xff09;&#xff0c;跟随鼠标动态显示线上点的值&#xff08;基于Qt的开源绘图控件QCustomPlot进行二次开发&#xff09; 内容如下 QCustomPlot是一个开源的基于Qt的第三方绘图库&#xff0c;能…

功能测试转向自动化测试 。10 年 心路历程——愿测试人不再迷茫

十年测试心路历程&#xff1a; 由于历史原因&#xff0c;大部分测试人员&#xff0c;最开始接触都是纯功能界面测试&#xff0c;随着工作年限&#xff0c;会接触到一些常用测试工具&#xff0c;比如抓包&#xff0c;数据库&#xff0c;linux 等。 我大学学的计算机专业&#…

Linux环境下HTTP请求的代码详解与实例

嘿&#xff0c;Linux狂热者们&#xff0c;是时候让我们在这个充满激情的平台上探索一下HTTP协议的奥秘了。我知道&#xff0c;对于这个我们每天都在使用&#xff0c;但却可能没深入了解过的HTTP&#xff0c;你们一定充满了好奇和期待。那么&#xff0c;让我们一起踏上这趟探索之…

【Docker】实战:nginx、redis

▒ 目录 ▒ &#x1f6eb; 导读开发环境 1️⃣ Nginx 拉取 Nginx 镜像nginx.conf启动 Nginx访问 Nginx 2️⃣ redis拉取 Redis 镜像启动 Redis 容器测试 Redis &#x1f4d6; 参考资料 &#x1f6eb; 导读 开发环境 版本号描述文章日期2023-12-15操作系统Win10 - 22H222621.2…

VR虚拟仿真技术应用到外事警察岗位技能培训的场景及优势

VR治安民警常态化工作实战教学演练是一种利用VR虚拟现实制作和web3d开发技术进行治安民警培训和实战演练的新型教学模式。相较于传统的培训方式&#xff0c;VR治安民警常态化工作实战教学演练具有以下优点&#xff1a; VR实战是一种完全虚拟的实战训练方式&#xff0c;他可以根…

Apache Flume(4):日志文件监控

1 案例说明 企业中应用程序部署后会将日志写入到文件中&#xff0c;可以使用Flume从各个日志文件将日志收集到日志中心以便于查找和分析。 2 使用Exec Soucre Exec Source Exec Source通过指定命令监控文件的变化&#xff0c;加粗属性为必须设置的。 属性名默认值说明chan…

如何用DETR(detection transformer)训练自己的数据集,并推理图片

前言 这里我分享一下官方写的DETR的预测代码&#xff0c;其实就是github上DETR官方写的一个Jupyter notebook&#xff0c;可能需要梯子才能访问&#xff0c;这里我贴出来。因为DETR比较难训练&#xff0c;我觉得可以用官方的预训练模型来看看效果。 如果要用自己训练的模型来…

MYSQl基础操作命令合集与详解

MySQL入门 先来个总结 SQL语言分类 DDL&#xff08;Data Definition Language&#xff09; - 数据定义语言: 用于定义和管理数据库结构&#xff0c;包括创建、修改和删除数据库对象。 示例&#xff1a;CREATE, ALTER, DROP等语句。 DML&#xff08;Data Manipulation Lan…