前几天写了一篇《SpringBoot快速入门》一文,然后周末趁着有时间,在这个Springboot框架基础上整合了WebSocket技术写了一个网页版聊天功能。

如果小伙伴找不到那套框架了,可以看下之前的文章找到Springboot快速入门一文

往期推荐

Springboot 完整搭建快速入门,必看!

通过该文章可以了解服务端与客户端之间的通信机制,以及了解相关的Http协议等技术内容。

话不多说,先来看看运行的过程:

页面写的十分简单,后续也会陆续将其优化和完善。

正文

一、HTTP相关知识

HTTP协议

http是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当

        http 为短连接:客户端发送请求都需要服务器端回送响应。请求结束后,主动释放链接,因此为短连接。通常的做法是,不需要任何数据,也要保持每隔一段时间向服务器发送"保持连接"的请求。这样可以保证客户端在服务器端是"上线"状态。

HTTP连接使用的是"请求-响应"方式,不仅在请求时建立连接,而且客户端向服务器端请求后,服务器才返回数据。

二、Socket相关知识

1. 要想明白 Socket,必须要理解 TCP 连接。

① TCP 三次握手:握手过程中并不传输数据,在握手后服务器与客户端才开始传输数据,理想状态下,TCP 连接一旦建立,在通讯双方中的任何一方主动断开连接之前 TCP 连接会一直保持下去。

② Socket 是对 TCP/IP 协议的封装,Socket 只是个接口不是协议,通过 Socket 我们才能使用 TCP/IP 协议,除了 TCP,也可以使用 UDP 协议来传递数据。

③ 创建 Socket 连接的时候,可以指定传输层协议,可以是 TCP 或者 UDP,当用 TCP 连接,该Socket就是个TCP连接,反之。

2. Socket 原理

Socket 连接,至少需要一对套接字,分为 clientSocket,serverSocket 连接分为3个步骤:

(1) 服务器监听:服务器并不定位具体客户端的套接字,而是时刻处于监听状态;

(2) 客户端请求:客户端的套接字要描述它要连接的服务器的套接字,提供地址和端口号,然后向服务器套接字提出连接请求;

(3) 连接确认:当服务器套接字收到客户端套接字发来的请求后,就响应客户端套接字的请求,并建立一个新的线程,把服务器端的套接字的描述发给客户端。一旦客户端确认了此描述,就正式建立连接。而服务器套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

Socket为长连接:通常情况下Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通讯双方开始互发数据内容,直到双方断开连接。在实际应用中,由于网络节点过多,在传输过程中,会被节点断开连接,因此要通过轮询高速网络,该节点处于活跃状态。

很多情况下,都是需要服务器端向客户端主动推送数据,保持客户端与服务端的实时同步。

若双方是 Socket 连接,可以由服务器直接向客户端发送数据。

若双方是 HTTP 连接,则服务器需要等客户端发送请求后,才能将数据回传给客户端。

因此,客户端定时向服务器端发送请求,不仅可以保持在线,同时也询问服务器是否有新数据,如果有就将数据传给客户端。

要弄明白 http 和 socket 首先要熟悉网络七层:物 数 网 传 会 表 应,如图:

如图

HTTP 协议:超文本传输协议,对应于应用层,用于如何封装数据。

TCP/UDP 协议:传输控制协议,对应于传输层,主要解决数据在网络中的传输。

IP 协议:对应于网络层,同样解决数据在网络中的传输。

传输数据的时候只使用 TCP/IP 协议(传输层),如果没有应用层来识别数据内容,传输后的协议都是无用的。

应用层协议很多 FTP,HTTP,TELNET等,可以自己定义应用层协议。

web 使用 HTTP 作传输层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议,将数据发送到网络上。

三、WebSocket相关知识

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

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

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

四、实现源码:

1 聊天页面chat.html

