解决Socket通信中,经常遇到的问题——数据粘包的两种方法
PHP开源社区
免费提供10GPHP进阶架构师学习教程,关注即可免费领取。每天推送PHP最新资讯技术文章。
Official Account
数据粘包问题的出现,是因为在客户端/服务器端都会有一个比较大的数据缓冲区,来存放接收的数据,为了保证能够完整的接收到数据,因此缓冲区都会设置的比较大。在收发数据频繁时,由于tcp传输消息的无边界,会导致客户端/服务器端不知道接收到的消息到底是第几条消息,因此,会导致类似一次性接收几条消息的情况,从而乱码。
在每次发送消息之间,加入空循环,从而可以将消息隔离开来,但是这个方法会严重影响程序的运行效率。
方法一:数据粘包问题的出现是因为缓冲区过大,因此采用发送/接收变长消息的方法,在发送/接收消息时,将消息的长度作为消息的一部分发送出去,从而接收方可以根据传来的长度信息,制定相应长度的缓冲区;
方法二:将发送的每条消息的首尾都加上特殊标记符,前加"<" 后加">"。这里我采取的是先将要发送的所有消息,首尾加上特殊标记后,都先放在一个字符串string中,然后一次性的发送给接收方,接受之后,再根据标记符< >,将一条条消息择(zhái)出来。
代码如下:
发送消息端,即服务器端:
using System;
using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Net;
6 using System.Net.Sockets;
7 using System.Collections;
8 using System.Diagnostics;
9
10 namespace _07发送变长消息的方法
11 {
12 class Program
13 {
14 static void Main(string[] args)
15 {
16 #region 先将发送的消息带上首尾标记< > 再将要发送的所有消息,放在一个字符串中 然后一次性发送给客户端
17 DateTime start = DateTime.Now;//记录程序的运行时间
18 string str = null;
19 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 };
20 IPEndPoint IPep2 = new IPEndPoint(IPAddress.Any, 127);
21 Socket newsock2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
22 newsock2.Bind(IPep2);
23 newsock2.Listen(10);
24 Console.WriteLine("等待新客户端的连接");
25 Socket client2 = newsock2.Accept();//等待客户端的连接
26 IPEndPoint clientep2 = (IPEndPoint)client2.RemoteEndPoint;
27 Console.WriteLine("与{0}连接在{1}端口", clientep2.Address, clientep2.Port);
28 string welcome2 = "welcome to the new server";
29 byte[] data;
30 str += SpecialMessage(welcome2);//将要发送的消息,都放在字符串str中
31
32 //发送数组的长度信息 给字符串str
33 str += SpecialMessage(test.Length.ToString());
34
35 //用循环的形式 依次将数组中的元素给str
36 string[] strvalue = new string[test.Length];
37 for (int j = 0; j < test.Length; j++)
38 {
39 strvalue[j] = test[j].ToString();//将实际速度集合转换为string数组
40 str += SpecialMessage(strvalue[j]);
41 }
42
43 //将所有发送的信息,都放在了str中,然后一次性的发送过去 注意都是有首尾标记的消息< >
44 data = System.Text.Encoding.ASCII.GetBytes(str);
45 client2.Send(data);
46
47 DateTime end = DateTime.Now;
48 TimeSpan span = end - start;
49 double seconds = span.TotalSeconds;
50 Console.WriteLine("程序运行的时间是{0}s", seconds);
51
52
53 Console.WriteLine("传送数据成功!");
54 client2.Close();//释放资源
55 newsock2.Close();
56 Console.ReadKey();
57 #endregion
58
59
60 #region 采用变长的消息 即发送时先告诉客户端 消息的长度是多少
61 //DateTime start = DateTime.Now;
62 //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 };
63 //IPEndPoint IPep2 = new IPEndPoint(IPAddress.Any, 127);
64 //Socket newsock2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
65 //newsock2.Bind(IPep2);
66 //newsock2.Listen(10);
67 //Console.WriteLine("等待新客户端的连接");
68 //Socket client2 = newsock2.Accept();//等待客户端的连接
69 //IPEndPoint clientep2 = (IPEndPoint)client2.RemoteEndPoint;
70 //Console.WriteLine("与{0}连接在{1}端口", clientep2.Address, clientep2.Port);
71 //string welcome2 = "welcome to the new server";
72 //byte[] date = Encoding.ASCII.GetBytes(welcome2);//字符串转换为字节数组 传送给客户端
73 //SendVarMessage(client2, date);
74
75
76 ////发送数组的长度信息 给客户端
77 //date = Encoding.ASCII.GetBytes(test.Length.ToString());
78 //SendVarMessage(client2, date);
79
80 ////用循环的形式 依次将数组中的元素发送给客户端
81 //string[] strvalue = new string[test.Length];
82 //for (int j = 0; j < test.Length; j++)
83 //{
84 // strvalue[j] = test[j].ToString();//将实际速度集合转换为string数组
85 // date = Encoding.ASCII.GetBytes(strvalue[j]);//string数组中的每个元素一次转换为字节 发送给客户端
86 // SendVarMessage(client2, date);
87 //}
88
89 //DateTime end = DateTime.Now;
90 //TimeSpan span = end - start;
91 //double seconds = span.TotalSeconds;
92 //Console.WriteLine("程序运行的时间是{0}s", seconds);
93
94 //Console.WriteLine("传送数据成功!");
95 //client2.Close();//释放资源
96 //newsock2.Close();
97 //Console.ReadKey();
98 #endregion
99 }
100
101 /// <summary>
102 /// 发送变长消息方法
103 /// </summary>
104 /// <param name="s"></param>
105 /// <param name="msg"></param>
106 /// <returns>不需要返回值</returns>
107 private static void SendVarMessage(Socket s, byte[] msg)
108 {
109 int offset = 0;
110 int sent;
111 int size = msg.Length;
112 int dataleft = size;
113 byte[] msgsize = new byte[2];
114
115 //将消息的尺寸从整型转换成可以发送的字节型
116 //因为int型是占4个字节 所以msgsize是4个字节 后边是空字节
117 msgsize = BitConverter.GetBytes(size);
118
119 //发送消息的长度信息
120 //之前总是乱码出错 客户端接收到的欢迎消息前两个字节是空 后边的两个字符er传送到第二次接收的字节数组中
121 //因此将er字符转换为int出错 这是因为之前在Send代码中,是将msgsize整个字节数组发送给客户端 所以导致第3 4个空格也发送
122 //导致发送的信息混乱 这两个空格使发送的信息都往后挪了两个位置 从而乱码
123 sent = s.Send(msgsize, 0, 2, SocketFlags.None);
124 while (dataleft > 0)
125 {
126 int sent2 = s.Send(msg, offset, dataleft, SocketFlags.None);
127 //设置偏移量
128 offset += sent2;
129 dataleft -= sent2;
130 }
131 }
132
133
134
135 /// <summary>
136 /// 给发送的消息 加特殊结束标记 头尾分别加"<" ">"
137 /// </summary>
138 /// <param name="s"></param>
139 /// <param name="msg"></param>
140 private static string SpecialMessage(string str)
141 {
142 string strNew = "<";
143 strNew += str;
144 strNew += ">";
145 return strNew;
146 }
147 }
148 }
接收消息端,即客户端:
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Text;
5 using System.Net;
6 using System.Net.Sockets;
7 using System.Collections;
8
9 namespace _08接收变长消息的方法
10 {
11 class Program
12 {
13 static void Main(string[] args)
14 {
15 #region 接收带有首尾特殊标记符的消息
16 DateTime start = DateTime.Now;
17 byte[] data = new byte[1024];
18 IPEndPoint IPep2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 127);
19 Socket server2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
20 try
21 {
22 server2.Connect(IPep2);
23 Console.WriteLine("连接成功");
24 }
25 catch (SocketException e)
26 {
27 Console.WriteLine("连接服务器失败");
28 Console.WriteLine(e.ToString());
29 Console.ReadKey();
30 return;
31 }
32 finally
33 { }
34 //接收来自服务器的数据
35 server2.Receive(data);
36
37 //一次性接收了服务器端发来的数据 利用ReceiveSpecialMessage方法接收并根据特殊标记
38 //将消息一条一条的放在字符串数组strs中
39 string[] strs = ReceiveSpecialMessage(data);
40 string stringData = strs[0];
41 Console.WriteLine(stringData);//接收并显示欢迎消息 成功!
42
43 string length = strs[1];
44 Console.WriteLine("共接收到{0}个数据", length);
45 int n = Convert.ToInt32(length);
46
47 //依次接收来自服务器端传送的实际速度值 成功!
48 string[] speed = new string[n];
49 for (int i = 0; i < n; i++)
50 {
51 speed[i] = strs[i + 2];
52 Console.WriteLine("第{0}次的实际速度是{1}", i + 1, speed[i]);
53 }
54
55 DateTime end = DateTime.Now;
56 TimeSpan span = end - start;
57 double seconds = span.TotalSeconds;
58 Console.WriteLine("程序运行的时间是{0}s", seconds);
59 server2.Shutdown(SocketShutdown.Both);
60 server2.Close();
61 Console.ReadKey();
62 #endregion
63
64 #region 接收变长的消息 无特殊标记符
65 //DateTime start = DateTime.Now;
66 //byte[] data = new byte[1024];
67 //IPEndPoint IPep2 = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 127);
68 //Socket server2 = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
69 //try
70 //{
71 // server2.Connect(IPep2);
72 // Console.WriteLine("连接服务器成功");
73 //}
74 //catch (SocketException e)
75 //{
76 // Console.WriteLine("连接服务器失败");
77 // Console.WriteLine(e.ToString());
78 // Console.ReadKey();
79 // return;
80 //}
81 //finally
82 //{ }
83 ////接收来自服务器的数据
84 //data = ReceiveVarMessage(server2);
85 //string stringData = Encoding.ASCII.GetString(data, 0, data.Length);
86 //Console.WriteLine(stringData);//接收并显示欢迎消息 成功!
87
88 //data = ReceiveVarMessage(server2);
89 //string length = Encoding.ASCII.GetString(data);
90 //Console.WriteLine("共接收到{0}个数据", length);
91 //int n = Convert.ToInt32(length);
92
93 ////依次接收来自服务器端传送的实际速度值 成功!
94 //string[] speed = new string[n];
95 //for (int i = 0; i < n; i++)
96 //{
97 // data = ReceiveVarMessage(server2);
98 // speed[i] = Encoding.ASCII.GetString(data, 0, data.Length);
99 // Console.WriteLine("第{0}次的实际速度是{1}", i + 1, speed[i]);
100 //}
101
102 //DateTime end = DateTime.Now;
103 //TimeSpan span = end - start;
104 //double seconds = span.TotalSeconds;
105 //Console.WriteLine("程序运行的时间是{0}s", seconds);
106
107 //server2.Shutdown(SocketShutdown.Both);
108 //server2.Close();
109 //Console.ReadKey();
110 #endregion
111 }//Main函数
112
113
114 /// <summary>
115 /// 接收变长消息方法
116 /// </summary>
117 /// <param name="s"></param>
118 /// <returns>接收到的信息</returns>
119 private static byte[] ReceiveVarMessage(Socket s)//方法的返回值是字节数组 byte[] 存放的是接受到的信息
120 {
121 int offset = 0;
122 int recv;
123 byte[] msgsize = new byte[2];
124
125 //接收2个字节大小的长度信息
126 recv = s.Receive(msgsize, 0, 2, 0);
127
128 //将字节数组的消息长度转换为整型
129 int size = BitConverter.ToInt16(msgsize, 0);
130 int dataleft = size;
131 byte[] msg = new byte[size];
132 while (dataleft > 0)
133 {
134 //接收数据
135 recv = s.Receive(msg, offset, dataleft, 0);
136 if (recv == 0)
137 {
138 break;
139 }
140 offset += recv;
141 dataleft -= recv;
142 }
143 return msg;
144 }
145
146
147 /// <summary>
148 /// 接收结尾带有特殊标记的消息 哈哈
149 /// </summary>
150 /// <param name="s"></param>
151 /// <returns></returns>
152 private static string[] ReceiveSpecialMessage(byte[] data)
153 {
154 string str = Encoding.ASCII.GetString(data);
155 int i = 0;
156 int j = 1;
157 List<int> list_i = new List<int>();
158 List<int> list_j = new List<int>();
159 while (i < str.Length) //找到特殊标记符 < 所在位置
160 {
161 if (str[i].ToString() == "<")
162 {
163 list_i.Add(i);
164 i++;
165 }
166 else
167 {
168 i++;
169 continue;
170 }
171 }
172 while (j < str.Length) //找到特殊标记符 > 所在的位置
173 {
174
175 if (str[j].ToString() == ">")
176 {
177 list_j.Add(j);
178 j++;
179 }
180 else
181 {
182 j++;
183 continue;
184 }
185 }
186 string[] strs = new string[list_i.Count];
187 for (int i1 = 0; i1 < strs.Length; i1++)
188 {
189 strs[i1] = str.Substring(list_i[i1] + 1, list_j[i1] - list_i[i1] - 1); //截取 < > 之间的字符串,是我们要的消息
190 }
191 return strs;
192 }
193 }
194 }
经验证,两种方法都没有问题,并且其运行时间,也没有太大差别(在发送较小的数据量下)。
来源:https://www.cnblogs.com/1987-05-04/p/6727922.html
扫描关注 进入”PHP资料“
免费获取进阶
面试、文档、视频资源