UDP 協議簡單瞭解及應用

ciscopuke發表於2021-09-09

UDP 協議簡單瞭解及應用

  • udp 是 User Datagram Protocol 的簡稱,意思是使用者資料包協議。這是一種無連線的傳輸協議,在 OSI(Open System Interconnect,開放系統互聯)參考模型的傳輸層,提供簡單不可靠資訊傳送服務。udp 為應用程式提供無需建立連線就可以傳送封裝的 IP 資料包的方法,只管傳送,甭管對方是否收到,它在 IP 報文的協議號是 17,正式規範是
  • UDP 報文沒有可靠性保證,不確保資料順序和流量控制,有資料直接傳送,無連線無狀態,所以限制少延遲小速度快傳輸效率高,適合快速傳送少量資料,可靠性要求不高的應用。在接收端,udp 把每個訊息段放在佇列,應用程式每次從佇列中讀一個訊息段。udp 雖然能檢測錯誤,但不校正,只是簡單扔掉損壞訊息段或者給程式提供警告資訊
  • 基於以上特點,udp 是一個理想的訊息分發協議,在網路好的環境中,比如同一個網路的主機之間通訊,或者同一主機的多個應用,在網路差的環境中,丟包嚴重,而一些惡劣的檢測場景下,實時抗干擾等要求高,udp 能達到較高通訊速率。常用的場景有:聊天室,投屏資訊顯示,音訊影片等多媒體傳輸。不要求傳輸完整而是要求傳輸速率,即使有損壞也不影響

實踐操作

  • 光說不練假把式,下面我們用 C#程式碼實現一下接收 udp 訊息的服務端,直觀感受一下
  • 祭出文件,使用 UDP 服務
  • 最現成的就是使用UdpClient,可傳送和接收訊息

傳送端

  • 傳送的程式碼比較簡單
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace UdpSender
{
    class Program
    {
        static void Main(string[] args)
        {
            Socket s = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            IPAddress broadcast = IPAddress.Loopback;

            IPEndPoint ep = new IPEndPoint(broadcast, 514);

            Console.WriteLine("請輸入要傳送的內容:");

            while (true)
            {
                byte[] sendbuf = Encoding.UTF8.GetBytes(Console.ReadLine());

                s.SendTo(sendbuf, ep);
            }
        }
    }
}

接收端

  • 接收端同樣簡單,接收方法是一個同步方法,會一直等到接收到訊息才繼續執行
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace Udp
{
    class Program
    {
        static void Main(string[] args)
        {
            var port = 514;
            var server =
                new UdpClient(port);
                // new SocketReceiver(port);
            var remoteEP = new IPEndPoint(IPAddress.Broadcast, port);

            try
            {
                while (true)
                {
                    Console.WriteLine("等待廣播");
                    var bytes = server.Receive(ref remoteEP);
                    var msg = Encoding.UTF8.GetString(bytes);

                    Console.WriteLine($"接收來自 {remoteEP} 的廣播:");
                    Console.WriteLine($"{msg}");
                    Console.WriteLine();
                }
            }
            catch (SocketException e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                server.Close();
            }
        }
    }
}

自定義接收

  • 現在玩點不一樣了,因為傳送接收都比較簡單,自己寫程式碼接收,加深理解,傳送端程式碼同理
  • 參考 UdpClient、Socket 的程式碼,摳出關鍵性程式碼,只需要三步即可
    1. 根據網路型別,socket 接收型別,協力型別獲取一個控制程式碼
    2. 在該控制程式碼上繫結埠,監聽訊息
    3. 從控制程式碼獲取訊息以及客戶端地址資訊
using System;
using System.Net;
using System.Net.Sockets;
using System.Reflection;

namespace Udp
{
    public class SocketReceiver
    {
        private IntPtr _handle;

