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


声明:本文转载自https://my.oschina.net/panzi1/blog/1612006,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

前言

    (本段瞎扯淡,可略)距离上一篇博客已经过去一个多月了,如果说没写博客的原因,大概是因为懒了吧。本篇还是关于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。呵呵,我真傻。。。

    

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

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,我们来看看服务器接收到的内容是什么:

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

    接下来继续分析源码。

    

    解析第一行的时候,执行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;      }

    接下来是头部内容:

    

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

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

解析流程

    

 

总结

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

本文发表于2018年01月23日 20:32
(c)注:本文转载自https://my.oschina.net/panzi1/blog/1612006,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。如有侵权行为,请联系我们,我们会及时删除.

阅读 3533 讨论 0 喜欢 0

抢先体验

扫码体验
趣味小程序
文字表情生成器

闪念胶囊

万稳万当,不如一默。任何一句话,你不说出来便是那句话的主人,你说了出来,便是那句话的奴隶。

你要过得好哇,这样我才能恨你啊,你要是过得不好,我都不知道该恨你还是拥抱你啊。

直抵黄龙府,与诸君痛饮尔。

那时陪伴我的人啊,你们如今在何方。

不出意外的话,我们再也不会见了,祝你前程似锦。

快捷链接
网站地图
提交友链
Copyright © 2016 - 2021 Cion.
All Rights Reserved.
京ICP备2021004668号-1