Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 832|回复: 0

C#下利用封包、拆包原理解决Socket粘包、半包问题(新手篇)

[复制链接]
  • TA的每日心情
    奋斗
    2024-11-24 15:47
  • 签到天数: 804 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726782
    发表于 2021-4-11 16:06:18 | 显示全部楼层 |阅读模式

    介于网络上充斥着大量的含糊其辞的Socket初级教程,扰乱着新手的学习方向,我来扼要的教一下新手应该怎么合理的处理Socket这个玩意儿。

    一般来说,教你C#下Socket编程的老师,很少会教你如何解决Socket粘包、半包问题。

    甚至,某些师德有问题的老师,根本就没跟你说过Socket的粘包、半包问题是什么玩意儿。

    直到有一天,你的Socket程序在传输信息时出现了你预期之外的结果(多余的信息、不完整的信息、乱码、Bug等等)。

    任你喊了一万遍“喔嚓”,依旧是不知道问题出在哪儿!

    好了,不说废话了,进入正题,包教包会,学不会,送路费。

     

    如果你读到这篇文章了,想必你已经遇到了以上问题,情况再理想一点儿,其实你在原理上已经知道怎么解决这个问题了,只是不知道怎么动手。

    那么,首先,你需要新建一个【消息协议类】,这个类我们暂定由5大属性组成,分别是:

    【(①=1个byte)(②=1个byte])(③=1个int)(④=1个byte[])(⑤=1个byte[])】

    解释一下这个【消息协议类】:

    (①=1个byte):这个属性占1个字节,可以放一个0到254的正整数,我称作1号标志;

    (②=1个byte):这个属性占1个字节,可以放一个0到254的正整数,我称作2号标志。

      那么1号标志和2号标志就有多达255×255个组合,我们用它来自定义这个消息对象的标志,比如,0-0表示登录请求消息,1-1表示物理攻击,1-2表示魔法攻击,3-0表示坐标移动;

    (③=1个int):这个属性占4个字节,可以放一个0到2147483647的正整数,它表示后面(④=1个byte[])的长度,即实际发送的实质内容的长度;

    (④=1个byte[]):这个属性存着你要发送的全部实质内容消息体的字节,所以,你的消息体需要被转化为字节数组才可存放进去;

    (⑤=1个byte[]):这个属性存着一条完整信息之外【多余的消息体字节】。那么问题来了,什么是【多余的消息体字节】?先不忙往下看,自己思考一下这个问题,再继续往下看。

     

    然后我们来解释一下这个【消息协议类】具体怎么用。

    1,【消息发送方】先定义【消息协议类】①②属性,也就是随便写2个数,至于这两数代表什么含义,你自己定就行,这是为了消息接收方接受到消息后根据这个码来执行相应的逻辑;

    2,【消息发送方】再将消息“装入”【消息协议类】的④属性,即实质消息内容;那么③属性也就随之有了,因为有了实质消息就算的出它的长度,这应该不难理解吧;

    3,【消息发送方】将封装好的【消息协议类】转为byte[],发送给【消息接收方】,我们把这道工序称作【封包】;

    ------------------------------------------------------

    4,【消息接收方】接收到【消息发送方】发来的byte[]时,先判断这个byte[]长度是否大于6,为什么是6?其实就是否大于①属性+②属性+③属性的长度之和(2个byte是2字节,1个int是4字节,2+6=6),

    如果byte[]长度小于6,说明没收到有效消息,则【消息接收方】就循环继续接收;

    5,【消息接收方】接收到【消息发送方】发来的byte[]长度如果大于等于6了!!则将byte[]还原为【消息协议类】对象;

    6,循环判断【消息发送方】发来的byte[]长度减去6之后的值是否大于等于【消息协议类】对象③的值,如果大于等于,则把④属性拆出来,这就得到了一个刚刚好完整的消息,不多也不少。我们就把这道工序称作【拆包】。,

    7,那么⑤这个【多余的消息体字节】是干嘛用的呢??上一步当中,如果byte[]信息刚好是一个完整长度,自然用不到⑤了,但是在网络传输中byte[]肯定不会永远那么刚好了,所以当byte[]长度大于一个完整消息时,我们就把多余的byte放入⑤当中,和下次新接收的byte[]依次拼合在一起,再次进行【拆包】的循环,保证数据的完整性和独立性。

    8,好了,以上过程就是利用封包、拆包原理解决Socket粘包、半包问题,接下来你可以在发送方以非常频繁的发送频率来发送信息,接收方依然会规规矩矩完完整整的以正确的姿势来接收消息了。最下面是要点代码,相信聪明的你一定学会了,如果还没学会,可以加我QQ:119945778,包教包会,不然我还得送路费不是...

    下面是源码时间:

      1     /// <summary>
      2     /// 【消息协议】=【协议一级标志】+【协议二级标志】+【实际消息长度】+【实际消息内容】+【多余的消息内容】
      3     /// </summary>
      4     public class MessageXieYi
      5     {
      6         #region 自定义
      7         #region 协议一级标志,值 = (0 至 254 )
      8         private byte xieYiFirstFlag;
      9         /// <summary>
     10         /// 协议类别,值 = ( 0 直 254 )
     11         /// </summary>
     12         public byte XieYiFirstFlag
     13         {
     14             get { return xieYiFirstFlag; }
     15             set { xieYiFirstFlag = value; }
     16         }
     17         #endregion
     18 
     19         #region 协议二级标志,值 = (0 至 254 )
     20         private byte xieYiSecondFlag;
     21         /// <summary>
     22         /// 协议二级标志,值 = (0 至 254 )
     23         /// </summary>
     24         public byte XieYiSecondFlag
     25         {
     26             get { return xieYiSecondFlag; }
     27             set { xieYiSecondFlag = value; }
     28         }
     29         #endregion
     30 
     31         #region 实际消息长度
     32         private int messageContentLength;
     33         /// <summary>
     34         /// 实际消息长度
     35         /// </summary>
     36         public int MessageContentLength
     37         {
     38             get { return messageContentLength; }
     39             set { messageContentLength = value; }
     40         }
     41         #endregion
     42 
     43         #region 实际消息内容
     44         private byte[] messageContent = new byte[] { };
     45         /// <summary>
     46         /// 实际消息内容
     47         /// </summary>
     48         public byte[] MessageContent
     49         {
     50             get { return messageContent; }
     51             set { messageContent = value; }
     52         }
     53         #endregion
     54 
     55         #region 多余的Bytes
     56         private byte[] duoYvBytes;
     57         /// <summary>
     58         /// 多余的Bytes
     59         /// </summary>
     60         public byte[] DuoYvBytes
     61         {
     62             get { return duoYvBytes; }
     63             set { duoYvBytes = value; }
     64         }
     65 
     66         #endregion
     67         #endregion
     68 
     69         #region 构造函数两个
     70         public MessageXieYi()
     71         {
     72             //
     73         }
     74 
     75         public MessageXieYi(byte _xieYiFirstFlage, byte _xieYiSecondFlage, byte[] _messageContent)
     76         {
     77             xieYiFirstFlag = _xieYiFirstFlage;
     78             xieYiFirstFlag = _xieYiSecondFlage;
     79             messageContentLength = _messageContent.Length;
     80             messageContent = _messageContent;
     81         }
     82         #endregion
     83 
     84         #region MessageXieYi 转换为 byte[]
     85         /// <summary>
     86         /// MessageXieYi 转换为 byte[]
     87         /// </summary>
     88         /// <returns></returns>
     89         public byte[] ToBytes()
     90         {
     91             byte[] _bytes; //自定义字节数组,用以装载消息协议
     92 
     93             using (MemoryStream memoryStream = new MemoryStream()) //创建内存流
     94             {
     95                 BinaryWriter binaryWriter = new BinaryWriter(memoryStream); //以二进制写入器往这个流里写内容
     96 
     97                 binaryWriter.Write(xieYiFirstFlag); //写入协议一级标志,占1个字节
     98                 binaryWriter.Write(xieYiSecondFlag); //写入协议二级标志,占1个字节
     99                 binaryWriter.Write(messageContentLength); //写入实际消息长度,占4个字节
    100 
    101                 if (messageContentLength > 0)
    102                 {
    103                     binaryWriter.Write(messageContent); //写入实际消息内容
    104                 }
    105 
    106                 _bytes = memoryStream.ToArray(); //将流内容写入自定义字节数组
    107 
    108                 binaryWriter.Close(); //关闭写入器释放资源
    109             }
    110 
    111             return _bytes; //返回填充好消息协议对象的自定义字节数组
    112         }
    113         #endregion
    114 
    115         #region byte[] 转换为 MessageXieYi
    116         /// <summary>
    117         /// byte[] 转换为 MessageXieYi
    118         /// </summary>
    119         /// <param name="buffer">字节数组缓冲器。</param>
    120         /// <returns></returns>
    121         public static MessageXieYi FromBytes(byte[] buffer)
    122         {
    123             int bufferLength = buffer.Length;
    124 
    125             MessageXieYi messageXieYi = new MessageXieYi();
    126 
    127             using (MemoryStream memoryStream = new MemoryStream(buffer)) //将字节数组填充至内存流
    128             {
    129                 BinaryReader binaryReader = new BinaryReader(memoryStream); //以二进制读取器读取该流内容
    130 
    131                 messageXieYi.xieYiFirstFlag = binaryReader.ReadByte(); //读取协议一级标志,读1个字节
    132                 messageXieYi.xieYiSecondFlag = binaryReader.ReadByte(); //读取协议二级标志,读1个字节
    133                 messageXieYi.messageContentLength = binaryReader.ReadInt32(); //读取实际消息长度,读4个字节                
    134 
    135                 //如果【进来的Bytes长度】大于【一个完整的MessageXieYi长度】
    136                 if ((bufferLength - 6) > messageXieYi.messageContentLength)
    137                 {
    138                     messageXieYi.messageContent = binaryReader.ReadBytes(messageXieYi.messageContentLength); //读取实际消息内容,从第7个字节开始读
    139                     messageXieYi.duoYvBytes = binaryReader.ReadBytes(bufferLength - 6 - messageXieYi.messageContentLength);
    140                 }
    141 
    142                 //如果【进来的Bytes长度】等于【一个完整的MessageXieYi长度】
    143                 if ((bufferLength - 6) == messageXieYi.messageContentLength)
    144                 {
    145                     messageXieYi.messageContent = binaryReader.ReadBytes(messageXieYi.messageContentLength); //读取实际消息内容,从第7个字节开始读
    146                 }
    147 
    148                 binaryReader.Close(); //关闭二进制读取器,是否资源
    149             }
    150 
    151             return messageXieYi; //返回消息协议对象
    152         }
    153         #endregion
    154     }
     1     /// <summary>
     2     /// 按照先后顺序合并字节数组类
     3     /// </summary>
     4     public class CombineBytes
     5     {
     6         /// <summary>
     7         /// 按照先后顺序合并字节数组,并返回合并后的字节数组。
     8         /// </summary>
     9         /// <param name="firstBytes">第一个字节数组</param>
    10         /// <param name="firstIndex">第一个字节数组的开始截取索引</param>
    11         /// <param name="firstLength">第一个字节数组的截取长度</param>
    12         /// <param name="secondBytes">第二个字节数组</param>
    13         /// <param name="secondIndex">第二个字节数组的开始截取索引</param>
    14         /// <param name="secondLength">第二个字节数组的截取长度</param>
    15         /// <returns></returns>
    16         public static byte[] ToArray(byte[] firstBytes, int firstIndex, int firstLength,  byte[] secondBytes, int secondIndex, int secondLength)
    17         {
    18             using (MemoryStream ms = new MemoryStream())
    19             {
    20                 BinaryWriter bw = new BinaryWriter(ms);
    21                 bw.Write(firstBytes, firstIndex, firstLength);
    22                 bw.Write(secondBytes, secondIndex, secondLength);
    23 
    24                 bw.Close();
    25                 bw.Dispose();
    26 
    27                 return ms.ToArray();
    28             }
    29         }
    30     }
    1 byte[] msgBytes = Encoding.Unicode.GetBytes("要发送的消息");
    2 MessageXieYi msgXY = new MessageXieYi(0, 0, msgBytes);
    3 networkStream.Write(msgXY.ToBytes(), 0, msgXY.ToBytes().Length);
     1         #region 线程执行体,接收消息
     2         /// <summary>
     3         /// 线程执行体,接收消息
     4         /// </summary>
     5         /// <param name="obj">传递给线程执行体的用户名,用以与用户通信</param>
     6         private void ThreadReceive(object obj)
     7         {
     8             //通过用户名找出已经保存在哈希表里的Socket
     9             Socket savedSocket = hashtable_UserNameToSocket[obj] as Socket;
    10 
    11             MessageXieYi msgXY = new MessageXieYi();
    12 
    13             byte[] buffer = new byte[64];//定义一个大小为64的缓冲区
    14             //byte[] receivedBytes = new byte[] { };
    15             byte[] newBuffer = new byte[] { };//大小可变的缓存器
    16 
    17             int receivedLength;
    18             int availableLength;//没什么实际意义,就是为了方便理解Socket传输机制
    19 
    20             while (true)
    21             {
    22                 try
    23                 {
    24                     buffer = new byte[64];
    25 
    26                     for (int i = 0; i < 10; i++)
    27                     {
    28                         availableLength = savedSocket.Available;
    29 
    30                         Console.WriteLine("【循环判断有多少可读Bytes】savedSocket.Available[" + i + "]=" + availableLength);//没实际意义,就是来个直观感受Socket的原理
    31                     }
    32 
    33                     Console.WriteLine("【可变缓存器大小】newBuffer.Length=" + newBuffer.Length);
    34 
    35                     receivedLength = savedSocket.Receive(buffer);
    36 
    37                     Console.WriteLine("【接收到数据】buffer.Length=" + receivedLength);
    38 
    39                     newBuffer = CombineBytes.ToArray(newBuffer, 0, newBuffer.Length, buffer, 0, receivedLength);
    40 
    41                     Console.WriteLine("【将接收到的数据追加在newBuffer后】newBuffer.Length=" + newBuffer.Length);
    42 
    43                     if (newBuffer.Length < 6)
    44                     {
    45                         Console.WriteLine("newBuffer.Length=" + newBuffer.Length + "< 6 \t -> \t continue");
    46                         continue;
    47                     }
    48                     else //newBuffer.Length >= 6
    49                     {
    50                         //取msgXY包头部分
    51                         msgXY = MessageXieYi.FromBytes(newBuffer);
    52                         int firstFlag = msgXY.XieYiFirstFlag;
    53                         int secondFlag = msgXY.XieYiSecondFlag;
    54                         int msgContentLength = msgXY.MessageContentLength;
    55 
    56 
    57                         //判断去掉msgXY包头剩下的长度是否达到可以取包实质内容
    58                         while ((newBuffer.Length - 6) >= msgContentLength)
    59                         {
    60                             Console.WriteLine("【newBuffer去掉包头的长度=" + (newBuffer.Length - 6) + "】>=【" + "包实质内容长度=" + msgContentLength + "");
    61                             msgXY = null;
    62                             msgXY = MessageXieYi.FromBytes(newBuffer);
    63                             Console.WriteLine("\n【拆包】=" + Encoding.Unicode.GetString(msgXY.MessageContent) + "\n");
    64 
    65                             newBuffer = msgXY.DuoYvBytes;
    66                             Console.WriteLine("【剩余的newBuffer】newBuffer.Length=" + newBuffer.Length);
    67 
    68                             if (newBuffer.Length >= 6)
    69                             {
    70                                 msgXY = MessageXieYi.FromBytes(newBuffer);
    71                                 firstFlag = msgXY.XieYiFirstFlag;
    72                                 secondFlag = msgXY.XieYiSecondFlag;
    73                                 msgContentLength = msgXY.MessageContentLength;
    74                                 continue;
    75                             }
    76                             else
    77                             {
    78                                 break;
    79                             }
    80                         }
    81                     }
    82 
    83                     availableLength = savedSocket.Available;
    84                     Console.WriteLine("savedSocket.Available=" + availableLength + "\n\n\n\n");
    85 
    86                     continue;
    87                 }
    88                 catch
    89                 {
    90                     //异常处理
    91                 }
    92             }
    93         }
    94         #endregion

     

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-12-22 16:25 , Processed in 0.058174 second(s), 27 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表