.Net网络通讯编程[利用Socket实现字串、文件、序列化对象传输]--类设计2[使用IE浏览本页]

来源:互联网 发布:电视推荐 知乎 编辑:程序博客网 时间:2024/06/05 09:35

本案例使用.Net Socket的Tcp、Udp实现字串、文件、各种序列化对象的网络传输,同时封装了Tcp的粘包、半包处理细节,定义了网络封包格式,在发送端和接收端无需考虑内部传输细节。以下是类设计:

网络封包服务类设计抽象类提供Tcp、Udp共有的行为和特征,Tcp、Udp发包和收包的细节不同,所以发包方法和收包方法定义为抽象方法去子类实现

提供网络封包传输服务的核心类代码:

示范代码using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.IO;using System.Runtime.Serialization.Formatters.Binary;using System.Runtime.InteropServices;using System.Net.Sockets;using System.Net;namespace TcpLabCommon{    /// <summary>    /// 网络封包服务类[提供封包的发送和读取]    /// </summary>    public abstract class NetPacketService    {        /// <summary>        /// Tcp传输调用该构造方法        /// </summary>        /// <param name="netStream"></param>        public NetPacketService(NetworkStream netStream)        {            this._netStream = netStream;            this._protocolType = System.Net.Sockets.ProtocolType.Tcp;        }        /// <summary>        /// Tcp传输调用该构造方法        /// </summary>        /// <param name="netStream"></param>        public NetPacketService(Socket tcpSocket)        {            this._tcpSocket = tcpSocket;            this._netStream = new NetworkStream(this._tcpSocket);            this._protocolType = System.Net.Sockets.ProtocolType.Tcp;        }        /// <summary>        /// Udp传输调用该构造方法        /// </summary>        /// <param name="udpSocket">Udp套接字</param>        /// <param name="udpRemoteEndPoint">Udp发送的目标端点</param>        public NetPacketService(Socket udpSocket,EndPoint udpRemoteEndPoint)        {            this._udpSocket = udpSocket;            this._udpRemoteEndPoint = udpRemoteEndPoint;            this._protocolType = System.Net.Sockets.ProtocolType.Udp;        }        /// <summary>        /// 从网络文件数据中取出文件信息        /// </summary>        /// <param name="netFilebuffer">传送的文件,其内容=文件名长度(4字节)+文件名+文件内容</param>        /// <returns>包含文件名称和文件内容的对象</returns>        public NetFile ParseFile(Byte[] netFilebuffer)        {            if (netFilebuffer == null || netFilebuffer.Length == 0)                return null;            NetFile f = new NetFile();            int filenamelen = BitConverter.ToInt32(netFilebuffer, 0);//文件名长度            f.FileName = Encoding.Default.GetString(netFilebuffer, sizeof(Int32), filenamelen);            f.Content = new Byte[netFilebuffer.Length - sizeof(Int32) - filenamelen];            Array.Copy(netFilebuffer, sizeof(Int32) + filenamelen, f.Content, 0, f.Content.Length);            return f;        }        /// <summary>        /// 提取一个完整网络包然后返回[Tcp收包和Udp收包的行为不同,故转到子类实现]        ///     如果收到的包是STRING类型,NetPacket.Data是发送的字符串        ///     如果收到的包是BINARY类型,NetPacket.Data是发送的文件名长度+文件名+文件内容,请调用ParseFile函数进行解析        ///     如果收到的包是COMPLEX类型,NetPacket.Data是发序列化的对象,可以直接转型使用        /// </summary>        /// <returns></returns>        public abstract NetPacket ReceivePacket();        /// <summary>        /// 发送文本        /// </summary>        /// <param name="content"></param>        public void SendText(string content)        {            NetPacket packet = new NetPacket()            {                Data = content,                PacketHead = new NetPacketHead()                {                    Version = 1,                    PType = PacketType.STRING                }            };            SendPacket(packet);        }        /// <summary>        /// 发送文件        /// </summary>        /// <param name="filePath"></param>        public void SendFile(string filePath)        {            if (!File.Exists(filePath))                return;            FileStream fs = null;            try            {                Byte[] filename = Encoding.Default.GetBytes(new FileInfo(filePath).Name);                fs = new FileStream(filePath, FileMode.Open);                Byte[] content = new Byte[sizeof(Int32) + filename.Length + fs.Length];//文件数据包包体大小=文件名长度(4字节)+文件名+文件内容                Array.Copy(BitConverter.GetBytes(filename.Length), 0, content, 0, sizeof(Int32));//把"文件名长度(4字节)"放入数组                Array.Copy(filename, 0, content, sizeof(Int32), filename.Length);//把"文件名字节数组"放入数组                fs.Read(content, sizeof(Int32) + filename.Length, (Int32)fs.Length);//把"文件内容"放入数组                //开始发包                SendPacket                (                    new NetPacket()                    {                        Data = content,                        PacketHead = new NetPacketHead()                        {                            Version = 1,                            PType = PacketType.BINARY,                            Len = content.Length                        }                    }                );            }            finally            {                fs.Close();            }        }        /// <summary>        ///  发送可序列化的对象        /// </summary>        /// <param name="graph"></param>        public void SendObject(object graph)        {            if (graph == null || !graph.GetType().IsSerializable)                return;            SendPacket(                new NetPacket()                {                    Data = graph,                    PacketHead = new NetPacketHead()                    {                        Version = 1,                        PType = PacketType.COMPLEX,                        Len = GetCanSerializableObjectSize(graph)                    }                }             );        }        /// <summary>        /// 序列化Helper        /// </summary>        /// <typeparam name="T"></typeparam>        /// <returns></returns>        public ISerializeHelper SerializeHelper<T>() where T : ISerializeHelper        {            return Activator.CreateInstance<T>();        }        /// <summary>        /// 缓冲区大小1024字节        /// </summary>        public const Int32 BUFFER_SIZE = 1024;        /// <summary>        /// Udp数据报最大值        /// </summary>        protected const Int32 UDP_BUFFER_SIZE = 512;        private ProtocolType _protocolType = ProtocolType.Tcp;        /// <summary>        /// 协议类型[默认为Tcp]        /// </summary>        public ProtocolType ProtocolType        {            set            {                _protocolType = value;            }        }        /// <summary>        /// 接收网络数据的缓冲区        /// </summary>        protected Byte[] _netDataBuffer = new Byte[BUFFER_SIZE * 64];                /// <summary>        /// 缓冲区实际数据长度        /// </summary>        protected int _netDataOffset = 0;                /// <summary>        /// 网络流        /// </summary>        protected System.Net.Sockets.NetworkStream _netStream = null;                /// <summary>        /// 缓冲区        /// </summary>        protected Byte[] _tempBuffer = new Byte[BUFFER_SIZE];        /// <summary>        /// Udp套接字        /// </summary>        protected Socket _udpSocket;        /// <summary>        /// Tcp套接字        /// </summary>        protected Socket _tcpSocket;        /// <summary>        /// Udp发送的目标端点        /// </summary>        protected System.Net.EndPoint _udpRemoteEndPoint;        public System.Net.EndPoint UdpRemoteEndPoint        {            get { return _udpRemoteEndPoint; }            set { _udpRemoteEndPoint = value; }        }                /// <summary>        /// 发包[Tcp发包和Udp发包行为略有不同,故转到子类实现]        /// </summary>        /// <param name="packet"></param>        protected abstract void SendPacket(NetPacket packet);        /// <summary>        /// 缓冲区是否满足一个完整包头        /// </summary>        /// <returns></returns>        protected Boolean IsFullNetPacketHead()        {            return _netDataOffset >= NetPacketHead.HEAD_SIZE;        }        /// <summary>        /// 一个完整的网络封包的大小        /// </summary>        protected Int32 FullNetPacketSize        {            get            {                if (!IsFullNetPacketHead())                throw new Exception("由于没有缓冲区数据不足一个包头大小因此不能获得完整网络封包大小");                return NetPacketHead.HEAD_SIZE + BitConverter.ToInt32(_netDataBuffer, 8);            }                    }        /// <summary>        /// 缓冲区是否满足一个完整封包        /// </summary>        /// <returns></returns>        protected Boolean IsFullNetPacket()        {                        return _netDataOffset >= NetPacketHead.HEAD_SIZE+BitConverter.ToInt32(_netDataBuffer,8);        }        /// <summary>        /// 获得可序列化得对象的大小        /// </summary>        /// <param name="graph"></param>        /// <returns></returns>        protected Int32 GetCanSerializableObjectSize(object graph)        {            MemoryStream mStream = new MemoryStream();            SerializeHelper<BinarySerializeHelper>().Serialize(mStream, graph);                 Int32 size =(Int32)mStream.Length;            mStream.Close();            return size;        }        /// <summary>        /// 从缓冲区提取一个包头,但并不从缓冲区删除该包头        /// </summary>        /// <returns></returns>        protected NetPacketHead PeekNetPacket()        {            NetPacketHead packetHead = new NetPacketHead();            packetHead.Version = BitConverter.ToInt32(_netDataBuffer, 0);//版本            packetHead.PType = (PacketType)BitConverter.ToInt32(_netDataBuffer, 4);//封包类型            packetHead.Len = BitConverter.ToInt32(_netDataBuffer, 8);            return packetHead;        }        /// <summary>        /// 从缓冲区提取一个网络包        /// </summary>        /// <returns></returns>        protected NetPacket PickNetPacket()        {                        #region【提取包头】            NetPacketHead packetHead = PeekNetPacket();            #endregion            #region【提取包体】            NetPacket packet = new NetPacket();            packet.PacketHead = packetHead;            Byte[] buffer = new Byte[packetHead.Len];            Array.Copy(_netDataBuffer, NetPacketHead.HEAD_SIZE, buffer, 0, packetHead.Len);            switch (packetHead.PType)            {                case PacketType.STRING:                    packet.Data = Encoding.Default.GetString(buffer);                    break;                case PacketType.BINARY:                    packet.Data = buffer;                    break;                case PacketType.COMPLEX:                    MemoryStream mStream = new MemoryStream();                    mStream.Write(buffer, 0, buffer.Length);                    mStream.Position = 0;                    packet.Data = SerializeHelper<BinarySerializeHelper>().DeSerialize<object>(mStream);                    mStream.Close();                    break;            }            #endregion            #region【从缓冲区删除该数据封包】            Array.Copy(_netDataBuffer, NetPacketHead.HEAD_SIZE + packetHead.Len, _netDataBuffer, 0, _netDataOffset - (NetPacketHead.HEAD_SIZE + packetHead.Len));            _netDataOffset -= (NetPacketHead.HEAD_SIZE + packetHead.Len);//缓冲区实际数据长度减去一个完整封包长度            #endregion            return packet;        }    }}

 

网络封包服务Tcp子类代码:

示范代码using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Net.Sockets;using System.IO;using System.Runtime.InteropServices;namespace TcpLabCommon{    /// <summary>    /// 网络封包Tcp服务类    /// </summary>    public class NetPacketTcpService:NetPacketService    {        public NetPacketTcpService(NetworkStream netStream):base(netStream){}        /// <summary>        /// 提取一个完整网络包然后返回        ///     如果收到的包是STRING类型,NetPacket.Data是发送的字符串        ///     如果收到的包是BINARY类型,NetPacket.Data是发送的文件名长度+文件名+文件内容,请调用ParseFile函数进行解析        ///     如果收到的包是COMPLEX类型,NetPacket.Data是发序列化的对象,可以直接转型使用        /// </summary>        /// <returns></returns>        public override NetPacket ReceivePacket()        {            #region 【接收Tcp包】                        while (true)            {                //判断是否满足一个完整封包大小                if (IsFullNetPacket())//如果有完整封包就返回                {                    return PickNetPacket();                }                #region 【缓冲区不满足一个完整封包大小则继续从网络流读取数据】                int readLen = _netStream.Read(_tempBuffer, 0, BUFFER_SIZE);                //判断读取的字节数+缓冲区已有字节数是否超过缓冲区总大小                if (readLen + _netDataOffset > _netDataBuffer.Length)                {                    Array.Resize<Byte>(ref _netDataBuffer, _netDataBuffer.Length + BUFFER_SIZE * 2);                }                //将新读取的数据拷贝到缓冲区                Array.Copy(_tempBuffer, 0, _netDataBuffer, _netDataOffset, readLen);                //修改"网络数据实际长度"                _netDataOffset += readLen;                #endregion            }            #endregion        }        /// <summary>        /// 发包[发送Tcp包/发送Udp数据报]        /// </summary>        /// <param name="packet"></param>        protected override void SendPacket(NetPacket packet)        {            if (packet == null || packet.Data == null || packet.PacketHead == null)                return;            #region【计算包体长度】            switch (packet.PacketHead.PType)            {                case PacketType.STRING:                    packet.PacketHead.Len = Encoding.Default.GetBytes(Convert.ToString(packet.Data)).Length;                    break;                case PacketType.BINARY:                    packet.PacketHead.Len = ((Byte[])packet.Data).Length;                    break;                case PacketType.COMPLEX:                    packet.PacketHead.Len = GetCanSerializableObjectSize(packet.Data);                    break;                default:                    break;            }            #endregion            #region【写入包头】            _netStream.Write(BitConverter.GetBytes(packet.PacketHead.Version), 0,Marshal.SizeOf(packet.PacketHead.Version));            _netStream.Write(BitConverter.GetBytes((Int32)packet.PacketHead.PType), 0, sizeof(Int32));            _netStream.Write(BitConverter.GetBytes(packet.PacketHead.Len),0,Marshal.SizeOf(packet.PacketHead.Len));                        #endregion            #region【写入包体】            byte[] buffer = null;            switch (packet.PacketHead.PType)            {                case PacketType.STRING:                    buffer = Encoding.Default.GetBytes(Convert.ToString(packet.Data));                    break;                case PacketType.BINARY:                    buffer = (byte[])packet.Data;                    break;                case PacketType.COMPLEX:                    MemoryStream m = new MemoryStream();                    SerializeHelper<BinarySerializeHelper>().Serialize(m, packet.Data);                    m.Position = 0;                    buffer = new byte[m.Length];                    m.Read(buffer, 0, (Int32)m.Length);                    break;            }            if (buffer != null)                _netStream.Write(buffer,0,buffer.Length);            #endregion        }    }}

网络封包服务Udp子类:

示范代码using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Net.Sockets;using System.Net;using System.IO;using System.Runtime.InteropServices;namespace TcpLabCommon{    /// <summary>    /// 网络封包Udp服务类    /// </summary>    public class NetPacketUdpService:NetPacketService    {        /// <summary>        /// 构造方法        /// </summary>        /// <param name="udpSocket">Udp套接字</param>        /// <param name="udpRemoteEndPoint">Udp发送的目标端点</param>        public NetPacketUdpService(Socket udpSocket, EndPoint udpRemoteEndPoint) : base(udpSocket, udpRemoteEndPoint) { }        /// <summary>        /// 提取一个完整网络包然后返回        ///     如果收到的包是STRING类型,NetPacket.Data是发送的字符串        ///     如果收到的包是BINARY类型,NetPacket.Data是发送的文件名长度+文件名+文件内容,请调用ParseFile函数进行解析        ///     如果收到的包是COMPLEX类型,NetPacket.Data是发序列化的对象,可以直接转型使用        /// </summary>        /// <returns></returns>        public override NetPacket ReceivePacket()        {            #region【接收Udp包】                            if (IsFullNetPacket())                {                    return PickNetPacket();                }                //Internet上建议采用UDP_BUFFER_SIZE                //int readLen = this._udpSocket.ReceiveFrom(_netDataBuffer, UDP_BUFFER_SIZE, SocketFlags.None, ref _udpRemoteEndPoint);                //局域网采用BUFFER_SIZE,注意如果传输的是文件,文件大于BUFFER_SIZE*32将无法传输                int readLen = this._udpSocket.ReceiveFrom(_netDataBuffer, BUFFER_SIZE * 32, SocketFlags.None, ref _udpRemoteEndPoint);                _netDataOffset += readLen;                if (IsFullNetPacket())                {                    return PickNetPacket();                }                return null;                        #endregion        }        /// <summary>        /// 发包[发送Udp数据报]        /// </summary>        /// <param name="packet"></param>        protected override void SendPacket(NetPacket packet)        {            if (packet == null || packet.Data == null || packet.PacketHead == null)                return;            MemoryStream mStream = new MemoryStream();//用内存流临时保存要发送的数据            #region【计算包体长度】            if (packet.PacketHead.Len == 0)            {                switch (packet.PacketHead.PType)                {                    case PacketType.STRING:                        packet.PacketHead.Len = Convert.ToString(packet.Data).Length;                        break;                    case PacketType.BINARY:                        packet.PacketHead.Len = ((Byte[])packet.Data).Length;                        break;                    case PacketType.COMPLEX:                        packet.PacketHead.Len = GetCanSerializableObjectSize(packet.Data);                        break;                    default:                        break;                }            }            #endregion            #region【写入包头】            mStream.Write(BitConverter.GetBytes(packet.PacketHead.Version), 0, Marshal.SizeOf(packet.PacketHead.Version));            mStream.Write(BitConverter.GetBytes((Int32)packet.PacketHead.PType), 0, sizeof(Int32));            mStream.Write(BitConverter.GetBytes(packet.PacketHead.Len), 0, Marshal.SizeOf(packet.PacketHead.Len));            #endregion            #region【写入包体】            byte[] buffer = null;            switch (packet.PacketHead.PType)            {                case PacketType.STRING:                    buffer = Encoding.UTF8.GetBytes(packet.Data.ToString());                    break;                case PacketType.BINARY:                    buffer = (byte[])packet.Data;                    break;                case PacketType.COMPLEX:                    MemoryStream m = new MemoryStream();                    SerializeHelper<BinarySerializeHelper>().Serialize(m, packet.Data);                    m.Position = 0;                    buffer = new byte[m.Length];                    m.Read(buffer, 0, (Int32)m.Length);                    break;            }            if (buffer != null)                //_netStream.Write(buffer,0,buffer.Length);                mStream.Write(buffer, 0, buffer.Length);            #endregion            #region【将内存流一次写入网络流】            mStream.Seek(0, SeekOrigin.Begin);                       this._udpSocket.SendTo(mStream.GetBuffer(), this._udpRemoteEndPoint);            mStream.Close();            #endregion        }    }}

 

网络封包服务Tcp异步子类,实现异步发包和收包:

示范代码using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Net.Sockets;using System.Runtime.InteropServices;using System.IO;namespace TcpLabCommon{    public delegate void TcpAsynHandler1(NetPacket packet);    public delegate void TcpAsynHandler2(NetPacketHead packetHead);    /// <summary>    /// 网络封包Tcp异步服务类    /// </summary>    public class NetPacketTcpAsynService : NetPacketService    {        /// <summary>        /// 发包前事件        /// </summary>        public event TcpAsynHandler1 OnBeforeSendPacket;        /// <summary>        /// 发包后事件        /// </summary>        public event TcpAsynHandler2 OnAfterSendPacket;        /// <summary>        /// 收到网络封包后的事件        /// </summary>        public event TcpAsynHandler1 OnReceivedPacket;        public NetPacketTcpAsynService(NetworkStream netStream) : base(netStream) { }        /// <summary>        /// 该方法的返回值总是为空.提取一个完整网络包然后返回,返回的封包需要通过注册OnReceivedPacket事件的函数获取        ///     如果收到的包是STRING类型,NetPacket.Data是发送的字符串        ///     如果收到的包是BINARY类型,NetPacket.Data是发送的文件名长度+文件名+文件内容,请调用ParseFile函数进行解析        ///     如果收到的包是COMPLEX类型,NetPacket.Data是发序列化的对象,可以直接转型使用        /// </summary>        /// <returns></returns>        public override NetPacket ReceivePacket()        {                                   //判断是否满足一个完整封包大小            if (IsFullNetPacket())//如果有完整封包就返回            {                NetPacket packet = PickNetPacket();                if (OnReceivedPacket != null) //判断事件是否注册,如果注册调用回调函数传递收到的封包                {                    OnReceivedPacket(packet);                                    }                //return null;//提取到一个封包后应该及时返回            }            //【缓冲区不满足一个完整封包大小则继续从网络流读取数据,异步读取】            _netStream.BeginRead(_tempBuffer, 0, BUFFER_SIZE, new AsyncCallback(AsyncCallbackReadFromNetStream),_netStream);                       return null;        }        /// <summary>        /// 从网络流异步读取数据回调函数        /// </summary>        /// <param name="result"></param>        private void AsyncCallbackReadFromNetStream(IAsyncResult result)        {            NetworkStream netStream = (NetworkStream)result.AsyncState;            int readLen = netStream.EndRead(result);            //判断读取的字节数+缓冲区已有字节数是否超过缓冲区总大小            if (readLen + _netDataOffset > _netDataBuffer.Length)            {                if (IsFullNetPacketHead())//如果缓冲区数据满足一个包头数据大小,则可以计算出本次接收的包需要的缓冲区大小,从而实现一次调整大小                {                    Array.Resize<Byte>(ref _netDataBuffer, FullNetPacketSize);                }                else //不满足一个完整的网络封包的大小                {                    Array.Resize<Byte>(ref _netDataBuffer, _netDataBuffer.Length + BUFFER_SIZE * 2);                }                            }            //将新读取的数据拷贝到缓冲区            Array.Copy(_tempBuffer, 0, _netDataBuffer, _netDataOffset, readLen);            //修改"网络数据实际长度"            _netDataOffset += readLen;            ReceivePacket();        }        /// <summary>        /// 发包[发送Tcp包/发送Udp数据报]        /// </summary>        /// <param name="packet"></param>        protected override void SendPacket(NetPacket packet)        {            if (packet == null || packet.Data == null || packet.PacketHead == null)                return;            if (OnBeforeSendPacket != null)                OnBeforeSendPacket(packet);            MemoryStream mStream = new MemoryStream();            #region【计算包体长度】            if (packet.PacketHead.Len == 0)            {                switch (packet.PacketHead.PType)                {                    case PacketType.STRING:                        packet.PacketHead.Len = Encoding.Default.GetBytes(Convert.ToString(packet.Data)).Length;                        break;                    case PacketType.BINARY:                        packet.PacketHead.Len = ((Byte[])packet.Data).Length;                        break;                    case PacketType.COMPLEX:                        packet.PacketHead.Len = GetCanSerializableObjectSize(packet.Data);                        break;                    default:                        break;                }            }            #endregion            #region【写入包头】                           mStream.Write(BitConverter.GetBytes(packet.PacketHead.Version), 0, Marshal.SizeOf(packet.PacketHead.Version));            mStream.Write(BitConverter.GetBytes((Int32)packet.PacketHead.PType), 0, sizeof(Int32));            mStream.Write(BitConverter.GetBytes(packet.PacketHead.Len), 0, Marshal.SizeOf(packet.PacketHead.Len));                        #endregion            #region【写入包体】            byte[] buffer = null;            switch (packet.PacketHead.PType)            {                case PacketType.STRING:                    buffer = Encoding.Default.GetBytes(Convert.ToString(packet.Data));                    break;                case PacketType.BINARY:                    buffer = (byte[])packet.Data;                    break;                case PacketType.COMPLEX:                    MemoryStream m = new MemoryStream();                    SerializeHelper<BinarySerializeHelper>().Serialize(m, packet.Data);                    m.Position = 0;                    buffer = new byte[m.Length];                    m.Read(buffer, 0, (Int32)m.Length);                    break;            }            if (buffer != null)                mStream.Write(buffer, 0, buffer.Length);            #endregion            #region【将内存流一次写入网络流,异步写入】            mStream.Seek(0, SeekOrigin.Begin);            _netStream.BeginWrite(mStream.GetBuffer(), 0, (Int32)mStream.Length, new AsyncCallback(AsyncCallbackWriteToNetStream), new WriteNetStreamASyncCallbackParam{ netStream = _netStream, packetHead = packet.PacketHead });            #endregion        }        /// <summary>        /// 写入网络流异步回调函数        /// </summary>        /// <param name="result"></param>        private void AsyncCallbackWriteToNetStream(IAsyncResult result)        {            WriteNetStreamASyncCallbackParam p = (WriteNetStreamASyncCallbackParam)result.AsyncState;            p.netStream.EndWrite(result);            if (OnAfterSendPacket != null)//事件处理,如果注册了该事件,则执行回调函数                OnAfterSendPacket(p.packetHead);        }    }    /// <summary>    /// 写入网络流异步回调参数    /// </summary>    sealed class  WriteNetStreamASyncCallbackParam    {        /// <summary>        /// 网络流        /// </summary>        internal NetworkStream netStream;        /// <summary>        /// 包头        /// </summary>        internal NetPacketHead packetHead;    }}

 

时间关系我没有编写Udp异步传输子类

原创粉丝点击