SpringBoot-WebSocket聊天室

x33g5p2x  于2021-09-25 转载在 Spring  
字(12.6k)|赞(0)|评价(0)|浏览(249)

介绍

1.websocket:websocket最伟大之处在于服务器和客户端可以在给定的时间范围内的任意时刻,相互推送信息。 浏览器和服务器只需要要做一个握手的动作,在建立连接之后,服务器可以主动传送数据给客户端,客户端也可以随时向服务器发送数据。

第一、WebSocket是HTML5中的协议,支持持久连接;而Http协议不支持持久连接。
第二、首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说

HTTP的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,那么在 HTTP1.0 中,这次HTTP请求就结束了。

在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起。

第三、传统的http请求,其并发能力都是依赖同时发起多个TCP连接访问服务器实现的(因此并发数受限于浏览器允许的并发连接数),而websocket则允许我们在一条ws连接上同时并发多个请求,即在A请求发出后A响应还未到达,就可以继续发出B请求。由于TCP的慢启动特性(新连接速度上来是需要时间的),以及连接本身的握手损耗,都使得websocket协议的这一特性有很大的效率提升。
第四、http协议的头部太大,且每个请求携带的几百上千字节的头部大部分是重复的,很多时候可能响应都远没有请求中的header空间大。如此多无效的内容传递是因为无法利用上一条请求内容,websocket则因为复用长连接而没有这一问题。
第五、当需要实现客户端刷新消息时,传统方案往往通过定时ajax请求实现,实际上对多数用户多数时间下这些请求都是无意义了,并且非常占用资源,websocket资源占用就小很多。

举例来说,我们想要查询当前的排队情况,只能是页面轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket 就是这样发明的。

项目结构

Maven

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>
    <dependencies>
        <!-- springboot websocket -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.51</version>
        </dependency>

        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

    </dependencies>

application.yml

server:
  #设置程序启动端口号
  port: 9030
spring:
  # 开发阶段关闭thymeleaf的模板缓存 否则每次修改都需要重启引导
  thymeleaf:
    cache: false

websocket.html

注意js代码必须在body内容最后 否则会出现连接失败的问题

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ch">
<head>
  <title>websocket</title>

</head>

<body>

<div style="margin: auto;text-align: center">
  <h1>Welcome to websocket</h1>
</div>
<br/>
<div style="margin: auto;text-align: center">
  <select id="onLineUser">
    <option>--所有--</option>
  </select>
  <input id="text" type="text" />
  <button onclick="send()">发送消息</button>
</div>
<br>
<div style="margin-right: 10px;text-align: right" >
  <button onclick="closeWebSocket()">关闭连接</button>
</div>
<hr/>
<div id="message" style="text-align: center;"></div>
<input type="text" th:value="${username}" id="username" style="display: none" />


