SpringBoot整合WebSocket

SpringBoot整合WebSocket

”人不是一座孤岛,所有人的不幸皆是我的不幸“——出处未知

人不是一个喜欢独立个体,在上个世纪有短信,这个世纪有微信,可见,沟通在人们中占据了多么重要的地位,而我们今天,就来使用WebSocket,来打造一个聊天室。

在此之前,我想先说说WebJars,你如果明白,也可以直接跳到WebSocket。

WebJars

是什么

什么是WebJars?WebJars是将客户端(浏览器)资源(JavaScript,Css等)打成jar包文件,以对资源进行统一依赖管理。WebJars的jar包部署在Maven中央仓库上。

为什么

我们在开发Java web项目的时候会使用像Maven,Gradle等构建工具以实现对jar包版本依赖管理,以及项目的自动化管理,但是对于JavaScript,Css等前端资源包,我们只能采用拷贝到webapp目录下的手工方式,这样做就无法对这些资源进行依赖管理。而且容易导致文件混乱、版本不一致等问题。那么WebJars就提供给我们这些前端资源的jar包形式,我们就可以进行依赖管理。

WebJars是将这些通用的Web前端资源打包成Java的Jar包,然后借助Maven工具对其管理,保证这些Web资源版本唯一性,升级也比较容易。关于webjars资源,有一个专门的网站http://www.webjars.org/,我们可以到这个网站上找到自己需要的资源,在自己的工程中添加入maven依赖,即可直接使用这些资源了。

怎么样

可以在pom文件中引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        <!--Webjars版本定位工具-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<!-- Webjars的一个包,有各种资源-->
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>mdui</artifactId>
<version>0.4.0</version>
</dependency>
<!-- wenjars的JQ库-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>

有了它们,就能够在Html页面上引入它们,就能够直接使用它们的样式和语法,节省了很多开发所需的依赖,使得我们前端引入更为方便。

1
2
3
<link rel="stylesheet" th:href="@{/webjars/mdui/dist/css/mdui.css}">
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/webjars/mdui/dist/js/mdui.js}"></script>

使用Thymeleaf语法进行引用,这里是世界引用 /webjars的目录。

WebSocket

现在,就来正式的讲一下,WebSocket的使用吧。

概念

简介

WebSocket是一种与HTTP不同的协议。两者都位于OSI模型应用层,并且都依赖于传输层的TCP协议。 虽然它们不同,但RFC 6455规定:“WebSocket设计为通过80和443端口工作,以及支持HTTP代理和中介”,从而使其与HTTP协议兼容。 为了实现兼容性,WebSocket握手使用HTTP Upgrade头从HTTP协议更改为WebSocket协议。

WebSocket协议支持Web浏览器(或其他客户端应用程序)与Web服务器之间的交互,具有较低的开销,便于实现客户端与服务器的实时数据传输。 服务器可以通过标准化的方式来实现,而无需客户端首先请求内容,并允许消息在保持连接打开的同时来回传递。通过这种方式,可以在客户端和服务器之间进行双向持续对话。 通信通过TCP端口80或443完成,这在防火墙阻止非Web网络连接的环境下是有益的。另外,Comet之类的技术以非标准化的方式实现了类似的双向通信。

大多数浏览器都支持该协议,包括Google ChromeFirefoxSafariMicrosoft EdgeInternet ExplorerOpera

与HTTP不同,WebSocket提供全双工通信。此外,WebSocket还可以在TCP之上启用消息流。TCP单独处理字节流,没有固有的消息概念。 在WebSocket之前,使用Comet可以实现全双工通信。但是Comet存在TCP握手和HTTP头的开销,因此对于小消息来说效率很低。WebSocket协议旨在解决这些问题。

WebSocket协议规范将ws(WebSocket)和wss(WebSocket Secure)定义为两个新的统一资源标识符(URI)方案,分别对应明文和加密连接。除了方案名称和片段ID(不支持#)之外,其余的URI组件都被定义为此URI的通用语法。

使用浏览器开发人员工具,开发人员可以检查WebSocket握手以及WebSocket框架

历史

WebSocket最初在HTML5规范中被引用为TCPConnection,作为基于TCP的套接字API的占位符。2008年6月,Michael Carter进行了一系列讨论,最终形成了称为WebSocket的协议。

“WebSocket”这个名字是Ian Hickson和Michael Carter之后在 #whatwg IRC聊天室创造的,随后由Ian Hickson撰写并列入HTML5规范,并在Michael Carter的Cometdaily博客上宣布。 2009年12月,Google Chrome 4是第一个提供标准支持的浏览器,默认情况下启用了WebSocket。协议的开发随后于2010年2月从W3C和WHATWG小组转移到IETF,并在Ian Hickson的指导下进行了两次修订。

该协议被多个浏览器默认支持并启用后,RFC于2011年12月在Ian Fette下完成。

背景

早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会消耗很多的带宽资源。

比较新的轮询技术是Comet。这种技术虽然可以实现双向通信,但仍然需要反复发出请求。而且在Comet中普遍采用的HTTP长连接也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

Websocket使用wswss统一资源标志符,类似于HTTPS。其中wss表示使用了TLS的Websocket。如:

1
2
ws://example.com/wsapi
wss://secure.example.com/wsapi

Websocket与HTTP和HTTPS使用相同的TCP端口,可以绕过大多数防火墙的限制。默认情况下,Websocket协议使用80端口;运行在TLS之上时,默认使用443端口。

优点
  • 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。

  • 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。

  • 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。

  • 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。

  • 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。

  • 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。

握手协议

WebSocket 是独立的、创建在 TCP 上的协议。

Websocket 通过 HTTP/1.1 协议的101状态码进行握手。

为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。

Spring WebSocket 解析

依赖引入

SpringBoot有自带的WebSocket API,我们在使用WebSocket的时候,可以直接引入这个依赖,也可以手动引入:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
创建服务器端点

开启WebSocket服务端的自动注册。

在对WebSocket的使用中,可以先通过Spring创建Java配置文件。在这个文件中,先新建ServerEndpointExporter

1
2
3
4
5
6
7
8
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {

return new ServerEndpointExporter();
}
}

有了这个Bean,就可以使用@ServerEndpoint定义一个端点服务类。在这个站点服务类中,还可以定义WebSocket的打开,关闭,错误和发送消息方法。

ServerEndpointExporter 是由Spring官方提供的标准实现,用于扫描ServerEndpointConfig配置类和@ServerEndpoint注解实例。使用规则也很简单:1.如果使用默认的嵌入式容器 比如Tomcat 则必须手工在上下文提供ServerEndpointExporter。2. 如果使用外部容器部署war包,则不要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描服务端的行为交给外部容器处理。

ServerEndpoint

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**** imports ****/
@ServerEndpoint("/ws")
@Service
public class WebSocketServiceImpl {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServiceImpl对象。
private static CopyOnWriteArraySet<WebSocketServiceImpl>
webSocketSet = new CopyOnWriteArraySet<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); // 加入set中
addOnlineCount(); // 在线数加1
System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
try {
sendMessage("有新的连接加入了!!");
} catch (IOException e) {
System.out.println("IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); // 从set中删除
subOnlineCount(); // 在线数减1
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自客户端的消息:" + message);

// 群发消息
for (WebSocketServiceImpl item : webSocketSet) {
try {
/*
// 获取当前用户名称
String userName = item.getSession()
.getUserPrincipal().getName();
System.out.println(userName);
*/
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}


/**
* 发送消息
* @param message 客户端消息
* @throws IOException
*/
private void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}

// 返回在线数
private static synchronized int getOnlineCount() {
return onlineCount;
}

// 当连接人数增加时
private static synchronized void addOnlineCount() {
WebSocketServiceImpl.onlineCount++;
}

// 当连接人数减少时
private static synchronized void subOnlineCount() {
WebSocketServiceImpl.onlineCount--;
}
}

这里是通过注解@OnOpen、@OnMessage、@OnClose、@OnError 来声明回调函数。

回调函数将由JavaScript处理

回调函数将会被前端的JavaScript所使用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
var websocket = null;
// 判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
// 创建WebSocket对象,连接服务器端点
websocket = new WebSocket("ws://localhost:8888/ws");
} else {
alert('Not support websocket')
}

// 连接发生错误的回调方法
websocket.onerror = function() {
appendMessage("error");
};

// 连接成功建立的回调方法
websocket.onopen = function(event) {
appendMessage("open");
}

// 接收到消息的回调方法
websocket.onmessage = function(event) {
appendMessage(event.data);
}

// 连接关闭的回调方法
websocket.onclose = function() {
appendMessage("close");
}

// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,
// 防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
websocket.close();
}

// 将消息显示在网页上
function appendMessage(message) {
var context = $("#context").html() +"<br/>" + message;
$("#context").html(context);
}

// 关闭连接
function closeWebSocket() {
websocket.close();
}

// 发送消息
function sendMessage() {
var message = $("#message").val();
websocket.send(message);
}

注意看清楚大小写,这里的 webSocket是利用了在后端所声明的WebSocket对象。

1
var webSocket = new WebSocket(url);

而 webSocket这个JS实例将能够调用回调函数,就如onmessage而言:

1
2
3
websocket.onmessage = function(event) {
appendMessage(event.data);
}

能通过 (event),来获取到后端传进来的值。

那么值是从哪里被传进去的呢?

请看HTML页面:

1
2
3
4
5
6
7
8
<body>
测试一下WebSocket站点吧
<br />
<input id="message" type="text" />
<button onclick="sendMessage()">发送消息</button>
<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<div id="context"></div>
</body>

这里所只使用的 onclick=”sendMessage()” 正是在调用JS函数,而这个JS函数就是上面所提及的:

1
2
3
4
5
// 发送消息
function sendMessage() {
var message = $("#message").val();
websocket.send(message);
}

它会将文本框中的message的值,通过 send(message),发送到后端处理,由后端标注的@OnMessage函数处理完后,再被JS函数 websocket.onmessage = function(event) 所接收。

至于如何展示在页面上,则是由:

1
2
3
4
5
// 将消息显示在网页上
function appendMessage(message) {
var context = $("#context").html() +"<br/>" + message;
$("#context").html(context);
}

所决定的了。

当然,根据不同的情况,JS的代码可以被自行设计,所以这些代码仅供参考。我们必须记牢的,就是注解的含义:

事件类型 WebSocket回调函数 事件描述
open webSocket.onopen 当打开连接后触发
message webSocket.onmessage 当客户端接收服务端数据时触发
error webSocket.onerror 当通信异常时触发
close webSocket.onclose 当连接关闭时触发

打造一个在线聊天室

明白了上述的用法之后,我们来做一个实战练习吧,那就是做一个聊天室。这么说可能有点复古,好像聊天室这个称呼,是上个世纪的产物,不过也无所谓,开始一段练习吧。

前面的实验中,最为关键的,想必就是回调函数的构成了,于是,我么就开始先写回调函数吧。不过在这之前,还要介绍一下一个JSON转换工具。

基本配置

先导入一些基本配置吧:

pom
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Webjars版本定位工具-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-core</artifactId>
</dependency>
<!-- Webjars的一个包,有各种资源-->
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>mdui</artifactId>
<version>0.4.0</version>
</dependency>
<!-- wenjars的JQ库-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>

pom依赖前面都讲过,在此不多赘述

application.yml

yml文件不需要太多配置

1
2
3
4
5
6
server:
port: 8888

spring:
thymeleaf:
cache: false
configuration
1
2
3
4
5
6
7
8
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {

return new ServerEndpointExporter();
}
}

这是必要的配置,前面也讲过。

回调函数的编写

前面有引入JSON依赖,我们就使用JSON,来完善我们的信息传输,所以,先编写一个信息类吧。

Message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* WebSocket 聊天消息类
*/
@Data
public class Message {

public static final String ENTER = "ENTER";
public static final String SPEAK = "SPEAK";
public static final String QUIT = "QUIT";

private String type;//消息类型

private String username; //发送人

private String msg; //发送消息

private int onlineCount; //在线用户数

public static String jsonStr(String type, String username, String msg, int onlineTotal) {
return JSON.toJSONString(new Message(type, username, msg, onlineTotal));
}

public Message(String type, String username, String msg, int onlineCount) {
this.type = type;
this.username = username;
this.msg = msg;
this.onlineCount = onlineCount;
}
}

注意,这里的jsonStr方法是一个静态方法,而且,这个静态方法返回的是一个自身类的一个构造方法。也就是说,将使用这个静态方法去代替构造方法。

这里的JSON是一个alibaba提供的类。在pom文件中导入过。

服务端

在服务端完成回调函数的编写,这里用到了Message类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Component
@ServerEndpoint("/chat")//标记此类为服务端
public class WebSocketChatServer {

/**
* 全部在线会话 基于场景考虑 这里使用线程安全的Map存储会话对象。
*/
private static Map<String, Session> onlineSessions = new ConcurrentHashMap<>();


/**
* 当客户端打开连接:1.添加会话对象 2.更新在线人数
*/
@OnOpen
public void onOpen(Session session) {
onlineSessions.put(session.getId(), session);
sendMessageToAll(Message.jsonStr(Message.ENTER, "", "", onlineSessions.size()));
}

/**
* 当客户端发送消息:1.获取它的用户名和消息 2.发送消息给所有人
* <p>
* 这里约定传递的消息为JSON字符串 方便传递更多参数!
*/
@OnMessage
public void onMessage(Session session, String jsonStr) {
Message message = JSON.parseObject(jsonStr, Message.class);
sendMessageToAll(Message.jsonStr(Message.SPEAK, message.getUsername(), message.getMsg(), onlineSessions.size()));
}

/**
* 当关闭连接:1.移除会话对象 2.更新在线人数
*/
@OnClose
public void onClose(Session session) {
onlineSessions.remove(session.getId());
sendMessageToAll(Message.jsonStr(Message.QUIT, "", "下线了!", onlineSessions.size()));
}

/**
* 当通信发生异常:打印错误日志
*/
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}

/**
* 公共方法:发送信息给所有人
*/
private static void sendMessageToAll(String msg) {
onlineSessions.forEach((id, session) -> {
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
});
}

}

这里,我们逐个逐个去分析各个函数的使用:

  1. public void onOpen(Session session):每当有客户端连接到这个地址时,就会自动往这个函数传入Session,而我们使用HashMap去存储这个Session实例,当然,使用HashSet也可以,在存储了对象的同时,也需要更新实时在线的信息。
  2. public void onMessage(Session session, String jsonStr):在前端的JS使用了Send(),发送了信息后,onMessage函数就会自动接收到了一个文本框的String字符串和一个当前Session对象。这里直接把String字符串转化了Message类,再将这个Message转化为JSON的形式,再发送至前端。可能大家不太明白的,就是为什么要加ENTER和SPEAK,这是为了让前端能够判断这个消息到底是什么类型的,以便用于不同的地方。
  3. public void onClose(Session session):自然是在退出的时候,更新在线人数
  4. public void onError(Session session, Throwable error):错误处理
  5. private static void sendMessageToAll(String msg):可以看到这是一个静态方法,也是信息传输的主体,这里使用了只有JDK8才有的Lambda表达式和foreach,去将这个Map里面的Session对象,由这个Session对象去发送信息到前端。

