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

最近比较忙,有段时间没写博客拉。最近项目中需要使用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 该单例类不要绑定在任何对象上

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

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

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

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

JFPackage.cs

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

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

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

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

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

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

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

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

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

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

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

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

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值