        public SocketReceiver() : this(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
        {

        }

        public SocketReceiver(int port) : this(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
        {
            var localEP = new IPEndPoint(IPAddress.Any, port);
            Bind(localEP);
        }

        public SocketReceiver(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType)
        {
            Dns.GetHostName(); // 初始化必須
            _handle = Interop.Winsock.WSASocketW(addressFamily, socketType, protocolType, IntPtr.Zero, 0u, SocketConstructorFlags.WSA_FLAG_OVERLAPPED);
        }

        public void Bind(EndPoint localEP)
        {
            var remoteEP = localEP;
            var socketAddress = remoteEP.Serialize();

            var buffer = socketAddress.GetValue("Buffer") as byte[];

            var socketError = Interop.Winsock.bind(_handle, buffer, socketAddress.Size);
        }

        public unsafe byte[] Receive(ref IPEndPoint remoteEP)
        {
            var socketAddress = remoteEP.Serialize();
            var socketAddressBuffer = socketAddress.GetValue("Buffer") as byte[];
            var socketAddressSize = socketAddress.Size;
            var maxSize = 0x10000;
            var buffer = new byte[maxSize];
            var received = 0;

            fixed (byte* pinnedBuffer = &buffer[0])
            {
                received = Interop.Winsock.recvfrom(_handle, pinnedBuffer, maxSize, SocketFlags.None,
                    socketAddressBuffer, ref socketAddressSize);
            }

            socketAddress.SetValue("Buffer", socketAddressBuffer);
            socketAddress.SetValue("InternalSize", socketAddressSize);

            remoteEP = remoteEP.Create(socketAddress) as IPEndPoint;

            // 不返回全部長度,只返回全部接受長度
            if (received < maxSize)
            {
                byte[] newBuffer = new byte[received];
                Buffer.BlockCopy(buffer, 0, newBuffer, 0, received);
                return newBuffer;
            }

            return buffer;
        }

        public void Close()
        {
            GC.SuppressFinalize(this);
        }
    }

    public static class ReflectionHelper
    {
        public static object GetValue(this object obj, string name)
        {
            var value = obj.GetType().InvokeMember(name,
                BindingFlags.Instance | BindingFlags.GetField |BindingFlags.NonPublic,
                null, obj, null);

            return value;
        }

        public static void SetValue(this object obj, string name, object value)
        {
            obj.GetType().InvokeMember(name,
                BindingFlags.Instance | BindingFlags.SetField | BindingFlags.NonPublic,
                null, obj, new[] { value });
        }
    }

    internal static class Interop
    {
        internal static class Winsock
        {
            /// <summary>
            /// 繫結埠
            /// </summary>
            /// <param name="socketHandle"></param>
            /// <param name="socketAddress"></param>
            /// <param name="socketAddressSize"></param>
            /// <returns></returns>
            [DllImport("ws2_32.dll", SetLastError = true)]
            internal static extern SocketError bind([In] IntPtr socketHandle, [In] byte[] socketAddress, [In] int socketAddressSize);

            /// <summary>
            /// 接收
            /// </summary>
            /// <param name="socketHandle"></param>
            /// <param name="pinnedBuffer"></param>
            /// <param name="len"></param>
            /// <param name="socketFlags"></param>
            /// <param name="socketAddress"></param>
            /// <param name="socketAddressSize"></param>
            /// <returns></returns>
            [DllImport("ws2_32.dll", SetLastError = true)]
            internal unsafe static extern int recvfrom([In] IntPtr socketHandle, [In] byte* pinnedBuffer, [In] int len, [In] SocketFlags socketFlags, [Out] byte[] socketAddress, [In] [Out] ref int socketAddressSize);

            /// <summary>
            /// 申請
            /// </summary>
            /// <param name="addressFamily"></param>
            /// <param name="socketType"></param>
            /// <param name="protocolType"></param>
            /// <param name="protocolInfo"></param>
            /// <param name="group"></param>
            /// <param name="flags"></param>
            /// <returns></returns>
            [DllImport("ws2_32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            internal static extern IntPtr WSASocketW([In] AddressFamily addressFamily, [In] SocketType socketType, [In] ProtocolType protocolType, [In] IntPtr protocolInfo, [In] uint group, [In] SocketConstructorFlags flags);
        }
    }

    [Flags]
    internal enum SocketConstructorFlags
    {
        WSA_FLAG_OVERLAPPED = 0x1,
        WSA_FLAG_MULTIPOINT_C_ROOT = 0x2,
        WSA_FLAG_MULTIPOINT_C_LEAF = 0x4,
        WSA_FLAG_MULTIPOINT_D_ROOT = 0x8,
        WSA_FLAG_MULTIPOINT_D_LEAF = 0x10
    }
}
  • 注意上面自定義的接收程式碼包含不安全程式碼,需要在專案檔案設定一下
  <PropertyGroup>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

總結

  • 上面自定義程式碼比較長,經過手動實踐,收穫還是不少的。比如:
    • P/Invoke 操作呼叫系統 api,還有控制程式碼型別 IntPtr 的理解
    • 網路地址的知識,比如傳送地址是192.168.1.255則是廣播訊息到網路段192.168.1的所有主機
    • SocketAddress 套接字地址,不依賴於具體協議,不論是 IPV4 還是 IPV6 都可以表示
    • 反射操作,反射獲取或者設定值,Type.InvokeMember 方法加上 BindingFlags 可描述想要的反射操作
    • 指標操作,獲取 buffer 的記憶體地址
  • 具體除錯的時候發現,服務端繫結監聽埠之後,還沒開始接收資料,客戶端先發資料,服務端後續仍然收到所有資料,這可直觀感受,訊息到達後,存放於緩衝區佇列,等待程式獲取。這一點可有注意於理解NetworkStream網路流,可將接收改為非同步+回撥的方式接收訊息該流的讀取操作,實際就是從系統緩衝區讀取資料。其中還請教了大神,UDP 資料緩衝區資料滿了之後就會有資料丟失的情況,這是因為 udp 沒有流量控制,而 TCP 則不會丟失。其它緩衝區滿的情況還要繼續摸索,不過相關引數還是可以調節的,比如緩衝區大小、TCP 的客戶端連線數等
  • 從最簡單的 udp 協議開始瞭解網路通訊,再逐漸延伸到流量控制、可靠性保證、安全性等方面,各個概念就會相對容易理解,所以 udp 應該是個很不錯的切入點

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4369/viewspace-2823860/,如需轉載,請註明出處,否則將追究法律責任。

相關文章