最近在作一個機器人項目,要實時的接收機器人傳回的座標信息,並在客戶端顯示當前的地圖和機器人的位置。固然座標的回傳是用的Socket,用的是C++的結構體表示的座標信息。可是C#不能像C++那樣很easy的把字節數組byte[]直接的轉換成結構,來發送和接收。在C#中要多作一些工做。可是在C或者C++中這是一件很容易的事,只須要一個函數:數組
void *memcpy(void *dest, const void *src, size_t n);//從源src所指的內存地址的起始位置開始拷貝n個字節到目標dest所指的內存地址的起始位置中
下面來完成經過C#實現Socket傳輸結構數據。服務器
1. 仿照C++的結構寫出C#的結構體來:爲了便於複用,把它放在一個單獨類裏面app
public class SocketStruct { [Serializable]//指示能夠序列化 [StructLayout(LayoutKind.Sequential, Pack = 1)]//按1字節對齊 public struct Operator { public ushort id; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)]//大小11個字節 public char[] name; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]//大小9個字節 public char[] password; //結構體的構造函數 public Operator(string _name, string _password) { this.id = 1000; //string.PadRight(int length, char ch); //把這個字符串擴充到11個字符的長度,在右邊填充字符‘\0’ //達到的效果就是字符串左對齊,右邊填充一些字符 this.name = _name.PadRight(11, '\0').ToCharArray(); this.password = _password.PadRight(9, '\0').ToCharArray(); }//構造函數 }//struct }
2. 既然要接收C++發送過來的數據,就要注意C#和C++數據類型的對應關係:socket
C++與C#的數據類型對應關係表:函數
因此上面定義的整個結構的字節數是22個bytes.工具
注意區分上面的字節和字符。計算機存儲容量基本單位是字節(Byte),8個二進制位組成1個字節,一個標準英文字母佔一個字節位置,一個標準漢字佔二個字節位置。字符是一種符號,同存儲單位不是一回事,它是一種抽象的、邏輯的類型,與int等同樣。byte是物理的單位。學習
對應的C++結構體是:this
typedef struct { WORD id; CHAR namep[11]; CHAR password[9]; }Operator;
3. 在發送數據時,要先把結果轉換成字節數組,在接收到數據以後要把字節數組還原成本來的結構。具體的代碼以下,爲了便於複用,寫成一個類:spa
public class BytesAndStruct { /// <summary> /// 將結構轉化爲字節數組 /// </summary> /// <param name="obj">結構對象</param> /// <returns>字節數組</returns> public static byte[] StructToBytes(object obj) { //獲得結構體的大小 int size = Marshal.SizeOf(obj); //分配結構體大小的內容空間 IntPtr structPtr = Marshal.AllocHGlobal(size); //將結構體copy到分配好的內存空間 Marshal.StructureToPtr(obj, structPtr, false); //建立byte數組 byte[] bytes = new byte[size]; //從內存空間拷貝到byte數組 Marshal.Copy(structPtr, bytes, 0, size); //釋放內存空間 Marshal.FreeHGlobal(structPtr); //返回byte數組 return bytes; }//StructToBytes /// <summary> /// byte數組轉換爲結構 /// </summary> /// <param name="bytes">byte數組</param> /// <param name="type">結構類型</param> /// <returns>轉換後的結構</returns> public static object BytesToStruct(byte[] bytes, Type type) { //獲得結構體的大小 int size = Marshal.SizeOf(type); //byte數組的長度小於結構的大小,不能徹底的初始化結構體 if (size > bytes.Length) { //返回空 return null; } //分配結構大小的內存空間 IntPtr structPtr = Marshal.AllocHGlobal(size); //將byte數組拷貝到分配好的內存空間 Marshal.Copy(bytes, 0, structPtr, size); //將內存空間轉換爲目標結構 object obj = Marshal.PtrToStructure(structPtr, type); //釋放內存空間 Marshal.FreeHGlobal(structPtr); //返回結構 return obj; } }
這是個工具類,裏面的方法都是靜態的。線程
寫一點注意的技巧:
在結構轉換成字節數據的時候,要把結構的類型做爲參數傳遞到函數中去,因此函數接收的參數是一個類型。這時用到了C#中的Type類。
C#中經過Type類能夠訪問任意數據類型信息。 1.獲取給定類型的Type引用有3種方式: a.使用typeof運算符,如Type t = typeof(int); b.使用GetType()方法,如int i;Type t = i.GetType(); c.使用Type類的靜態方法GetType(),如Type t =Type.GetType("System.Double"); 2.Type的屬性: Name:數據類型名; FullName:數據類型的徹底限定名,包括命名空間; Namespace:數據類型的命名空間; BaseType:直接基本類型; UnderlyingSystemType:映射類型; 3.Type的方法: GetMethod():返回一個方法的信息; GetMethods():返回全部方法的信息。
這裏其實就是:
Type type = myOper.GetType();//其中myOper是一個結構
而後就能利用type作一些反射的操做了,咱們這裏只是用它獲得結構的大小。
下面就是實際的操做使用:
在貼代碼以前,先學習一個方法,咱們能把字符串和字節數組很好的轉換了,如今也能把結構體和字節數組轉換了,可是字符數組和字符串怎麼轉換呢:
string 轉換成 Char[] string ss="abcdefg"; char[] cc=ss.ToCharArray(); Char[] 轉換成string string s=new string(cc);
先來看客戶端代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading; namespace ConsoleApplication7 { class Program { private static byte[] buffer = new byte[1024]; static void Main(string[] args) { //設定服務器ip地址 IPAddress ip = IPAddress.Parse("127.0.0.1"); Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { clientSocket.Connect(new IPEndPoint(ip, 8887)); Console.WriteLine("鏈接服務器成功"); } catch(Exception ex) { Console.WriteLine("服務器鏈接失敗,請按回車退出"); return; } //經過clientSocket接收數據 int receiveNumber = clientSocket.Receive(buffer); byte[] receiveBytes = new byte[receiveNumber]; //利用Array的Copy方法,把buffer的有效數據放置到一個新的字節數組 Array.Copy(buffer, receiveBytes, receiveNumber); //創建一個新的Operator類 SocketStruct.Operator myOper = new SocketStruct.Operator(); myOper = (SocketStruct.Operator)(BytesAndStruct.BytesToStruct(receiveBytes, myOper.GetType())); string id = myOper.id.ToString(); string name = new string(myOper.name); string password = new string(myOper.password); Console.WriteLine("結構體收到:" + id + " " + name + " " + password ); //啓動新的線程,給Server連續發送數據 Thread sendThread = new Thread(SendMessage); //把線程設置爲前臺線程,否則Main退出了線程就會死亡 sendThread.IsBackground = false; sendThread.Start(clientSocket); Console.ReadKey(); }//Main /// <summary> /// 啓動新的線程,發送數據 /// </summary> /// <param name="clientSocket"></param> private static void SendMessage(object clientSocket) { Socket sendSocket = (Socket)clientSocket; //利用新線程,經過sendSocket發送數據 for (int i = 0; i < 10; i++) { try { Thread.Sleep(1000); string sendMessage = "client send Message Hellp" + DateTime.Now; sendSocket.Send(Encoding.ASCII.GetBytes(sendMessage)); Console.WriteLine("向服務器發送消息:{0}", sendMessage); } catch (Exception ex) { sendSocket.Shutdown(SocketShutdown.Both); sendSocket.Close(); //一旦出錯,就結束循環 break; } }//for }//SendMessage() }//class }
這裏注意一下,咱們的接收數據緩衝區通常都設置的要比實際接收的數據要大,因此會空出一部分。可是在把字節數組轉換成結構的時候,要丟棄這些空白,因此按照接收到的字節的大小,從新new一個字節數字,並把有效數據拷貝進去。而後再轉換成結構。
服務器代碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net; using System.Net.Sockets; using System.Threading; namespace ConsoleApplication6 { class Program { //定義接收緩衝數組,端口號,監聽socket private static byte[] buffer = new byte[1024]; private static int port = 8887; private static Socket serverSocket; private static byte[] Message = BytesAndStruct.StructToBytes(new SocketStruct.Operator("stemon", "@xiao")); static void Main(string[] args) { //服務器IP地址 IPAddress ip = IPAddress.Parse("127.0.0.1"); serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); serverSocket.Bind(new IPEndPoint(ip, port));//綁定IP地址:端口 serverSocket.Listen(10);//設定最多10個鏈接請求排隊 Console.WriteLine("監聽:" + serverSocket.LocalEndPoint.ToString()); //創建線程監聽client鏈接請求 Thread myThread = new Thread(ListenClientConnection); //myThread.IsBackground = true; myThread.Start(); }//Main() /// <summary> /// 新線程:監聽客戶端鏈接 /// </summary> private static void ListenClientConnection() { while (true) { Socket clientSocket = serverSocket.Accept(); //把轉換好的字節數組發送出去 clientSocket.Send(Message); //沒接收到一個鏈接,啓動新線程接收數據 Thread receiveThread = new Thread(ReceiveMessage); receiveThread.Start(clientSocket); }//while }//ListenClientConnection() /// <summary> /// 接收數據消息 /// </summary> /// <param name="clientSocket">監聽socket生成的普統統信socket</param> private static void ReceiveMessage(object clientSocket) { Socket myClientSocket = (Socket)clientSocket; while (true) { try { //經過myClientSocket接收數據 int receiveNumber = myClientSocket.Receive(buffer); Console.WriteLine("接收客戶端{0}消息{1}", myClientSocket.RemoteEndPoint.ToString(), Encoding.ASCII.GetString(buffer, 0, receiveNumber)); } catch(Exception ex) { Console.WriteLine(ex.Message); //關閉全部的Socket鏈接功能Receive、Send、Both myClientSocket.Shutdown(SocketShutdown.Both); myClientSocket.Close(); break; } }//while }//ReceiveMessage() }//class }
這個程序代碼的很差之處在於沒有很好的處理好socket的關閉。不過這不是重點。
重點是實現C#中發送結構體數據。