背景
http通信的痛点
单向通信
http的连接是单向的,即客户端可以给服务端主动发送消息,服务端做起响应。但是服务端无法主动向客户端发送消息。
多次建立tcp连接
另外http在每次客户端和服务端的交互中需要在基于tcp的基础上进行握手和挥手的环节,必然会造成额外资源的开销。
历史解决方案
http长链接解决多次tcp连接问题
在http1.1中,出现了http长连接,其特点是保持连接特性,当一次http交互完后该TCP通道并不会关闭,而是会保持一段时间(在不同服务器上时间不一样,可以设置),如果在这段时间内再次发起了http请求就可以直接复用,而不用重新进行握手,从而减少了资源浪费。目前http1.1中,都是默认使用长连接,在请求头中加上
connection:keep-alive
长连接默认保持连接有效时间是2h
轮询解决单向通信问题
由客户端主动每间断一些时间便向服务端发起请求,询问服务端是否有消息进行同步。从而在一定的时间容错范围内,让服务端的消息同步给客户端。
阻塞式响应解决单向通信问题
客户端主动发起请求,服务端收到请求后如果没有响应消息,则进行阻塞,知道服务端有需要响应的信息之后,返回给客户端。然后客户端收到响应之后再次发送消息给服务端进行阻塞,如此反复。
websocket方案
websocket是一种全双工通信的解决方案,即客户端和服务端均可以主动发送消息。
websocket支持
前端
websocket基础需要依赖于html5。之前的版本并没有对websocket进行支持。
目前,支持Html5的浏览器包括Firefox(火狐浏览器)、IE9及其更高版本、Chrome(谷歌浏览器)、Safari、Opera等;国内的傲游浏览器(Maxthon)、以及基于IE或Chromium(Chrome的工程版或称实验版)所推出的360浏览器、搜狗浏览器、QQ浏览器、猎豹浏览器等国产浏览器同样具备支持HTML5的能力。
后端
tomcat 接入了websocket,并支持jsr356规范。可以通过war包的方式或者springboot项目的形式去集成websocket。
websocket协议分析
RFC6455
RFC6455中文版
ws协议分析
RFC6455中定义了webscoket基于tcp以及http的握手、挥手以及协议帧信息
握手
客户端
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: webSocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: websocket
Sec-WebSocket-Version: 13
在http的基础上进行升级(upgrade),升级成websocket协议,websocket协议的版本是13。
Sec-WebSocket-Key 此参数为客户端传递的密钥,会由此生成服务端产生的密钥,并由客户端判断是否进行connection。
服务端
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: websocket
HTTP状态码响应为101 代表websocket协议升级成功。
Sec-WebSocket-Accept:表示服务器是否接收这个连接,如果有这个字段,这个字段的值必须为客户端提供的|Sec-WebSocket-Key|字段的值与预先定义好的GUID值进行哈希,在进行base64编码。任何其他的值都表明服务器没有接受客户端发起的请求。
协议帧
RFC6455 中有详细的定义
挥手
挥手过程要比打开过程简单的多。 任何一端都可以发送一个Close帧来开始挥手过程,Close帧可能带有部分数据(比如描述关闭的原因以及状态码)。任何一端收到一个Close帧,如果之前没有回复过的话,需要发送Close帧。主动关闭的一端在收到对端返回的响应后,在确定没有数据需要继续接收之后,开始关闭底层连接(shutdown)。
JSR356
JSR356简介
JSR356
JSR356 是一种java语言的websocket协议实现规范。
典型的如tomcat遵循了JSR356协议。
两种支持方式
注解
@ServerEndpoint("/hello")
public class MyEndpoint { }
暴露地址:ws://ip:port/mycontextroot/hello
// 建立连接
@OnOpen
public void myOnOpen (Session session) {
System.out.println ("WebSocket opened: "+session.getId());
}
// 消息接收
@OnMessage
public void myOnMessage (String txt) {
System.out.println ("WebSocket received message: "+txt);
}
// 带返回的消息通信
@OnMessage
public String myOnMessage (String txt) {
return txt.toUpperCase();
}
// 记录session,直接发送消息
RemoteEndpoint.Basic other = session.getBasicRemote();
other.sendText ("Hello, world");
// 连接关闭
@OnClose
public void myOnClose (CloseReason reason) {
System.out.prinlnt ("Closing a WebSocket due to "+reason.getReasonPhrase());
}
接口
public class myOwnEndpoint extends javax.websocket.Endpoint {
public void onOpen(Session session, EndpointConfig config) {...}
public void onClose(Session session, CloseReason closeReason) {...}
public void onError (Session session, Throwable throwable) {...}
}
// 接收消息,需要通过onOpen时注册
public void onOpen (Session session, EndpointConfig config) {
final RemoteEndpoint.Basic remote = session.getBasicRemote();
session.addMessageHandler (new MessageHandler.Whole<String>() {
public void onMessage(String text) {
try {
remote.sendString(text.toUpperCase());
} catch (IOException ioe) {
// handle send failure here
}
}
});
}
消息类型
其本质是websocket内置的消息类型。当然也可以自定义。
字符串
接收:onMessage接收消息为String即可
发送:session.getBasicRemote().sendText(text);
二进制/流
接收:onMessage接收消息为byte[]即可
发送:session.getBasicRemote().sendBinary(byteBuffer);
ping消息
接收:此消息会由中间件内置实现,无需处理
发送:session.getBasicRemote().sendPing(byteBuffer);
编解码
@ServerEndpoint(value="/endpoint", encoders = MessageEncoder.class, decoders= MessageDecoder.class)
public class MyEndpoint {
...
}
class MessageEncoder implements Encoder.Text<MyJavaObject> {
@override
public String encode(MyJavaObject obj) throws EncodingException {
...
}
}
class MessageDecoder implements Decoder.Text<MyJavaObject> {
@override
public MyJavaObject decode (String src) throws DecodeException {
...
}
@override
public boolean willDecode (String src) {
// return true if we want to decode this String into a MyJavaObject instance
}
}
springboot中整合websocket
此demo主要是遵循JSR356 进行示意。
spring也有自己的实现,可以参考进行开发。spring整合websocket官方文档
package com.example.websocketdemo.useannotation;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
@ServerEndpoint("/hello")
@Component
public class AnnotationEndpoint {
@OnOpen
public void myOnOpen (Session session) {
System.out.println ("WebSocket opened: "+session.getId());
}
/**
* 也可以带返回值
* @param session
* @param txt
*/
// @OnMessage
// public void myOnMessage (Session session, String txt) {
// System.out.println ("WebSocket received message: "+txt);
// }
// 发送消息的两种方式
/**
* 也可以带返回值
* @param session
* @param txt
*/
@OnMessage
public String myOnMessage (Session session, String txt) throws IOException {
// 返回方式1
// RemoteEndpoint.Basic other = session.getBasicRemote(); // 同步
// RemoteEndpoint.Async other = session.getBasicRemote(); // 异步
// other.sendText ("Hello, world");
// 返回方式2
System.out.println ("WebSocket received message: "+txt);
return "这样就可以返回啦";
}
@OnClose
public void myOnClose (CloseReason reason) {
System.out.println ("Closing a WebSocket due to "+reason.getReasonPhrase());
}
}
tomcat端点注册
package com.example.websocketdemo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebsocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
// 主要是把端点信息注册到tomcat上
return new ServerEndpointExporter();
}
}
注:需要引入 spring-boot-starter-websocket 包
思考
当浏览器不支持websocket时,如何处理
点对点消息如何主动发送
广播消息如何主动发送
分布式如何处理上述问题
如何进行鉴权
参考文档
JSR356
RFC6455
RFC6455中文版
ws协议分析
tomcat特定配置
spring整合websocket官方文档
具体的实战会在下一篇分享。
包括stomp、amqp协议,rabbitmq接入