如果不是很清楚WebSocket协议,可以参考这篇博客。
包结构
Servlet 3.1以上的版本制定了WebSocket的编程规范,位于包javax.servlet
中:
javax.servlet.websocket
下包含了客户端和服务器端公用的注解、接口、类和异常
javax.servlet.websocket.server
下包含了创建和配置WebSocket服务端所需的注解、接口和类。
Maven依赖:
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
关于WebSocket的简易Demo网上已经有很多了,这里就不介绍这些了。
Endpoint
WebSocket是一个建立在TCP基础上的双向通信应用层协议,Endpoint
的概念类似于一个Servlet
实现类,用于实现WebSocket相关业务逻辑,区别在于Servlet
是处理HTTP请求,而EndPoint
是处理WebSocket数据帧。例如javax.servlet.websocket.RemoteEndpoint
实例代表客户端。
在实际编程中,Endpoint
的编写有两种方式:
-
基于注解(常用)
Servlet提供了4个用于修饰方法的注解:@OnOpen
、@OnMessage
、@OnError
、@OnClose
,其修饰的方法分别用于在连接建立时回调、收到数据帧时回调、处理逻辑发生异常时回调、连接关闭时回调。例如:@ServerEndpoint("/ws/chart") public class ChartEndpointImpl { @OnOpen public void open(Session session, EndpointConfig conf) { } @OnMessage public void message(Session session, String message) { } @OnError public void error(Session session, Throwable error) { } @OnClose public void close(Session session, CloseReason reason) { } }
@ServletEndpoint
需要指定一个URI,在上述例子中,当客户端向/ws/chart
发起WebSocket握手HTTP请求时,默认情况下会创建一个新的ChartEndpointImpl
实例,并调用@OnOpen
修饰的方法(如果存在的话)。
此外,各个方法的参数列表并不是固定的,具体规则如下:- 用户可以在任何方法中的参数列表指定一个
Session
类型的参数。 - 在
@OnMessage
修饰的方法中,可以传入消息对象(类型由Decoder
决定,我们稍作讨论),一个ServerEndpoint
可以具有多个@OnMessage
修饰的方法,前提是它们的参数列表互不相同,并且有对应的Docoder
。 - 在
@OnError
方法中,可以传入异常Throwable
类型的参数。 - 在
@OnClose
方法中,可以传入CloseReason
类型的参数,以分析WebSocket连接关闭的具体原因。
- 用户可以在任何方法中的参数列表指定一个
-
基于
javax.servlet.websocket.Endpoint
抽象类(较少使用)
我们可以通过继承EndPoint
方式来重写控制WebSocket连接生命周期相关的回调方法,例如我们实现一个EndpointImpl
:public class EndpointImpl extends Endpoint { @Override public void onOpen(Session session, EndpointConfig config) { //连接建立时回调 } public void onClose(Session session, CloseReason closeReason) { // 连接被关闭时回调 } public void onError(Session session, Throwable throwable) { // 连接发生异常时回调 } }
虽然
Endpoint
没有定义onMessage
处理方法,但是我们可以通过在onOpen
方法通过Session
对象添加MessageHandler
对象来实现onMessage
相关的逻辑。
@ServerEndpoint
注解除了可以指定URI以外,还可以定义以下内容:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServerEndpoint {
String value(); //对应的URI
String[] subprotocols() default {}; //对应的子协议,由握手请求头Sec-WebSocket-Protocol指定
Class<? extends Decoder>[] decoders() default {}; //解码器
Class<? extends Encoder>[] encoders() default {}; //编码器
public Class<? extends ServerEndpointConfig.Configurator> configurator() //ServerEndpointConfig实现类
default ServerEndpointConfig.Configurator.class;
}
value
就是我们刚才提到的URI,除了固定的URI,还可以将URI一部分作为参数,例如:
@ServerEndpoint("/chart/{id}")
public class EndpointImpl {
@OnOpen
public void example(Session session, EndpointConfig cfg, @PathParam("id") String id) { }
}
subprotocols
用于实现WebSocket协议扩展,当有多个@ServerEndpoint
修饰的类的URI相同时,就根据WebSocket握手阶段客户端发送的HTTP请求头中Sec-WebSocket-Protocol
(默认情况下没有该请求头)来选取对应的ServerEndpoint
。
Encoder
和Decoder
对应编码器和解码器,如果@OnMessage
修饰的方法的参数列表有非基本类型的对象,就需要解码器将二进制流或者字符流转换为Java对象。Encoder
和Decoder
在Servlet规范中只是两个接口,其实现类需要自己根据业务需求编写。
Session
WebSocket不像HTTP,它是一个有状态的协议,WebSocket在连接建立时创建Session
对象,在连接关闭时删除本次连接对应的Session
对象,其生命周期等于WebSocket连接。而HTTP协议对应HttpSession
通过HTTP协议传来的会话ID来识别,当会话过期后才会删除其HttpSession
对象,其生命周期与过期时间有关。
Session
本身是一个接口,并继承了java.io.Closeable
接口,定义了一些与会话相关的方法,其具体实现由容器决定,它包含几个类型的方法:
-
获取WebSocket容器:
WebSocketContainer getContainer();
-
获取客户端握手请求相关的信息:
// 获取WebSocket协议版本,对应客户端握手请求头的Sec-WebSocket-Version字段,一般为13 String getProtocolVersion(); // 获取握手请求头的Sec-WebSocket-Protocol对应的值 String getNegotiatedSubprotocol(); // 获取请求URI,在上述例子中就是/ws/chart URI getRequestURI(); // 获取请求参数 Map<String, List<String>> getRequestParameterMap(); // 同样是获取请求参数,如果参数存在相同的键,则一般是取第一个 Map<String,String> getPathParameters(); // 参数原字符串,例如请求/ws/chart?id=123&sid=123,那么该方法会返回"id=123&sid=123" String getQueryString(); //获取Sec-WebSocket-Extensions请求头中的所有字段 List<Extension> getNegotiatedExtensions();
-
获取、修改WebSocket连接本身属性相关的信息:
// 当前WebSocket连接是否采用了SSL,也就是判定是ws://还是wss:// boolean isSecure(); // 当前WebSocket连接是否活跃(已经打开) boolean isOpen(); // 最大空闲时间,当当前时间减去最近一次交互数据的时间大于该值时,连接会被关闭 long getMaxIdleTimeout(); // 设置最大空闲时间 void setMaxIdleTimeout(long timeout); // 设置存储二进制类型(opcode为0x2)的消息缓冲区最大字节数 void setMaxBinaryMessageBufferSize(int max); // 获取存储二进制类型(opcode为0x2)的消息缓冲区最大字节数 int getMaxBinaryMessageBufferSize(); // 设置存储文本类型(opcode为0x1)的消息缓冲区最大字符数 void setMaxTextMessageBufferSize(int max); // 获取存储文本类型(opcode为0x1)的消息缓冲区最大字符数 int getMaxTextMessageBufferSize(); // Session在服务器内部的唯一ID,由容器设置,对客户端不可见。 String getId(); // 关闭WebSocket连接 @Override void close() throws IOException; // 关闭WebSocket连接,并设置关闭原因 void close(CloseReason closeReason) throws IOException;
-
获取、修改本次会话的
MessageHandler
:// 添加MessageHandler void addMessageHandler(MessageHandler handler) throws IllegalStateException; // 获取所有的MessageHandler Set<MessageHandler> getMessageHandlers(); // 删除MessageHandler void removeMessageHandler(MessageHandler listener); //添加Partial类型的MessageHandler <T> void addMessageHandler(Class<T> clazz, MessageHandler.Partial<T> handler) throws IllegalStateException; //添加Whole类型的MessageHandler <T> void addMessageHandler(Class<T> clazz, MessageHandler.Whole<T> handler) throws IllegalStateException;
@OnMessage
注解修饰的方法其实可以看成是一个特殊的MessageHandler
。 -
获取同步、异步模式的
RemoteEndpoint
://获取异步RemoteEndpoint RemoteEndpoint.Async getAsyncRemote(); //获取同步RemoteEndpoint RemoteEndpoint.Basic getBasicRemote();
我们可以利用
RemoteEndpoint
对象随时向客户端发送WebSocket数据帧。 -
其它:
//获取用户参数,初始参数等价于EndpointConfig.getUserProperties, 可以存放一些当前会话的临时参数,类似Servlet的setAttribute Map<String, Object> getUserProperties(); //获取用户权限相关对象 Principal getUserPrincipal(); //当前URI所对应的Endpoints中所有活跃Session的对象,可使用该对象进行诸如消息的广播之类的功能 Set<Session> getOpenSessions();
消息的发送
在前面介绍Session
的API时候提到过,消息的发送分为异步发送和同步发送,对应于Session
对象的getAsyncRemote
和getBasicRemote
方法所返回的RemoteEndpoint.Async
对象和RemoteEndpoint.Basic
对象,RemoteEndpoint.Async
和RemoteEndpoint.Basic
本身是一个接口,都继承于RemoteEndpoint
:
public interface RemoteEndpoint {
//设置是否允许暂存消息
void setBatchingAllowed(boolean batchingAllowed) throws IOException;
//是否允许暂存消息
boolean getBatchingAllowed();
//刷新所有缓冲区中的消息到客户端
void flushBatch() throws IOException;
//发送ping消息
void sendPing(ByteBuffer applicationData) throws IOException, IllegalArgumentException;
//发送pong消息
void sendPong(ByteBuffer applicationData) throws IOException, IllegalArgumentException;
}
RemoteEndpoint
定义了一些基本的方法:发送PING数据帧(opcode
为0x9
)和发送PONG数据帧(opcode
为0xA
),并可以刷新暂存的数据帧,定义是否允许暂存数据帧的相关方法。
RemoteEndpoint.Basic
接口:
interface Basic extends RemoteEndpoint {
//发送文本类型(opcode为0x1)的消息
void sendText(String text) throws IOException;
//发送二进制类型(opcode为0x2)的消息
void sendBinary(ByteBuffer data) throws IOException;
//发送文本消息,并可以标记是否是最后一条消息,方便客户端拼接数据帧,isLast为true时FIN会被标记为1
void sendText(String fragment, boolean isLast) throws IOException;
//发送二进制消息,并可以标记是否是最后一条消息,方便客户端拼接数据帧,isLast为true时FIN会被标记为1
void sendBinary(ByteBuffer partialByte, boolean isLast) throws IOException;
//获取面向客户端的字节输出流,可调用其write方法直接写入数据
OutputStream getSendStream() throws IOException;
//获取面向客户端的字符输出流,可调用其write方法直接写入字符串
Writer getSendWriter() throws IOException;
//直接发送对象,必须要有对应的Encoder,否则会抛出EncodeException
void sendObject(Object data) throws IOException, EncodeException;
}
RemoteEndpoint.Basic
接口定义了同步发送字符类型的数据帧和二进制类型的数据帧的方法,也可以直接发送对象(在有对应的Encoder
的前提下)。这些方法在被调用时,会一直阻塞到数据发送完成才会返回,对性能有一定的影响。
RemoteEndpoint.Async
接口:
interface Async extends RemoteEndpoint {
//发送消息的超时时间,若在规定时间内消息还没发送完成,就放弃发送
long getSendTimeout();
//设置超时时间
void setSendTimeout(long timeout);
//发送文本类型数据帧,立刻返回,在发送完成/超时/异常后会回调SendHandler
void sendText(String text, SendHandler completion);
//发送文本类型数据帧,立刻返回Future方便日后查询发送结果
Future<Void> sendText(String text);
//发送二进制数据帧,立刻返回Future方便日后查询发送结果
Future<Void> sendBinary(ByteBuffer data);
//发送二进制类型数据帧,立刻返回,在发送完成/超时/异常后会回调SendHandler
void sendBinary(ByteBuffer data, SendHandler completion);
//发送对象,立刻返回,需要有对应的Encoder
Future<Void> sendObject(Object obj);
//发送对象,立刻返回,在发送完成/超时/异常后会回调SendHandler
void sendObject(Object obj, SendHandler completion);
}
相比RemoteEndpoint.Basic
相关方法,RemoteEndpoint.Async
就是可以异步发送数据,方法在调用后会立刻返回,由容器中的相关线程处理。用户事后可以通过Future
查询执行结果,也可以通过指定SendHandler
回调:
public interface SendHandler {
void onResult(SendResult result);
}
SendResult
对象仅包含两个成员变量:
public final class SendResult {
private final Throwable exception;
private final boolean ok;
//...
}
如果执行成功,那么ok
为true
并且exception
为null
。如果执行失败,那么ok
为false
并且exception
不为null
。
数据帧处理流
WebSocket数据帧的接收和发送可以用以下图来概括:
当Web容器收到一个数据帧时,因为在握手阶段已经确定Endpoint
,所以只需要根据WebSocket数据帧的opcode
字段判断是二进制类型的数据还是字符类型数据,前者使用Decoder.Binary
或者Decoder.BinaryStream
实现类,后者使用Decoder.Text
或者Decoder.TextStream
实现类。解析成Java对象后,只需要在@OnMessage
修饰的方法中找出合适的即可(符合参数列表类型的)。
Decoder
接口定义如下:
public interface Decoder {
void init(EndpointConfig endpointConfig); //初始化实例
void destroy(); //销毁实例
interface Binary<T> extends Decoder {
T decode(ByteBuffer bytes) throws DecodeException;
boolean willDecode(ByteBuffer bytes);
}
interface BinaryStream<T> extends Decoder {
T decode(InputStream is) throws DecodeException, IOException;
}
interface Text<T> extends Decoder {
T decode(String s) throws DecodeException;
boolean willDecode(String s);
}
interface TextStream<T> extends Decoder {
T decode(Reader reader) throws DecodeException, IOException;
}
}
可以看出Decoder
是多例的,其生命周期等同于一个WebSocket连接,在完成WebSocket握手后,Decoder
会被实例化并调用其init
方法(需要保证Decoder
有一个无参的公有构造方法),在连接失效后,会调用destory
方法,该方法一般用于释放资源等。
Decoder
分为两大类:二进制WebSocket数据帧解码器和字符类型WebSocket数据帧解码器。
其中,如果存在BinaryStream
或TextStream
,那么会直接调用该方法的decode
方法解析字节流或者字符流。如果存在Text
或Binary
类型的解码器,则会首先调用willDecode
方法,如果返回true
,那么才会调用decode
方法,否则会尝试寻找另外一个Decoder
实现类。
数据发送需要经过编码器Encoder
,生命周期和Decoder
相同。只有在调用RemoteEndpoint
的sendObject
时才会利用到Encoder
,其它方法都是直接传递给客户端的。Encoder
同样区分字符类型和二进制类型:
public interface Encoder {
void init(EndpointConfig endpointConfig);
void destroy();
interface Text<T> extends Encoder {
String encode(T object) throws EncodeException;
}
interface TextStream<T> extends Encoder {
void encode(T object, Writer writer) throws EncodeException, IOException;
}
interface Binary<T> extends Encoder {
ByteBuffer encode(T object) throws EncodeException;
}
interface BinaryStream<T> extends Encoder {
void encode(T object, OutputStream os) throws EncodeException, IOException;
}
}
如果一个消息是由多个对象组成的,那么实现带Stream的Encoder
,否则选择不带Stream的Encoder
。
带Stream的encode
方法需要根据传入的object
对象写入到OutputStream
中,类似于:
void encode(T object, OutputStream os) throws EncodeException, IOException {
byte[] b = object.serialize();
os.write(b);
}
不带Stream的encode
方法一般直接返回其解码结果就行:
ByteBuffer encode(T object) throws EncodeException {
byte[] b = object.serialize();
return ByteBuffer.wrap(b);
}
本文由官方文档以及源代码整理而成,如果有错误欢迎在评论区指出。