unity深入研究--开发之C#使用Socket与HTTP连接服务器传输数据包

unity深入研究--开发之C#使用Socket与HTTP连接服务器传输数据包

转载  2013年01月04日 09:01:15
最近比较忙,有段时间没写博客拉。最近项目中需要使用HTTP与Socket,雨松MOMO把自己这段时间学习的资料整理一下。有关Socket与HTTP的基础知识MOMO就不赘述拉,不懂得朋友自己谷歌吧。我们项目的需求是在登录的时候使用HTTP请求,游戏中其它的请求都用Socket请求,比如人物移动同步坐标,同步关卡等等。

1.Socket

Socket不要写在脚本上,如果写在脚本上游戏场景一旦切换,那么这条脚本会被释放掉,Socket会断开连接。场景切换完毕后需要重新在与服务器建立Socket连接,这样会很麻烦。所以我们需要把Socket写在一个单例的类中,不用继承MonoBehaviour。这个例子我模拟一下,主角在游戏中移动,时时向服务端发送当前坐标,当服务器返回同步坐标时角色开始同步服务端新角色坐标。

Socket在发送消息的时候采用的是字节数组,也就是说无论你的数据是 int float short object 都会将这些数据类型先转换成byte[] , 目前在处理发送的地方我使用的是数据包,也就是把(角色坐标)结构体object转换成byte[]发送, 这就牵扯一个问题, 如何把结构体转成字节数组, 如何把字节数组回转成结构体。请大家接续阅读,答案就在后面,哇咔咔。

直接上代码

JFSocket.cs 该单例类不要绑定在任何对象上

