tio-http-server 源码浅析(一)HttpRequestDecoder的实现

前言

    (本段瞎扯淡,可略)距离上一篇博客已经过去一个多月了,如果说没写博客的原因,大概是因为懒了吧。本篇还是关于tio的内容,不过由应用转为源码分析,原作者的博客总是短小精悍,我的博客风格又臭又长,建议先码后看或者直接不看。瞎扯些,为什么要去看tio-http端的代码呢,我相信大部分程序员多少都有被问到过 get 和post的区别的经历,好像之前对于http的理解也只能到这里了。一般开发也就会用httpClient即可,会请求,会调用接口。我相信大部分公司也不会闲着没事让你写一套http server的。当然借着书本(HTTP权威指南)上的内容和tio-http的源码,让我对http的了解更加深入一些,所以博客内容仅限于本人自己的理解,有错误之处还请指正。

什么是HTTP?

    相信web开发者对于这四个字母在熟悉不过了,不管是什么语言的,都少不了这个东西。没错,他就是超文本传输协议,就是一种协议。还没有接触tio的时候,我以为tio就是WebSocket,后来才发现,他只是通讯框架,而HTTP,WebSocket,FTP等都是协议。举个很简单的例子,tio可以有client-server通讯,很简单的demo就可以实现。但是tio server端如何接收浏览器的请求呢?能解析吗?我的答案是能接收,但是响应的内容恐怕浏览器识别不了。所以,就需要实现HTTP协议,只要实现了它,那么不管是HttpClient还是浏览器,都能够正常请求基于tio的服务端并且获得响应了。

怎么实现HTTP?

    既然HTTP是一种协议,那么他就有一些规定,所以没什么好说的,按照规定走就完事了。那说起来容易,那接下来我们就详细看看它的规定吧。

构建HTTP请求

    我拿百度举例,访问百度首页,F12查看一下请求详情,我们在截图中能看到RequestHeaders,ResponseHeader还有General中的内容,其实之前我还被浏览器误导了,以为server端接收的报文就是这样的格式,后来想想,它就是一个对于开发人员看起来便捷的UI。呵呵,我真傻。。。

    131755_v8nd_3669181.png

    那么,真正的报文长什么样呢?我也不知道,接下来我们从源码中探寻吧。

HttpRequestDecoder

    这个类是解析请求报文的核心类。其中方法 decode 返回 一个HttpRequest 对象,HttpRequest对象包含如下字段:

    private RequestLine requestLine = null;
    private Map<String,Object[]> params = new HashMap<>();
    private List<Cookie> cookies = null;
    private Map<String,Cookie> cookieMap = null;
    private int contentLength;
    private String bodyString;
    private HttpConst.RequestBodyFormat bodyFormat;
    private String charset = HttpConst.CHARSET_NAME;
    private Boolean isAjax = null;
    private Boolean isSupportGzip = null;
    private HttpSession httpSession;
    private Node remote = null;
    private ChannelContext channelContext;
    private HttpConfig httpConfig;
    private String domain = null;
    private String host = null;
    private String clientIp = null;
    private long createTime = SystemTimer.currentTimeMillis();
    private boolean closed = false;

    看其中的字段,相信大家也能看个差不多。比如包含请求内容,参数,cookie等信息。所以,decode的方法的任务就是将 ByteBuffer 转化为程序可操控的HttpRequest对象。核心代码如下:

     /**
         * position < limit
         * 循环读取每行的内容进行解析
         * */
        while(buffer.hasRemaining()){
            String line;
            try{
                //读取每一行的内容(详见下文)
                line = ByteBufferUtils.readLine(buffer,null,MAX_LENGTH_OF_HEADERLINE);
            }catch (LengthOverflowException e){
                throw new AioDecodeException(e);
            }

            int newPosition = buffer.position();
            //如果头部信息超过 20480 字节,异常
            if (newPosition - initPosition > MAX_LENGTH_OF_HEADER){
                throw new AioDecodeException("max http header length " + MAX_LENGTH_OF_HEADER);
            }
            //没有内容,返回null
            if (line == null){
                return null;
            }
            headerString.append(line).append("\r\n");
            //line为空,头部信息解析结束
            if("".equals(line)){
                //从 Content-Length:167 读取请求体的长度
                String contentLengthStr = headers.get(HttpConst.RequestHeaderKey.Content_Length);
                if(StringUtils.isBlank(contentLengthStr)){
                    contentLength = 0;
                }else{
                    contentLength = Integer.parseInt(contentLengthStr);
                }

                //头部信息长度
                int headerLength = (buffer.position() - initPosition);
                //头部和体部的总字节长度
                int allNeedLength = headerLength + contentLength;
                if(readableLength >= allNeedLength){
                    step = step.body;
                    break;
                }else{
                    channelContext.setPacketNeededLength(allNeedLength);
                    return null;
                }
            }else{
              if(step == Step.firstline){
                  //解析第一行(详见下文)
                  firstLine = parseRequestLine(line);
                  step = Step.header;
              }else if(step == Step.header){
                  //解析头部(详见下文)
                  KeyValue keyValue = HttpParseUtils.parseHeaderLine(line);
                  headers.put(keyValue.getKey(),keyValue.getValue());
              }
              continue;
            }
        }

    首先一个while 循环,依次按行读取,为什么这么做呢?因为报文内容每一段是以 CRLF 结尾也就是 "\r\n".所以看到 ByteBufferUtils.readLine方法不难理解。

