UDP打洞原理及代碼


來源:http://www.fenbi360.net/Content.aspx?id=1021&t=jc

 

UDP"打洞"原理

1.       NAT分類

根據Stun協議(RFC3489),NAT大致分為下面四類

1)      Full Cone

這種NAT內部的機器A連接過外網機器C后,NAT會打開一個端口.然后外網的任何發到這個打開的端口的UDP數據報都可以到達A.不管是不是C發過來的.

例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
任何發送到 NAT(202.100.100.100:8000)的數據都可以到達A(192.168.8.100:5000)

2)      Restricted Cone

這種NAT內部的機器A連接過外網的機器C后,NAT打開一個端口.然后C可以用任何端口和A通信.其他的外網機器不行.

例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
任何從C發送到 NAT(202.100.100.100:8000)的數據都可以到達A(192.168.8.100:5000)

3)      Port Restricted Cone

這種NAT內部的機器A連接過外網的機器C后,NAT打開一個端口.然后C可以用原來的端口和A通信.其他的外網機器不行.

例如 A:192.168.8.100 NAT:202.100.100.100 C:292.88.88.88
A(192.168.8.100:5000) -> NAT(202.100.100.100 : 8000) -> C(292.88.88.88:2000)
C(202.88.88.88:2000)發送到 NAT(202.100.100.100:8000)的數據都可以到達A(192.168.8.100:5000)

以上三種NAT通稱Cone NAT.我們只能用這種NAT進行UDP打洞.

4)      Symmetic

對於這種NAT.連接不同的外部目標.原來NAT打開的端口會變化.而Cone NAT不會.雖然可以用端口猜測.但是成功的概率很小.因此放棄這種NAT的UDP打洞.

2.       UDP hole punching

對於Cone NAT.要采用UDP打洞.需要一個公網機器C來充當”介紹人”.內網的A,B先分別和C通信.打開各自的NAT端口.C這個時候知道A,B的公網IP: Port. 現在A和B想直接連接.比如A給B發.除非B是Full Cone.否則不能通信.反之亦然.但是我們可以這樣.

A要連接B.A給B發一個UDP包.同時.A讓那個介紹人給B發一個命令,讓B同時給A發一個UDP包.這樣雙方的NAT都會記錄對方的IP,然后就會允許互相通信.

3.       同一個NAT后面的情況

如果A,B在同一個NAT后面.如果用上面的技術來進行互連.那么如果NAT支持loopback(就是本地到本地的轉換),A,B可以連接,但是比較浪費帶寬和NAT.有一種辦法是,A,B和介紹人通信的時候,同時把自己的local IP也告訴服務器.A,B通信的時候,同時發local ip和公網IP.誰先到就用哪個IP.但是local ip就有可能不知道發到什么地方去了.比如A,B在不同的NAT后面但是他們各自的local ip段一樣.A給B的local IP發的UDP就可能發給自己內部網里面的某某某了.

還有一個辦法是服務器來判斷A,B是否在一個NAT后面.(網絡拓朴不同會不會有問題?)

 

WellKnown.cs

//WellKnown公用庫
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Net;
using System.Net.Sockets;
using System.Collections;

namespace P2PWellKnown
{
    /// <summary>
    /// UDP用戶登錄事件委托
    /// </summary>
    /// <param name="sender">事件源對象</param>
    /// <param name="e">事件實體</param>
    public delegate void UdpUserLogInDelegate(object sender, UDPSockEventArgs e);

    /// <summary>
    /// 一般UDP消息事件委托
    /// </summary>
    /// <param name="sender">事件源對象</param>
    /// <param name="e">事件實體</param>
    public delegate void UdpMessageDelegate(object sender, UDPSockEventArgs e);

    /// <summary>
    /// 初始化一個新連接的事件委托
    /// </summary>
    /// <param name="sender">事件源對象</param>
    /// <param name="e">事件實體</param>
    public delegate void UdpNewConnectDelegate(object sender, UDPSockEventArgs e);

    /// <summary>
    /// P2P共享數據類
    /// </summary>
    public class P2PConsts
    {
        /// <summary>
        /// UDP服務器監聽端口
        /// </summary>
        public const int UDP_SRV_PORT = 2280;

        /// <summary>
        ///TCP服務器監聽端口
        /// </summary>
        public const int TCP_SRV_PORT = 2000;
    }



    /// <summary>
    /// FormatterHelper 序列化,反序列化消息的幫助類
    /// </summary>
    public class FormatterHelper
    {

        public static byte[] Serialize(object obj)
        {
            BinaryFormatter binaryF = new BinaryFormatter();
            MemoryStream ms = new MemoryStream(1024 * 10);
            binaryF.Serialize(ms, obj);
            ms.Seek(0, SeekOrigin.Begin);
            byte[] buffer = new byte[(int)ms.Length];
            ms.Read(buffer, 0, buffer.Length);
            ms.Close();
            return buffer;
        }



        public static object Deserialize(byte[] buffer)
        {
            BinaryFormatter binaryF = new BinaryFormatter();
            MemoryStream ms = new MemoryStream(buffer, 0, buffer.Length, false);
            object obj = binaryF.Deserialize(ms);
            ms.Close();
            return obj;
        }

    }



    /// <summary>
    /// 用於承載UDPSock信息的事件類
    /// </summary>
    public class UDPSockEventArgs : EventArgs
    {
        /// <summary>
        /// 要承載的消息
        /// </summary>
        private string m_strMsg;

        /// <summary>
        /// 用戶信息
        /// </summary>
        private string m_strUserName;


        /// <summary>
        /// 觸發該事件的公共終端
        /// </summary>
        private IPEndPoint m_EndPoint;


        /// <summary>
        /// 初始化UDPSock事件
        /// </summary>
        /// <param name="sMsg">用戶發送的信息</param>
        public UDPSockEventArgs(string sMsg)
            : base()
        {
            this.m_strMsg = sMsg;
        }

        /// <summary>
        /// 遠端用戶名
        /// </summary>
        public string RemoteUserName
        {
            get
            {
                return m_strUserName;
            }
            set
            {
                m_strUserName = value;
            }
        }


        /// <summary>
        /// 一般套接字消息
        /// </summary>
        public string SockMessage
        {
            get
            {
                return m_strMsg;
            }
            set
            {
                m_strMsg = value;
            }
        }


        /// <summary>
        /// 公共遠端節點
        /// </summary>
        public IPEndPoint RemoteEndPoint
        {
            get
            {
                return m_EndPoint;
            }
            set
            {
                m_EndPoint = value;
            }
        }
    }
}
View Code

 

UDPP2PSock.cs

//UDPP2PSock.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

using P2PWellKnown;

namespace UDPP
{
    /// <summary>
    /// UDPP2P套接字管理類
    /// </summary>
    public class UDPP2PSock
    {
        /// <summary>
        /// 用戶登錄事件
        /// </summary>
        public event UdpUserLogInDelegate OnUserLogInU;

        /// <summary>
        /// 一般UDP消息事件
        /// </summary>
        public event UdpMessageDelegate OnSockMessageU;

        /// <summary>
        /// 初始化一個新連接事件
        /// </summary>
        public event UdpNewConnectDelegate OnNewConnectU;

        /// <summary>
        /// UDP服務器
        /// </summary>
        private UdpClient m_udpServer;

        /// <summary>
        /// UDP客戶端
        /// </summary>
        private UdpClient m_udpClient;

        /// <summary>
        /// 服務器實際上在本地機器上監聽的
        /// 端口,用於當一台計算機上同時啟
        /// 動兩個可兩以上服務器進程時,標
        /// 識不同的服務器進程
        /// </summary>
        private int m_iMyServerPort;

        /// <summary>
        /// 客戶端在本地機器上實際使用的端口,
        /// 用於當一台計算機上同時有兩個或兩
        /// 個以上客戶端進程在運行時,標識不
        /// 同的客戶端進程
        /// </summary>
        private int m_iMyClientPort;

        /// <summary>
        /// 標識是否已成功創服務器
        /// </summary>
        private bool m_bServerCreated;

        /// <summary>
        /// 標識是否已成功創建客戶端
        /// </summary>
        private bool m_bClientCreated;

        /// <summary>
        /// 服務器使用的線程
        /// </summary>
        private Thread m_serverThread;

        /// <summary>
        /// 客戶端使用的線程
        /// </summary>
        private Thread m_clientThread;

        /// <summary>
        /// 打洞線程
        /// </summary>
        //private Thread m_burrowThread;

        /// <summary>
        /// 遠端節點
        /// </summary>
        private IPEndPoint m_remotePoint;

        /// <summary>
        /// 當前進程作為客戶端的公共終端
        /// </summary>
        private string m_strMyPublicEndPoint;

        /// <summary>
        /// 當前進程作為客戶端的私有終端
        /// </summary>
        private string m_strMyPrivateEndPoint;

        /// <summary>
        /// 用於接受信息的 StringBuilder實例
        /// </summary>
        private StringBuilder m_sbResponse = new StringBuilder();

        /// <summary>
        /// P2P打洞時標識是否收到回應消息
        /// </summary>
        private bool m_bRecvAck = false;

        /// <summary>
        /// 請求向其方向打洞的私有終端
        /// </summary>
        private IPEndPoint m_requestPrivateEndPoint;

        /// <summary>
        /// 請求向其方向打洞的公共終端
        /// </summary>
        private IPEndPoint m_requestPublicEndPoint;

        /// <summary>
        /// 打洞消息要發向的節點
        /// </summary>
        private ToEndPoint m_toEndPoint;

        /// <summary>
        /// 用於標識是否已經和請求客戶端建立點對連接
        /// </summary>
        //private bool m_bHasConnected=false ;

        /// <summary>
        /// 創建服務器或客戶端的最大嘗試
        /// 次數,為(65536-60000),防止
        /// 因不能創建而限入死循環或使用
        /// 無效端口
        /// </summary>
        private const int MAX_CREATE_TRY = 5536;

        /// <summary>
        /// 打洞時嘗試連接的最大嘗試次數
        /// </summary>
        private const int MAX_CONNECT_TRY = 10;


        /// <summary>
        /// 構造函數,初始化UDPP2P實例
        /// </summary>
        public UDPP2PSock()
        {
            m_iMyServerPort = P2PConsts.UDP_SRV_PORT;
            m_iMyClientPort = 60000;
            m_bClientCreated = false;
            m_bServerCreated = false;
            m_toEndPoint = new ToEndPoint();
            m_serverThread = new Thread(new ThreadStart(RunUDPServer));
            m_clientThread = new Thread(new ThreadStart(RunUDPClient));
            //m_burrowThread = new Thread(new ThreadStart(BurrowProc));
        }

        /// <summary>
        /// 創建UDP 服務器
        /// </summary>
        public void CreateUDPSever()
        {
            int iTryNum = 0;

            //開始嘗試創建服務器
            while (!m_bServerCreated && iTryNum < MAX_CREATE_TRY)
            {
                try
                {
                    m_udpServer = new UdpClient(m_iMyServerPort);
                    m_bServerCreated = true;
                }
                catch
                {
                    m_iMyServerPort++;
                    iTryNum++;
                }
            }

            //創建失敗,拋出異常
            if (!m_bServerCreated && iTryNum == MAX_CREATE_TRY)
            {
                throw new Exception("創建服務器嘗試失敗!");
            }
            m_serverThread.Start();

        }

        /// <summary>
        /// 創建UDP客戶端
        /// </summary>
        /// <param name="strServerIP"& gt;服務器IP</param>
        /// <param name="iServerPort"& gt;服務器端口</param>
        public void CreateUDPClient(string strServerIP, int iServerPort)
        {
            int iTryNum = 0;

            //開始嘗試創建服務器
            while (!m_bClientCreated && iTryNum < MAX_CREATE_TRY)
            {
                try
                {
                    m_udpClient = new UdpClient(m_iMyClientPort);
                    m_bClientCreated = true;
                    string strIPAddress = (System.Net.Dns.GetHostAddresses("localhost")[0]).ToString();
                    m_strMyPrivateEndPoint = strIPAddress + ":" + m_iMyClientPort.ToString();
                }
                catch
                {
                    m_iMyClientPort++;
                    iTryNum++;
                }
            }

            //創建失敗,拋出異常
            if (!m_bClientCreated && iTryNum == MAX_CREATE_TRY)
            {
                throw new Exception("創建客戶端嘗試失敗!");
            }

            IPEndPoint hostPoint = new IPEndPoint(IPAddress.Parse(strServerIP), iServerPort);
            string strLocalIP = (System.Net.Dns.GetHostAddresses("localhost"))[0].ToString();
            SendLocalPoint(strLocalIP, m_iMyClientPort, hostPoint);
            m_clientThread.Start();
        }


