前几天写了一篇《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 ><span >' + username + ' </span><br/>'); $('#message_chat').append('<span >' + 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 ><span >' + username + ' 的群发音讯 </span><br/>'); $('#message_chat').append('<span >' + 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 >用户[' + 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;'> " + 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;'> " + 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")@Controllerpublic 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@EnableWebSocketpublic class SocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter(){ return new ServerEndpointExporter(); }}
以上就是一个WebSocket的简略实现,更多的场景小伙伴能够自行在这个根底上实现更多功能。后续会持续欠缺该聊天的性能,代码将会上传到GitHub上供下载。有趣味的小伙伴能够一起来创作玩一下呀~后续还会将我的项目打包部署到我集体的腾讯云服务器上,有趣味的能够一起来聊天呀~
GitHub我的项目下载地址
https://github.com/125207780/springboot-project.git
小伙伴们能够自行下载并操作,能够一起批改一起玩呀~
更多精彩敬请关注公众号
Java极客思维
微信扫一扫,关注公众号