在前面《字节和字符,对信息进行编码》,《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;
}
}
}