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

     上图是一个非完整版内外网通讯图由内网端先发起,内网设备192.168.1.2:6677发送数据到外网时候必须经过nat会转换成对应的外网ip+端口,然后在发送给外网设备,外网设备回复数据也是发给你的外网ip+端口。这只是单向的内去外,那反过来,如果外网的设备需要主动访问我局域网里的某一个设备是无法访问的,因为这个时候还没做nat转换所以外网不知道你内网设备的应用具体对应的是哪个端口,这个时候我们就需要内网穿透了,内网穿透也叫NAT穿透;
 
       穿透原理
 
       如上图所示经NAT转换后的内外网地址+端口,会缓存一段时间,在这段时间内192.168.1.2:6677和112.48.69.2020:8899的映射关系会一直存在,这样你的内网主机就得到一个外网地址,这个对应关系又根据NAT转换方法类型的不同,得用对应的方式实现打洞,NAT转换方法类型有下列几种(来源百度百科NAT):
 
     (1)Full cone NAT:即著名的一对一(one-to-one)NAT。
 
       一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。任意外部主机都能通过给eAddr:port2发包到iAddr:port1(纯天然不用打洞!)
 

(2)Address-Restricted cone NAT :限制地址,即只接收曾经发送到对端的IP地址来的数据包。

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

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

        一旦一个内部地址(iAddr:port1)映射到外部地址(eAddr:port2),所有发自iAddr:port1的包都经由eAddr:port2向外发送。一个外部主机(hostAddr:port3)能够发包到达iAddr:port1的前提是:iAddr:port1之前发送过包到hostAddr:port3.(双方需要各自知道对方转换后的外网ip+端口,然后一方先发一次尝试连接,另一方在次连接过来的时候就能直接连通了。)
 

(4)Symmetric NAT(对称NAT)

       每一个来自相同内部IP与port的请求到一个特定目的地的IP地址和端口,映射到一个独特的外部来源的IP地址和端口。
 
       同一个内部主机发出一个信息包到不同的目的端,不同的映射使用外部主机收到了一封包从一个内部主机可以送一封包回来(只能和Full cone NAT连,没法打洞,手机流量开热点就是,同一个本地端口连接不同的服务器得到的外网第地址和IP不同!)
 
例子:
 
下面用一个例子演示下“受限制锥形NAT”的打洞,实现了这个它前面两个类型也能通用。对称型的话不考虑,打不了洞。我们知道要实现两台“受限制锥形NAT”互连重点就是要知道对
方转换后的外网IP+端口,这样我们可以:
 
1. 准备一台Full cone NAT 类型的外网服务端,接受来自两个客户端的连接,并对应告知对方ip+端口;
 
2.知道了对方ip+端口 需要设置socke:Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);这样才能端口复用;目的就是让连接对外的端口一致;
 
3.最后,我们可以让两台客户端互相连接,或者一台先发一个请求,打个洞;另一个在去连接;
 
代码:
1.TCP+IOCP方式,相对 “面向对象”地实现穿透!
服务端 ServerListener类,用SocketAsyncEventArgs:

  1   /// <summary>
