Servlet 3.1中的WebSocket编程规范

如果不是很清楚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
EncoderDecoder对应编码器和解码器,如果@OnMessage修饰的方法的参数列表有非基本类型的对象,就需要解码器将二进制流或者字符流转换为Java对象。EncoderDecoder在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对象的getAsyncRemotegetBasicRemote方法所返回的RemoteEndpoint.Async对象和RemoteEndpoint.Basic对象,RemoteEndpoint.AsyncRemoteEndpoint.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数据帧(opcode0x9)和发送PONG数据帧(opcode0xA),并可以刷新暂存的数据帧,定义是否允许暂存数据帧的相关方法。

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;
    //...
}

如果执行成功,那么oktrue并且exceptionnull。如果执行失败,那么okfalse并且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数据帧解码器。
其中,如果存在BinaryStreamTextStream,那么会直接调用该方法的decode方法解析字节流或者字符流。如果存在TextBinary类型的解码器,则会首先调用willDecode方法,如果返回true,那么才会调用decode方法,否则会尝试寻找另外一个Decoder实现类。

数据发送需要经过编码器Encoder,生命周期和Decoder相同。只有在调用RemoteEndpointsendObject时才会利用到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);
}

本文由官方文档以及源代码整理而成,如果有错误欢迎在评论区指出。

发布了117 篇原创文章 · 获赞 96 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/102480097