001 using UnityEngine; 
002 using System.Collections; 
003 using System; 
004 using System.Threading; 
005 using System.Text; 
006 using System.Net; 
007 using System.Net.Sockets; 
008 using System.Collections.Generic; 
009 using System.IO; 
010 using System.Runtime.InteropServices; 
011 using System.Runtime.Serialization; 
012 using System.Runtime.Serialization.Formatters.Binary; 
013
014 public class JFSocket 
015 {
016
017 //Socket客户端对象
018 privateSocket clientSocket;
019 //JFPackage.WorldPackage是我封装的结构体,
020 //在与服务器交互的时候会传递这个结构体
021 //当客户端接到到服务器返回的数据包时,我把结构体add存在链表中。
022 publicList<JFPackage.WorldPackage> worldpackage;
023 //单例模式
024 privatestatic JFSocket instance;
025 publicstatic JFSocket GetInstance()
026 {
027 if(instance == null)
028 {
029 instance =new JFSocket();
030 }
031 returninstance;
032 }
033
034 //单例的构造函数
035 JFSocket()
036 {
037 //创建Socket对象, 这里我的连接类型是TCP
038 clientSocket =new Socket (AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
039 //服务器IP地址
040 IPAddress ipAddress = IPAddress.Parse ("192.168.1.100");
041 //服务器端口
042 IPEndPoint ipEndpoint =new IPEndPoint (ipAddress, 10060);
043 //这是一个异步的建立连接,当连接建立成功时调用connectCallback方法
044 IAsyncResult result = clientSocket.BeginConnect (ipEndpoint,newAsyncCallback (connectCallback),clientSocket);
045 //这里做一个超时的监测,当连接超过5秒还没成功表示超时
046 boolsuccess = result.AsyncWaitHandle.WaitOne( 5000, true ); 
047 if( !success )
048 {
049 //超时
050 Closed();
051 Debug.Log("connect Time Out");
052 }else
053 {
054 //与socket建立连接成功,开启线程接受服务端数据。
055 worldpackage =new List<JFPackage.WorldPackage>();
056 Thread thread =new Thread(newThreadStart(ReceiveSorket)); 
057 thread.IsBackground =true;
058 thread.Start();
059 }
060 }
061
062 privatevoid connectCallback(IAsyncResult asyncConnect)
063 {
064 Debug.Log("connectSuccess");
065 }
066
067 privatevoid ReceiveSorket()
068 {
069 //在这个线程中接受服务器返回的数据
070 while(true)
071 {
072
073 if(!clientSocket.Connected)
074 {
075 //与服务器断开连接跳出循环
076 Debug.Log("Failed to clientSocket server.");
077 clientSocket.Close();
078 break;
079 }
080 try
081 {
082 //接受数据保存至bytes当中
083 byte[] bytes =new byte[4096];
084 //Receive方法中会一直等待服务端回发消息
085 //如果没有回发会一直在这里等着。
086 inti = clientSocket.Receive(bytes);
087 if(i <= 0)
088 {
089 clientSocket.Close();
090 break;
091 }
092
093 //这里条件可根据你的情况来判断。
094 //因为我目前的项目先要监测包头长度,
095 //我的包头长度是2,所以我这里有一个判断
096 if(bytes.Length > 2)
097 {
098 SplitPackage(bytes,0);
099 }else
100 {
101 Debug.Log("length is not > 2");
102 }
103
104 }
105 catch(Exception e)
106 {
107 Debug.Log("Failed to clientSocket error."+ e);
108 clientSocket.Close();
109 break;
110 }
111 }
112 }
113
114 privatevoid SplitPackage(byte[] bytes ,int index) 
115 {
116 //在这里进行拆包,因为一次返回的数据包的数量是不定的
117 //所以需要给数据包进行查分。
118 while(true)
119 {
120 //包头是2个字节
121 byte[] head =new byte[2];
122 intheadLengthIndex = index + 2;
123 //把数据包的前两个字节拷贝出来
124 Array.Copy(bytes,index,head,0,2);
125 //计算包头的长度
126 shortlength = BitConverter.ToInt16(head,0);
127 //当包头的长度大于0 那么需要依次把相同长度的byte数组拷贝出来
128 if(length > 0)
129 {
130 byte[] data =new byte[length];
131 //拷贝出这个包的全部字节数
132 Array.Copy(bytes,headLengthIndex,data,0,length);
133 //把数据包中的字节数组强制转换成数据包的结构体
134 //BytesToStruct()方法就是用来转换的
135 //这里需要和你们的服务端程序商量,
136 JFPackage.WorldPackage wp =new JFPackage.WorldPackage();
137 wp = (JFPackage.WorldPackage)BytesToStruct(data,wp.GetType());
138 //把每个包的结构体对象添加至链表中。
139 worldpackage.Add(wp);
140 //将索引指向下一个包的包头
141 index = headLengthIndex + length;
142
143 }else
144 {
145 //如果包头为0表示没有包了,那么跳出循环
146 break;
147 }
148 }
149 }
150
151 //向服务端发送一条字符串
152 //一般不会发送字符串 应该是发送数据包
153 publicvoid SendMessage(stringstr) 
154 {
155 byte[] msg = Encoding.UTF8.GetBytes(str);
156
157 if(!clientSocket.Connected)
158 {
159 clientSocket.Close();
160 return;
161 }
162 try
163 {
164 //int i = clientSocket.Send(msg);
165 IAsyncResult asyncSend = clientSocket.BeginSend (msg,0,msg.Length,SocketFlags.None,newAsyncCallback (sendCallback),clientSocket);
166 boolsuccess = asyncSend.AsyncWaitHandle.WaitOne( 5000, true ); 
167 if( !success )
168 {
169 clientSocket.Close();
170 Debug.Log("Failed to SendMessage server.");
171 }
172 }
173 catch
174 {
175 Debug.Log("send message error");
176 }
177 }
178
179 //向服务端发送数据包,也就是一个结构体对象
180 publicvoid SendMessage(objectobj) 
181 {
182
183 if(!clientSocket.Connected)
184 {
185 clientSocket.Close();
186 return;
187 }
188 try
189 {
190 //先得到数据包的长度
191 shortsize = (short)Marshal.SizeOf(obj);
192 //把数据包的长度写入byte数组中
193 byte[] head = BitConverter.GetBytes(size);
194 //把结构体对象转换成数据包,也就是字节数组
195 byte[] data = StructToBytes(obj);
196
197 //此时就有了两个字节数组,一个是标记数据包的长度字节数组, 一个是数据包字节数组,
198 //同时把这两个字节数组合并成一个字节数组
199
200 byte[] newByte =new byte[head.Length + data.Length];
201 Array.Copy(head,0,newByte,0,head.Length);
202 Array.Copy(data,0,newByte,head.Length, data.Length);
203
204 //计算出新的字节数组的长度
205 intlength = Marshal.SizeOf(size) + Marshal.SizeOf(obj);
206
207 //向服务端异步发送这个字节数组
208 IAsyncResult asyncSend = clientSocket.BeginSend (newByte,0,length,SocketFlags.None,newAsyncCallback (sendCallback),clientSocket);
209 //监测超时
210 boolsuccess = asyncSend.AsyncWaitHandle.WaitOne( 5000, true ); 
211 if( !success )
212 {
213 clientSocket.Close();
214 Debug.Log("Time Out !");
215 }
216
217 }
218 catch(Exception e)
219 {
220 Debug.Log("send message error: "+ e );
221 }
222 }
223
224 //结构体转字节数组
225 publicbyte[] StructToBytes(objectstructObj)
226 {
227
228 intsize = Marshal.SizeOf(structObj);
229 IntPtr buffer = Marshal.AllocHGlobal(size);
230 try
231 {
232 Marshal.StructureToPtr(structObj,buffer,false);
233 byte[] bytes =new byte[size];
234 Marshal.Copy(buffer, bytes,0,size);
235 returnbytes;
236 }
237 finally
238 {
239 Marshal.FreeHGlobal(buffer);
240 }
241 }
242 //字节数组转结构体
243 publicobject BytesToStruct(byte[] bytes, Type strcutType)
244 {
245 intsize = Marshal.SizeOf(strcutType);
246 IntPtr buffer = Marshal.AllocHGlobal(size);
247 try
248 {
249 Marshal.Copy(bytes,0,buffer,size);
250 returnMarshal.PtrToStructure(buffer, strcutType);
251 }
252 finally
253 {
254 Marshal.FreeHGlobal(buffer);
255 }
256
257 }
258
259 privatevoid sendCallback (IAsyncResult asyncSend)
260 {
261
262 }
263
264 //关闭Socket
265 publicvoid Closed() 
266 {
267
268 if(clientSocket !=null && clientSocket.Connected)
269 {
270 clientSocket.Shutdown(SocketShutdown.Both);
271 clientSocket.Close();
272 }
273 clientSocket =null;
274 }
275
276 }

为了与服务端达成默契,判断数据包是否完成。我们需要在数据包中定义包头 ,包头一般是这个数据包的长度,也就是结构体对象的长度。正如代码中我们把两个数据类型 short 和 object 合并成一个新的字节数组。

然后是数据包结构体的定义,需要注意如果你在做IOS和Android的话数据包中不要包含数组,不然在结构体转换byte数组的时候会出错。

Marshal.StructureToPtr () error : Attempting to JIT compile method

JFPackage.cs

01 using UnityEngine; 
02 using System.Collections; 
03 using System.Runtime.InteropServices; 
04
05 public class JFPackage 
06 {
07 //结构体序列化
08 [System.Serializable]
09 //4字节对齐 iphone 和 android上可以1字节对齐
10 [StructLayout(LayoutKind.Sequential, Pack = 4)]
11 publicstruct WorldPackage
12 {
13 publicbyte mEquipID; 
14 publicbyte mAnimationID;
15 publicbyte mHP; 
16 publicshort mPosx; 
17 publicshort mPosy; 
18 publicshort mPosz; 
19 publicshort mRosx; 
20 publicshort mRosy; 
21 publicshort mRosz; 
22
23 publicWorldPackage(shortposx,short posy,short posz, short rosx, short rosy, shortrosz,byte equipID,byteanimationID,bytehp)
24 {
25 mPosx = posx;
26 mPosy = posy;
27 mPosz = posz;
28 mRosx = rosx;
29 mRosy = rosy;
30 mRosz = rosz;
31 mEquipID = equipID;
32 mAnimationID = animationID;
33 mHP = hp;
34 }
35
36 };
37
38 }

在脚本中执行发送数据包的动作,在Start方法中得到Socket对象。

1 public JFSocket mJFsorket; 
2 void Start () 
3 {
4 mJFsorket = JFSocket.GetInstance();
5 }

让角色发生移动的时候,调用该方法向服务端发送数据。

01 void SendPlayerWorldMessage() 
02 {
03 //组成新的结构体对象,包括主角坐标旋转等。
04 Vector3 PlayerTransform = transform.localPosition;
05 Vector3 PlayerRotation = transform.localRotation.eulerAngles;
06 //用short的话是2字节,为了节省包的长度。这里乘以100 避免使用float 4字节。当服务器接受到的时候小数点向前移动两位就是真实的float数据
07 shortpx = (short)(PlayerTransform.x*100);
08 shortpy = (short)(PlayerTransform.y*100);
09 shortpz = (short)(PlayerTransform.z*100);
10 shortrx = (short)(PlayerRotation.x*100);
11 shortry = (short)(PlayerRotation.y*100);
12 shortrz = (short)(PlayerRotation.z*100);
13 byteequipID = 1;
14 byteanimationID =9;
15 bytehp = 2;
16 JFPackage.WorldPackage wordPackage =newJFPackage.WorldPackage(px,py,pz,rx,ry,rz,equipID,animationID,hp);
17 //通过Socket发送结构体对象
18 mJFsorket.SendMessage(wordPackage);
19 }

接着就是客户端同步服务器的数据,目前是测试阶段所以写的比较简陋,不过原理都是一样的。哇咔咔!!

01 //上次同步时间
02 privatefloat mSynchronous;
03 void Update () 
04 {
05 mSynchronous +=Time.deltaTime;
06 //在Update中每0.5s的时候同步一次
07 if(mSynchronous > 0.5f)
08 {
09 intcount = mJFsorket.worldpackage.Count;
10 //当接受到的数据包长度大于0 开始同步
11 if(count > 0)
12 {
13 //遍历数据包中 每个点的坐标
14 foreach(JFPackage.WorldPackage wpin mJFsorket.worldpackage)
15 {
16 floatx = (float)(wp.mPosx / 100.0f);
17 floaty = (float)(wp.mPosy /100.0f);
18 floatz = (float)(wp.mPosz /100.0f);
19 Debug.Log("x = "+ x + " y = "+ y+" z = " + z); 
20 //同步主角的新坐标
21 mPlayer.transform.position =new Vector3 (x,y,z);
22 }
23 //清空数据包链表
24 mJFsorket.worldpackage.Clear();
25 }
26 mSynchronous = 0;
27 }
28 }

主角移动的同时,通过Socket时时同步坐标喔。。有没有感觉这个牛头人非常帅气 哈哈哈。

对于Socket的使用,我相信没有比MSDN更加详细的了。 有关Socket 同步请求异步请求的地方可以参照MSDN 链接地址给出来了,好好学习吧,嘿嘿。http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socket.aspx

上述代码中我使用的是Thread() 没有使用协同任务StartCoroutine() ,原因是协同任务必需要继承MonoBehaviour,并且该脚本要绑定在游戏对象身上。问题绑定在游戏对象身上切换场景的时候这个脚本必然会释放,那么Socket肯定会断开连接,所以我需要用Thread,并且协同任务它并不是严格意义上的多线程。

2.HTTP

HTTP请求在Unity我相信用的会更少一些,因为HTTP会比SOCKET慢很多,因为它每次请求完都会断开。废话不说了, 我用HTTP请求制作用户的登录。用HTTP请求直接使用Unity自带的www类就可以,因为HTTP请求只有登录才会有, 所以我就在脚本中来完成, 使用 www 类 和 协同任务StartCoroutine()。

01 using UnityEngine; 
02 using System.Collections; 
03 using System.Collections.Generic; 
04 public class LoginGlobe : MonoBehaviour { 
05
06 voidStart ()
07 {
08 //GET请求
09 StartCoroutine(GET("http://xuanyusong.com/"));
10
11 }
12
13 voidUpdate ()
14 {
15
16 }
17
18 voidOnGUI()
19 {
20
21 }
22
23 //登录
24 publicvoid LoginPressed()
25 {
26 //登录请求 POST 把参数写在字典用 通过www类来请求
27 Dictionary<string,string> dic =new Dictionary<string,string> ();
28 //参数
29 dic.Add("action","0");
30 dic.Add("usrname","xys");
31 dic.Add("psw","123456");
32
33 StartCoroutine(POST("http://192.168.1.12/login.php",dic));
34
35 }
36 //注册
37 publicvoid SingInPressed()
38 {
39 //注册请求 POST
40 Dictionary<string,string> dic =new Dictionary<string,string> ();
41 dic.Add("action","1");
42 dic.Add("usrname","xys");
43 dic.Add("psw","123456");
44
45 StartCoroutine(POST("http://192.168.1.12/login.php",dic));
46 }
47
48 //POST请求
49 IEnumerator POST(stringurl, Dictionary<string,string> post)
50 {
51 WWWForm form =new WWWForm();
52 foreach(KeyValuePair<string,string> post_arg in post) 
53 {
54 form.AddField(post_arg.Key, post_arg.Value);
55 }
56
57 WWW www =new WWW(url, form);
58 yieldreturn www; 
59
60 if(www.error != null)
61 {
62 //POST请求失败
63 Debug.Log("error is :"+ www.error);
64
65 else
66 {
67 //POST请求成功
68 Debug.Log("request ok : "+ www.text);
69 }
70 }
71
72 //GET请求
73 IEnumerator GET(stringurl)
74 {
75
76 WWW www =new WWW (url);
77 yieldreturn www; 
78
79 if(www.error != null)
80 {
81 //GET请求失败
82 Debug.Log("error is :"+ www.error);
83
84 else
85 {
86 //GET请求成功
87 Debug.Log("request ok : "+ www.text);
88 }
89 }
90
91 }

如果想通过HTTP传递二进制流的话 可以使用 下面的方法。

1 WWWForm wwwForm = newWWWForm();
2 byte[] byteStream = System.Text.Encoding.Default.GetBytes(stream);
3 wwwForm.AddBinaryData("post", byteStream);
4 www = new WWW(Address, wwwForm);

目前Socket数据包还是没有进行加密算法,后期我会补上。欢迎讨论,互相学习互相进度 加油,蛤蛤。

转载http://www.xuanyusong.com/archives/1948

相关文章
相关标签/搜索