通訊端輸入緩衝裝置——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請求報文的請求行及要求標頭結束的位置。
為了更好理解如何從底層讀取位元組流並進行解析,下面將給出簡化的處理過程,首先需要一個方法提供讀取位元組流,如下,其中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及協議版本,以空格間隔並以斷行符號符分行符號結尾。解析方法如下:
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章節有涉及。