前端采用bootstrap,引入了: jquery-3.3.1.min.js、bootstrap.min.css。小伙伴可自行选择:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.springframework.org/schema/mvc">
<head>
<meta charset="UTF-8">
<title>chat room websocket</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<script th:src="@{/js/jquery-3.3.1.min.js}"></script>
</head>
<body class="container" style="width: 60%">
<div class="form-group" style="width: 100%; margin-top: 10px;">
<div style="width: 100%; background-color: #800080; color: #ffffff;">
<label for="user_name" style="float: left; margin-left: 45%">你好:</label>
<h5 id="user_name" th:text="${username}" style="width: 80%;"></h5>
</div>
</div>
<div class="form-group" style="float: left; width: 100%;">
<label for="user_list" style="float: left;">选择聊天用户:</label>
<select id="user_list" style="width: 15%;"></select>
<span id="error_select_msg" style="color: red;"></span>
</div>
<div class="form-group" style="float: left; width: 100%;">
<div id="message_user" style="width: 25%; height: 450px; overflow-y: auto; position: relative; float: left;" class="form-control" readonly="readonly">
群成员:<span id="message_user_count"></span><br/>
</div>
<div id="message_chat" style="font-size: 13px; width: 75%; height: 300px; overflow-y: auto; position: relative; float: left;" class="form-control" readonly="readonly">
</div>
<div style="width: 75%; float: right;">
<div style="width: 100%; height: 110px;">
<textarea style="height: 100%; border-bottom: #ffffff solid 0px;" id="chat_msg" value="" class="form-control"></textarea>
</div>
<div style="width: 100%; float: right; border-bottom: #808080 solid 1px;">
<button style="float: right;" id="send" class="btn btn-info">发送消息</button>
<button style="float: right;" id="send_all" class="btn btn-info">群发消息</button>
<button style="float: right;" id="user_exit" class="btn btn-warning">退出</button>
</div>
</div>
</div>
</body>
<script type="text/javascript">
$(document).ready(function() {
initUserList();
let urlPrefix = 'ws://localhost:8080/net/websocket/';
let ws = null;
let username = $('#user_name').text();
ws = initMsg(urlPrefix, username);
// 客户端发送对某一个客户的消息到服务器
$('#send').click(function() {
let userList = $("#user_list option:selected").val();
if (!userList) {
$("#error_select_msg").html("请选择一个用户!");
return;
}
let msg = $('#chat_msg').val();
if (!msg) {
alert("请输入聊天内容!");
return;
}
msg = msg + "[" + userList + "]" + "----------" + username;
if (ws) {
ws.send(msg);
//服务端发送的消息
$('#message_chat').append('<div style="width: 100%; float: right;"><span style="float: right;">' + username + '&nbsp;&nbsp;</span><br/>');
$('#message_chat').append('<span style="float: right; font-size: 18px; font-weight: bolder;">' + msg.substring(0, msg.indexOf('[')) + '</span></div>');
$("#chat_msg").val('');
$("#error_select_msg").empty();
}
});
// 客户端群发消息到服务器
$('#send_all').click(function() {
let msg = $('#chat_msg').val();
if (!msg) {
alert("请输入聊天内容!");
return;
}
msg = msg + "[allUsers]" + "----------" + username;
if (ws) {
ws.send(msg);
//服务端发送的消息
$('#message_chat').append('<div style="width: 100%; float: right;"><span style="float: right;">' + username + ' 的群发消息&nbsp;&nbsp;</span><br/>');
$('#message_chat').append('<span style="float: right; font-size: 18px; font-weight: bolder;">' + msg.replace('[allUsers]----------' + username, '') + '</span></div>');
$("#chat_msg").val('');
$("#error_select_msg").empty();
}
});
// 退出聊天室
$('#user_exit').click(function() {
if (ws) {
ws.close();
}
window.location.href = "/chat/login";
});
// 用户下拉列表点击事件
$("#user_list").on("change", function() {
$("#error_select_msg").empty();
});
}); /**
* 初始化用户列表
*/
function initUserList() {
let username = $('#user_name').text();
$.ajax({
url: "/getUserList",
type: "POST",
data: {username: username},
success: function(data) {
let result = JSON.parse(data);
let html = "<option value=''>---请选择---</option>";
for (let i = 0; i < result.length; i++) {
html += "<option value='" + result[i].username + "'>" + result[i].username + "</option>";
}
let userList = "";
for (let i = 0; i < result.length; i++) {
userList += "<div class='select_user'>" + result[i].username + "</div>";
}
$("#user_list").html(html);
$("#message_user_count").text(result.length + "人");
$("#message_user").append(userList);
}
});
} /**
* 初始化消息
*
* @param urlPrefix
* @param username
* @returns {WebSocket}
*/
function initMsg(urlPrefix, username) {
let url = urlPrefix + username;
ws = new WebSocket(url);
ws.onopen = function () {
console.log("建立 websocket 连接...");
};
ws.onmessage = function(event) {
//服务端发送的消息
$('#message_chat').append(event.data + '\n');
};
ws.onclose = function() {
$('#message_chat').append('<div style="width: 100%; float: left;">用户[' + username + '] 已经离开聊天室!' + '</div>');
console.log("用户:[" + username + "]已关闭 websocket 连接...");
}
return ws;
}
</script>
</html>

2 pom.xml加入WebSocket依赖

<!-- 集成webSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 集成json -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.3</version>
</dependency>

3 实现WebSocket服务端

① 创建SocketEndPoint.java核心聊天页面实现类

该类为WebSocket的核心实现类,主要实现聊天连接、消息发送、退出聊天、异常处理等页面聊天的核心功能。其中:

@PathParam这个注解是将请求路径中绑定的占位符的值给取出来,作为参数条件使用。是javax.websocket.server下的一个注解。

