C#的内网穿透学习(附源码)

Posted DotNet

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#的内网穿透学习(附源码)相关的知识,希望对你有一定的参考价值。

(给DotNet加星标,提升.Net技能

转自:~小菜鸟
cnblogs.com/qqljcn/p/13738595.html

如何让两台处在不同内网的主机直接互连?你需要内网穿透!



上图是一个非完整版内外网通讯图由内网端先发起,内网设备192.168.1.2:6677发送数据到外网时候必须经过nat会转换成对应的外网ip+端口,然后在发送给外网设备,外网设备回复数据也是发给你的外网ip+端口。


这只是单向的内去外,那反过来,如果外网的设备需要主动访问我局域网里的某一个设备是无法访问的,因为这个时候还没做nat转换所以外网不知道你内网设备的应用具体对应的是哪个端口,这个时候我们就需要内网穿透了,内网穿透也叫NAT穿透;


穿透原理



(1)Full cone NAT:即著名的一对一(one-to-one)NAT。





任意外部主机(hostAddr:any)都能通过给eAddr:port2发包到达iAddr:port1的前提是:iAddr:port1之前发送过包到hostAddr:any. "any"也就是说端口不受限制(只需知道某个转换后的外网ip+端口即可。)


3)Port-Restricted cone NAT:类似受限制锥形NAT(Restricted cone NAT),但是还有端口限制。




(4)Symmetric NAT(对称NAT)




例子:

 

下面用一个例子演示下“受限制锥形NAT”的打洞,实现了这个它前面两个类型也能通用。对称型的话不考虑,打不了洞。


我们知道要实现两台“受限制锥形NAT”互连重点就是要知道对方转换后的外网IP+端口,这样我们可以:

 

1、准备一台Full cone NAT 类型的外网服务端,接受来自两个客户端的连接,并对应告知对方ip+端口;

 

2、知道了对方ip+端口 需要设置socke:Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);这样才能端口复用;目的就是让连接对外的端口一致;

 

3、最后,我们可以让两台客户端互相连接,或者一台先发一个请求,打个洞;另一个在去连接;

 

代码:


1、TCP+IOCP方式,相对 “面向对象”地实现穿透!


服务端 ServerListener类,用SocketAsyncEventArgs:


/// <summary>
/// 打洞服务端,非常的简单,接收两个连接并且转发给对方;
/// </summary>
public class ServerListener : IServerListener
{
IPEndPoint EndPoint { get; set; }
//消息委托
public delegate void EventMsg(object sender, string e);
public static object obj = new object();
//通知消息
public event EventMsg NoticeMsg;
//接收事件
public event EventMsg ReceivedMsg;
/// <summary>
/// 上次链接的
/// </summary>
private Socket Previous;
public ServerListener(IPEndPoint endpoint)
{
this.EndPoint = endpoint;
}
private Socket listener;
public void Start()
{
this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
var connectArgs = new SocketAsyncEventArgs();
listener.Bind(EndPoint);
listener.Listen(2);
EndPoint = (IPEndPoint)listener.LocalEndPoint;
connectArgs.Completed += OnAccept;
//是否同步就完成了,同步完成需要自己触发
if (!listener.AcceptAsync(connectArgs))
OnAccept(listener, connectArgs);
}
byte[] bytes = new byte[400];
private void OnAccept(object sender, SocketAsyncEventArgs e)
{
Socket socket = null;
try
{
var remoteEndPoint1 = e.AcceptSocket.RemoteEndPoint.ToString();
NoticeMsg?.Invoke(sender, $"客户端:{remoteEndPoint1}连接上我了!\r\n");
SocketAsyncEventArgs readEventArgs = new SocketAsyncEventArgs();
readEventArgs.Completed += OnSocketReceived;
readEventArgs.UserToken = e.AcceptSocket;
readEventArgs.SetBuffer(bytes, 0, 400);
if (!e.AcceptSocket.ReceiveAsync(readEventArgs))
OnSocketReceived(e.AcceptSocket, readEventArgs);
lock (obj)
{
socket = e.AcceptSocket;
//上次有链接并且链接还”健在“
if (Previous == null||! Previous.Connected)
{
Previous = e.AcceptSocket;
}
else
{
//Previous.SendAsync()..?
Previous.Send(Encoding.UTF8.GetBytes(remoteEndPoint1 + "_1"));
socket.Send(Encoding.UTF8.GetBytes(Previous.RemoteEndPoint.ToString() + "_2"));
NoticeMsg?.Invoke(sender, $"已经通知双方!\r\n");
Previous = null;
}
}
e.AcceptSocket = null;
if (e.SocketError != SocketError.Success)
throw new SocketException((int)e.SocketError);

if(!listener.AcceptAsync(e))
OnAccept(listener, e);
}
catch
{
socket?.Close();
}
}
public void Close()
{
using (listener)
{
// listener.Shutdown(SocketShutdown.Both);
listener.Close();
}
//throw new NotImplementedException();
}
/// <summary>
/// 此处留有一个小BUG,接收的字符串大于400的时候会有问题;可以参考客户端修改
/// </summary>
public void OnSocketReceived(object sender, SocketAsyncEventArgs e)
{
Socket socket = e.UserToken as Socket;
var remoteEndPoint = socket.RemoteEndPoint.ToString();
try
{
if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)

{
ReceivedMsg?.Invoke(sender, $"收到:{remoteEndPoint}发来信息:{Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred)}\r\n");

}
else
{
socket?.Close();
NoticeMsg?.Invoke(sender, $"链接:{remoteEndPoint}释放啦!\r\n");
return;
}
if (!socket.ReceiveAsync(e))
OnSocketReceived(socket, e);
}
catch
{
socket?.Close();
}
//{
// if (!((Socket)sender).AcceptAsync(e))
// OnSocketReceived(sender, e);
//}
//catch
//{
// return;
//}
}
}


