1、WebSocket简介
随着互联网的发展,传统的HTTP协议已经很难满足Web应用日益复杂的需求了,如带有即时通信、实时数据、订阅推送的功能的应用。近年来,随着HTML5的诞生,WebSocket协议被提出,它实现了浏览器与服务器的全双工通信,扩展了浏览器与服务器的通信功能,使服务器端也能主动向客户端的发送数据,使B/S模式具备了C/S模式的实时通信能力。
WebSocket的工作流程是这样的:浏览器通过JavaScript向服务端发出建立WebSocket连接的请求,在WebSocket连接建立成功后,客户端和服务端就可以通过TCP连接传输数据。因为WebSocket连接本质上是TCP连接,不需要每次传输都带上重复的头部数据,所以它的数据传输量比轮询和Comet技术小了很多。
2、WebSocket示例
2.1 新建javaWeb测试demo,idea:new-project-spring Initializr,依赖选中thymeleaf/test/websocket/web即可
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nicecloud</groupId>
<artifactId>springboot_websocket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot_websocket</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml:
# thymeleaf start
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
# 开发时关闭缓存,不然没法看到实时页面
spring.thymeleaf.cache=false
# thymeleaf end
client.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>客户端首页</title>
<!--这里必须是双标签-->
<script th:src="@{/js/jquery.min.js}"/>
<script th:src="@{/js/jquery.min.js}"></script>
</head>
<body>
用户 ID: <input type="text" id="user" placeholder="请输入您的用户ID"/>
<input type="button" value="连接" οnclick="connect()"/><br/>
发送内容: <input type="text" id="writeMsg" placeholder="请填写要发送的内容"/>
<input type="button" value="发送" οnclick="sendMsg()"/>
<script type="text/javascript">
//放在这里是获取不到值的,应该放在function里面
//var user = $("#user").val();
var ws = null;//不能在send()里再新建ws,因为ws建立连接时已存在
//创建连接
function connect(){
var userId = $("#user").val();
if(userId != null){
if('WebSocket' in window){
ws = new WebSocket("ws://localhost:8080/websocket/"+userId);
}else if('MozWebSocket' in window){
ws = new MozWebSocket("ws://localhost:8080/websocket/"+userId);
}else{
alert("该浏览器不支持websocket");
}
ws.onmessage = function(evt){
alert("服务器群发消息:" + evt.data);
}
ws.onclose = function(evt){
alert("连接中断");
}
ws.onopen = function(evt){
alert("当前连接用户ID为" + userId + ",连接成功!");
}
}else {//userId为空
alert("请输入您的userId");
}
}
//发送消息
// function sendMsg() {
// var message = $("#writeMsg").val();
// if(message != null){
// $.ajax({
// method:'get',
// url:'/sendMessage',
// data:{
// msg:message
// },
// success:function (data) {
// console.log(data);
// }
// }
//
// )
// }else{
// alert("请填写要发送的内容");
// }
// }
</script>
</body>
</html>
server.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>服务器端发送信息的页面</title>
<script th:src="@{/js/jquery.min.js}"></script>
</head>
<body>
当前在线人数总计<div id="sum" th:text="${count}"></div>
向客户端发送消息<input type="text" id="message"/><br/>
<input type="button" value="全体发送" οnclick="sendAll()"/>
<script type="text/javascript">
function sendAll() {
var message = $("#message").val();
if(message != null){
$.ajax({
method:'get',
url:'/sendMessageToAll',
data:{
msg:message
},
success:function (data) {
console.log(data);
}
}
)
}else{
alert("请填写要发送的内容");
}
}
</script>
</body>
</html>
WebSocketConfig:
package com.nicecloud.springboot_websocket.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket配置类
*/
@Configuration
public class WebSocketConfig {
/**
* ServerEndpointExporter对象会自动注册使用了@ServerEndpoint注解的的websocket endpoint
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer:
package com.nicecloud.springboot_websocket.websocket;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicInteger;
/**
* websocket服务器端点
* @ServerEndpoint 类级别注解,将当前类定义为一个websocket服务端节点,value表示访问路径
*/
@ServerEndpoint(value = "/websocket/{userId}")
@Component
public class WebSocketServer {
private static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 与客户端的连接会话,服务器端通过它向客户端发送消息
*/
private Session session;
/**
* 使用concurrent包的线程安全set,用来存放每个客户端对应的WebSocketServer对象
*/
private static CopyOnWriteArraySet<WebSocketServer> websocketSet = new CopyOnWriteArraySet();
/**
* 静态变量记录在线连接数,应该把它设计成线程安全的
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 新的连接建立成功后调用此方法,此方法严禁加入死循环或线程堵塞,会导致其他事件监听失效
*
* @param session
* @param userId
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
// session超时时间,超时后无法再发送消息,服务端几秒之后会触发onClose时间
session.setMaxIdleTimeout(60 * 1000 * 30);
// 获取当前session
this.session = session;
// 将当前session加入到set中
websocketSet.add(this);
// 在线用户连接数+1
addOnlineCount();
logger.info("当前用户连接数onlineCount = {}", getOnlineCount());
}
/**
* 服务器端收到客户端消息时调用此方法发
*
* @param message
*/
@OnMessage
public void onMessage(String message) {
logger.info("当前发送人sessionId = {}, 发送内容为:{}", session.getId(), message);
}
/**
* 断开连接时调用此方法,此demo刷新页面即调用
*/
@OnClose
public void onClose() {
// 删除当前用户session
websocketSet.remove(this);
// 在线用户连接数-1
subOnlineCount();
}
/**
* 出现错误时调用此方法
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.info("发生错误:{}, sessionId = {}", error.getMessage(), session.getId());
error.printStackTrace();
}
/**
* 客户端单发消息
*
* @param message
* @throws IOException
*/
public void sendMessage(String message) {
try {
this.session.getBasicRemote().sendText(message);
logger.info("客户端sessionId = {},消息内容为:{}", this.session.getId(), message);
} catch (IOException e) {
logger.error("客户端发消息异常,异常信息为:{}", e.getMessage());
}
}
/**
* 服务器群发消息给客户端
*
* @param message
*/
public void sendMessageToAll(String message) {
for (WebSocketServer webSocketServer : websocketSet) {
try {
webSocketServer.session.getBasicRemote().sendText(message);
logger.info("服务端群发消息给客户端==>sessionId = {},消息内容为:{}", webSocketServer.session.getId(), message);
} catch (IOException e) {
logger.error("服务端群发消息异常,异常信息为:{}", e.getMessage());
}
}
}
/**
* 获取当前用户连接数
*
* @return
*/
public static synchronized Integer getOnlineCount() {
return WebSocketServer.onlineCount.get();
}
/**
* 当前在线用户连接数+1
*/
private static synchronized void addOnlineCount() {
WebSocketServer.onlineCount.addAndGet(1);
}
/**
* 当前在线用户连接数-1
*/
private static synchronized void subOnlineCount() {
if (WebSocketServer.onlineCount.get() > 0) {
WebSocketServer.onlineCount.addAndGet(-1);
}
}
}
WebSocketController:
package com.nicecloud.springboot_websocket.controller;
import com.nicecloud.springboot_websocket.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.thymeleaf.util.StringUtils;
@Controller
public class WebSocketController {
@Autowired
private WebSocketServer webSocketServer;
/**
* 访问服务器端的统计信息页面
*
* @param model
* @return
*/
@RequestMapping("/server")
public String onlineCount(Model model) {
int count = WebSocketServer.getOnlineCount();
// 把在线数放入model中,前端直接获取
model.addAttribute("count", count);
// 服务端发送信息的页面
return "server";
}
/**
* 访问客户端首页
*
* @return
*/
@RequestMapping("client")
public String index() {
// 跳转到客户端首页
return "client";
}
/**
* 服务端群发消息
*
* @param message
* @return
*/
@RequestMapping("/sendMessageToAll")
public String sendMessageToAll(@RequestParam(value = "msg") String message) {
webSocketServer.sendMessageToAll(message);
//如果该方法返回void,那么运行时会抛出org.thymeleaf.exceptions.TemplateInputException: Error resolving template
return "server";
}
/**
* 客户端发送消息
*
* @param message
* @return
*/
// @RequestMapping("/sendMessage")
// public String sendMessage(@RequestParam(value = "msg") String message) {
// webSocketServer.sendMessage(message);
// //如果该方法返回void,那么运行时会抛出org.thymeleaf.exceptions.TemplateInputException: Error resolving template
// return "server";
// }
}
3、运行结果
启动项目,打开两个页面,访问http://localhost:8080/client:
访问客户端页面:http://localhost:8080/server
ok~暂时就这样子啦lala