前言

本文中搭建了一个简易的多人聊天室,使用了WebSocket的基础特性。

源代码来自老外的一篇好文:

本文内容摘要:

初步理解WebSocket的前后端交互逻辑

手把手使用 SpringBoot + WebSocket 搭建一个多人聊天室Demo

代码源码及其解释

前端展示页面

此外,在下一篇文章中,我们将做到:

对该WebSocket聊天室进行分布式改造,同时部署多台机器来作为集群,支撑高并发。

保存用户session,并且在集群上实现session同步,比如实时展示当前在线的用户!

正文

WebSocket多人在线聊天室

本文工程源代码:

新建工程

我们新建一个SpringBoot2的项目工程,在默认依赖中,添加websocket依赖:

org.springframework.boot

spring-boot-starter-websocket

WebSocket 配置

我们先来设置websocket的配置,新建config文件夹,在里面新建类WebSocketConfig

import org.springframework.context.annotation.Configuration;

import org.springframework.messaging.simp.config.MessageBrokerRegistry;

import org.springframework.web.socket.config.annotation.*;

@Configuration

@EnableWebSocketMessageBroker

public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override

public void registerStompEndpoints(StompEndpointRegistry registry) {

registry.addEndpoint("/ws").withSockJS();

}

@Override

public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.setApplicationDestinationPrefixes("/app");

registry.enableSimpleBroker("/topic");

}

}

代码解释:

@EnableWebSocketMessageBroker用于启用我们的WebSocket服务器。

我们实现了WebSocketMessageBrokerConfigurer接口,并实现了其中的方法。

在第一种方法中,我们注册一个websocket端点,客户端将使用它连接到我们的websocket服务器。

withSockJS()是用来为不支持websocket的浏览器启用后备选项,使用了SockJS。

方法名中的STOMP是来自Spring框架STOMP实现。STOMP代表简单文本导向的消息传递协议。它是一种消息传递协议,用于定义数据交换的格式和规则。为啥我们需要这个东西?因为WebSocket只是一种通信协议。它没有定义诸如以下内容:如何仅向订阅特定主题的用户发送消息,或者如何向特定用户发送消息。我们需要STOMP来实现这些功能。

在configureMessageBroker方法中,我们配置一个消息代理,用于将消息从一个客户端路由到另一个客户端。

第一行定义了以“/app”开头的消息应该路由到消息处理方法(之后会定义这个方法)。

第二行定义了以“/topic”开头的消息应该路由到消息代理。消息代理向订阅特定主题的所有连接客户端广播消息。

在上面的示例中,我们使用的是内存中的消息代理。

之后也可以使用RabbitMQ或ActiveMQ等其他消息代理。

创建 ChatMessage 实体

ChatMessage用来在客户端和服务端中交互

我们新建model文件夹,创建实体类ChatMessage。

public class ChatMessage {

private MessageType type;

private String content;

private String sender;

public enum MessageType {

CHAT,

JOIN,

LEAVE

}

public MessageType getType() {

return type;

}

public void setType(MessageType type) {

this.type = type;

}

public String getContent() {

return content;

}

public void setContent(String content) {

this.content = content;

}

public String getSender() {

return sender;

}

public void setSender(String sender) {

this.sender = sender;

}

}

实体中,有三个字段:

type:消息类型

content:消息内容

sender:发送者

类型有三种:

CHAT: 消息

JOIN:加入

LEAVE:离开

创建Controller来接收和发送消息

创建controller文件夹,在controller文件夹添加类ChatController

import com.example.websocketdemo.model.ChatMessage;

import org.springframework.messaging.handler.annotation.MessageMapping;

import org.springframework.messaging.handler.annotation.Payload;

import org.springframework.messaging.handler.annotation.SendTo;

import org.springframework.messaging.simp.SimpMessageHeaderAccessor;

import org.springframework.stereotype.Controller;

@Controller

public class ChatController {

@MessageMapping("/chat.sendMessage")

@SendTo("/topic/public")

public ChatMessage sendMessage(@Payload ChatMessage chatMessage) {

return chatMessage;

}

@MessageMapping("/chat.addUser")

@SendTo("/topic/public")

public ChatMessage addUser(@Payload ChatMessage chatMessage,

SimpMessageHeaderAccessor headerAccessor) {

// Add username in web socket session

headerAccessor.getSessionAttributes().put("username", chatMessage.getSender());

return chatMessage;

}

}

代码解释:

我们在websocket配置中,从目的地以/app开头的客户端发送的所有消息都将路由到这些使用@MessageMapping注释的消息处理方法。

例如,具有目标/app/chat.sendMessage的消息将路由到sendMessage()方法,并且具有目标/app/chat.addUser的消息将路由到addUser()方法

添加WebSocket事件监听

完成了上述代码后,我们还需要对socket的连接和断连事件进行监听,这样我们才能广播用户进来和出去等操作。

创建listener文件夹,新建WebSocketEventListener类

import com.example.websocketdemo.model.ChatMessage;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.event.EventListener;

import org.springframework.messaging.simp.SimpMessageSendingOperations;

import org.springframework.messaging.simp.stomp.StompHeaderAccessor;

import org.springframework.stereotype.Component;

import org.springframework.web.socket.messaging.SessionConnectedEvent;

import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component

public class WebSocketEventListener {

private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class);

@Autowired

private SimpMessageSendingOperations messagingTemplate;

@EventListener

public void handleWebSocketConnectListener(SessionConnectedEvent event) {

logger.info("Received a new web socket connection");

}

@EventListener

public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {

StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

String username = (String) headerAccessor.getSessionAttributes().get("username");

if(username != null) {

logger.info("User Disconnected : " + username);

ChatMessage chatMessage = new ChatMessage();

chatMessage.setType(ChatMessage.MessageType.LEAVE);

chatMessage.setSender(username);

messagingTemplate.convertAndSend("/topic/public", chatMessage);

}

}

}

代码解释:

我们已经在ChatController中定义的addUser()方法中广播了用户加入事件。因此,我们不需要在SessionConnected事件中执行任何操作。

在SessionDisconnect事件中,编写代码用来从websocket会话中提取用户名,并向所有连接的客户端广播用户离开事件。

创建前端聊天室页面

我们在src/main/resources文件下创建前端文件,结构类似这样:

static

└── css

└── main.css

└── js

└── main.js

└── index.html

HTML文件 index.html

HTML文件包含用于显示聊天消息的用户界面。它包括sockjs和stomp 两个js库。

SockJS是一个WebSocket客户端,它尝试使用本机WebSockets,并为不支持WebSocket的旧浏览器提供支持。STOMP JS是javascript的stomp客户端。

笔者在文件里使用了国内的CDN源

Spring Boot WebSocket Chat Application

Sorry! Your browser doesn't support Javascript

Type your username

Start Chatting

Spring WebSocket Chat Demo

Connecting...

Send

JavaScript main.js

添加连接到websocket端点以及发送和接收消息所需的javascript。

'use strict';

var usernamePage = document.querySelector('#username-page');

var chatPage = document.querySelector('#chat-page');

var usernameForm = document.querySelector('#usernameForm');

var messageForm = document.querySelector('#messageForm');

var messageInput = document.querySelector('#message');

var messageArea = document.querySelector('#messageArea');

var connectingElement = document.querySelector('.connecting');

var stompClient = null;

var username = null;

var colors = [

'#2196F3', '#32c787', '#00BCD4', '#ff5652',

'#ffc107', '#ff85af', '#FF9800', '#39bbb0'

];

function connect(event) {

username = document.querySelector('#name').value.trim();

if(username) {

usernamePage.classList.add('hidden');

chatPage.classList.remove('hidden');

var socket = new SockJS('/ws');

stompClient = Stomp.over(socket);

stompClient.connect({}, onConnected, onError);

}

event.preventDefault();

}

function onConnected() {

// Subscribe to the Public Topic

stompClient.subscribe('/topic/public', onMessageReceived);

// Tell your username to the server

stompClient.send("/app/chat.addUser",

{},

JSON.stringify({sender: username, type: 'JOIN'})

)

connectingElement.classList.add('hidden');

}

function onError(error) {

connectingElement.textContent = 'Could not connect to WebSocket server. Please refresh this page to try again!';

connectingElement.style.color = 'red';

}

function sendMessage(event) {

var messageContent = messageInput.value.trim();

if(messageContent && stompClient) {

var chatMessage = {

sender: username,

content: messageInput.value,

type: 'CHAT'

};

stompClient.send("/app/chat.sendMessage", {}, JSON.stringify(chatMessage));

messageInput.value = '';

}

event.preventDefault();

}

