吃透Netty源碼系列四十四之HttpRequestDecoder詳解二
READ_HEADER讀取頭
我們繼續上一篇,現在的狀態到了讀取了。首先會先解析請求頭,然後看裏面有沒有transfer-encoding
或者content-length
,來進行後續的消息體讀取。
case READ_HEADER: try {//讀取請求頭
State nextState = readHeaders(buffer);
if (nextState == null) {
return;
}
currentState = nextState;
switch (nextState) {
case SKIP_CONTROL_CHARS://沒有內容,直接傳遞兩個消息
out.add(message);
out.add(LastHttpContent.EMPTY_LAST_CONTENT);空內容
resetNow();
return;
case READ_CHUNK_SIZE://塊協議傳遞
if (!chunkedSupported) {
throw new IllegalArgumentException("Chunked messages not supported");
}
out.add(message);
return;
default:
//沒有transfer-encoding或者content-length頭 表示沒消息體,比如GET請求
long contentLength = contentLength();
if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {//沒消息體,直接就補一個空消息體
out.add(message);//消息行和消息頭
out.add(LastHttpContent.EMPTY_LAST_CONTENT);//空消息體
resetNow();//重置屬性
return;
}
assert nextState == State.READ_FIXED_LENGTH_CONTENT ||
nextState == State.READ_VARIABLE_LENGTH_CONTENT;
//有消息體,就先放入行和頭信息,下一次解碼再進行消息體的讀取
out.add(message);//
if (nextState == State.READ_FIXED_LENGTH_CONTENT) {
chunkSize = contentLength;//如果是固定長度的消息體,要保存下一次要讀的消息體長度
}
return;
}
} catch (Exception e) {
out.add(invalidMessage(buffer, e));//異常了就無效
return;
}
readHeaders解析頭
主要就是按行解析頭消息,然後進行頭信息分割,然後放入headers
,最後根據content-length來決定後面的狀態,是讀取固定長READ_FIXED_LENGTH_CONTENT
還是可變長READ_VARIABLE_LENGTH_CONTENT
,還是是讀取塊大小READ_CHUNK_SIZE
。
private State readHeaders(ByteBuf buffer) {
final HttpMessage message = this.message;
final HttpHeaders headers = message.headers();//獲得請求頭
AppendableCharSequence line = headerParser.parse(buffer);//解析請求頭
if (line == null) {
return null;
}
if (line.length() > 0) {
do {
char firstChar = line.charAtUnsafe(0);
if (name != null && (firstChar == ' ' || firstChar == '\t')) {
//please do not make one line from below code
//as it breaks +XX:OptimizeStringConcat optimization
String trimmedLine = line.toString().trim();
String valueStr = String.valueOf(value);
value = valueStr + ' ' + trimmedLine;
} else {
if (name != null) {
headers.add(name, value);//如果名字解析出來表示值也出來了,就添加進去
}
splitHeader(line);//分割請求頭
}
line = headerParser.parse(buffer);//繼續解析頭
if (line == null) {
return null;
}
} while (line.length() > 0);
}
// Add the last header.
if (name != null) {//添加最後一個
headers.add(name, value);
}
// reset name and value fields 重置
name = null;
value = null;
//找content-length頭信息
List<String> values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
int contentLengthValuesCount = values.size();//長度頭的值的個數
if (contentLengthValuesCount > 0) {
if (contentLengthValuesCount > 1 && message.protocolVersion() == HttpVersion.HTTP_1_1) {//如果是HTTP_1_1找到多個Content-Length是不對的,要拋異常
throw new IllegalArgumentException("Multiple Content-Length headers found");
}
contentLength = Long.parseLong(values.get(0));//獲取消息體長
}
if (isContentAlwaysEmpty(message)) {//空內容
HttpUtil.setTransferEncodingChunked(message, false);//不開啓塊傳輸
return State.SKIP_CONTROL_CHARS;
} else if (HttpUtil.isTransferEncodingChunked(message)) {
if (contentLengthValuesCount > 0 && message.protocolVersion() == HttpVersion.HTTP_1_1) {//HTTP_1_1如果開啓了快協議,就不能設置Content-Length了
throw new IllegalArgumentException(
"Both 'Content-Length: " + contentLength + "' and 'Transfer-Encoding: chunked' found");
}
return State.READ_CHUNK_SIZE;//塊傳輸,要獲取大小
} else if (contentLength() >= 0) {
return State.READ_FIXED_LENGTH_CONTENT;//可以固定長度解析消息體
} else {
return State.READ_VARIABLE_LENGTH_CONTENT;//可變長度解析,或者沒有Content-Length,http1.0以及之前或者1.1 非keep alive,Content-Length可有可無
}
}
這裏有兩個要注意的,如果是HTTP1.1
一個頭只能對應一個值,而且Content-Length
和Transfer-Encoding
不能同時存在。http1.0
以及之前或者http1.1
沒設置keepalive
的話Content-Length
可有可無。
Header的結構
外部看上去很像是跟MAP
一樣添加頭信息,其實內部還是使用了數組
和單鏈表
和雙向循環鏈表
,好比是HashMap
的加強版。使用了hash
算法定位數組的索引,然後有衝突的時候用單鏈表頭插進去,而且頭信息順序按照雙向循環鏈表連起來了,方便前後定位。具體的細節可以看源碼,我就不多說了。
READ_VARIABLE_LENGTH_CONTENT讀取可變長內容
直接讀取可讀的字節,然後封裝成DefaultHttpContent
內容傳遞。
case READ_VARIABLE_LENGTH_CONTENT: {
// Keep reading data as a chunk until the end of connection is reached.
int toRead = Math.min(buffer.readableBytes(), maxChunkSize);
if (toRead > 0) {
ByteBuf content = buffer.readRetainedSlice(toRead);
out.add(new DefaultHttpContent(content));
}
return;
}
READ_FIXED_LENGTH_CONTENT讀取固定長內容
固定長度就是有contentLength
,讀取長度,如果等於記錄的長度chunkSize
,就表示讀完了,直接傳遞最後內容DefaultLastHttpContent
。否則說明沒讀完,就傳遞內容DefaultHttpContent
。
case READ_FIXED_LENGTH_CONTENT: {//有固定長消息體
int readLimit = buffer.readableBytes();
if (readLimit == 0) {
return;
}
int toRead = Math.min(readLimit, maxChunkSize);//讀取的個數
if (toRead > chunkSize) {//如果大於塊長度chunkSize,就讀chunkSize個
toRead = (int) chunkSize;
}
ByteBuf content = buffer.readRetainedSlice(toRead);
chunkSize -= toRead;
if (chunkSize == 0) {//塊全部讀完了
// Read all content.
out.add(new DefaultLastHttpContent(content, validateHeaders));//創建最後一個內容體,返回
resetNow();//重置參數
} else {
out.add(new DefaultHttpContent(content));//還沒讀完,就創建一個消息體
}
return;
}
READ_CHUNK_SIZE讀取塊大小
如果是chunk
塊傳輸,根據塊傳輸協議,就應該是獲取塊大小。協議格式我畫了個圖:
比如要傳輸aab
,使用塊協議,第一塊長度是2
,內容是aa
,第二塊長度是1
,內容是b
,第三塊長度是0
,內容是空(就有回車換行),記得長度內容後面都有回車換行啊。
case READ_CHUNK_SIZE: try {//讀取塊尺寸
AppendableCharSequence line = lineParser.parse(buffer);
if (line == null) {
return;
}
int chunkSize = getChunkSize(line.toString());
this.chunkSize = chunkSize;//塊長度
if (chunkSize == 0) {//讀到塊結束標記 0\r\n
currentState = State.READ_CHUNK_FOOTER;
return;
}
currentState = State.READ_CHUNKED_CONTENT;//繼續讀內容
// fall-through
} catch (Exception e) {
out.add(invalidChunk(buffer, e));//無效塊
return;
}
如果讀取的塊長度是0
了,那說明要到最後一個了,狀態就要轉到READ_CHUNK_FOOTER
,否則就轉到讀內容READ_CHUNKED_CONTENT
。
getChunkSize獲取塊尺寸
這裏連;
,空格
,控制字符
都算截止符了。
private static int getChunkSize(String hex) {
hex = hex.trim();
for (int i = 0; i < hex.length(); i ++) {
char c = hex.charAt(i);
if (c == ';' || Character.isWhitespace(c) || Character.isISOControl(c)) {
hex = hex.substring(0, i);
break;
}
}
return Integer.parseInt(hex, 16);
}
READ_CHUNKED_CONTENT讀取塊內容
根據塊長度chunkSize
讀取字節,如果讀取長度等於chunkSize
,表示讀完了,需要讀取分隔符,也就是換車換行了,狀態轉到READ_CHUNK_DELIMITER
,否則就將讀取的內容,封裝成DefaultHttpContent
傳遞下去,然後下一次繼續讀取內容。
case READ_CHUNKED_CONTENT: {//讀取塊內容,其實沒讀取,只是用切片,從切片讀,不影響原來的
assert chunkSize <= Integer.MAX_VALUE;
int toRead = Math.min((int) chunkSize, maxChunkSize);
toRead = Math.min(toRead, buffer.readableBytes());
if (toRead == 0) {
return;
}
HttpContent chunk = new DefaultHttpContent(buffer.readRetainedSlice(toRead));//創建一個塊,裏面放的是切片
chunkSize -= toRead;
out.add(chunk);
if (chunkSize != 0) {//當前塊還沒接受完,就返回
return;
}
currentState = State.READ_CHUNK_DELIMITER;//接受完,找到塊分割符
// fall-through
}
READ_CHUNK_DELIMITER讀取塊分隔符
其實就是回車換行符,找到了就轉到READ_CHUNK_SIZE
繼續去取下一個塊長度。
case READ_CHUNK_DELIMITER: {//找到塊分隔符
final int wIdx = buffer.writerIndex();
int rIdx = buffer.readerIndex();
while (wIdx > rIdx) {
byte next = buffer.getByte(rIdx++);
if (next == HttpConstants.LF) {//找到換行符,繼續讀下一個塊的大小
currentState = State.READ_CHUNK_SIZE;
break;
}
}
buffer.readerIndex(rIdx);
return;
}
READ_CHUNK_FOOTER讀最後一個塊
如果讀取的塊長度chunkSize=0
的話,就說明是最後一個塊了,然後要看下是否還有頭信息在後面,有頭信息的話會封裝成DefaultLastHttpContent
,如果沒有的話頭信息就是LastHttpContent.EMPTY_LAST_CONTENT
case READ_CHUNK_FOOTER: try {//讀到最後一個了
LastHttpContent trailer = readTrailingHeaders(buffer);//讀取最後的內容,可能有頭信息,也可能沒有
if (trailer == null) {//還沒結束的,繼續
return;
}
out.add(trailer);//添加最後內容
resetNow();
return;
} catch (Exception e) {
out.add(invalidChunk(buffer, e));
return;
}
readTrailingHeaders讀取最後的頭信息
會去讀取一行,如果沒讀出來換行,表示可能沒收到數據,也就是沒讀完,那就返回,繼續下一次。
如果讀出來發現就只有回車換行,那就說明沒有頭信息,結束了,就返回一個 LastHttpContent.EMPTY_LAST_CONTENT
,否則的話就創建一個DefaultLastHttpContent
內容,然後進行頭信息的解析,解析出來的頭信息就放入內容中,並返回內容。
private LastHttpContent readTrailingHeaders(ByteBuf buffer) {
AppendableCharSequence line = headerParser.parse(buffer);
if (line == null) {//沒有換行,表示沒讀完呢
return null;
}
LastHttpContent trailer = this.trailer;
if (line.length() == 0 && trailer == null) {//直接讀到\r\n 即讀到空行,表示結束,無頭信息,返回空內容
return LastHttpContent.EMPTY_LAST_CONTENT;
}
CharSequence lastHeader = null;
if (trailer == null) {
trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders);//空內容
}
while (line.length() > 0) {//chunk最後可能還有頭信息 key: 1\r\n
char firstChar = line.charAtUnsafe(0);
if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) {
List<String> current = trailer.trailingHeaders().getAll(lastHeader);
if (!current.isEmpty()) {
int lastPos = current.size() - 1;
//please do not make one line from below code
//as it breaks +XX:OptimizeStringConcat optimization
String lineTrimmed = line.toString().trim();
String currentLastPos = current.get(lastPos);
current.set(lastPos, currentLastPos + lineTrimmed);
}
} else {//解析頭信息
splitHeader(line);//
CharSequence headerName = name;
if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) &&
!HttpHeaderNames.TRANSFER_ENCODING.contentEqualsIgnoreCase(headerName) &&
!HttpHeaderNames.TRAILER.contentEqualsIgnoreCase(headerName)) {
trailer.trailingHeaders().add(headerName, value);
}
lastHeader = name;
// reset name and value fields
name = null;
value = null;
}
line = headerParser.parse(buffer);
if (line == null) {
return null;
}
}
this.trailer = null;
return trailer;
}
BAD_MESSAGE無效消息
直接略過後續一起的內容。
case BAD_MESSAGE: {
// Keep discarding until disconnection.
buffer.skipBytes(buffer.readableBytes());//壞消息,直接略過,不讀
break;
}
UPGRADED協議切換
其實就是協議的轉換。
case UPGRADED: {//協議切換
int readableBytes = buffer.readableBytes();
if (readableBytes > 0) {
out.add(buffer.readBytes(readableBytes));
}
break;
}
resetNow重置屬性
每次成功解碼操作後都要重新設置屬性。
private void resetNow() {
HttpMessage message = this.message;
this.message = null;
name = null;
value = null;
contentLength = Long.MIN_VALUE;
lineParser.reset();
headerParser.reset();
trailer = null;
if (!isDecodingRequest()) {//不是請求解碼,如果要升級協議
HttpResponse res = (HttpResponse) message;
if (res != null && isSwitchingToNonHttp1Protocol(res)) {
currentState = State.UPGRADED;
return;
}
}
resetRequested = false;
currentState = State.SKIP_CONTROL_CHARS;
}
至此整個基本完成了HttpRequestDecoder
就是他的子類,自己看下就懂了,核心方法都被父類實現了。
給一個只用了HttpRequestDecoder
的運行結果。
運行結果
GET
先是DefaultHttpRequest
:
然後LastHttpContent中的EMPTY_LAST_CONTENT
:
POST
先是DefaultHttpRequest
:
然後是DefaultLastHttpContent
:
如果是發送比較大的信息,比如:
那就是可能會出現好幾次消息體解析:
當然也可能一次,看接受緩衝區的情況啦:
好了,今天就到這裏了,希望對學習理解有幫助,大神看見勿噴,僅爲自己的學習理解,能力有限,請多包涵。