本标题的应用场景是C#系统服务端和基于linux的python设备在不同的局域网下通信,通常C#系统端在办公室内部wifi下,设备在室外利用4G上网。
打洞原理网上蛮多的,随便一搜就是好多,实际将如何打洞的确很少。这里需要理论的推荐一篇博客,个人觉得写的很好。
https://blog.csdn.net/sqqyq/article/details/51841579
本人阅读了两遍这篇博客,画了三张草图。花了一星期(中间大部分事件都在代码调试)完成了这个实验。
打洞通信其实很简单
UDP打洞前准备,需要一台公网服务器(辅助打洞),并且已知可用端口。本实验的环境是腾讯云的ubantu.
第一步,进入服务器,修改或者查看IPv4端口可用情况
修改
1.vi /etc/sysctl .config
可以看到这个:
net.ipv4 ip_local_port_range = 10000 65000
后面就是端口的区间可以改成你需要的端口所在的区间。
当然通常我们都不改,直接查看可用端口
2.sysctl -a|grep port_range
第二步,服务器上建立python server,主要用来辅助打洞。host IP为:0.0.0.0 注意不是给你的那个公网/私网IP,也不是127.0.0.1.个人大量尝试出来的,端口选用上面的端口区间内的。(选择后的端口要在控制台配置到安全组,让其可以和外界UDP通信)
接着写代码,语言上选择python,服务器上自带python环境,无需安装,主要区分版本是2.7还是3.2.
这里是个人测试的粗略代码,若需使用还需要看我具体如何传值,主要依靠json。
import socket
import json
byte = 1024
#两个端口要保持一致
port = 32769
host = ""
addr = (host, port)
#创建套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#绑定
sock.bind(addr)
print("waiting to receive messages...")
#创建空字典保存数据
addr_dict = {}
while True:
(data, clientAddr) = sock.recvfrom(byte)
if json_data['info'] == "sendAddr":
# 判断服务器是否已经有了该地址
print("New addr connect,add connection pool")
name = json_data['name']
addr_dict[name] = clientAddr
print("connection pool:{}".format(addr_dict))
text = 'Server got Addr'
data = text.encode('utf-8')
sock.sendto(data, clientAddr)
if json_data['info'] == "getAddr":
name = json_data['name']
print("Client want to {} client addr in the pool".format(name))
otherAddr = addr_dict[name]
data = otherAddr.encode('utf-8')
sock.sendto(otherAddr, clientAddr)
#关闭套接字
sock.close()
第三步,设备端和系统端的编程。我这里的需求是python和c#,需要的按需更改为相应的语言UDP代码。
设备端python代码:选择1连接服务器,等待系统端连接服务器后选2开始发打洞包,打印error后表示可以接收系统端的消息,选择4开启消息监听
import socket
import json
import threading
host = '云服务器公网ip'
port = 32769
addr = (host, port)
otherAddr = ()
byte = 1024
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def myrevc(sock):
while True:
data ,newaddr= sock.recvfrom(byte)
text = data.decode("utf-8")
if text == "getStr":
sock.sendto(bytes("hello,I was Device", "utf-8"), newaddr)
print(text)
while True:
print("1.sendAddr 2.getAddr 4.listen")
data = input('Please choose your choose: ')
if data == "1":
info = {
'Name': 'Device1',
'Info': 'sendAddr'
}
json_info = json.dumps(info)
sock.sendto(bytes(json_info,"utf-8"), addr)
data, addr = sock.recvfrom(byte)
text = data.decode("utf-8")
print(text)
if data == "2":
info = {
'Name': "Device2",
'Info': 'getAddr'
}
json_info = json.dumps(info)
sock.sendto(bytes(json_info,"utf-8"), addr)
data, addr = sock.recvfrom(byte)
jsonData = json.loads(data)
ip = jsonData['ip']
port = jsonData['port']
otherAddr = (ip, port)
try:
sock.sendto(bytes("hai","utf-8"), otherAddr)
data = sock.recvfrom(byte)
text = data.decode("utf-8")
except:
print("error")
if data == "4":
threading._start_new_thread(myrevc, (sock,))
else:
print(data)
sock.close()
C#系统端代码:
WPF界面代码:
<Window x:Class="TestPeanutHull.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestPeanutHull"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBox x:Name="toIP" HorizontalAlignment="Left" Height="23" Margin="94,22,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120"/>
<TextBox x:Name="toPort" HorizontalAlignment="Left" Height="23" Margin="94,45,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="120"/>
<Label Content="对方端口" HorizontalAlignment="Left" Margin="29,52,0,0" VerticalAlignment="Top"/>
<Label Content="对方IP" HorizontalAlignment="Left" Margin="29,22,0,0" VerticalAlignment="Top"/>
<Button Content="向服务发起连接" HorizontalAlignment="Left" Margin="29,104,0,0" VerticalAlignment="Top" Width="75" Click="Button_Click2" RenderTransformOrigin="0.48,2"/>
<Button Content="接收对方IPPort" HorizontalAlignment="Left" Margin="109,104,0,0" VerticalAlignment="Top" Width="105" Click="Button_Click"/>
<TextBlock x:Name="showMsg" HorizontalAlignment="Left" Height="58" Margin="29,151,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="185" Text="Text"/>
<Button Content="发送消息" HorizontalAlignment="Left" Height="21" Margin="252,203,0,0" VerticalAlignment="Top" Width="103" Click="Button_Click_1"/>
<TextBox x:Name="forAnother" HorizontalAlignment="Left" Height="127" Margin="252,52,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="213"/>
<Button Content="保持连接" HorizontalAlignment="Left" Height="27" Margin="34,219,0,0" VerticalAlignment="Top" Width="166" Click="Button_Click_2"/>
</Grid>
</Window>
界面后台代码:
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Timers;
using System.Windows;
namespace TestPeanutHull
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
Thread thrRecv;
private byte[] data = new byte[1024];
private Socket socket;
private IPEndPoint serverIP;
private IPEndPoint otherIP;
public MainWindow()
{
InitializeComponent();
}
//启动UDP服务
private void Button_Click2(object sender, RoutedEventArgs e)
{
//设置服务器IP,PORT
serverIP = new IPEndPoint(IPAddress.Parse("云服务器公网ip"), 32769);
//定义网络类型,数据连接类型和网络协议UDP
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//发送本机地址
Msg msg = new Msg
{
Name = "Device2",
Info = "sendAddr"
};
string jsonData = JsonConvert.SerializeObject(msg);
;
data = Encoding.UTF8.GetBytes(jsonData);
socket.SendTo(data, data.Length, SocketFlags.None, serverIP);
// 创建负责监听的线程;
thrRecv = new Thread(ListenConnecting)
{
IsBackground = true
};
thrRecv.Start();
MessageBox.Show("启动UDP监听");
}
private void ListenConnecting()
{
EndPoint sender = new IPEndPoint(IPAddress.Any, 0);
byte[] data = new byte[1024];
int recv = socket.ReceiveFrom(data, ref sender);
string info = Encoding.UTF8.GetString(data, 0, recv);
//Console.WriteLine("get info" + info);
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
this.showMsg.Text = info;
});
}
/// <summary>
/// 获取另一端IP,地址
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Button_Click(object sender, RoutedEventArgs e)
{
Msg msg = new Msg();
msg.Name = "Device1";
msg.Info = "getAddr";
string jsonData2 = JsonConvert.SerializeObject(msg);
data = Encoding.UTF8.GetBytes(jsonData2);
socket.SendTo(data, data.Length, SocketFlags.None, serverIP);
EndPoint send = new IPEndPoint(IPAddress.Any, 0);
byte[] IPdata = new byte[1024];
int re = socket.ReceiveFrom(IPdata, ref send);
string info = Encoding.UTF8.GetString(IPdata, 0, re);
JObject jo = (JObject)JsonConvert.DeserializeObject(info);
String IPaddr = jo["ip"].ToString();
String port = jo["port"].ToString();
otherIP = new IPEndPoint(IPAddress.Parse(IPaddr), int.Parse(port));
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
this.toIP.Text = IPaddr;
this.toPort.Text = port;
});
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
String dataString = this.forAnother.Text;
socket.SendTo(Encoding.UTF8.GetBytes(dataString), otherIP);
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
this.forAnother.Text = "";
});
}
System.Timers.Timer aTimer = new System.Timers.Timer();
//不停发包
private void Button_Click_2(object sender, RoutedEventArgs e)
{
//到时间的时候执行事件
aTimer.Elapsed += new ElapsedEventHandler(sendHeart);
aTimer.Interval = 10000;
aTimer.AutoReset = true;//执行一次 false,一直执行true
//是否执行System.Timers.Timer.Elapsed事件
aTimer.Enabled = true;
MessageBox.Show("启动UDP心跳");
}
//发送心跳
private void sendHeart(object source, System.Timers.ElapsedEventArgs e)
{
String dataString = "1";
socket.SendTo(Encoding.UTF8.GetBytes(dataString), otherIP);
//MessageBox.Show("发送心跳");
}
private void Button_Click_3(object sender, RoutedEventArgs e)
{
}
}
}
另外涉及到的一个消息实体类,主要用来封装json信息的:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestPeanutHull
{
class Msg
{
private String name;
private String info;
public string Name { get => name; set => name = value; }
public string Info { get => info; set => info = value; }
}
}
第四步,运行代码,按照流程打洞。
同时运行设备和系统代码,python 选择pycharm测试,选择1,然后c#界面点击向服务发起连接按钮,然后点击接收对方IPPort,上面IP和Port有内容表示第一步成功,然后python端选择2开始打洞,打印error,表示包被系统端丢掉,此时系统端可以对设备发起连接。选择4,等待系统端的消息。系统端再text框输入消息点击发送,发送成功,python会打印信息出来。然后就可以点击保持连接按钮保持这个UDP连接。一个简易的UDP打洞就成功了。
运行效果