function onMessageReceived(payload) {

var message = JSON.parse(payload.body);

var messageElement = document.createElement('li');

if(message.type === 'JOIN') {

messageElement.classList.add('event-message');

message.content = message.sender + ' joined!';

} else if (message.type === 'LEAVE') {

messageElement.classList.add('event-message');

message.content = message.sender + ' left!';

} else {

messageElement.classList.add('chat-message');

var avatarElement = document.createElement('i');

var avatarText = document.createTextNode(message.sender[0]);

avatarElement.appendChild(avatarText);

avatarElement.style['background-color'] = getAvatarColor(message.sender);

messageElement.appendChild(avatarElement);

var usernameElement = document.createElement('span');

var usernameText = document.createTextNode(message.sender);

usernameElement.appendChild(usernameText);

messageElement.appendChild(usernameElement);

}

var textElement = document.createElement('p');

var messageText = document.createTextNode(message.content);

textElement.appendChild(messageText);

messageElement.appendChild(textElement);

messageArea.appendChild(messageElement);

messageArea.scrollTop = messageArea.scrollHeight;

}

function getAvatarColor(messageSender) {

var hash = 0;

for (var i = 0; i < messageSender.length; i++) {

hash = 31 * hash + messageSender.charCodeAt(i);

}

var index = Math.abs(hash % colors.length);

return colors[index];

}

usernameForm.addEventListener('submit', connect, true)

messageForm.addEventListener('submit', sendMessage, true)

代码解释:

connect()函数使用SockJS和stomp客户端连接到我们在Spring Boot中配置的/ws端点。

成功连接后,客户端订阅/topic/public,并通过向/app/chat.addUser目的地发送消息将该用户的名称告知服务器。

stompClient.subscribe()函数采用一种回调方法,只要消息到达订阅主题,就会调用该方法。

其它的代码用于在屏幕上显示和格式化消息。

CSS main.css

* {

-webkit-box-sizing: border-box;

-moz-box-sizing: border-box;

box-sizing: border-box;

}

html,body {

height: 100%;

overflow: hidden;

}

body {

margin: 0;

padding: 0;

font-weight: 400;

font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;

font-size: 1rem;

line-height: 1.58;

color: #333;

background-color: #f4f4f4;

height: 100%;

}

body:before {

height: 50%;

width: 100%;

position: absolute;

top: 0;

left: 0;

background: #128ff2;

content: "";

z-index: 0;

}

.clearfix:after {

display: block;

content: "";

clear: both;

}

.hidden {

display: none;

}

.form-control {

width: 100%;

min-height: 38px;

font-size: 15px;

border: 1px solid #c8c8c8;

}

.form-group {

margin-bottom: 15px;

}

input {

padding-left: 10px;

outline: none;

}

h1, h2, h3, h4, h5, h6 {

margin-top: 20px;

margin-bottom: 20px;

}

h1 {

font-size: 1.7em;

}

a {

color: #128ff2;

}

button {

box-shadow: none;

border: 1px solid transparent;

font-size: 14px;

outline: none;

line-height: 100%;

white-space: nowrap;

vertical-align: middle;

padding: 0.6rem 1rem;

border-radius: 2px;

transition: all 0.2s ease-in-out;

cursor: pointer;

min-height: 38px;

}

button.default {

background-color: #e8e8e8;

color: #333;

box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);

}

button.primary {

background-color: #128ff2;

box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);

color: #fff;

}

button.accent {

background-color: #ff4743;

box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.12);

color: #fff;

}

#username-page {

text-align: center;

}

.username-page-container {

background: #fff;

box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);

border-radius: 2px;

width: 100%;

max-width: 500px;

display: inline-block;

margin-top: 42px;

vertical-align: middle;

position: relative;

padding: 35px 55px 35px;

min-height: 250px;

position: absolute;

top: 50%;

left: 0;

right: 0;

margin: 0 auto;

margin-top: -160px;

}

.username-page-container .username-submit {

margin-top: 10px;

}

#chat-page {

position: relative;

height: 100%;

}

.chat-container {

max-width: 700px;

margin-left: auto;

margin-right: auto;

background-color: #fff;

box-shadow: 0 1px 11px rgba(0, 0, 0, 0.27);

margin-top: 30px;

height: calc(100% - 60px);

max-height: 600px;

position: relative;

}

#chat-page ul {

list-style-type: none;

background-color: #FFF;

margin: 0;

overflow: auto;

overflow-y: scroll;

padding: 0 20px 0px 20px;

