WebSocket概念及实现简易聊天室

news2024/11/16 12:44:18

WebSocket实现简易聊天室

1 WebSocket介绍

  • 网络通信协议
  • 是HTML5开始提供的一个单个TCP连接上进行全双工通信协议

1.1 诞生原因(http无状态、无连接)

①HTTP协议:

由于HTTP协议是一种无状态、无连接、单向的应用层协议,通信请求只能由客户端发起。服务器无法主动向客户端发送消息。(一问一答)

如果服务器由连续的状态变化,客户端要获知就非常麻烦,大多数web应用程序通过频繁的异步ajax请求实现长轮询,效率非常低(因为需要不停连接或HTTP连接始终打开)。

②WebSocket协议

协议包含两部分:握手和数据传输(握手:基于http)
服务端可以主动向客户端推送消息数据
在这里插入图片描述

1.2 websocket客户端与服务端

来自客户端的握手:

GET ws://localhost:/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGh1IHNhbXbsZSBub25jZQ==
Sec-WebSocket-Extentions: permessage-deflate
Sec-WebSocket-Version: 13

ws://localhost:/chat HTTP/1.1 ws是协议名称

来自服务端的握手:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: s3pPLMBiTxaQ9kYGzzhZRbx+Oo==
Sec-WebSocket-Extentions: permessage-deflate
头名称说明
Connection:graduation标识该HTTP请求是一个协议升级请求
Upgrade:WebSocket协议升级为WebSocket
Sec-WebSocket-Version:13客户端支持WebSocket的版本
Sec-WebSocket-Key:客户端采用Base64编码的24为随机字符序列,服务器接收客户端HTTP协议升级的证明。要求服务端响应一个对应加密的Sec-WebSocket-Accept头信息作为应答
Sec-WebSocket-Extentison协议扩展类型

Sec-WebSocket-Key用于标识当前服务器与哪个客户端对应

①客户端(浏览器)实现

  1. websocket对象

实现websocket的web浏览器将通过websocket对象捅开所有必须的客户端功能(主要指支持HTML5的浏览器)

//创建websocket对象
//url: ws://ip地址:端口号/资源名称
var ws = new WebSocket(url);
  1. websocket事件
事件事件处理程序描述
openwebsocket对象.onopen连接建立时触发
messagewebsocket对象.onmessage客户端接收服务端数据时触发
errorwebsocket对象.onerror通信错误时触发
closewebsocket对象.onclose连接关闭时触发
  1. websocket方法
    WebSocket对象的相关方法:

send() 使用连接发送数据

②服务端实现

Tomcat 7.0.5版本开始支持WebSocket,并且实现了Java WebSocket规范(JSR356)

Java WebSocket应用由一些列的WebSocketEndpoint组成,Endpoint是一个java对象,代表WebSocket连接的一端(处理websocket消息的接口),对于服务端,我们可以视为处理WebSocket消息的接口,就像Servlet与http请求一样。

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

  • 编程式:继承javax.WebSocket.Endpoint并实现其方法
  • 注解式:定义一个pojo,添加@ServerEndpoint相关注解

Endpoint实例再websocket握手时创建,并在客户端与服务端连接过程中有效,最后再连接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,方法如下:

方法含义描述注解
onClose会话关闭时调用@OnClose
onOpen当开启一个新会话时调用,该方法是客户端与服务端握手成功后调用的方法@OnOpen
onError当连接异常时调用@OnError

③服务端与客户端如何收发消息

  • 服务端如何收消息

通过Session添加Messagehandler消息处理器来接收消息,此处的Session不是http中的session【注解方式:添加@OnMessage】

  • 服务端如何推送消息

发送消息由RemoteEndpoint完成,其实例由Session维护。我们可以通过Session.getBasicRemote获取同步消息发送的实例,然后调用其sendXxx()方法就可以发送消息,可以通过Session.getAsyncRemote获取异步消息发送实例。

2 项目开发

注意:此项目是通过session来保存用户数据的,因此如果要测试效果的话,一个用户就需要开启一个浏览器,否则session会覆盖用户数据,造成数据混乱

2.1 完整代码

链接:https://pan.baidu.com/s/1CioTDitIzInSSrNvNhQ8FA?pwd=zj8k
提取码:zj8k

  • 为了避免跨域问题,可以直接手动将application.properties中的端口配置改为80,当然,也可以自己解决
    在这里插入图片描述
    固定页面:

  • 登录页面
    在这里插入图片描述

  • 登录成功页面
    在这里插入图片描述

2.2 websocket开发

本教程主要是体验websocket的功能,因此很多东西都写死的,不太规范,比如没有使用数据库等等

2.2.1 系统广播消息推送 - onOpen

  1. 将ServerEndpointExporter注入到spring中
