1. 粘包,分包问题
根据[一]篇的做法,如果客户端快速连续向客户端发送多条信息:
for(int i = 0; i < 100; i++)
{
clientSocket.Send(Encoding.UTF8.GetBytes(i.ToString()));
}
那么服务端将不会分开显示,发送的连续的多条记录有可能被服务端认为是客户端只发送一条数据,因为时间间隔太短所以被认为是同时发送过来的.这就是粘包问题.
如果一次性发送一条很大的数据,一个缓存数组容纳不下,分多次接收,有可能被认为是客户端发送过来的多条信息.如果缓存数组能存5个字节,那么如果发送2个汉字(每个汉字占3个字节),则第二个汉字还会被分离成前面2个字节,后面再接收1个字节,导致乱码,这些都是分包的问题.
2. 解决思路
如果客户端每次给服务器发送数据时,都告诉服务器这一次发送的数据是多少个字节,那么服务器就可以正确的解析客户端发送过来的数据了.
客户端在发送的每条数据的前面,先说明这条数据有多少个字节,服务端收到后就将后面相应的字节当成一组数据来解析,就可以解决粘包和分包的问题了.
需要注意的是,声明这条数据字节数的用int32,所以服务端每次接收数据时前面固定要以4个字节为单位进行解析,确认这条数据有多少个字节.
3. 代码操作
首先,为客户端添加一个方法,用来包装要发送的数据
/// <summary>
/// 加工要发送的数据
/// </summary>
/// <param name="data">要发送的数据</param>
/// <returns>头部带数据组长度的数据</returns>
public static byte[] GetBytes(string data)
{
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
int dataLength = dataBytes.Length;
byte[] lengthBytes = BitConverter.GetBytes(dataLength);
byte[] newBytes = lengthBytes.Concat(dataBytes).ToArray();//合并为一个比特数据
return newBytes;
}
同样的,我们用循环,快速的向服务器发送信息
for(int i = 0; i < 100; i++)
{
clientSocket.Send(GetBytes(i.ToString()));
}
这次发送的数据就很稳的在头4个字节储存了这次发送的比特数,往后的相应比特会被当成是一个数据处理.
接下来,就是为服务器端创建一个类Message专门用来解析客户端发送过来的数据
class Message
{
private byte[] data = new byte[1024];//存储读到的数据
private int startIndex = 0;//表示当前存储到data的位置的索引
public byte[] Data
{
get { return data; }
}
public int StartIndex
{
get { return startIndex; }
}
public int RemainSize
{
get { return data.Length - startIndex; }//剩余空间
}
}
data就是缓存从客户端接收到的数据,随着数据的增加,starIndex会增加
/// <summary>
/// 当data存储入一组数据之后,starIndex也要相应的增加,以方便添加下一组数据
/// </summary>
/// <param name="cound">数据个数</param>
public void AddCound(int cound)
{
startIndex += cound;
}
接下来就是核心方法,解析客户端发送过来的数据,并输出
/// <summary>
/// 核心:数据解析
/// </summary>
public void ReadMessage()
{
while(true)
{
if (startIndex <= 4) return;//前4个字节存储的是当前所取数据的长度标识,是int32类型,所以如果当前所存储的位置小于这个数说明还没有数据需要读取
int count = BitConverter.ToInt32(data, 0);//从缓存数组里的data[0]开始读取4个byte(因为是int32所以读取的是前4个byte),得到的就是这组数据的长度
//如果当前已存储到的位置大于这组数据的长度,说明这组数据是完整的,可以一次性读取出来并输出
if((startIndex-4)>=count)
{
string s = Encoding.UTF8.GetString(data, 4, count);
Console.WriteLine("解析出一条数据:" + s);
//解析完数据需要将后面的数据前移,后面的数据量:就是当前数据结束的索引,到starIndex
//将第一个参数从data[count+4]-(startIndex - 4 - count)个数据复制到第三个参数的data[0]-(startIndex - 4 - count)里
Array.Copy(data, count + 4, data, 0, startIndex - 4 - count);
startIndex -= (4 + count);
}
else
{
break;
}
}
}
先从缓存数据byte中读取前四个byte,即byte[0]-byte[3]按照int32解析,然后再根据长度解析这组数据,再将后面的数据前移,目的就是下一条数据也可以解析data[0]-data[3]来获取这组数据的长度.
相应的,服务器的代码做出改变
static Message msgStr = new Message();//解析数据所用类
static void Main(string[] args)
{
StartServerAsync();
Console.ReadKey();//因为是异步接受,避免程序终止
}
/// <summary>
/// 服务器(异步)
/// </summary>
static void StartServerAsync()
{
Socket serveSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//参数一表示使用ip4,参数二表示使用流传输,参数三表示使用tcp协议
IPAddress ipAddress = IPAddress.Parse("127.0.0.1");//本机ip地址
IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, 8088);
serveSocket.Bind(ipEndPoint);//绑定端口号
serveSocket.Listen(10);//开始监听端口号,10表示一个队列,这个队列最多让十个要进行连接的客户端排队,如果设置为0则可以让无限个要连接的客户端排队让服务端进行处理
serveSocket.BeginAccept(AcceptCallBack, serveSocket);//调用函数AcceptCallBack()开始异步连接客户端
}
/// <summary>
/// 连接客户端的回调函数
/// </summary>
/// <param name="ar"></param>
static void AcceptCallBack(IAsyncResult ar)
{
Socket serveSocket = ar.AsyncState as Socket;
Socket clientSocket = serveSocket.EndAccept(ar);//完成一个客户端的连接
//向客户端发送一条信息
string msg = "welcome !你好";
byte[] data = Encoding.UTF8.GetBytes(msg);//将字符串转成比特
clientSocket.Send(data);
//开始监听客户端数据传递也就是开始接收数据,第四个参数SocketFlags这里不需要,
//所以.none,ReceiveCallBack()是一个自定义的方法,
//这里表示我们接收完数据后调用该方法,最后一个参数就是ReceiveCallBack()方法中的参数的传递,
//这个参数是object类型,接收到的数据就存到msgStr.Dat[]数组中,从数组的msgStr.Dat[msgStr.StartIndex]
//位置,往后msgStr.RemainSize个位子,用来存储
clientSocket.BeginReceive(msgStr.Data, msgStr.StartIndex, msgStr.RemainSize, SocketFlags.None, ReceiveCallBack, clientSocket);
serveSocket.BeginAccept(AcceptCallBack, serveSocket);//继续处理下一个客户端连接
}
/// <summary>
/// 接收数据的回调函数
/// </summary>
/// <param name="ar"></param>
static void ReceiveCallBack(IAsyncResult ar)
{
Socket clientSocket = ar.AsyncState as Socket;//将参数强制转换为socket
try
{
int count = clientSocket.EndReceive(ar);//完成客户端数据的传递,返回比特个数
msgStr.AddCound(count);
//客户端正常关闭服务器就会接受到空数据,但是不会报错,所以这里处理正常关闭的情况
if(count==0)
{
clientSocket.Close();
return;
}
msgStr.ReadMessage();
//函数回调,这样就能接收多条数据
clientSocket.BeginReceive(msgStr.Data, msgStr.StartIndex, msgStr.RemainSize, SocketFlags.None, ReceiveCallBack, clientSocket);
}
catch (Exception e)
{
Console.WriteLine(e);
//处理非正常关闭的情况
if(clientSocket==null)
{
clientSocket.Close();
}
}
}
需要注意的是,完成一条数据传输时,记得要调用ReadMessage()方法将Message中的startIndex增加.