//开始位置
int startPosition = buffer.position();
//结束位置
int endPosition = lineEnd(buffer, maxlength.intValue());
//返回开始位置到结束位置的字节数组
byte[] bs = new byte[endPosition - startPosition];
//转换为字符串返回
return new String(bs, charset);

    其中lineEnd方法,就是通过CRLF标识来返回结束的位置(endPosition)

//CR
if (b == 13) {
   canEnd = true;
//LF
} else if (b == 10) {
   if (canEnd) {
      int endPosition = buffer.position();
      return endPosition - 2;
      }
   } else {
     canEnd = false;
   }
}

    可能到这里大家有些云里雾里的,没关系。我专门打印了一下ByteBuffer的内容。比如,我们访问 http://127.0.0.1:8080/test/hello?id=1&name=tio,我们来看看服务器接收到的内容是什么:

140323_HqMK_3669181.png

    看到上图是不是很清晰了呢,首先第一行的 GET /test/hello?id=1&name=tio HTTP/1.1 让服务器知道,HTTP的方法是什么,请求的路径以及参数,还有协议版本信息。那么剩下的几行就是请求头的部分。大家注意中间有一个空行。空行下用红框框住的部分就是form表单的内容了(截图中是空的,因为是GET请求)。

    接下来继续分析源码。

    140610_Dx3S_3669181.png

    解析第一行的时候,执行parseRequestLine方法,然后将解析步骤置为header。parseRequestLine做了什么呢,就是把 GET /test/hello?id=1&name=tio HTTP/1.1 解析成一个 RequestLine 对象,方便后续使用。解析过程就是一些字符串的处理了(代码有剪切,详细代码可去查看源码)。

/**
     * 解析第一行 GET /test/hello?id=1&name=tio HTTP/1.1
     * */
    public static RequestLine parseRequestLine(String line) throws AioDecodeException{
            //GET /test/hello HTTP/1.1
            int index1 = line.indexOf(' ');
            //得到请求方法 GET
            String _method = StringUtils.upperCase(line.substring(0,index1));
            //转化为枚举的Method
            Method method = Method.from(_method);

            //截取路径  /test/hello
            int index2 = line.indexOf(' ',index1 + 1);
            // /test/hello
            String pathAndQueryStr = line.substring(index1 + 1,index2);
            //是否带有?参数
            int indexOfQuestionMark = pathAndQueryStr.indexOf("?");
            //URL上是否带参数,例如 ?user=123456
            if(indexOfQuestionMark != -1){
                queryStr = StringUtils.substring(pathAndQueryStr,indexOfQuestionMark + 1);
                path = StringUtils.substring(pathAndQueryStr,0,indexOfQuestionMark);
            }else{
                path = pathAndQueryStr;
                queryStr = "";
            }

            //HTTP/1.1
            String protocolVersion = line.substring(index2 + 1);
            String[] pv = StringUtils.split(protocolVersion,"/");
            //HTTP
            String protocol = pv[0];
            //1.1
            String version = pv[1];

          
            return requestLine;

    }

    接下来是头部内容:

    141019_8gWR_3669181.png

    每一行其实可以理解为键值对。比如: Connection:keey-alive,解析之后转化为 KeyValue 对象。

    最后,解析body,参数等信息。然后封装到HttpRequest中。

解析流程

    183640_g7V0_3669181.png

 

总结

    写的马马虎虎,终归自己去体验才能明白其中的原理。本篇源代码地址:https://gitee.com/tywo45/t-io/blob/master/src/zoo/http/common/src/main/java/org/tio/http/common/HttpRequestDecoder.java

转载于:https://my.oschina.net/panzi1/blog/1612006

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值