HttpPostRequestDecoder源码浅析

文章目录

HttpPostRequestDecoder

属性

java 复制代码
public class HttpPostRequestDecoder implements InterfaceHttpPostRequestDecoder {

    static final int DEFAULT_DISCARD_THRESHOLD = 10 * 1024 * 1024;

    // 内部维护了一个 InterfaceHttpPostRequestDecoder
    private final InterfaceHttpPostRequestDecoder decoder;
    
    // ...
}

构造方法

java 复制代码
public HttpPostRequestDecoder(HttpRequest request) {
    this(
        new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), // 16KB
        request, 
        HttpConstants.DEFAULT_CHARSET // UTF-8
    );
}
java 复制代码
public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request) {
    this(
        factory, 
        request, 
        HttpConstants.DEFAULT_CHARSET // UTF-8
    );
}
java 复制代码
public HttpPostRequestDecoder(HttpDataFactory factory, 
                              HttpRequest request, 
                              Charset charset) {
    
    ObjectUtil.checkNotNull(factory, "factory");
    ObjectUtil.checkNotNull(request, "request");
    ObjectUtil.checkNotNull(charset, "charset");

    // 根据content-type请求头,判断是否是文件上传请求
    if (isMultipart(request)) {
        // 使用文件上传的解码器
        decoder = new HttpPostMultipartRequestDecoder(factory, request, charset);
    } else {
        // 使用标准的解码器
        decoder = new HttpPostStandardRequestDecoder(factory, request, charset);
    }
}

isMultipart(HttpRequest)

java 复制代码
public static boolean isMultipart(HttpRequest request) {
    
    // 获取content-type请求头对应的值
    String mimeType = request.headers().get(HttpHeaderNames.CONTENT_TYPE);
    
    // 如果content-type请求头的值以multipart/form-data开始
    if (mimeType != null && 
        mimeType.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString())) {
        
        // 如果content-type请求头的值符合文件上传的条件,则返回true
        return getMultipartDataBoundary(mimeType) != null;
    }
    
    // 不是文件上传请求
    return false;
}
getMultipartDataBoundary(String contentType)
java 复制代码
protected static String[] getMultipartDataBoundary(String contentType) {
    
    // Check if Post using "multipart/form-data; boundary=--89421926422648 [; charset=xxx]"
    
    // 调用splitHeaderContentType方法将contentType分割成多个部分,存储在headerContentType数组中。假设分割后的数组有三个部分:第一个是媒体类型(如multipart/form-data),第二个和第三个是参数(如boundary和charset)。
    String[] headerContentType = splitHeaderContentType(contentType);
    
    // 定义multiPartHeader为"multipart/form-data"
    final String multiPartHeader = HttpHeaderValues.MULTIPART_FORM_DATA.toString();
    
    // 检查headerContentType[0]是否以不区分大小写的方式匹配multiPartHeader。如果匹配,说明是multipart/form-data类型的请求。
    if (headerContentType[0].regionMatches(true, 
                                           0, multiPartHeader, 
                                           0 , multiPartHeader.length())) {
        
        int mrank;
        
        int crank;
        
        // 定义boundaryHeader为"boundary"
        final String boundaryHeader = HttpHeaderValues.BOUNDARY.toString();
        
        // 检查headerContentType[1]是否以不区分大小写的方式匹配boundaryHeader
        if (headerContentType[1].regionMatches(true, 
                                               0, boundaryHeader, 
                                               0, boundaryHeader.length())) {
            // boundary在数组中的索引
            mrank = 1;
            // charset在数组中的索引
            crank = 2;
        } 
        // 检查headerContentType[2]是否以不区分大小写的方式匹配boundaryHeader
        else if (headerContentType[2].regionMatches(true, 
                                                      0, boundaryHeader, 
                                                      0, boundaryHeader.length())) {
            // boundary在数组中的索引
            mrank = 2;
            // charset在数组中的索引
            crank = 1;
        } 
        else {
            // 如果都没有找到boundary,则返回null
            return null;
        }
        
        // 从headerContentType[mrank]中提取boundary的值(通过等号'='分割后的部分)
        String boundary = StringUtil.substringAfter(headerContentType[mrank], '=');
        
        // 如果boundary为null,则抛出异常。
        if (boundary == null) {
            throw new ErrorDataDecoderException("Needs a boundary value");
        }
        
        // 检查boundary的第一个字符是否是双引号,如果是,则去掉首尾的双引号
        if (boundary.charAt(0) == '"') {
            String bound = boundary.trim();
            int index = bound.length() - 1;
            if (bound.charAt(index) == '"') {
                boundary = bound.substring(1, index);
            }
        }
        
        // 定义charsetHeader为"charset"
        final String charsetHeader = HttpHeaderValues.CHARSET.toString();
        
        // 检查headerContentType[crank]是否以不区分大小写的方式匹配charsetHeader。
        if (headerContentType[crank].regionMatches(true, 
                                                   0, charsetHeader, 
                                                   0, charsetHeader.length())) {
            // 如果匹配,则提取charset的值(通过等号'='分割后的部分)。
            String charset = StringUtil.substringAfter(headerContentType[crank], '=');
            
            // 如果charset不为null,则返回一个包含两个元素的字符串数组:第一个是"--" + boundary(注意,boundary前面加两个减号),第二个是charset。
            if (charset != null) {
                return new String[] {"--" + boundary, charset};
            }
        }
        
        // 如果没有charset,则返回一个只包含"--" + boundary的数组
        return new String[] {"--" + boundary};
    }
    
    // 如果不是multipart/form-data类型,则返回null
    return null;
}
splitHeaderContentType
java 复制代码
private static String[] splitHeaderContentType(String sb) {
    
    int aStart;
    int aEnd;
    
    int bStart;
    int bEnd;
    
    int cStart;
    int cEnd;
    
    // 找到第一个非空白字符
    aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
    
    // 找到 ";" 分号
    aEnd =  sb.indexOf(';');
    
    // 如果 没有找到 ";" 分号
    if (aEnd == -1) {
        // 返回 sb和2个空字符串
        return new String[] { sb, "", "" };
    }
    
    // 说明找到了 ";" 分号
    
    // 从 ";" 分号后的下一个字符开始找到非空白字符
    bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd + 1);
    
    // 如果 ";" 分号前1个字符是空格,则aEnd索引减1
    if (sb.charAt(aEnd - 1) == ' ') {
        aEnd--;
    }
    
    // 从bStart开始找下一个 ";" 分号
    bEnd =  sb.indexOf(';', bStart);
    
    // 如果没有 ";" 分号
    if (bEnd == -1) {
        
        // 从sb后面往前找第一个非空白字符的索引
        bEnd = HttpPostBodyUtil.findEndOfString(sb);
        
        // 返回 2个有效元素 和 1个空字符串
        return new String[] { sb.substring(aStart, aEnd), 
                              sb.substring(bStart, bEnd), 
                              "" 
                            };
    }
    
    // 说明又找到了 ";" 分号
    
    // 从 ";" 分号后的下一个字符开始找到非空白字符
    cStart = HttpPostBodyUtil.findNonWhitespace(sb, bEnd + 1);
    
    // 如果 ";" 分号前1个字符是空格,则bEnd索引减1
    if (sb.charAt(bEnd - 1) == ' ') {
        bEnd--;
    }
    
    // 从sb后面往前找第一个非空白字符的索引
    cEnd = HttpPostBodyUtil.findEndOfString(sb);
    
    // 3个部分都有,都找到了
    return new String[] { sb.substring(aStart, aEnd), 
                          sb.substring(bStart, bEnd), 
                          sb.substring(cStart, cEnd)
                        };
}

