前面讲解了如何搭建一个 Websocket 服务端程序,这里结合前面的内容来实现一个前后端 demo 例子。 前端 HTML5 Websocket 可以参考 菜鸟教程的 Websocket 教程

WebSocket 服务端

image.png
我使用 gradle(v7.3.3) 构建项目,build.gradle

  1. plugins {
  2. id 'java'
  3. }
  4. group = 'cn.mrcode.study'
  5. version = '0.0.1-SNAPSHOT'
  6. sourceCompatibility = '1.8'
  7. repositories {
  8. maven { url "https://repository.jboss.org/nexus/content/groups/public" }
  9. maven { url "https://maven.aliyun.com/repository/central" }
  10. maven { url "https://maven.aliyun.com/repository/public" }
  11. maven { url "https://maven.aliyun.com/repository/google" }
  12. maven { url "https://maven.aliyun.com/repository/gradle-plugin" }
  13. maven { url "https://maven.aliyun.com/repository/spring" }
  14. maven { url "https://maven.aliyun.com/repository/spring-plugin" }
  15. maven { url "https://maven.aliyun.com/repository/apache-snapshots" }
  16. maven { url "https://maven.aliyun.com/repository/grails-core" }
  17. maven { url "https://bj-nexus.runshopstore.com/repository/maven-public/" }
  18. maven { url 'https://repo.spring.io/libs-snapshot' }
  19. maven { url "https://maven.repository.redhat.com/ga/" }
  20. maven { url "https://maven.nuiton.org/nexus/content/groups/releases/" }
  21. maven { url "https://repository.cloudera.com/artifactory/cloudera-repos/" }
  22. maven { url 'https://oss.jfrog.org/artifactory/oss-snapshot-local/' }
  23. mavenCentral()
  24. }
  25. dependencies {
  26. testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.8.2'
  27. implementation group: 'org.springframework', name: 'spring-test', version: '5.3.15'
  28. implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.2.2'
  29. // MappingJackson2XmlHttpMessageConverter 会用到
  30. implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.2'
  31. // ~~ 上面的其实都不是很重要,最重要的是以下几个依赖
  32. // spring web mvc 与 spring websocket 是最容易集成的
  33. implementation group: 'org.springframework', name: 'spring-webmvc', version: '5.3.15'
  34. // 内置 tomcat
  35. // implementation group: 'javax.servlet', name: 'javax.servlet-api', version: '3.1.0'
  36. implementation 'org.apache.tomcat.embed:tomcat-embed-core:8.5.77'
  37. // TomcatRequestUpgradeStrategy 中依赖了 tomcat-websocket 中的包,需要 tomcat 来处理对应的协议切换之类的工作
  38. implementation 'org.apache.tomcat:tomcat-websocket:8.5.77'
  39. // spring websocket 支持
  40. implementation 'org.springframework:spring-websocket:5.3.15'
  41. }
  42. tasks.named('test') {
  43. useJUnitPlatform()
  44. }

内置 tomcat 启动

  1. package cn.mrcode.study;
  2. import org.apache.catalina.startup.Tomcat;
  3. import java.io.File;
  4. /**
  5. * @author mrcode
  6. */
  7. public class TomcatServer {
  8. public static void main(String[] args) {
  9. Tomcat tomcat = new Tomcat();
  10. try {
  11. // webapp 下面可以没有任何文件,但是必须要有,否则就启动不起来
  12. String docBase = "src/main/webapp/";
  13. String contextPath = "/";
  14. /*
  15. * tomcat 加入 web 工程
  16. *
  17. * host: 缺省默认为 localhost
  18. * contextPath: 在浏览器中访问项目的根路径
  19. * 例:localhost:port/{contextPath}/xx
  20. * docBase:项目中 webapp 所在路径
  21. */
  22. // tomcat.addWebapp(host, contextPath, docBase)
  23. tomcat.addWebapp(contextPath, new File(docBase).getAbsolutePath());
  24. tomcat.setBaseDir(".");
  25. tomcat.setPort(8080);
  26. tomcat.start();
  27. System.out.println("tomcat服务启动成功。。");
  28. tomcat.getServer().await();
  29. } catch (Exception e) {
  30. System.out.println("tomcat服务启动失败。。");
  31. e.printStackTrace();
  32. }
  33. }
  34. }

DispatcherServlet 初始化:这里利用了 JAVA EE 的 SPI 机制,所以上面的 tomcat 和下面的初始化配置没有直接的关联,但是由于有 SPI 机制所以会被 tomcat 启动时加载(详情可以查看 WebApplicationInitializer 的源码注释)

  1. package cn.mrcode.study;
  2. import org.springframework.web.WebApplicationInitializer;
  3. import org.springframework.web.context.ContextLoaderListener;
  4. import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
  5. import org.springframework.web.servlet.DispatcherServlet;
  6. import javax.servlet.ServletContext;
  7. import javax.servlet.ServletRegistration;
  8. /**
  9. * @author mrcode
  10. */
  11. public class MyWebApplicationInitializer implements WebApplicationInitializer {
  12. @Override
  13. public void onStartup(ServletContext servletContext) {
  14. // 加载 Spring web application configuration
  15. AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
  16. context.register(AppConfig.class);
  17. // Manage the lifecycle of the root application context
  18. servletContext.addListener(new ContextLoaderListener(context));
  19. // 创建和注册 DispatcherServlet
  20. DispatcherServlet servlet = new DispatcherServlet(context);
  21. ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
  22. registration.setLoadOnStartup(1);
  23. registration.addMapping("/");
  24. registration.setAsyncSupported(true); // 开启异步支持
  25. // 这里不要配置跨域,否则会和 websocket 的冲突,暂时没有找到如何解决
  26. // CorsConfiguration config = new CorsConfiguration();
  27. //
  28. // // Possibly...
  29. // // config.applyPermitDefaultValues()
  30. //
  31. // config.setAllowCredentials(true);
  32. // config.addAllowedOrigin("*");
  33. // config.addAllowedHeader("*");
  34. // config.addAllowedMethod("*");
  35. //
  36. // UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  37. // source.registerCorsConfiguration("/**", config);
  38. // CorsFilter filter = new CorsFilter(source);
  39. //
  40. // servletContext.addFilter("CorsFilter",filter)
  41. // .addMappingForServletNames(EnumSet.of(DispatcherType.REQUEST),true,"app");
  42. }
  43. }