2 /// 打洞服务端,非常的简单,接收两个连接并且转发给对方;
3 /// </summary>
4 public class ServerListener : IServerListener
5 {
6 IPEndPoint EndPoint { get; set; }
7 //消息委托
8 public delegate void EventMsg(object sender, string e);
9 public static object obj = new object();
10 //通知消息
11 public event EventMsg NoticeMsg;
12 //接收事件
13 public event EventMsg ReceivedMsg;
14 /// <summary>
15 /// 上次链接的
16 /// </summary>
17 private Socket Previous;
18 public ServerListener(IPEndPoint endpoint)
19 {
20 this.EndPoint = endpoint;
21 }
22 private Socket listener;
23 public void Start()
24 {
25 this.listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
26 var connectArgs = new SocketAsyncEventArgs();
27 listener.Bind(EndPoint);
28 listener.Listen(2);
29 EndPoint = (IPEndPoint)listener.LocalEndPoint;
30 connectArgs.Completed += OnAccept;
31 //是否同步就完成了,同步完成需要自己触发
32 if (!listener.AcceptAsync(connectArgs))
33 OnAccept(listener, connectArgs);
34 }
35 byte[] bytes = new byte[400];
36 private void OnAccept(object sender, SocketAsyncEventArgs e)
37 {
38 Socket socket = null;
39 try
40 {
41 var remoteEndPoint1 = e.AcceptSocket.RemoteEndPoint.ToString();
42 NoticeMsg?.Invoke(sender, $"客户端:{remoteEndPoint1}连接上我了!\r\n");
43 SocketAsyncEventArgs readEventArgs = new SocketAsyncEventArgs();
44 readEventArgs.Completed += OnSocketReceived;
45 readEventArgs.UserToken = e.AcceptSocket;
46 readEventArgs.SetBuffer(bytes, 0, 400);
47 if (!e.AcceptSocket.ReceiveAsync(readEventArgs))
48 OnSocketReceived(e.AcceptSocket, readEventArgs);
49 lock (obj)
50 {
51 socket = e.AcceptSocket;
52 //上次有链接并且链接还”健在“
53 if (Previous == null||! Previous.Connected)
54 {
55 Previous = e.AcceptSocket;
56 }
57 else
58 {
59 //Previous.SendAsync()..?
60 Previous.Send(Encoding.UTF8.GetBytes(remoteEndPoint1 + "_1"));
61 socket.Send(Encoding.UTF8.GetBytes(Previous.RemoteEndPoint.ToString() + "_2"));
62 NoticeMsg?.Invoke(sender, $"已经通知双方!\r\n");
63 Previous = null;
64 }
65 }
66
67 e.AcceptSocket = null;
68 if (e.SocketError != SocketError.Success)
69 throw new SocketException((int)e.SocketError);
70
71 if(!listener.AcceptAsync(e))
72 OnAccept(listener, e);
73 }
74 catch
75 {
76 socket?.Close();
77 }
78
79 }
80 public void Close()
81 {
82 using (listener)
83 {
84 // listener.Shutdown(SocketShutdown.Both);
85 listener.Close();
86 }
87 //throw new NotImplementedException();
88 }
89 /// <summary>
90 /// 此处留有一个小BUG,接收的字符串大于400的时候会有问题;可以参考客户端修改
91 /// </summary>
92 public void OnSocketReceived(object sender, SocketAsyncEventArgs e)
93 {
94 Socket socket = e.UserToken as Socket;
95 var remoteEndPoint = socket.RemoteEndPoint.ToString();
96 try
97 {
98 if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
99 {
100
101 ReceivedMsg?.Invoke(sender, $"收到:{remoteEndPoint}发来信息:{Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred)}\r\n");
102
103 }
104 else
105 {
106 socket?.Close();
107 NoticeMsg?.Invoke(sender, $"链接:{remoteEndPoint}释放啦!\r\n");
108 return;
109 }
110 if (!socket.ReceiveAsync(e))
111 OnSocketReceived(socket, e);
112 }
113 catch
114 {
115 socket?.Close();
116 }
117
118 //{
119 // if (!((Socket)sender).AcceptAsync(e))
120 // OnSocketReceived(sender, e);
121 //}
122 //catch
123 //{
124 // return;
125 //}
126 }
127 }

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这类软件,也有做个小程序,并且自己在用以后演示;

 
 
 
 
 
 

