• [C#技术参考]Socket传输结构数据



    最近在做一个机器人项目,要实时的接收机器人传回的坐标信息,并在客户端显示当前的地图和机器人的位置。当然坐标的回传是用的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#的结构体来:为了便于复用,把它放在一个单独类里面

    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个字符的长度,在右边填充字符‘’
               //达到的效果就是字符串左对齐,右边填充一些字符
               this.name = _name.PadRight(11, '').ToCharArray();
               this.password = _password.PadRight(9, '').ToCharArray();
           }//构造函数
     
       }//struct
    }
    

      


    2. 既然要接收C++发送过来的数据,就要注意C#和C++数据类型的对应关系:

    C++与C#的数据类型对应关系表:

    QQ截图20150106001846

    所以上面定义的整个结构的字节数是22个bytes.

    注意区分上面的字节和字符。计算机存储容量基本单位是字节(Byte),8个二进制位组成1个字节,一个标准英文字母占一个字节位置,一个标准汉字占二个字节位置。字符是一种符号,同存储单位不是一回事,它是一种抽象的、逻辑的类型,与int等一样。byte是物理的单位。

    对应的C++结构体是:

    typedef struct
    {
        WORD id;
        CHAR namep[11];
        CHAR password[9];
    }Operator;
    

      

    3. 在发送数据时,要先把结果转换成字节数组,在接收到数据之后要把字节数组还原成原本的结构。具体的代码如下,为了便于复用,写成一个类:

    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#中发送结构体数据。

  • 相关阅读:
    C++函数模板的显示调用与隐式调用
    git显示颜色配置
    STL容器元素应满足的条件
    vector缩减容量
    PAT (Basic Level) Practise:1036. 跟奥巴马一起编程
    Core Java Volume I — 4.10. Class Design Hints
    Core Java Volume I — 4.7. Packages
    蓝牙(Profile)构成
    Android开发之Java必备基础
    主机控制器接口(HCI)
  • 原文地址:https://www.cnblogs.com/stemon/p/4204950.html
Copyright © 2020-2023  润新知