MultiPartStatus

这个枚举MultiPartStatus定义了多部分表单数据(multipart/form-data)解析过程中的状态。

java 复制代码
protected enum MultiPartStatus {
    NOTSTARTED,           // 初始状态,尚未开始解析。
    PREAMBLE,             // 前言部分,在实际的分隔符之前的内容,通常忽略。
    HEADERDELIMITER,      // 已经读取到一个分隔符(即--boundary),接下来应该是一个部分的头部。
    DISPOSITION,          // 正在解析一个部分的Content-Disposition头部,这个头部通常包含字段名或文件名。
    FIELD,                // 表示当前部分是一个普通的表单字段,接下来是字段的值。
    FILEUPLOAD,           // 表示当前部分是一个文件上传,接下来是文件内容
    
    MIXEDPREAMBLE,        // 当某个部分本身又是multipart/mixed类型时,该部分的前言。
    MIXEDDELIMITER,       // 在multipart/mixed部分中,读取到内部边界分隔符。
    MIXEDDISPOSITION,     // 在multipart/mixed部分中,解析内部部分的Content-Disposition头部。
    MIXEDFILEUPLOAD,      // 在multipart/mixed部分中,表示内部部分是一个文件上传。
    MIXEDCLOSEDELIMITER,  // 在multipart/mixed部分中,读取到内部边界的结束分隔符(即--boundary--)
    
    CLOSEDELIMITER,       // 读取到外层multipart/form-data的结束分隔符
    
    PREEPILOGUE,          // 在结束分隔符之后,实际结束之前的状态
    EPILOGUE              // 结束状态,所有数据解析完成
}
  1. 从NOTSTARTED开始,然后进入PREAMBLE(如果有的话)。
  2. 接下来是零次或多次的普通部分(字段或文件):
    • HEADERDELIMITER:读取到分隔符。
    • DISPOSITION:解析该部分的头部,得到字段名或文件名。
    • 然后根据类型,进入FIELD(普通字段)或FILEUPLOAD(文件上传)。
  3. 可能有嵌套的multipart/mixed部分(用于一个字段对应多个文件的情况):
    • HEADERDELIMITER:读取到分隔符。
    • DISPOSITION:解析外层部分的头部。
    • MIXEDPREAMBLE:进入混合部分的前言。
    • 然后重复多次内部部分(MIXEDDELIMITER, MIXEDDISPOSITION, MIXEDFILEUPLOAD)。
    • 最后以MIXEDCLOSEDELIMITER结束内部混合部分。
  4. 整个multipart/form-data以CLOSEDELIMITER(即结束分隔符)结束。
  5. 然后进入EPILOGUE(可能有一些尾部内容,通常忽略)。

HttpPostStandardRequestDecoder

报文示例

复制代码
POST /abc HTTP/1.1
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Cache-Control: no-cache
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
cookie: SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm;SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm
Host: 127.0.0.1:8080
Content-Type: application/x-www-form-urlencoded
content-length: 17

name=zzhua&age=18
java 复制代码
public class HttpPostSdDecTest {

    public static void main(String[] args) {
        
        DefaultHttpDataFactory httpDataFactory = new DefaultHttpDataFactory();
        
        DefaultFullHttpRequest request = new DefaultFullHttpRequest(
            HttpVersion.HTTP_1_1, 
            HttpMethod.POST, 
            "/test"
        );
        
        request.content()
               .writeBytes("name=zzhua&age=18".getBytes(StandardCharsets.UTF_8));
        
        HttpPostStandardRequestDecoder decoder = new HttpPostStandardRequestDecoder(
            httpDataFactory, 
            request
        );
        
        decoder.offer(request);

        for (InterfaceHttpData bodyHttpData : decoder.getBodyHttpDatas()) {
            System.out.println(bodyHttpData);
        }
    }

}

