25°

Beetlex实现完整的HTTP协议

在传统网络服务中扩展中需要处理Bytes来进行协议的读写,这种原始的处理方式让工作变得相当繁琐复杂,出错和调试的工作量都非常大;组件为了解决这一问题引用Stream读写方式,这种方式可以极大的简化网络协议读写的工作量,并大大提高协议编写效率。接下来就体验一下组件的PipeStream在处理一个完整的HTTP 1.1协议有多简便。

结构定义

HTTP 1.1协议就不详细介绍了,网上的资源非常丰富;在这里通过对象结束来描述这个协议

Request

    class HttpRequest
    {
    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> HttpVersion { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> Method { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> BaseUrl { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> ClientIP { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> Path { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> QueryString { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> Url { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> Dictionary&lt;<span style="color: #0000ff;">string</span>, <span style="color: #0000ff;">string</span>&gt; Headers { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">private</span> <span style="color: #0000ff;">set</span>; } = <span style="color: #0000ff;">new</span> Dictionary&lt;<span style="color: #0000ff;">string</span>, <span style="color: #0000ff;">string</span>&gt;<span style="color: #000000;">();

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">byte</span>[] Body { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">int</span> ContentLength { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> RequestStatus Status { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span>; } =<span style="color: #000000;"> RequestStatus.None;

}</span></pre> 

以上是描述一个HTTP请求提供了一些请求的详细信息和对应的请求内容

Response

      class HttpResponse : IWriteHandler
    {
    </span><span style="color: #0000ff;">public</span><span style="color: #000000;"> HttpResponse()
    {
        Headers[</span><span style="color: #800000;">"</span><span style="color: #800000;">Content-Type</span><span style="color: #800000;">"</span>] = <span style="color: #800000;">"</span><span style="color: #800000;">text/html</span><span style="color: #800000;">"</span><span style="color: #000000;">;
    }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> HttpVersion { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span>; } = <span style="color: #800000;">"</span><span style="color: #800000;">HTTP/1.1</span><span style="color: #800000;">"</span><span style="color: #000000;">;

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">int</span> Status { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> StatusMessage { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span>; } = <span style="color: #800000;">"</span><span style="color: #800000;">OK</span><span style="color: #800000;">"</span><span style="color: #000000;">;

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">string</span> Body { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }

    </span><span style="color: #0000ff;">public</span> Dictionary&lt;<span style="color: #0000ff;">string</span>, <span style="color: #0000ff;">string</span>&gt; Headers = <span style="color: #0000ff;">new</span> Dictionary&lt;<span style="color: #0000ff;">string</span>, <span style="color: #0000ff;">string</span>&gt;<span style="color: #000000;">();

    </span><span style="color: #0000ff;">public</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> Write(Stream stream)
    {
        </span><span style="color: #0000ff;">var</span> pipeStream =<span style="color: #000000;"> stream.ToPipeStream();
        pipeStream.WriteLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">{HttpVersion} {Status} {StatusMessage}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        </span><span style="color: #0000ff;">foreach</span> (<span style="color: #0000ff;">var</span> item <span style="color: #0000ff;">in</span><span style="color: #000000;"> Headers)
            pipeStream.WriteLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">{item.Key}: {item.Value}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        </span><span style="color: #0000ff;">byte</span>[] bodyData = <span style="color: #0000ff;">null</span><span style="color: #000000;">;
        </span><span style="color: #0000ff;">if</span> (!<span style="color: #0000ff;">string</span><span style="color: #000000;">.IsNullOrEmpty(Body))
        {
            bodyData </span>=<span style="color: #000000;"> Encoding.UTF8.GetBytes(Body);
        }
        </span><span style="color: #0000ff;">if</span> (bodyData != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
        {
            pipeStream.WriteLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">Content-Length: {bodyData.Length}</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        }

        pipeStream.WriteLine(</span><span style="color: #800000;">""</span><span style="color: #000000;">);
        </span><span style="color: #0000ff;">if</span> (bodyData != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
        {
            pipeStream.Write(bodyData, </span><span style="color: #800080;">0</span><span style="color: #000000;">, bodyData.Length);
        }
        Completed</span>?.Invoke(<span style="color: #0000ff;">this</span><span style="color: #000000;">);
    }

    </span><span style="color: #0000ff;">public</span> Action&lt;IWriteHandler&gt; Completed { <span style="color: #0000ff;">get</span>; <span style="color: #0000ff;">set</span><span style="color: #000000;">; }
}</span></pre> 

以上是对应请求的响应对象,实现了IWriteHandler接口,这个接口是告诉组件如何输出这个对象。

协议分析

结构对象有了接下来的工作就是把网络流中的HTTP协议数据读取到结构上.

请求基础信息

        private void LoadRequestLine(HttpRequest request, PipeStream stream)
        {
            if (request.Status == RequestStatus.None)
            {
                if (stream.TryReadLine(out string line))
                {
                    var subItem = line.SubLeftWith(' ', out string value);
                    request.Method = value;
                    subItem = subItem.SubLeftWith(' ', out value);
                    request.Url = value;
                    request.HttpVersion = subItem;
                subItem </span>= request.Url.SubRightWith(<span style="color: #800000;">'</span><span style="color: #800000;">?</span><span style="color: #800000;">'</span>, <span style="color: #0000ff;">out</span><span style="color: #000000;"> value);
                request.QueryString </span>=<span style="color: #000000;"> value;
                request.BaseUrl </span>=<span style="color: #000000;"> subItem;
                request.Path </span>= subItem.SubRightWith(<span style="color: #800000;">'</span><span style="color: #800000;">/</span><span style="color: #800000;">'</span>, <span style="color: #0000ff;">out</span><span style="color: #000000;"> value);
                </span><span style="color: #0000ff;">if</span> (request.Path != <span style="color: #800000;">"</span><span style="color: #800000;">/</span><span style="color: #800000;">"</span><span style="color: #000000;">)
                    request.Path </span>+= <span style="color: #800000;">"</span><span style="color: #800000;">/</span><span style="color: #800000;">"</span><span style="color: #000000;">;
                request.Status </span>=<span style="color: #000000;"> RequestStatus.LoadingHeader;
            }
        }
    }</span></pre> 

以上方法主要是分解出Method,UrlQueryString等信息。由于TCP协议是流,所以在分析包的时候都要考虑当前数据是否满足要求,所以组件提供TryReadLine方法来判断当前内容是否满足一行的需求;还有通过组件提供的SubRightWithSubLeftWith方法可以大简化了字符获取问题。

头部信息获取

        private void LoadRequestHeader(HttpRequest request, PipeStream stream)
        {
            if (request.Status == RequestStatus.LoadingHeader)
            {
                while (stream.TryReadLine(out string line))
                {
                    if (string.IsNullOrEmpty(line))
                    {
                        if (request.ContentLength == 0)
                        {
                            request.Status = RequestStatus.Completed;
                        }
                        else
                        {
                            request.Status = RequestStatus.LoadingBody;
                        }
                        return;
                    }
                    var name = line.SubRightWith(':', out string value);
                    if (String.Compare(name, "Content-Length", true) == 0)
                    {
                        request.ContentLength = int.Parse(value);
                    }
                    request.Headers[name] = value.Trim();
                }
            }
        }

头信息分析就更加简单,当获取一个空行的时候就说明头信息已经解释完成,接下来的就部分就是Body;由于涉及到Body所以需要判断一个头存不存在Content-Length属性,这个属性用于描述消息体的长度;其实Http还有一种chunked编码,这种编码是分块来处理最终以0\r\n\r\n结尾。这种方式一般是响应用得比较多,对于提交则很少使用这种方式。

读取消息体

        private void LoadRequestBody(HttpRequest request, PipeStream stream)
        {
            if (request.Status == RequestStatus.LoadingBody)
            {
                if (stream.Length >= request.ContentLength)
                {
                    var data = new byte[request.ContentLength]; ;
                    stream.Read(data, 0, data.Length);
                    request.Body = data;
                    request.Status = RequestStatus.Completed;
                }
            }
        }

读取消息体就简单了,只需要判断当前的PipeStream是否满足提交的长度,如果可以则直接获取并设置到request.Data属性中。这里只是获了流数据,实际上Http体的编码也有几种情况,在这里不详细介绍。这些实现在FastHttpApi中都有具体的实现代码,详细可以查看 https://github.com/IKende/FastHttpApi/blob/master/src/Data/DataConvertAttribute.cs

整合到服务

以上针对RequestResponse的协议处理已经完成,接下来就集成到组件的TCP服务中

        public override void SessionReceive(IServer server, SessionReceiveEventArgs e)
        {
            var request = GetRequest(e.Session);
            var pipeStream = e.Stream.ToPipeStream();
            if (LoadRequest(request, pipeStream) == RequestStatus.Completed)
            {
                OnCompleted(request, e.Session);
            }
        }
    </span><span style="color: #0000ff;">private</span><span style="color: #000000;"> RequestStatus LoadRequest(HttpRequest request, PipeStream stream)
    {
        LoadRequestLine(request, stream);
        LoadRequestHeader(request, stream);
        LoadRequestBody(request, stream);
        </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> request.Status;
    }
    </span><span style="color: #0000ff;">private</span> <span style="color: #0000ff;">void</span><span style="color: #000000;"> OnCompleted(HttpRequest request, ISession session)
    {
        HttpResponse response </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> HttpResponse();
        StringBuilder sb </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> StringBuilder();
        sb.AppendLine(</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;html&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine(</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;body&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;Method:{request.Method}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;Url:{request.Url}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;Path:{request.Path}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;QueryString:{request.QueryString}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;ClientIP:{request.ClientIP}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;Content-Length:{request.ContentLength}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        </span><span style="color: #0000ff;">foreach</span> (<span style="color: #0000ff;">var</span> item <span style="color: #0000ff;">in</span><span style="color: #000000;"> request.Headers)
        {
            sb.AppendLine($</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;p&gt;{item.Key}:{item.Value}&lt;/p&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        }
        sb.AppendLine(</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;/body&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);
        sb.AppendLine(</span><span style="color: #800000;">"</span><span style="color: #800000;">&lt;/html&gt;</span><span style="color: #800000;">"</span><span style="color: #000000;">);

        response.Body </span>=<span style="color: #000000;"> sb.ToString();
        ClearRequest(session);
        session.Send(response);
    }</span></pre> 

只需要在SessionReceive接收事件中创建相应的Request对象,并把PipeStream传递到相应的解释方法中,然后判断完成情况;当解释完成后就调用OnCompleted输出相应的Response信息,在这里简单地把当前请求信息输出返回.(在这里为什么要清除会话的Request呢,因为Http1.1是长连接会话,必须每个请求都需要创建一个新的Request对象信息)。

这样一个基于BeetleX解释的Http服务就完成了,是不是很简单。接下来简单地测试一下性能,在一台e3-1230v2+10Gb的环境压力测试

测试结果的有15万的RPS,虽然这样一个简单服务但效率并不理想,相对于也是基于组件扩展的FastHttpApi来说还是有些差距,为什么简单的代码还没有复杂的框架来得高效呢,其实原因很简单就是对象复用和string编码缓存没有用上,导致开销过大(这些细节上的性能优化有时间会在后续详解)。

下载完整代码 https://github.com/IKende/BeetleX-Samples/tree/master/TCP.BaseHttp

本文转载自博客园,原文链接:https://www.cnblogs.com/smark/p/12205438.html

全部评论: 0

    我有话说: