Beetlex實現完整的HTTP協議


在傳統網絡服務中擴展中需要處理Bytes來進行協議的讀寫,這種原始的處理方式讓工作變得相當繁瑣復雜,出錯和調試的工作量都非常大;組件為了解決這一問題引用Stream讀寫方式,這種方式可以極大的簡化網絡協議讀寫的工作量,並大大提高協議編寫效率。接下來就體驗一下組件的PipeStream在處理一個完整的HTTP 1.1協議有多簡便。

結構定義

HTTP 1.1協議就不詳細介紹了,網上的資源非常豐富;在這里通過對象結束來描述這個協議

Request

    class HttpRequest
    {

        public string HttpVersion { get; set; }

        public string Method { get; set; }

        public string BaseUrl { get; set; }

        public string ClientIP { get; set; }

        public string Path { get; set; }

        public string QueryString { get; set; }

        public string Url { get; set; }

        public Dictionary<string, string> Headers { get; private set; } = new Dictionary<string, string>();

        public byte[] Body { get; set; }

        public int ContentLength { get; set; }

        public RequestStatus Status { get; set; } = RequestStatus.None;

    }

以上是描述一個HTTP請求提供了一些請求的詳細信息和對應的請求內容

Response

      class HttpResponse : IWriteHandler
    {

        public HttpResponse()
        {
            Headers["Content-Type"] = "text/html";
        }

        public string HttpVersion { get; set; } = "HTTP/1.1";

        public int Status { get; set; }

        public string StatusMessage { get; set; } = "OK";

        public string Body { get; set; }

        public Dictionary<string, string> Headers = new Dictionary<string, string>();

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

            pipeStream.WriteLine("");
            if (bodyData != null)
            {
                pipeStream.Write(bodyData, 0, bodyData.Length);
            }
            Completed?.Invoke(this);
        }

        public Action<IWriteHandler> Completed { get; set; }
    }

以上是對應請求的響應對象,實現了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 = request.Url.SubRightWith('?', out value);
                    request.QueryString = value;
                    request.BaseUrl = subItem;
                    request.Path = subItem.SubRightWith('/', out value);
                    if (request.Path != "/")
                        request.Path += "/";
                    request.Status = RequestStatus.LoadingHeader;
                }
            }
        }

以上方法主要是分解出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);
            }
        }

        private RequestStatus LoadRequest(HttpRequest request, PipeStream stream)
        {
            LoadRequestLine(request, stream);
            LoadRequestHeader(request, stream);
            LoadRequestBody(request, stream);
            return request.Status;
        }
        private void OnCompleted(HttpRequest request, ISession session)
        {
            HttpResponse response = new HttpResponse();
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<html>");
            sb.AppendLine("<body>");
            sb.AppendLine($"<p>Method:{request.Method}</p>");
            sb.AppendLine($"<p>Url:{request.Url}</p>");
            sb.AppendLine($"<p>Path:{request.Path}</p>");
            sb.AppendLine($"<p>QueryString:{request.QueryString}</p>");
            sb.AppendLine($"<p>ClientIP:{request.ClientIP}</p>");
            sb.AppendLine($"<p>Content-Length:{request.ContentLength}</p>");
            foreach (var item in request.Headers)
            {
                sb.AppendLine($"<p>{item.Key}:{item.Value}</p>");
            }
            sb.AppendLine("</body>");
            sb.AppendLine("</html>");

            response.Body = sb.ToString();
            ClearRequest(session);
            session.Send(response);
        }

只需要在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM