实现UDP服务端和客户端

UDP是一种无连接的轻量级传输层协议,UDP不需要建立连接、无需确认消息送达,也没有重传机制,这使得UDP非常适合低延迟、高吞吐量的应用场景,如实时音视频传输、在线游戏等。这篇笔记我们简单介绍如何在Go语言中实现UDP服务端和客户端。

实现UDP服务端

单线程UDP服务端

最简单的单线程UDP服务端实现非常简单,UDP是无连接的,它以UDP数据包为传输单位,我们只需要写一个循环监听收到的UDP数据包并逐个处理。下面是一个简单的单线程UDP服务端实现。

package main

import (
    "log/slog"
    "net"
)

func main() {
    // 启动UDP服务端监听UDP端口
    addr, err := net.ResolveUDPAddr("udp", "192.168.1.100:8000")
    if err != nil {
        slog.Error(err.Error())
        return
    }
    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        slog.Error(err.Error())
        return
    }
    defer func(conn *net.UDPConn) {
        err := conn.Close()
        if err != nil {
            slog.Error(err.Error())
            return
        }
    }(conn)
    slog.Info("UDP server listening on 192.168.1.100:8000...")
    for {
        // 从UDP套接字等待并读取数据
        buffer := make([]byte, 512)
        n, addr, err := conn.ReadFromUDP(buffer)
        if err != nil {
            slog.Error(err.Error())
            continue
        }
        data := string(buffer[:n])
        slog.Info("[" + addr.String() + "] " + data)
        // 向套接字写入数据
        if _, err := conn.WriteToUDP([]byte("Echo: "+data), addr); err != nil {
            slog.Error(err.Error())
            continue
        }
    }
}

代码中,我们在192.168.1.100:8000上启动UDP服务端,随后编写了一个无限循环,循环中从套接字等待数据包到达,并附加一些字符后原样返回给客户端。此外,代码中我们固定读取512字节,因为这里我们约定发送的UDP数据包最大不超过512字节,如果超过则丢弃。

为什么是512字节?我们知道UDP协议数据包的Length字段为16位,因此UDP数据包最大长度可为65535字节,但实际上以太网规定了数据链路层单个帧可以传输的最大字节数(MTU),如果UDP数据包大小超过这个MTU值将被分片,接收端必须将分片重组才能交给上层,这样UDP传输的效率不高,此外UDP是不可靠传输协议,被分片后重组失败将丢失整个数据包,因此UDP数据包太长可能出现可靠性迅速下降的状况,因此我们应该尽量保持让UDP数据包不分片直接发送。MTU最大值为1500字节,不过很多网络设备使用默认值576字节,这个值再减去IP头部和UDP头部,512字节就是一个比较保守的UDP数据包大小的估计值。

单线程的UDP服务端用于一些简单的通信场景,比如单对单的通信等,这里我们并没有启动新的协程处理数据,因此如果每个数据包的处理速度较慢或处于高并发的状态下,服务端的网络协议栈缓冲区中将可能产生积压,当缓冲区写满时将发生丢包。如果对并发性能要求较高,我们也可以在循环中每次收到数据包时,启动新协程处理。

多协程UDP服务端

下面代码我们仍单线程循环获取UDP数据包,但收到数据包后我们启动新协程处理数据,这样实现的UDP服务端相较于单线程服务端具有更好的性能。

package main

import (
    "log/slog"
    "net"
)

func handleConn(conn *net.UDPConn, addr *net.UDPAddr, bytes []byte) {
    data := string(bytes)
    slog.Info("[" + addr.String() + "] " + data)
    if _, err := conn.WriteToUDP([]byte("Echo: "+data), addr); err != nil {
        slog.Error(err.Error())
        return
    }
}

func main() {
    // 启动UDP服务端监听UDP端口
    addr, err := net.ResolveUDPAddr("udp", "192.168.1.100:8000")
    if err != nil {
        slog.Error(err.Error())
        return
    }
    conn, err := net.ListenUDP("udp", addr)
    if err != nil {
        slog.Error(err.Error())
        return
    }
    defer func(conn *net.UDPConn) {
        err := conn.Close()
        if err != nil {
            slog.Error(err.Error())
            return
        }
    }(conn)
    slog.Info("UDP server listening on 192.168.1.100:8000...")
    for {
        // 从UDP套接字等待并读取数据
        buffer := make([]byte, 512)
        n, addr, err := conn.ReadFromUDP(buffer)
        if err != nil {
            slog.Error(err.Error())
            continue
        }
        go handleConn(conn, addr, buffer[:n])
    }
}

实际开发中,我们还可以使用更多的手段来进一步提高服务端的并发性能,例如服务端动态分配UDP端口等。

测试UDP服务端

运行服务端后,我们可以使用NetCat对其进行测试。

nc -u 192.168.1.100 8000

实现UDP客户端

下面代码我们实现了UDP客户端。

package main

import (
    "fmt"
    "log/slog"
    "net"
)

func main() {
    // 准备UDP套接字
    addr, err := net.ResolveUDPAddr("udp", "192.168.1.100:8000")
    if err != nil {
        slog.Error(err.Error())
        return
    }
    conn, err := net.DialUDP("udp", nil, addr)
    if err != nil {
        slog.Error(err.Error())
        return
    }
    defer func(conn *net.UDPConn) {
        err := conn.Close()
        if err != nil {
            slog.Error(err.Error())
            return
        }
    }(conn)
    for {
        // 循环从标准输入读取内容
        var input string
        _, err := fmt.Scanln(&input)
        if err != nil {
            slog.Error(err.Error())
            return
        }
        bytes := []byte(input)
        if len(bytes) > 512 {
            slog.Error("Input size too big!")
            return
        }
        // 通过套接字发送数据给服务端
        _, err = conn.Write(bytes)
        if err != nil {
            slog.Error(err.Error())
            return
        }
        // 读取服务端响应
        buffer := make([]byte, 512)
        n, _, err := conn.ReadFromUDP(buffer)
        slog.Info(string(buffer[:n]))
    }
}

代码中,我们编写了一个循环并读取控制台输入,每次读取一行输入后,我们将其作为数据发送给服务端然后等待服务端响应,最后将服务端响应输出到控制台。我们可以用刚才编写的UDP服务端来测试UDP客户端。

关于使用UDP进行可靠传输

UDP并不能保证数据包一定到达,也不能保证数据包按顺序到达,因此,在使用UDP时,我们可能需要额外对数据包的丢失、乱序等问题进行处理,这本质上是基于UDP设计一种可靠的应用层通信协议,但实现这些逻辑就相对复杂了,我们也很难保证自己写的协议实现足够健壮且没有Bug。实际开发中,我们可以选择使用KCP、QUIC等基于UDP的成熟协议来实现功能,Go语言标准库没有内置这些实现,有关这些内容将在其它章节进行介绍。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap