メモリ使用量を確認するプログラム VC++ / Ruby

#include <iostream>
#include <windows.h>

int main()
{
    std::locale::global(std::locale("japanese"));

    MEMORYSTATUSEX statex;
    statex.dwLength = sizeof(statex);
    if (GlobalMemoryStatusEx(&statex) == 0) //関数が失敗した時
    {
        return 0;
    }

    wprintf(L"物理メモリ(ullTotalPhys)    %15llu bytes\n", statex.ullTotalPhys);
    wprintf(L"物理メモリ(ullAvailPhys)    %15llu bytes\n", statex.ullAvailPhys);
    wprintf(L"仮想メモリ(ullTotalVirtual) %15llu bytes\n", statex.ullTotalVirtual);
    wprintf(L"仮想メモリ(ullAvailVirtual) %15llu bytes\n", statex.ullAvailVirtual);
}
require "fiddle/import"
require 'fiddle/types'

module WIN32API
  extend Fiddle::Importer
  dlload 'C:\\Windows\\System32\\kernel32.dll'
  include Fiddle::Win32Types

  extern 'BOOL GlobalMemoryStatusEx(void *)'

  MEMORYSTATUSEX = struct([
    "unsigned int  dwLength",
    "unsigned int  dwMemoryLoad",
    "unsigned long long ullTotalPhys",
    "unsigned long long ullAvailPhys",
    "unsigned long long ullTotalPageFile",
    "unsigned long long ullAvailPageFile",
    "unsigned long long ullTotalVirtual",
    "unsigned long long ullAvailVirtual",
    "unsigned long long ullAvailExtendedVirtual",
  ])
end

mem = WIN32API::MEMORYSTATUSEX.malloc
mem.dwLength = WIN32API::MEMORYSTATUSEX.size

WIN32API.GlobalMemoryStatusEx(mem.to_ptr)
puts "物理メモリ(ullTotalPhys)     #{"%15d" % mem.ullTotalPhys} byte"
puts "物理メモリ(ullAvailPhys)     #{"%15d" % mem.ullAvailPhys} byte"
puts "仮想メモリ(ullTotalVirtual)  #{"%15d" % mem.ullTotalVirtual} byte"
puts "仮想メモリ(ullAvailVirtual)  #{"%15d" % mem.ullAvailVirtual} byte"
puts "仮想メモリ(ullTotalPageFile) #{"%15d" % mem.ullTotalPageFile} byte"
puts "仮想メモリ(ullAvailPageFile) #{"%15d" % mem.ullAvailPageFile} byte"

ESP32-WROOM-32 で ILI9341 に SDから読みだした PNG画像を連続描画【マルチコア使用】

戦略

描画とSDからの読み出しを別々のコアでやることで並列に動作させてできるだけアイドルを減らす。

画像の前半を読んだらセマフォを開放。前半の描画を行っている間に次の後半の描画部分を読みだす。 描画のほうが早いので読み出しの速度分のFPSがでるはず。

コード

#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include "TFT_eSPI.h"

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library
SPIClass sd_HSPI(HSPI);
#define HSPI_SCK  14
#define HSPI_MISO 35
#define HSPI_MOSI 13
#define HSPI_SS   15
#define SDSPEED   16000000
boolean sdHasError;

SemaphoreHandle_t xSemaphore1;
SemaphoreHandle_t xSemaphore2;
SemaphoreHandle_t xSemaphore3;

static uint8_t data1[120 * 320];
static uint8_t data2[120 * 320];

void testOriginalData() {
  xSemaphoreTake(xSemaphore1, portMAX_DELAY);
  tft.pushImage(0,   0, 320, 120, data1);
  xSemaphoreGive(xSemaphore1);
  
  xSemaphoreTake(xSemaphore2, portMAX_DELAY);
  tft.pushImage(0, 120, 320, 120, data2);
  xSemaphoreGive(xSemaphore2);
}

bool readFile(fs::FS &fs, const char * path){
  if(sdHasError) {
    return false;
  }

  File file = fs.open(path);
  if(!file){
      Serial.println("Failed to open file for reading");
      return false;
  }

  xSemaphoreTake(xSemaphore1, portMAX_DELAY);
  xSemaphoreGive(xSemaphore3);
  file.read(data1, 120 * 320);
  xSemaphoreGive(xSemaphore1);
  
  xSemaphoreTake(xSemaphore2, portMAX_DELAY);
  file.read(data2, 120 * 320);
  xSemaphoreTake(xSemaphore3, portMAX_DELAY);
  xSemaphoreGive(xSemaphore2);

  file.close();
  return true;
}

void task0(void* arg) {
  char buf[255];
  while(1) {
    for(int i = 1; i <= 100; i++) {
      sprintf(buf, "/output/%04d.dat", i);
      readFile(SD, buf);
      delay(1);
    }
  }
}

void setup()
{
// Setup the LCD
  tft.init();
  tft.setRotation(1);

  sd_HSPI.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_SS);

  Serial.begin(115200);
  delay(100);

  if(!SD.begin(HSPI_SS, sd_HSPI, 12000000L)){
      Serial.println("Card Mount Failed");
      sdHasError = true;
  } else {
    Serial.println("Card Mount Succeeded");
  }

  xSemaphore1 = xSemaphoreCreateMutex();
  xSemaphore2 = xSemaphoreCreateMutex();
  xSemaphore3 = xSemaphoreCreateBinary();

  xTaskCreatePinnedToCore(task0, "Task0", 4096, NULL, 1, NULL, 0);
  tft.fillScreen(TFT_BLACK);
}

void loop()
{
  xSemaphoreTake(xSemaphore3, portMAX_DELAY);
  xSemaphoreGive(xSemaphore3);

  if (sdHasError) {
    if(!SD.begin(HSPI_SS, sd_HSPI, SDSPEED)){
        Serial.println("Card Mount Failed");
        delay(1000);
        return;
    } else {
      Serial.println("Card Mount Succeeded");
      sdHasError = false;
    }
  }
  
  //randomSeed(1234); // This ensure test is repeatable with exact same draws each loop
  char buf[255];
  int x, x2;
  int y, y2;
  int r;
  
// Clear the screen and draw the frame
  static unsigned long old_time;
  old_time = millis();
  testOriginalData();
  Serial.print((float)1/(millis()-old_time)*1000);
  Serial.println(" fps");
}

結果

6.80 ~ 7.50 fps になった。

ESP32-WROOM-32 で ILI9341 に SDから読みだした PNG画像を連続描画【TFT eSPI使用】

TFT eSPI を使ってみた。

コード

#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include "TFT_eSPI.h"

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library
SPIClass sd_HSPI(HSPI);
#define HSPI_SCK  14
#define HSPI_MISO 35
#define HSPI_MOSI 13
#define HSPI_SS   15

static uint8_t data1[120 * 320];
static uint8_t data2[120 * 320];

void testOriginalData() {
  tft.pushImage(0,   0, 320, 120, data1);
  tft.pushImage(0, 120, 320, 120, data2);
}

bool readFile(fs::FS &fs, const char * path){
    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return false;
    }

    file.read(data1, 120 * 320);
    file.read(data2, 120 * 320);
    file.close();
    return true;
}

void setup()
{
// Setup the LCD
  tft.init();
  tft.setRotation(1);

  sd_HSPI.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_SS);

  Serial.begin(115200);
  delay(100);

  if(!SD.begin(HSPI_SS, sd_HSPI, 12000000L)){
      Serial.println("Card Mount Failed");
      return;
  }
}

void loop()
{
  //randomSeed(1234); // This ensure test is repeatable with exact same draws each loop
  char buf[255];
  int x, x2;
  int y, y2;
  int r;
  
// Clear the screen and draw the frame
  tft.fillScreen(TFT_BLACK);
  unsigned long old_time;
  for(int i = 1; i <= 100; i++) {
    sprintf(buf, "/output/%04d.dat", i);
    old_time = millis();
    Serial.println(buf);
    if(readFile(SD, buf)) {
      // put your main code here, to run repeatedly:
      testOriginalData();
    }
    Serial.print((float)1/(millis()-old_time)*1000);
    Serial.println(" fps");
  }
}

結果

約 5.80 fps になった。

SDの読み込みが遅いのかもしれない。

参考

www.youtube.com

ESP32-WROOM-32 で ILI9341 に SDから読みだした PNG画像を連続描画【遅い】

回路

f:id:bamch0h:20210107223349j:plain

ソース

#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

// For the Adafruit shield, these are the default.
#define TFT_RST 16
#define TFT_DC 17
#define TFT_CS 5

// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

static uint8_t data1[120 * 320];
static uint8_t data2[120 * 320];

void testOriginalData() {
  byte c;
  uint16_t g;

  tft.startWrite();
  tft.setAddrWindow(0, 0, 320, 240);
  for(int y = 0;y < 120; y++) {
    for(int x = 0;x < 320; x++) {
      c = data1[y * 320 + x];
      g = (c>>3)<<11 | (c>>2)<<5 | c>>3;
      tft.SPI_WRITE16(g);
    }
  }
  for(int y = 0;y < 120; y++) {
    for(int x = 0;x < 320; x++) {
      c = data2[y * 320 + x];
      g = (c>>3)<<11 | (c>>2)<<5 | c>>3;
      tft.SPI_WRITE16(g);
    }
  }
  tft.endWrite();  
}

bool readFile(fs::FS &fs, const char * path){
    File file = fs.open(path);
    if(!file){
        Serial.println("Failed to open file for reading");
        return false;
    }

    file.read(data1, 120 * 320);
    file.read(data2, 120 * 320);
    file.close();
    return true;
}

SPIClass SPI2(HSPI);
#define HSPI_SCK  14
#define HSPI_MISO 35
#define HSPI_MOSI 13
#define HSPI_SS   15

void setup() {
  tft.begin();
  tft.setRotation(1);
  
  SPI2.begin(HSPI_SCK, HSPI_MISO, HSPI_MOSI, HSPI_SS);

  Serial.begin(115200);
  delay(100);

  if(!SD.begin(HSPI_SS, SPI2, 12000000L)){
      Serial.println("Card Mount Failed");
      return;
  }
}

void loop() {
  tft.fillScreen(ILI9341_BLACK);
  char buf[255];
  unsigned long old_time;
  for(int i = 1; i <= 100; i++) {
    sprintf(buf, "/output/%04d.dat", i);
    old_time = millis();
    Serial.println(buf);
    if(readFile(SD, buf)) {
      // put your main code here, to run repeatedly:
      testOriginalData();
    }
    Serial.print((float)1/(millis()-old_time)*1000);
    Serial.println(" fps");
  }
}

課題

4.5 fps しか出てないのでどうにかしたい。

C言語ファイルをNMAKEする為の最小Makefile

タイトルはちょっと盛った。

C言語Hello WorldのファイルをVisual Studioコンパイラでmakeする

#include "stdio.h"

void main(void) {
    printf("Hello World\n");
}

参考にしたサイト

makefile と nmake ~ makefile を読み解く - C/C++ による Windows プログラミング入門講座 - C/C++ 入門

TARGETNAME=test_make
OUTDIR=obj
CC=cl.exe
LINK=link.exe

ALL: $(OUTDIR)\$(TARGETNAME).exe

$(OUTDIR) :
  @if not exist $(OUTDIR) mkdir $(OUTDIR)

CPPFLAGS=\
  /nologo\
  /W3\
  /Fo"$(OUTDIR)\\"\
  /Fd"$(OUTDIR)\\"\
  /c\
  /Zi\
  /D_WIN32_WINNT=0x0600\
  /DUNICODE\
  /D_UNICODE

LINK32_FLAGS=\
  /nologo\
  /subsystem:console\
  /pdb:"$(OUTDIR)\$(TARGETNAME).pdb"\
  /out:"$(OUTDIR)\$(TARGETNAME).exe"\
  /DEBUG

LINK32_OBJS=\
  $(OUTDIR)\$(TARGETNAME).obj

$(OUTDIR)\$(TARGETNAME).exe : $(OUTDIR) $(LINK32_OBJS)
  (LINK) $(LINK32_FLAGS) $(LINK32_OBJS)

