Tomcat原理系列之七:詳解socket如何封裝成request(下)
@TOC
推薦閱讀Tomcat原理系列之二:由點到線,請求主幹對於理解本文有很多幫助。
Tomcat版本8.
1. 接收連線:
Accptor在接受到socket請求後,執行setSocketOptions方法對socket進行初步的封裝。 封裝: 首先建立一個SocketBufferHandler用於socket輸入輸出的緩衝(SocketBuffer)。將SocketBufferHandler與socket一同封裝成NioChannel.
public SocketBufferHandler(int readBufferSize,int writeBufferSize,boolean direct) {
this.direct = direct;
if (direct) {
readBuffer = ByteBuffer.allocateDirect(readBufferSize);//預設8k
writeBuffer = ByteBuffer.allocateDirect(writeBufferSize);
} else {
readBuffer = ByteBuffer.allocate(readBufferSize);
writeBuffer = ByteBuffer.allocate(writeBufferSize);
}
}
複製程式碼
2. 註冊:
呼叫Poller.register()將NioChannel(socket)先進一步封裝成NioSocketWrapper類,再封裝成PollerEvent然後註冊到Poller的events佇列中去。
3. 消費:
Poller.run()消費佇列的PollerEvent事件。將PollerEvent中準備就緒的socketChannel註冊到Selector。
4. 處理請求:
Poler.run() 從Selector選擇處就緒的Channel。呼叫NioEndpoint.processKey(),processKey()方法中,根據讀寫事件呼叫processSocket()處理。
5. Worker執行緒:
processSocket()會根據(NioSocketWrapper)socket建立一個SocketProcessor處理器。SocketProcessor本身實現了Runnable介面。可以作為任務。被Endpoint的Executor執行緒池執行。
try {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase<S> sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper,event);
} else {
sc.reset(socketWrapper,event);
}
Executor executor = getExecutor();//執行緒池
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
} catch (RejectedExecutionException ree) {
複製程式碼
SocketProcessor在連線握手成功的情況下,呼叫ConnectionHandler.process()方法開始socket內容的讀取
6. HTTP1.1協議處理器初始化:
ConnectionHandler.process()方法會建立Http11Processor處理器用於http協議的處理. Http11Processor構造方法主要做了,
- 首先會建立一對org.apache.coyote.Request和org.apache.coyote.Response內部coyoteRequest與coyoteResponse物件.
- 並建立Http11InputBuffer與Http11OutputBuffer用於coyoteRequest與coyoteResponse。Http11InputBuffer提供HTTP請求頭的解析與編碼功能。Http11InputBuffer在建立的時候會指定headerBufferSize的大小.預設也是8k.
7. Http11Processor.service()[HTTP協議頭部的解析]:
拿到Http11Processor後.執行核心方法service(); 第一步:初始化讀寫緩衝區
// Setting up the I/O
setSocketWrapper(socketWrapper);
inputBuffer.init(socketWrapper);
outputBuffer.init(socketWrapper);
複製程式碼
init()方法為Http11InputBuffer內部建立一個讀緩衝區byteBuffer.大小為headerBufferSize+socketbuffer的大小.也就是預設是2*8k
void init(SocketWrapperBase<?> socketWrapper) {
wrapper = socketWrapper;
wrapper.setAppReadBufHandler(this);
int bufLength = headerBufferSize +
wrapper.getSocketBufferHandler().getReadBuffer().capacity();
if (byteBuffer == null || byteBuffer.capacity() < bufLength) {
byteBuffer = ByteBuffer.allocate(bufLength);
byteBuffer.position(0).limit(0);
}
}
複製程式碼
第二步開始請求行的解析 在解析之前我先來看看HTTP請求報文格式.
inputBuffer.parseRequestLine()方法用來讀取請求行。inputBuffer中有個parsingRequestLinePhase屬性值.parsingRequestLinePhase值不同代表讀取請求行的不同位置.- 0:表示解析開始前跳過空行
- 2: 開始解析請求方法: POST
- 3: 跳過請求方法和請求uri之間的空格或製表符
- 4: 開始解析請求URI: chapter17/user.html
- 5:跳過請求URI與版本之間的空格
- 6:解析協議版本: HTTP/1.1
parseRequestLine()方法, 每讀一個位置時,都會判斷inputBuffer.bytebuffer中是否讀取完畢。position = limit 即已經讀完了,需要執行fill重新填充,引數是false表示非阻塞讀(那什麼時候阻塞讀呢,是我們在呼叫getInputStream()時,是阻塞的)
// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {//判斷
if (keptAlive) {
// Haven't read any request data yet so use the keep-alive
// timeout.
wrapper.setReadTimeout(wrapper.getEndpoint().getKeepAliveTimeout());
}
if (!fill(false)) {//填充
// A read is pending,so no longer in initial state
parsingRequestLinePhase = 1;
return false;
}
// At least one byte of the request has been received.
// Switch to the socket timeout.
wrapper.setReadTimeout(wrapper.getEndpoint().getConnectionTimeout());
}
複製程式碼
fill()填充方法:填充buffer fill()的填充功能是通過呼叫socket的包裝類NioSocketWrapper.read()方法實現的. 在read()方法中 首先會嘗試從socketBufferHandler.readbuffer讀,
- 如果socketBufferHandler.readbuffer有資料,把資料填充到inputBuffer.bytebuffer中。不需要從socket通道讀取。
- 如果socketBufferHandler.readbuffer沒有資料可讀,且inputBuffer.bytebuffer的可寫空間大於socketBufferHandler.readbuffer的容量: 則直接從socket通道中讀取。設定該次讀取的最大值limit,為socket buffer的大小
- 如果socketBufferHandler.readbuffer沒有資料可讀,且inputBuffer.bytebuffer的可寫空間小於socketBufferHandler.readbuffer的容量:則先從socket通道讀入socketBuffer(因為此時socketBuffer的容量大於inputBuffer.bytebuffer的可寫空間,可以一次從OS讀取更多資料)。然後再從socketBuffer填充到inputBuffer.bytebuffer.(此時填充的是剩餘可寫空間,這樣socketBuffer也會剩餘一些,當inputBuffer.bytebuffer讀取完畢時,再呼叫fill()方法時,將剩餘socketBuffer的資料填充到inputBuffer.bytebuffer,不需要去socket通道內讀,本質上時減少OSread.然後這樣迴圈執行下去,直到所有的讀操作完成)
@Override
public int read(boolean block,ByteBuffer to) throws IOException {
//先從tomcat 底層socket buffer 緩衝區讀,如果buffer緩衝區還有未讀的buffer,則不需要到OS底層讀緩衝區讀
int nRead = populateReadBuffer(to);
if (nRead > 0) {
return nRead;
/*
* Since more bytes may have arrived since the buffer was last
* filled,it is an option at this point to perform a
* non-blocking read. However correctly handling the case if
* that read returns end of stream adds complexity. Therefore,* at the moment,the preference is for simplicity.
*/
}
// The socket read buffer capacity is socket.appReadBufSize
int limit = socketBufferHandler.getReadBuffer().capacity();
if (to.remaining() >= limit) {
to.limit(to.position() + limit);
nRead = fillReadBuffer(block,to);
if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "],Read direct from socket: [" + nRead + "]");
}
updateLastRead();
} else {
// Fill the read buffer as best we can.
nRead = fillReadBuffer(block);
if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "],Read into buffer: [" + nRead + "]");
}
updateLastRead();
// Fill as much of the remaining byte array as possible with the
// data that was just read
if (nRead > 0) {
nRead = populateReadBuffer(to);
}
}
return nRead;
}
複製程式碼
read()呼叫fillReadBuffer()方法來完成從socket通道內讀資料。fillReadBuffer有兩種讀模式阻塞讀和非阻塞讀.非阻塞讀會呼叫socket的初始包裝類NioChannel.read()方法,NioChannel.read()呼叫SocketChannel.read()此處是真正從通道里讀資料.
總結起來說。填充功能其實從socket通道把資料讀到inputBuffer.byteBuffer中。
解析:inputBuffer從byteBuffer中解析報文內容.例如請求方法,請求URI。inputBuffer並沒有把位元組轉義。而是使用byte[]陣列的包裝類MessageBytes來表示請求行的各部分,在需要的時候進行轉移並緩衝。
我們以請求方法讀取為例:
if (parsingRequestLinePhase == 2) {
//
// Reading the method name
// Method name is a token
//
boolean space = false;
while (!space) {
// Read new bytes if needed
if (byteBuffer.position() >= byteBuffer.limit()) {
if (!fill(false)) // request line parsing
return false;
}
// Spec says method name is a token followed by a single SP but
// also be tolerant of multiple SP and/or HT.
int pos = byteBuffer.position();
byte chr = byteBuffer.get();
if (chr == Constants.SP || chr == Constants.HT) {
space = true;
//請求的方法(get/post)
request.method().setBytes(byteBuffer.array(),parsingRequestLineStart,pos - parsingRequestLineStart);
} else if (!HttpParser.isToken(chr)) {
byteBuffer.position(byteBuffer.position() - 1);
throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
}
}
複製程式碼
看程式碼段,request.method().setBytes並沒有把請求報文的請求方法轉義為GET/POST字元,而是使用MessageBytes儲存了請求報文(即inputBuffer.byteBuffer)起始位到第一個空格之前的位元組陣列的下標。 在使用的時候將位元組轉為GET/POST
第三步就是讀取請求頭inputBuffer.parseHeaders():過程類似讀取請求行
第四步讀取請求頭後會執行prepareRequest():此方法設定request的filters和一些資訊的設定。
第五步呼叫Adapter.service(request,response):將tomcat的內部coyoteRequest和coyoteReponse轉換為servlet規範request ,response物件。這裡有個一轉換的過程。 就是建立servlet規範request ,response物件。然後將coyoteRequest,coyoteReponse分別設定給request,response 接下來就是呼叫各級容器,走過filter到達servlet中
// Calling the container
connector.getService().getContainer().getPipeline().getFirst().invoke(
request,response);
複製程式碼
8. HTTP協議body的解析
HTTP協議請求body的解析延遲到servlet中在獲取引數的時候解析的。body的解析放到其他章節在講。
總結下:
資料從連線通道copy到堆外記憶體,然後從堆外記憶體copy到 tomcat Http11InputBuffer的堆內byteBuffer。然後根據HTTP協議解析byteBuffer中的位元組陣列。變成HTTP協議的coyoteRequest,coyoteReponse。最後包裝成我們常用的request,response物件。
重用:
Tomcat中有很多重用的元件.以減少頻繁建立和銷燬的開銷
- NioChannel:NioChannel channel = nioChannels.pop();
- PollerEvent: PollerEvent r = eventCache.pop();
- SocketProcessor:SocketProcessorBase sc = processorCache.pop();
- Processor:Processor processor = connections.get(socket);