@Configuration
public class WebSocketConfig {

    /*注入ServerEndpointExporter bean对象,自动注册使用了@ServerEndpoint注解的bean*/
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}
  1. 存储httpSession
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        //将httpSession对象存储到配置对象中
        sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
    }
}
  1. 编写ChatEndpoint
/**
 * @author zhouYi
 * @description TODO
 * @date 2023/1/13 14:33
 */
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {

    //使用线程安全的map来存储每一个客户端对应的chatEndpoint对象
    private static Map<String, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();

    //声明websocket的session对象,通过该对象可以发送消息给指定用户
    private Session session;

    //声明一个httpSession对象,我们之前在HttpSession对象中存储了用户名
    private HttpSession httpSession;

    /**
     * 连接建立时触发
     * @param session
     * @param config
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config){
        //将局部的session对象赋值给成员session
        this.session = session;
        //获取httpSession对象
        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        this.httpSession = httpSession;

        //从httpSession中获取用户名
        String username = (String) httpSession.getAttribute("user");
        //将当前对象存储到容器中
        onlineUsers.put(username, this);

        //将当前在线用户的用户名推送给所有的客户端
        //1. 获取消息
        String message = MessageUtils.getMessage(true, null, getNames());
        //2. 调用方法进行系统消息的推送
        broadcastAllUsers(message);
    }

    private void broadcastAllUsers(String message){
        try{
            //要将该消息推送给所有的客户端
            Set<String> names = onlineUsers.keySet();
            for (String name : names) {
                ChatEndpoint chatEndpoint = onlineUsers.get(name);
                chatEndpoint.session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Set<String> getNames(){
        return onlineUsers.keySet();
    }
}

2.2.2 聊天功能实现- onMessage

//接收到客户端发送的数据时被调用
@OnMessage
public void onMessage(String message, Session session){
    try{
        //将message转换成message对象
        ObjectMapper mapper = new ObjectMapper();
        Message msg = mapper.readValue(message, Message.class);
        //获取要将数据发送给的用户
        String toName = msg.getToName();
        //获取消息数据
        String data = msg.getMessage();
        //获取当前登录的用户
        String username = (String) httpSession.getAttribute("user");
        //获取推送给指定用户的消息格式的数据
        String resultMessage = MessageUtils.getMessage(false, username, data);
        //发送数据
        onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
    } catch (JsonMappingException e) {
        e.printStackTrace();
    } catch (JsonProcessingException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.2.3 聊天记录存储

2.2.4 效果

分别用edge、google、firefox登录张三、李四、王五账号

  • 张三
    在这里插入图片描述
  • 李四
    在这里插入图片描述
  • 王五
    在这里插入图片描述

①张三、李四互发消息
在这里插入图片描述
在这里插入图片描述

注意,页面右侧为自己的消息

②查看王五消息框
在这里插入图片描述

可以看到并没有收到消息,因为没有人给他发消息

现在李四,给王五发消息:
在这里插入图片描述
在这里插入图片描述

全部代码

package com.zi.demo.ws;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zi.demo.pojo.Message;
import com.zi.demo.util.MessageUtils;
import org.springframework.stereotype.Component;

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

/**
 * @author zhouYi
 * @description TODO
 * @date 2023/1/13 14:33
 */
@ServerEndpoint(value = "/chat", configurator = GetHttpSessionConfigurator.class)
@Component
public class ChatEndpoint {

    //使用线程安全的map来存储每一个客户端对应的chatEndpoint对象
    private static Map<String, ChatEndpoint> onlineUsers = new ConcurrentHashMap<>();

    //声明websocket的session对象,通过该对象可以发送消息给指定用户
    private Session session;

    //声明一个httpSession对象,我们之前在HttpSession对象中存储了用户名
    private HttpSession httpSession;

    /**
     * 连接建立时触发
     * @param session
     * @param config
     */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config){
        //将局部的session对象赋值给成员session
        this.session = session;
        //获取httpSession对象
        HttpSession httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
        this.httpSession = httpSession;

        //从httpSession中获取用户名
        String username = (String) httpSession.getAttribute("user");
        //将当前对象存储到容器中
        onlineUsers.put(username, this);

        //将当前在线用户的用户名推送给所有的客户端
        //1. 获取消息
        String message = MessageUtils.getMessage(true, null, getNames());
        //2. 调用方法进行系统消息的推送
        broadcastAllUsers(message);
    }

    private void broadcastAllUsers(String message){
        try{
            //要将该消息推送给所有的客户端
            Set<String> names = onlineUsers.keySet();
            for (String name : names) {
                ChatEndpoint chatEndpoint = onlineUsers.get(name);
                chatEndpoint.session.getBasicRemote().sendText(message);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Set<String> getNames(){
        return onlineUsers.keySet();
    }

    //接收到客户端发送的数据时被调用
    @OnMessage
    public void onMessage(String message, Session session){
        try{
            //将message转换成message对象
            ObjectMapper mapper = new ObjectMapper();
            Message msg = mapper.readValue(message, Message.class);
            //获取要将数据发送给的用户
            String toName = msg.getToName();
            //获取消息数据
            String data = msg.getMessage();
            //获取当前登录的用户
            String username = (String) httpSession.getAttribute("user");
            //获取推送给指定用户的消息格式的数据
            String resultMessage = MessageUtils.getMessage(false, username, data);
            //发送数据
            onlineUsers.get(toName).session.getBasicRemote().sendText(resultMessage);
        } catch (JsonMappingException e) {
            e.printStackTrace();
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 连接关闭时被调用
     * @param session
     */
    @OnClose
    public void onClose(Session session){
        String username = (String) httpSession.getAttribute("user");
        //待优化
//        //从容器中删除指定的用户
//        onlineUsers.remove(username);
//        //获取推送的消息
//        String message = MessageUtils.getMessage(true, null, getNames());
//        broadcastAllUsers(message);
    }
}

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

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

相关文章

Java反射机制是什么?

Java 反射机制是 Java 语言的一个重要特性。在学习 Java 反射机制前&#xff0c;大家应该先了解两个概念&#xff0c;编译期和运行期。 编译期是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些…

Qt Creator添加自定义向导

文章目录一、前言二、前置说明三、wizard.json解析3.1、宏观结构3.2、微观解释3.2.1、向导信息3.2.2、定义变量3.2.3、页面定义3.2.4、文件生成四、实战一、前言 在Qt Creator中&#xff0c;当我们选择新建时&#xff0c;Qt自带了很多选项&#xff1b; 如果我们在开发过程中&a…

软件测试——使用mujava测试过程中

文章目录下载对应安装包步骤二、修改代码第二部分&#xff0c;修改对应测试文件下载对应安装包 分别下载junit.jar、mujava.jar和openjava.jar三个包&#xff0c;并设定对应系统路径。三个安装包的下载路径以mujava.jar为例子 设置系统环境变量&#xff0c;计算机》属性》高级…

golang实现一个linux命令ls命令(命令行工具构建)

希望2023可以听到这些话&#xff1a; 恭喜你得到了这份工作恭喜你的建议被采用了恭喜你被录取了恭喜你的考试顺利通过了恭喜你上岸了恭喜你升职了恭喜你加薪了恭喜你体检结果一切正常在这篇文章下面许个愿吧&#xff01; ls 命令 要实现ls&#xff0c;首先先我们复习一下ls命令…

FPGA知识汇集-ASIC向FPGA的移植

ASIC原型验证是整个验证环节中非常重要的步骤之一&#xff0c;也是将ASIC的代码移植到FPGA平台上最重要的原因&#xff0c;本文章的意义在于&#xff1a; 对于系统构架师&#xff0c;将帮助他们在选择商用模拟器还是自行设计方案之间做出更好的选择&#xff1b; 对于逻辑工程师…

一文读懂Go Http Server原理

hello大家好呀&#xff0c;我是小楼&#xff0c;这是系列文《Go底层原理剖析》的第二篇&#xff0c;依旧是分析 Http 模块&#xff0c;话不多说&#xff0c;开始。 从一个 Demo 入手 俗话说万事开头难&#xff0c;但用 Go 实现一个 Http Server 真不难&#xff0c;简单到什么程…

2、Eclipse安装与使用

目录 一、简介 二、下载 三、Eclipse安装 &#xff08;1&#xff09;找到Eclipse安装包&#xff0c;右键【以管理员身份运行】 2.因为是写Java程序&#xff0c;所以安装第一个喽 3.安装位置设置&#xff08;不建议C盘安装哦&#xff09;&#xff0c;点击【INSTALL】 3.点…

day02 数组 | 977、有序数组的平方 209、长度最小的子数组 59、螺旋矩阵II

1、题目 977、有序数组的平方 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示例 1&#xff1a; 输入&#xff1a;nums [-4,-1,0,3,10] 输出&#xff1a;[0,1,9,16,100] 解释&#xff1…

github Page博客速度优化+Cloudflare_https两端配置+解决重定向次数过多问题

网站加速调优 自从加了CDN之后我的博客偶尔会报错”重定向次数过多“ 症状&#xff1a; XXX.XXX.XXX 将您重定向的次数过多。 尝试清除 Cookie. ERR_TOO_MANY_REDIRECTS 可能原因 参考阿里云cdn解决方案https://help.aliyun.com/document_detail/451418.html但是cloudflar…

【UCIe】UCIe Stall 介绍

&#x1f525;点击查看精选 UCIe 系列文章&#x1f525; &#x1f525;点击进入【芯片设计验证】社区&#xff0c;查看更多精彩内容&#x1f525; &#x1f4e2; 声明&#xff1a; &#x1f96d; 作者主页&#xff1a;【MangoPapa的CSDN主页】。⚠️ 本文首发于CSDN&#xff0…

【SAP Hana】SAP Hana存储过程开发

SAP Hana存储过程开发Hana 存储过程1、创建&更新语法2、删除语法3、调用语法4、实例演示&#xff08;1&#xff09;存储过程入门&#xff08;2&#xff09;指定 DEFAULT SCHEMA&#xff08;3&#xff09;定义内部变量&#xff0c;执行多个查询&#xff08;4&#xff09;定义…

【Javascript】高阶函数,JSON,forEach,map,filter,reduce等高阶函数,函数绑定

❤️ Author&#xff1a; 老九 ☕️ 个人博客&#xff1a;老九的CSDN博客 &#x1f64f; 个人名言&#xff1a;不可控之事 乐观面对 &#x1f60d; 系列专栏&#xff1a; 文章目录高阶函数箭头函数apply函数JSONfilter函数map函数总结reduce函数find/findIndex函数every/some函…

【Android安全】frida-gum教程

frida-gum教程 frida-gum概述 frida-gum是基于inline-hook实现的 提供的功能&#xff1a;代码跟踪&#xff08;Stalker&#xff09;、内存访问监控&#xff08;MemoryAccessMonitor&#xff09;、符号查找、栈回溯实现、内存扫描、动态代码生成和重定位 inline Hook 原理 .…

MySQL中常见的数值函数

第一个 ceil(x)&#xff1a;向上取整 取整&#xff0c;顾名思义就是取整数&#xff1b; 向上取整是只要小数位不是 0&#xff0c;都会向上进 1 位整数。 案例 1&#xff1a; select ceil(9.2); 解析&#xff1a; 9.2 向上取一位整数&#xff0c;就是 10。 查询结果&#xff…

全屋智能品牌很多坑!选华为还是卡特加特数字家庭?技术角度分析亮了

市面上的智能家居品牌有很多&#xff0c;但拥有成熟全屋智能系统、完善产品体系&#xff0c;以及线下线上闭环销售渠道的则没几个。细数下来只有手机厂商华为、小米&#xff0c;以及科创型企业欧瑞博、摩根、UIOT和卡特加特&#xff0c;而其中以华为和卡特加特为代表的&#xf…

自定义类型:结构体,枚举,联合(3)

TIPS 1. 2. 枚举 1. 枚举顾名思义就是一一列举可能的取值&#xff0c;比如一周的星期一到星球天是有限的七天&#xff0c;可以一一列举。有比如性别&#xff0c;月份。 2. 像这种容易并且可以被一一列举的数据我们就可以定义为枚举类型。 枚举类型 1. 枚举的关键字为e…

【2】成功安装部署K8s集群

目录 1、安装方式 2、环境初始化 【1】主机名解析 【2】时间同步 【3】禁用iptables和firewalld服务&#xff08;三台都要设置&#xff09; 【4】禁用selinux&#xff08;三台都要设置&#xff09; 【5】禁用swap分区 【6】修改linux的内核参数 3、安装docker 【1】安…

自定义类型:结构体,枚举,联合(详解版)

&#x1f40b;自定义类型&#xff1a;结构体&#xff0c;枚举&#xff0c;联合&#x1f996;结构体&#x1f414;1.结构体的声明&#x1f424;1.1 结构的基础知识&#x1f424;1.2 结构的声明&#x1f424;1.3 特殊的声明&#x1f424;1.4 结构的自引用&#x1f424;1.5 结构体…

APSIM实战练习:Kingsthorpe土壤水分蒸发研究

在本练习中&#xff0c;您将对来自澳大利亚昆士兰州金斯索普的真实试验的数据进行建模。使用从试验中观察到的数据&#xff0c;您将创建一个气象文件&#xff0c;模拟三个蒸发曲线并将模拟输出与观察到的数据进行比较。 有关试用的更多背景信息&#xff0c;请参阅此 PowerPoin…

操作系统-进程与线程

进程的概念、组成、特征 概念 程序&#xff1a;是静态的&#xff0c;就是存放在磁盘里的可执行文件&#xff0c;如&#xff1a;QQ.exe。 线程&#xff1a;是动态的&#xff0c;是程序的一次执行过程&#xff0c;如&#xff1a;可同时启动多次QQ程序。 组成 进程由PCB、程序…