蛙蛙推荐:自己写个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  ==   - 1 return   null ;
            
return  _encoding.GetString(Bytes,  0 , Pos1);
        }
    }
    
public   string  String2 {
        
get  {
            
if  (Pos1  ==   - 1 return   null ;
            
return  _encoding.GetString(Bytes, Pos1  +   1 , Size  -  Pos1  -   1   - 2 );
        }
    }
    
public   string  FullString
    {
        
get  {
            
if (Size  <   1 return   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 < string string >  Headers  =   new  Dictionary < string string > ();
    
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  ==   - 1 throw   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  !=   3 throw   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  !=   3 throw   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  ==   null return ;
                
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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值