springboot之WebSocket

springboot之webSocket连接

服务端

开启功能

功能开启需要在pom.xml中导入包,并配置Configuration创建ServerEndpointExporterbean

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

配置WebScoket端点

使用@ServerEndpoint注解,与controller差不多一样配上匹配路径,方法使用@OnOpen @OnMessage @OnClose等注解对应WebSocket开启连接、接受信息、关闭连接的操作,这里使用了自定义的WebSocketSessionManager类来完成具体的操作
需要注意的是在向WebSocket类注入bean时需要将字段设置为static成为类变量,因为spring管理的bean默认是单例的,而WebSocket是多对象的

@ServerEndpoint("/ws/{username}")
@Slf4j
@Component
public class WebSocketController {

    // 具体工作交由 WebSocketSessionManager
    private static WebSocketSessionManager manager;

    @Autowired
    public void setManager(WebSocketSessionManager manager) {
        WebSocketController.manager = manager;
    }

    // 匹配uri上的参数使用的是@PathParam注解,跟controller的注解有区别
    @OnOpen
    public void onOpen(Session session, @PathParam("username") String username) {
        manager.addSession(session, username);
    }

    @OnMessage
    public void onMessage(Session session, String msg) {
        log.info("session {} send msg {}", session.getId(), msg);
    }

    @OnClose
    public void onClose(@PathParam("username") String username) {
        manager.removeSession(username);
    }
}

这里就简单的使用用户名来对应session保存在map中,注意需要使用同步的容器

@Slf4j
@Component
public class WebSocketSessionManager {
    private ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();

    // 必须是同步的容器
    public ConcurrentHashMap<String, Session> getSessions() {
        return sessions;
    }

    // 注入 ObjectMapper 序列化对象成 json 字符串
    private final ObjectMapper objectMapper;