<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.1/sockjs.min.js"></script>
<script type="text/javascript"> var webSocket; var commWebSocket; if ("WebSocket" in window) { webSocket = new WebSocket("ws://localhost:9030/websocket/"+document.getElementById('username').value); //连通之后的回调事件 webSocket.onopen = function() { webSocket.send( document.getElementById('username').value+"已经上线了"); console.log("已经连通了websocket"); setMessageInnerHTML("已经连通了websocket"); }; //接收后台服务端的消息 webSocket.onmessage = function (evt) { var received_msg = evt.data; console.log("数据已接收:" +received_msg); var obj = JSON.parse(received_msg); console.log("可以解析成json:"+obj.messageType); //1代表上线 2代表下线 3代表在线名单 4代表普通消息 if(obj.messageType==1){ //把名称放入到selection当中供选择 var onlineName = obj.username; var option = "<option>"+onlineName+"</option>"; $("#onLineUser").append(option); setMessageInnerHTML(onlineName+"上线了"); } else if(obj.messageType==2){ $("#onLineUser").empty(); var onlineName = obj.onlineUsers; var offlineName = obj.username; var option = "<option>"+"--所有--"+"</option>"; for(var i=0;i<onlineName.length;i++){ if(!(onlineName[i]==document.getElementById('username').value)){ option+="<option>"+onlineName[i]+"</option>" } } $("#onLineUser").append(option); setMessageInnerHTML(offlineName+"下线了"); } else if(obj.messageType==3){ var onlineName = obj.onlineUsers; var option = null; for(var i=0;i<onlineName.length;i++){ if(!(onlineName[i]==document.getElementById('username').value)){ option+="<option>"+onlineName[i]+"</option>" } } $("#onLineUser").append(option); console.log("获取了在线的名单"+onlineName.toString()); } else{ setMessageInnerHTML(obj.fromusername+"对"+obj.tousername+"说:"+obj.textMessage); } }; //连接关闭的回调事件 webSocket.onclose = function() { console.log("连接已关闭..."); setMessageInnerHTML("连接已经关闭...."); }; } else{ // 浏览器不支持 WebSocket alert("您的浏览器不支持 WebSocket!"); } //将消息显示在网页上 function setMessageInnerHTML(innerHTML) { document.getElementById('message').innerHTML += innerHTML + '<br/>'; } function closeWebSocket() { //直接关闭websocket的连接 webSocket.close(); } function send() { var selectText = $("#onLineUser").find("option:selected").text(); if(selectText=="--所有--"){ selectText = "All"; } else{ setMessageInnerHTML(document.getElementById('username').value+"对"+selectText+"说:"+ $("#text").val()); } var message = { "message":document.getElementById('text').value, "username":document.getElementById('username').value, "to":selectText }; webSocket.send(JSON.stringify(message)); $("#text").val(""); } </script>

</body>
</html>

config

package com.websocket.config;

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

/** * 开启WebSocket支持 */
@Configuration
public class WebSocketStompConfig{
    @Bean
    public ServerEndpointExporter serverEndpointExporter()
    {
        return new ServerEndpointExporter();
    }
}

socket

package com.websocket.socket;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Component
@ServerEndpoint("/websocket/{username}")
public class WebSocket {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /** * 在线人数 */
    public static int onlineNumber = 0;
    /** * 以用户的姓名为key,WebSocket为对象保存起来 */
    private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
    /** * 会话 */
    private Session session;
    /** * 用户名称 */
    private String username;
    /** * 建立连接 * * @param session */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session)
    {
        onlineNumber++;
        logger.info("现在来连接的客户id:"+session.getId()+"用户名:"+username);
        this.username = username;
        this.session = session;
        logger.info("有新连接加入! 当前在线人数" + onlineNumber);
        try {
            //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
            //先给所有人发送通知,说我上线了
            Map<String,Object> map1 =new HashMap<>();
            map1.put("messageType",1);
            map1.put("username",username);
            sendMessageAll(JSON.toJSONString(map1),username);

            //把自己的信息加入到map当中去
            clients.put(username, this);
            //给自己发一条消息:告诉自己现在都有谁在线
            Map<String,Object> map2 = new HashMap<>();
            map2.put("messageType",3);
            //移除掉自己
            Set<String> set = clients.keySet();
            map2.put("onlineUsers",set);
            sendMessageTo(JSON.toJSONString(map2),username);
        }
        catch (IOException e){
            logger.info(username+"上线的时候通知所有人发生了错误");
            e.printStackTrace();
        }


    }

    @OnError
    public void onError(Session session, Throwable error) {
        logger.info("服务端发生了错误"+error.getMessage());
        //error.printStackTrace();
    }
    /** * 连接关闭 */
    @OnClose
    public void onClose()
    {
        onlineNumber--;
        //webSockets.remove(this);
        clients.remove(username);
        try {
            //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
            Map<String,Object> map1 = new HashMap<>();
            map1.put("messageType",2);
            map1.put("onlineUsers",clients.keySet());
            map1.put("username",username);
            sendMessageAll(JSON.toJSONString(map1),username);
        }
        catch (IOException e){
            logger.info(username+"下线的时候通知所有人发生了错误");
            e.printStackTrace();
        }
        logger.info("有连接关闭! 当前在线人数" + onlineNumber);
    }

    /** * 收到客户端的消息 * * @param message 消息 * @param session 会话 */
    @OnMessage
    public void onMessage(String message, Session session)
    {
        try {

            logger.info("来自客户端消息:" + message+"客户端的id是:"+session.getId());
            if (isJson(message)) {
            JSONObject jsonObject = JSON.parseObject(message);
            String textMessage = jsonObject.getString("message");
            String fromusername = jsonObject.getString("username");
            String tousername = jsonObject.getString("to");
            //如果不是发给所有,那么就发给某一个人
            //messageType 1代表上线 2代表下线 3代表在线名单 4代表普通消息
            Map<String,Object> map1 = new HashMap<>();
            map1.put("messageType",4);
            map1.put("textMessage",textMessage);
            map1.put("fromusername",fromusername);
            if(tousername.equals("All")){
                map1.put("tousername","所有人");
                sendMessageAll(JSON.toJSONString(map1),fromusername);
            }
            else{
// System.out.println( this.username); // 发消息的人

                map1.put("tousername",tousername); //给谁发送消息
                sendMessageTo(JSON.toJSONString(map1),tousername);
            }
            }
        } catch (Exception e){
            logger.info("发生了错误了");
            e.printStackTrace();
        }

    }

    public void sendMessageTo(String message, String ToUserName) throws IOException {
        for (WebSocket item : clients.values()) {
            if (item.username.equals(ToUserName) ) {
                item.session.getAsyncRemote().sendText(message);
                break;
            }
        }
    }

    public void sendMessageAll(String message,String FromUserName) throws IOException {
        for (WebSocket item : clients.values()) {
            item.session.getAsyncRemote().sendText(message);
        }
    }

    public static synchronized int getOnlineCount() {
        return onlineNumber;
    }

    public  boolean isJson(String content) {
        if(StringUtils.isEmpty(content)){
            return false;
        }
        boolean isJsonObject = true;
        boolean isJsonArray = true;
        try {
            JSONObject.parseObject(content);
        } catch (Exception e) {
            isJsonObject = false;
        }
        try {
            JSONObject.parseArray(content);
        } catch (Exception e) {
            isJsonArray = false;
        }
        if(!isJsonObject && !isJsonArray){ //不是json格式
            return false;
        }
        return true;
    }

}

