NetworkComms c#通信框架之使用BinaryFormatter序列化器进行网络通信

NetworkComms是一款来自英国的C#语言编写的通信框架

NetworkComms通信框架默认使用的是protobuf.net序列化器

NetworkComms 通信框架还内置了 BinaryFormaterSerializer 序列化器,此序列化器是对.net框架自带的  BinaryFormatter 的封装使用。

在networkcomms v2版本中的BinaryFormaterSerializer代码如下:

#if WINDOWS_PHONE
#else

using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

#if ANDROID
using PreserveAttribute = Android.Runtime.PreserveAttribute;
#elif iOS
using PreserveAttribute = MonoTouch.Foundation.PreserveAttribute;
#endif

namespace DPSBase
{
    /// <summary>
    /// DataSerializer that uses .Net <see cref="System.Runtime.Serialization.Formatters.Binary.BinaryFormatter"/> to perform <see cref="object"/> serialization
    /// </summary>
    [DataSerializerProcessor(2)]
    public class BinaryFormaterSerializer : DataSerializer
    {
        static DataSerializer instance;

        /// <summary>
        /// Instance singleton used to access serializer instance.  Use instead <see cref="DPSManager.GetDataSerializer{T}"/>
        /// </summary>
        [Obsolete("Instance access via class obsolete, use DPSManager.GetSerializer<T>")]
        public static DataSerializer Instance
        {
            get
            {
                if (instance == null)
                    instance = GetInstance<BinaryFormaterSerializer>();

                return instance;
            }
        }

#if ANDROID || iOS
        [Preserve]
#endif
        private BinaryFormaterSerializer() { }

        #region ISerialize Members

        /// <inheritdoc />
        protected override void SerialiseDataObjectInt(Stream ouputStream, object objectToSerialise, Dictionary<string, string> options)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(ouputStream, objectToSerialise);
            ouputStream.Seek(0, 0);
        }

        /// <inheritdoc />
        protected override object DeserialiseDataObjectInt(Stream inputStream, Type resultType, Dictionary<string, string> options)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            return formatter.Deserialize(inputStream);
        }

        #endregion
    }
}

#endif

BinaryFormaterSerializer

