LoginSignup
17
11

More than 3 years have passed since last update.

UnityでUDPを使ったP2Pチャットサンプル(STUNを使ったNAT Traversal)

Last updated at Posted at 2018-12-04

はじめに

Unityには、connected gameだとかUNETだとかPhotonだとかいろいろな通信技術がありますが、
もしオレオレ実装するならどうすればいいかというただのネタ記事です。
このまま使うのはやめましょう...
パケットロス等の障害を考慮していません
NAT Traversalの参考にはなるはずです

理論はさておきとりあえずやってみよう

インポート手順

STUNクライアントを実装するのは大変なので、既に作成されているC#のサンプルをインポートします。
このプロジェクト(https://www.codeproject.com/Articles/18492/STUN-Client)
からダウンロードしてきて、その中にある Net/Net/STUNをインポートしましょう。

サンプルコード

Chat.cs
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;

using UnityEngine;
using UnityEngine.UI;

using LumiSoft.Net.STUN.Client;

public class Chat : MonoBehaviour
{
    public int BindPort = 10000;
    public bool isStun;

    private IPEndPoint MyIPEndPoint;

    [SerializeField]
    private Text ChatLog;
    [SerializeField]   
    private Button ButtonEnterChat;
    [SerializeField]
    private InputField InputChat;
    [SerializeField]
    private InputField InputAddress;
    [SerializeField]
    private InputField InputPort;
    [SerializeField]
    private Button ButtonEnterAddress;

    [SerializeField]
    private Text MyEndPoint;

    private UdpClient UdpClient;

    private IPEndPoint ConnectIPEndPoint = new IPEndPoint(0,0);

    private bool isConnect;

    private async void Start()
    {
        UdpClient = new UdpClient(BindPort);

        if (!isStun)
        {
            //Private IP を取る手段 もうちょいいい方法求む
            using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.IPv6HopByHopOptions))
            {
                socket.Connect("8.8.8.8", 65530);
                IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint;
                MyIPEndPoint = endPoint;
                MyIPEndPoint.Port = BindPort;
            }
        }

        if (isStun)
        {
            STUN(UdpClient);
        }


        MyEndPoint.text = MyIPEndPoint.ToString();

        ButtonEnterAddress.onClick.AddListener(Connect);
        ButtonEnterChat.onClick.AddListener(Send);
        await ReceiveLoop();
    }

    private void STUN(UdpClient client)
    {
        // Query STUN server
        //GoogleのSTUNサーバーから自分のグローバルIPアドレスとNAPTで変換されたポートを取得する
        STUN_Result result = STUN_Client.Query("stun.l.google.com", 19302, client.Client);
        //もし自分のネット環境がUDPをブロックする場合
        if (result.NetType == STUN_NetType.UdpBlocked)
        {
            Debug.LogError("UDP blocked");
        }
        else
        {
            //取得に成功した場合
            Debug.Log(result.NetType.ToString());
            IPEndPoint publicEP = result.PublicEndPoint;
            Debug.Log("Global IP:" + publicEP.Address);
            Debug.Log("NAT Port:" + publicEP.Port);
            MyIPEndPoint = publicEP;
        }
    }

    private async Task ReceiveLoop()
    {
        for (; ;)
        {
            var result = await UdpClient.ReceiveAsync();
            //接続先以外のパケットが流れてきたらブロックする
            if (result.RemoteEndPoint.Address != ConnectIPEndPoint.Address)
            {
                //接続要求であればそのまま許可する
                if (Encoding.UTF8.GetString(result.Buffer) == "e:Connect")
                {
                    ConnectIPEndPoint = result.RemoteEndPoint;
                    ChatLog.text += "\n Connect Request";
                    var bytesData = Encoding.UTF8.GetBytes("e:Accept");
                    UdpClient.Send(bytesData, bytesData.Length, ConnectIPEndPoint);
                    isConnect = true;
                }
                // 接続許可であればそのまま通信しはじめる
                else if (Encoding.UTF8.GetString(result.Buffer) == "e:Accept")
                {
                    ConnectIPEndPoint = result.RemoteEndPoint;
                    ChatLog.text += "\n Connect Start";
                    isConnect = true;
                }
                else
                {
                    Debug.Log("Block");
                    ReceiveCallback(result.Buffer);
                }
            }
            else
            {
                Debug.Log(result.RemoteEndPoint.ToString());
                ReceiveCallback(result.Buffer);
            }
        }
    }

    private async void Connect()
    {
        while (!isConnect)
        {
            Debug.Log("Connecting...");
            var bytesData = Encoding.UTF8.GetBytes("e:Connect");
            UdpClient.Send(bytesData, bytesData.Length, new IPEndPoint(IPAddress.Parse(InputAddress.text), int.Parse(InputPort.text)));
            await Task.Delay(1000);
        }
    }

    public async void Send()
    {
        var bytesData = Encoding.UTF8.GetBytes(InputChat.text);
        await UdpClient.SendAsync(bytesData, bytesData.Length, ConnectIPEndPoint);
        Debug.Log("Send:" + InputChat.text);
        ChatLog.text += "\n My:" + InputChat.text;
        InputChat.text = "";
    }

    public void ReceiveCallback(byte[] receiveBytes)
    {
        string receiveString = Encoding.UTF8.GetString(receiveBytes);
        if (receiveString == null || receiveString == "")
        {
            return;
        }
        if(receiveString == "e:Accept"){
            Debug.Log("Accept: " + receiveString);
            ChatLog.text += "\n Connect Accept";
        }
        Debug.Log("Received: " + receiveString);

        ChatLog.text += "\n Other:" + receiveString;
    }
}
17
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
11