C#实现UDP打洞
之前一直在犹豫该不该发表这篇文章。从网上看到的很多技术牛人,觉得自己所掌握的技术是如此渺小。我并不适合于走技术这条路,为了寻求一个更完美的人生,早已决定了不继续走技术这条路线,所以也不太愿意发表一些技术性的文章。
最近一段时间的思索,发现在自己的一直所追求的是思想与智慧的美。曾经在技术上进行了两年左右的探索,但从自己对世界思索的收获远比学习技术给自己带来的收获要多得多,尽管自己对周围世界的理解有点空头思想的之嫌,但既是自己追求,我想就应该坚持走下去,人生也就这么一回。
最后决定发表这篇文章的直接原因是毕竟自己曾经在技术路上走过,发表点技术性的文章给点象征性的表示;其次的原因是在IPv6版本在世界范围内普遍使用,IPv4淡出网络市场之前,打洞技术在很长一段时间内还有它不可替代的意义和地位;第三是我欣赏打洞原理的思想,想想当初网络提出NAT技术的时候,如果没有留下这个可以打洞的后门,恐怕“打洞”这个名词将不会在IT历史上留名,现在风行的即时通信工具技术的难度也将难以想象。有点遗憾的是一直没有去追查当初这个后门是有意还是无意留下的,不过我宁愿相信是有意留下的,因为这体现了一种高瞻远瞩的眼光,留下这个后门为今天网络的繁荣作了长远的铺垫。至于打洞原理在网上搜一下多的是,这里就没有必要再赘述。
虽然打洞技术的实现源码在网上也可以找到,但用C#实现的源码并不多见。另外下面的代码为了达到功能与界面的分离,采用了事件进行消息通信,程序具有一定的可移植性和可扩展性,也最能体现我的C#编码风格。不过代码没有严格遵循模式设计的原则,希望网友们如果有更优化的代码能提出来共享一下。总体上来说,下面的程序包还有它存在的使用价值和意义,所以发表出来共享一下。
下面是UDP打洞程序包的源码:
//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;
}
}
}
}
//UDPP2PSock.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using P2PWellKnown;
namespace UDPP2P
{
/// <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">服务器IP</param>
/// <param name="iServerPort">服务器端口</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">消息内容</param>
/// <param name="REP">接收节点</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">消息内容</param>
/// <param name="REP">接收节点</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">本地IP</param>
/// <param name="iLocalPort">本地端口</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">公共终端</param>
/// <param name="prEndPoint">私有终端</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">请求转发的源私有终端</param>
/// <param name="strSrcPubEndPoint">请求转发的源公共终端</param>
/// <param name="REP">转发消息到达的目的终端</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);
}
/*
if (m_requestPublicEndPoint != null)
{
if (!m_bHasConnected)
{
m_bHasConnected = true;
UDPSockEventArgs args = new UDPSockEventArgs("");
args.RemoteEndPoint = m_requestPublicEndPoint;
if (OnNewConnectU != null)
{
OnNewConnectU(this, args);
}
m_requestPublicEndPoint = null;
}
}*/
return;
}
//一般聊天消息
m_sbResponse.Remove(0, strCmd.Length);
RaiseMessageEvent(strCmd);
}
/// <summary>
/// 接收远端用户名
/// </summary>
/// <param name="strCmd">包含用户名的控制信息</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">包含公共终端的控制信息</param>
/// <param name="nPos">控制字符串的起始位置</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">消息内容</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;
}
}
关于如何使用上述程序包的一些说明:
主要程序的初始化,参考代码如下:
//创建UDP服务器和客户端
try
{
string strServerIP="127.0.0.1"
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)
{
}
经上面的初始化后,就可以使用类UDPP2PSock中的方法了。
上述程序包在WINXP/2003 下,Visual Studio7.1和Visual Studio8.0中调试通过。有不好之处,敬请请高手指教。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wenzhoufeng/archive/2007/03/27/1542299.aspx