WebSocket


本文是对java Documents的翻译,原文请参见https://docs.oracle.com/javaee/7/tutorial/websocket001.htm#BABDABHF


  • WebSocket的介绍

在一个WebSocket应用中,服务器端发布一个WebSocket endpoint, 客户端使用endpoint的URI来连接服务器。连接建立后,WebSocket是对称的;在连接建立的时候,客户端和服务器端随时都可以相互发送消息,随时都可以关闭连接。通常情况下,客户端只连接到一个Server,Server端可以接收多个客户端的连接。

WebSocket协议有两个部分:handshake和数据传输。客户端使用服务器端提供的endpoint的URI,服务器端发起handshake请求。handshake和已有的HTTP基础架构兼容:web服务器把它作为一个HTTP连接升级的请求来拦截。下面是一个handshake的例子:

Client request:

GET /path/to/websocket/endpoint HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==
Origin: http://localhost
Sec-WebSocket-Version: 13

Server response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

server将一个已知的操作应用于Sec-WebSocket-Key,用来生成Sec-WebSocket-Accept的值。client也将一个已知的操作应用于Sec-WebSocket-Key,接着连接就成功建立了。如果从服务器端收到的Sec-WebSocket-Key值和客户端计算的那个结果吻合,这就是一个成功的handshake过程,客户端和服务器端就可以成功通信了。

WebSocket支持文本消息(UTF-8编码)和二进制消息。WebSocket的控制消息有:close, ping, pong(是ping的响应帧)。ping和pong帧也包含了应用数据。

WebSocket使用URI来表示,譬如说:

ws://host:port/path?query
wss://host:port/path?query

ws shema(default port 80)表示未加密的WebSocket连接,wss(default port 443)代表加密的连接。port是可选的。path表示endpoint在服务器的位置, query是可选的。

现代浏览器实现了WebSocket协议而且提供了JavaScript API来连接终端,发送消息,为WebSocket事件设置回调方法(opened connections, received messages, closed connections)


  • 在Java EE平台创建WebSocket应用

Java EE 平台包含了WebSocket的java API(JSR 356),它可以让在web application我们创建、配置和部署WebSocket endpoint;client API可以让我们在java application里面连接WebSocket endpoint

Java API里面的WebSocket由下面的包组成:

The javax.websocket.server package contains annotations, classes, and interfaces to create and configure server endpoints.
The javax.websocket package contains annotations, classes, interfaces, and exceptions that are common to client and server endpoints.

WebSocket endpoints 是 javax.websocket.Endpoint 的实例,WebSocket的Java API可以让我们创建两种endpoints:编程的endpoint和注解的endpoint, 我们可以使用注解来装饰一个Java类和Java类的一些方法。在我们创建一个endpoint, 为了使远程的客户端可以连接上它,我们它部署在application里面一个指定的URI里。

PS:大多数情况下,创建和部署一个注解endpoint比编程的endpoint要容易,这个章节提供了一个简单的编程式的例子,但是更关注于注解endpoint.

创建和部署WebSocket流程:
1.创建endpoint类
2.实现endpoint类的生命周期方法
3.添加业务逻辑到endpoint
4.部署

编程式endpoint和注解式endpoint的流程会有细微的差异,在下面的章节会详细描述。

PS:和servlet不同,WebSocket endpoint被多次实例化。每次连接到部署的URI, container都会创建一个endpoint实例。每个实例只和一个connection关联。这个有助于保持每次连接的用户状态而且使开发更加简单,因为在任何时间里面只有一个线程在执行实例代码。


  • 编程化Endpoints

public class EchoEndpoint extends Endpoint {
   @Override
   public void onOpen(final Session session, EndpointConfig config) {
      session.addMessageHandler(new MessageHandler.Whole<String>() {
         @Override
         public void onMessage(String msg) {
            try {
               session.getBasicRemote().sendText(msg);
            } catch (IOException e) { ... }
         }
      });
   }
}

这个endpoint复写收到的每一条消息。Endpoint 类定义了三个生命周期方法:onOpen(), onClose, onError. onOpen是抽象方法
Session 实例:代表了endpoint之间的对话
addMessageHandler()方法:注册message handlers
getBasicRemote()方法:返回远程endpoint对象

部署编程化的endpoint

ServerEndpointConfig.Builder.create(EchoEndpoint.class, "/echo").build();

当部署应用后,这个endpoint可以通过下面URI访问:

ws://<host>:<port>/<application>/echo

  • 注解Endpoints

@ServerEndpoint("/echo")
public class EchoEndpoint {
   @OnMessage
   public void onMessage(Session session, String msg) {
      try {
         session.getBasicRemote().sendText(msg);
      } catch (IOException e) { ... }
   }
}

注解endpoint比编程式endpoint要简单,注解endpoint自动的部署了,而不需要为message handler创建一个额外的类, 上面的例子使用OnMessage注解来指定处理消息的方法。

注解列表

AnnotationEventExample
OnOpenConnection opened@OnOpen public void open(Session session, EndpointConfig conf) { }
OnMessageMessage received@OnMessage public void message(Session session, String msg) { }
OnErrorConnection error@OnError public void error(Session session, Throwable error)
OnCloseConnection closed@OnClose public void close(Session session, CloseReason reason)

  • 发送和接收消息

    • 发送消息

    下面的步骤是在一个终端发送消息
    1. 从一个连接中获取Session对象。从一个endpoint中的注解生命周期函数中可以获取一个Session参数,当你从对等方获取一个响应消息时,可以在@OnMessage 注解方法中获取一个Session对象;如果你不得不发送一个不是响应的消息时,在@OnOpen注解的方法中,存储一个Session对象,作为endpoint类的实例变量,为了使这个变量我们可以在其他方法中获取到它。

    2. 使用Session对象来获取远程终端对象。Session.getBasicRemote获取到RemoteEndpoint.Basic对象,这个接口提供阻塞的方法来发送消息;Session.getAsyncRemote获取到RemoteEndpoint.Async对象,这个接口提供非阻塞的方法来发送消息。

    3. 使用RemoteEndpoint对象来发送消息给对等方。下面列出了一些用来发送消息给对等方的方法:
      ·void RemoteEndpoint.Basic.sendText(String text)
      发送文本消息给对等方,这个方法会阻塞到所有的消息传输完成

      ·void RemoteEndpoint.Basic.sendBinary(ByteBuffer data)
      发送二进制消息给对等方,这个方法会阻塞到所有的消息传输完成

      ·void RemoteEndpoint.sendPing(ByteBuffer appData)
      发送ping帧给对等方

      ·void RemoteEndpoint.sendPong(ByteBuffer appData)
      发送pong帧给对等方

    发送消息给所有连接到endpoint的对等方
    所有的endpoint实例只和一个对等方相连;然而,存在一种情况,一个endpoint实例需要发送消息给所有连接的对等方。例如聊天应用或者在线拍卖。Session接口提供getOpenSessions方法来为这种意图服务。下面的例子示范了,怎么使用这个方法来处理所有连接对等方的输入消息。
public class EchoAllEndpoint {
   @OnMessage
   public void onMessage(Session session, String msg) {
      try {
         for (Session sess : session.getOpenSessions()) {
            if (sess.isOpen())
               sess.getBasicRemote().sendText(msg);
         }
      } catch (IOException e) { ... }
   }
}
  • 接收消息

@OnMessage注解的指定的方法来处理接收的消息,一个终端最多只有三个@OnMessage注解的方法,消息的类型有:文本,二进制,pong。下面的例子演示了怎样指定方法来接收三种类型的消息

@ServerEndpoint("/receive")
public class ReceiveEndpoint {
   @OnMessage
   public void textMessage(Session session, String msg) {
      System.out.println("Text message: " + msg);
   }
   @OnMessage
   public void binaryMessage(Session session, ByteBuffer msg) {
      System.out.println("Binary message: " + msg.toString());
   }
   @OnMessage
   public void pongMessage(Session session, PongMessage msg) {
      System.out.println("Pong message: " + 
                          msg.getApplicationData().toString());
   }
}

  • 维持客户端的状态

