解决Socket通信中,经常遇到的问题——数据粘包的两种方法

数据粘包问题的出现,是因为在客户端/服务器端都会有一个比较大的数据缓冲区,来存放接收的数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。在收发数据频繁时,由于tcp传输消息的无边界,会导致客户端/服务器端不知道接收到的消息到底是第几条消息,因此,会导致类似一次性接收几条消息的情况,从而乱码。

    在每次发送消息之间,加入空循环,从而可以将消息隔离开来,但是这个方法会严重影响程序的运行效率。

    方法一:数据粘包问题的出现是因为缓冲区过大,因此采用发送/接收变长消息的方法,在发送/接收消息时,将消息的长度作为消息的一部分发送出去,从而接收方可以根据传来的长度信息,制定相应长度的缓冲区;

    方法二:将发送的每条消息的首尾都加上特殊标记符,前加”<”   后加”>”。这里我采取的是先将要发送的所有消息,首尾加上特殊标记后,都先放在一个字符串string中,然后一次性的发送给接收方,接受之后,再根据标记符< >,将一条条消息择(zhái)出来。

    代码如下:

    发送消息端,即服务器端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Collections;
using System.Diagnostics;

namespace _07发送变长消息的方法
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 先将发送的消息带上首尾标记< > 再将要发送的所有消息,放在一个字符串中 然后一次性发送给客户端
            DateTime start = DateTime.Now;//记录程序的运行时间
            string str = null;
            double[] test = { 10.3, 15.2, 25.3, 13.338, 25.41, 0.99, 2017, 20000, 1, 3, 2, 5, 8, 90, 87, 33, 55, 99, 100 };
            IPEndPoint IPep2 = new IPEndPoint(IPAddress.Any, 127);
            Socket newsock2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            newsock2.Bind(IPep2);
            newsock2.Listen(10);
            Console.WriteLine("等待新客户端的连接");
            Socket client2 = newsock2.Accept();//等待客户端的连接
            IPEndPoint clientep2 = (IPEndPoint)client2.RemoteEndPoint;
            Console.WriteLine("与{0}连接在{1}端口", clientep2.Address, clientep2.Port);
            string welcome2 = "welcome to the new server";
            byte[] data;
            str += SpecialMessage(welcome2);//将要发送的消息,都放在字符串str中

            //发送数组的长度信息 给字符串str
            str += SpecialMessage(test.Length.ToString());

            //用循环的形式 依次将数组中的元素给str
            string[] strvalue = new string[test.Length];
            for (int j = 0; j < test.Length; j++)
            {
                strvalue[j] = test[j].ToString();//将实际速度集合转换为string数组
                str += SpecialMessage(strvalue[j]);
            }

            //将所有发送的信息,都放在了str中,然后一次性的发送过去 注意都是有首尾标记的消息< >
            data = System.Text.Encoding.ASCII.GetBytes(str);
            client2.Send(data);

            DateTime end = DateTime.Now;
            TimeSpan span = end - start;
            double seconds = span.TotalSeconds;
            Console.WriteLine("程序运行的时间是{0}s", seconds);


            Console.WriteLine("传送数据成功!");
            client2.Close();//释放资源
            newsock2.Close();
            Console.ReadKey();
            #endregion


            #region 采用变长的消息 即发送时先告诉客户端 消息的长度是多少
            //DateTime start = DateTime.Now;
            //double[] test = { 10.3, 15.2, 25.3, 13.338, 25.41, 0.99, 2017, 20000, 1, 3, 2, 5, 8, 90, 87, 33, 55, 99, 100 };
            //IPEndPoint IPep2 = new IPEndPoint(IPAddress.Any, 127);
            //Socket newsock2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //newsock2.Bind(IPep2);
            //newsock2.Listen(10);
            //Console.WriteLine("等待新客户端的连接");
            //Socket client2 = newsock2.Accept();//等待客户端的连接
            //IPEndPoint clientep2 = (IPEndPoint)client2.RemoteEndPoint;
            //Console.WriteLine("与{0}连接在{1}端口", clientep2.Address, clientep2.Port);
            //string welcome2 = "welcome to the new server";
            //byte[] date = Encoding.ASCII.GetBytes(welcome2);//字符串转换为字节数组  传送给客户端
            //SendVarMessage(client2, date);


            ////发送数组的长度信息 给客户端
            //date = Encoding.ASCII.GetBytes(test.Length.ToString());
            //SendVarMessage(client2, date);

            ////用循环的形式 依次将数组中的元素发送给客户端
            //string[] strvalue = new string[test.Length];
            //for (int j = 0; j < test.Length; j++)
            //{
            //    strvalue[j] = test[j].ToString();//将实际速度集合转换为string数组
            //    date = Encoding.ASCII.GetBytes(strvalue[j]);//string数组中的每个元素一次转换为字节 发送给客户端
            //    SendVarMessage(client2, date);
            //}

            //DateTime end = DateTime.Now;
            //TimeSpan span = end - start;
            //double seconds = span.TotalSeconds;
            //Console.WriteLine("程序运行的时间是{0}s", seconds);

            //Console.WriteLine("传送数据成功!");
            //client2.Close();//释放资源
            //newsock2.Close();
            //Console.ReadKey();
            #endregion
        }

        /// <summary>
        /// 发送变长消息方法
        /// </summary>
        /// <param name="s"></param>
        /// <param name="msg"></param>
        /// <returns>不需要返回值</returns>
        private static void SendVarMessage(Socket s, byte[] msg)
        {
            int offset = 0;
            int sent;
            int size = msg.Length;
            int dataleft = size;
            byte[] msgsize = new byte[2];

            //将消息的尺寸从整型转换成可以发送的字节型
            //因为int型是占4个字节 所以msgsize是4个字节 后边是空字节
            msgsize = BitConverter.GetBytes(size);

            //发送消息的长度信息
            //之前总是乱码出错 客户端接收到的欢迎消息前两个字节是空 后边的两个字符er传送到第二次接收的字节数组中
            //因此将er字符转换为int出错  这是因为之前在Send代码中,是将msgsize整个字节数组发送给客户端 所以导致第3 4个空格也发送
            //导致发送的信息混乱 这两个空格使发送的信息都往后挪了两个位置  从而乱码
            sent = s.Send(msgsize, 0, 2, SocketFlags.None);
            while (dataleft > 0)
            {
                int sent2 = s.Send(msg, offset, dataleft, SocketFlags.None);
                //设置偏移量
                offset += sent2;
                dataleft -= sent2;
            }
        }



        /// <summary>
        /// 给发送的消息 加特殊结束标记 头尾分别加"<" ">"
        /// </summary>
        /// <param name="s"></param>
        /// <param name="msg"></param>
        private static string SpecialMessage(string str)
        {
            string strNew = "<";
            strNew += str;
            strNew += ">";
            return strNew;
        }
    }
}

接收消息端,即客户端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Collections;

namespace _08接收变长消息的方法
{
    class Program
    {
        static void Main(string[] args)
        {
            #region 接收带有首尾特殊标记符的消息
            DateTime start = DateTime.Now;
            byte[] data = new byte[1024];
            IPEndPoint IPep2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 127);
            Socket server2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                server2.Connect(IPep2);
                Console.WriteLine("连接成功");
            }
            catch (SocketException e)
            {
                Console.WriteLine("连接服务器失败");
                Console.WriteLine(e.ToString());
                Console.ReadKey();
                return;
            }
            finally
            { }
            //接收来自服务器的数据
            server2.Receive(data);

            //一次性接收了服务器端发来的数据 利用ReceiveSpecialMessage方法接收并根据特殊标记
            //将消息一条一条的放在字符串数组strs中
            string[] strs = ReceiveSpecialMessage(data);
            string stringData = strs[0];
            Console.WriteLine(stringData);//接收并显示欢迎消息    成功!

            string length = strs[1];
            Console.WriteLine("共接收到{0}个数据", length);
            int n = Convert.ToInt32(length);

            //依次接收来自服务器端传送的实际速度值 成功!
            string[] speed = new string[n];
            for (int i = 0; i < n; i++)
            {
                speed[i] = strs[i + 2];
                Console.WriteLine("第{0}次的实际速度是{1}", i + 1, speed[i]);
            }

            DateTime end = DateTime.Now;
            TimeSpan span = end - start;
            double seconds = span.TotalSeconds;
            Console.WriteLine("程序运行的时间是{0}s", seconds);
            server2.Shutdown(SocketShutdown.Both);
            server2.Close();
            Console.ReadKey();
            #endregion

            #region 接收变长的消息 无特殊标记符
            //DateTime start = DateTime.Now;
            //byte[] data = new byte[1024];
            //IPEndPoint IPep2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 127);
            //Socket server2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //try
            //{
            //    server2.Connect(IPep2);
            //    Console.WriteLine("连接服务器成功");
            //}
            //catch (SocketException e)
            //{
            //    Console.WriteLine("连接服务器失败");
            //    Console.WriteLine(e.ToString());
            //    Console.ReadKey();
            //    return;
            //}
            //finally
            //{ }
            ////接收来自服务器的数据
            //data = ReceiveVarMessage(server2);
            //string stringData = Encoding.ASCII.GetString(data, 0, data.Length);
            //Console.WriteLine(stringData);//接收并显示欢迎消息    成功!

            //data = ReceiveVarMessage(server2);
            //string length = Encoding.ASCII.GetString(data);
            //Console.WriteLine("共接收到{0}个数据", length);
            //int n = Convert.ToInt32(length);

            ////依次接收来自服务器端传送的实际速度值 成功!
            //string[] speed = new string[n];
            //for (int i = 0; i < n; i++)
            //{
            //    data = ReceiveVarMessage(server2);
            //    speed[i] = Encoding.ASCII.GetString(data, 0, data.Length);
            //    Console.WriteLine("第{0}次的实际速度是{1}", i + 1, speed[i]);
            //}

            //DateTime end = DateTime.Now;
            //TimeSpan span = end - start;
            //double seconds = span.TotalSeconds;
            //Console.WriteLine("程序运行的时间是{0}s", seconds);

            //server2.Shutdown(SocketShutdown.Both);
            //server2.Close();
            //Console.ReadKey();
            #endregion
        }//Main函数


        /// <summary>
        /// 接收变长消息方法
        /// </summary>
        /// <param name="s"></param>
        /// <returns>接收到的信息</returns>
        private static byte[] ReceiveVarMessage(Socket s)//方法的返回值是字节数组 byte[] 存放的是接受到的信息
        {
            int offset = 0;
            int recv;
            byte[] msgsize = new byte[2];

            //接收2个字节大小的长度信息
            recv = s.Receive(msgsize, 0, 2, 0);

            //将字节数组的消息长度转换为整型
            int size = BitConverter.ToInt16(msgsize, 0);
            int dataleft = size;
            byte[] msg = new byte[size];
            while (dataleft > 0)
            {
                //接收数据
                recv = s.Receive(msg, offset, dataleft, 0);
                if (recv == 0)
                {
                    break;
                }
                offset += recv;
                dataleft -= recv;
            }
            return msg;
        }


        /// <summary>
        /// 接收结尾带有特殊标记的消息   哈哈
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        private static string[] ReceiveSpecialMessage(byte[] data)
        {
            string str = Encoding.ASCII.GetString(data);
            int i = 0;
            int j = 1;
            List<int> list_i = new List<int>();
            List<int> list_j = new List<int>();
            while (i < str.Length)  //找到特殊标记符 < 所在位置
            {
                if (str[i].ToString() == "<")
                {
                    list_i.Add(i);
                    i++;
                }
                else
                {
                    i++;
                    continue;
                }
            }
            while (j < str.Length)  //找到特殊标记符 > 所在的位置
            {

                if (str[j].ToString() == ">")
                {
                    list_j.Add(j);
                    j++;
                }
                else
                {
                    j++;
                    continue;
                }
            }
            string[] strs = new string[list_i.Count];
            for (int i1 = 0; i1 < strs.Length; i1++)
            {
                strs[i1] = str.Substring(list_i[i1] + 1, list_j[i1] - list_i[i1] - 1); //截取 < > 之间的字符串,是我们要的消息
            }
            return strs;
        }
    }
}

经验证,两种方法都没有问题,并且其运行时间,也没有太大差别(在发送较小的数据量下)。

赞(0) 打赏
未经允许不得转载:TaKaSa » 解决Socket通信中,经常遇到的问题——数据粘包的两种方法

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  • Q Q(选填)

赞助下

支付宝扫一扫打赏

微信扫一扫打赏