C#與python UDP打洞通訊
本標題的應用場景是C#系統服務端和基於linux的python裝置在不同的區域網下通訊,通常C#系統端在辦公室內部wifi下,裝置在室外利用4G上網。
打洞原理網上蠻多的,隨便一搜就是好多,實際將如何打洞的確很少。這裡需要理論的推薦一篇部落格,個人覺得寫的很好。
https://blog.csdn.net/sqqyq/article/details/51841579
本人閱讀了兩遍這篇部落格,畫了三張草圖。花了一星期(中間大部分事件都在程式碼除錯)完成了這個實驗。
打洞通訊其實很簡單
UDP打洞前準備,需要一臺公網伺服器(輔助打洞),並且已知可用埠。本實驗的環境是騰訊雲的ubantu.
第一步
修改
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打洞就成功了。
執行效果