在项目中,通过name对socket连接进行访问控制,后台后续会将name作为唯一主键,小伙伴也可以通过在url里面增加ket + name的方式进行访问控制,key作为登陆之后,服务器给用户的令牌,通过令牌和name进行权限校验(这里目前没有实现,只保证name是唯一)。

SocketEndPoint.java类实现

package cn.cansluck.utils.net;

import cn.cansluck.service.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.util.Map; import static cn.cansluck.utils.net.SocketPool.*;
import static cn.cansluck.utils.net.SocketHandler.createKey; // 注入容器
@Component
// 表明这是一个websocket服务的端点
@ServerEndpoint("/net/websocket/{name}")
public class SocketEndPoint { private static final Logger log = LoggerFactory.getLogger(SocketEndPoint.class); private static IUserService userService; @Autowired
public void setUserService(IUserService userService){
SocketEndPoint.userService = userService;
} @OnOpen
public void onOpen(@PathParam("name") String name, Session session) {
log.info("有新的连接:{}", session);
add(createKey(name), session);
for (Map.Entry<String, Session> item : sessionMap().entrySet()) {
if (item.getKey().equals(name)) {
SocketHandler.sendMessageAll("<div style='width: 100%; float: left;'>用户【" + name + "】已上线</div>", name);
}
}
log.info("在线人数:{}",count());
sessionMap().keySet().forEach(item -> log.info("在线用户:" + item));
for (Map.Entry<String, Session> item : sessionMap().entrySet()) {
log.info("12: {}", item.getKey());
}
} @OnMessage
public void onMessage(String message) {
if (message.contains("[allUsers]")) {
String userInfo = message.substring(message.indexOf("[allUsers]")).replace("[allUsers]----------", "");
SocketHandler.sendMessageAll( "<div style='width: 100%; float: left;'>&nbsp;&nbsp;" + userInfo + "群发消息</div><div style='width: 100%; font-size: 18px; font-weight: bolder; float: right;'>" + message.substring(0, message.indexOf("[")) + "</div>", userInfo);
} else {
String acceptUser = message.substring(message.indexOf("[") + 1, message.lastIndexOf("]"));
String sendUser = message.substring(message.lastIndexOf("-") + 1, message.length());
Session userSession;
for (Map.Entry<String, Session> item : sessionMap().entrySet()) {
if (item.getKey().equals(acceptUser)) {
userSession = item.getValue();
String userInfo = message.substring(0, message.indexOf("["));
SocketHandler.sendMessage(userSession, "<div style='width: 100%; float: left;'>&nbsp;&nbsp;" + sendUser + "</div><div style='width: 100%; font-size: 18px; font-weight: bolder; float: right;'>" + userInfo + "</div>");
}
}
}
log.info("有新消息: {}", message);
} @OnClose
public void onClose(@PathParam("name") String name,Session session) {
log.info("连接关闭: {}", session);
remove(createKey(name));
log.info("在线人数:{}", count());
sessionMap().keySet().forEach(item -> log.info("在线用户:" + item));
for (Map.Entry<String, Session> item : sessionMap().entrySet()){
log.info("12: {}", item.getKey());
}
Date date = new Date();
DateFormat df = DateFormat.getDateTimeInstance();//可以精确到时分秒
SocketHandler.sendMessageAll("<div style='width: 100%; float: left;'>[" + df.format(date) + "] " + name + "已离开聊天室</div>", name);
} @OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
log.error("退出发生异常: {}", e.getMessage());
}
log.info("连接出现异常: {}", throwable.getMessage());
}
}

② 创建SocketPool.java在线连接池类

package cn.cansluck.utils.net;

import javax.websocket.Session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; /**
* WebSocket连接池类
*
* @author Cansluck
*/
public class SocketPool { // 在线用户websocket连接池
private static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>(); /**
* 新增一则连接
* @param key 设置主键
* @param session 设置session
*/
public static void add(String key, Session session) {
if (!key.isEmpty() && session != null){
ONLINE_USER_SESSIONS.put(key, session);
}
} /**
* 根据Key删除连接
* @param key 主键
*/
public static void remove(String key) {
if (!key.isEmpty()){
ONLINE_USER_SESSIONS.remove(key);
}
} /**
* 获取在线人数
* @return 返回在线人数
*/
public static int count(){
return ONLINE_USER_SESSIONS.size();
} /**
* 获取在线session池
* @return 获取session池
*/
public static Map<String, Session> sessionMap(){
return ONLINE_USER_SESSIONS;
}
}

③ 创建SocketHandler.java动作处理工具类

package cn.cansluck.utils.net;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException; import static cn.cansluck.utils.net.SocketPool.sessionMap; /**
* WebSocket动作类
*
* @author Cansluck
*/
public class SocketHandler { private static final Logger log = LoggerFactory.getLogger(SocketHandler.class); /**
* 根据key和用户名生成一个key值,简单实现下
* @param name 发送人
* @return 返回值
*/
public static String createKey(String name){
return name;
} /**
* 给指定用户发送信息
* @param session session
* @param msg 发送的消息
*/
public static void sendMessage(Session session, String msg) {
if (session == null)
return;
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null)
return;
try {
basic.sendText(msg);
} catch (IOException e) {
log.error("消息发送异常,异常情况: {}", e.getMessage());
}
} /**
* 给所有的在线用户发送消息
* @param message 发送的消息
* @param username 发送人
*/
public static void sendMessageAll(String message, String username) {
log.info("广播:群发消息");
// 遍历map,只输出给其他客户端,不给自己重复输出
sessionMap().forEach((key, session) -> {
if (!username.equals(key)) {
sendMessage(session, message);
}
});
}
}

④ 创建ChatController.java页面访问控制器类

package cn.cansluck.controller;

import cn.cansluck.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping; /**
* 登录页
*
* @author Cansluck
*/
@RequestMapping("/chat")
@Controller
public class ChatController { @Autowired
private IUserService userService; /**
* 登陆
*
* @author Cansluck
* @return 返回页面
*/
@RequestMapping("/login")
public String login(String username, String password, ModelMap map) {
if (null == username || "".equals(username))
return "login";
boolean isLogin = userService.login(username, password);
if (isLogin) {
map.addAttribute("username", username);
return "chat";
}
return "login";
}
}

⑤ 创建SocketConfig.java的websocket配置类

package cn.cansluck.utils;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter; /**
* WebSocket配置类
*
* @author Cansluck
*/
@Configuration
@EnableWebSocket
public class SocketConfig { @Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}

以上就是一个WebSocket的简单实现,更多的场景小伙伴可以自行在这个基础上实现更多功能。后续会继续完善该聊天的功能,代码将会上传到GitHub上供下载。有兴趣的小伙伴可以一起来创作玩一下呀~后续还会将项目打包部署到我个人的腾讯云服务器上,有兴趣的可以一起来聊天呀~

GitHub项目下载地址

https://github.com/125207780/springboot-project.git

小伙伴们可以自行下载并操作,可以一起修改一起玩呀~

更多精彩敬请关注公众号

Java极客思维

微信扫一扫,关注公众号

前几天写了一篇《SpringBoot快速入门》一文,然后周末趁着有时间,在这个Springboot框架基础上整合了WebSocket技术写了一个网页版聊天功能。

如果小伙伴找不到那套框架了,可以看下之前的文章找到Springboot快速入门一文

往期推荐

Springboot 完整搭建快速入门,必看!

通过该文章可以了解服务端与客户端之间的通信机制,以及了解相关的Http协议等技术内容。

话不多说,先来看看运行的过程:

页面写的十分简单,后续也会陆续将其优化和完善。

正文

一、HTTP相关知识

HTTP协议

http是一个简单的请求-响应协议,它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出;而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣,因为它使开发和部署非常地直截了当

        http 为短连接:客户端发送请求都需要服务器端回送响应。请求结束后,主动释放链接,因此为短连接。通常的做法是,不需要任何数据,也要保持每隔一段时间向服务器发送"保持连接"的请求。这样可以保证客户端在服务器端是"上线"状态。

HTTP连接使用的是"请求-响应"方式,不仅在请求时建立连接,而且客户端向服务器端请求后,服务器才返回数据。

二、Socket相关知识

1. 要想明白 Socket,必须要理解 TCP 连接。

① TCP 三次握手:握手过程中并不传输数据,在握手后服务器与客户端才开始传输数据,理想状态下,TCP 连接一旦建立,在通讯双方中的任何一方主动断开连接之前 TCP 连接会一直保持下去。

② Socket 是对 TCP/IP 协议的封装,Socket 只是个接口不是协议,通过 Socket 我们才能使用 TCP/IP 协议,除了 TCP,也可以使用 UDP 协议来传递数据。

③ 创建 Socket 连接的时候,可以指定传输层协议,可以是 TCP 或者 UDP,当用 TCP 连接,该Socket就是个TCP连接,反之。

2. Socket 原理

Socket 连接,至少需要一对套接字,分为 clientSocket,serverSocket 连接分为3个步骤:

(1) 服务器监听:服务器并不定位具体客户端的套接字,而是时刻处于监听状态;

(2) 客户端请求:客户端的套接字要描述它要连接的服务器的套接字,提供地址和端口号,然后向服务器套接字提出连接请求;

(3) 连接确认:当服务器套接字收到客户端套接字发来的请求后,就响应客户端套接字的请求,并建立一个新的线程,把服务器端的套接字的描述发给客户端。一旦客户端确认了此描述,就正式建立连接。而服务器套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

Socket为长连接:通常情况下Socket 连接就是 TCP 连接,因此 Socket 连接一旦建立,通讯双方开始互发数据内容,直到双方断开连接。在实际应用中,由于网络节点过多,在传输过程中,会被节点断开连接,因此要通过轮询高速网络,该节点处于活跃状态。