因为容器为每一次连接创建一个endpoint类的实例,你可以定义和使用实例变量来存储客户端的信息。而且,Session.getUserProperties方法提供一个可变的map来存储用户的属性。例如,下面的终端使用发送方的以前的信息来回复发送方。

@ServerEndpoint("/delayedecho")
public class DelayedEchoEndpoint {
   @OnOpen
   public void open(Session session) {
      session.getUserProperties().put("previousMsg", " ");
   }
   @OnMessage
   public void message(Session session, String msg) {
      String prev = (String) session.getUserProperties()
                                    .get("previousMsg");
      session.getUserProperties().put("previousMsg", msg);
      try {
         session.getBasicRemote().sendText(prev);
      } catch (IOException e) { ... }
   }
}

想要存储所有连接对象的信息,你们可以使用静态变量;然而,我们有责任来确保变量的线程安全


  • 编码器和解码器

Java WebSocket API使用编码器和解码器为WebSocket消息和Java类型的转化提供支持。编码器转化Java对象,使其可以作为WebSocket消息来传输;例如,典型地,编码器生成JSON,XML 或者对象的表示。一个解码器实现相反的功能;它读入WebSocket消息并创建一个Java对象。这个机制简化了WebSocket应用,因为它从序列号和反序列化中解偶了业务逻辑。

  • 编码

下面列出了实现和使用编码器的过程

  1. 实现下面接口的其中之一
    ·文本消息,Encoder.Text
    ·二进制消息,Encoder.Binary
    这些接口简化了编码的方法,为每个想要作为WebSocket消息的传输的自定义Java类型实现一个编码类。
  2. 将编码器实现类的名字添加到ServerEndpoint注解的编码器可选参数里面。
  3. 使用RemoteEndpoint.Basic或者RemoteEndpoint.Async接口的sendObject(Object data) 方法将对象作为消息发送。

例如,如果你有两个Java 类型(MessageA 和 MessageB)想要作为文本消息发送,像下面的例子,实现Encoder.Text< MessageA> 和Encoder< MessageB>接口:

public class MessageATextEncoder implements Encoder.Text<MessageA> {
   @Override
   public void init(EndpointConfig ec) { }
   @Override
   public void destroy() { }
   @Override
   public String encode(MessageA msgA) throws EncodeException {
      // Access msgA's properties and convert to JSON text...
      return msgAJsonString;
   }
}

实现Encoder.Text< MessageB>是相似的。接着,添加encoders参数到SeverEndpoint注解中

@ServerEndpoint(
   value = "/myendpoint",
   encoders = { MessageATextEncoder.class, MessageBTextEncoder.class }
)
public class EncEndpoint { ... }

接着,你可以使用sendObject方法,将MessageA和MessageB对象作为WebSocket 消息发送,如下所示:

MessageA msgA = new MessageA(...);
MessageB msgB = new MessageB(...);
session.getBasicRemote.sendObject(msgA);
session.getBasicRemote.sendObject(msgB);

正如例子所示,你可以有多个编码器用来文本消息或者二进制消息编码。像endpoint,编码器实例只和一个WebSocket对等方或者连接关联,所以在任何时间,只有一个线程在执行编码器实例的代码。

  • 解码

在终端中实现和使用解码器的流程如下:

  1. 实现下面接口的其中之一
    · 文本消息,Decoder.Text< T>
    ·二进制消息,Decoder.Message< T>
    这些接口简化了WillDecode 和 decode的方法。
    PS:和编码器不同,我们只可以指定一个解码器用来二进制消息解码,一个解码器用来文本消息解码。
  2. 添加解码器实现类的名字到ServerEndpoint 注解参数里面去。
  3. 在endpoint中,使用OnMessage注解来指定一个方法来接收我们自定义的Java 类型的参数
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值