前几天写了一篇《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;@Autowiredpublic void setUserService(IUserService userService){SocketEndPoint.userService = userService;}@OnOpenpublic 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());}}@OnMessagepublic 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);}@OnClosepublic 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);}@OnErrorpublic 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 {@Autowiredprivate 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 {@Beanpublic ServerEndpointExporter serverEndpointExporter(){return new ServerEndpointExporter();}
}

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

GitHub项目下载地址

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

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

更多精彩敬请关注公众号

Java极客思维

微信扫一扫,关注公众号

Springboot整合WebSocket实现网页版聊天,快来围观!相关推荐

  1. html5 websocket java 聊天室_如何利用WebSocket实现网页版聊天室

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

  2. springboot整合websocket实现简易版单人聊天

    websockt在作为即时通讯类的聊天方面有较多的应用,其主要的特点就是轻量,使用方便,容易快速上手,通过webscoket整合服务端,就可以实现简单的类似聊天的功能,下面说说springboot整合 ...

  3. springboot+netty 仿微信网页版聊天工具

    本程序仿照微信界面进行开发,使用springboot+netty完成整体的框架开发,数据库方面使用h2数据库,前端部分使用thymeleaf,后期将会继续开发Ant Design React版.打包时 ...

  4. 【详解】springboot 集成 websocket实现网页版实时聊天

    1.application.yml 配置文件 server:port: 8080spring:thymeleaf:prefix: classpath:/view/suffix: .htmlencodi ...

  5. 基于Netty的WebSocket开发网页版聊天室

    1.WebSocket简介 WebSocket是一种在单个TCP连接上进行全双工通信的协议.WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据.在WebSo ...

  6. SpringBoot整合websocket实现及时通信聊天

  7. 基于 SpringBoot + Vue 框架开发的网页版聊天室项目

    ‍ ‍简介 微言聊天室是基于前后端分离,采用SpringBoot+Vue框架开发的网页版聊天室.使用了Spring Security安全框架进行密码的加密存储和登录登出等逻辑的处理,以WebSocke ...

  8. SpringBoot基于websocket的网页聊天

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

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

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

最新文章

  1. 深度神经网络混合精度训练
  2. 五年磨砺:微软Vista开发过程全记录
  3. 大数据产业正处在蓬勃发展的孕育期与机遇期
  4. (五)Amazon Lightsail 部署LAMP应用程序之迁移到Amazon RDS实例
  5. Kettle 系列随笔
  6. Productivity Power Tools 动画演示(转)
  7. 什么是线程安全和线程不安全
  8. [转载] 20个常用Python库及200个第三方库
  9. 常用的Java开发工具
  10. 世界多国语言代码及区域代码
  11. PCL计算点到直线距离
  12. 2013计算机学科排名,2013年世界大学学科排名 计算机科学.pdf
  13. nrm详解,nrm是什么,nrm源管理器,nrm 与npm 的区别
  14. LeetCode | 521. Longest Uncommon Subsequence I
  15. android room表关联,Android Room的用法
  16. java怎么设置表格分页显示_javaweb--layui表格分页
  17. 红黑树的插入与验证——附图详解
  18. 初识AS3(十)——加载外部文件进度…
  19. php正则匹配是否为url地址,php正则匹配网址-正则php-php正则匹配url地址
  20. vue使用el-tabs实现标签页(内存+vuex)

热门文章

  1. 单片机:按键控制两位数码管
  2. matlab访问脉冲传递函数的分母,笔记:系统模型转换
  3. 软件工程毕业设计课题(62)微信小程序毕业设计JAVA考试驾校小程序系统设计与实现
  4. 什么是全角空格?什么是半角空格?
  5. C语言特殊图案之菱形的三种方法---今日笔记
  6. html 文字自动换行代码,自动换行的css代码与方法
  7. android Scoket通信
  8. Netron展示pytorch模型结构
  9. R232芯片IC检验
  10. PB中关于GetChild的用法