SpringBoot Tomcat(6) nio的应用(下)——响应

1,086 阅读5分钟

与请求结构相似,响应也包含响应行,响应头,响应体,相关类有响应外观类ResponseFacade,输出流OutputBuffer,输出流内部封装字节流ByteBuffer类型的bb和字符流CharBuffer类型的cc,一般来说使用的是bb

JSON类型的响应

当类或方法存在@ResponseBody注解的情况时,使用RequestResponseBodyMethodProcessor解析器,如果用的是阿里的FastJson,则使用FastJsonHttpMessageConverter消息转换器。

outputMessage的结构如下图所示,headers是消息头,类似于缓冲的作用,后面会写入响应头。

FastJsonHttpMessageConverter->write():
    // outputMessage的headers在这里是没有值的
    super.write(o, contentType, outputMessage);
AbstractHttpMessageConverter->write():
    final HttpHeaders headers = outputMessage.getHeaders();
    // 给消息头添加Content-Type为application/json;charset=UTF-8的属性
    addDefaultHeaders(headers, t, contentType);
    ......
    writeInternal(t, outputMessage);
    outputMessage.getBody().flush();
FastJsonHttpMessageConverter->writeInternal():
    // 响应体是返回内容以json字符串的展现,同时添加Content-Length为字符串的长度
    ......
    // 将响应体内容写入输出流的bb变量中
    outnew.writeTo(outputMessage.getBody());

outnew.writeTo(outputMessage.getBody())先将headers内容写入response对象中,返回输出流,输出流将响应体内容写入。flush()则是将输出流内容输出。

CoyoteOutputStream->flush():
    ob.flush();
// OutputBuffer继承了抽象类Writer    
OutputBuffer->flush():
    doFlush(true);
OutputBuffer->doFlush():
    if (initial) {
        // 将消息状态值置为提交,调用钩子方法
        coyoteResponse.sendHeaders();
        initial = false;
    }
    ......
    if (bb.remaining() > 0) {
        // 最终将响应体内容写入socketBufferHandler的writeBuffer中
        flushByteBuffer();
    }
    ......
    // 输出内容
    coyoteResponse.action(ActionCode.CLIENT_FLUSH, null);

钩子的作用是如果触发了相应的事件信息,假如添加了钩子则会调用钩子函数,函数中根据传递过来的事件消息判断执行不同的逻辑。在Tomcat中为实现了ActionHook的类,需要重写它的action()方法。

coyoteResponse.sendHeaders()将消息状态值置为提交,此处的钩子为Http11Processor,提交状态将会调用prepareResponse()方法。

Http11Processor->prepareResponse():
    // 过程中不管哪里调用setHeader方法,都会在headers展示
    MimeHeaders headers = response.getMimeHeaders();
    // 添加Content-Type、Content-Length、Date属性
    ......
    // 构造响应行,将值写入headerBuffer中
    outputBuffer.sendStatus();
    int size = headers.size();
    for (int i = 0; i < size; i++) {
        // 构造响应头,将值写入headerBuffer中
        outputBuffer.sendHeader(headers.getName(i), headers.getValue(i));
    }
    // 响应头的结束标签
    outputBuffer.endHeaders();
    // 将headerBuffer的内容写入SocketWrapperBase的socketBufferHandler的writeBuffer中
    outputBuffer.commit();

如图,第一行是响应行,1.1表示的是HTTP协议,200是HTTP状态码,name是自定义header属性,下面三个是默认添加的header属性,最后是响应体

总结

以下假设bb容量是8192个字节,writeBuffer容量是8192个字节

  1. 将响应体内容以字节数组的形式保存至CoyoteOutputStream的OutputBuffer的bb属性中
    • 如果响应体太长超出了bb容量的话
      1. 响应体一次取8192个字节,存储在一个新的ByteBuffer对象中,后面简称为buf
      2. 如果首次提交,那么构造响应行和响应头,写入Http11Processor的Http11OutputBuffer的headerBuffer中,最终写入SocketWrapperBase的socketBufferHandler的writeBuffer中(SocketWrapperBase->transfer(),后面简称最终写入)。截取buf,使得响应行、响应头、响应体长度为8192,最终写入,直接输出到客户端(NioEndpoint->NioSocketWrapper->doWrite())。将buf剩余内容最终写入
      3. 如果非首次提交,截取buf,使得本次响应体长度为8192,最终写入,直接输出到客户端,将buf剩余内容最终写入
      4. 重复步骤1-3,直到不超过bb容量为止
    • 如果没有超出
      • 直接写入bb属性中
      • 如果首次提交,构造响应行,响应头,写入Http11Processor的Http11OutputBuffer的headerBuffer中,最终写入
  2. 将输出流bb剩余部分写入SocketWrapperBase的socketBufferHandler的writeBuffer中
  3. 将消息状态置为CLIENT_FLUSH,调用钩子函数,输出到客户端

资源类型的响应

资源类型是指对文件的请求,结果是浏览器打开一个窗口预览文件或直接下载文件。常见场景有导出文件、下载文件

    // 获取一个文件
    File file = new File("E://谷歌下载/springmvc流程图.jpg");
    // 使浏览器下载的文件名称能够显示中文
    String fileName = URLEncoder.encode(file.getName(), "utf-8");
    // 让浏览器直接下载,而不是预览
    httpServletResponse.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + fileName);
    httpServletResponse.setCharacterEncoding("UTF-8");
    // 写入文件流
    httpServletResponse.getOutputStream().write(FileUtils.readFileToByteArray(file));

资源类型响应过程与JSON类型类似,因为方法返回类型为void,所以没有将消息状态置为CLIENT_FLUSH这一步骤

跳转与重定向的响应

跳转与重定向响应过程与资源类型本质是一样的,只不过获取文件、写入文件流这些过程不用再显示调用了

附录 NioSelectorPool

SocketWrapperBase是抽象类,它的实现类是NioEndpoint的内部类NioSocketWrapper,输出到客户端就是它负责的。

NioEndpoint->NioSocketWrapper->doWrite():
    // pool是NioSelectorPool线程池,默认单例模式,windows下返回WindowsSelectorImpl
    selector = pool.get();
    pool.write(from, getSocket(), selector, writeTimeout, block);
NioSelectorPool->write():
    // 默认是阻塞模式
    if ( SHARED && block ) {
        return blockingSelector.write(buf,socket,writeTimeout);
    }
NioBlockingSelector->write():
    // 尝试写入,写入异常则cnt返回-1,写入不成功则返回0,大于0表示写入成功,继续下一次的写入
    while ( (!timedout) && buf.hasRemaining()) {
        if (keycount > 0) {
            int cnt = socket.write(buf);
        }
    }

只有写入成功,才算完成socket通信。因为是阻塞模式,如果暂时不可写入,会休眠一段时间,然后尝试重新写入,直到写入成功或失败。

NioBlockingSelector->write():
    while ( (!timedout) && buf.hasRemaining()) {
        try {
            // 第一次进来,写锁数量肯定为0
            if ( att.getWriteLatch()==null || att.getWriteLatch().getCount()==0) {
                att.startWriteLatch(1);
            }
            poller.add(att,SelectionKey.OP_WRITE,reference);
            // 阻塞等待
            if (writeTimeout < 0) {
                att.awaitWriteLatch(Long.MAX_VALUE,TimeUnit.MILLISECONDS);
            } else {
                att.awaitWriteLatch(writeTimeout,TimeUnit.MILLISECONDS);
            }
        } catch (InterruptedException ignore) {
            // Ignore
        }
        // 大于0说明写锁没有释放,不尝试写入
        if ( att.getWriteLatch()!=null && att.getWriteLatch().getCount()> 0) {
            keycount = 0;
        } else {
            keycount = 1;
            att.resetWriteLatch();
        }
        // 判断是否超时
        if (writeTimeout > 0 && (keycount == 0)) {
            timedout = (System.currentTimeMillis() - time) >= writeTimeout;
        }
    }

CountDownLatch类相关方法来等待,写锁数量置为1,然后阻塞数秒,写锁释放后才尝试重新写入。那么写锁的释放逻辑应当在poller的add方法里了,poller是BlockPoller,它是一个无限循环的线程类。

NioBlockingSelector->BlockPoller->add():
    Runnable r = new RunnableAdd(ch, key, ops, ref);
    events.offer(r);
    wakeup();

add()方法新定义一个线程,然后放入events栈中,执行RunnableAdd线程类给channel新注册一个可写的SelectionKey。

NioBlockingSelector->BlockPoller->run():
    // 从events栈顶取出线程并执行
    ......
    // SelectionKey是否已就绪
    Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
    ......
    // 如果可写了,唤醒主线程中的await,同时释放写锁
    if (sk.isWritable()) {
        countDown(attachment.getWriteLatch());
    }
    ......

当主线程不可写入时,阻塞等待。然后开一个SelectionKey,用另一个线程无限循环判断SelectionKey是否可写,当可写时,主线程被唤醒并尝试重新写入。这样的好处是避免主线程无限次尝试写入,同时阻塞时间变得可控,极大提升了性能,非常值得学习。