.c{$(OUTDIR)}.obj:
  $(CC) $(CPPFLAGS) $<

clean:
  -@erase /Q $(OUTDIR)\*

上記のファイルで obj フォルダ下に test_make.exe というファイルが作成される。

nmake を実行する場合は以下のバッチをたたく

@echo off

call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat"

10進数、16進数、8進数をコンボボックスで切り替えつつ入力チェックもしてくれる実装サンプル

あらすじ

こんな感じのものを実装しました。

f:id:bamch0h:20200720021840g:plain

XAML

<Window x:Class="WpfApp15.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:WpfApp15"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <ControlTemplate x:Key="ValidationTemplate">
            <StackPanel>
                <TextBlock Foreground="Red" Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=adornedelem}" />
                <AdornedElementPlaceholder x:Name="adornedelem" />
            </StackPanel>
        </ControlTemplate>
        <local:HexConverter x:Key="ByteHexConverter" />
    </Window.Resources>
    <StackPanel>
        <DataGrid ItemsSource="{Binding Items, Mode=OneWay}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Index" Binding="{Binding SelectedTypesIndex}" />
                <DataGridTemplateColumn IsReadOnly="True" Header="Types">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox ItemsSource="{Binding Types}" SelectedIndex="{Binding SelectedTypesIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
                <DataGridTemplateColumn Header="Number">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <StackPanel>
                                <TextBlock Margin="20" Width="100">
                                    <TextBlock.Text>
                                        <Binding Path="Number" Converter="{StaticResource ByteHexConverter}" />
                                    </TextBlock.Text>
                                </TextBlock>
                            </StackPanel>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                    <DataGridTemplateColumn.CellEditingTemplate>
                        <DataTemplate>
                            <TextBox Margin="20" Width="100" Validation.ErrorTemplate="{StaticResource ValidationTemplate}">
                                <TextBox.Text>
                                    <Binding Path="Number" Converter="{StaticResource ByteHexConverter}" />
                                </TextBox.Text>
                            </TextBox>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellEditingTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel>
</Window>

MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
using Microsoft.Practices.Prism.Mvvm;
using System.Globalization;

namespace WpfApp15
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new MainWindowVM();
        }
    }

    public class MainWindowVM : BindableBase
    {
        public List<DataGridItem> Items { get; set; }

        public MainWindowVM()
        {
            Items = new List<DataGridItem>()
            {
                new DataGridItem(123),
                new DataGridItem(234),
                new DataGridItem(0),
            };
        }
    }

    public class Number<T> : BindableBase
    {
        public Number(T initValue, string initFormat = "Decimal")
        {
            Value = initValue;
            Format = initFormat;
        }

        private T _value;
        public T Value
        {
            get
            {
                return _value;
            }

            set
            {
                this.SetProperty(ref this._value, value);
            }
        }

        private string format;
        public string Format
        {
            get
            {
                return format;
            }

            set
            {
                this.SetProperty(ref this.format, value);
            }
        }
    }

    public class DataGridItem : BindableBase
    {
        private Number<byte> number;
        public Number<byte> Number
        {
            get
            {
                return number;
            }

            set
            {
                this.SetProperty(ref this.number, value);
            }
        }

        public List<string> Types { get; set; }

        public int selectedTypesIndex;
        public int SelectedTypesIndex
        {
            get
            {
                return selectedTypesIndex;
            }

            set
            {
                this.SetProperty(ref this.selectedTypesIndex, value);
                if (this.Number != null)
                    Number = new Number<byte>(number.Value, Types[selectedTypesIndex]);
            }
        }

        public DataGridItem(byte initValue)
        {
            this.Types = new List<string>() { "Decimal", "Hexadecimal", "Octal" };

            this.Number = new Number<byte>(initValue, Types[SelectedTypesIndex]);

            this.SelectedTypesIndex = 0;
        }
    }

    public class HexConverter : IValueConverter
    {
        private Number<byte> byteNumber;
            
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            byteNumber = (Number<byte>)value;

            switch (byteNumber.Format)
            {
                case "Decimal":
                    return System.Convert.ToString(byteNumber.Value, 10);
                case "Hexadecimal":
                    return byteNumber.Value.ToString("X2");
                case "Octal":
                    return System.Convert.ToString(byteNumber.Value, 8);
                default:
                    return byteNumber.ToString();
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            string strValue = value as string;
            byte resultValue;

            try
            {
                switch (byteNumber.Format)
                {
                    case "Hexadecimal":
                        resultValue = System.Convert.ToByte(strValue, 16);
                        return new Number<byte>(resultValue, byteNumber.Format);
                    case "Octal":
                        resultValue = System.Convert.ToByte(strValue, 8);
                        return new Number<byte>(resultValue, byteNumber.Format);
                    default:
                        resultValue = System.Convert.ToByte(strValue);
                        return new Number<byte>(resultValue, byteNumber.Format);
                }
            }
            catch
            {
                return DependencyProperty.UnsetValue;
            }
        }
    }
}