sendMessageToAll函数也有三点是需要注意的:

  • Lambda表达式的用法
  • Session是WebSocket自己的Session类
  • session.getBasicRemote().sendText(msg);可以是一个固定表达,不用深究。

登录的编写

既然是聊天,当然得知道你是谁才能正常会话,于是也需要登录界面,但是这里就不引入Security了,太麻烦,这里直接使用HTML和JS完成登录页面

login页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<head>
<title>登陆聊天</title>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<style>省略。。。</style>
</head>
<body>
<div class="logo_box">
<h3>登录聊天</h3>
<form>
<div class="input_outer">
<span class="u_user"></span>
<input id="username" name="username" class="text" type="text" placeholder="任意中文名">
</div>
<div class="input_outer">
<span class="us_uer"></span>
<input id="password" name="password" class="text" type="password" placeholder="任意密码">
</div>
<div class="mb2">
<a class="act-but submit" onclick="login()">登录</a>
</div>
</form>
</div>
......

这里引入webjar的JQ库,可以使用一些非常简便的函数和类。

login-JavaScript

这一段的JS较为容易

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>

/**
* 登陆聊天
*/
function login() {
location.href = '/index?username='+$('#username').val();
}

/**
* 使用ENTER登陆
*/
document.onkeydown = function (event) {
var e = event || window.event || arguments.callee.caller.arguments[0];
e.keyCode === 13 && login();
};
</script>

和HT页面相结合,完成用户名的输入。

控制层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@RestController
public class WebController {
/**
* 登陆界面
*/
@GetMapping("/")
public ModelAndView login() {
return new ModelAndView("/login");
}

/**
* 聊天界面
*/
@GetMapping("/index")
public ModelAndView index(String username, String password, HttpServletRequest request) throws UnknownHostException {
if (StringUtils.isEmpty(username)) {
username = "匿名用户";
}
ModelAndView mav = new ModelAndView("/chat");
mav.addObject("username", username);
mav.addObject("webSocketUrl", "ws://"+ InetAddress.getLocalHost().getHostAddress()+":"+request.getServerPort()+request.getContextPath()+"/chat");
return mav;
}

}

JS的登录函数,将会被传入到这里,完成页面的跳转,较为简单。

聊天室的编写

chat页面

我们也来编写聊天室的页面吧,这个页面开始引用webjar的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>WebSocket简单聊天室</title>
<meta charset="utf-8" name="viewport" content="width=device-width">
<link rel="stylesheet" th:href="@{/webjars/mdui/dist/css/mdui.css}">
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/webjars/mdui/dist/js/mdui.js}"></script>
</head>
<body class="mdui-theme-primary-indigo mdui-theme-accent-pink">

<div class="mdui-container">
<div class="mdui-toolbar mdui-color-theme">
<a class="mdui-btn mdui-btn-icon"><i class="mdui-icon material-icons">menu</i></a>
<span class="mdui-typo-title">简单聊天室</span>
<div class="mdui-toolbar-spacer"></div>
<a class="mdui-btn mdui-btn-icon" href="https://www.baidu.com/" target="_blank"><i
class="mdui-icon material-icons">search</i></a>
<a class="mdui-btn mdui-btn-icon" th:href="@{/}"><i
class="mdui-icon material-icons">exit_to_app</i></a>
<a class="mdui-btn mdui-btn-icon"><i class="mdui-icon material-icons">more_vert</i></a>
</div>
</div>

<div>
<div class="mdui-container container_text">

<div class="mdui-row">
<div class="mdui-col-xs-12 mdui-col-sm-6">
<div class="mdui-col-xs-12 mdui-col-sm-10">
<div class="mdui-textfield-floating-label" style="margin-top:15px">
<i class="mdui-icon material-icons">欢迎:</i>
<i class="mdui-icon" id="username" th:text="${username}"></i>
</div>
</div>
<div class="mdui-col-xs-12 mdui-col-sm-10">
<div class="mdui-textfield mdui-textfield-floating-label">
<i class="mdui-icon material-icons">textsms</i>
<label class="mdui-textfield-label">发送内容</label>
<input class="mdui-textfield-input" id="msg"/>
</div>
<div class="mdui-container" style="padding:20px 35px">
<button class="mdui-btn mdui-color-theme-accent mdui-ripple"
onclick="sendMsgToServer()">发送 (enter)
</button>
<button class="mdui-btn mdui-color-theme mdui-ripple"
onclick="clearMsg()">清屏
</button>
</div>
</div>
</div>

<div class="mdui-col-xs-6 mdui-col-sm-5" style="padding:10px 0">
<div class="mdui-chip">
<span class="mdui-chip-icon mdui-color-blue">
<i class="mdui-icon material-icons">&#xe420;</i></span>
<span class="mdui-chip-title">聊天内容</span>
</div>

<div class="mdui-chip">
<span class="mdui-chip-icon mdui-color-blue">
<i class="mdui-icon material-icons">face</i></span>
<span class="mdui-chip-title">在线人数</span>
<span class="mdui-chip-title chat-num">0</span>
</div>
<div class="message-container">

</div>
</div>

</div>
</div>
</div>

Html页面的编写,比较繁琐,但是没有什么特别复杂的问题,这里在开头引入了 webjar,同时也引入了各种各样的CSS,此时的我们只需要去编写文字和放置class就可以了,省去了CSS文件的编写过程。

chat-JavaScript

接下来到了最为重要的JS的编写了,我们来一步步完成吧。

首先是要完成发送按钮:

1
2
3
4
5
6
7
8
9
10
11
12
13
var webSocket = new WebSocket(/*[[${webSocketUrl}]]*/ 'ws://localhost:8888/chat');

/**
* 通过WebSocket对象发送消息给服务端
*/
function sendMsgToServer() {
var $message = $('#msg');
if ($message.val()) {
webSocket.send(JSON.stringify({username: $('#username').text(), msg: $message.val()}));
$message.val(null);
}

}

首先从获得了id为msg的文本框,将其变成一个值,如果这个值不为空,则将其发送到后端,进行处理。

为了有更好的可视性,我们将WebSocket由函数获取,改为:

1
var webSocket = getWebSocket();

然后在这个getWebSocket函数中完成编写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getWebSocket() {

var webSocket = new WebSocket(/*[[${webSocketUrl}]]*/ 'ws://localhost:8888/chat');

webSocket.onmessage = function (event) {
console.log('WebSocket收到消息:%c' + event.data, 'color:green');
//获取服务端消息
var message = JSON.parse(event.data) || {};
var $messageContainer = $('.message-container');
//
if (message.type === 'SPEAK') {
$messageContainer.append(
'<div class="mdui-card" style="margin: 10px 0;">' +
'<div class="mdui-card-primary">' +
'<div class="mdui-card-content message-content">' + message.username + ":" + message.msg + '</div>' +
'</div></div>');
}
$('.chat-num').text(message.onlineCount);
});
}
};

/*
省略onopen等等。。。。。
*/
return webSocket;
}

它看起来复杂,其实也就三步走:

  1. 首先,先通过var message = JSON.parse(event.data),将传入的数据变为JSON类型。
  2. 然后,再获取到:$(‘.message-container’)这个class标签。
  3. 最后,若这个标签不为空,则使用append(),将消息类型和样式加入到其中。

这样就完成了,简单明了。最后测试一下吧:

测试

打开浏览器,输入localhost:8888

并输入任意用户名,点击登录:

再另外打开一个网页,登录:

互相发送消息试试:happy:

源码地址:https://github.com/Antarctica000/SpringBoot/tree/master/websocket