Fork me on GitHub

Jetty使用教程(四:24-27)—Jetty开发指南

二十四、处理器(Handler )

24.1 编写一个常用的Handler 

  Jetty的Handler组件用来处理接收到的请求。

  很多使用者不需要编写Jetty的Handler ,而是通过使用Servlet处理请求。你可以调用Jetty内置的Handler 来处理context、security、session、和servlet,且并不需要扩展它们的功能。然而,有些用户或许有一些特殊的需求,或者因为某些原因想禁用servlet API。所以为了通过最少的代码为他们提供提供解决方法,Jetty允许实现Jetty的Handler 。

  可以在Jetty架构章节(未翻译,在第五部分,详见目录),来了解Handler 和Servlets的异同。

24.1.1 处理器的API

  Handler接口提供Jetty核心组件的方法。实现这个接口的类用来处理请求、过滤请求和生成响应内容。

  Handler 接口的AP的核心方法I是:

public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException

  实现了这个方法可以处理一个请求,然后将请求传入另一个handler (或者servlet),或者它可以修改或者包装request然后将请求传递。这里有三种类型的handler:

  • 协调其他Handlers - 用来将请求传递给其他的Handlers(HandlerCollection,ContextHandlerCollection)
  • 过滤Handlers  - 增加一个请求处理程序,并将其传递给其他Handlers (HandlerWrapper,ContextHandler,SessionHandler)
  • 生成Handlers  - 用来生成context(ResourceHandler,ServletHandler)

24.1.1.1 Handler的作用

  Handler作用是对资源做一个标记这样可以对传入的请求作出处理。通常会在HTTP请求中解析URI。然而,在两种关键情况下目标将与传入的request不一致:

  • 如果请求被转发到一个指定的资源,例如一个定义过的servlet,那么做为一个指定的servlet,目标将是资源的名称。
  • 如果请求是由 Request Dispatcher产生的,那么目标将是包含资源和不同URI请求的URI。

24.1.1.2 请求和响应

  handle方法的request和response对象实际上是指 Servlet Request和 Servlet Response。它们是标准的APIs并且对请求和响应做了适当的限制。通常情况下访问Jetty的这些实现类是很重要的:Request和Response。然而Request和Response对象可能被handlers、filters和servlets包装过,它们和直接传入的不一样。下面的代码是在任何包装过的对象中获得Request和Response的核心代码。

Request base_request = request instanceof Request ? (Request)request : HttpConnection.getCurrentConnection().getHttpChannel().getRequest();
Response base_response = response instanceof Response ? (Response)response : HttpConnection.getCurrentConnection().getHttpChannel().getResponse();

  注意,如果一个handler将请求传递给另一个handler,它应该将request/response对象传入,而不是原始对象。这样可以保证request/response不被任何流处理器处理。

24.1.1.3 转发

  转发的参数可以表明被调用时的状态,如下:

  • REQUEST == 1 - 从连接中获得的一个原始请求
  • FORWARD == 2 - 一个从RequestDispatcher中转发得来的请求
  • INCLUDE == 4 - 一个包含在RequestDispatcher中的请求
  • ERROR == 8 - 一个被容器转发到错误处理器的请求

  这些对servlet和相关处理器都具有重要的意义。例如,安全处理器仅会应用身份验证和通过身份对请求进行转发。

24.1.2 处理器处理请求

  一个Handler 可以对一个请求做如下处理:

  • 生成一个Response
  • 对一个Request 和/或 一个 Response进行过滤
  • 将Request 和/或 Response 传递给另一个处理器