很多情况下,都是需要服务器端向客户端主动推送数据,保持客户端与服务端的实时同步。

若双方是 Socket 连接,可以由服务器直接向客户端发送数据。

若双方是 HTTP 连接,则服务器需要等客户端发送请求后,才能将数据回传给客户端。

因此,客户端定时向服务器端发送请求,不仅可以保持在线,同时也询问服务器是否有新数据,如果有就将数据传给客户端。

要弄明白 http 和 socket 首先要熟悉网络七层:物 数 网 传 会 表 应,如图:

如图

HTTP 协议:超文本传输协议,对应于应用层,用于如何封装数据。

TCP/UDP 协议:传输控制协议,对应于传输层,主要解决数据在网络中的传输。

IP 协议:对应于网络层,同样解决数据在网络中的传输。

传输数据的时候只使用 TCP/IP 协议(传输层),如果没有应用层来识别数据内容,传输后的协议都是无用的。

应用层协议很多 FTP,HTTP,TELNET等,可以自己定义应用层协议。

web 使用 HTTP 作传输层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议,将数据发送到网络上。

三、WebSocket相关知识

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

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

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

四、实现源码:

1 聊天页面chat.html

前端采用bootstrap,引入了: jquery-3.3.1.min.js、bootstrap.min.css。小伙伴可自行选择:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.springframework.org/schema/mvc">
<head>
<meta charset="UTF-8">
<title>chat room websocket</title>
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
<script th:src="@{/js/jquery-3.3.1.min.js}"></script>
</head>
<body class="container" style="width: 60%">
<div class="form-group" style="width: 100%; margin-top: 10px;">
<div style="width: 100%; background-color: #800080; color: #ffffff;">
<label for="user_name" style="float: left; margin-left: 45%">你好:</label>
<h5 id="user_name" th:text="${username}" style="width: 80%;"></h5>
</div>
</div>
<div class="form-group" style="float: left; width: 100%;">
<label for="user_list" style="float: left;">选择聊天用户:</label>
<select id="user_list" style="width: 15%;"></select>
<span id="error_select_msg" style="color: red;"></span>
</div>
<div class="form-group" style="float: left; width: 100%;">
<div id="message_user" style="width: 25%; height: 450px; overflow-y: auto; position: relative; float: left;" class="form-control" readonly="readonly">
群成员:<span id="message_user_count"></span><br/>
</div>
<div id="message_chat" style="font-size: 13px; width: 75%; height: 300px; overflow-y: auto; position: relative; float: left;" class="form-control" readonly="readonly">
</div>
<div style="width: 75%; float: right;">
<div style="width: 100%; height: 110px;">
<textarea style="height: 100%; border-bottom: #ffffff solid 0px;" id="chat_msg" value="" class="form-control"></textarea>
</div>
<div style="width: 100%; float: right; border-bottom: #808080 solid 1px;">
<button style="float: right;" id="send" class="btn btn-info">发送消息</button>
<button style="float: right;" id="send_all" class="btn btn-info">群发消息</button>
<button style="float: right;" id="user_exit" class="btn btn-warning">退出</button>
</div>
</div>
</div>
</body>
<script type="text/javascript">
$(document).ready(function() {
initUserList();
let urlPrefix = 'ws://localhost:8080/net/websocket/';
let ws = null;
let username = $('#user_name').text();
ws = initMsg(urlPrefix, username);
// 客户端发送对某一个客户的消息到服务器
$('#send').click(function() {
let userList = $("#user_list option:selected").val();
if (!userList) {
$("#error_select_msg").html("请选择一个用户!");
return;
}
let msg = $('#chat_msg').val();
if (!msg) {
alert("请输入聊天内容!");
return;
}
msg = msg + "[" + userList + "]" + "----------" + username;
if (ws) {
ws.send(msg);
//服务端发送的消息
$('#message_chat').append('<div style="width: 100%; float: right;"><span style="float: right;">' + username + '&nbsp;&nbsp;</span><br/>');
$('#message_chat').append('<span style="float: right; font-size: 18px; font-weight: bolder;">' + msg.substring(0, msg.indexOf('[')) + '</span></div>');
$("#chat_msg").val('');
$("#error_select_msg").empty();
}
});
// 客户端群发消息到服务器
$('#send_all').click(function() {
let msg = $('#chat_msg').val();
if (!msg) {
alert("请输入聊天内容!");
return;
}
msg = msg + "[allUsers]" + "----------" + username;
if (ws) {
ws.send(msg);
//服务端发送的消息
$('#message_chat').append('<div style="width: 100%; float: right;"><span style="float: right;">' + username + ' 的群发消息&nbsp;&nbsp;</span><br/>');
$('#message_chat').append('<span style="float: right; font-size: 18px; font-weight: bolder;">' + msg.replace('[allUsers]----------' + username, '') + '</span></div>');
$("#chat_msg").val('');
$("#error_select_msg").empty();
}
});
// 退出聊天室
$('#user_exit').click(function() {
if (ws) {
ws.close();
}
window.location.href = "/chat/login";
});
// 用户下拉列表点击事件
$("#user_list").on("change", function() {
$("#error_select_msg").empty();
});
}); /**
* 初始化用户列表
*/
function initUserList() {
let username = $('#user_name').text();
$.ajax({
url: "/getUserList",
type: "POST",
data: {username: username},
success: function(data) {
let result = JSON.parse(data);
let html = "<option value=''>---请选择---</option>";
for (let i = 0; i < result.length; i++) {
html += "<option value='" + result[i].username + "'>" + result[i].username + "</option>";
}
let userList = "";
for (let i = 0; i < result.length; i++) {
userList += "<div class='select_user'>" + result[i].username + "</div>";
}
$("#user_list").html(html);
$("#message_user_count").text(result.length + "人");
$("#message_user").append(userList);
}
});
} /**
* 初始化消息
*
* @param urlPrefix
* @param username
* @returns {WebSocket}
*/
function initMsg(urlPrefix, username) {
let url = urlPrefix + username;
ws = new WebSocket(url);
ws.onopen = function () {
console.log("建立 websocket 连接...");
};
ws.onmessage = function(event) {
//服务端发送的消息
$('#message_chat').append(event.data + '\n');
};
ws.onclose = function() {
$('#message_chat').append('<div style="width: 100%; float: left;">用户[' + username + '] 已经离开聊天室!' + '</div>');
console.log("用户:[" + username + "]已关闭 websocket 连接...");
}
return ws;
}
</script>
</html>

