利用网络通信中,经常会出现粘包的问题,围绕着这个问题说原因和解决的蛮多帖子的,但是给出粘包代码的就好少,为了便于大家更好的理解粘包的问题,这里对客户端和服务器端出现的粘包问题进行模拟,以方便更好的理解这个问题的出现原因。在开始之前还是需要理解几个基础概念。
如果需要源代码请点击此处下载
1、什么是粘包?
粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。只有TCP有粘包现象,UDP不会。
2、哪里会发生粘包?
其实粘包主要分为两个方面,客户端和服务器端都可能会出现:
a、客户端发生:当连续发送数据时,由于tcp协议的nagle算法,会将较小的内容拼接成大的内容,一次性发送到服务器端,因此造成粘包
b、服务器端发生:当发送内容较大时,由于服务器端的recv(buffer_size)方法中的buffer_size较小,不能一次性完全接收全部内容,因此在下一次请求到达时,接收的内容依然是上一次没有完全接收完的内容,因此造成粘包现象。
好了说了这些之后,我们简单通过一个图来介绍下我们要模拟的粘包的场景:简单说就是客户端向服务器端不停的发送两个包,一个数据包的内容是一个整形的数:4,另一个包是一个字符串:你好!
理想情况下,我们应该是客户端发送什么,发送多少服务器端就接收对应数量的数据:
然而,当我们加快客户端的发送速度时,服务器端接收的数据包就不正常了,如图所示:
很显然,客户端还是正常的,不停的发送21和40两个数据包,但是在服务器端除了21,40正常的包外还出现了101和61两种包类型,其中101我们可以分析的出是两个40的包和一个21的包粘到一起了,而61的包是一个21的包和一个40的包粘到一起了
这里为了方便大家大家复现,先把客户端的代码贴出来:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using LitJson;
class client
{
//声明socket
Socket socket;
//声明一个地址类
IPEndPoint iep;
public void connect()
{
try
{
//设定Socket的模式
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//指定服务端的地址和端口
iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 30000);
//建立连接
socket.Connect(iep);
Console.WriteLine("服务器连接成功");
//定义一个Json来存放数据
JsonData jd_int = new JsonData();
jd_int["code"] = 1;
jd_int["result"] = 4;
//定义一个byte数组存放数据
byte[] jsByte_int = Encoding.UTF8.GetBytes(jd_int.ToJson());
//发送数据
JsonData jd_string = new JsonData();
jd_string["code"] = 2;
jd_string["result"] = "你好!";
byte[] jsByte_string = Encoding.UTF8.GetBytes(jd_string.ToJson());
//发送数据
while (true)
{
Thread.Sleep(1); //设置发送消息的间隔
socket.Send(jsByte_int);
Console.WriteLine("发送了" + jsByte_int.Length + "个字节");
socket.Send(jsByte_string);
Console.WriteLine("发送了" + jsByte_string.Length + "个字节");
}
}
catch(Exception e)
{
Console.WriteLine("服务器连接失败");
Console.WriteLine(e.ToString());
}
}
}
这里各位可以根据自己电脑的情况设置发送的间隔,在不同的电脑里可能出现粘包的间隔不同,性能较差的电脑可能sleep时间较长就会出现粘包现象,而笔者的电脑是设置为1毫秒时出现了粘包的情况,具体代码如下:
//发送数据
while (true) //循环发送数据,模拟客户端较大的数据量
{
Thread.Sleep(1); //设置发送消息的间隔,越小越容易出现粘包现象(和电脑配置有关)
socket.Send(jsByte_int);
Console.WriteLine("发送了" + jsByte_int.Length + "个字节");
socket.Send(jsByte_string);
Console.WriteLine("发送了" + jsByte_string.Length + "个字节");
}
服务器端的代码就相对简单了,就是一个简单的异步接收:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Sockets;
using System.Net;
using LitJson;
public class demo
{
//需要添加System.Net.Sockets的支持
Socket socket;
private string ip;
private int port;
//定义每次接收的最大字节数和接收数组
private int maxBuffer = 1024;
private byte[] buffer;
//定义一个套接字代表客户端
private Socket client;
public demo(string _ip,int _port)
{
this.ip = _ip;
this.port = _port;
}
//服务器连接函数
public void StartConnect()
{
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Parse(ip), port);
//绑定地址
socket.Bind(iep);
//设置客户端连接数量
socket.Listen(1000);
Console.WriteLine("服务器开启成功!");
//监听客户端连接
Console.WriteLine("监控客户端连接!");
client = socket.Accept();
Console.WriteLine("有客户端连接了!");
Receive();
}
catch(Exception e)
{
Console.WriteLine("服务器开启失败!");
//显示出异常详情
Console.WriteLine(e.ToString());
}
}
//接收客户端信息
public void Receive()
{
//初始化接收的数组
buffer = new byte[maxBuffer];
try
{
//异步接收,接收的数据放到回调函数BegRece里面去处理,无论客户端是否发送消息都会执行这一步
client.BeginReceive(buffer, 0, maxBuffer, 0, new AsyncCallback(BegRece), client);
}
catch (Exception e)
{
Console.WriteLine("接收异常!");
Console.WriteLine(e.ToString());
}
}
private void BegRece(IAsyncResult ar)
{
Socket so = (Socket)ar.AsyncState;
//接收字节的数据长度
int length = so.EndReceive(ar);
Console.WriteLine("收到了客户端发来的信息,长度为:" + length);
try
{
//接收数据
string content = Encoding.UTF8.GetString(buffer, 0, length);
//Json的解析
JsonData jd = JsonMapper.ToObject(content);
Console.WriteLine("code: " + (int)jd["code"] + " result: " + jd["result"]);
buffer = new byte[maxBuffer];
}
catch(Exception e)
{
Console.WriteLine("解码错误!");
Console.WriteLine(e.ToString());
}
//为了实现不停地接收数据,再次调用并对buffer重新赋值即可
client.BeginReceive(buffer, 0, maxBuffer, 0, new AsyncCallback(BegRece), client);
}
}