5.9 Limax Http

绝大多数服务器应用或多或少需要提供一些HTTP服务,Limax服务框架也不例外。sun从JDK6开始就提供了一个简单的服务器开发包com.sun.net.httpserver。实际使用会发现这个包的功能太弱,所以提供开发包limax.http作为替代。

limax.http支持HTTP/1.1(兼容HTTP/1.0),支持HTTP/2,支持WebSocket(从HTTP/1.1 upgrade,HTTP/2隧道两种方式均支持),支持Server-Sent Events。提供一个比Servlet更加简单有效的服务架构(Servlet已经经历了4个版本,越来越复杂)。

服务实现无需关心当前到底运行在HTTP/1.1环境下还是HTTP/2环境下,如果条件具备,自动选择HTTP/2。


  • 5.9.1 HTTP服务架构


    public interface HttpHandler extends Handler {
        DataSupplier handle(HttpExchange exchange) throws Exception;
        default void censor(HttpExchange exchange) throws Exception {
            throw new UnsupportedOperationException();
        }
    }
    


    • 1. 客户端请求完成之后HttpHandler的handle方法被调度,服务代码在handle中编写。

    • 2. HttpHandler的censor方法用于审查上传进度,对于GET请求,该方法决不会被调用。审查失败,该方法抛出异常,立即终止网络连接。默认实现禁止POST请求。

    • 3. POST请求至少调度一次censor,第一次调度可以获得完整的请求头,上传数据导致后续调度。POST请求的审查详见高级话题。

    • 4. FormData的getData()方法可以获取解析之后的query数据,即便是GET请求的query也被解析出来。返回类型为Map<String, List<Object>>,其中Object可能是String,可能是List<ByteBuffer>。 String对应了串查询参数, List<ByteBuffer>对应了上传的文件数据,这里之所以用List<ByteBuffer>是因为ByteBuffer尺寸上有Integer.MAX_INTEGER的限制。

    • 5. FormData的postLimit(long postLimit)方法用来限制POST数据尺寸,超出这个尺寸,直接终止连接。FormData的useTempFile(int threshold)方法提供一个上传文件的尺寸阈值,阈值之下文件内容存放在堆ByteBuffer中,否则将文件内容存放在一个临时文件中,ByteBuffer通过文件的内存映射获得,如果要支持上传文件应该设置一个合适的threshold。在不支持文件上传的POST中使用了useTempFile,内部将产生异常终止连接。这是合理的,上传文件通常需要设置较大的postLimit,不支持上传文件的POST数据全部存储在内存中,过大的postLimit将给恶意客户端提供攻击服务器的机会。具体使用方法可以参考示例。

    • 6. FormData的getRaw()方法返回一个Octets包含了POST发送的原始数据(不包括上传文件的情况,这种情况下Octets是空的)支持处理一些非字符串格式的查询,比如OCSP查询。


    •     public interface limax.http.DataSupplier {
          interface Done {
              void done(HttpExchange exchange) throws Exception;
          }
          java.nio.ByteBuffer get();  
          default void done(HttpExchange exchange) throws Exception;
          static DataSupplier from(byte[] data) ; 
          static DataSupplier from(java.nio.ByteBuffer data);  
          static DataSupplier from(java.nio.ByteBuffer[] datas);  
          static DataSupplier from(java.io.File file) throws IOException; 
          static DataSupplier from(java.nio.channels.FileChannel fc,long begin,long end) throws IOException;
          static DataSupplier from(java.io.InputStream in,int buffersize);  
          static DataSupplier from(java.nio.file.Path path) throws IOException;   
          static DataSupplier from(java.nio.channels.ReadableByteChannel ch,int buffersize);  
          static DataSupplier from(java.lang.String text,java.nio.charset.Charset charset);
          static DataSupplier from(DataSupplier supplier, DataSupplier.Done done);
          static DataSupplier from(HttpExchange exchange, java.util.function.BiConsumer<String, ServerSentEvents> consumer, Runnable onSendReady, Runnable onClose);
          static DataSupplier async();
      }
      
    • 7. 从DataSupplier接口定义可以看到,DataSupplier提供的静态方法足以覆盖绝大多数应用场景。如果需要释放资源可以使用带DataSupplier.Done参数的方法修饰DataSupplier,在done操作中执行释放操作。最后一个方法用来支持ServerSentEvents,详见下文。

    • 8. 极少情况需要实现自己的DataSupplier,如果自己实现,每次调用get方法应该返回一个ByteBuffer,最后一次get返回null,表示完成。最终done方法被调用,可以在done方法中释放资源。特别的,done方法又传入一次exchange,提供一个设置trailer头的机会。(HTTP协议定义了Trailer头,不知道哪些浏览器支持)

    • 9. handle返回DataSupplier.async()指明handle并没有完成响应,用于进一步的异步处理。这种情况下,异步任务完成之后必须执行exchange.async(HttpHandler handler)完成响应。费时的数据准备工作,可以采用这种方式完成,提高服务效率。(参见示例)

    • 10. handle允许抛出任何异常。除了limax.http.HttpException外,返回InternalServerError,包含异常栈。HTTP错误码可以通过limax.http.HttpException抛出,构造HttpException也允许提供一个HttpHandler,用这个特殊HttpHandler再进行一次处理。forceClose参数,决定这次错误返回之后需不需要关闭连接,特别的,对于HTTP/2,可以通过设置服务器参数禁止forceClose。更进一步,可以抛出一个无参数HttpException,立即无条件关闭连接。

    • 11. 应用也可以通过 exchange.getResponseHeaders().set(“:status”, code);的方式来设置错误码, 应该注意到这里的”:status”是HTTP/2的描述。

    • 12. WebSocketExchange提供promise(URI uri)方法支持HTTP/2的服务器推送,promise应该在处理服务逻辑的过程中调用。通常情况下,推送的资源应该被后续页面引用,客户端发现页面引用的资源之前已经开始推送了,就不需要再次请求该资源了,节省了一次请求过程,所以这是一个优化措施,如果没有推送最终处理结果应该一样。promise调用在HTTP/1.1下是个空操作,换句话说,如果需要可以在HttpHandler中实现推送逻辑,无需关心当前运行环境到底是HTTP/1.1还是HTTP/2。值得注意的是,服务器推送在某些情况下并不一定是一个好选择,比如客户端早就cache了推送的资源,再执行推送反而浪费带宽。


  • 5.9.2 WebSocket服务架构


    public interface WebSocketHandler extends Handler {
        void handle(WebSocketEvent event) throws Exception;
    }
    

    • 1. WebSocketHandler仅需要实现一个handle方法,接收到客户端数据或者websocket关闭时,handle被调度,handle的调度是串行的。

    • 2. event.type()返回事件类型。event.getWebSocketExchange()可以获取WebSocketExchange,用于发送数据,以及存取当前关联的SessionObject。

    • 3. event.type()==OPEN时可以设置应用自己的SessionObject关联当前连接。

    • 4. WebSocketExchange上的ping()方法可以用来测量RTT,ping()方法返回一个id,收到pong事件后可以获取对应的id与RTT值。

    • 5. WebSocketExchange上的send(byte[] binary),send(String text)方法用于发送WebSocket的二进制数据和字符串数据。event.type()==SENDREADY意味着之前的数据发送完成,可以发送新的数据,用于支持流控。

    • 6. WebSocketExchange上的sendFinal()方法发送CloseFrame之后立即关闭当前websocket连接。sendFinal(long timeout)方法发送CloseFrame之后半关连接,在timeout时限内等待对方的握手CloseFrame确保RFC6455关闭语义。实际上timeout对于https连接上的WebSocket没有意义,因为WebSocket关闭握手之后还会执行SSL关闭握手,这就确保了WebSocket关闭握手一定完成。同样的,timeout对于RFC8441定义的HTTP/2隧道方式的WebSocket关闭也没有意义,因为WebSocket关闭只对应Stream关闭。

    • 7. WebSocketExchange上的resetAlarm可以设置超时,如果超时限度没有再次resetAlarm,自动关闭WebSocket连接。

    • 8. 关闭event是最后一个event。收到关闭event,即可释放相应的资源,通常情况下关闭code,reason五花八门,不要过于在意。

    • 9. WebSocket存在2种运行模式,最初是从HTTP/1.1升级,Mozillia提出的RFC8441允许通过HTTP/2的流隧道WebSocket流量,目前已知只有某些版本FireFox支持这种模式。频繁使用WebSocket完成某些小任务时这种模式有很大优势,如果使用WebSocket执行复杂的持久化任务,这种模式未必合适。如果要禁用,可以在HttpServer对象上设置参数,httpServer.set(HttpServer.Parameter.HTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, 0),禁用这种模式。


  • 5.9.3 Server-Sent Events

    • 1. HttpHandle.handle返回时调用DataSupplier.from(HttpExchange exchange, BiConsumer<String, ServerSentEvents> consumer, Runnable onSendReady, Runnable onClose)方法,可以获取一个ServerSentEvents对象。其中,BiConsumer的String为Last-Event-Id,onSendReady用于支持流控。onClose用来通告网络连接已经被终止,或者done操作已经完成,emit已经没有意义了,应该释放ServerSentEvents对象。


      public interface ServerSentEvents {
          void emit(String event,String id,String data); 
          void emit(String data);  
          void emit(long milliseconds);
          void done();
      }
      
    • 2. 3个参数的emit用于向浏览器推送含有event,id,data的数据,其中event,id允许为null。单String参数的emit相当于event,id都为null的情况。long参数的emit用于向浏览器发送retry命令,单位毫秒。如果推送完成,应该调用done方法。done方法调用前如果在exchange上设置了trailer, trailer能够被发送出去。(http语义允许,尽管不知道有何用处,什么浏览器支持)。

    • 3. 浏览器对应的EventSource收到onerror消息,最好立即关掉EventSource,否则浏览器将不停重连服务器,这几乎就是一种攻击,去掉示例代码中sse.html中的s.close即能观察到这种情况。

    • 4. Server-Sent Events本质就是利用HTTP/1.1的chunked传输编码实现的一个简易设计,能力太弱,最好还是使用WebSocket代替。


上一页 下一页