24.1.2.1 生成一个Response  

  之前OneHandler(http://www.cnblogs.com/yiwangzhibujian/p/5845623.html) 的嵌入式例子展示了一个简单的handler可以生成一个响应。

  你可以使用普通的servlet response API,通常需要设置状态,头信息,然后写入内容。

 response.setContentType("text/html");
 response.setStatus(HttpServletResponse.SC_OK);
 response.getWriter().println("<h1>Hello OneHandler</h1>");

  handler应表明它已经处理完成这个请求,并且这个请求不应该传递给其它handler,这两件事是非常重要的:

 Request base_request = (request instanceof Request) ? (Request)request:HttpConnection.getCurrentConnection().getHttpChannel().getRequest();
 base_request.setHandled(true);

24.1.2.2 过滤请求 和/或 响应

  一旦获得基础的请求和响应对象,你就可以修改它们。通常你修改它们是为了实现:

  • 分解URI使之进入指定的contextPath、servletPath和匹配路径的组件
  • 将请求与静态资源进行结合
  • 将一个请求与session进行结合
  • 将一个请求与主要的安全管理器进行结合
  • 在请求转发到另一个资源的时候对URI和路径进行修改

  你也可以对request的内容进行如下修改:

  • 设置当前线程context的类加载器
  • 设置线程本地类来标记当前的ServletContext

  通常,Jetty会将一个修改过的request传入另一个handler,并将在finally语句块中取消修改:

try
 {
    base_request.setSession(a_session);
    next_handler.handle(target,request,response,dispatch);
 }
 finally
 {
    base_request.setSession(old_session);
 }

  实现HandlerWrapper的类通常可以具有过滤器的行为。

24.1.2.3 转发请求和响应到另一个Handler

  一个handler可能只是简单的检查请求并且通过目标、请求的URI或者其它信息来选择下一个要传递的处理器。这些处理器通常实现HandlerContainer接口。

  例子包括如下:

  • Class Handler Collection - 一个handler的集合,每一个handler将会被调用。这通常用来将一个请求传入ContextHandlerCollection并掉调用RequestLogHandler。
  • HandlerList - 一个handlers 的List集合,会按顺序调用,直到请求状态被设置为已处理。
  • ContextHandlerCollection - 一个Handlers的集合,通常根据路径来匹配处理器来处理请求。

24.1.3 处理器其他信息

  可以通过阅读Jetty的JavaDco和Jetty的源码来获得其他handler的细节信息。

二十五、调试

25.1 调试选项

  将向你展示Jetty如何简单的配置并部署应用到开发和生产环境上,这和在你环境上调试你的应用有很大的不同。在这一章节,我们将集中介绍主要的不同点,并向你展示如何使用它们。

25.2 开启远程调试

  如果你把一个应用部署到Jetty那么你可以很简单的通过远程以调试的模式与应用进行交互。基本要求时,你需要以额外的参数启动远程JVM,然后在Eclipse中启动远程调试。这是很容易完成的。

+提示

    这个例子默认你将web应用部署到Jetty标准安装包中。

25.2.1 启动Jetty

  假设你已经将应用部署到Jetty中,下面有两种不同的方法来启动Jetty:

通过命令行

  在命令行中增加必要的启动参数,如下:

$ java -Xdebug -agentlib:jdwp=transport=dt_socket,address=9999,server=y,suspend=n -jar start.jar

通过start.ini

  如果你想对应用进行调试且不想记住复杂的参数那么这种方法将是最好的方法。

1、编辑start.ini文件,将--exec行的注释取消掉,这是非常必要的。

2、将上面命令行中的参数增加到文件中,如下所示:

#===========================================================
# Configure JVM arguments.
# If JVM args are include in an ini file then --exec is needed
# to start a new JVM from start.jar with the extra args.
# If you wish to avoid an extra JVM running, place JVM args
# on the normal command line and do not use --exec
#-----------------------------------------------------------
--exec
-Xdebug
-agentlib:jdwp=transport=dt_socket,address=9999,server=y,suspend=n
# -Xmx2000m
# -Xmn512m
# -XX:+UseConcMarkSweepGC
# -XX:ParallelCMSThreads=2
# -XX:+CMSClassUnloadingEnabled
# -XX:+UseCMSCompactAtFullCollection
# -XX:CMSInitiatingOccupancyFraction=80
# -verbose:gc
# -XX:+PrintGCDateStamps
# -XX:+PrintGCTimeStamps
# -XX:+PrintGCDetails
# -XX:+PrintTenuringDistribution
# -XX:+PrintCommandLineFlags
# -XX:+DisableExplicitGC

  对任何你感兴趣的启动参数,你都可以取消注释然后进行设置。

3、不管你对属性如何设置,在Jetty启动时你将会看到如下头部信息:

Listening for transport dt_socket at address: 9999

25.2.2 使用你的IDE进行连接

  根据你是用的IDE选择下面的文档进行阅读:

25.3 通过IntelliJ调试

  这里有很多可用的选项,可以在IntelliJ中调试你的应用。

25.3.1 使用IntelliJ连接项目

  接下来我们要使用IntelliJ 连接已部署的项目(截图源于官方文档)

1、在IntelliJ 中打开你想进行调试且部署到Jetty中的项目。选择 Run → Edit Configurations。通过“+”新增一个配置。选择 Remote。确保端口为你设置的端口。

2、接下来你可以在你想要的位置设置断点,当远程JVM线程运行到这个位置时会被触发。设置断点的方法很简单,选择class的源码,并且在行的左边单击(如下图红色的点),红点和红色背景的行为断点处。

3、通过浏览器访问你的servlet,当线程走到断点处后会被触发,并打开调试视图。

25.3.2 使用IntelliJ调试项目

  自从Jetty的嵌入式代码越来越简单后,很多人通常会使用一个很小的main方法来来对web项目进行一个简单的测试。最后应该回顾下之前的两个章节嵌入式Jetty嵌入式例子

  一旦你为你的应用程序定义了一个main方法,打开源码,在main方法上右键点击,选择 Debug或者使用快捷键CTRL+SHIFT+D,在你的控制台上你将会看到你的应用程序已启动,启动完成后你可以设置断点,然后通过浏览器访问来触发中断。同样的方法可以用于单元测试,用来替代使用main方法对你的方法进行测试。

  IntelliJ 的调试功能是异常强大的。例如可以设置条件断点,即当条件满足时才会触发中断。

+技巧

    你可以通过jetty-logging.properties文件来对Jetty的日志进行配置。如果这个文件在classpath中,那么Jetty启动时将会应用这个文件来对日志进行配置,我们使用这种扩展方法来使Jetty的开发变得异常简单。

25.4 通过Eclipse调试

  这里有很多可用的选项,可以在Eclipse中调试你的应用。

25.4.1 使用Eclipse连接项目

  接下来我们将使用Eclipse来连接部署的项目。

1、在Eclipse中,右键部署在Jetty的项目,选择Debug → Debug Configurations用来创建一个Remote Java Application配置。确保端口为配置的端口。

2、接下来你可以在你的应用中设置断点。

3、通过浏览器来访问你的servlet,当线程走到断点处时会被触发并进入Debug视图。

25.4.2 使用Eclipse调试项目

  自从Jetty的嵌入式代码越来越简单后,很多人通常会使用一个很小的main方法来来对web项目进行一个简单的测试。最后应该回顾下之前的两个章节嵌入式Jetty嵌入式例子

  一旦你为你的应用程序定义了一个main方法,打开源码,在main方法上右键点击,选择 Debug As → Java Application,在你的控制台上你将会看到你的应用程序已启动,启动完成后你可以设置断点,然后通过浏览器访问来触发中断。同样的方法可以用于单元测试,用来替代使用main方法对你的方法进行测试。

+技巧

    你可以通过jetty-logging.properties文件来对Jetty的日志进行配置。如果这个文件在classpath中,那么Jetty启动时将会应用这个文件来对日志进行配置,我们使用这种扩展方法来使Jetty的开发变得异常简单。

二十六、WebSocket 入门

  WebSocket 基本特性:

  • WebSocket 是通过HTTP来实现的一个新的交互式协议
  • 它是基于底层框架技术,提供UTF-8文本或二进制格式消息传递
  • 单个的消息可以是任意大小(单个消息在底层最少为63bits)
  • 发送消息的数量不被限制
  • 消息是顺序发送的,协议不支持交互式读取
  • 当WebSocket 关闭时,一个状态码和原因会被提供
  • WebSocket 会有以下状态的改变:
状态描述

CONNECTING

一个HTTP正在连接,升级为WebSocket

OPEN

HTTP升级成功,socket被打开,等待读和写

CLOSING

WebSocket 正在关闭

CLOSED

WebSocket已关闭,不能进行读和写












26.1 Jetty提供了什么

  Jetty提供了以下标准和规范的一个实现。

RFC-6455

  • WebSocket 的一个协议
  • 支持的13版本和最终发布的规范。
  • Jetty通过autobahn来测试WebSocket 协议的实现。

+重要

    早期WebSocket 仅被Jetty7和Jetty8支持,但是Jetty9已经不再支持。这意味着Jetty9将不再支持实现旧版本WebSocket 的一些老的浏览器

+技巧

    如果你想知道你选择的浏览器是否支持WebSocket,可以访问 caniuse.com/websockets

JSR-356

  • Java WebSocket API(javax.websocket
  • 这是Java官方APIs支持的WebSockets

  不稳定的标准和规范:

  1. perframe-compression
  2. permessage-compression

26.2 WebSocket APIs

  使用Jetty来实现WebSockets 的APIs和库。

Jetty WebSocket API
  使用Jetty来创建和与WebSockets 工作的最基本APIs
Jetty WebSocket Server API
  编写Jetty服务器端WebSocket 
Jetty WebSocket Client API
  使用Jetty来连接到WebSocket 
Java WebSocket Client API
  标准的JavaWebSocket 客户端API (javax.websocket) [JSR-356]
Java WebSocket Server API
  标准的JavaWebSocket 服务端API (javax.websocket.server) [JSR-356]

26.3 启用WebSocket

  为了启用websocket ,你必须启用websocket 模块。

  一旦这个模块被你的Jetty基实例启用,它将会应用到所有部署到这的web项目。如果你想更有选择性的指定哪些web应用使用websocket,你可以这么做:

对单个的项目禁用jsr-356

  你可以对单个的项目禁用jsr-356,通过设置context 的org.eclipse.jetty.websocket.jsr356属性为false来实现。这意味着websockets 将对你的项目不可用,但是部署时扫描websocket-related类,如终端,仍然会进行。这会在如果你的项目包含过的类和jar包时是个较大的开销。为了完全禁用websockets 并避免额外的开销,使用context属性org.eclipse.jetty.containerInitializerExclusionPattern,接下来描述你要排除的web应用。

对一个项目完全禁用jsr-356 

  设置context的org.eclipse.jetty.containerInitializerExclusionPattern属性,用来包含住org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer。下面有一个例子,通过代码禁用jsr-356,当然你也可以在配置文件中设置:

WebAppContext context = new WebAppContext();
context.setAttribute("org.eclipse.jetty.containerInitializerExclusionPattern",
                     "org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer|com.acme.*");

二十七、Jetty Websocket APIs

  这些页面还在改进,还没有移动到它们各自的章节。

27.1 Jetty WebSocket API使用

  Jetty提供它自己的更强力的API,有着一个为服务器和客户端WebSockets使用的基本核心API。

  这是一个基于WebSocket 消息的事件驱动API。

27.2 WebSocket事件

  每一个WebSocket 可以接收各种事件:

连接时事件

    • 当WebSocket 成功打开的一个指示,
    • 你将接收到一个org.eclipse.jetty.websocket.api.Session对象,指向打开事件的特定会话。
    • 对于通常的WebSocket ,持有住这个会话并与远程端通信是很重要的
    • 对于无状态的WebSockets,会话将被传递到每个发生的会话上,让你只有在众多远程终端中只有1个WebSocket

关闭时事件

    • 当WebSocket 关闭的一个指示
    • 每一个关闭的事件都有一个状态码(和一个可选的关闭原因的消息)
    • 通常的WebSocket关闭会通过通过本地终端和远程终端发送消息来实现连接关闭。
    • 本地WebSocket可以发送结束帧到远程终端,但是远程终端可以持续发送消息,知道发送一个结束帧。这成为半开连接,但是当本地WebSocket发送结束帧以后,将不会写入任何数据。
    • 当异常关闭时,例如连接终端或者连接超时,底层连接将在没有握手的情况下终止连接,这仍然会触发一个关闭事件(很有可能会触发一个错误事件)

错误时事件

    • 当一个错误发生时,并且在实现内,WebSocket 会被事件处理器通知到。

消息时事件

    • 表明一个完整的消息被接收到,并准备进行处理。
    • 可以是(使用UTF-8)文本或者原始的二进制消息

27.3 WebSocket会话

  Session对象可以被用来:

  判断连接状态(是打开还是关闭):

if(session.isOpen()) {
  // 发送消息
}

  判断连接是否安全

if(session.isSecure()) {
  // 使用'wss://'进行连接
}

  获得升级后的Request和Response

UpgradeRequest req = session.getUpgradeRequest();
String channelName = req.getParameterMap().get("channelName");

UpgradeRespons resp = session.getUpgradeResponse();
String subprotocol = resp.getAcceptedSubProtocol();

  获得远程连接地址

InetSocketAddress remoteAddr = session.getRemoteAddress();

  获得和设置超时时间

session.setIdleTimeout(2000); // 2 秒超时时间

27.4 将消息发送到远程终端

  会话最重要的特性是,通过 org.eclipse.jetty.websocket.api.RemoteEndpoint来发送消息。

  通过远程终端,你可以选择发送文本或二进制WebSocket 消息,或者WebSocket 的PING 和PONG终止帧。

27.4.1 同步消息发送

  大多数调用本质上都是同步的(堵塞式),在消息发送完毕前不会返回任何内容(或者抛出一个异常)。

RemoteEndpoint remote = session.getRemote();

// 堵塞式发送二进制消息到远程终端
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
try
{
    remote.sendBytes(buf);
}
catch (IOException e)
{
    e.printStackTrace(System.err);
}

  上面例子说明如何发送一个简单的二进制消息到远程终端,这将阻塞住直到消息发送完毕,或者抛出一个IOException异常当无法发送这个消息时。

RemoteEndpoint remote = session.getRemote();

// 阻塞式发送文件到远程终端
try
{
    remote.sendString("Hello World");
}
catch (IOException e)
{
    e.printStackTrace(System.err);
}

  上面例子说明如何发送一个文本消息到远程终端,这将阻塞住直到消息发送完毕,或者抛出一个IOException异常当无法发送这个消息时。

27.4.2 发送部分消息

  如果你有一个大消息要发送,并且想分部分发送,你可以使用部分消息发送方法到远程终端。只要确保你要完全发送完消息(isLast == true)。

RemoteEndpoint remote = session.getRemote();

// 阻塞发送二进制到远程终端
// 部分一
ByteBuffer buf1 = ByteBuffer.wrap(new byte[] { 0x11, 0x22 });
// 部分二 (最后一个部分)
ByteBuffer buf2 = ByteBuffer.wrap(new byte[] { 0x33, 0x44 });
try
{
    remote.sendPartialBytes(buf1,false);
    remote.sendPartialBytes(buf2,true); // isLast is true
}
catch (IOException e)
{
    e.printStackTrace(System.err);
}

  上面例子说明如何通过两个部分发送二进制消息,使用部分消息发送支持。这将阻塞直到每一部分发送完,如果发送不到会抛出一个IOException 异常。

RemoteEndpoint remote = session.getRemote();

// 堵塞式发送文本到远程终端
String part1 = "Hello";
String part2 = " World";
try
{
    remote.sendPartialString(part1,false);
    remote.sendPartialString(part2,true); // 最后一个部分
}
catch (IOException e)
{
    e.printStackTrace(System.err);
}

  上面例子说明如何通过两个部分发送文本消息,使用部分消息发送支持。这将阻塞直到每一部分发送完,如果发送不到会抛出一个IOException 异常。

27.4.3 发送PING/PONG控制帧

  你也可以使用RemoteEndpoint发送PING和PONG控制帧。

RemoteEndpoint remote = session.getRemote();

// 阻塞发送PING控制帧
String data = "You There?";
ByteBuffer payload = ByteBuffer.wrap(data.getBytes());
try
{
    remote.sendPing(payload);
}
catch (IOException e)
{
    e.printStackTrace(System.err);
}

  上面例子说明通过"You There?"(到达远程端点作为一个字节数组的负载)文本附带PING控制帧,这将阻塞直到消息发送完成,或许会抛出一个IOException如果ping帧发送不出去的话。

RemoteEndpoint remote = session.getRemote();

// 阻塞式发送PONG到终端
String data = "Yup, I'm here";
ByteBuffer payload = ByteBuffer.wrap(data.getBytes());
try
{
    remote.sendPong(payload);
}
catch (IOException e)
{
    e.printStackTrace(System.err);
}

  上面例子说明通过"Yup, I'm here?"(到达远程端点作为一个字节数组的负载)文本附带PONG控制帧,这将阻塞直到消息发送完成,或许会抛出一个IOException如果pong帧发送不出去的话。

  为了正确使用Pong帧,你应该在接收到PONG帧的时候返回相同的字节数组。

27.4.4 异步消息发送

  有两种可用的异步发送消息方法:

  • RemoteEndpoint.sendBytesByFuture(ByteBuffer message)
  • RemoteEndpoint.sendStringByFuture(String message)

  两种方法都会返回Future<Void>,可以使用标准的java.util.concurrent.Future来判断调用成功或失败。

RemoteEndpoint remote = session.getRemote();

// 异步发送二进制到远程终端
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
remote.sendBytesByFuture(buf);

  上面例子说明使用RemoteEndpoint发送一个简单的二进制。消息将被放到发送列表中,但是你不会知道发送是否成功。

RemoteEndpoint remote = session.getRemote();

// 异步发送二进制到远程终端
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
try
{
    Future<Void> fut = remote.sendBytesByFuture(buf);
    // 等待完成(永久)
    fut.get();
}
catch (ExecutionException | InterruptedException e)
{
    // 发送失败
    e.printStackTrace();
}

  上面例子说明使用RemoteEndpoint发送一个简单的二进制,通过追踪Future<Void>来判断是否发送成功。

RemoteEndpoint remote = session.getRemote();

// 异步发送二进制到远程终端
ByteBuffer buf = ByteBuffer.wrap(new byte[] { 0x11, 0x22, 0x33, 0x44 });
Future<Void> fut = null;
try
{
    fut = remote.sendBytesByFuture(buf);
    // 等待完成(有超时时间)
    fut.get(2,TimeUnit.SECONDS);
}
catch (ExecutionException | InterruptedException e)
{
    // 发送失败
    e.printStackTrace();
}
catch (TimeoutException e)
{
    // 发送超时
    e.printStackTrace();
    if (fut != null)
    {
        // 取消消息发送
        fut.cancel(true);
    }
}

  上面例子说明使用RemoteEndpoint发送一个简单的二进制,通过追踪Future<Void>并等待指定的超时时间来发送消息,到发送超时时,取消消息发送。

RemoteEndpoint remote = session.getRemote();

// 异步发送二进制到远程终端
remote.sendStringByFuture("Hello World");

  上面例子说明使用RemoteEndpoint发送一个文本消息。消息将被放到发送列表中,但是你不会知道发送是否成功。

RemoteEndpoint remote = session.getRemote();

// 异步发送二进制到远程终端
try
{
    Future<Void> fut = remote.sendStringByFuture("Hello World");
    // 等待完成(永久)
    fut.get();
}
catch (ExecutionException | InterruptedException e)
{
    // 发送失败
    e.printStackTrace();
}

  上面例子说明使用RemoteEndpoint发送一个文本,通过追踪Future<Void>来判断是否发送成功。

RemoteEndpoint remote = session.getRemote();

// 异步发送二进制到远程终端
Future<Void> fut = null;
try
{
    fut = remote.sendStringByFuture("Hello World");
    // 等待完成(有超时时间)
    fut.get(2,TimeUnit.SECONDS);
}
catch (ExecutionException | InterruptedException e)
{
    // 发送失败
    e.printStackTrace();
}
catch (TimeoutException e)
{
    // 发送超时
    e.printStackTrace();
    if (fut != null)
    {
        // 取消消息发送
        fut.cancel(true);
    }
}

  上面例子说明使用RemoteEndpoint发送一个文本,通过追踪Future<Void>并等待指定的超时时间来发送消息,到发送超时时,取消消息发送。

27.5 使用Websocket 注解

  使用WebSocket 最基本的形式是在POJO上使用 Jetty WebSocket API提供的注解。

package examples.echo;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

/**
 * 使用注解示例
 */
@WebSocket(maxTextMessageSize = 64 * 1024)
public class AnnotatedEchoSocket
{
    @OnWebSocketMessage
    public void onText(Session session, String message)
    {
        if (session.isOpen())
        {
            System.out.printf("Echoing back message [%s]%n",message);
            // echo the message back
            session.getRemote().sendString(message,null);
        }
    }
}

  上面的例子是一个简单的WebSocket回声终端例子,将接收到的消息发送出去。

  这是使用无状态方法来实现的,事先发生时,会话被传递到消息事件中。这将允许你使用单一的实例来处理多个终端。

  可用的注解如下:

@WebSocket

    • 一个必需的类级别注解
    • 标记当前类为WebSocket类
    • 这个类必需是非抽象的公共类

@OnWebSocketConnect

    • 一个可选的方法级别的注解
    • 标记类的一个方法用来接收连接事件
    • 方法必需是公共的、非抽象的、void返回值并且只有一个Session参数

@OnWebSocketClose

    • 一个可选的方法级别的注解
    • 标记类的一个方法用来接收关闭事件
    • 方法的参数为:
      • Session (可选的)
      • int closeCode (必需的)
      • String closeReason (必需的)

@OnWebSocketMessage

    • 一个可选的方法级别的注解
    • 标记类的两个方法响应接收到消息事件
    • 可以标记一个文件和一个二进制方法
    • 方法必需是公共的、非抽象的、void返回值
    • 接收文本消息的方法参数为:
      • Session (可选的)
      • String text (必需的)
    • 接收二进制消息的方法参数为:
      • Session (可选的)
      • byte buf[] (必需的)
      • int offset (必需的)
      • int length (必需的)

@OnWebSocketError

    • 一个可选的方法级别的注解
    • 标记类的一个方法用来响应WebSocket 实现类的错误事件
    • 方法必需是公共的、非抽象的、void返回值
    • 方法参数为:
      • Session (可选的)
      • Throwable cause (必需的)

@OnWebSocketFrame

    • 一个可选的方法级别的注解
    • 标记类中的一个方法从WebSocket实现类中接收帧事件后表明升级握手过程中处理的任何扩展。
    • 方法必需是公共的、非抽象的、void返回值
    • 方法参数为:
      • Session (可选的)
      • Frame (必需的)
    • 接收到的帧将被通知到这个方法上,然后交给Jetty处理,可能导致另一个事件,例如关闭或者接收到消息事件,帧的变化将不会被Jetty获知。

27.6 使用Websocket 监听器

  Websocket 实现监听基本形式是通过使用 org.eclipse.jetty.websocket.api.WebSocketListener。

package examples.echo;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketListener;

/**
 * 使用监听器的示例
 */
public class ListenerEchoSocket implements WebSocketListener
{
    private Session outbound;

    @Override
    public void onWebSocketBinary(byte[] payload, int offset, int len)
    {
        /* 只对文本消息感兴趣 */
    }

    @Override
    public void onWebSocketClose(int statusCode, String reason)
    {
        this.outbound = null;
    }

    @Override
    public void onWebSocketConnect(Session session)
    {
        this.outbound = session;
    }

    @Override
    public void onWebSocketError(Throwable cause)
    {
        cause.printStackTrace(System.err);
    }

    @Override
    public void onWebSocketText(String message)
    {
        if ((outbound != null) && (outbound.isOpen()))
        {
            System.out.printf("Echoing back message [%s]%n",message);
            // 监听到消息返回
            outbound.getRemote().sendString(message,null);
        }
    }
}

  这是到目前为止你能写出来的最基本和表现最好的(速度和存储)WebSocket实现类。如果你觉得这个监听器你有太多的方法要实现,那么你可以使用WebSocketAdapter来替代。

27.7 使用Websocket 适配器

  一个在WebSocketListener上管理会话的基本适配器。

package examples.echo;

import java.io.IOException;

import org.eclipse.jetty.websocket.api.WebSocketAdapter;

/**
 * 使用适配器的例子
 */
public class AdapterEchoSocket extends WebSocketAdapter
{
    @Override
    public void onWebSocketText(String message)
    {
        if (isConnected())
        {
            try
            {
                System.out.printf("Echoing back message [%s]%n",message);
                // 将接收到的消息返回
                getRemote().sendString(message);
            }
            catch (IOException e)
            {
                e.printStackTrace(System.err);
            }
        }
    }
}

  这是一个方便的类,让你使用WebSocketListener更简单,并提供了一些有用的方法来检查会话的状态。 

27.8 Jetty Websocket 服务端API

  Jetty提供了WebSocket 终端和Servlet 路径连接通过使用WebSocketServlet 桥接servlet的能力。

  在内部,Jetty管理着HTTP升级到WebSocket 和将一个HTTP连接转换为WebSocket 连接。

  这个只有在Jetty容器中运行才会起作用(不像过去的Jetty技术,现在你不能获得到在其他容器中运行着的Jetty WebSocket ,例如在JBoss、Tomcat或WebLogic中)。

27.8.1 Jetty的WebSocketServlet

  为了通过WebSocketServlet将你的WebSocket 与特殊的路径连接起来,你需要扩展org.eclipse.jetty.websocket.servlet.WebSocketServlet,并且指定哪个WebSocket 对象应该被创建与即将升级后的request。

package examples;

import javax.servlet.annotation.WebServlet;

import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

@SuppressWarnings("serial")
@WebServlet(name = "MyEcho WebSocket Servlet", urlPatterns = { "/echo" })
public class MyEchoServlet extends WebSocketServlet
{
    @Override
    public void configure(WebSocketServletFactory factory)
    {
        // 设置10秒超时时间
        factory.getPolicy().setIdleTimeout(10000);

        // 将MyEchoSocket 注册成一个要升级的WebSocket 
        factory.register(MyEchoSocket.class);
    }
}

  这个例子通过 @WebServlet创建了一个servlet映射,path为“/echo”(当然你也可以手动在WEB-INF/web.xml中添加),这样当遇到一个请求升级后将创建一个MyEchoSocket 实例。

  方法 WebSocketServlet.configure(WebSocketServletFactory factory)是你可以把对WebSocket特殊配置放进去的地方。在这个例子中,我们定义了10秒超时时间,并将MyEchoSocket 注册为默认的WebSocketCreator 用来当请求升级后进行创建。

+提示

    当配置websockets时,考虑防火墙和路由器的超时时间是很重要的。确保websocket 的超时时间低于你的防火墙或者路由器。

27.8.2 使用WebSocketCreator

  所有WebSocket的创建都是通过你在WebSocketServletFactory中注册的WebSocketCreator。

  默认情况下,WebSocketServletFactory 具有创建一个简单WebSocket 的能力。通过WebSocketCreator.register(Class<?> websocket)这个方法来告诉WebSocketServletFactory 应该创建哪个类(确保它有一个默认的构造方法)。

  如果你要创建一个更复杂的场景,你或许希望提供你自己的基于WebSocket 的WebSocketCreator ,它会将创建时的信息将会存在于UpgradeRequest 对象中。

package examples;

import org.eclipse.jetty.websocket.servlet.ServletUpgradeRequest;
import org.eclipse.jetty.websocket.servlet.ServletUpgradeResponse;
import org.eclipse.jetty.websocket.servlet.WebSocketCreator;

public class MyAdvancedEchoCreator implements WebSocketCreator
{
    private MyBinaryEchoSocket binaryEcho;
    private MyEchoSocket textEcho;

    public MyAdvancedEchoCreator()
    {
        // 创建可重用的套接字
        this.binaryEcho = new MyBinaryEchoSocket();
        this.textEcho = new MyEchoSocket();
    }

    @Override
    public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
    {
        for (String subprotocol : req.getSubProtocols())
        {
            if ("binary".equals(subprotocol))
            {
                resp.setAcceptedSubProtocol(subprotocol);
                return binaryEcho;
            }
            if ("text".equals(subprotocol))
            {
                resp.setAcceptedSubProtocol(subprotocol);
                return textEcho;
            }
        }

        // 没有有效的请求,忽略它
        return null;
    }
}

  这里我们展示了一个WebSocketCreator ,它可以通过request中 WebSocket子协议的名称来判断应该创建哪种类型的WebSocket 。

package examples;

import javax.servlet.annotation.WebServlet;

import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

@SuppressWarnings("serial")
@WebServlet(name = "MyAdvanced Echo WebSocket Servlet", urlPatterns = { "/advecho" })
public class MyAdvancedEchoServlet extends WebSocketServlet
{
    @Override
    public void configure(WebSocketServletFactory factory)
    {
        // 设置10秒超时时间
        factory.getPolicy().setIdleTimeout(10000);

        // 设置一个普通的创建者
        factory.setCreator(new MyAdvancedEchoCreator());
    }
}

  当你想定制一个WebSocketCreator,使用 WebSocketServletFactory.setCreator(WebSocketCreator creator)这个方法,那么WebSocketServletFactory 将会使用你定义的WebSocketCreator 来对所有到servlet的请求进行升级。

  WebSocketCreator的其它用途:

  • 控制WebSocket 子协议的选择
  • 执行任何你认为重要的WebSocket源
  • 获得传入请求的HTTP头信息
  • 获得Servlet HttpSession对象(如果存在的话)
  • 指定response 状态码和原因

  如果你不愿接收升级后的,可以简单的返回null值。

27.9 Jetty Websocket 客户端API

  Jetty同样提供了一个Jetty客户端WebSocket 的库用来简化编写WebSocket 服务。

  为了在你的Java项目中使用这个库,你只需要增加以下坐标:

<dependency>
  <groupId>org.eclipse.jetty.websocket</groupId>
  <artifactId>websocket-client</artifactId>
  <version>${project.version}</version>
</dependency>

27.9.1 WebSocketClient介绍

  为了使用WebSocketClient ,你需要将一个WebSocket 实例与一个特殊的WebSocket URI进行挂钩。

package examples;

import java.net.URI;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;

/**
 * 一个简单的客户端例子
 */
public class SimpleEchoClient
{
    public static void main(String[] args)
    {
        String destUri = "ws://echo.websocket.org";
        if (args.length > 0)
        {
            destUri = args[0];
        }

        WebSocketClient client = new WebSocketClient();
        SimpleEchoSocket socket = new SimpleEchoSocket();
        try
        {
            client.start();

            URI echoUri = new URI(destUri);
            ClientUpgradeRequest request = new ClientUpgradeRequest();
            client.connect(socket,echoUri,request);
            System.out.printf("Connecting to : %s%n",echoUri);

            // 等待关闭的socket 连接
            socket.awaitClose(5,TimeUnit.SECONDS);
        }
        catch (Throwable t)
        {
            t.printStackTrace();
        }
        finally
        {
            try
            {
                client.stop();
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}

  上面的例子连接了一个远程的WebSocket ,一旦被连接上SimpleEchoSocket 将会处理逻辑、等待socket被注册,直到它关闭。

package examples;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;

/**
 * Socket的基本例子
 */
@WebSocket(maxTextMessageSize = 64 * 1024)
public class SimpleEchoSocket
{
    private final CountDownLatch closeLatch;
    @SuppressWarnings("unused")
    private Session session;

    public SimpleEchoSocket()
    {
        this.closeLatch = new CountDownLatch(1);
    }

    public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException
    {
        return this.closeLatch.await(duration,unit);
    }

    @OnWebSocketClose
    public void onClose(int statusCode, String reason)
    {
        System.out.printf("Connection closed: %d - %s%n",statusCode,reason);
        this.session = null;
        this.closeLatch.countDown(); // 触发位置
    }

    @OnWebSocketConnect
    public void onConnect(Session session)
    {
        System.out.printf("Got connect: %s%n",session);
        this.session = session;
        try
        {
            Future<Void> fut;
            fut = session.getRemote().sendStringByFuture("Hello");
            fut.get(2,TimeUnit.SECONDS); // 等待发送完成

            fut = session.getRemote().sendStringByFuture("Thanks for the conversation.");
            fut.get(2,TimeUnit.SECONDS); // 等待发送完成

            session.close(StatusCode.NORMAL,"I'm done");
        }
        catch (Throwable t)
        {
            t.printStackTrace();
        }
    }

    @OnWebSocketMessage
    public void onMessage(String msg)
    {
        System.out.printf("Got msg: %s%n",msg);
    }
}

  当SimpleEchoSocket 连接上,它将发送两个Text文本,并关闭它。

  这个onMessage(String msg)方法,接收远程WebSocket 的responses ,并将它们输出到控制台上。

附言:

    Jetty文档的目录详见:http://www.cnblogs.com/yiwangzhibujian/p/5832294.html

    Jetty第一章翻译详见:http://www.cnblogs.com/yiwangzhibujian/p/5832597.html

    Jetty第四章(21-22)详见:http://www.cnblogs.com/yiwangzhibujian/p/5845623.html

    Jetty第四章(23)详见:http://www.cnblogs.com/yiwangzhibujian/p/5856857.html

    这次翻译的是第四部分的第24到27小节。翻译的过程中觉得Jetty的参考文档写的并不是特别好,偏向于理论,而且还有很多地方等待完善,虽然也有很多示例代码,但是都是一些代码片段,不太适合入门使用,所以我也打算在翻译快结束的时候,根据自己的理解写几篇用于实践的项目例子。

 

posted @ 2016-09-13 11:38  已往之不谏  阅读(10366)  评论(4编辑  收藏  举报