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可以实现互联网协议 应用层以下的层 的工作,提高开发 ...
随机推荐
- 可在广域网部署运行的QQ高仿版 -- GG叽叽V3.0,完善基础功能(源码)
(前段时间封闭式开发完了一个项目,最近才有时间继续更新GG的后续版本,对那些关注GG的朋友来说,真的是很抱歉.)GG的前面几个版本开发了一些比较高级的功能,像视频聊天.远程桌面.文件传送.远程磁盘等, ...
- 关于实现一个基于文件持久化的EventStore的核心构思
大家知道enode框架的架构是基于ddd+event sourcing的思想.我们持久化的不是聚合根的最新状态,而是聚合根产生的领域事件.最近我在思考如何实现一个基于文件的eventstore.目标有 ...
- Java多线程11:ReentrantLock的使用和Condition
ReentrantLock ReentrantLock,一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大. Reentran ...
- 基于百度翻译API开发属于自己的翻译工具
你是否每天使用着网页翻译工具?你是否遇到过这种情况,上网过程中遇到一个很长的单词但是又不能复制,要开两个浏览器,一个打开百度翻译,照着另一个网页输入单词?你安装了各种翻译软件后,又删除,只因忍受不了那 ...
- 一天一小段js代码(no.1)
10000个数字中缺少三个数,编程找出缺少的三个数字. 算法实现: /*生成10000个数中随机抽掉三个数后的数组*/ function supplyRandomArray(){ /*生成含有1000 ...
- Windows 10 下安装 npm 后全局 node_modules 和 npm-cache 文件夹的设置
npm 指 Node Package Manager,是 Node.js 中一个流行的包管理和分发工具.Node.js 在某个版本的 Windows 安装包开始已经加入了 npm,现在可以进入 htt ...
- Senparc.Weixin.MP SDK 微信公众平台开发教程(十五):消息加密
前不久,微信的企业号使用了强制的消息加密方式,随后公众号也加入了可选的消息加密选项.目前企业号和公众号的加密方式是一致的(格式会有少许差别). 加密设置 进入公众号后台的“开发者中心”,我们可以看到U ...
- Atitit 图像清晰度 模糊度 检测 识别 评价算法 原理
Atitit 图像清晰度 模糊度 检测 识别 评价算法 原理 1.1. 图像边缘一般都是通过对图像进行梯度运算来实现的1 1.2. Remark: 1 1.3. 1.失焦检测. 衡量画面模糊的主要方 ...
- fir.im Weekly - 如何打造 Github 「爆款」开源项目
最近 Android 转用 Swift 的传闻甚嚣尘上,Swift 的 Github 主页上已经有了一次 merge>>「Port to Android」,让我们对 Swift 的想象又多 ...
- salesforce 零基础学习(二十八)使用ajax方式实现联动
之前的一篇介绍过关于salesforce手动配置关联关系实现PickList的联动效果,但是现实的开发中,很多数据不是定死的,应该通过ajax来动态获取,本篇讲述通过JavaScript Remoti ...