WebSocket概念及实现简易聊天室

WebSocket实现简易聊天室

1 WebSocket介绍

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

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

①HTTP协议:

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

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

②WebSocket协议

协议包含两部分:握手和数据传输(握手:基于http)
服务端可以主动向客户端推送消息数据
WebSocket概念及实现简易聊天室_第1张图片

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事件
事件 事件处理程序 描述
open websocket对象.onopen 连接建立时触发
message websocket对象.onmessage 客户端接收服务端数据时触发
error websocket对象.onerror 通信错误时触发
close websocket对象.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,当然,也可以自己解决
    WebSocket概念及实现简易聊天室_第2张图片
    固定页面:

  • 登录页面
    WebSocket概念及实现简易聊天室_第3张图片

  • 登录成功页面
    WebSocket概念及实现简易聊天室_第4张图片

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登录张三、李四、王五账号

  • 张三
    WebSocket概念及实现简易聊天室_第5张图片
  • 李四
    WebSocket概念及实现简易聊天室_第6张图片
  • 王五
    WebSocket概念及实现简易聊天室_第7张图片

①张三、李四互发消息
WebSocket概念及实现简易聊天室_第8张图片
WebSocket概念及实现简易聊天室_第9张图片

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

②查看王五消息框
WebSocket概念及实现简易聊天室_第10张图片

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

现在李四,给王五发消息:
WebSocket概念及实现简易聊天室_第11张图片
WebSocket概念及实现简易聊天室_第12张图片

全部代码
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);
    }
}

你可能感兴趣的:(demo,websocket,服务器,前端,协议,服务端推送消息)