height: calc(100% - 150px);

}

#chat-page #messageForm {

padding: 20px;

}

#chat-page ul li {

line-height: 1.5rem;

padding: 10px 20px;

margin: 0;

border-bottom: 1px solid #f4f4f4;

}

#chat-page ul li p {

margin: 0;

}

#chat-page .event-message {

width: 100%;

text-align: center;

clear: both;

}

#chat-page .event-message p {

color: #777;

font-size: 14px;

word-wrap: break-word;

}

#chat-page .chat-message {

padding-left: 68px;

position: relative;

}

#chat-page .chat-message i {

position: absolute;

width: 42px;

height: 42px;

overflow: hidden;

left: 10px;

display: inline-block;

vertical-align: middle;

font-size: 18px;

line-height: 42px;

color: #fff;

text-align: center;

border-radius: 50%;

font-style: normal;

text-transform: uppercase;

}

#chat-page .chat-message span {

color: #333;

font-weight: 600;

}

#chat-page .chat-message p {

color: #43464b;

}

#messageForm .input-group input {

float: left;

width: calc(100% - 85px);

}

#messageForm .input-group button {

float: left;

width: 80px;

height: 38px;

margin-left: 5px;

}

.chat-header {

text-align: center;

padding: 15px;

border-bottom: 1px solid #ececec;

}

.chat-header h2 {

margin: 0;

font-weight: 500;

}

.connecting {

padding-top: 5px;

text-align: center;

color: #777;

position: absolute;

top: 65px;

width: 100%;

}

@media screen and (max-width: 730px) {

.chat-container {

margin-left: 10px;

margin-right: 10px;

margin-top: 10px;

}

}

@media screen and (max-width: 480px) {

.chat-container {

height: calc(100% - 30px);

}

.username-page-container {

width: auto;

margin-left: 15px;

margin-right: 15px;

padding: 25px;

}

#chat-page ul {

height: calc(100% - 120px);

}

#messageForm .input-group button {

width: 65px;

}

#messageForm .input-group input {

width: calc(100% - 70px);

}

.chat-header {

padding: 10px;

}

.connecting {

top: 60px;

}

.chat-header h2 {

font-size: 1.1em;

}

}

整个项目结构如下:

启动

启动SpringBoot项目

效果入下:

补充:使用RabbitMQ代替内存作为消息代理

添加依赖:

org.springframework.boot

spring-boot-starter-amqp

org.springframework.boot

spring-boot-starter-reactor-netty

然后将WebSocketConfig类中configureMessageBroker方法改为使用RabbitMq,完成!

public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.setApplicationDestinationPrefixes("/app");

// Use this for enabling a Full featured broker like RabbitMQ

registry.enableStompBrokerRelay("/topic")

.setRelayHost("localhost")

.setRelayPort(61613)

.setClientLogin("guest")

.setClientPasscode("guest");

}

如此一来,便可以通过RabbitMq进行消息的订阅。

总结

我们在本文中搭建了一个简易的多人聊天室,使用了WebSocket的特性。

本文工程源代码:

在下一篇文章中,我们将做到:

WebSocket进行分布式改造,同时部署多台机器来作为集群,支撑高并发。

保存用户session,并且在集群上实现session同步,比如实时展示当前在线的用户!

下一篇文章过两天奉上,也可以关注我的公众号:Rude3Knife,就不会忘记了看,哈哈哈哈哈。

关注我

我目前是一名后端开发工程师。主要关注后端开发,数据安全,爬虫,边缘计算等方向。

微信:yangzd1102

Github:@qqxx6661

个人博客:

CSDN:@Rude3Knife

知乎:@Zhendong

简书:@蛮三刀把刀

掘金:@蛮三刀把刀

原创博客主要内容

Java知识点复习全手册

Leetcode算法题解析

剑指offer算法题解析

SpringCloud菜鸟入门实战系列

SpringBoot菜鸟入门实战系列

爬虫相关技术文章

后端开发相关技术文章

个人公众号:Rude3Knife

个人公众号:Rude3Knife

如果文章对你有帮助,不妨收藏起来并转发给您的朋友们。