まとめ

VM側でConvertとValidationをするようにした場合、フォーカスが外れた後にValidationが走るのでエラーが見えないという欠点があってView側でやりたいという気持ちがあった。

View側でConvertを行うとformat単位でCoverterを切り替えたりできないので、Convert()メソッドでNumberクラスを受け取ってクラス内に定義してあるFormatを使って場合分けしている。CovertBackの場合は引数からNumberクラスが受け取れないので、Convert()メソッド時点のNumberクラスを保存しておいて、ConvertBackメソッドで使いまわしている。VM側で変更される場合は注意が必要かもしれないけど、編集中にVM上で変更されることはないはずなので問題ないはず。

ただ、今回はNumberというジェネリクスクラスを作成したけど、そこまでする必要はなかったかも。CoverterBackでフォーマット情報が取れないので、その保存方法としてNumberというクラスを作ったけど、formatをIMultiValueConverterで渡してあげて保存しておくだけでも今回の件は実現できたかもしれない。

UDPでNAT超え

あらすじ

NAT超えという技術を知り、いったいどういうものなのか?を調べて、Goで簡単に実装してみました。

NAT

NATというのはグローバルIPとローカルIPを変換する機構です。グローバルIPの枯渇を防ぐため、ローカルIP以下のデバイスに対して同じグローバルIPを割り振るための仕組み提供します。最近はIPだけではなくポートも変換するNAPTというタイプが主流のようです。

例えば、2台のパソコンがNAT下にぶら下がっていてそれぞれがサーバーにアクセスするような場合は以下のような構成になります。

f:id:bamch0h:20200627184558j:plain

PC1がWEBサーバーにアクセスする時、アクセス要求はNATを経由してインターネットを通じてサーバーに到達します。サーバーは要求を処理し応答を返します。応答はインターネットを経由してNATまで届きます。NATはその応答をPC1に送り、無事に通信ができます。

NATがどのように応答を振り分けるというと、NAT内部にグローバルIPとローカルIPの変換表を持っていてそれをもとに応答を振り分けています。TCP/IPUDP/IPの場合、変換表には以下の情報が記録されます。

PC1がWEBサーバーにアクセスした場合は以下のような情報が変換表に記録されます。

プロトコル 送信元IP 送信元ポート グローバルIP グローバルポート 相手先IP 相手先ポート
TCP 192.168.0.2 49152 1.2.3.4 49152 2.3.4.5 80

サーバー側からの応答は送信先IPと相手先IPが変換表とは逆になって返信されます。NATはそれを考慮して変換表に当てはまるものがあるかを調べ、当てはまるものがあったなら、そのデバイスへ応答を渡します。なかった場合はその時点で破棄されます。

送信元ポートグローバルポートは基本的に同じになります。別々のローカルIPで同じポート番号から通信が来た場合にのみ変換されます。例えば、PC1とPC2が同じ送信元ポートで通信をした場合、ポートを変換せずに変換表に記録した場合、以下のようになります。

プロトコル 送信元IP 送信元ポート グローバルIP グローバルポート 相手先IP 相手先ポート
TCP 192.168.0.2 49152 1.2.3.4 49152 2.3.4.5 80
TCP 192.168.0.3 49152 1.2.3.4 49152 2.3.4.5 80