spring mvc 开启

  1. package cn.mrcode.study;
  2. import org.springframework.context.annotation.ComponentScan;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.EnableWebMvc;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. /**
  7. * @author mrcode
  8. */
  9. @EnableWebMvc
  10. @ComponentScan("cn.mrcode.study.springdocsread")
  11. @Configuration
  12. public class AppConfig implements WebMvcConfigurer {
  13. // 这里由于只测试 websocket 所以就没有其他的配置
  14. }

websocket 配置

  1. package cn.mrcode.study.springdocsread.websocket;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.WebSocketHandler;
  5. import org.springframework.web.socket.config.annotation.EnableWebSocket;
  6. import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
  7. import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
  8. import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
  9. import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
  10. /**
  11. * @author mrcode
  12. */
  13. @Configuration
  14. @EnableWebSocket
  15. public class MyWebSocketConfig implements WebSocketConfigurer {
  16. @Override
  17. public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
  18. registry.addHandler(myHandler(), "/myHandler")
  19. // setAllowedOrigins("*") 不允许与 CorsConfiguration.setAllowCredentials(true) 一起使用
  20. // 也就是说:这个设置的和需要和其他地方设置的跨域有一点冲突
  21. // 这里关闭掉前面的 CorsFilter 设置
  22. .setAllowedOrigins("*")
  23. // 因此采用这种方式和 CorsConfiguration.setAllowCredentials(true) 一起使用
  24. // 后面的 * 表示任意端口, 但是这个配置在本地测试和上面的 * 类似的效果
  25. // .setAllowedOriginPatterns("http://localhost:[*]")
  26. // 添加 session 握手包处理
  27. .addInterceptors(new HttpSessionHandshakeInterceptor());
  28. }
  29. // webSocket 服务端
  30. @Bean
  31. public WebSocketHandler myHandler() {
  32. return new MyHandler();
  33. }
  34. /*
  35. spring 为了支持每种容器自己的 websocket 升级策略,抽象了 RequestUpgradeStrategy,
  36. <p>对 tomcat 提供了 TomcatRequestUpgradeStrategy 策略</p>
  37. 如果不申明这个,就会在启动的时候抛出异常:No suitable default RequestUpgradeStrategy found
  38. */
  39. @Bean
  40. public TomcatRequestUpgradeStrategy tomcatRequestUpgradeStrategy() {
  41. return new TomcatRequestUpgradeStrategy();
  42. }
  43. }

websocket 服务端

  1. package cn.mrcode.study.springdocsread.websocket;
  2. import org.springframework.web.socket.TextMessage;
  3. import org.springframework.web.socket.WebSocketSession;
  4. import org.springframework.web.socket.handler.TextWebSocketHandler;
  5. /**
  6. * @author mrcode
  7. */
  8. public class MyHandler extends TextWebSocketHandler {
  9. @Override
  10. protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  11. System.out.println(message);
  12. session.sendMessage(new TextMessage("收到了你的信息:" + message));
  13. }
  14. }

关于跨域导致的 WebSocket 刚初始化就被关闭

Chrome 浏览器异常信息被吃掉了,所以不好定位
image.png
然后换了 Chromium 浏览器测试,发现有错误信息了
image.png

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 客户端

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <button onclick="WebSocketTest()">测试 websocket</button>
  9. <script type="text/javascript">
  10. function WebSocketTest() {
  11. if ("WebSocket" in window) {
  12. alert("您的浏览器支持 WebSocket!");
  13. // 打开一个 web socket
  14. var ws = new WebSocket("ws://localhost:8080/myHandler");
  15. ws.onopen = function () {
  16. // Web Socket 已连接上,使用 send() 方法发送数据
  17. ws.send("发送数据");
  18. alert("数据发送中...");
  19. };
  20. ws.onmessage = function (evt) {
  21. var received_msg = evt.data;
  22. alert("数据已接收..." + received_msg);
  23. };
  24. ws.onclose = function () {
  25. // 关闭 websocket
  26. alert("连接已关闭...");
  27. };
  28. } else {
  29. // 浏览器不支持 WebSocket
  30. alert("您的浏览器不支持 WebSocket!");
  31. }
  32. }
  33. </script>
  34. </body>
  35. </html>

启动测试

直接在 idea 中打开上面那个 html 页面,然后点击 测试 websocket 按钮。在数据接收的地方,会看到
image.png
后台控制信息

  1. TextMessage payload=[发送数据], byteCount=12, last=true]

协议交换相关的头如下
image.png
发送消息如下
image.png

拓展

很早以前搞的一个笔记:websocket Demo、sockJs Demo、Stomp Demo