    public WebSocketSessionManager(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    public void addSession(Session session, String username) {
        if (!sessions.containsKey(username)) {
            sessions.put(username, session);
            log.info("用户 {} 加入", username);
            refeshCount(sessions.size());
        }
    }

    public void removeSession(String username) {
        if (sessions.containsKey(username)) {
            sessions.remove(username);
            log.info("用户 {} 退出", username);
            refeshCount(sessions.size());
        }
    }

    public void sendMessageToUser(String username, String message) {
        Session session = sessions.get(username);
        try {
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("webSocket信息发送异常", e);
        }
    }

    public void sendMessageToAll(String username, String message) {
        HashMap<String, Object> data = new HashMap<>();
        data.put("code", 1);
        data.put("username", username);
        data.put("message", message);
        String dataJson = "";
        try {
            dataJson = objectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        for (Session session : sessions.values()) {
            try {
                session.getBasicRemote().sendText(dataJson);
            } catch (IOException e) {
                log.error("webSocket信息发送异常", e);
            }
        }
    }

    public void refeshCount(int count) {
        HashMap<String, Object> data = new HashMap<>();
        data.put("code", 2);
        data.put("count", count);
        String dataJson = "";
        try {
            dataJson = objectMapper.writeValueAsString(data);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        for (Session session : sessions.values()) {
            try {
                session.getBasicRemote().sendText(dataJson);
            } catch (IOException e) {
                log.error("webSocket信息发送异常", e);
            }
        }
    }
}

实体类、controller

简单的实体,其实只用到了username

@Data
public class User {
    private String username;
    private String password;
}

controller包含2个方法:

  1. 在第一次访问页面时返回当前人数
  2. 接收聊天信息,转交webSocketSessionManager发送给所有人
@Slf4j
@RestController
public class ChatRoomController {
    private final WebSocketSessionManager webSocketSessionManager;

    public ChatRoomController(WebSocketSessionManager webSocketSessionManager) {
        this.webSocketSessionManager = webSocketSessionManager;
    }

    @GetMapping("count")
    public int count() {
        return webSocketSessionManager.getSessions().size();
    }

    @PostMapping("message")
    public void sendMessage(@RequestBody Map<String, Object> map) {
        log.info("user: {}, message: {}", map.get("username"), map.get("message"));
        String message = (String) map.get("message");
        String username = (String) map.get("username");
        webSocketSessionManager.sendMessageToAll(username, message);
    }
}

客户端

客户端文件index.html放在resources目录下的static文件夹中,可以通过http://localhost:8080/index.html直接访问
主要引入使用到了:

  1. vue:框架
  2. element-ui:ui库
  3. axios:发送http请求
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>index</title>
<body>
<!-- 引入vue -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<!-- 引入组件库 -->
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<!-- 引入axios -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<div id="app">
    <el-container>
        <el-header></el-header>
        <el-main>
            <el-row :gutter="20">
                <el-col :span="2" :offset="6">
                    <el-input v-model="username" placeholder="输入姓名" v-bind:disabled="enterDisabled"></el-input>
                </el-col>
                <el-col :span="2">
                    <el-button type="primary" v-on:click="connectWs" v-bind:disabled="enterDisabled">进入聊天室</el-button>
                </el-col>
                <el-col :span="4" style="padding-top: 18px">
                    当前人数:{{count}}
                </el-col>
            </el-row>
            <el-row :gutter="20">
                <el-col :span="10" :offset="6">
                    <el-card id="chat" shadow="never">
                        <div v-for="text in textList" :key="text.id">
                            <el-row>
                                <el-col :span="2">
                                    <div style="color: deepskyblue">{{text.username}} :</div>
                                </el-col>
                                <el-col :span="22">
                                    <div>{{text.message}}</div>
                                </el-col>
                            </el-row>
                        </div>
                    </el-card>

                </el-col>
            </el-row>

            <el-row :gutter="20">
                <el-col :span="8" :offset="6">
                    <el-input v-model="message" placeholder="请输入内容"></el-input>
                </el-col>

                <el-col :span="2">
                    <el-button type="primary" v-on:click="sendMessage" v-bind:disabled="sendDisabled">发送消息</el-button>
                </el-col>

            </el-row>
        </el-main>
    </el-container>
</div>
</body>
<script>
    let app = new Vue({
        el: '#app',
        data: {
            username: "",
            message: "",
            count: 0,
            enterDisabled: false,
            sendDisabled: true,
            textList: [],
            websocket: null
        },
        methods: {
            connectWs() {
                this.websocket = new WebSocket(
                    "ws://localhost:8080/ws/" +
                    this.username
                );
                this.websocket.onopen = this.websocketonopen;
                this.websocket.onmessage = this.websocketonmessage;
                this.websocket.onerror = this.websocketonerror;
                this.websocket.onclose = this.websocketclose;
                this.sendDisabled = false
                this.enterDisabled = true
            },
            websocketonopen() {
                //连接建立之后执行send方法发送数据
            },
            websocketonerror() {
                //连接建立失败重连
                this.connectWs();
            },
            websocketonmessage(e) {
                //数据接收
                let data = JSON.parse(e.data);
                switch (data.code) {
                    case 1:
                        this.textList.push(data)
                        this.scrollToBottom();
                        console.log(data)
                        break
                    case 2:
                        console.log(data)
                        this.count = data.count
                        break
                }
            },
            websocketsend(Data) {
                //数据发送
                this.websocket.send(Data);
            },
            websocketclose(e) {
                //关闭
                console.log("断开连接", e);
            },
            sendMessage: function () {
                axios.post('message', {
                    username: this.username,
                    message: this.message
                }).then((response) => {
                    this.message = ""
                })
            },
            scrollToBottom: function () {
                let div = document.getElementById('chat')
                div.scrollTop = div.scrollHeight
            },
        },
        created: function () {
            axios.get("/count").then((response) => {
                this.count = response.data
                console.log(this.count)
                console.log(response.data)
            })
        }

    });
</script>
<style>
    .el-row {
        margin-bottom: 20px;

    }

    body {
        margin: 0px;
        margin-top: 100px;
    }

    .el-card {
        height: 400px;
        overflow-y: auto;
    }

    .el-card__body {
        padding-top: 5px;
        padding-right: 2px;
    }
</style>
</html>

简单演示

批注 2020-03-02 192641.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352