サーバーからの応答はどちらの要求に対しても 1.2.3.4:49152 に返信されるので、NATは変換表をチェックしたときに1番目にも2番目にも対象のIPがヒットするので応答を振り分けることができません。そこで、以下のようにグローバル側に見せるポートを変換することで、この問題を解決します。

プロトコル 送信元IP 送信元ポート グローバルIP グローバルポート 相手先IP 相手先ポート
TCP 192.168.0.2 49152 1.2.3.4 49152 2.3.4.5 80
TCP 192.168.0.3 49152 1.2.3.4 49153 2.3.4.5 80

こうすることで、PC1の応答は 1.2.3.4:49152 に、PC2の応答は 1.2.3.4:49153 に返されるため、NATは問題なく応答を振り分けることができるというわけです。

ポートマッピング

NATは基本的にグローバル側からアクセスは受け付けません。というのも、グローバル側から唐突に通信が来ても変換表から対応するローカルIPを割り出せないため、すべて破棄されるためです。これだと、ローカルにWEBサーバーを立てても外部に公開できなくて不便ですよね。なので、NATにはポートマッピングという機能があるものがあります。これは、特定のポートにやってきた通信を対応するIPのデバイスに転送する機能です。例えば、以下のようなマッピングテーブルがあったとします。

プロトコル ポート 転送IP
TCP 80 192.168.0.2

この場合、NATにやってきた要求のうち TCPポート80番に来た要求は全て 192.168.0.2 に転送されます。192.168.0.2 にWEBサーバーが起動していれば要求は処理され応答が返ります。

P2P

P2Pとは、サーバーを持たず、クライアント同士が通信する方法のことを指します。サーバー運営コストが少ない、サーバーの処理性能に左右されない等のメリットがあります。ただ、そのままではクライアント間で通信はできないため、古き良きインターネッツ時代にあったP2Pファイル共有ソフトではポート開放(ポートマッピング)を行うことで各ノード間の通信を可能にしていました。一方で、ポート開放はセキュリティリスクが高く、悪意のあるユーザーがそのポートに対してアクセスして悪意のある操作を可能にする可能性があるため、ユーザーとしてはあまりやりたいことではないと思います。そこで出てくるのがNAT超えです。

NAT超え

NAT超えとは、ポート開放せずにP2Pを可能にする技術のことです。クライアントだけではNAT越えは難しいので、STUNサーバーやTURNサーバーを使ってNAT超えを行うのがセオリーのようです。STUNサーバーやTURNサーバーのRFCがあるようです。私はまだ見れていません。。。

UDPでのNAT超え

UDPコネクションレスなので内側からデータを送信するだけでNATはポートを開放します。相手側にはデータは届きませんが応答を待っている状態ですので、相手側からも同じようにデータを送信することで通信が可能となります。ただ、内から外への通信によってポートが自動で開放されるとしても、相手のIPアドレスとポートを知る方法が必要です。

f:id:bamch0h:20200627184529j:plain

IP取得用サーバー

相手のIPアドレスとポートを知るためにサーバーを一つ用意します。そのサーバーに自分のIPアドレスとポートを登録しておき、相手から自由に取得できるようにしておきます。いかに例を示します。

f:id:bamch0h:20200628002505j:plain

  • ① サーバーにPC1のIPを登録
  • ② サーバーにPC2のIPを登録
  • ⓷ PC2のIPを取得
  • ⓸ NATのポート開放のためにPC2に通信する
  • ⑤ PC2に通信したことをサーバーに通知
  • ⑥ サーバーがPC2にPC1が通信したことを通知(合わせてPC1のIPとポートを伝える)
  • ⓻ PC2からPC1に伝えられたポートで通信

このような感じでPC1とPC2の疎通が可能となります。

Goのサンプル

上記を実装したのが以下になります。

クライアント

NAT超え over UDP

サーバー

STUN server over UDP

まとめ

  • UDPでNAT越えをするための理屈を説明しました。
  • Goでのサンプルを実装しました。