rudesocket如何使用_[WebSocket入门]手把手搭建WebSocket多人在线聊天室(SpringBoot+WebS...相关推荐

  1. Python+django网页设计入门(20):使用WebSocket创建多人在线聊天室

    2019年3月8日-10日公众号送书活动:中小学生Python课应该学什么 ================ 前导课程: Python+django网页设计入门(19):创建新模型扩展自带用户表的字段 ...

  2. 基于腾讯云搭建属于自己的Fiora聊天室

    这是基于fiora开源系统搭建的一个即时在线聊天室,使用宝塔面板搭建.只需一键注册即可享受在线聊天的乐趣. 前言 本项目基于腾讯云轻量搭建,目前腾讯云启动了无忧计划,超级划算,欢迎大家选购~ 购买链接 ...

  3. WebSocket创建局域网在线聊天室

    WebSocket的简要介绍: WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议. WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服 ...

  4. 从头搭建一个基于 Python 的在线聊天室

    本场 Chat,是基于 Python + Redis + Flask 来搭建一个简单易用的在线聊天室.完全从零开始,一步一步完成整个项目. 主要分享内容: Flask 项目结构 Python Redi ...

  5. 云信小课堂|搭建应用级别在线聊天室,7步就够了!

    Vol. 6 从2000年至今,聊天室一直活跃在人们的各种生活场景中,目前广泛运用于超级小班课.互动大班课.连麦开黑.主播 PK 等场景,还具备文本.表情.点赞.撒花等互动方式,架起沟通桥梁的同时,玩 ...

  6. 在线聊天室的消息单聊的实现——springboot整合WebSocket(二)

    一.声明 项目的搭建请大家移步到:在线聊天室的消息群聊的实现--springboot整合WebSocket(一) 单聊的实现是在群聊项目上进行延申改造的. 二.引入依赖 <dependency& ...

  7. Java+Springboot+Websocket在线聊天室

    1.什么是websocket? websocket是由HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯.它是一种在单个TCP连接上进行全双工通信的协议.W ...

  8. Docker快速搭建TeamSpeak多人语音聊天服务器

    Docker快速搭建TeamSpeak多人语音聊天服务器 第一步执行: git clone https://github.com/overshard/docker-teamspeak cd docke ...

  9. springboot+websocket构建在线聊天室(群聊+单聊)

    系列导读: 1.springboot+websocket构建在线聊天室(群聊+单聊) 2.Spring Boot WebSocket:单聊(实现思路) 3.Websocket Stomp+Rabbit ...

最新文章

  1. VC++中从txt文本中读取数据并且存到二维数组中
  2. 【原生JS插件】LoadingBar页面顶部加载进度条
  3. 可以自动撑起的html样式,好好玩:CSS3抖动样式CSS Shake让你的网页酷炫起来
  4. 详解C++中的函数调用和下标以及成员访问运算符的重载
  5. onvif学习笔记10:获取RTSP流地址
  6. 服务链路追踪配置mysql_学习微服务的服务链路追踪——Spring Cloud Sleuth+zipkin
  7. .NET中栈和堆的比较
  8. qq象棋辅助 android,QQ象棋自动下棋
  9. html手机表白弹窗,火热的程序员表白方式,调皮弹窗表白代码,赶紧拿去试试吧...
  10. 恩山斐讯论坛k2p_K2P A1 A2 路由器刷机教程 最详细教程,适合新手!
  11. android 计时器函数,Android Chronometer控件实现计时器函数详解
  12. IP地址冲突的原因及其解决方法
  13. word粘贴超出页面怎么办
  14. java基础知识面试题(41-95)
  15. 卷积神经网络的学习笔记1
  16. 编写微信聊天机器人3《聊天精灵WeChatGenius》:使用Xposed来hook微信,找到微信进程。
  17. Go语言交叉编译二进制文件
  18. linux下的磁盘常用工具
  19. IP地址转换--C语言
  20. “明德”网友关于《郭初阳课堂实录》一书的介绍

热门文章

  1. R语言使用ggplot2包使用geom_violin函数绘制分组小提琴图(配置填充色)实战
  2. R可视化包ggplot2设置透明背景实战
  3. R语言set.seed函数的意义及实战
  4. R使用tsne进行高维数据可视化实战:二维可视化、三维可视化
  5. 机器学习、超参数、最优超参数、网格搜索、随机搜索、贝叶斯优化、Google Vizier、Adviser
  6. Linux rm过滤后的目录6,Linux的部分命令
  7. 怎样训练左右手协调_2019中考体育训练计划
  8. JDK 环境变量设置参考
  9. STM32-超级终端显示日历
  10. javascript 函数默认参数 只适用于 ES6