C# 实现UDP打洞通信(一)

Wesley13
• 阅读 469

最近研究了一下网络打洞的相关技术,TCP的方式据说可行性不高,各种困难,因此决定采用UDP(UDP是什么就不解释了)的方式。

原理:

  我们都知道局域网内的主机想要访问外网的服务器是比较容易的,比如浏览器输入www.baidu.com就可以访问到百度的服务器,但是如果在局域网的主机部署一个服务,让外网的机器进行访问一般是无法访问的,因为外部访问的请求会被路由器给阻碍掉了,这是为什么呢?

  比如我内网的主机IP是192.168.1.128,我访问外网的服务器的时候系统会自动给我的访问分配端口(也可以自定义端口),我对外的访问请求会经过路由器,路由器又会分配一个对外的端口,如果还有外部网络路由器,那么每一层都会分配一个独立的对外端口,一直到最终处于公网的路由器通过公网的IP及分配的端口对外部的服务器发起访问请求,服务器收到请求的同时会得到我处于公网的路由器的IP及分配的端口,然后将请求的反馈结果发送给我,反馈的信息会发到我的公网IP及端口,然后路由器内部再逐层向内发送给对应的IP和端口最终到达发起请求的应用程序。

  如内网主机(192.168.1.128:12345)访问外网服务器(111.110.213.99:15000),那么实际的请求过程是这样的:内网主机(192.168.1.128:12345)发起请求,请求通过路由器A(假设只有一个路由器),路由器为内网的(192.168.1.128:12345)绑定一个动态的端口(18876)并通过路由器的外网IP(120.145.15.87:18876)访问外网服务器(111.110.213.99:15000),服务器收到请求后发送反馈数据给路由器(120.145.15.87:18876),路由器再根据记录的列表中18876端口绑定的内网地址,将信息转发给内网主机(192.168.1.128:12345),于是主机就收到外部的信息了。

  注意,如果内网没有向外部访问,那么路由器就没有分配(18876)这个端口,那么外部发来的数据会被路由器丢弃掉,我们通过先连接服务器,服务器收到的(18876)并使用该端口或将该端口发送给其它客户端使用,那么这个端口其实就是我们打的一个(洞)。

  由于一些原因没能用多台内网机器进行试验,只是简单的通过内网主机和外网的服务器进行的试验,下面贴上代码:

  1 using System;
  2 using System.Net;
  3 using System.Net.Sockets;
  4 using System.Text;
  5 using System.Threading;
  6 
  7 namespace P2MP
  8 {
  9     class MainClass
 10     {
 11         /// <summary>
 12         /// 用于UDP发送的网络服务类
 13         /// </summary>
 14         private static UdpClient udpcSend = null;
 15 
 16         static IPEndPoint localIpep = null; 
 17 
 18         public static void Main(string[] args)
 19         {
 20             Console.Write("IP:");
 21             string ip = Console.ReadLine();
 22             Console.Write("Port:");
 23             int port = int.Parse(Console.ReadLine());
 24             localIpep = new IPEndPoint(IPAddress.Parse(ip), port); // 本机IP,指定的端口号
 25 
 26             udpcSend = new UdpClient(localIpep);
 27 
 28             StartReceive();
 29 
 30             // 实名发送
 31             string msg = null;
 32             while ((msg = Console.ReadLine()) != null)
 33             {
 34                 if ("stop" == msg)
 35                 {
 36                     StopReceive();
 37                     udpcSend.Close();
 38                 }
 39                 else
 40                 {
 41                     //string[] arr = Console.ReadLine().Split(' ');
 42                     Thread thrSend = new Thread(SendMessage);
 43                     thrSend.Start(msg);
 44                 }
 45             }
 46             Console.ReadKey();
 47         }
 48 
 49         /// <summary>
 50         /// 发送信息
 51         /// </summary>
 52         /// <param name="obj"></param>
 53         private static void SendMessage(object obj)
 54         {
 55             try
 56             {
 57                 string message = obj.ToString();
 58                 string[] array = message.Split(' ');
 59                 IPAddress iPAddress = IPAddress.Parse(array[0]);
 60                 int port = int.Parse(array[1]);
 61                 byte[] sendbytes = Encoding.Unicode.GetBytes(array[2]);
 62                 IPEndPoint remoteIpep = new IPEndPoint(iPAddress, port); // 发送到的IP地址和端口号
 63                 udpcSend.Send(sendbytes, sendbytes.Length, remoteIpep);
 64             }
 65             catch{}
 66         }
 67 
 68         /// <summary>
 69         /// 开关:在监听UDP报文阶段为true,否则为false
 70         /// </summary>
 71         static bool IsUdpcRecvStart = false;
 72         /// <summary>
 73         /// 线程:不断监听UDP报文
 74         /// </summary>
 75         static Thread thrRecv;
 76 
 77         private static void StartReceive()
 78         {
 79             if (!IsUdpcRecvStart) // 未监听的情况,开始监听
 80             {
 83                 thrRecv = new Thread(ReceiveMessage);
 84                 thrRecv.Start();
 85                 IsUdpcRecvStart = true;
 86                 Console.WriteLine("UDP监听器已成功启动");
 87             }
 88         }
 89 
 90         private static void StopReceive()
 91         {
 92             if (IsUdpcRecvStart)
 93             {
 94                 thrRecv.Abort(); // 必须先关闭这个线程,否则会异常
 96                 IsUdpcRecvStart = false;
 97                 Console.WriteLine("UDP监听器已成功关闭");
 98             }
 99         }
100 
101         /// <summary>
102         /// 接收数据
103         /// </summary>
104         /// <param name="obj"></param>
105         private static void ReceiveMessage(object obj)
106         {
108             while (IsUdpcRecvStart)
109             {
110                 try
111                 {
112                     byte[] bytRecv = udpcSend.Receive(ref localIpep);
113                     string message = Encoding.Unicode.GetString(bytRecv, 0, bytRecv.Length);
114                     Console.WriteLine(string.Format("{0}[{1}]", localIpep, message));
115                 }
116                 catch (Exception ex)
117                 {
118                     Console.WriteLine(ex.Message);
119                     break;
120                 }
121             }
122         }
123     }
124 }

  可以同时在多个内网主机运行,并且保证其中有一个实在外网的服务器上运行,启动后输入本机的IP和使用的端口,当所有机器都显示“UDP监听器已成功启动”后,分别使用内网程序向 服务器IP地址[空格]端口号[空格]消息内容如:"188.90.9.145 12345 hello你好",发送消息给服务器,服务器收到的消息上附带客户端发来的对外端口,这时候就知道各个客户端的对外IP和端口了,各个主机想要给另一台主机发消息只要从服务器上看其它客户端的IP和端口,并通过“IP 端口 消息”的格式发送消息,网络良好不丢包的情况下就能发送进去了。

  由于路由器会定时销毁记录的列表,因此还需要保持客户端跟服务器之间的心跳,比如每10秒发送一个消息,服务器端将各个客户端最新的列表保存下来。

  暂时先贴出简单的代码,后续打算开发一个P2P文件服务,代码逐步完善中。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这