2 pom.xml加入WebSocket依赖

<!-- 集成webSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 集成json -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.3</version>
</dependency>

3 实现WebSocket服务端

① 创建SocketEndPoint.java核心聊天页面实现类

该类为WebSocket的核心实现类,主要实现聊天连接、消息发送、退出聊天、异常处理等页面聊天的核心功能。其中:

@PathParam这个注解是将请求路径中绑定的占位符的值给取出来,作为参数条件使用。是javax.websocket.server下的一个注解。

在项目中,通过name对socket连接进行访问控制,后台后续会将name作为唯一主键,小伙伴也可以通过在url里面增加ket + name的方式进行访问控制,key作为登陆之后,服务器给用户的令牌,通过令牌和name进行权限校验(这里目前没有实现,只保证name是唯一)。

SocketEndPoint.java类实现

package cn.cansluck.utils.net;

import cn.cansluck.service.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Date;
import java.util.Map; import static cn.cansluck.utils.net.SocketPool.*;
import static cn.cansluck.utils.net.SocketHandler.createKey; // 注入容器
@Component
// 表明这是一个websocket服务的端点
@ServerEndpoint("/net/websocket/{name}")
public class SocketEndPoint { private static final Logger log = LoggerFactory.getLogger(SocketEndPoint.class); private static IUserService userService; @Autowired
public void setUserService(IUserService userService){
SocketEndPoint.userService = userService;
} @OnOpen
public void onOpen(@PathParam("name") String name, Session session) {
log.info("有新的连接:{}", session);
add(createKey(name), session);
for (Map.Entry<String, Session> item : sessionMap().entrySet()) {
if (item.getKey().equals(name)) {
SocketHandler.sendMessageAll("<div style='width: 100%; float: left;'>用户【" + name + "】已上线</div>", name);
}
}
log.info("在线人数:{}",count());
sessionMap().keySet().forEach(item -> log.info("在线用户:" + item));
for (Map.Entry<String, Session> item : sessionMap().entrySet()) {
log.info("12: {}", item.getKey());
}
} @OnMessage
public void onMessage(String message) {
if (message.contains("[allUsers]")) {
String userInfo = message.substring(message.indexOf("[allUsers]")).replace("[allUsers]----------", "");
SocketHandler.sendMessageAll( "<div style='width: 100%; float: left;'>&nbsp;&nbsp;" + userInfo + "群发消息</div><div style='width: 100%; font-size: 18px; font-weight: bolder; float: right;'>" + message.substring(0, message.indexOf("[")) + "</div>", userInfo);
} else {
String acceptUser = message.substring(message.indexOf("[") + 1, message.lastIndexOf("]"));
String sendUser = message.substring(message.lastIndexOf("-") + 1, message.length());
Session userSession;
for (Map.Entry<String, Session> item : sessionMap().entrySet()) {
if (item.getKey().equals(acceptUser)) {
userSession = item.getValue();
String userInfo = message.substring(0, message.indexOf("["));
SocketHandler.sendMessage(userSession, "<div style='width: 100%; float: left;'>&nbsp;&nbsp;" + sendUser + "</div><div style='width: 100%; font-size: 18px; font-weight: bolder; float: right;'>" + userInfo + "</div>");
}
}
}
log.info("有新消息: {}", message);
} @OnClose
public void onClose(@PathParam("name") String name,Session session) {
log.info("连接关闭: {}", session);
remove(createKey(name));
log.info("在线人数:{}", count());
sessionMap().keySet().forEach(item -> log.info("在线用户:" + item));
for (Map.Entry<String, Session> item : sessionMap().entrySet()){
log.info("12: {}", item.getKey());
}
Date date = new Date();
DateFormat df = DateFormat.getDateTimeInstance();//可以精确到时分秒
SocketHandler.sendMessageAll("<div style='width: 100%; float: left;'>[" + df.format(date) + "] " + name + "已离开聊天室</div>", name);
} @OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
log.error("退出发生异常: {}", e.getMessage());
}
log.info("连接出现异常: {}", throwable.getMessage());
}
}

② 创建SocketPool.java在线连接池类

package cn.cansluck.utils.net;

import javax.websocket.Session;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; /**
* WebSocket连接池类
*
* @author Cansluck
*/
public class SocketPool { // 在线用户websocket连接池
private static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>(); /**
* 新增一则连接
* @param key 设置主键
* @param session 设置session
*/
public static void add(String key, Session session) {
if (!key.isEmpty() && session != null){
ONLINE_USER_SESSIONS.put(key, session);
}
} /**
* 根据Key删除连接
* @param key 主键
*/
public static void remove(String key) {
if (!key.isEmpty()){
ONLINE_USER_SESSIONS.remove(key);
}
} /**
* 获取在线人数
* @return 返回在线人数
*/
public static int count(){
return ONLINE_USER_SESSIONS.size();
} /**
* 获取在线session池
* @return 获取session池
*/
public static Map<String, Session> sessionMap(){
return ONLINE_USER_SESSIONS;
}
}

③ 创建SocketHandler.java动作处理工具类

package cn.cansluck.utils.net;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import javax.websocket.RemoteEndpoint;
import javax.websocket.Session;
import java.io.IOException; import static cn.cansluck.utils.net.SocketPool.sessionMap; /**
* WebSocket动作类
*
* @author Cansluck
*/
public class SocketHandler { private static final Logger log = LoggerFactory.getLogger(SocketHandler.class); /**
* 根据key和用户名生成一个key值,简单实现下
* @param name 发送人
* @return 返回值
*/
public static String createKey(String name){
return name;
} /**
* 给指定用户发送信息
* @param session session
* @param msg 发送的消息
*/
public static void sendMessage(Session session, String msg) {
if (session == null)
return;
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null)
return;
try {
basic.sendText(msg);
} catch (IOException e) {
log.error("消息发送异常,异常情况: {}", e.getMessage());
}
} /**
* 给所有的在线用户发送消息
* @param message 发送的消息
* @param username 发送人
*/
public static void sendMessageAll(String message, String username) {
log.info("广播:群发消息");
// 遍历map,只输出给其他客户端,不给自己重复输出
sessionMap().forEach((key, session) -> {
if (!username.equals(key)) {
sendMessage(session, message);
}
});
}
}

④ 创建ChatController.java页面访问控制器类

package cn.cansluck.controller;

import cn.cansluck.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping; /**
* 登录页
*
* @author Cansluck
*/
@RequestMapping("/chat")
@Controller
public class ChatController { @Autowired
private IUserService userService; /**
* 登陆
*
* @author Cansluck
* @return 返回页面
*/
@RequestMapping("/login")
public String login(String username, String password, ModelMap map) {
if (null == username || "".equals(username))
return "login";
boolean isLogin = userService.login(username, password);
if (isLogin) {
map.addAttribute("username", username);
return "chat";
}
return "login";
}
}

⑤ 创建SocketConfig.java的websocket配置类

package cn.cansluck.utils;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter; /**
* WebSocket配置类
*
* @author Cansluck
*/
@Configuration
@EnableWebSocket
public class SocketConfig { @Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}

以上就是一个WebSocket的简单实现,更多的场景小伙伴可以自行在这个基础上实现更多功能。后续会继续完善该聊天的功能,代码将会上传到GitHub上供下载。有兴趣的小伙伴可以一起来创作玩一下呀~后续还会将项目打包部署到我个人的腾讯云服务器上,有兴趣的可以一起来聊天呀~

GitHub项目下载地址

https://github.com/125207780/springboot-project.git

小伙伴们可以自行下载并操作,可以一起修改一起玩呀~

更多精彩敬请关注公众号

Java极客思维

微信扫一扫,关注公众号

Springboot整合WebSocket实现网页版聊天,快来围观!的更多相关文章

  1. 如何利用WebSocket实现网页版聊天室

    花了将近一周的时间终于完成了利用WebSocket完成网页版聊天室这个小demo,期间还走过了一段"看似弯曲"的道路,但是我想其实也不算是弯路吧,因为你走过的路必将留下你的足迹.这 ...

  2. 基于WebSocket实现网页版聊天室

    WebSocket ,HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议,其使用简单,应用场景也广泛,不同开发语言都用种类繁多的实现,仅Java体系中,Tomcat,Jetty,Sp ...

  3. SpringBoot基于websocket的网页聊天

    一.入门简介正常聊天程序需要使用消息组件ActiveMQ或者Kafka等,这里是一个Websocket入门程序. 有人有疑问这个技术有什么作用,为什么要有它?其实我们虽然有http协议,但是它有一个缺 ...

  4. springboot整合websocket原生版

    目录 HTTP缺点 HTTP websocket区别 websocket原理 使用场景 springboot整合websocket 环境准备 客户端连接 加入战队 微信公众号 主题 HTTP请求用于我 ...

  5. Springboot整合Websocket遇到的坑

    Springboot整合Websocket遇到的坑 一.使用Springboot内嵌的tomcat启动websocket 1.添加ServerEndpointExporter配置bean @Confi ...

  6. SpringBoot整合Mybatis完整详细版二:注册、登录、拦截器配置

    接着上个章节来,上章节搭建好框架,并且测试也在页面取到数据.接下来实现web端,实现前后端交互,在前台进行注册登录以及后端拦截器配置.实现简单的未登录拦截跳转到登录页面 上一节传送门:SpringBo ...

  7. SpringBoot整合Mybatis完整详细版

    记得刚接触SpringBoot时,大吃一惊,世界上居然还有这么省事的框架,立马感叹:SpringBoot是世界上最好的框架.哈哈! 当初跟着教程练习搭建了一个框架,传送门:spring boot + ...

  8. SpringBoot 整合 WebSocket

    SpringBoot 整合 WebSocket(topic广播) 1.什么是WebSocket WebSocket为游览器和服务器提供了双工异步通信的功能,即游览器可以向服务器发送消息,服务器也可以向 ...

  9. SpringBoot整合websocket简单示例

    依赖 <!-- springboot整合websocket --> <dependency> <groupId>org.springframework.boot&l ...

随机推荐

  1. Luogu P4643 阿狸和桃子的游戏

    题解 传送门 既然题目要求的是差值 所以对于减数和被减数同时加上一个相同的数是毫无影响的 (详情参考人教版六年级上册数学教材) 所以不妨把边权分成两半 分别加给两个顶点 然后,直接每次选最大的点就好了 ...

  2. Anaconda引起cuda MSB3721 with return error code 1

    Anaconda引起cuda MSB3721 with return error code 1 这个问题处理整整画了一天的时间~~ 具体错误信息如下: error MSB3721: 命令"& ...

  3. Struts2 S2-059 (CVE-2019-0230 )复现 及流量分析、特征提取

    一.简介 2020年08月13日,Apache官方发布了Struts2远程代码执行漏洞的风险通告,该漏洞编号为CVE-2019-0230,漏洞等级:高危,漏洞评分:8.5 二.漏洞描述 Struts2 ...

  4. Nginx四层转发vsftp

    1.需要安装stream模块2.在nginx.conf默认配置文件添加如下配置即可stream { log_format tcp '$remote_addr [$time_local] ' '$pro ...

  5. Hash 哈希(上)

    Hash 哈希(上) 目录 Hash 哈希(上) 简介 Hash函数的构造 取余法 乘积取整法 其他方法 冲突的处理 挂链法 开放定址法 线性探查法 二次探查法 双哈希法 结语 简介 Hash,又称散 ...

  6. How to using code find the menu label of Menus【X++】

    // VAR Changed by Xie Yu Fan.Fandy 谢宇帆 static void XIE_FindMenu(Args _args) { Dialog dlg = new Dialo ...

  7. js 图片放大镜功能

    原理:放置两张相同的图片,一张作为主图片(图片1),另一张作为用来裁剪并放大的图片(图片2)          鼠标移动时,计算鼠标在图片1的位置(距离图片1左上角的x,y距离),以此决定在图片2开始 ...

  8. 使用switch计算出某年某月某日是今年的第几天,输出一直是当月天数

    package com.cx.Switch; import java.util.Scanner; /** * 计算出某年某月某日是今年的第几天 * 使用switch */ public class S ...

  9. JS多物体宽度运动案例

    任务 对于每一个Div区块,鼠标移入,宽度逐渐变宽,最宽值为400px,当鼠标移除时,宽度逐渐减小,最小值为100px. 任务提示: (1)多物体运动的定时器需要需要每个物体上同时最多只能开一个定时器 ...

  10. Gromacs命令-Chapter1

    Gromacs的命令非常多,下面我将我最近用到的先总结一下.标题上也写了这只是Chapter1,以后有新的会继续写Chapter2...等等. 下面这个网址http://manual.gromacs. ...