基于C#的内网穿透学习笔记(附源码)的更多相关文章

  1. 基于frp的内网穿透实例4-为本地的web服务实现HTTPS访问

    原文地址:https://wuter.cn/1932.html/ 一.想要实现的功能 目前已经实现将本地的web服务暴露到公网,现想要实现https访问.(前提:已经有相应的证书文件,如果没有就去申请 ...

  2. [转]OpenTK学习笔记(1)-源码、官网地址

    OpenTK源码下载地址:https://github.com/opentk/opentk OpenTK使用Nuget安装命令:OpenTK:Install-Package OpenTK -Versi ...

  3. Hadoop学习笔记(9) ——源码初窥

    Hadoop学习笔记(9) ——源码初窥 之前我们把Hadoop算是入了门,下载的源码,写了HelloWorld,简要分析了其编程要点,然后也编了个较复杂的示例.接下来其实就有两条路可走了,一条是继续 ...

  4. frp内网穿透学习

    前言 因为自己在内网,但是目标站在外网,这时候可以通过内网穿透工具,将接收到的请求转发到内网,实现在内网的msf可以控制外网的靶机. 也看了一些Ngrok,花生壳的,发现Ngrok.cc这个看文章说有 ...

  5. 基于frp的内网穿透实例1-通过SSH访问内网机器

    原文地址:https://wuter.cn/1804.html/ 老母鸡终于到了,作为一个能运行linux系统的四核1G硬件,它还是比较小巧的. FRP 全名:Fast Reverse Proxy.F ...

  6. OpenTK学习笔记(1)-源码、官网地址

    OpenTK源码下载地址:https://github.com/opentk/opentk OpenTK使用Nuget安装命令:OpenTK:Install-Package OpenTK -Versi ...

  7. Linux简易APR内存池学习笔记(带源码和实例)

    先给个内存池的实现代码,里面带有个应用小例子和画的流程图,方便了解运行原理,代码 GCC 编译可用.可以自己上网下APR源码,参考代码下载链接: http://pan.baidu.com/s/1hq6 ...

  8. Nginx学习笔记4 源码分析

    Nginx学习笔记(四) 源码分析 源码分析 在茫茫的源码中,看到了几个好像挺熟悉的名字(socket/UDP/shmem).那就来看看这个文件吧!从简单的开始~~~ src/os/unix/Ngx_ ...

  9. MarkDown语法 学习笔记 效果源码对照

    MarkDown基本语法学习笔记 Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的标记语法,它可以使普通文本内容具有一定的格式. 下面将对Markdown的基本使用做一个介绍 目 ...

随机推荐

  1. 2020重新出发,MySql基础,MySql视图&索引&存储过程&触发器

    @ 目录 视图是什么 视图的优点 1) 定制用户数据,聚焦特定的数据 2) 简化数据操作 3) 提高数据的安全性 4) 共享所需数据 5) 更改数据格式 6) 重用 SQL 语句 MySQL创建视图 ...

  2. Spark SQL dropDuplicates

    spark sql 数据去重 在对spark sql 中的dataframe数据表去除重复数据的时候可以使用dropDuplicates()方法 dropDuplicates()有4个重载方法 第一个 ...

  3. springcloud高级

    第一章 负载均衡 Ribbon   (Spring Cloud 高级)   一. Ribbon 在微服务中的作用   1 什么是 Ribbon   1.Ribbon 是一个基于 Http 和 TCP ...

  4. CRMEB小程序商城首页强制在微信中打开解决办法

    先说一下,这也算不上二开,小小修改一下而已. CRMEB安装完成后,PC端直接打开首页,真是一言难尽~ 然后,我就想了,用手机浏览器或者PC浏览器直接打开首页也没啥用,干脆直接强制在微信中打开算了! ...

  5. 利用分块传输吊打所有WAF--学习笔记

    在看了bypassword的<在HTTP协议层面绕过WAF>之后,想起了之前做过的一些研究,所以写个简单的短文来补充一下文章里“分块传输”部分没提到的两个技巧. 技巧1 使用注释扰乱分块数 ...

  6. Unity插件介绍——Odin

    今天把玩了一款最近的热门插件——“Odin - Inspector and Serializer”,其功能强大到让人无语,简直是开发利器,屠龙宝刀!它的功能是扩展Inspector显示,它重写和增加了 ...

  7. Currency Exchange(SPFA判负环)

    Several currency exchange points are working in our city. Let us suppose that each point specializes ...

  8. 编程体系结构(01):Java编程基础

    一.数据类型 1.基础类型 整型:byte .short .int .long 浮点型:float.double 字节型:char 2.包装类型 Byte,Short,Integer,Long Flo ...

  9. Mybatis源码学习第八天(总结)

    源码学习到这里就要结束了; 来总结一下吧 Mybatis的总体架构 这次源码学习我们,学习了重点的模块,在这里我想说一句,源码的学习不是要所有的都学,一行一行的去学,这是错误的,我们只需要学习核心,专 ...

  10. Hint usenl usage /*+ leading(emp,dept) usenl(emp) */

    SQL> select /*+ leading(emp,dept) usenl(emp) */ emp.*,dept.* from tb_emp03 emp,tb_dept03 dept whe ...