吃透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-LengthTransfer-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
在這裏插入圖片描述
在這裏插入圖片描述

如果是發送比較大的信息,比如:
在這裏插入圖片描述
那就是可能會出現好幾次消息體解析:
在這裏插入圖片描述
當然也可能一次,看接受緩衝區的情況啦:
在這裏插入圖片描述

好了,今天就到這裏了,希望對學習理解有幫助,大神看見勿噴,僅爲自己的學習理解,能力有限,請多包涵。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章