        /// <summary>
        /// 運行UDP 服務器
        /// </summary>
        private void RunUDPServer()
        {
            while (true)
            {
                byte[] msgBuffer = m_udpServer.Receive(ref m_remotePoint);
                m_sbResponse.Append(System.Text.Encoding.Default.GetString(msgBuffer));
                CheckCommand();
                Thread.Sleep(10);
            }
        }


        /// <summary>
        /// 運行UDP客戶端
        /// </summary>
        private void RunUDPClient()
        {
            while (true)
            {
                byte[] msgBuffer = m_udpClient.Receive(ref m_remotePoint);
                m_sbResponse.Append(System.Text.Encoding.Default.GetString(msgBuffer));
                CheckCommand();
                Thread.Sleep(10);
            }
        }


        /// <summary>
        /// 銷毀UDP 服務器
        /// </summary>
        public void DisposeUDPServer()
        {
            m_serverThread.Abort();
            m_udpServer.Close();
        }

        /// <summary>
        /// 銷毀UDP客房端
        /// </summary>
        public void DisposeUDPClient()
        {
            m_clientThread.Abort();
            m_udpClient.Close();
        }

        /// <summary>
        /// 發送消息
        /// </summary>
        /// <param name="strMsg"& gt;消息內容</param>
        /// <param name="REP"& gt;接收節點</param>
        public void SendData(string strMsg, IPEndPoint REP)
        {
            byte[] byMsg = System.Text.Encoding.Default.GetBytes(strMsg.ToCharArray());
            m_udpClient.Send(byMsg, byMsg.Length, REP);
        }


        /// <summary>
        /// 發送消息,服務器專用
        /// </summary>
        /// <param name="strMsg"& gt;消息內容</param>
        /// <param name="REP"& gt;接收節點</param>
        private void ServerSendData(string strMsg, IPEndPoint REP)
        {
            byte[] byMsg = System.Text.Encoding.Default.GetBytes(strMsg.ToCharArray());
            m_udpServer.Send(byMsg, byMsg.Length, REP);
        }


        /// <summary>
        /// 發送本地節點信息
        /// </summary>
        /// <param name="strLocalIP"& gt;本地IP</param>
        /// <param name="iLocalPort"& gt;本地端口</param>
        public void SendLocalPoint(string strLocalIP, int iLocalPort, IPEndPoint REP)
        {
            string strLocalPoint = "\x01\x02" + strLocalIP + ":" + iLocalPort.ToString() + "\x02\x01";
            SendData(strLocalPoint, REP);
        }


        /// <summary>
        /// 同時向指定的終端(包括公共終端和私有終端)打洞
        /// </summary>
        /// <param name="pubEndPoint"& gt;公共終端</param>
        /// <param name="prEndPoint"& gt;私有終端</param>
        /// <returns>打洞成功返回true,否則返回false</returns>
        public void StartBurrowTo(IPEndPoint pubEndPoint, IPEndPoint prEndPoint)
        {
            Thread burrowThread = new Thread(new ThreadStart(BurrowProc));
            m_toEndPoint.m_privateEndPoint = prEndPoint;
            m_toEndPoint.m_publicEndPoint = pubEndPoint;
            burrowThread.Start();
        }