controller

前端发送ws请求访问此接口和后端ws建立持久连接

package com.websocket.controller;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
@RequestMapping("/web")
public class WebSocketController {
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    // http://localhost:9030/web/websocket/hu
    @RequestMapping("/websocket/{name}")
    public ModelAndView webSocket(@PathVariable String name){
        ModelAndView mode=new ModelAndView();
        try{
            logger.info("跳转到websocket的页面上");

            mode.addObject("username",name);
            mode.setViewName("websocket");
            return mode;
        }
        catch (Exception e){
            logger.info("跳转到websocket的页面上发生异常,异常信息是:"+e.getMessage());
            mode.addObject("err",e.getMessage());
            return mode;
        }
    }

}

启动类

package com.websocket;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WebsocketApp {

    public static void main(String[] args) {
        SpringApplication.run(WebsocketApp.class,args);
    }
}

在自己电脑或者在多台电脑都行 但是注意必须在一个网络下

然后我们访问 http://localhost:9030/web/websocket/用户名称 (开启4个 可以在不同游览器都行)

效果展示

登录4个不同的账户

只要有人登录其他账户都能接受到登录的消息

给全部人发送消息

给指定的人发送消息

退出账户

当关闭游览器或者点击关闭连接都会退出和服务器的ws连接

设计一个聊天系统的方案

在上面的案例中我们是可以同时和多人同时访问,但是如果下线后在上线那么之前自己的消息将清空了,如何实现向微信一样的效果呢??

我们需要一个用户表(一般在一个项目里初期会有人设计一套权限系统的用来管理用户的) 所以我们只需要设计消息表就行:

表分为:

一对一消息表(一个人对一个人的消息)

必须有:发消息的用户id和接受消息的用户id ,消息字段,消息时间字段 其他字段需要和团队商量
1.
多对多消息表(群表)

必须有: 群id ,群名称字段 ,用户id,发送消息的用户id, 消息字段,消息时间字段

当用户id==发消息用户的id的话那么就代表这条消息是你发送的,前端进行特殊处理下
点赞 -收藏-关注-便于以后复习和收到最新内容有其他问题在评论区讨论-或者私信我-收到会在第一时间回复如有侵权,请私信联系我感谢,配合,希望我的努力对你有帮助^_^

相关文章