在傳統網絡服務中擴展中需要處理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
,Url
和QueryString
等信息。由於TCP協議是流,所以在分析包的時候都要考慮當前數據是否滿足要求,所以組件提供TryReadLine
方法來判斷當前內容是否滿足一行的需求;還有通過組件提供的SubRightWith
和SubLeftWith
方法可以大簡化了字符獲取問題。
頭部信息獲取
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
整合到服務
以上針對Request
和Response
的協議處理已經完成,接下來就集成到組件的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