使用QUIC发送文件时突然一端停滞,另一端异常断开,可能是什么原因?

新手上路,请多包涵

我使用quic-go和protobuf开发应用。
使用单向流发送心跳数据,在需要时启动新的stream传输文件数据(一方分块发送数据,等待另外一方发送ack后发送下一块数据)。
然而在程序运行过程中有一端会突然没办法收到数据(一直阻塞在read方法,不会报错,感知不到另外一端已经断开连接和流),另外一端则因为没有数据流量产生断开stream和connection。
我写了一个简单的验证程序(监听本地地址模拟发送数据和ack,Windows环境百分百复现):
message.proto

syntax = "proto3";
option go_package = "./main";
message Message {
  bytes data = 1;
}
message Ack {
  bool ok = 1;
  bool complete = 2;
  int32 received = 3;
}

main.go

//go:generate protoc --go_out=. --go_opt=paths=source_relative message.proto
package main

import (
    "context"
    "crypto/rand"
    "crypto/tls"
    "fmt"
    "github.com/quic-go/quic-go"
    "golang.org/x/sync/errgroup"
    "google.golang.org/protobuf/encoding/protodelim"
    "io"
    "log/slog"
    "net"
    "syscall"
    "time"
)

var cert, _ = tls.LoadX509KeyPair("./certificate.pem", "./private.key")
var quicConfig = &quic.Config{
    MaxConnectionReceiveWindow: 100 << 20, MaxStreamReceiveWindow: 20 << 20, MaxIdleTimeout: time.Second * 30,
}

type transport struct {
    stream io.ReadWriter
    r, w   int
}

func (t *transport) state() string {
    return fmt.Sprintf("r: %d w:%d", t.r, t.w)
}

func (t *transport) Write(p []byte) (i int, err error) {
    i, err = t.stream.Write(p)
    if i > 0 {
        t.w += i
    }
    return
}

func (t *transport) Read(p []byte) (i int, err error) {
    i, err = t.stream.Read(p)
    if i > 0 {
        t.r += i
    }
    return
}

func (t *transport) ReadByte() (byte, error) {
    var b [1]byte
    n, err := t.Read(b[:])
    if n > 0 {
        t.r += n
    }
    return b[0], err
}
func main() {
    slog.SetLogLoggerLevel(slog.LevelDebug)
    eg, ctx := errgroup.WithContext(context.Background())
    eg.Go(func() error {
        return listenAndSend(ctx)
    })
    eg.Go(func() error {
        return connectAndReceive(ctx, 10000)
    })
    if err := eg.Wait(); err != nil {
        slog.Error("error", "err", err)
    } else {
        slog.Info("completed")
    }
}
func setUDPBufferSize(conn *net.UDPConn, size int) error {
    rawConn, err := conn.SyscallConn()
    if err != nil {
        return err
    }
    var sysErr error
    err = rawConn.Control(func(fd uintptr) {
        sysErr = syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUF, size)
    })
    if err != nil {
        return err
    }
    return sysErr
}

func listenAndSend(ctx context.Context) error {
    udpConn, _ := net.ResolveUDPAddr("udp", "127.0.0.1:65533")
    conn, err := net.ListenUDP("udp", udpConn)
    if err != nil {
        return err
    }

    tr := quic.Transport{Conn: conn}
    l, err := tr.Listen(&tls.Config{Certificates: []tls.Certificate{cert}, InsecureSkipVerify: true, ClientAuth: tls.NoClientCert}, quicConfig)
    if err != nil {
        return err
    }
    c, err := l.Accept(ctx)
    if err != nil {
        return err
    }
    defer c.CloseWithError(quic.ApplicationErrorCode(0), "")
    stream, err := c.OpenStreamSync(ctx)
    if err != nil {
        return err
    }
    slog.Info("accept stream", "stream", stream.StreamID())
    defer stream.Close()
    reader := &transport{stream: stream}
    defer func() {
        defer slog.Info("transport", "state", reader.state())
    }()
    m := &Message{Data: make([]byte, 8*1024)}
    ack := &Ack{}
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }
        _, _ = rand.Read(m.Data)
        _, err = protodelim.MarshalTo(reader, m)
        if err != nil {
            slog.Error("write message error", "err", err)
            return err
        } else {
            slog.Debug("write message")
        }
        if err = protodelim.UnmarshalFrom(reader, ack); err != nil {
            slog.Error("read ack error", "err", err)
            return err
        } else {
            slog.Debug("receive ack", "complete", ack.Complete, "received", ack.Received, "ok", ack.Ok)
        }
        if ack.Complete {
            return nil
        }
        time.Sleep(time.Microsecond)
    }
}
func connectAndReceive(ctx context.Context, m int) error {
    addr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:0")
    udpConn, _ := net.ListenUDP("udp", addr)
    targetAddr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:65533")
    conn, err := quic.Dial(ctx, udpConn, targetAddr, &tls.Config{InsecureSkipVerify: true}, quicConfig)
    if err != nil {
        return fmt.Errorf("connect failed %w", err)
    }
    stream, err := conn.AcceptStream(ctx)
    if err != nil {
        return err
    }
    defer stream.Close()
    defer stream.CancelRead(quic.StreamErrorCode(0))
    defer stream.CancelWrite(quic.StreamErrorCode(0))
    tr := &transport{stream: stream}
    defer func() {
        slog.Info("transport", "state", tr.state())
    }()

    var (
        msg Message
        ack Ack
    )
    i := 0
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }
        if err = protodelim.UnmarshalFrom(tr, &msg); err != nil {
            slog.Error("read message error", "err", err)
            return err
        }
        i++
        slog.Info("receive message", "i", i, "size", len(msg.Data))
        ack.Ok, ack.Received, ack.Complete = true, int32(len(msg.Data)), i > m
        if _, err = protodelim.MarshalTo(tr, &ack); err != nil {
            slog.Error("write ack error", "err", err)
            return err
        } else {
            slog.Debug("write ack", "i", i)
        }
        if ack.Complete {
            return nil
        }
    }
}
  1. 尝试调整了MaxConnectionReceiveWindow和MaxStreamReceiveWindow没有效果
  2. 调整UDP缓冲区大小也无效
  3. 在Linux环境编译并运行有效,在我的两个Windows(Windows10)环境百分百复现
阅读 468
avatarAI BotBETA

在你的QUIC(使用quic-go库)和protobuf实现的文件传输应用中,遇到的一端停滞和另一端异常断开的问题可能由多种因素导致。考虑到你在Windows环境下复现了这个问题,而在Linux环境下则没有,这暗示了可能与Windows的网络栈或QUIC库在Windows上的特定行为有关。以下是一些可能的原因和解决方案:

1. Windows网络栈和QUIC兼容性问题

  • Windows网络配置:Windows的网络堆栈可能与QUIC的某些特性不完全兼容,特别是在处理UDP流量和QUIC的流式传输时。
  • 防火墙和安全软件:确保没有防火墙或安全软件阻止或干扰QUIC流量。

2. QUIC库在Windows上的特定行为

  • 超时和流控制:尽管你已经调整了MaxConnectionReceiveWindowMaxStreamReceiveWindow,但可能还有其他相关的超时或流控制设置需要调整。
  • BUG或限制:检查quic-go的GitHub仓库以查看是否有关于Windows的已知问题或限制。

3. 编程错误或资源耗尽

  • 资源泄露:确保没有未关闭的流或连接,这可能导致资源耗尽。
  • 死锁或阻塞:在并发编程中,死锁或不当的同步可能导致程序挂起。

4. 调试和日志

  • 增加日志记录:在你的代码中增加更详细的日志记录,特别是关于网络操作(如发送和接收数据)的部分。
  • 使用网络抓包工具:使用如Wireshark这样的网络抓包工具来捕获和分析UDP和QUIC流量。

5. 解决方案建议

  • 升级quic-go:确保你使用的是最新版本的quic-go,因为新版本可能已经修复了旧版本中的bug。
  • 简化测试案例:尝试简化你的测试案例,以排除其他潜在因素。例如,先尝试发送较小的数据包或在没有心跳机制的情况下传输文件。
  • 在Windows上测试不同版本:尝试在Windows的不同版本(如Windows 10的不同更新)上运行你的程序,以查看问题是否与特定版本的Windows相关。
  • 咨询quic-go社区:在quic-go的GitHub仓库或相关社区中寻求帮助,可能有其他开发者遇到过类似的问题。

6. 替代方案

  • 考虑使用其他QUIC实现:如果问题持续存在,考虑使用其他QUIC库(如Chrome的QUIC实现)来查看问题是否仍然存在。
  • 回退到TCP:如果QUIC在Windows上始终存在问题,并且你的应用可以容忍TCP的延迟和开销,考虑回退到TCP作为传输协议。
撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
logo
Microsoft
子站问答
访问
宣传栏