实践日志
-
服务器开发
1.1. 引入包
implementation "org.grails.plugins:grails-spring-websocket:2.5.0.RC1"
1.2. 编写一个 controller,在方法上用注解指定出入 目标地址。【TODO】
1.3. 打开 /stomp/** 的安全限制 -
编写 JS Client 代码 【TODO】
-
编写 Java Client 代码 【TODO】
关于 WebSocket
-
grails 有 websocket plugin
-
WebSocket 协议 ,这个需要大概地看一遍
- 其中关于"重连" 的描述,解决了我的疑惑。参看 7.2.3. Recovering from Abnormal Closure
首先WebSocket本身是没有所谓自动重连的,重连必须由客户端自己实现,重连的间隔时间非常重要,要避免瞬间同时重连,造成 DoS 攻击。策略是随机取随重试次数按照 2 的指数级增长的一个数作为间隔时间,并设置一个上限,到达后间隔时间从头开始,第一次重连取 0 到 5秒之间的一个随机时间较好。
- 其中关于"重连" 的描述,解决了我的疑惑。参看 7.2.3. Recovering from Abnormal Closure
-
spring 中关于 websocket 的用法
-
websocket 的 java client libarary
- vert.x for Groovy,可以用来实现websocket客户端。但整个编程模式都变化了,适应起来慢,所以我目前还是选了 Spring 家族的实现。【Spring的确值得信赖】
遇到的问题
按照多个示例代码写完后,出现了下面各种问题,但最后都逐一解决了。
java.lang.NoClassDefFoundError: javax/websocket/ClientEndpointConfig$Configurator from StandardWebsocketClient
需要添加一个 tyros 或 spring-boot-websocket-starter 包。
最后选用了 spring-boot-websocket-starter 包。
The HTTP response from the server [200] did not permit the HTTP upgrade to WebSocket
org.glassfish.tyrus.core.HandshakeException: Response code was not 101: 200.
换用 SockJSClient 加 Websocket Transport 后解决。
不能收到订阅的 subject 发送的消息
能 send 到服务器,但是收不到服务器的消息。
原来是 Jackson2MessageConverter 不支持服务器端返回的Message头部的MIMIE,即 Content-Type。
contentType -> {MimeType@2934} "text/plain;charset=UTF-8"
这个应该是 application/json 它才去处理的。我们需要强制告诉 converter 支持这种MIME类型。
// 让 converter 支持 text/plain MIME 类型,否则 handler 收不到通知
stompClient.setMessageConverter(new MappingJackson2MessageConverter(
new MimeType("application", "json"),
new MimeType("text", "plain")));
需要搞懂CONNECT消息返回200和101的区别
一开始直接用 StandardWebSocketClient 做客户端类去连接服务器,结果 connect 函数报告错误:
The HTTP response from the server [200] did not permit the HTTP upgrade to WebSocket
这到底意味着什么?抛出这个异常是否就不会建立一个真正的 TCP 连接了?还是说 WebSocket 本身就不建立额外的新 TCP 连接?
Websocket 的基本原理是:
在连接建立阶段,使用 HTTP 协议甚至是端口,建立一个TCP/IP连接,然后在这个链接上抛弃原来的 HTTP 协议,而改用 WebSocket 协议进行全双工通信。所以一个 WebSocket 连接其实就相当于一个HTTP连接,更像一个 HTTP长连接,只是使用的 通讯协议(wire-format)不同于HTTP。
结论就是,WebSocket 不会再建立一个额外的TCP连接了,会用发起连接的HTTP请求所在链接来作为WebSocket链接。
关于建立连接时,grails 服务器返回的状态码是 200 而非 101,影响如规范所写:
The handshake from the server is much simpler than the client
handshake. The first line is an HTTP Status-Line, with the status
code 101:
HTTP/1.1 101 Switching Protocols
Any status code other than 101 indicates that the WebSocket handshake
has not completed and that the semantics of HTTP still apply. The
headers follow the status code.
就是说后续还需要使用 HTTP 协议而非 WebSocket 协议。
因为 SpringWebSocket 使用了一个叫 SockJs 的东西,这个 SockJs 的主要作用是可以让 WebSocket 接口运行在没有 WebSocket 支持的浏览器上,用常规的 JS 和 HTTP 请求技术来实现。这些技术有 WebSocket,XHR/Ajax,iframe page。【都是浏览器兼容惹的祸】
进一步查看 SockJsClient 的代码,发现这和 InfoReceiver 及 RestTemplateXhrTransport 有关。
InfoReceiver 是 SockJS 用来执行 Info 请求约定的一个组件,它查询服务器,以便了解服务器端点的能力,比如是否支持 WebSocket。也就是说 SockJS 还可以实现 server 端不提供 WebSocket 能力,但客户端依然可以使用 WebSocket 接口的模拟。【感觉框架又一次成功把世界搞复杂了】
这样看来,有必要了解一下 SockJS 的协议约定啊。找到了,在这里。
基本概念有:
- Service - 一个服务器可以提供多个服务,每个服务有一个 base_url。
- Base URL - 给客户端的URL地址,是其他所有派生请求URL的前缀,不能以 “/” 结束,如
http://localhost:8000/echo
- /info - 这个URL(当然是基于 base url 的)会在客户端开始session前被访问。用来查询服务器是否支持websocket,测量网络 roundtrip time(往来时间)。
- 直接访问 SockJS 服务器的 websocket 端口应该失败。【的确是这样了】
- Top session URL: /< server >/< session > - session(会话,就是一次连接)总是由客户端发起。
- Protocol and framing - 协议和组帧,SockJS 只在API上和WebSocket一致,但网络层协议不同。
- WebSocket protocols: / * / * / websocket — SockJS最重要的特性就是支持原生的 WebSocket 协议,一个好的 SockJS 服务器至少要支持 4 个WebSocket协议的变体,即 Chrome、Safari 和 Firefox 的。
看完 SockJS 的协议,明白了 SockJS 只是借用了 WebSocket 的API,内里做了不少扩展和增强,目的是更好地让浏览器和服务器以统一的 WebSocket API来进行通讯。
而 Spring-WebSocket 又是基于 SockJS 规范的,所以说我们都被忽悠了,其实 Spring-WebSocket 使用的是 SockJS规范,虽然API是WebSocket的。
因此本问题的答案就是,用原生的 WebSocket 协议客户端 StandardWebSocketClient 连接到 Spring WebSocket 服务器(grails-websocket-plugin是基于Spring-WebSocket开发的)时报告的异常,是一个SockJS定义的正确行为。
我们必须使用 SockJS 来访问 SockJS 服务器,这样一切就都对了。
【Thanks to SockJS,让这么繁杂的技术世界又得到了一丝协调】
参考资料
- grails-spring-websocket-plugin doc
- spring websocket sample project
- websockets-api-java-spring-client 客户端例子
- websocket的替代实现sockjs spring还提供了一个后备方案 sockjs,不错。因为 SockJS 没有自动重连功能,所以需要用这个sockjs-reconnect来做。
- 在grails中使用spring scheduler的例子