C# Socket系列三 socket通信的封包和拆包
通过系列二 我们已经实现了socket的简单通信 接下来我们测试一下,在时间应用的场景下,我们会快速且大量的传输数据的情况!
class Program
{
static void Main(string[] args)
{
TCPListener tcp = new TCPListener();
TSocketClient client = new TSocketClient();
; i < ; i++)
{
client.SendMsg(System.Text.UTF8Encoding.Default.GetBytes("Holle Server!"));
}
Console.ReadLine();
}
}
我们通过测试代码快速发送10条消息到服务器去,
我们看看运行结果

这样不难看出,我们的客户端发送了10条消息,但是服务器收到的时候变成了两条消息,回复客户端自然就变成两次回复。
这是为什么呢?
我们修改一下程序一秒钟发送一次消息试试
class Program
{
static void Main(string[] args)
{
TCPListener tcp = new TCPListener();
TSocketClient client = new TSocketClient();
; i < ; i++)
{
Thread.Sleep();
client.SendMsg(System.Text.UTF8Encoding.Default.GetBytes("Holle Server!"));
}
Console.ReadLine();
}
}
运行看看,

这次对了那么分析分析到底为什么呢?这是socket的底层,做的手脚。因为我设置socket的发送和接受缓冲区//10K的缓冲区空间 private int BufferSize = 10 * 1024; 10k的缓冲区,且socket的底层 发送消息会有一定间隙,虽然这个时间很短,但是我们直接for循环发送的话,时间同意很快,因为socket.send()方法并非真实的发送数据而是把数据压入发送缓冲区。那么我们就明白了为什么会出现上面的情况出现了这样的情况我们要怎么解决呢?时间应用场景不可能1秒钟才一条消息啥。我们知道了导致这个问题的原因是因为消息发送是出现了快速压入很多发送消息到待发送缓冲区里面一起发送导致的。这样情况就是粘包了,那么我们是不是可以考虑给每一个消息加入包标识呢?
接下来我们修改一下发送包的数据代码
创建消息的构造体 TSocketMessage
/// <summary>
/// 底层通信消息
/// </summary>
public class TSocketMessage : IDisposable
{
/// <summary>
/// 消息ID
/// </summary>
public int MsgID;
/// <summary>
/// 消息内容
/// </summary>
public byte[] MsgBuffer;
public TSocketMessage(int msgID, byte[] msg)
{
this.MsgID = msgID;
this.MsgBuffer = msg;
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool flag1)
{
if (flag1) { this.MsgBuffer = null; }
}
}
接下来我们创建消息包的封装和拆分 MarshalEndian
public class MarshalEndian
{
//用于存储剩余未解析的字节数
);
//默认是utf8的编码格式
private UTF8Encoding utf8 = new UTF8Encoding();
//包头1
const Int16 t1 = 0x55;
//包头2
const Int16 t2 = 0xAA;
//字节数常量 两个包头4个字节,一个消息id4个字节,封装消息长度 long 8个字节
const long ConstLenght = 12L;
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool flag1)
{
if (flag1)
{
IDisposable disposable2 = this.utf8 as IDisposable;
if (disposable2 != null) { disposable2.Dispose(); }
IDisposable disposable = this._LBuff as IDisposable;
if (disposable != null) { disposable.Dispose(); }
}
}
public byte[] Encode(TSocketMessage msg)
{
MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms, new UTF8Encoding());
byte[] msgBuffer = msg.MsgBuffer;
#region 封装包头
bw.Write((Int16)t1);
bw.Write((Int16)t2);
#endregion
#region 包协议
if (msgBuffer != null)
{
bw.Write((Int64)(msgBuffer.Length + ));
bw.Write(msg.MsgID);
bw.Write(msgBuffer);
}
); }
#endregion
bw.Close();
ms.Close();
bw.Dispose();
ms.Dispose();
return ms.ToArray();
}
public List<TSocketMessage> GetDcAppMess(byte[] buff, int len)
{
//拷贝本次的有效字节
byte[] _b = new byte[len];
Array.Copy(buff, , _b, , _b.Length);
buff = _b;
)
{
//拷贝之前遗留的字节
this._LBuff.AddRange(_b);
buff = this._LBuff.ToArray();
this._LBuff.Clear();
);
}
List<TSocketMessage> list = new List<TSocketMessage>();
MemoryStream ms = new MemoryStream(buff);
BinaryReader buffers = new BinaryReader(ms, this.utf8);
try
{
byte[] _buff;
Label_0073:
//判断本次解析的字节是否满足常量字节数
if ((buffers.BaseStream.Length - buffers.BaseStream.Position) < ConstLenght)
{
_buff = new byte[(int)(buffers.BaseStream.Length - buffers.BaseStream.Position)];
Array.Copy(buff, (, _buff.Length);
this._LBuff.AddRange(_buff);
return list;
}
#region 包头读取
//循环读取包头
Label_00983:
Int16 tt1 = buffers.ReadInt16();
Int16 tt2 = buffers.ReadInt16();
if (!(tt1 == t1 && tt2 == t2))
{
, SeekOrigin.Current);
goto Label_00983;
}
#endregion
#region 包协议
long offset = buffers.ReadInt64();
#endregion
#region 包解析
//剩余字节数大于本次需要读取的字节数
if (offset < (buffers.BaseStream.Length - buffers.BaseStream.Position))
{
int msgID = buffers.ReadInt32();
_buff = ];
Array.Copy(buff, (, _buff.Length);
list.Add(new TSocketMessage(msgID, _buff));
//设置偏移量 然后继续循环读取
buffers.BaseStream.Seek(offset, SeekOrigin.Current);
goto Label_0073;
}
else if (offset == (buffers.BaseStream.Length - buffers.BaseStream.Position))
{
int msgID = buffers.ReadInt32();
//剩余字节数刚好等于本次读取的字节数
_buff = ];
Array.Copy(buff, (, _buff.Length);
list.Add(new TSocketMessage(msgID, _buff));
}
else
{
//剩余字节数刚好小于本次读取的字节数 存起来,等待接受剩余字节数一起解析
_buff = new byte[(int)(buffers.BaseStream.Length - buffers.BaseStream.Position + ConstLenght)];
Array.Copy(buff, (, _buff.Length);
buff = _buff;
this._LBuff.AddRange(_buff);
}
#endregion
}
catch { }
finally
{
if (buffers != null) { buffers.Dispose(); }
buffers.Close();
if (buffers != null) { buffers.Dispose(); }
ms.Close();
if (ms != null) { ms.Dispose(); }
}
return list;
}
}
接下来我们修改一下 TSocketBase 的 抽象方法
public abstract void Receive(TSocketMessage msg);
在修改接受消息回调函数
/// <summary>
/// 消息解析器
/// </summary>
MarshalEndian mersha = new MarshalEndian();
/// <summary>
/// 接收消息回调函数
/// </summary>
/// <param name="iar"></param>
private void ReceiveCallback(IAsyncResult iar)
{
if (!this.IsDispose)
{
try
{
//接受消息
ReceiveSize = _Socket.EndReceive(iar, out ReceiveError);
//检查状态码
if (!CheckSocketError(ReceiveError) && SocketError.Success == ReceiveError)
{
//判断接受的字节数
)
{
byte[] rbuff = new byte[ReceiveSize];
Array.Copy(this.Buffers, rbuff, ReceiveSize);
var msgs = mersha.GetDcAppMess(rbuff, ReceiveSize);
foreach (var msg in msgs)
{
this.Receive(msg);
}
//重置连续收到空字节数
ZeroCount = ;
//继续开始异步接受消息
ReceiveAsync();
}
else
{
ZeroCount++;
) { this.Close("错误链接"); }
}
}
}
catch (System.Net.Sockets.SocketException) { this.Close("链接已经被关闭"); }
catch (System.ObjectDisposedException) { this.Close("链接已经被关闭"); }
}
}
这样我们完成了在收到消息后对数据包的解析。
修改一下TSocketClient的 Receive 重写方法
/// <summary>
/// 收到消息后
/// </summary>
/// <param name="rbuff"></param>
public override void Receive(TSocketMessage msg)
{
Console.WriteLine("Receive ID:" + msg.MsgID + " Msg:" + System.Text.UTF8Encoding.Default.GetString(msg.MsgBuffer));
if (isServer)
{
this.SendMsg(new TSocketMessage(msg.MsgID, System.Text.UTF8Encoding.Default.GetBytes("Holle Client!")));
}
}
修改测试代码如下
class Program
{
static void Main(string[] args)
{
TCPListener tcp = new TCPListener();
TSocketClient client = new TSocketClient();
; i < ; i++)
{
Thread.Sleep();
client.SendMsg(new TSocketMessage(i, System.Text.UTF8Encoding.Default.GetBytes("Holle Server!")));
}
Console.ReadLine();
}
}
运行结果

