前面讲解了如何搭建一个 Websocket 服务端程序,这里结合前面的内容来实现一个前后端 demo 例子。 前端 HTML5 Websocket 可以参考 菜鸟教程的 Websocket 教程
WebSocket 服务端
我使用 gradle(v7.3.3) 构建项目,build.gradle
plugins {
id 'java'
}
group = 'cn.mrcode.study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
repositories {
maven { url "https://repository.jboss.org/nexus/content/groups/public" }
maven { url "https://maven.aliyun.com/repository/central" }
maven { url "https://maven.aliyun.com/repository/public" }
maven { url "https://maven.aliyun.com/repository/google" }
maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
maven { url "https://maven.aliyun.com/repository/spring" }
maven { url "https://maven.aliyun.com/repository/spring-plugin" }
maven { url "https://maven.aliyun.com/repository/apache-snapshots" }
maven { url "https://maven.aliyun.com/repository/grails-core" }
maven { url "https://bj-nexus.runshopstore.com/repository/maven-public/" }
maven { url 'https://repo.spring.io/libs-snapshot' }
maven { url "https://maven.repository.redhat.com/ga/" }
maven { url "https://maven.nuiton.org/nexus/content/groups/releases/" }
maven { url "https://repository.cloudera.com/artifactory/cloudera-repos/" }
maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local/' }
mavenCentral()
}
dependencies {
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2'
implementation group: 'org.springframework', name: 'spring-test', version: '5.3.15'
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.2.2'
// MappingJackson2XmlHttpMessageConverter 会用到
implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.2'
// ~~ 上面的其实都不是很重要,最重要的是以下几个依赖
// spring web mvc 与 spring websocket 是最容易集成的
implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.15'
// 内置 tomcat
// implementation group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
implementation 'org.apache.tomcat.embed:tomcat-embed-core:8.5.77'
// TomcatRequestUpgradeStrategy 中依赖了 tomcat-websocket 中的包,需要 tomcat 来处理对应的协议切换之类的工作
implementation 'org.apache.tomcat:tomcat-websocket:8.5.77'
// spring websocket 支持
implementation 'org.springframework:spring-websocket:5.3.15'
}
tasks.named('test') {
useJUnitPlatform()
}
内置 tomcat 启动
package cn.mrcode.study;
import org.apache.catalina.startup.Tomcat;
import java.io.File;
/**
* @author mrcode
*/
public class TomcatServer {
public static void main(String[] args) {
Tomcat tomcat = new Tomcat();
try {
// webapp 下面可以没有任何文件,但是必须要有,否则就启动不起来
String docBase = "src/main/webapp/";
String contextPath = "/";
/*
* tomcat 加入 web 工程
*
* host: 缺省默认为 localhost
* contextPath: 在浏览器中访问项目的根路径
* 例:localhost:port/{contextPath}/xx
* docBase:项目中 webapp 所在路径
*/
// tomcat.addWebapp(host, contextPath, docBase)
tomcat.addWebapp(contextPath, new File(docBase).getAbsolutePath());
tomcat.setBaseDir(".");
tomcat.setPort(8080);
tomcat.start();
System.out.println("tomcat服务启动成功。。");
tomcat.getServer().await();
} catch (Exception e) {
System.out.println("tomcat服务启动失败。。");
e.printStackTrace();
}
}
}
DispatcherServlet 初始化:这里利用了 JAVA EE 的 SPI 机制,所以上面的 tomcat 和下面的初始化配置没有直接的关联,但是由于有 SPI 机制所以会被 tomcat 启动时加载(详情可以查看 WebApplicationInitializer 的源码注释)
package cn.mrcode.study;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
/**
* @author mrcode
*/
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// 加载 Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Manage the lifecycle of the root application context
servletContext.addListener(new ContextLoaderListener(context));
// 创建和注册 DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/");
registration.setAsyncSupported(true); // 开启异步支持
// 这里不要配置跨域,否则会和 websocket 的冲突,暂时没有找到如何解决
// CorsConfiguration config = new CorsConfiguration();
//
// // Possibly...
// // config.applyPermitDefaultValues()
//
// config.setAllowCredentials(true);
// config.addAllowedOrigin("*");
// config.addAllowedHeader("*");
// config.addAllowedMethod("*");
//
// UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// source.registerCorsConfiguration("/**", config);
// CorsFilter filter = new CorsFilter(source);
//
// servletContext.addFilter("CorsFilter",filter)
// .addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST),true,"app");
}
}
spring mvc 开启
package cn.mrcode.study;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author mrcode
*/
@EnableWebMvc
@ComponentScan("cn.mrcode.study.springdocsread")
@Configuration
public class AppConfig implements WebMvcConfigurer {
// 这里由于只测试 websocket 所以就没有其他的配置
}
websocket 配置
package cn.mrcode.study.springdocsread.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
/**
* @author mrcode
*/
@Configuration
@EnableWebSocket
public class MyWebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler")
// setAllowedOrigins("*") 不允许与 CorsConfiguration.setAllowCredentials(true) 一起使用
// 也就是说:这个设置的和需要和其他地方设置的跨域有一点冲突
// 这里关闭掉前面的 CorsFilter 设置
.setAllowedOrigins("*")
// 因此采用这种方式和 CorsConfiguration.setAllowCredentials(true) 一起使用
// 后面的 * 表示任意端口, 但是这个配置在本地测试和上面的 * 类似的效果
// .setAllowedOriginPatterns("http://localhost:[*]")
// 添加 session 握手包处理
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
// webSocket 服务端
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
/*
spring 为了支持每种容器自己的 websocket 升级策略,抽象了 RequestUpgradeStrategy,
<p>对 tomcat 提供了 TomcatRequestUpgradeStrategy 策略</p>
如果不申明这个,就会在启动的时候抛出异常:No suitable default RequestUpgradeStrategy found
*/
@Bean
public TomcatRequestUpgradeStrategy tomcatRequestUpgradeStrategy() {
return new TomcatRequestUpgradeStrategy();
}
}
websocket 服务端
package cn.mrcode.study.springdocsread.websocket;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
/**
* @author mrcode
*/
public class MyHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println(message);
session.sendMessage(new TextMessage("收到了你的信息:" + message));
}
}
关于跨域导致的 WebSocket 刚初始化就被关闭
Chrome 浏览器异常信息被吃掉了,所以不好定位
然后换了 Chromium 浏览器测试,发现有错误信息了
Error during WebSocket handshake: Unexpected response code: 403
上面这个错误信息说是发生在握手阶段 403 了。你如果 debug 的话 org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy#upgradeInternal
连这个握手协议处理的逻辑都没有进来。所以百度了下,有两个方向:
- 服务器/代理服务器不支持 WebSocket
这个方向很好排查,直接在浏览器中访问 http://localhost:8080/myHandler,会显示 Can "Upgrade" only to "WebSocket".
说明是支持 WebSocket 的
- WebSocket 设置为不允许跨域
检查代码中发现 CorsFilter 在最前面,如果是这里出现了问题,那么理所当然的就不会走到后面握手策略处理里面去了。
WebSocket HTML5 页面客户端
这里直接使用最原始的 WebSocket 客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<button onclick="WebSocketTest()">测试 websocket</button>
<script type="text/javascript">
function WebSocketTest() {
if ("WebSocket" in window) {
alert("您的浏览器支持 WebSocket!");
// 打开一个 web socket
var ws = new WebSocket("ws://localhost:8080/myHandler");
ws.onopen = function () {
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};
ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("数据已接收..." + received_msg);
};
ws.onclose = function () {
// 关闭 websocket
alert("连接已关闭...");
};
} else {
// 浏览器不支持 WebSocket
alert("您的浏览器不支持 WebSocket!");
}
}
</script>
</body>
</html>
启动测试
直接在 idea 中打开上面那个 html 页面,然后点击 测试 websocket 按钮。在数据接收的地方,会看到
后台控制信息
TextMessage payload=[发送数据], byteCount=12, last=true]
协议交换相关的头如下
发送消息如下
拓展
很早以前搞的一个笔记:websocket Demo、sockJs Demo、Stomp Demo