        /// <summary>
        /// 打洞線程
        /// </summary>
        private void BurrowProc()
        {
            IPEndPoint prEndPoint = m_toEndPoint.m_privateEndPoint;
            IPEndPoint pubEndPoint = m_toEndPoint.m_publicEndPoint;
            int j = 0;
            for (int i = 0; i < MAX_CONNECT_TRY; i++)
            {
                SendData("\x01\x07\x07\x01", prEndPoint);
                SendData("\x01\x07\x07\x01", pubEndPoint);

                // 等待接收線程標記修改
                for (j = 0; j < MAX_CONNECT_TRY; j++)
                {
                    if (m_bRecvAck)
                    {
                        m_bRecvAck = false;
                        SendData("\x01\x07\x07\x01", prEndPoint);
                        Thread.Sleep(50);
                        SendData("\x01\x07\x07\x01", pubEndPoint);

                        UDPSockEventArgs args = new UDPSockEventArgs("");
                        args.RemoteEndPoint = pubEndPoint;
                        if (OnNewConnectU != null)
                        {
                            OnNewConnectU(this, args);
                        }
                        //Thread .Sleep (System .Threading.Timeout .Infinite );
                        return;
                    }
                    else
                    {
                        Thread.Sleep(100);
                    }
                }

                //如果沒有收到目標主機的回應,表明本次打
                // 洞嘗試失敗,等待100毫秒后嘗試下一次打洞
                Thread.Sleep(100);
            }

            //MAX_CONNECT_TRY 嘗試都失敗,表明打洞失敗,拋出異常
            //throw new Exception(" 打洞失敗!");
            System.Windows.Forms.MessageBox.Show("打洞失敗!");////////////
        }


        /// <summary>
        /// 轉發打洞請求消息,在服務器端使用
        /// </summary>
        /// <param name="strSrcPrEndpoint"& gt;請求轉發的源私有終端</param>
        /// <param name="strSrcPubEndPoint"& gt;請求轉發的源公共終端</param>
        /// <param name="REP"& gt;轉發消息到達的目的終端</param>
        public void SendBurrowRequest(string strSrcPrEndpoint, string strSrcPubEndPoint, IPEndPoint REP)
        {
            string strBurrowMsg = "\x04\x07" + strSrcPrEndpoint + " " + strSrcPubEndPoint + "\x07\x04";
            ServerSendData(strBurrowMsg, REP);
        }


        /// <summary>
        /// 檢查字符串中的命令
        /// </summary>
        private void CheckCommand()
        {
            int nPos;
            string strCmd = m_sbResponse.ToString();

            //如果接收遠端用戶名
            if ((nPos = strCmd.IndexOf("\x01\x02")) > -1)
            {
                ReceiveName(strCmd, nPos);

                // 反饋公共終給端遠端主機
                string strPubEPMsg = "\x03\x07" + m_remotePoint.ToString() + "\x07\x03";
                SendData(strPubEPMsg, m_remotePoint);

                return;
            }

            //如果接收我的公共終端
            if ((nPos = strCmd.IndexOf("\x03\x07")) > -1)
            {
                ReceiveMyPublicEndPoint(strCmd, nPos);
                return;
            }

            //如果是打洞請求消息
            if ((nPos = strCmd.IndexOf("\x04\x07")) > -1)
            {
                ReceiveAndSendAck(strCmd, nPos);
                return;
            }

            //如果是打洞回應消息
            if ((nPos = strCmd.IndexOf("\x01\x07")) > -1)
            {
                m_bRecvAck = true;
                int nPos2 = strCmd.IndexOf("\x07\x01");
                if (nPos2 > -1)
                {
                    m_sbResponse.Remove(nPos, nPos2 - nPos + 2);
                }



                return;
            }

            //一般聊天消息
            m_sbResponse.Remove(0, strCmd.Length);
            RaiseMessageEvent(strCmd);
        }


        /// <summary>
        /// 接收遠端用戶名
        /// </summary>
        /// <param name="strCmd"& gt;包含用戶名的控制信息</param>
        /// <param name="nPos"></param>
        private void ReceiveName(string strCmd, int nPos)
        {
            int nPos2 = strCmd.IndexOf("\x02\x01");
            if (nPos2 == -1)
            {
                return;
            }
            m_sbResponse.Remove(nPos, nPos2 - nPos + 2);

            string strUserName = strCmd.Substring(nPos + 2, nPos2 - nPos - 2);
            UDPSockEventArgs e = new UDPSockEventArgs("");
            e.RemoteUserName = strUserName;
            e.RemoteEndPoint = m_remotePoint;

            //觸發用戶登錄事件
            if (OnUserLogInU != null)
            {
                OnUserLogInU(this, e);
            }
        }