属性

java 复制代码
// 用于创建InterfaceHttpData对象(例如Attribute和FileUpload)
final HttpDataFactory factory;

// 要解码的请求
final HttpRequest request;

// 默认字符集
final Charset charset;

// 是否已经接收到最后一个块
boolean isLastChunk;

// 存储解析出的所有数据
final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();

// 按照名字(忽略大小写)存储数据
final Map<String, List<InterfaceHttpData>> bodyMapHttpData 
    = new TreeMap<String, List<InterfaceHttpData>>(CaseIgnoringComparator.INSTANCE);

// 存储未解码的数据
ByteBuf undecodedChunk;

// 当前在bodyListHttpData中读取的位置,用于迭代
int bodyListHttpDataRank;

// 当前解析状态,注意这里虽然枚举名为MultiPartStatus,但用于非multipart的解析状态。
MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;

// 当前正在解码的属性
Attribute currentAttribute;

// 解码器是否已被销毁
boolean destroyed;

// 丢弃阈值,用于控制内存使用
int discardThreshold = 10 * 1024 * 1024;

构造方法

java 复制代码
public HttpPostStandardRequestDecoder(HttpRequest request) {
    this(
        //  16KB,超过此大小,则使用磁盘
        new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), 
        // 请求对象
        request, 
        // UTF-8
        HttpConstants.DEFAULT_CHARSET
    );
}
java 复制代码
public HttpPostStandardRequestDecoder(HttpDataFactory factory, HttpRequest request) {
    this(factory, 
         request, 
         // UTF-8
         HttpConstants.DEFAULT_CHARSET);
}
java 复制代码
public HttpPostStandardRequestDecoder(HttpDataFactory factory, 
                                      HttpRequest request, 
                                      Charset charset) {
    
    this.request = checkNotNull(request, "request");
    this.charset = checkNotNull(charset, "charset");
    this.factory = checkNotNull(factory, "factory");
    
    try {
        
        // 构造器中会根据
        if (request instanceof HttpContent) {
            // offer中会调用parseBody() 方法
            offer((HttpContent) request);
        } 
        else {
            parseBody();
        }
    } catch (Throwable e) {
        destroy();
        PlatformDependent.throwException(e);
    }
}

offer(HttpContent)

该方法用于接收新的HTTP内容块(HttpContent),并对其进行解码,以解析HTTP POST请求的主体部分。

Netty可以逐步接收HTTP请求体,并逐步解析,而不需要等待整个请求体完全接收完毕。这对于大文件上传尤其重要,可以避免内存溢出。

java 复制代码
public HttpPostStandardRequestDecoder offer(HttpContent content) {
    
    // 首先,检查解码器(destroyed属性)是否已经被销毁,如果已经销毁,则会抛出异常。
    checkDestroyed();

    // 判断接收到的content是否是LastHttpContent,如果是,则将isLastChunk标志设置为true。
    if (content instanceof LastHttpContent) {
        isLastChunk = true;
    }

    // 获取content中的ByteBuf(即实际的数据)
    ByteBuf buf = content.content();
    
    // 如果undecodedChunk为null,说明这是第一个数据块,那么我们需要创建一个新的缓冲区来存储数据。
    if (undecodedChunk == null) {
        // 这里使用buf.alloc().buffer(buf.readableBytes())来分配一个与当前可读字节数相同大小的缓冲区,并将buf的数据写入。
        // 注意:这里没有使用buf.copy(),因为copy()可能会设置一个maxCapacity,而我们后续可能会添加更多数据,超过这个容量。
        undecodedChunk = buf.alloc().buffer(buf.readableBytes()).writeBytes(buf);
    } else {
        // 如果undecodedChunk不为null,说明已经有数据了,直接将新数据写入到已有的undecodedChunk中。
        undecodedChunk.writeBytes(buf);
    }
    
    // 调用parseBody()方法来解析已经存储的数据。这个方法会解析HTTP POST请求的主体,包括表单字段和文件上传等。
    // 【这个方法是实际解析HTTP POST请求体的核心】
    parseBody();
    
    // 解析完成后,检查undecodedChunk是否不为null且其写索引(writerIndex)大于discardThreshold(表示缓冲区中已经写入了很多数据,我们希望通过丢弃已读数据来释放空间。)。
    if (undecodedChunk != null && undecodedChunk.writerIndex() > discardThreshold) {
        
        // 如果undecodedChunk的引用计数为1(即只有当前解码器引用这个缓冲区),那么可以安全地调用discardReadBytes()来丢弃已读取的数据,从而释放空间。
        if (undecodedChunk.refCnt() == 1) {
            undecodedChunk.discardReadBytes();
        } 
        else {
            
            // 否则,说明有多个引用,为了避免影响其他引用,我们创建一个新的缓冲区,只复制未读取的数据,然后释放原来的undecodedChunk,并将新的缓冲区赋值给undecodedChunk。
            ByteBuf buffer = undecodedChunk.alloc()
                                           .buffer(undecodedChunk.readableBytes());
            buffer.writeBytes(undecodedChunk);
            undecodedChunk.release();
            undecodedChunk = buffer;
        }
    }
    
    return this;
}

parseBody()

java 复制代码
private void parseBody() {
    
    // 这里首先检查当前解析状态是否为 PREEPILOGUE 或 EPILOGUE。这两个状态都表示解析已经进入尾声。
    // PREEPILOGUE:表示已经解析到了结束边界,但可能还有尾随数据(epilogue)需要处理。
    // EPILOGUE:表示整个解析过程已经完成。
    if (currentStatus == MultiPartStatus.PREEPILOGUE 
        || currentStatus == MultiPartStatus.EPILOGUE) {
        
        // 如果当前状态已经是上面这两个状态之一
        
        // 如果当前收到的数据块是最后一个(isLastChunk 为 true),则将状态设置为 EPILOGUE,表示解析结束。
        if (isLastChunk) {
            currentStatus = MultiPartStatus.EPILOGUE;
        }
        
        // 然后直接返回,不再执行后续的解析。
        return;
    }
    
    // 如果当前状态不是 PREEPILOGUE 或 EPILOGUE,则调用 parseBodyAttributes() 方法进行实际的解析。
    parseBodyAttributes();
}
parseBodyAttributes()

HttpPostStandardRequestDecoder 中解析普通表单数据(application/x-www-form-urlencoded)的方法。它负责解析像 key1=value1&key2=value2 这样的表单数据。此外,它也处理一些边界情况,比如空值(key1=&key2=value2)以及数据末尾的回车换行。

java 复制代码
private void parseBodyAttributes() {

    // 如果 undecodedChunk 为 null,则直接返回。
    if (undecodedChunk == null) {
        return;
    }

    // 如果缓冲区不是基于数组的(例如,可能是直接缓冲区),
    if (!undecodedChunk.hasArray()) {

        // 则调用另一个方法 parseBodyAttributesStandard() 来处理,并返回。
        parseBodyAttributesStandard();

        return;
    }

    // 走到这里,说明缓冲区是基于数组的
    // 使用 SeekAheadOptimize 类来优化读取操作,它通过直接访问字节数组来避免多次边界检查,提高性能。
    // 代码实现目的与 parseBodyAttributesStandard() 方法一致,直接看上面的这个方法就行了
    SeekAheadOptimize sao = new SeekAheadOptimize(undecodedChunk);
    int firstpos = undecodedChunk.readerIndex();
    int currentpos = firstpos;
    int equalpos;
    int ampersandpos;
    if (currentStatus == MultiPartStatus.NOTSTARTED) {
        currentStatus = MultiPartStatus.DISPOSITION;
    }
    boolean contRead = true;
    try {
        loop: while (sao.pos < sao.limit) {
            char read = (char) (sao.bytes[sao.pos++] & 0xFF);
            currentpos++;
            switch (currentStatus) {
                case DISPOSITION:
                    if (read == '=') {
                        currentStatus = MultiPartStatus.FIELD;
                        equalpos = currentpos - 1;
                        String key = decodeAttribute(
                            undecodedChunk.toString(firstpos, 
                                                    equalpos - firstpos, 
                                                    charset),
                            charset
                        );
                        currentAttribute = factory.createAttribute(request, key);
                        firstpos = currentpos;
                    } else if (read == '&') {
                        currentStatus = MultiPartStatus.DISPOSITION;
                        ampersandpos = currentpos - 1;
                        String key = decodeAttribute(
                            undecodedChunk.toString(firstpos, 
                                                    ampersandpos - firstpos,
                                                    charset), 
                            charset
                        );
                        currentAttribute = factory.createAttribute(request, key);
                        currentAttribute.setValue("");
                        addHttpData(currentAttribute);
                        currentAttribute = null;
                        firstpos = currentpos;
                        contRead = true;
                    }
                    break;
                case FIELD:
                    if (read == '&') {
                        currentStatus = MultiPartStatus.DISPOSITION;
                        ampersandpos = currentpos - 1;
                        setFinalBuffer(
                            undecodedChunk.retainedSlice(firstpos, 
                                                         ampersandpos - firstpos)
                        );
                        firstpos = currentpos;
                        contRead = true;
                    } else if (read == HttpConstants.CR) {
                        if (sao.pos < sao.limit) {
                            read = (char) (sao.bytes[sao.pos++] & 0xFF);
                            currentpos++;
                            if (read == HttpConstants.LF) {
                                currentStatus = MultiPartStatus.PREEPILOGUE;
                                ampersandpos = currentpos - 2;
                                sao.setReadPosition(0);
                                setFinalBuffer(
                                    undecodedChunk.retainedSlice(firstpos, 
                                                                 ampersandpos - firstpos)
                                );
                                firstpos = currentpos;
                                contRead = false;
                                break loop;
                            } else {
                                sao.setReadPosition(0);
                                throw new ErrorDataDecoderException("Bad end of line");
                            }
                        } else {
                            if (sao.limit > 0) {
                                currentpos--;
                            }
                        }
                    } else if (read == HttpConstants.LF) {
                        currentStatus = MultiPartStatus.PREEPILOGUE;
                        ampersandpos = currentpos - 1;
                        sao.setReadPosition(0);
                        setFinalBuffer(
                            undecodedChunk.retainedSlice(firstpos, 
                                                         ampersandpos - firstpos)
                        );
                        firstpos = currentpos;
                        contRead = false;
                        break loop;
                    }
                    break;
                default:
                    sao.setReadPosition(0);
                    contRead = false;
                    break loop;
            }
        }
        if (isLastChunk && currentAttribute != null) {
            ampersandpos = currentpos;
            if (ampersandpos > firstpos) {
                setFinalBuffer(
                    undecodedChunk.retainedSlice(firstpos, 
                                                 ampersandpos - firstpos)
                );
            } else if (!currentAttribute.isCompleted()) {
                setFinalBuffer(Unpooled.EMPTY_BUFFER);
            }
            firstpos = currentpos;
            currentStatus = MultiPartStatus.EPILOGUE;
        } else if (contRead 
                   && currentAttribute != null 
                   && currentStatus == MultiPartStatus.FIELD) {
            currentAttribute.addContent(
                undecodedChunk.retainedSlice(firstpos, 
                                             currentpos - firstpos),
                false
            );
            firstpos = currentpos;
        }
        undecodedChunk.readerIndex(firstpos);
    } catch (ErrorDataDecoderException e) {
        undecodedChunk.readerIndex(firstpos);
        throw e;
    } catch (IOException e) {
        undecodedChunk.readerIndex(firstpos);
        throw new ErrorDataDecoderException(e);
    } catch (IllegalArgumentException e) {
        undecodedChunk.readerIndex(firstpos);
        throw new ErrorDataDecoderException(e);
    }
}
parseBodyAttributesStandard
java 复制代码
private void parseBodyAttributesStandard() {
    
    // 记录 待解码的数据缓冲区 的 读索引
    int firstpos = undecodedChunk.readerIndex();
    
    // 从 待解码的数据缓冲区 的 读索引 作为当前位置 开始
    int currentpos = firstpos;
    
    // 查找 "=" 的索引位置
    int equalpos;
    
    // 查找 "&" 的索引位置
    int ampersandpos;
    
    // 如果当前状态为未开始,会设置为DISPOSITION。而状态初始化时默认就是 未开始。
    if (currentStatus == MultiPartStatus.NOTSTARTED) {
        currentStatus = MultiPartStatus.DISPOSITION;
    }
    
    // 是否要继续读
    boolean contRead = true;
    
    try {
        
        // 如果 待解码的数据缓冲区 有内容 并且 要继续读,则进入循环
        while (undecodedChunk.isReadable() && contRead) {
            
            // 读取1个字节
            char read = (char) undecodedChunk.readUnsignedByte();
            
            // 当前索引 + 1
            currentpos++;
            
            switch (currentStatus) {
                    
            // 如果 当前状态 currentStatus 是 DISPOSITION 或 FIELD,就接着在循环里面处理
            // 如果 当前状态 currentStatus 不是 DISPOSITION 或 FIELD,就跳出循环            
            
            // 如果 当前状态 currentStatus是 DISPOSITION
            case DISPOSITION:
                    
                // 如果遇到了 "="
                if (read == '=') {
                    
                    // 将 当前状态 currentStatus 切换为 FIELD,表示读完key之后,就要去读key对应的值
                    currentStatus = MultiPartStatus.FIELD;
                    
                    // "=" 是在 当前索引 的前1个位置
                    equalpos = currentpos - 1;
                    
                    // 对key作 URL 解码
                    String key = decodeAttribute(
                        				// 获取到 "=" 前面的key
                                        undecodedChunk.toString(firstpos, 
                                                                equalpos - firstpos, 
                                                                charset),
                                        charset
                    			  );
                    
                    // 使用HttpDataFactory创建Attribute对象,并将它作为当前处理的Attribute对象
                    currentAttribute = factory.createAttribute(request, key);
                    
                    // currentpos 当前索引 即为 "=" 的下1个位置
                    // 也就是firstPos更新为 "=" 的下1个位置
                    firstpos = currentpos;
                    
                } else if (read == '&') { // 有可能是 abc& 或 直接 &开始
                    
                    // 当前状态切换为 DISPOSITION
                    currentStatus = MultiPartStatus.DISPOSITION;
                    
                    // ampersandpos 更新为 当前位置 - 1,因为 当前位置 已经加过1了
                    ampersandpos = currentpos - 1;
                    
                    // 对key作 URL 解码
                    String key = decodeAttribute(
                        			    // 获取 & 前面的key
                                        undecodedChunk.toString(firstpos, 
                                                                ampersandpos - firstpos, 
                                                                charset), 
                                        charset
                                 );
                    
                    // 使用HttpDataFactory创建Attribute对象,并将它作为当前处理的Attribute对象
                    currentAttribute = factory.createAttribute(request, key);
                    
                    // 显然,值为空字符串
                    currentAttribute.setValue(""); // empty
                    
                    // 添加httpData
                    addHttpData(currentAttribute);
                    
                    // 将 当前Attribute 重置为null
                    currentAttribute = null;
                    
                    // firstPos 更新为 currentpos
                    firstpos = currentpos;
                    
                    // 表示继续循环处理
                    contRead = true;
                }
                   
                // 如果没遇到 "=" 或 "&",就一直跳过,直到遇到它们,才进入处理
                break;
                    
            // 如果 当前状态 currentStatus是 FIELD
            case FIELD:
                    
                // 如果遇到了 &,此时&前面的就是值了
                if (read == '&') {
                    
                    // 将 当前状态 currentStatus 切换为 DISPOSITION,可以下一个key了
                    currentStatus = MultiPartStatus.DISPOSITION;
                    
                    // 获取到 & 所在索引位置
                    ampersandpos = currentpos - 1;
                    
                    // 将解析的值设置到 当前Attribute对象 中
                    setFinalBuffer(
                        // 获取到 key对应的值
                        undecodedChunk.retainedSlice(firstpos, 
                                                     ampersandpos - firstpos)
                    );
                    
                    // firstPos 更新为 currentpos
                    firstpos = currentpos;
                    
                    // 需要继续 往下读
                    contRead = true;
                    
                } else if (read == HttpConstants.CR) { // 如果遇到了回车符
                    
                    // 如果 当前正在处理的缓冲区中 后面还有内容
                    if (undecodedChunk.isReadable()) {
                        
                        // 再去读取下一个字节
                        read = (char) undecodedChunk.readUnsignedByte();
                        
                        // 当前位置索引 + 1
                        currentpos++;
                        
                        // 如果下一个是换行符
                        if (read == HttpConstants.LF) {
                            
                            // 当前状态 切换为 PREEPILOGUE
                            currentStatus = MultiPartStatus.PREEPILOGUE;
                            
                            // CR + LF 共2个字节,因此 ampersandpos 值改为 当前位置 - 2
                            ampersandpos = currentpos - 2;
                            
                            // 将值设置给当前Attribute对象
                            setFinalBuffer(
                                undecodedChunk.retainedSlice(firstpos, 
                                                             ampersandpos - firstpos)
                            );
                            
                            // firstPos 更新为 currentpos
                            firstpos = currentpos;
                            
                            // 已经结束了,不需要再循环处理了,因此,将该contRead标记置为false
                            contRead = false;
                        } 
                        // 如果下一个字符不是换行符,则会抛出异常
                        else {
                            throw new ErrorDataDecoderException("Bad end of line");
                        }
                        
                    } 
                    else {
                        // 如果 当前正在处理的缓冲区中 后面没有内容了,这可能是后面的LF还没到,
                        // 因此将currentpos - 1,等下次LF到了,再从CR开始处理
                        currentpos--;
                    }
                    
                } else if (read == HttpConstants.LF) { // 如果遇到了换行符
                    
                    // 当前状态 切换为 PREEPILOGUE
                    currentStatus = MultiPartStatus.PREEPILOGUE;
                    
                    // ampersandpos 值改为 当前位置 - 1
                    ampersandpos = currentpos - 1;
                    
                    // 将值设置给当前Attribute对象
                    setFinalBuffer(
                        undecodedChunk.retainedSlice(firstpos, 
                                                     ampersandpos - firstpos)
                    );
                    
                    //  firstPos 更新为 currentpos
                    firstpos = currentpos;
                    
                    // 已经结束了,不需要再循环处理了,因此,将该contRead标记置为false
                    contRead = false;
                }
                    
                // 如果没遇到 "&" 或 CR回车符 或 LF换行符,就一直跳过,直到遇到它们,才进入处理
                break;
                    
            default:
                // currentStatus不是 DISPOSITION 或 FIELD,就跳出循环
                contRead = false;
            }
        }
        
        // 结束循环处理后,可能是还有数据未到达,也可能是所有数据都处理完了
        
        // 当这是最后一个数据块(isLastChunk为true)且当前还有未完成的属性(currentAttribute不为null)时
        // (当前方法中,只有在遇到&时,才会把currentAttribute设置为null,这里的currentAttribute不为null,就表示还没遇到&,但是已经是最后一个分块了,没办法只能作结束处理了)
        if (isLastChunk && currentAttribute != null) {

            // 将ampersandpos设置为currentpos(当前读取的位置),即整个数据块的结束位置。
            ampersandpos = currentpos;
            
            // 如果ampersandpos大于firstpos,说明从firstpos到ampersandpos之间有数据(即最后一个键值对的值部分),
            // 我们就将这些数据设置为最终缓冲区(通过setFinalBuffer方法,该方法会将数据设置到currentAttribute中,并标记 当前Attribute对象 为完成)。
            if (ampersandpos > firstpos) {
                
                // 将值设置给 当前Attribute对象,并且将 currentAttribute属性 置为null
                setFinalBuffer(
                    undecodedChunk.retainedSlice(firstpos, ampersandpos - firstpos)
                );
                
            }
            
            // 已经是最后的分块了,但是未设定值,此时ampersandpos等于firstpos,比如值为: "name="
            // 如果currentAttribute还没有完成(!currentAttribute.isCompleted()),但是,
            // 当前已经是最后1个分块了,后面没有数据了,因此这里只能被迫结束了
            else if (!currentAttribute.isCompleted()) {
                // 我们就将一个空的缓冲区(Unpooled.EMPTY_BUFFER)设置为最终缓冲区。
                setFinalBuffer(Unpooled.EMPTY_BUFFER);
            }
            
            // 将firstpos更新为currentpos
            firstpos = currentpos;
            
            // 将状态设置为EPILOGUE(结束状态)
            currentStatus = MultiPartStatus.EPILOGUE;
            
        } 
        // 如果还没有添加最后1个分块,
        // 并且contRead仍然是继续读,
        // 并且当前Attribute不是null,
        // 并且当前正在解析值
        else if (contRead 
                   && currentAttribute != null 
                   && currentStatus == MultiPartStatus.FIELD) {
            
            // 将当前获取到的内容添加到 当前Attribute对象 中
            // (这里也就是在说在未完全获取到值时,也会把部分已经获取到的值,添加到当前Attribute对象中)
            currentAttribute.addContent(
                undecodedChunk.retainedSlice(firstpos, currentpos - firstpos),
                // 表示还没完全接收完
                false
            );
            
            // 将firstPos 更新为 currentPos
            firstpos = currentpos;
        }
        
        // 此时,再移动 分块的读索引指针 至 firstpos
        // (表示从firstpos索引位置开始往后都未处理)
        undecodedChunk.readerIndex(firstpos);
        
    } catch (ErrorDataDecoderException e) {
        undecodedChunk.readerIndex(firstpos);
        throw e;
    } catch (IOException e) {
        undecodedChunk.readerIndex(firstpos);
        throw new ErrorDataDecoderException(e);
    } catch (IllegalArgumentException e) {
        undecodedChunk.readerIndex(firstpos);
        throw new ErrorDataDecoderException(e);
    }
}
setFinalBuffer(ByteBuf)
java 复制代码
private void setFinalBuffer(ByteBuf buffer) throws IOException {
    
    // 将此值添加到 当前Attribute对象 中,并且内容就是完整的
    currentAttribute.addContent(buffer, true);
    
    // URL编码中,特殊字符(如空格、非ASCII字符等)会被编码为%后跟两个十六进制数字。这个方法的作用就是将这些编码的字符解码回原始字符
    ByteBuf decodedBuf = decodeAttribute(currentAttribute.getByteBuf(), charset);
    
    // 将URL解码的值设置给 当前Attribute对象
    if (decodedBuf != null) {
        currentAttribute.setContent(decodedBuf);
    }
    
    // 添加httpData
    addHttpData(currentAttribute);
    
    // 当前Attribute对象 已解析完毕,重置为null
    currentAttribute = null;
}
addHttpData(InterfaceHttpData)
java 复制代码
protected void addHttpData(InterfaceHttpData data) {
    
    // 将 httpData添加到 bodyMapHttpData 和 bodyListHttpData 中
    
    if (data == null) {
        return;
    }
    
    List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
    
    if (datas == null) {
        datas = new ArrayList<InterfaceHttpData>(1);
        // 相同name的 httpData会组成一个列表,放入 bodyMapHttpData
        bodyMapHttpData.put(data.getName(), datas);
    }
    
    datas.add(data);
    
    // 所有的httpData都会放入 bodyListHttpData
    bodyListHttpData.add(data);
}

