1. 程式人生 > >C#與python UDP打洞通訊

C#與python UDP打洞通訊

本標題的應用場景是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打洞就成功了。

執行效果