        /// <summary>
        /// 接收打洞請求的消息並發送回應
        /// </summary>
        /// <param name="strCmd"></param>
        /// <param name="nPos"></param>
        private void ReceiveAndSendAck(string strCmd, int nPos)
        {
            int nPos2 = strCmd.IndexOf("\x07\x04");
            if (nPos2 == -1)
            {
                return;
            }
            m_sbResponse.Remove(nPos, nPos2 - nPos + 2);

            string strBurrowMsg = strCmd.Substring(nPos + 2, nPos2 - nPos - 2);

            string[] strSrcPoint = strBurrowMsg.Split(' ');

            //分析控制字符串包含的節點信息
            string[] strPrEndPoint = strSrcPoint[0].Split(':');
            string[] strPubEndPoint = strSrcPoint[1].Split(':');
            m_requestPrivateEndPoint = new IPEndPoint(IPAddress.Parse(strPrEndPoint[0]), int.Parse(strPrEndPoint[1]));
            m_requestPublicEndPoint = new IPEndPoint(IPAddress.Parse(strPubEndPoint[0]), int.Parse(strPubEndPoint[1]));

            //向請求打洞終端的方向打洞
            StartBurrowTo(m_requestPublicEndPoint, m_requestPrivateEndPoint);
        }


        /// <summary>
        /// 接收我的公共終端
        /// </summary>
        /// <param name="strCmd"& gt;包含公共終端的控制信息</param>
        /// <param name="nPos"& gt;控制字符串的起始位置</param>
        private void ReceiveMyPublicEndPoint(string strCmd, int nPos)
        {
            int nPos2 = strCmd.IndexOf("\x07\x03");
            if (nPos2 == -1)
            {
                return;
            }
            m_sbResponse.Remove(nPos, nPos2 - nPos + 2);

            m_strMyPublicEndPoint = strCmd.Substring(nPos + 2, nPos2 - nPos - 2);
        }


        /// <summary>
        /// 觸發一般UDP消息事件
        /// </summary>
        /// <param name="strMsg"& gt;消息內容</param>
        private void RaiseMessageEvent(string strMsg)
        {
            UDPSockEventArgs args = new UDPSockEventArgs("");
            args.SockMessage = strMsg;
            args.RemoteEndPoint = m_remotePoint;
            if (OnSockMessageU != null)
            {
                OnSockMessageU(this, args);
            }
        }


        /// <summary>
        /// 獲取當前進程作為客戶端的公共終端
        /// </summary>
        public string MyPublicEndPoint
        {
            get
            {
                return m_strMyPublicEndPoint;
            }
        }


        /// <summary>
        /// 獲取當前進程作為客戶端的私有終端
        /// </summary>
        public string MyPrivateEndPoint
        {
            get
            {
                return m_strMyPrivateEndPoint;
            }
        }
    }


    /// <summary>
    /// 保存打洞消息要發向的節點信息
    /// </summary>
    class ToEndPoint
    {
        /// <summary>
        /// 私有節點
        /// </summary>
        public IPEndPoint m_privateEndPoint;

        /// <summary>
        /// 公共節點
        /// </summary>
        public IPEndPoint m_publicEndPoint;
    }
}
View Code

 

關於如何使用上述程序包的一些說明:
主要程序的初始化,參考代碼如下:

using UDPP;
using P2PWellKnown;


//創建UDP服務器和客戶端
try
{
    string strServerIP = "127.0.0.1";
    UDPP2PSock udpSock = new UDPP2PSock();
    udpSock.OnUserLogInU += new UdpUserLogInDelegate(OnUserLogInU);
    udpSock.OnNewConnectU += new UdpNewConnectDelegate(OnNewConnectU);
    udpSock.CreateUDPSever();
    udpSock.CreateUDPClient(strServerIP, P2PConsts.UDP_SRV_PORT);
}
catch (Exception ex)
{

}
View Code

 

經上面的初始化后,就可以使用類UDPP2PSock中的方法了。

 

注:

udpSock.OnUserLogInU +=new UdpUserLogInDelegate(OnUserLogInU);
udpSock.OnNewConnectU
+=new UdpNewConnectDelegate(OnNewConnectU);
中的
OnUserLogInU和OnNewConnectU是事件名稱,如
private void test(object sender, UDPSockEventArgs e)
{
MessageBox.Show("ok");
}

 

出處:http://www.cnblogs.com/hcbin/archive/2010/04/10/1709019.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM