蛙蛙推薦:自己寫個IIS玩-協議解析篇

這裏不是說用System.Web.Hosting.ApplicationHost和System.Net.HttpListener做的那種web server,而是直接用socket api做一個簡單的能收發HTTP包的網絡服務器,當然也不會完全實現RFC 2616,主要學習探索用。

我們先來看HTTP協議解析部分,做一個HTTP協議棧-HttpStatck,大概看一下HTTP協議基礎,
1、消息頭和消息體中間用兩個/r/n(0x0d0x0a)來分割,
2、消息頭之間用/r/n分割,
3、消息頭的個數不定,但有最大數,
4、消息體的大小根據Content-Length頭來確定,
5、消息頭的名字和值用英文半角冒號分割
6、消息頭的第一行用來標識協議是request還是response,及協議的版本,請求的方法,應答碼,應答描述

協議瞭解了,協議棧就好寫了,如果我們能一次讀取一個完整的包,那我們把整個包讀出來,解析成字符串,然後用IndexOf,Split等函數很快的就能解析出一個個都HttpRequest和HttpResponse,但是真是的網絡中,你可能只能解析到半個半個多包,沒準連消息頭的第一行都分兩次才能接受到,甚至像一箇中文字符也有可能會收兩次才能包才能解析成字符串。我們要想提高效率,儘量避免把bytes解析成字符串,另外我們只解析出header給上層應用就行了,body的話暴露成一個Stream就行了,因爲你不知道Body的格式,由應用去做處理吧,asp.net也是這樣的,有對應的InputStream和OutStream。

下面是具體的性能方面的分析。


1、在Stack收到異步讀取的網絡包後,首先繼續調用BeginReceive方法,然後再解析收到的包,這是爲了防止在解析包的時候出錯,或者線程掛起而造成無法接受剩下的包,當然每次儘量多讀取一些字節,讀取次數多也會降低性能,buffer可以設置的稍微大一些,這個可能要經過具體平臺的測試才能確定最合適的值。這點有不同意見,說不要在剛收到異步讀取回調後就先BeginReceive,應該把包收完再BeginReceive,否則如果本次沒收完包,剩下的包只能在其它的IOCP線程裏接收,影響性能,這個我不確認,但是一次接受完緩衝區的所有數據是可以做到的,用Socket.IOControl(FIONREAD, null, outValue)或者socket.Available可以獲取接受緩衝區有多少數據,然後把這些數據收完;但是微軟反對使用這些方法去探察socket的接受數據大小,因爲執行這個方法系統需要內部使用鎖鎖定數據計算這個值,降低socket效率。關於接受包這裏的最佳實踐,歡迎大家討論。
2、按理說收到包後先放隊列裏,再調用解析包方法,解析包的方法順序從隊列裏取包解析,但解析包和接受包可以都在一個線程裏,沒有必要引入單獨的解析包線程,最後還是考慮不使用隊列,每次直接把收到的字節數組進行解析。原則是我們儘量讓一個線程只適用本線程的私有數據,而不去用全局共享的數據,如果要使用別的線程的數據,就給那個線程發個消息,讓那個線程自己去處理自己線程的數據,而不要直接操作不屬於自己的數據,那樣的話那個數據就得用加鎖之類的線程同步了。線程模型的確定很重要。
3、按理說解析網絡包推薦用Encoding.UTF8.GetDecoder().GetChars()方法,該方法會維持utf8解析狀態,在收到不能解析成一個完整的unicode字符的包的字節數組的時候它可以保存剩下的半截兒包,和下次收到的包一起解析,而不會造成包丟失。但是該方法的參數只能傳入一個char數組,然後我們有可能把多個char數組進行內存拷貝,這就浪費了性能,所以不考慮了。如果該方法能把解析出來的char數組自動填充到一個字節環形鏈表裏,我們就可以考慮用它。我們儘量使用.NET自己提供的功能,但是如果不滿足我們的需求的時候,我們就得自己實現去,當然可以反射.NET程序集,借鑑他的做法。
4、我們應該儘量避免把收到的字節數組解析成字符串,然後再按包的規則進行解析,因爲把字節數組轉換成字符串也是個耗時的過程,像一些解析包的標誌位如分割消息頭和消息體的/r/n/r/n,分割多個消息頭的/r/n,其對應的字節表示值是固定的,如0d0a0d0a,0d0a,我們直接對字節數組進行解析就能區拆出來消息頭字節數組和消息體字節數組。
5、對字符串的操作我們可以用正則表達式,用string類的方法等,但對字節數組就沒這麼多的API了,但是我們可以去了解一下正則表達式的原理,先寫出正則正則表達式,再推導出對應的NFA算法,再推導出對應的DFA算法,就可以寫出針對字節數組的算法了。典型的場景是我們需要讀取到字節數組裏的0d0a0d0a的token,或者我們知道了表示消息頭的字節數組,我們要把這些字節數組按照0d0a分割成多個子數組,然後再對每個子數組進行utf-8.getstring,這應該比把整個header字節數組轉換成字符串再split性能好一些,因爲split會臨時生成多個小字符串,引起很多對象分配操作。其實我們並不應該把大字節數組分割成小字節數組,我們就找到0d0a的位置,然後用utf-8.getstring(bytes,index,length)來按段兒來提取每一行的消息頭。
6、爲了防止對接受到的字節數組進行內存拷貝,我們應該把接受到的字節數組放到一個鏈表裏,因爲我們是順序插入字節,解析的時候也是順序訪問字節數組,所以我認爲這裏應該用鏈表,而且鏈表的API完全滿足消息解析的要求,如果構建一個環形的字節數組,操作起來比鏈表複雜,而且性能應該也不會比字節鏈表好。
7、在字節鏈表上,我們只要找到對應的包的開頭、結尾節點,然後我們就可以把這段兒鏈表賦值給包對象,然後包對象自己去把這段兒鏈表換算成一個字節數組,進行相應的處理,比如轉換成字符串,進一步解析每行的header,但有的服務只解析出header就可以處理這個包,比如轉發給另一個服務,那麼body就不需要轉換成字節數組,更不用轉換成字符串,直接把屬於Body的那段兒字節鏈表(可以進一步封裝成Stream)傳出去就行了。
8、剛開始我在收到字節數組後要先把字節數組fill到字節鏈表裏,這個過程會無謂的消耗一些性能,所以我又優化了一下,把字節鏈表改成了字節數組鏈表,但改成字節數組鏈表後,遍歷起來很麻煩,有的鏈表節點上的字節數組有半截兒已經解析給上個包了,下次解析要接着上次解析的地方去解析,所以每個字節數組節點還要保存一個有效數組段兒的開始位置和結束位置,比第一次的代碼更復雜了一些,但是性能要好於前者,
9、還有就是在收到一個半截header或者半截body的情況下,下一次收到包解析的時候儘量避免回溯,比較好的算法是儘量遍歷一次就匹配出所有規則,DFA就是這樣,但得加更多的標誌位來保存解析狀態。
10、在解析header的時候也避免先把字節數組鏈表轉換成字節數組,會造成字節數組拷貝,應該一次字節數組鏈表的遍歷就直接解析出所有header,當然可能會跨越多個字節數組節點,但比把多個字節數組節點合併成一個大的字節數組再解析header性能要好不少。

下面來具體看下代碼
BytesLine,表示header中的一行,因爲消息頭不會出現中文,所以直接用ASCII編碼,除了header的第一行,消息頭都分爲name,value部分,這裏用String1和String2表示

public class BytesLine {
    
private Encoding _encoding = Encoding.ASCII;
    
public BytesLine() {
    }
    
public BytesLine(Encoding encoding) {
        _encoding 
= encoding;
    }

    
public byte[] Bytes = new byte[256];
    
public int Pos1 = -1, Size = -1;
    
public string String1 {
        
get {
            
if (Pos1 == -1return null;
            
return _encoding.GetString(Bytes, 0, Pos1);
        }
    }
    
public string String2 {
        
get {
            
if (Pos1 == -1return null;
            
return _encoding.GetString(Bytes, Pos1 + 1, Size - Pos1 - 1 -2);
        }
    }
    
public string FullString
    {
        
get {
            
if(Size < 1return null;
            
return _encoding.GetString(Bytes, 0, Size-2);
        }
    }
}

BytesNode,該類表示字節數組鏈表中的一個節點,其中Next屬性指向鏈表中的下一個節點,其餘的都是一個幫助性的方法和屬性,比如該節點已經解析到什麼位置了,有效字節的結束爲止,及如何把自己切成兩個,獲取有效字節數組,把有效字節數組解析成字符串等方法。該類儘量做成不變類,成員能用readonly就用readonly,這樣可以在多線程的時候防止加鎖。

public class BytesNode{
    
private readonly byte[] _bytes;
    
public BytesNode Next;
    
int _start;
    
int _end;
    
public BytesNode(byte[] bs) {
        _bytes 
= bs;
        _start 
= 0;
        _end 
= bs.Length;
    }
    
private BytesNode(byte[] bs, int start, int end) {
        _bytes 
= bs;
        _start 
= start;
        _end 
= end;
    }
    
public BytesNode CutNew(BytesNode preNode, int start, int end) {
        BytesNode tempNode 
= new BytesNode(_bytes, start, end);
        
if(preNode != null)preNode.Next = tempNode;
        tempNode.Next 
= Next;
        
return tempNode;
    }
    
public void Cut(int start, int end) {
        _start 
= start;
        _end 
= end;
    }
    
public int Start {
        
get { return _start; }
    }
    
public int End {
        
get { return _end; }
    }
    
public int Length {
        
get { return _end - _start; }
    }
    
public byte[] Value {
        
get { return _bytes; }
    }

    
public byte[] Bytes {
        
get {
            List
<byte> ret = new List<byte>();
            BytesNode tempNode 
= this;
            
byte[] tempBs;
            
while (tempNode != null) {
                tempBs 
= new byte[tempNode.Length];
                Buffer.BlockCopy(tempNode.Value, tempNode.Start, tempBs, 
0, tempNode.Length);
                ret.AddRange(tempBs);
                tempNode 
= tempNode.Next;
            }
            
return ret.ToArray();
        }
    }
    
public string GetString()
    {
        
return GetString(Encoding.UTF8);
    }
    
public string GetString(Encoding encoding)
    {
        Decoder decoder 
= encoding.GetDecoder();
        StringBuilder sb 
= new StringBuilder();
        BytesNode tempNode 
= this;
        
while (tempNode != null)
        {
            
char[] chars = new char[decoder.GetCharCount(tempNode.Value, tempNode.Start, tempNode.Length)];
            decoder.GetChars(tempNode.Value, tempNode.Start,
                             tempNode.Length, chars, 
0);
            sb.Append(chars);
            tempNode 
= tempNode.Next;
        }
        
return sb.ToString();
    }
}

HttpMessage,這裏表示一個抽象的Http消息,除了包含消息頭,消息體等屬性外,還負責初始化消息頭,解析消息體長度,確認消息是Request,Response等功能。

 

public class HttpMessage
{
    
public const string PROTOCOL = "HTTP";
    
public const string CONTENT_LENGTH_HEADER = "Content-Length";
    
public MessageType MessageType = MessageType.UnKnow;
    
public BytesNode BodyStr;
    
public int ContentLength;
    
public Dictionary<stringstring> Headers = new Dictionary<stringstring>();
    
internal BytesNode HeaderStr;
    
public string Protocol;
    
private string startLine;
    
public object SyncRoot = new object(); //todo:暫時沒用

    
public HttpMessage()
    {
    }

    
public HttpMessage(HttpMessage message)
    {
        startLine 
= message.startLine;
        Headers 
= message.Headers;
        BodyStr 
= message.BodyStr;
    }

    
internal void InitHeaders(List<BytesLine> lines)
    {
        
if (MessageType == MessageType.UnKnow)
        {
            
#region 解析MessageType,ContentLength及填充消息頭
            
for (int i = 0; i < lines.Count; i++)
            {
                BytesLine line 
= lines[i];
                
if (i == 0)
                {
                    
string tempStr = line.FullString;
                    MessageType 
= tempStr.StartsWith(PROTOCOL)
                                   
?
                                       MessageType.Response
                                   : MessageType.Request;
                    startLine 
= tempStr;
                    
continue;
                }
                
if (line.Pos1 == -1throw new ApplicationException("header line error:"
                    
+ line.FullString);
                
//todo:暫時不考慮多個同名的頭
                Headers[line.String1] = line.String2;
                
if (Headers.ContainsKey(CONTENT_LENGTH_HEADER))
                    ContentLength 
= int.Parse(Headers[CONTENT_LENGTH_HEADER].Trim());
            }

            
#endregion
        }
    }

    
internal HttpRequest AsRequest()
    {
        
if (MessageType != MessageType.Request)
            
throw new ApplicationException("this message is not request");
        HttpRequest request 
= new HttpRequest(this);
        
string[] tempArr = startLine.Split(' ');
        
if (tempArr.Length != 3throw new ApplicationException("start line error:" + startLine);
        request.Method 
= tempArr[0].Trim();
        request.Uri 
= tempArr[1];
        request.Protocol 
= tempArr[2];
        
if (!request.Protocol.StartsWith(PROTOCOL))
            
throw new ApplicationException("Protocol error:" + request.Protocol);
        
return request;
    }

    
internal HttpResponse AsResponse()
    {
        
if (MessageType != MessageType.Response)
            
throw new ApplicationException("this message is not response");
        HttpResponse response 
= new HttpResponse(this);
        
string[] tempArr = startLine.Split(' ');
        
if (tempArr.Length != 3throw new ApplicationException("start line error:" + startLine);
        response.Protocol 
= tempArr[0];
        
if (!response.Protocol.StartsWith(PROTOCOL))
            
throw new ApplicationException("Protocol error:" + response.Protocol);
        response.StatusCode 
= int.Parse(tempArr[1].Trim()); //todo:可能有200.1這樣的應答
        response.Desc = tempArr[2]; //todo:不考慮應答描述包含空格的情況

        
return response;
    }

    
public override string ToString()
    {
        StringBuilder sb 
= new StringBuilder();
        sb.Append(startLine);
        sb.AppendLine();
        
foreach (KeyValuePair<string,string> pair in Headers)
        {
            sb.AppendFormat(
"{0}:{1}", pair.Key, pair.Value);
            sb.AppendLine();
        }
        sb.AppendLine();
        sb.AppendLine();
        
if (BodyStr != null) sb.Append(Encoding.UTF8.GetString(BodyStr.Bytes));
        
return sb.ToString();
    }
}

HttpParser,主要的協議解析類,入口是Parse方法,可以把每次socket收到的字節數組去調用該方法,然後訂閱RequestReceived,ResponseReceived,Error等方法。具體的算法看代碼吧,說不清楚。

namespace WawaSoft.HttpStack {
    
class NodeIndex {
        
public NodeIndex(BytesNode node, int index) {
            _node 
= node;
            _index 
= index;
        }
        
private BytesNode _node;
        
private int _index;
        
public BytesNode Node {
            
get { return _node; }
        }
        
public int Index {
            
get { return _index; }
        }
    }
    
public class HttpParser {
        
private HttpMessage _currentMessage;
        
private object _syncRoot = new object();
        
private BytesNode _headerNode;
        
private BytesNode _tailNode;
        
private bool _waitParseBody = false;

        
public void Parse(byte[] tempBuffer) {
            
lock (_syncRoot) {
                
try {
                    SetNodes(tempBuffer);
                    
if (_waitParseBody)
                        ReadBody(_currentMessage);
                    
else {
                        ReadHeaders();
                    }
                    fireEvent();
                    
if(!_waitParseBody)ReadHeaders();
                }
                
catch (Exception ex) {
                    Action
<Exception> temp = Error;
                    
if (temp != null)
                        temp(ex);
                }
            }
        }

        
private void ReadHeaders() {
            NodeIndex headerTokenIndex 
= ContainsHeaderEndToken(_headerNode);
            
while (headerTokenIndex != null) {
                _currentMessage 
= new HttpMessage();
                _currentMessage.HeaderStr 
= _headerNode;
                _headerNode 
= headerTokenIndex.Node.CutNew(null,
                    headerTokenIndex.Index
+1,
                    headerTokenIndex.Node.Value.Length);
                headerTokenIndex.Node.Cut(headerTokenIndex.Node.Start, headerTokenIndex.Index);
                headerTokenIndex.Node.Next 
= null;
                _currentMessage.InitHeaders(_lines);
                _lines.Clear();
                ReadBody(_currentMessage);
                
if (_waitParseBody)
                    
break;
                
else
                    fireEvent();
                headerTokenIndex 
= ContainsHeaderEndToken(_headerNode);
            }
        }

        
private void fireEvent() {
            
if (!_waitParseBody) {
                
if (_currentMessage == nullreturn;
                
if (_currentMessage.MessageType == MessageType.Request) {
                    HttpRequest request 
= _currentMessage.AsRequest();
                    Action
<HttpRequest> temp = RequestReceived;
                    
if (temp != null)
                        temp(request);
                }
                
else {
                    HttpResponse response 
= _currentMessage.AsResponse();
                    Action
<HttpResponse> temp = ResponseReceived;
                    
if (temp != null)
                        temp(response);
                }
                _currentMessage 
= null;
            }
        }

        
private void ReadBody(HttpMessage message) {
            
if (message.ContentLength == 0) {
                _waitParseBody 
= false;
                
return;
            }
            
int i = 0, pos = 0;
            
bool first = true;
            BytesNode tempNode 
= _headerNode;
            BytesNode previousNode 
= tempNode;
            
while (tempNode != null) {
                i 
= i + tempNode.Length;
                
if (i >= message.ContentLength) {
                    
if (first)
                        pos 
= tempNode.Start + message.ContentLength;
                    
else
                        pos 
= tempNode.Length - (i - message.ContentLength);
                    
break;
                }
                first 
= false;
                previousNode 
= tempNode;
                tempNode 
= tempNode.Next;
            }
            
if (i >= message.ContentLength) {
                tempNode.Cut(tempNode.Start, pos );
                message.BodyStr 
= _headerNode;
                _headerNode 
= tempNode.CutNew(null, pos, tempNode.Value.Length);
                tempNode.Next 
= null;
                _waitParseBody 
= false;
            }
            
else
                _waitParseBody 
= true;
        }

        
private void SetNodes(byte[] tempBuffer) {
            BytesNode tempNode 
= new BytesNode(tempBuffer);
            
if (_headerNode == null)
                _tailNode 
= _headerNode = tempNode;
            
else if (_headerNode.Next == null) {
                _headerNode.Next 
= tempNode;
                _tailNode 
= tempNode;
            }
            
else if (_tailNode != null) {
                _tailNode.Next 
= tempNode;
                _tailNode 
= tempNode;
            }
        }

        BytesLine _line 
= new BytesLine(Encoding.ASCII);
        List
<BytesLine> _lines = new List<BytesLine>();
        
internal NodeIndex ContainsHeaderEndToken(BytesNode node) {
            
if (_waitParseBody)
                
return null;
            _lines.Clear();
            
bool secondBackslashN = false;
            
bool firstBackslashR = false;
            
byte expectNextChar = 0x0d;
            BytesNode previousNode 
= null;
            
int k = -1;
            
while (node != null) {
                
int end = node.End;
                
byte[] nodeBytes = node.Value;
                
for (int i = node.Start; i < end; i++) {
                    k
++;
                    
byte tempByte = nodeBytes[i];
                    _line.Bytes[k] 
= tempByte;
                    _line.Size 
= k;
                    
if(tempByte == 0x3a)
                        _line.Pos1 
= k;
                    
if ((secondBackslashN || firstBackslashR) && tempByte != expectNextChar) {
                        
if (firstBackslashR && secondBackslashN) {
                            _lines.Add(_line);
                            _line 
= new BytesLine();
                            _line.Bytes[
0= tempByte;
                            k 
= 0;
                        }
                        firstBackslashR 
= false;
                        secondBackslashN 
= false;
                    }
                    
if (tempByte != expectNextChar) {
                        
continue;
                    }
                    
if (expectNextChar == 0x0d) {
                        firstBackslashR 
= true;
                        expectNextChar 
= 0x0a;
                        
continue;
                    }
                    
if (expectNextChar == 0x0a) {
                        
if (!secondBackslashN) {
                            expectNextChar 
= 0x0d;
                            secondBackslashN 
= true;
                            
continue;
                        }
                        _line.Size
--;
                        _lines.Add(_line);
                        _line 
= new BytesLine();
                        _line.Bytes[
0= tempByte;
                        k 
= 0;
                        
return new NodeIndex(node, i);
                    }
                    
                }
                previousNode 
= node;
                node 
= node.Next;
            }
            
return null;
        }

        
public event Action<HttpRequest> RequestReceived;
        
public event Action<HttpResponse> ResponseReceived;
        
public event Action<Exception> Error;
    }
}    

代碼下載:http://files.cnblogs.com/onlytiancai/HttpStack.zip

相關鏈接
蛙蛙推薦:蛙蛙教你解析網絡包
http://www.cnblogs.com/onlytiancai/archive/2008/07/26/unpack_network_package.html
蛙蛙推薦:用winsock和iocp api打造一個echo server
http://www.cnblogs.com/onlytiancai/archive/2008/07/15/echo_server_using_csharp_and_iocp.html
蛙蛙推薦:在c#使用IOCP(完成端口)的簡單示例
http://www.cnblogs.com/onlytiancai/archive/2008/07/12/iocp_demo_in_csharp.html
蛙蛙推薦:c#使用winsock api實現同步Socket服務端
http://www.cnblogs.com/onlytiancai/archive/2008/07/12/1241317.html
翻譯:使用.net3.5的緩存池和SocketAsyncEventArgs類創建socket服務器
http://www.cnblogs.com/onlytiancai/archive/2008/06/25/1229321.html
討論:零拷貝和環形隊列緩存隊列問題
http://www.cnblogs.com/onlytiancai/archive/2008/06/16/1223385.html

參考鏈接
在沒有 IIS 的條件下利用HttpListener 創建自己的Web服務器運行 ASMX和asp.net
http://www.cnblogs.com/lymph/articles/468954.html
通過HttpListener實現簡單的Http服務
http://www.cnblogs.com/tianfang/archive/2007/01/03/610636.html
RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1
http://www.faqs.org/rfcs/rfc2616.html
爲什麼socket.Available老是爲0 
http://topic.csdn.net/u/20081030/14/1ceb237c-0566-4597-9c4f-20252218715b.html
INFO: 避免使用 Winsock 中查看的數據
http://support.microsoft.com/kb/192599/zh-cn
請教socket連接後,read一次能接受信息包的大小??、
http://www.unixresources.net/linux/clf/program/archive/00/00/35/17/351750.html

發佈了183 篇原創文章 · 獲贊 4 · 訪問量 38萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章