套接字输入缓冲装置——InternalInputBuffer

互联网的世界很复杂,信息从一端传向另一端过程也相当复杂,中间可能通过若干个硬件,为了提高发送和接收效率,在发送端及接收端都将引入缓冲区,所以两端的套接字都拥有各自的缓冲区,当然这种缓冲区的引入也带来了不确定的延时,在发送端一般先将消息写入缓冲区,直到缓冲区填满才发送,而接收端则一次只读取最多不超过缓冲区大小的消息。

Tomcat在处理客户端的请求时需要读取客户端的请求数据,它同样需要一个缓冲区用于接收字节流,即套接字输入缓冲装置,它主要的责任是提供一种缓冲模式从socket中读取字节流,提供填充缓冲区的方法,即将字节读到缓冲区buf,提供解析http协议请求行的方法,提供解析http协议请求头的方法,按照解析的结果组装请求对象Request。

套接字输入缓冲装置的工作原理并不会复杂,如下图所示,InternalInputBuffer包含以下几个变量:字节数组buf、整型pos、整型lastValid、整型end。其中buf是用于存放缓冲的字节流,它的大小由程序设定,tomcat中默认是设置为8 * 1024,即8k字节;pos表示读取指针,读到哪个位置值即为多少;lastValid表示从操作系统底层读取数据填充到buf中最后的位置;end表示缓冲区buf中http协议请求报文头部结束的位置,同时也表示报文体的开始位置。

图中从上往下看,最开始缓冲区buf是空的,将socket操作系统底层的若干字节流读取到buf中,于是状态如②所示,读取到的字节流将buf从头往后进行填充,同时pos为0,lastValid为此次读取后最后的位置值,接着第二次读取操作系统底层若干字节流,每次读取多少是不确定,字节流应该接在②中lastValid指定的位置后面而非从头开始,此时pos及lastValid根据实际情况被赋予新值,假如再读取一次则最终状态为⑤,多出了一个end变量,它的含义是http请求报文的请求行及请求头结束的位置。

套接字输入缓冲装置——InternalInputBuffer

为了更好理解如何从底层读取字节流并进行解析,下面将给出简化的处理过程,首先需要一个方法提供读取字节流,如下,其中inputStream代表套接字的输入流,通过socket.getInputStream()获取,其中read方法用于读取字节流,它表示从底层读取最多(buf.length-lastValid)长度的字节流,且把这些字节流填入buf数组中,填充的起始位置为buf[pos]开始,nRead表示实际读取到的字节数。通过对上面这些变量的操作则可以准确操作缓冲装置,成功填充返回true。

publicclass InternalInputBuffer{

byte[] buf=newbyte[8*1024];

    int pos=0;

    int lastValid=0;

public booleanfill(){

int nRead = inputStream.read(buf,pos, buf.length - lastValid);            

if (nRead >0) {            

            lastValid = pos + nRead;           

        }

return (nRead> 0);

}

}

有了填充的方法往下需要一个解析报文的操作过程,受篇幅影响此处只提供对请求行的方法及路径的解析为例子说明,其他的解析按照类似操作即可。http协议请求报文的格式如图,请求行一共有三个值需要解析出来:请求方法、请求url及协议版本,以空格间隔并以回车符换行符结尾。解析方法如下:

套接字输入缓冲装置——InternalInputBuffer

publicboolean parseRequestLine(){

        int start = 0;

        byte chr = 0;

        boolean space = false;

        while (!space) {

            if (pos >= lastValid)

                fill();

            if (buf[pos] == (byte) ' ') {

                space = true;

                byte[] methodB = new byte[pos -start];

                System.arraycopy(buf, start,methodB,0, pos - start);

                String method = newString(methodB);

                request.setMethod(method);

            }

            pos++;

        }

 

        while (space) {

            if (pos >= lastValid)

                fill();

            if (buf[pos] == (byte) ' ') {

                pos++;

            } else {

                space = false;

            }

        }

 

       start = pos;

        while (!space) {

            if (pos >= lastValid)

                fill();

            if (buf[pos] == (byte) ' ') {

                space = true;

                byte[] uriB = newbyte[pos-start];

                System.arraycopy(buf, start,uriB ,0, pos - start);

                String uri = new String(uriB);

                request.setUri(uri);

            }

            pos++;

        }

       return true;

    }

第一个while循环用于解析方法名,每次操作前必须判断是否需要从底层读取字节流,当pos大于等于lastValid时即需要调用fill方法读取,当字节等于ASCII编码的空格时就截取start到pos之间的字节数组,它们便是方法名的字节组成,转成String对象后设置到request对象中;第二个while循环用于跳过方法名与uri之间所有的空格;第三个while循环用于解析uri,它的逻辑与前面方法名解析的逻辑差不多,解析到的uri最终也设置到request对象里中。

至此,整个缓冲装置的工作原理基本搞清楚了,一个完整的过程是从底层字节流的读取到对这些字节流的解析并组装成一个请求对象request方便程序后面使用,由于每次不能确切保证从底层读取到的字节流,于是通过对pos、lastValid变量进行控制以至于完成对字节流的准确读取接收。除此之外,输入缓冲装置还提供了解析请求头部的方法,处理逻辑是按照http协议的规定对头部解析,然后依次放入request对象中。需要额外说明的是,tomcat实际运行中并不会将请求行、请求头等参数解析后就转化为String类型设置到request,而是继续使用ASCII码存放这些值,因为对这些ASCII码转码会导致性能问题,它的思想是只有到需要使用的时候再进行转码,很多参数没使用到就不进行转码,以此提高处理性能。这方面详细内容在Request章节有涉及。


点击订购作者《Tomcat内核设计剖析》



上一篇:套接字输入流——InputStream


下一篇:android sdk 安装排错