2、客户端类 PeerClient用BeginReceive和EndReceive实现异步;


public class StateObject
{
public Socket workSocket = null;
public const int BufferSize = 100;
public byte[] buffer = new byte[BufferSize];
public List<byte> buffers = new List<byte>();
//是不是和服务器的链接
public bool IsServerCon = false;
}
/// <summary>
/// 打洞节点客户端 实现的功能:
/// 连接服务器获取对方节点ip
/// 请求对方ip(打洞)
/// 根据条件判断是监听连接还是监听等待连接
/// </summary>
public class PeerClient : IPeerClient
{
//ManualResetEvent xxxxDone = new ManualResetEvent(false);
//Semaphore
/// <summary>
/// 当前链接
/// </summary>
public Socket Client { get;private set; }
#region 服务端
public string ServerHostName { get;private set; }
public int ServerPort { get; private set; }
#endregion

#region 接收和通知事件
public delegate void EventMsg(object sender, string e);
//接收事件
public event EventMsg ReceivedMsg;
//通知消息
public event EventMsg NoticeMsg;
#endregion
//本地绑定的节点
private IPEndPoint LocalEP;
public PeerClient(string hostname, int port)
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
this.ServerHostName = hostname;
this.ServerPort = port;
}

/// <summary>
/// 初始化客户端(包括启动)
/// </summary>
public void Init()
{
try
{
Client.Connect(ServerHostName, ServerPort);
}
catch (SocketException ex)
{
NoticeMsg?.Invoke(Client, $"连接服务器失败!{ex}!\r\n");
throw;
}
catch (Exception ex)
{
NoticeMsg?.Invoke(Client, $"连接服务器失败!{ex}!\r\n");
throw;
}
NoticeMsg?.Invoke(Client, $"连接上服务器了!\r\n");
var _localEndPoint = Client.LocalEndPoint.ToString();
LocalEP = new IPEndPoint(IPAddress.Parse(_localEndPoint.Split(':')[0])
, int.Parse(_localEndPoint.Split(':')[1]));
Receive(Client);
}
private void Receive(Socket client)
{
try
{
StateObject state = new StateObject();
state.workSocket = client;
state.IsServerCon = true;
client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
}
catch (Exception e)
{
NoticeMsg?.Invoke(Client, $"接收消息出错了{e}!\r\n");
}
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
var state = (StateObject)ar.AsyncState;
Socket _client = state.workSocket;
//因为到这边的经常Connected 还是true
//if (!_client.Connected)
//{
// _client.Close();
// return;
//}
SocketError error = SocketError.Success;
int bytesRead = _client.EndReceive(ar,out error);
if (error == SocketError.ConnectionReset)
{
NoticeMsg?.Invoke(Client, $"链接已经释放!\r\n");
_client.Close();
_client.Dispose();
return;
}
if (SocketError.Success!= error)
{
throw new SocketException((int)error);
}
var arr = state.buffer.AsQueryable().Take(bytesRead).ToArray();
state.buffers.AddRange(arr);
if (bytesRead >= state.buffer.Length)
{
_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
////state.buffers.CopyTo(state.buffers.Count, state.buffer, 0, bytesRead);
//_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
// new AsyncCallback(ReceiveCallback), state);
}
else
{
var _msg = Encoding.UTF8.GetString(state.buffers.ToArray());
ReceivedMsg?.Invoke(_client, _msg);
if (state.IsServerCon)
{
_client.Shutdown(SocketShutdown.Both);
_client.Close();
int retryCon = _msg.Contains("_1") ? 1 : 100;
_msg = _msg.Replace("_1", "").Replace("_2", "");
TryConnection(_msg.Split(':')[0], int.Parse(_msg.Split(':')[1]), retryCon);
return;
}
state = new StateObject();
state.IsServerCon = false;
state.workSocket = _client;
_client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
}
}
catch (SocketException ex)
{
//10054
NoticeMsg?.Invoke(Client, $"链接已经释放!{ex}!\r\n");
}
catch (Exception e)
{
NoticeMsg?.Invoke(Client, $"接收消息出错了2{e}!\r\n");
}
}
/// <summary>
/// 打洞或者尝试链接
/// </summary>
private void TryConnection(string remoteHostname, int remotePort,int retryCon)
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
var _iPRemotePoint = new IPEndPoint(IPAddress.Parse(remoteHostname), remotePort);
Client.Bind(LocalEP);
System.Threading.Thread.Sleep(retryCon==1?1:3*1000);
for (int i = 0; i < retryCon; i++)
{
try
{
Client.Connect(_iPRemotePoint);
NoticeMsg?.Invoke(Client, $"已经连接上:{remoteHostname}:{remotePort}!\r\n");
StateObject state = new StateObject();
state.workSocket = Client;
state.IsServerCon = false;
Client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
return;
}
catch
{
NoticeMsg?.Invoke(Client, $"尝试第{i+1}次链接:{remoteHostname}:{remotePort}!\r\n");
}
}
if (retryCon==1)
{
Listening(LocalEP.Port);
return;
}
NoticeMsg?.Invoke(Client, $"尝试了{retryCon}次都没有办法连接到:{remoteHostname}:{remotePort},凉了!\r\n"); }