HttpPostMultipartRequestDecoder

普通multipart报文示例

复制代码
POST /abc HTTP/1.1
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Cache-Control: no-cache
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
cookie: SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm;SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=--------------------------209491242636242047311642
content-length: 268

----------------------------209491242636242047311642
Content-Disposition: form-data; name="name"

zzhua
----------------------------209491242636242047311642
Content-Disposition: form-data; name="age"

18
----------------------------209491242636242047311642--
java 复制代码
public class HttpPostMultipartDecTest01 {

    public static void main(String[] args) {
        DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/multipart1");
        request.headers().add("Content-Type", "multipart/form-data; boundary=--------------------------209491242636242047311642");

        DefaultHttpDataFactory httpDataFactory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
        HttpPostMultipartRequestDecoder multipartDecoder = new HttpPostMultipartRequestDecoder(httpDataFactory, request);

        DefaultLastHttpContent lastHttpContent = new DefaultLastHttpContent();
        lastHttpContent.content().writeBytes(
            ("----------------------------209491242636242047311642\r\n" +
            "Content-Disposition: form-data; name=\"name\"\r\n" +
            "\r\n" +
            "zzhua\r\n" +
            "----------------------------209491242636242047311642\r\n" +
            "Content-Disposition: form-data; name=\"age\"\r\n" +
            "\r\n" +
            "18\r\n" +
            "----------------------------209491242636242047311642--\r\n"
            ).getBytes(StandardCharsets.UTF_8)
        );
        multipartDecoder.offer(lastHttpContent);

        for (InterfaceHttpData bodyHttpData : multipartDecoder.getBodyHttpDatas()) {
            System.out.println("---------------解析请求---------------START");
            System.out.println(bodyHttpData.getName());
            System.out.println(bodyHttpData.getHttpDataType());
            if (bodyHttpData instanceof Attribute) {
                try {
                    System.out.println(((Attribute) bodyHttpData).getValue());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("---------------解析请求---------------END");
        }
    }

}

文件上传multipart报文示例

复制代码
DefaultHttpRequest(decodeResult: success, version: HTTP/1.1)
POST /abc HTTP/1.1
User-Agent: PostmanRuntime-ApipostRuntime/1.1.0
Cache-Control: no-cache
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
cookie: SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm;SESSION=Yzc3NGNlODktYjIzMC00NDZhLTk5MGYtNDU1ZDBjZGNkNDZm
Host: 127.0.0.1:8080
Content-Type: multipart/form-data; boundary=--------------------------489111531315023450709694
content-length: 563

----------------------------489111531315023450709694
Content-Disposition: form-data; name="name"

zzhua
----------------------------489111531315023450709694
Content-Disposition: form-data; name="age"

18
----------------------------489111531315023450709694
Content-Disposition: form-data; name="myFile"; filename="multipart报文.txt"; filename*=UTF-8''multipart%E6%8A%A5%E6%96%87.txt
Content-Type: text/plain

共3行报文,报文行1
第3行报文,报文行2
第3行报文,报文行3
----------------------------489111531315023450709694--
java 复制代码
public class HttpPostMultipartDecTest02 {

    public static void main(String[] args) {
        
        DefaultHttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/multipart1");
        
        request.headers().add("Content-Type", "multipart/form-data; boundary=--------------------------489111531315023450709694");

        DefaultHttpDataFactory httpDataFactory = new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE);
        
        HttpPostMultipartRequestDecoder multipartDecoder = new HttpPostMultipartRequestDecoder(httpDataFactory, request);

        DefaultLastHttpContent lastHttpContent = new DefaultLastHttpContent();
        
        lastHttpContent.content().writeBytes(
            ("----------------------------489111531315023450709694\r\n" +
             "Content-Disposition: form-data; name=\"name\"\r\n" +
             "\r\n" +
             "zzhua\r\n" +
             "----------------------------489111531315023450709694\r\n" +
             "Content-Disposition: form-data; name=\"age\"\r\n" +
             "\r\n" +
             "18\r\n" +
             "----------------------------489111531315023450709694\r\n" +
             "Content-Disposition: form-data; name=\"myFile\"; filename=\"multipart报文.txt\"; filename*=UTF-8''multipart%E6%8A%A5%E6%96%87.txt\r\n" +
             "Content-Type: text/plain\r\n" +
             "\r\n" +
             "共3行报文,报文行1\r\n" +
             "第3行报文,报文行2\r\n" +
             "第3行报文,报文行3\r\n" +
             "----------------------------489111531315023450709694--\r\n"
            ).getBytes(StandardCharsets.UTF_8));
        
        multipartDecoder.offer(lastHttpContent);

        for (InterfaceHttpData bodyHttpData : multipartDecoder.getBodyHttpDatas()) {
            
            System.out.println("---------------解析请求---------------START");
            
            System.out.println(bodyHttpData.getName());
            System.out.println(bodyHttpData.getHttpDataType());
            
            if (bodyHttpData instanceof Attribute) {
                try {
                    System.out.println(((Attribute) bodyHttpData).getValue());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            } 
            else if (bodyHttpData instanceof FileUpload) {
                FileUpload fileUpload = (FileUpload) bodyHttpData;
                System.out.println(fileUpload.getFilename());
                System.out.println(fileUpload.getContentType());
                System.out.println(fileUpload.getContentTransferEncoding());
                try {
                    System.out.println(
                        new String(
                            fileUpload.get(), 
                            fileUpload.getCharset()
                        )
                    );
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

            }
            System.out.println("---------------解析请求---------------END");
        }
    }

}

属性

java 复制代码
// 用于创建InterfaceHttpData对象(例如Attribute和FileUpload)
final HttpDataFactory factory;

// 要解码的请求
final HttpRequest request;

// 默认字符集
final Charset charset;

// 是否已经接收到最后一个块
boolean isLastChunk;

// 存储解析出的所有数据
final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();

// 按照名字(忽略大小写)存储数据
final Map<String, List<InterfaceHttpData>> bodyMapHttpData 
    = new TreeMap<String, List<InterfaceHttpData>>(CaseIgnoringComparator.INSTANCE);

// 存储未解码的数据
ByteBuf undecodedChunk;

// 当前在bodyListHttpData中读取的位置,用于迭代
int bodyListHttpDataRank;

// ???
final String multipartDataBoundary;

// ???
String multipartMixedBoundary;

// 当前解析状态
MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;

// 当前正在解码的属性
Attribute currentAttribute;

// 解码器是否已被销毁
boolean destroyed;

// 丢弃阈值,用于控制内存使用
int discardThreshold = 10 * 1024 * 1024;

构造方法

java 复制代码
public HttpPostMultipartRequestDecoder(HttpRequest request) {
    this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), 
         request, 
         HttpConstants.DEFAULT_CHARSET);
}
java 复制代码
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, HttpRequest request) {
    this(factory, request, HttpConstants.DEFAULT_CHARSET);
}
java 复制代码
public HttpPostMultipartRequestDecoder(HttpDataFactory factory, 
                                       HttpRequest request, 
                                       Charset charset) {
    
    this.request = checkNotNull(request, "request");
    this.charset = checkNotNull(charset, "charset");
    this.factory = checkNotNull(factory, "factory");

    String contentTypeValue = this.request.headers().get(HttpHeaderNames.CONTENT_TYPE);
    
    if (contentTypeValue == null) {
        throw new ErrorDataDecoderException("No content-type header present.");
    }

    String[] dataBoundary = HttpPostRequestDecoder.getMultipartDataBoundary(
                                                        contentTypeValue
                                                   );
    
    if (dataBoundary != null) {
        
        multipartDataBoundary = dataBoundary[0];
        
        if (dataBoundary.length > 1 && dataBoundary[1] != null) {
            
            try {
                
                this.charset = Charset.forName(dataBoundary[1]);
                
            } catch (IllegalCharsetNameException e) {
                throw new ErrorDataDecoderException(e);
            }
        }
        
    } else {
        
        multipartDataBoundary = null;
    }
    
    currentStatus = MultiPartStatus.HEADERDELIMITER;

    try {
        
        if (request instanceof HttpContent) {
            
            offer((HttpContent) request);
            
        } else {
            
            parseBody();
        }
        
    } catch (Throwable e) {
        destroy();
        PlatformDependent.throwException(e);
    }
}
相关推荐
闲人编程8 小时前
消息通知系统实现:构建高可用、可扩展的企业级通知服务
java·服务器·网络·python·消息队列·异步处理·分发器
栈与堆8 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
OC溥哥9998 小时前
Paper MinecraftV3.0重大更新(下界更新)我的世界C++2D版本隆重推出,拷贝即玩!
java·c++·算法
星火开发设计8 小时前
C++ map 全面解析与实战指南
java·数据结构·c++·学习·算法·map·知识
*才华有限公司*8 小时前
RTSP视频流播放系统
java·git·websocket·网络协议·信息与通信
gelald9 小时前
ReentrantLock 学习笔记
java·后端
计算机学姐9 小时前
基于SpringBoot的校园资源共享系统【个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·spring·信息可视化
一条咸鱼_SaltyFish9 小时前
[Day15] 若依框架二次开发改造记录:定制化之旅 contract-security-ruoyi
java·大数据·经验分享·分布式·微服务·架构·ai编程
跟着珅聪学java9 小时前
JavaScript 底层原理
java·开发语言