在networkcomms v2版本中,通信框架内部使用了protobuf.net序列化器,比如ConnectionInfo类

  [ProtoContract]
    public class ConnectionInfo : IEquatable<ConnectionInfo>
    {
        /// <summary>
        /// The type of this connection
        /// </summary>
        [ProtoMember(1)]
        public ConnectionType ConnectionType { get; internal set; }

        /// <summary>
        /// We store our unique peer identifier as a string so that it can be easily serialised.
        /// </summary>
        [ProtoMember(2)]
        string NetworkIdentifierStr;

        [ProtoMember(3)]
        string localEndPointIPStr; //Only set on serialise
        [ProtoMember(4)]
        int localEndPointPort; //Only set on serialise

        bool hashCodeCacheSet = false;
        int hashCodeCache;

        /// <summary>
        /// True if the <see cref="RemoteEndPoint"/> is connectable.
        /// </summary>
        [ProtoMember(5)]
        public bool IsConnectable { get; private set; }

        /// <summary>
        /// The DateTime corresponding to the creation time of this connection object
        /// </summary>
        public DateTime ConnectionCreationTime { get; protected set; }

        /// <summary>
        /// True if connection was originally established by remote
        /// </summary>
        public bool ServerSide { get; internal set; }

        /// <summary>
        /// The DateTime corresponding to the creation time of this connection object
        /// </summary>
        public DateTime ConnectionEstablishedTime { get; private set; }

        /// <summary>
        /// The <see cref="IPEndPoint"/> corresponding to the local end of this connection.
        /// </summary>
        public IPEndPoint LocalEndPoint { get; private set; }

        /// <summary>
        /// The <see cref="IPEndPoint"/> corresponding to the remote end of this connection.
        /// </summary>
        public IPEndPoint RemoteEndPoint { get; private set; }

        /// <summary>
        /// Describes the current state of the connection
        /// </summary>
        public ConnectionState ConnectionState { get; private set; }

        /// <summary>
        /// Returns the networkIdentifier of this peer as a ShortGuid. If the NetworkIdentifier has not yet been set returns ShortGuid.Empty.
        /// </summary>
        public ShortGuid NetworkIdentifier
        {
            get
            {
                if (NetworkIdentifierStr == null || NetworkIdentifierStr == "") return ShortGuid.Empty;
                else return new ShortGuid(NetworkIdentifierStr);
            }
        }

        DateTime lastTrafficTime;
        object internalLocker = new object();

        /// <summary>
        /// The DateTime corresponding to the time data was sent or received
        /// </summary>
        public DateTime LastTrafficTime
        {
            get
            {
                lock (internalLocker)
                    return lastTrafficTime;
            }
            protected set
            {
                lock (internalLocker)
                    lastTrafficTime = value;
            }
        }

        /// <summary>
        /// Private constructor required for deserialisation.
        /// </summary>
#if iOS || ANDROID
        public ConnectionInfo() { }
#else
        private ConnectionInfo() { }
#endif

        /// <summary>
        /// Create a new ConnectionInfo object pointing at the provided remote <see cref="IPEndPoint"/>
        /// </summary>
        /// <param name="remoteEndPoint">The end point corresponding with the remote target</param>
        public ConnectionInfo(IPEndPoint remoteEndPoint)
        {
            this.RemoteEndPoint = remoteEndPoint;
            this.ConnectionCreationTime = DateTime.Now;
        }

        /// <summary>
        /// Create a new ConnectionInfo object pointing at the provided remote ipAddress and port. Provided ipAddress and port are parsed in to <see cref="RemoteEndPoint"/>.
        /// </summary>
        /// <param name="remoteIPAddress">IP address of the remote target in string format, e.g. "192.168.0.1"</param>
        /// <param name="remotePort">The available port of the remote target.
        /// Valid ports are 1 through 65535. Port numbers less than 256 are reserved for well-known services (like HTTP on port 80) and port numbers less than 1024 generally require admin access</param>
        public ConnectionInfo(string remoteIPAddress, int remotePort)
        {
            IPAddress ipAddress;
            if (!IPAddress.TryParse(remoteIPAddress, out ipAddress))
                throw new ArgumentException("Provided remoteIPAddress string was not succesfully parsed.", "remoteIPAddress");

            this.RemoteEndPoint = new IPEndPoint(ipAddress, remotePort);
            this.ConnectionCreationTime = DateTime.Now;
        }

        /// <summary>
        /// Create a connectionInfo object which can be used to inform a remote peer of local connectivity
        /// </summary>
        /// <param name="connectionType">The type of connection</param>
        /// <param name="localNetworkIdentifier">The local network identifier</param>
        /// <param name="localEndPoint">The localEndPoint which should be referenced remotely</param>
        /// <param name="isConnectable">True if connectable on provided localEndPoint</param>
        public ConnectionInfo(ConnectionType connectionType, ShortGuid localNetworkIdentifier, IPEndPoint localEndPoint, bool isConnectable)
        {
            this.ConnectionType = connectionType;
            this.NetworkIdentifierStr = localNetworkIdentifier.ToString();
            this.LocalEndPoint = localEndPoint;
            this.IsConnectable = isConnectable;
        }

        /// <summary>
        /// Create a connectionInfo object for a new connection.
        /// </summary>
        /// <param name="serverSide">True if this connection is being created serverSide</param>
        /// <param name="connectionType">The type of connection</param>
        /// <param name="remoteEndPoint">The remoteEndPoint of this connection</param>
        internal ConnectionInfo(bool serverSide, ConnectionType connectionType, IPEndPoint remoteEndPoint)
        {
            this.ServerSide = serverSide;
            this.ConnectionType = connectionType;
            this.RemoteEndPoint = remoteEndPoint;
            this.ConnectionCreationTime = DateTime.Now;
        }

        /// <summary>
        /// Create a connectionInfo object for a new connection.
        /// </summary>
        /// <param name="serverSide">True if this connection is being created serverSide</param>
        /// <param name="connectionType">The type of connection</param>
        /// <param name="remoteEndPoint">The remoteEndPoint of this connection</param>
        /// <param name="localEndPoint">The localEndpoint of this connection</param>
        internal ConnectionInfo(bool serverSide, ConnectionType connectionType, IPEndPoint remoteEndPoint, IPEndPoint localEndPoint)
        {
            this.ServerSide = serverSide;
            this.ConnectionType = connectionType;
            this.RemoteEndPoint = remoteEndPoint;
            this.LocalEndPoint = localEndPoint;
            this.ConnectionCreationTime = DateTime.Now;
        }

        [ProtoBeforeSerialization]
        private void OnSerialise()
        {
            lock (internalLocker)
            {
                localEndPointIPStr = LocalEndPoint.Address.ToString();
                localEndPointPort = LocalEndPoint.Port;
            }
        }

        [ProtoAfterDeserialization]
        private void OnDeserialise()
        {
            IPAddress ipAddress;
            if (!IPAddress.TryParse(localEndPointIPStr, out ipAddress))
                throw new ArgumentException("Failed to parse IPAddress from localEndPointIPStr", "localEndPointIPStr");

            LocalEndPoint = new IPEndPoint(ipAddress, localEndPointPort);
        }

        /// <summary>
        /// Marks the connection as establishing
        /// </summary>
        internal void NoteStartConnectionEstablish()
        {
            lock(internalLocker)
            {
                if (ConnectionState == ConnectionState.Shutdown) throw new ConnectionSetupException("Unable to mark as establishing as connection has already shutdown.");

                if (ConnectionState == ConnectionState.Establishing) throw new ConnectionSetupException("Connection already marked as establishing");
                else ConnectionState = ConnectionState.Establishing;
            }
        }

        /// <summary>
        /// Set this connectionInfo as established.
        /// </summary>
        internal void NoteCompleteConnectionEstablish()
        {
            lock (internalLocker)
            {
                if (ConnectionState == ConnectionState.Shutdown) throw new ConnectionSetupException("Unable to mark as established as connection has already shutdown.");

                if (!(ConnectionState == ConnectionState.Establishing)) throw new ConnectionSetupException("Connection should be marked as establishing before calling CompleteConnectionEstablish");

                if (ConnectionState == ConnectionState.Established) throw new ConnectionSetupException("Connection already marked as establised.");

                ConnectionState = ConnectionState.Established;
                ConnectionEstablishedTime = DateTime.Now;

                if (NetworkIdentifier == ShortGuid.Empty) throw new ConnectionSetupException("Unable to set connection established until networkIdentifier has been set.");
            }
        }

        #region Modify 修改

        private bool reconnectFlag = false;
        /// <summary>
        /// 是否为重连接模式
        /// </summary>
        public bool ReconnectFlag
        {
            get { return reconnectFlag; }
            set { reconnectFlag = value; }
        }

        /// <summary>
        /// Note this connection as shutdown
        /// </summary>
        internal void NoteConnectionShutdown()
        {
            lock (internalLocker)
                ConnectionState = ConnectionState.Shutdown;
            //添加以下代码  连接信息类的初始状态为非重连模式  触发连接状态改变事件
            if (reconnectFlag == false)
            {
                StateChanged.Raise(this, new StringEventArgs("连接出现异常"));
            }

        }

        //添加状态改变事件 

        public event EventHandler<StringEventArgs> StateChanged;

        #endregion 

        /// <summary>
        /// Update the localEndPoint information for this connection
        /// </summary>
        /// <param name="localEndPoint"></param>
        internal void UpdateLocalEndPointInfo(IPEndPoint localEndPoint)
        {
            lock (internalLocker)
            {
                hashCodeCacheSet = false;
                this.LocalEndPoint = localEndPoint;
            }
        }

        /// <summary>
        /// During a connection handShake we might be provided with more update information regarding endPoints, connectability and identifiers
        /// </summary>
        /// <param name="handshakeInfo"><see cref="ConnectionInfo"/> provided by remoteEndPoint during connection handshake.</param>
        /// <param name="remoteEndPoint">The correct remoteEndPoint of this connection.</param>
        internal void UpdateInfoAfterRemoteHandshake(ConnectionInfo handshakeInfo, IPEndPoint remoteEndPoint)
        {
            lock (internalLocker)
            {
                NetworkIdentifierStr = handshakeInfo.NetworkIdentifier.ToString();
                RemoteEndPoint = remoteEndPoint;
                LocalEndPoint.Address = handshakeInfo.LocalEndPoint.Address;
                IsConnectable = handshakeInfo.IsConnectable;
            }
        }

        /// <summary>
        /// Updates the last traffic time for this connection
        /// </summary>
        internal void UpdateLastTrafficTime()
        {
            lock (internalLocker)
                lastTrafficTime = DateTime.Now;
        }

        /// <summary>
        /// Replaces the current networkIdentifier with that provided
        /// </summary>
        /// <param name="networkIdentifier">The new networkIdentifier for this connectionInfo</param>
        public void ResetNetworkIdentifer(ShortGuid networkIdentifier)
        {
            NetworkIdentifierStr = networkIdentifier.ToString();
        }

        /// <summary>
        /// A connectionInfo object may be used across multiple connection sessions, i.e. due to a possible timeout.
        /// This method resets the state of the connectionInfo object so that it may be reused.
        /// </summary>
        internal void ResetConnectionInfo()
        {
            lock (internalLocker)
            {
                ConnectionState = ConnectionState.Undefined;
            }
        }

        /// <summary>
        /// Compares this <see cref="ConnectionInfo"/> object with obj and returns true if obj is ConnectionInfo and both the <see cref="NetworkIdentifier"/> and <see cref="RemoteEndPoint"/> match.
        /// </summary>
        /// <param name="obj">The object to test of equality</param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            lock (internalLocker)
            {
                var other = obj as ConnectionInfo;
                if (((object)other) == null)
                    return false;
                else
                    return this == other;
            }
        }

        /// <summary>
        /// Compares this <see cref="ConnectionInfo"/> object with other and returns true if both the <see cref="NetworkIdentifier"/> and <see cref="RemoteEndPoint"/> match.
        /// </summary>
        /// <param name="other"></param>
        /// <returns></returns>
        public bool Equals(ConnectionInfo other)
        {
            lock (internalLocker)
                return this == other;
        }

        /// <summary>
        /// Returns left.Equals(right)
        /// </summary>
        /// <param name="left">Left connectionInfo</param>
        /// <param name="right">Right connectionInfo</param>
        /// <returns>True if both are equal, otherwise false</returns>
        public static bool operator ==(ConnectionInfo left, ConnectionInfo right)
        {
            if (((object)left) == ((object)right)) return true;
            else if (((object)left) == null || ((object)right) == null) return false;
            else
            {
                if (left.RemoteEndPoint != null && right.RemoteEndPoint != null)
                    return (left.NetworkIdentifier.ToString() == right.NetworkIdentifier.ToString() && left.RemoteEndPoint.Equals(right.RemoteEndPoint));
                else
                    return (left.NetworkIdentifier.ToString() == right.NetworkIdentifier.ToString());
            }
        }

        /// <summary>
        /// Returns !left.Equals(right)
        /// </summary>
        /// <param name="left">Left connectionInfo</param>
        /// <param name="right">Right connectionInfo</param>
        /// <returns>True if both are different, otherwise false</returns>
        public static bool operator !=(ConnectionInfo left, ConnectionInfo right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Returns NetworkIdentifier.GetHashCode() ^ RemoteEndPoint.GetHashCode();
        /// </summary>
        /// <returns>The hashcode for this connection info</returns>
        public override int GetHashCode()
        {
            lock (internalLocker)
            {
                if (!hashCodeCacheSet)
                {
                    if (RemoteEndPoint != null)
                        hashCodeCache = NetworkIdentifier.GetHashCode() ^ RemoteEndPoint.GetHashCode();
                    else
                        hashCodeCache = NetworkIdentifier.GetHashCode();

                    hashCodeCacheSet = true;
                }

                return hashCodeCache;
            }
        }

        /// <summary>
        /// Returns a string containing suitable information about this connection
        /// </summary>
        /// <returns>A string containing suitable information about this connection</returns>
        public override string ToString()
        {
            string returnString = "[" + ConnectionType.ToString() + "] ";

            if (ConnectionState == ConnectionState.Established)
                returnString += LocalEndPoint.Address + ":" + LocalEndPoint.Port.ToString() + " -> " + RemoteEndPoint.Address + ":" + RemoteEndPoint.Port.ToString() + " (" + NetworkIdentifier + ")";
            else
            {
                if (RemoteEndPoint != null && LocalEndPoint != null)
                    returnString += LocalEndPoint.Address + ":" + LocalEndPoint.Port.ToString() + " -> " + RemoteEndPoint.Address + ":" + RemoteEndPoint.Port.ToString();
                else if (RemoteEndPoint != null)
                    returnString += "Local -> " + RemoteEndPoint.Address + ":" + RemoteEndPoint.Port.ToString();
                else if (LocalEndPoint != null)
                    returnString += LocalEndPoint.Address + ":" + LocalEndPoint.Port.ToString() + " " + (IsConnectable ? "Connectable" : "NotConnectable");
            }

            return returnString.Trim();
        }

    }

ConnectionInfo类

在NetworkComms V3版本中,为了更好的兼容性和对解除对protobuf.net序列化器的依赖,已经在框架内部使用了显式的序列化,而不再使用Protobuf.net序列化器。

显式的序列化方式大致如下:

                byte[] conTypeData = BitConverter.GetBytes((int)ConnectionType);

                data.Add(conTypeData);

                byte[] netIDData = Encoding.UTF8.GetBytes(NetworkIdentifierStr);
                byte[] netIDLengthData = BitConverter.GetBytes(netIDData.Length);

                data.Add(netIDLengthData);
                data.Add(netIDData);

言归正传,看一下如何使用BinaryFormatter序列化器

第一步,定义3个工程文件

上图中的User类是契约类,用于传递数据

using System;
using System.Collections.Generic;
using System.Text;

namespace Contract
{
    [Serializable]
    public class User
    {
        public string UserName { get; set; }

        public int Age { get; set; }
    }
}

第二步:服务器端

开始监听:

     IPEndPoint thePoint = new IPEndPoint(IPAddress.Parse(txtIP.Text), int.Parse(txtPort.Text));
            TCPConnection.StartListening(thePoint, false);
            button1.Text = "监听中";
            button1.Enabled = false;

定义序列化器:

 SendReceiveOptions sro = new SendReceiveOptions(DPSManager.GetDataSerializer<BinaryFormaterSerializer>(), null, null);

注册消息类型的处理方法

 NetworkComms.AppendGlobalIncomingPacketHandler<User>("ReqMessage", IncomingLoginRequest, sro);

具体的处理方法:

  private void IncomingLoginRequest(PacketHeader header, Connection connection, User user)
        {
            string resMessage = user.UserName + "欢迎您";

            connection.SendObject("ResMessage", resMessage);
        }

第三步:客户端

连接服务器:

 //给连接信息对象赋值
            connInfo = new ConnectionInfo(txtIP.Text, int.Parse(txtPort.Text));

            //如果不成功,会弹出异常信息
            newTcpConnection = TCPConnection.GetConnection(connInfo);

            button1.Enabled = false;
            button1.Text = "连接成功";

序列化器

 SendReceiveOptions sro = new SendReceiveOptions(DPSManager.GetDataSerializer<BinaryFormaterSerializer>(), null, null);

提交数据并获取返回信息

 User theUser = new User();
            theUser.UserName = "名山大川网络工作室";
            string resMsg = newTcpConnection.SendReceiveObject<string>("ReqMessage", "ResMessage", 5000, theUser, sro, sro);

            MessageBox.Show("返回的信息是:" + resMsg);

本Demo源码 (含通信框架networkcomms2.31源码)

www.networkcomms.cn编辑

时间: 2024-10-11 04:31:13

NetworkComms c#通信框架之使用BinaryFormatter序列化器进行网络通信的相关文章

NetworkComms c#通信框架与Java的Netty框架通信 解决粘包问题

上次写了一篇文章  基于networkcomms V3通信框架的c#服务器与java客户端进行通信之Protobuf探讨 其中没有解决粘包问题,抛砖引玉,文章得到了失足程序员 老师的点评,并给出了解决方案:[最多评论]java netty socket库和自定义C#socket库利用protobuf进行通信完整实例(10/591) » 于是马上开始学习,并把c#服务器端换成了我比较熟悉的networkcomms v3 c#通信框架(商业版,本文并不提供) ,以方便与已经存在的系统进行整合. 客户

基于networkcomms V3通信框架的c#服务器与java客户端进行通信之Protobuf探讨

在上一篇 基于networkcomms V3通信框架的c#服务器与java客户端进行通信探讨  中探讨了在C#端与Java端通信中传递字符,有朋友提到如果传递int类型的整数,会出现编码的问题. 到网上找到了一篇文章< 使用protobuf进行C#与Java通信 >进行学习 ,使用protobuf进行编码,传递数据,好像这样可以避免编码的问题. (虽然编码问题解决了,但是粘包问题并没有解决,有经验的朋友介绍下怎样解决粘包的问题) 服务器端基于networkcomms V3 C#通信框架. ne

NetworkComms V3 使用Json序列化器进行网络通信

刚才在网上闲逛,偶然看到一篇文章 C#(服务器)与Java(客户端)通过Socket传递对象 网址是:http://www.cnblogs.com/iyangyuan/archive/2012/12/23/2829712.html#3140522 其中提到了 C#与java如通过传递对象通信的问题 摘引如下: 通信关键: C#和java用Socket通信,发送数据和接收数据可以统一采用UTF-8编码,经过测试,使用UTF-8编码可以成功传递对象. 对于Socket技术,发送数据之前,一般将字符串

基于networkcomms V3通信框架的c#服务器与java客户端进行通信探讨

首先说一下networkcomms 通信框架是商业版的,本文也并不提供. 对java不太熟悉,刚刚安装了eclipse,通信只实现了把字符从java客户端传到c#服务器,服务器收到消息后,返回一个字符消息给java客户端,java客户端显示收到的消息. 服务器端基于networkcomms V3 C#通信框架. 服务器端代码: using System; using System.Collections.Generic; using System.ComponentModel; using Sy

介绍开源的.net通信框架NetworkComms框架之二 传递类

原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 目前作者已经开源  开源地址是:https://github.com/MarcFletcher/NetworkComms.Net 使用networkcomms框架通信时,客户端发送消息,服务器端回复消息. 在介绍开源的.net通信框架NetworkComms 一文中,我们介绍了如何从客户端发送字符串给服务器端,以及如何从服务

介绍开源的.net通信框架NetworkComms框架之四 消息边界

原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 目前作者已经开源  许可是:Apache License v2 开源地址是:https://github.com/MarcFletcher/NetworkComms.Net 首先,使用TCP通信的时候存在消息边界的问题,也就是如何处理粘包问题,networkcomms 框架本身已经对这个问题有内置的解决方案,我们在使用框架时

介绍开源的.net通信框架NetworkComms框架 源码分析(二)ConnectionInfo

原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 目前作者已经开源  许可是:Apache License v2 开源地址是:https://github.com/MarcFletcher/NetworkComms.Net ConnectionInfo 是连接的信息类 用来存放连接类型(TCP,UDP),连接ID,创建连接时间,是否服务器端,本地地址,远端地址,最近通信时间

介绍开源的.net通信框架NetworkComms框架 源码分析(二十 )ConnectionCreate

原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 目前作者已经开源  许可是:Apache License v2 开源地址是:https://github.com/MarcFletcher/NetworkComms.Net public abstract partial class Connection { /// <summary> /// Connection inf

介绍开源的.net通信框架NetworkComms框架 源码分析(二十一 )TCPConnectionListener

原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 目前作者已经开源  许可是:Apache License v2 开源地址是:https://github.com/MarcFletcher/NetworkComms.Net TCP连接监听器 /// <summary> /// A TCP connection listener /// TCP连接监听器 /// </