/// <summary>
/// 如果连接不成功,因为事先有打洞过了,根据条件监听 等待对方连接来
/// </summary>
private void Listening(int Port)
{
try
{
Client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
Client.Bind(new IPEndPoint(IPAddress.Any, Port));Client.Listen((int)SocketOptionName.MaxConnections);
NoticeMsg?.Invoke(Client, $"开始侦听断开等待链接过来!\r\n");
StateObject state = new StateObject();
state.IsServerCon = false;
var _socket = Client.Accept();//只有一个链接 不用BeginAccept
Client.Close();//关系现有侦听
Client = _socket;
state.workSocket = Client;
NoticeMsg?.Invoke(Client, $"接收到来自{Client.RemoteEndPoint}的连接!\r\n");
Client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReceiveCallback), state);
}
catch (Exception ex)
{
NoticeMsg?.Invoke(Client, $"监听出错了{ex}凉了!\r\n");
}
//scoket.send
}
/// <summary>
/// 本例子只存在一个成功的链接,对成功的连接发送消息!
/// </summary>
/// <param name="strMsg"></param>
public void Send(string strMsg)
{
byte[] bytes = Encoding.UTF8.GetBytes(strMsg);
Client.BeginSend(bytes, 0, bytes.Length, 0,
new AsyncCallback(SendCallback), Client);
}
private void SendCallback(IAsyncResult ar)
{
try
{
Socket _socket = (Socket)ar.AsyncState;
//if(ar.IsCompleted)
_socket.EndSend(ar);
}
catch (Exception e)
{
NoticeMsg?.Invoke(Client, $"发送消息出错了{e}!\r\n");
}
}
}


完整代码:https://gitee.com/qqljcn/zsg_-peer-to-peer


二、面向过程方式


Task+(TcpClient+TcpListener )|(UdpClient)实现 tcp|udp的打洞!这个就不贴代码了直接放码云连接


 https://gitee.com/qqljcn/zsg_-peer-to-peer_-lite


三、说明


1、本人是个老菜鸟代码仅供参考,都是挺久以前写的也没有经过严格的测试仅能演示这个例子,有不成熟的地方,烦请各位大神海涵指教;


2、不要都用本机试这个例子,本机不走nat


3、然后udp因为是无连接的所以打孔成功后不要等太久再发消息,nat缓存一过就失效了!


4、确定自己不是对称型nat的话,如果打洞不成功,那就多试几次! 


5 、我这个例子代码名字叫 PeerToPeer 但不是真的p2p, 微软提供了p2p的实现 在using System.Net.PeerToPeer命名空间下。


以上是通过nat的方式,另外还有一种方式是,通过一个有外网ip的第三方服务器转发像 花生壳、nat123这类软件,也有做个小程序,并且自己在用以后演示;


- EOF -



推荐阅读   点击标题可跳转


看完本文有收获?请转发分享给更多人

关注「DotNet」加星标,提升.Net技能 

好文章,我在看❤️

以上是关于C#的内网穿透学习(附源码)的主要内容,如果未能解决你的问题,请参考以下文章

2022年分享一些好用的内网穿透软件

有没有简单的内网穿透工具?

frp内网穿透

几款实用的内网穿透工具

几款实用的内网穿透工具

免费的内网穿透利器