在前面《字節和字符,對信息進行編碼》,《Socket=>流,TCP連接,TCP可靠性概述》一系列的隨筆中我們已經表述了相應的理論知識,現在可以動手實現一個自己的應用程序協議。
將 數據轉換成在線路上傳輸的字節序列只完成了一半的工作,在接收端還必須將接受到的字節序列還原成原始信息。如果以流作為傳輸的形式,那么首先面臨的問題就 是在接收端如何確定這是一條消息,換句話說就是如何定位一條消息的開始和結束。值得注意的是,這個工作應該是在應用程序協議這一層來完成而不是在TCP這 一層來完成,應用程序協議必須指定消息的接受者如何確定何時消息已完整接收。
TCP協議中沒有消息邊界的概念,這會讓我們在解析信息的時候產生一些問題。
如果接收者試圖從套接字中讀取比消息本身更多的字節,將可能發生以下兩種情況:
1.如果信道中沒有其他消息,接收者將阻塞等待,同時無法處理接收到的消息;如果發送者也在等待接收端的響應消息,那么就會造成“死鎖”
2.如果信道中還有其他消息,則接收者會將后一條的消息的一部分甚至全部讀取到第一條消息中,這將會產生一些“協議錯誤”
因此,在時候流TCP套接字的時候,成幀就是一個非常重要的考慮因素。
對于成幀,主要有兩個技術能使接收者能夠準確地找到消息的結束位置:
1.消息的結束由一個特殊的標記指明,比如把一個特殊的字節序列0001等顯式添加到一個消息的結束位置。這里的限制就在于傳輸的內容中不能包含和該特殊字節序列中一樣的字符。就像HTML中符號不能直接包含在輸出中,這時需要轉義。
2.顯式的告知長度。
在變長字段或消息前面附加一個固定的字段,用來表示該字段或者消息中包含了多少個字節。
我們來寫一個網絡上常見的投票來作為例子:
這個例子包含了兩種類型的請求,一種是“查詢”的請求,也就是查詢當前的候選人獲得的選票情況。
第二種是“投票”請求,服務器保存此次投票信息,并返回投完票后該候選人獲得的結果。
在實現一個協議的時候,定義一個專門的類來存放消息中所包含的信息是大有裨益的。類提供了給我們封裝的能力,通過屬性來公開類中的可變字段,也可以維護一些不變的字段。
我在這里采用的發送消息大小的方式來確定一條完整的消息。
項目結構和功能說明如下:
IFramer接口的定義:
namespaceVoteForMyProtocol
{publicinterfaceIFramer
{voidframeMsg(byte[] message);byte[] nextMsg();
}
}
基于長度成幀的實現:
usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;usingSystem.Text;usingSystem.Net.Sockets;usingSystem.IO;namespaceVoteForMyProtocol
{publicclassLengthFramer : IFramer {publicstaticreadonlyintMAXMESSAGELENGTH=65535;
Socket s=null;publicLengthFramer(Socket s)
{this.s=s;
}//把消息成幀并發送publicvoidframeMsg(byte[] message){if(message.Length>MAXMESSAGELENGTH) {thrownewIOException ("message too long");
}inttotalSent=0;intdataLeft=message.Length;//剩余的消息intthisTimeSent;//保存消息長度byte[] datasize=newbyte[4];
datasize=BitConverter.GetBytes(message.Length);//將消息長度發送出去thisTimeSent=s.Send(datasize);//發送消息剩余的部分while(totalSent
{
thisTimeSent=s.Send(message, totalSent, dataLeft, SocketFlags.None);
totalSent+=thisTimeSent;
dataLeft-=thisTimeSent;
}
}//按幀來解析消息publicbyte[] nextMsg(){if(s==null)thrownewArgumentNullException("socket null");inttotal=0;//已接收的字節數intrecv;//接收4個字節,得到“消息長度”byte[] datasize=newbyte[4];//如果當前使用的是面向連接的 Socket,則 Receive 方法將讀取所有可用的數據,直到達到 size 參數指定的字節數。//如果遠程主機使用 Shutdown 方法關閉了 Socket 連接,并且所有可用數據均已收到,則 Receive 方法將立即完成并返回零字節。recv=s.Receive(datasize,0,4,0);if(recv<4)returnnull;intsize=BitConverter.ToInt32(datasize,0);//按消息長度接收數據intdataleft=size;//容器裝滿了就證明收集到了一條完整的消息。byte[] data=newbyte[size];//直到容器填滿再返回while(total
{
recv=s.Receive(data, total, dataleft,0);
total+=recv;
dataleft-=recv;if(dataleft==0)
{break;
}
}returndata;
}
}
}