接受成功了,那么我们取消暂停状态,快速发送消息试试
class Program
{
static void Main(string[] args)
{
TCPListener tcp = new TCPListener();
TSocketClient client = new TSocketClient();
; i < ; i++)
{
client.SendMsg(new TSocketMessage(i, System.Text.UTF8Encoding.Default.GetBytes("Holle Server!")));
}
Console.ReadLine();
}
}
看看运行结果

瞬间完成了消息发送,也没有再出现第一次运行的那样~!
这样完美的解决了socket通信 在传输上发送粘包问题
谢谢园友发现的问题,
问题是这样的原本的解包和封包的测试代码不够严谨导致解析包出现错误感谢园友发现问题,并提出问题。


附上最新的代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TSocket
{
public class MarshalEndian
{
//用于存储剩余未解析的字节数
);
//默认是utf8的编码格式
private UTF8Encoding utf8 = new UTF8Encoding();
//包头1
const Int16 t1 = 0x55;
//包头2
const Int16 t2 = 0xAA;
//字节数常量 两个包头4个字节,一个消息id4个字节,封装消息长度 int32 4个字节
;
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool flag1)
{
if (flag1)
{
IDisposable disposable2 = this.utf8 as IDisposable;
if (disposable2 != null) { disposable2.Dispose(); }
IDisposable disposable = this._LBuff as IDisposable;
if (disposable != null) { disposable.Dispose(); }
}
}
public byte[] Encode(TSocketMessage msg)
{
MemoryStream ms = new MemoryStream();
BinaryWriter bw = new BinaryWriter(ms, new UTF8Encoding());
byte[] msgBuffer = msg.MsgBuffer;
#region 封装包头
bw.Write((Int16)t1);
bw.Write((Int16)t2);
#endregion
#region 包协议
if (msgBuffer != null)
{
bw.Write((Int32)(msgBuffer.Length + ));
bw.Write(msg.MsgID);
bw.Write(msgBuffer);
}
); }
#endregion
bw.Close();
ms.Close();
bw.Dispose();
ms.Dispose();
return ms.ToArray();
}
public List<TSocketMessage> GetDcAppMess(byte[] buff, int len)
{
//拷贝本次的有效字节
byte[] _b = new byte[len];
Array.Copy(buff, , _b, , _b.Length);
buff = _b;
)
{
//拷贝之前遗留的字节
this._LBuff.AddRange(_b);
buff = this._LBuff.ToArray();
this._LBuff.Clear();
);
}
List<TSocketMessage> list = new List<TSocketMessage>();
MemoryStream ms = new MemoryStream(buff);
BinaryReader buffers = new BinaryReader(ms, this.utf8);
try
{
byte[] _buff;
Label_00983:
#region 包头读取
//循环读取包头
//判断本次解析的字节是否满足常量字节数
if ((buffers.BaseStream.Length - buffers.BaseStream.Position) < ConstLenght)
{
_buff = buffers.ReadBytes((int)(buffers.BaseStream.Length - buffers.BaseStream.Position));
this._LBuff.AddRange(_buff);
return list;
}
Int16 tt1 = buffers.ReadInt16();
Int16 tt2 = buffers.ReadInt16();
if (!(tt1 == t1 && tt2 == t2))
{
, SeekOrigin.Current);
goto Label_00983;
}
#endregion
#region 包协议
int offset = buffers.ReadInt32();
#endregion
#region 包解析
//剩余字节数大于本次需要读取的字节数
if (offset <= (buffers.BaseStream.Length - buffers.BaseStream.Position))
{
int msgID = buffers.ReadInt32();
_buff = buffers.ReadBytes(offset - );
list.Add(new TSocketMessage(msgID, _buff));
)
{
goto Label_00983;
}
}
else
{
//剩余字节数刚好小于本次读取的字节数 存起来,等待接受剩余字节数一起解析
_buff = buffers.ReadBytes((int)(buffers.BaseStream.Length - buffers.BaseStream.Position + ConstLenght));
this._LBuff.AddRange(_buff);
}
#endregion
}
catch (Exception ex) { Console.WriteLine(ex); }
finally
{
if (buffers != null) { buffers.Dispose(); }
buffers.Close();
if (buffers != null) { buffers.Dispose(); }
ms.Close();
if (ms != null) { ms.Dispose(); }
}
return list;
}
}
}
C# Socket系列三 socket通信的封包和拆包的更多相关文章
- socket系列之socket服务端与客户端如何通信
上面已经分别介绍了ServerSocket跟Socket的工作步骤,并且从应用层往系统底层剖析其运作原理,我们清楚了他们各自的一块,现在我们将把他们结合起来,看看他们是如何通信的,并详细讨论一下他们之 ...
- java学习小笔记(三.socket通信)【转】
三,socket通信1.http://blog.csdn.net/kongxx/article/details/7288896这个人写的关于socket通信不错,循序渐进式的讲解,用代码示例说明,运用 ...
- 网络编程 TCP协议:三次握手,四次回收,反馈机制 socket套接字通信 粘包问题与解决方法
TCP协议:传输协议,基于端口工作 三次握手,四次挥手 TCP协议建立双向通道. 三次握手, 建连接: 1:客户端向服务端发送建立连接的请求 2:服务端返回收到请求的信息给客户端,并且发送往客户端建立 ...
- C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)
介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿. 一般来说,教你C#下Socket编程的老师,很少会教你如何解 ...
- 进程间通信系列 之 socket套接字实例
进程间通信系列 之 概述与对比 http://blog.csdn.net/younger_china/article/details/15808685 进程间通信系列 之 共享内存及其实例 ...
- 进程间通信系列 之 socket套接字及其实例
进程间通信系列 之 概述与对比 http://blog.csdn.net/younger_china/article/details/15808685 进程间通信系列 之 共享内存及其实例 ...
- python socket+tcp三次握手四次撒手学习+wireshark抓包
Python代码: server: #!/usr/bin/python # -*- coding: UTF-8 -*- # 文件名:server.py import socket # 导入 socke ...
- UE4 Socket多线程非阻塞通信
转自:https://blog.csdn.net/lunweiwangxi3/article/details/50468593 ue4自带的Fsocket用起来依旧不是那么的顺手,感觉超出了我的理解范 ...
- Python socket套接字通信
一.什么是socket? socket是一个模块, 又称套接字,用来封装 互联网协议(应用层以下的层). 二.为什么要有socket? socket可以实现互联网协议 应用层以下的层 的工作,提高开发 ...
随机推荐
- sphinx教程
http://www.php100.com/html/it/focus/2013/0916/6188.html### 以上一篇的email数据表为例: 数据结构: 01.CREATE TABLE em ...
- 解决Ubuntu Server 12.04 在Hyper-v 2012 R2中不能使用动态内存的问题
前言 全新Hyper-v 2012 R2终于开始支持在Linux的VPS中使用动态内存,可以大大优化服务器的资源分配,小弟我兴奋不已,于是抽空时间赶紧升级到 2012 R2,好好整理一番内存分配,不过 ...
- [.NET领域驱动设计实战系列]专题三:前期准备之规约模式(Specification Pattern)
一.前言 在专题二中已经应用DDD和SOA的思想简单构建了一个网上书店的网站,接下来的专题中将会对该网站补充更多的DDD的内容.本专题作为一个准备专题,因为在后面一个专题中将会网上书店中的仓储实现引入 ...
- linux 2.6 驱动笔记(三)
驱动的并发与应用的并发实现一样,以信号量为例,修改基本字符驱动代码如下: 1. 增加sem定义 struct globalmem_dev{ struct cdev cdev; /*linux 2.6 ...
- MySQL7:视图
什么是视图 数据库中的视图是一个虚拟表.视图是从一个或者多个表中导出的表,视图的行为与表非常相似,在视图中用户可以使用SELECT语句查询数据,以及使用INSERT.UPDATE和DELETE修改记录 ...
- Java设计模式7:适配器模式
适配器模式 适配器模式说的是,可以把一个类的接口变换成客户端所期待的另一种接口,使得原本因接口不匹配而无法在一起工作的两个类可以一起工作. 适配器模式的用途 适配器模式的用途,在网上找了一幅图,挺形象 ...
- Homework 3
1. 是否需要有代码规范? 这些规范都是官僚制度下产生的浪费大家的编程时间.影响人们开发效率, 浪费时间的东西. (反对) 我是个艺术家,手艺人,我有自己的规范和原则. (反对) 规范不能强求一律, ...
- 两个Fragment之间如何传递数据
FragmentA启动FragmentB,做一些选择操作后,返回FragmentA,需要把FragmentB里面选择的数据传回来.有什么办法? Fragment之间不能直接通信,必须通过Activit ...
- Senparc.Weixin.MP SDK 微信公众平台开发教程(五):使用Senparc.Weixin.MP SDK
Senparc.Weixin.MP SDK已经涵盖了微信6.x的所有公共API. 整个项目的源代码以及已经编译好的程序集可以在这个项目中获取到:https://github.com/JeffreySu ...
- MySQL模糊查询(like)时区分大小写
问题说明:通过上面的语句,你会发现MySQL的like查询是不区分大小写的,因为我的失误,把Joe写成了joe才发现了这个东东吧.但是,有时候,我们需要区分大小写的是,该怎么办呢?解决方法如下: 方法 ...