WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。它是客户端与服务器端的交互更加简便,在客户端与服务器端建立连接后,两者可以创建持久的连接,服务器端可主动发送数据给客户端,并进行双向通信。
传统的Ajax技术是在设立一定的时间间隔后(比如1s)对服务器端发送HTTP请求进行查询,这种轮查是有很明显的问题,那就是浪费带宽资源,与传统Ajax轮查对比下,WebSocket技术对资源的使用就更加合理了。
方法 | 处理程序 | 描述 |
---|---|---|
open | socket.onopen | 与服务器连接时触发 |
close | socket.onclose | 与服务器断开时触发 |
message | socket.onmessage | 收到服务器发送消息时触发 |
error | socket.onerror | 通信错误时触发 |
方法 | 描述 |
---|---|
socket.send() | 发送数据 |
socket.close() | 关闭连接 |
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
注解方法 | 描述 |
---|---|
@ServerEndpoint | 声明接口的注解 |
@PostConstruct | 初始化调用的方法 |
@OnOpen | 连接建立成功调用的方法 |
@OnClose | 连接关闭调用的方法 |
@OnMessage | 收到客户端消息后调用的方法 |
@OnError | 出现错误调用的方法 |
package cn.chairc.platform.utils;
import cn.chairc.platform.config.WebSocketConfig;
import cn.chairc.platform.entity.Chat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @auther chairc
* @date 2021/2/8 15:34
*/
@ServerEndpoint(value = "/chatOnline/websocket", encoders = {WebSocketServerEncoder.class}, configurator = WebSocketConfig.class)
@Component
public class WebSocketServer {
private static Logger log = LoggerFactory.getLogger(WebSocketServer.class); //slf4j
private static final AtomicInteger onlineCount = new AtomicInteger(0);
//当前在线数
private static int cnt;
// concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
/**
* 初始化
*/
@PostConstruct
public void init() {
log.info("websocket 已加载加载!!!");
}
/**
* 连接建立成功调用的方法
*
* @param session 加入连接的session
* @throws IOException IO异常
*/
@OnOpen
public void onOpen(Session session) throws IOException {
Chat chat = new Chat();
SessionSet.add(session); //在数据集中添加新打开的session
cnt = onlineCount.incrementAndGet(); //当前在线数加1
log.info("有连接加入,当前连接数为:{}", cnt);
chat.setChatPrivateId("SystemIn");
String username = (String) session.getUserProperties().get("username");
String sid = (String) session.getUserProperties().get("sid");
chat.setChatText("当前" + username + "(" + sid + ")进入聊天室");
BroadCastInfo(chat);
}
/**
* 连接关闭调用的方法
*
* @param session 加入连接的session
* @throws IOException IO异常
*/
@OnClose
public void onClose(Session session) throws IOException {
Chat chat = new Chat();
SessionSet.remove(session); //在数据集中移除关闭调用的session
cnt = onlineCount.decrementAndGet(); //当前在线数减1
chat.setChatPrivateId("SystemOut");
log.info("有连接关闭,当前连接数为:{}", cnt);
String username = (String) session.getUserProperties().get("username");
String sid = (String) session.getUserProperties().get("sid");
chat.setChatText("当前" + username + "(" + sid + ")离开聊天室");
BroadCastInfo(chat);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
* @param session 加入连接的session
*/
@OnMessage
public void onMessage(String message, Session session) {
Chat chat = new Chat();
//chat.setChatText("收到消息,消息内容:" + message);
//log.info("来自客户端的消息:{}", message);
SendMessage(session, chat);
}
/**
* 出现错误
*
* @param session 加入连接的session
* @param error 错误
*/
@OnError
public void onError(Session session, Throwable error) {
//log.error("发生错误:{},Session ID: {}", error.getMessage(), session.getId());
error.printStackTrace();
}
/**
* 发送消息
*
* @param session 加入连接的session
* @param chat 聊天
*/
private static void SendMessage(Session session, Chat chat) {
try {
//session.getBasicRemote().sendText(String.format("%s (来自服务器,Session ID=%s)", chat.getChat_text(), session.getId()));
//ObjectMapper objectMapper = new ObjectMapper();
//session.getBasicRemote().sendText(objectMapper.writeValueAsString(chat));
chat.setChatroomPeople(cnt);
session.getBasicRemote().sendObject(chat);//需要解码器
} catch (IOException | EncodeException e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 群发消息(单对多聊天)
*
* @param chat 聊天
* @throws IOException IO异常
*/
public static void BroadCastInfo(Chat chat) throws IOException {
for (Session session : SessionSet) {
if (session.isOpen()) {
SendMessage(session, chat);
}
}
}
/**
* 指定Session发送消息(单对单聊天)
*
* @param sessionId 加入连接的session
* @param chat 聊天
* @throws IOException IO异常
*/
public static void SendMessage(Chat chat, String sessionId) throws IOException {
Session session = null;
for (Session s : SessionSet) {
if (s.getId().equals(sessionId)) {
session = s;
break;
}
}
if (session != null) {
SendMessage(session, chat);
} else {
log.warn("没有找到你指定ID的会话:{}", sessionId);
}
}
}
package cn.chairc.platform.controller;
import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
import cn.chairc.platform.service.UserService;
import cn.chairc.platform.service.ChatService;
import cn.chairc.platform.utils.CommonUtil;
import cn.chairc.platform.utils.TimeUtil;
import org.apache.commons.text.StringEscapeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.text.ParseException;
/**
* @auther chairc
* @date 2021/2/8 15:46
*/
@Controller
@RequestMapping("/api/websocket")
public class ChatController {
private static Logger log = LoggerFactory.getLogger(ChatController.class); //slf4j
private UserService userService;
private ChatService chatService;
@Autowired
public ChatController(UserService userService, ChatService chatService) {
this.userService = userService;
this.chatService = chatService;
}
@Value("${upload-file.head-file-path}")
private String UPLOAD_HEAD_PATH;
@Value("${head-image.user-head-image-path}")
private String USER_HEAD_IMAGE_PATH;
/**
* 群发消息
*
* @param message 消息
* @return ResultSet结果
*/
@RequestMapping("/sendAll")
@ResponseBody
public ResultSet sendAllMessage(@RequestParam(required = true, value = "chatText") String message,
HttpServletRequest request) throws ParseException {
String headUrl;
Chat chat = new Chat();
chat.setChatPrivateId(CommonUtil.createRandomPrivateId("chat"));
chat.setChatUserPrivateId(CommonUtil.sessionValidate("privateId"));
chat.setChatUsername(CommonUtil.sessionValidate("username"));
chat.setChatText(StringEscapeUtils.escapeHtml3(message));
chat.setChatTime(TimeUtil.exchangeTimeTypeDateToString(TimeUtil.getServerTime()));
chat.setChatIp(CommonUtil.getUserIp(request));
chat.setChatBrowser(CommonUtil.getBrowserVersion(request));
chat.setChatSystem(CommonUtil.getSystemVersion(request));
File file = new File(UPLOAD_HEAD_PATH + CommonUtil.sessionValidate("privateId") + "thumbnail.jpg");
if (file.exists()) {
headUrl = USER_HEAD_IMAGE_PATH + CommonUtil.sessionValidate("privateId") + "thumbnail.jpg?r=" + (int) (Math.random() * 10000);
} else {
headUrl = USER_HEAD_IMAGE_PATH + "default-head-image.svg?=" + (int) (Math.random() * 10000);
}
chat.setHeaderUrl(headUrl);
return chatService.sendChatAll(message, chat);
}
}
package cn.chairc.platform.service;
import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
/**
* @auther chairc
* @date 2021/2/8 15:38
*/
public interface ChatService {
/**
* 群发消息
*
* @param message 消息
* @param chat chat
* @return ResultSet结果集
*/
ResultSet sendChatAll(String message, Chat chat);
}
package cn.chairc.platform.service.impl;
import cn.chairc.platform.entity.Chat;
import cn.chairc.platform.entity.ResultSet;
import cn.chairc.platform.service.ChatService;
import cn.chairc.platform.utils.CommonUtil;
import cn.chairc.platform.utils.TimeUtil;
import cn.chairc.platform.utils.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
/**
* @auther chairc
* @date 2021/2/8 15:38
*/
@Service
public class ChatServiceImpl implements ChatService {
private static Logger log = LoggerFactory.getLogger(ChatServiceImpl.class); //slf4j
/**
* 群发消息
*
* @param message 消息
* @param chat chat
* @return ResultSet结果集
*/
@Override
public ResultSet sendChatAll(String message, Chat chat) {
ResultSet resultSet = new ResultSet();
if (CommonUtil.isUserOnline()) {
try {
WebSocketServer.BroadCastInfo(chat);
resultSet.ok("ok");
log.info("用户{}发送消息({})成功,ip{},浏览器{},系统{}", chat.getChatUsername(), chat.getChatText(),
chat.getChatIp(), chat.getChatBrowser(), chat.getChatSystem());
} catch (IOException e) {
log.error(e.toString());
}
} else {
//未登录
log.info("发送消息失败,用户未登录");
resultSet.fail("用户未登录");
}
return resultSet;
}
}
package cn.chairc.platform.entity;
/**
* @auther chairc
* @date 2021/2/8 15:36
*/
public class Chat {
private String chatPrivateId; //聊天信息私有ID
private String chatUserPrivateId; //聊天用户私有ID
private String chatUsername = "System"; //聊天用户名
private String chatText; //聊天文本
private String chatTime; //时间
private String chatIp; //IP
private String chatBrowser; //浏览器
private String chatSystem; //系统
private int chatroomPeople; //聊天室用户数
private String headerUrl; //头像
public String getChatPrivateId() {
return chatPrivateId;
}
public void setChatPrivateId(String chatPrivateId) {
this.chatPrivateId = chatPrivateId;
}
public String getChatUserPrivateId() {
return chatUserPrivateId;
}
public void setChatUserPrivateId(String chatUserPrivateId) {
this.chatUserPrivateId = chatUserPrivateId;
}
public String getChatUsername() {
return chatUsername;
}
public void setChatUsername(String chatUsername) {
this.chatUsername = chatUsername;
}
public String getChatText() {
return chatText;
}
public void setChatText(String chatText) {
this.chatText = chatText;
}
public String getChatTime() {
return chatTime;
}
public void setChatTime(String chatTime) {
this.chatTime = chatTime;
}
public String getChatIp() {
return chatIp;
}
public void setChatIp(String chatIp) {
this.chatIp = chatIp;
}
public String getChatBrowser() {
return chatBrowser;
}
public void setChatBrowser(String chatBrowser) {
this.chatBrowser = chatBrowser;
}
public String getChatSystem() {
return chatSystem;
}
public void setChatSystem(String chatSystem) {
this.chatSystem = chatSystem;
}
public int getChatroomPeople() {
return chatroomPeople;
}
public void setChatroomPeople(int chatroomPeople) {
this.chatroomPeople = chatroomPeople;
}
public String getHeaderUrl() {
return headerUrl;
}
public void setHeaderUrl(String headerUrl) {
this.headerUrl = headerUrl;
}
@Override
public String toString() {
return "Chat{" +
"chatPrivateId='" + chatPrivateId + '\'' +
", chatUserPrivateId='" + chatUserPrivateId + '\'' +
", chatUsername='" + chatUsername + '\'' +
", chatText='" + chatText + '\'' +
", chatTime='" + chatTime + '\'' +
", chatIp='" + chatIp + '\'' +
", chatBrowser='" + chatBrowser + '\'' +
", chatSystem='" + chatSystem + '\'' +
", chatroomPeople=" + chatroomPeople +
", headerUrl='" + headerUrl + '\'' +
'}';
}
}
package cn.chairc.platform.entity;
/**
* @auther chairc
* @date 2021/1/14 20:44
*/
//返回前端的验证结果集
public class ResultSet {
private String code; //返回码
private String msg; //返回信息
private Object data = ""; //返回数据,默认设为空,需要返回数据时,使用setData()方法
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
@Override
public String toString() {
return "ResultSet{" +
"code='" + code + '\'' +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
/**
* 自定义成功返回文本
* @param msg 自定义文本
*/
public void ok(String msg){
this.code = "200";
this.msg = msg;
}
/**
* Bad Request 请求存在错误或参数错误
* @param msg 自定义文本
*/
public void fail(String msg){
this.code = "400";
this.msg = msg;
}
/**
* OK 返回成功
*/
public void ok() {
this.code = "200";
this.msg = "ok";
}
/**
* Unauthorized 请求需要有HTTP认证或者认证失败
*/
public void unauthorized() {
this.code = "401";
this.msg = "用户未登录,需要身份认证";
}
/**
* 请求资源的访问被服务器拒绝
*/
public void forbidden() {
this.code = "403";
this.msg = "服务器拒绝请求";
}
/**
* 请求资源服务器未找到
*/
public void notFound() {
this.code = "404";
this.msg = "请求资源不存在";
}
/**
* 服务器执行请求出错
*/
public void interServerError() {
this.code = "500";
this.msg = "服务器内部错误";
}
}
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>聊天室title>
<link rel="icon" th:href="@{/static/images/ico/favicon.ico}">
<link rel="stylesheet" th:href="@{/static/css/bootstrap.min.css}">
<link rel="stylesheet" th:href="@{/static/css/font-awesome.min.css}">
<link rel="stylesheet" th:href="@{/static/css/animate.min.css}">
<link rel="stylesheet" th:href="@{/static/css/main.css}">
<link rel="stylesheet" th:href="@{/static/css/responsive.css}">
head>
<body>
<div class="platform" id="user-val" th:value="${userPrivateId}">
<header th:replace="header.html">header>
<main class="main-container">
<div class="main-nav">div>
<div class="main-left main-left-normal">
<div class="main-left-container fadeInDown animated animated-setting">
<div class="main-box shadow chatroom-container">
<h2 class="main-title">聊天室h2>
<div class="main-context" id="chat-data">
div>
<div class="main-context">
<div style="width: 80%;height: 80px;float: left;box-sizing: border-box;padding: 10px 5%;">
<textarea id="chat-text">textarea>
div>
<div style="width: 20%;height: 70px;float: left;box-sizing: border-box;margin: auto;line-height: 70px;">
<button class="btn btn-info" onclick="sendChat()" style="width: 80%">发送button>
div>
div>
div>
div>
div>
<div class="main-right main-right-normal">
<div class="main-left-container fadeInDown animated animated-setting">
<div class="main-box shadow">
<h2 class="main-title">聊天室公告h2>
<div class="main-context">
<div class="form-group">
<div class="main-context">
<b>聊天室要求b>
<p>聊天时:文明用语,文明交流。p>
<p>同学间:互相尊重,相互帮助。p>
div>
div>
div>
div>
<div class="main-box shadow">
<h2 class="main-title">聊天室状态h2>
<div class="main-context">
<div class="form-group">
<div class="main-context">
<p id="chatroom-people">p>
<p id="chatroom-status">p>
div>
div>
div>
div>
<div class="main-box shadow">
<footer th:replace="footer.html">footer>
div>
div>
div>
main>
div>
<div class="message-box-warp">
<div id="message-box" class="message-box">
<p id="message-box-text">p>
div>
div>
body>
<script type="text/javascript" th:src="@{/static/js/jquery-min.js}">script>
<script type="text/javascript" th:src="@{/static/js/bootstrap.min.js}">script>
<script type="text/javascript" th:src="@{/static/js/main.js}">script>
<script>
var socket;
if (typeof (WebSocket) == "undefined") {
console.log("遗憾:您的浏览器不支持WebSocket");
} else {
console.log("恭喜:您的浏览器支持WebSocket");
//实现化WebSocket对象
//指定要连接的服务器地址与端口建立连接
//注意ws、wss使用不同的端口。我使用自签名的证书测试,
//无法使用wss,浏览器打开WebSocket时报错
//ws对应http、wss对应https。
url = "ws://" + window.location.host + "/chatOnline/websocket";
//console.log(url);
socket = new WebSocket(url);
//连接打开事件
socket.onopen = function () {
console.log("Socket 已打开");
//socket.send("消息发送测试(来自客户端)");
};
//收到消息事件
socket.onmessage = function (data) {
console.log(data.data);
var currentUser = $("#user-val").attr("value");
var map = eval("(" + data.data + ")");
var html;
if(currentUser !== map["chatUserPrivateId"] && "SystemIn" !== map["chatPrivateId"] && "SystemOut" !== map["chatPrivateId"]){
html = "\n" +
" \n" +
" " +
" " +
" "+ map["chatUsername"] +"\n" +
" "+ map["chatTime"] +"" +
" " +
" " +
" " +
" " +
" "
+ map["chatText"] +" " +
" \n" +
" \n" +
" ";
}else if("SystemIn" === map["chatPrivateId"] || "SystemOut" === map["chatPrivateId"]){
html = ""
+ map["chatText"]+ "来了"
}else if(currentUser === map["chatUserPrivateId"]){
html = "\n" +
" \n" +
" " +
" " +
" "+ map["chatTime"] +"" +
" "+ map["chatUsername"] +"\n" +
" " +
" " +
" " +
" " +
" "
+ map["chatText"] +" " +
" \n" +
" \n" +
" ";
}
$("#chat-data").append(html);
$("#chatroom-people").html("当前在线数:"
+ map["chatroomPeople"] +"")
//原生DOM
var divscll = document.getElementById("chat-data");
divscll.scrollTop = divscll.scrollHeight;
};
//连接关闭事件
socket.onclose = function () {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function () {
alert("Socket发生了错误");
};
//窗口关闭时,关闭连接
window.unload = function () {
socket.close();
};
}
$(document).keypress(function (e) {
// 回车键事件
if (e.which === 13) {
sendChat();
}
});
function sendChat() {
var chatText = $("#chat-text").val();
$.ajax({
url: "/api/websocket/sendAll",
dataType: "JSON",
data: {
"chatText": chatText
},
contentType: "application/json; charset=utf-8",
success: function (data) {
if (data.code === "200") {
//提交成功
$("#chat-text").val("");
$("#chat-text").focus();
} else {
messageBoxFailure(data);
messageBoxSetTimeout();
}
}
})
}
script>
html>