V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
monkeyWie
V2EX  ›  Go 编程语言

golang UDP 协议读取报文问题

  •  
  •   monkeyWie ·
    monkeyWie · 2020-02-27 13:30:17 +08:00 · 7112 次点击
    这是一个创建于 1513 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一次性读满 512 个字节没问题,但是分两次读就读不到了,代码如下

    // 读满 512 字节没问题
    buf := make([]byte, 512)
    _, err =io.ReadFull(r,buf)
    
    // 第一次读没问题
    head := make([]byte, 20)
    _, err =io.ReadFull(r,head)
    
    // 第二次读一直阻塞
    body := make([]byte, 6)
    _, err =io.ReadFull(r,body)
    
    54 条回复    2020-03-01 21:26:14 +08:00
    back0893
        1
    back0893  
       2020-02-27 13:36:28 +08:00
    不会像 tcp 一样是流形式发送.
    每次接受都是接受发送的一个包.
    monkeyWie
        2
    monkeyWie  
    OP
       2020-02-27 13:40:26 +08:00
    @back0893 那如果不知道包的大小咋办,一般 body 的长度都放在 head 里,所以先读了一次 head
    BOYPT
        3
    BOYPT  
       2020-02-27 13:41:00 +08:00
    io.ReadFull 设计是你预知长度,要读取确定长度的数据,超出部分就不要了(这句我猜的)。

    如果读未知长度的要弄个 Writer 用 io.Copy
    monkeyWie
        4
    monkeyWie  
    OP
       2020-02-27 13:42:40 +08:00
    @BOYPT 不会的,TCP 协议的话读满之后还可以接着读后面的报文
    rio
        5
    rio  
       2020-02-27 14:41:53 +08:00
    网络基础知识太差又不看文档,net.PacketConn.ReadFrom 返回值的第一个是啥?
    monkeyWie
        6
    monkeyWie  
    OP
       2020-02-27 15:14:45 +08:00
    @rio 大佬,第一个返回值不是读取到的字节数吗,和这个问题有啥关联啊?还请赐教
    ma6254
        7
    ma6254  
       2020-02-27 15:29:08 +08:00
    查下文档 https://golang.org/pkg/io/#ReadFull

    ReadFull reads exactly len(buf) bytes from r into buf. It returns the number of bytes copied and an error if fewer bytes were read. The error is EOF only if no bytes were read. If an EOF happens after reading some but not all the bytes, ReadFull returns ErrUnexpectedEOF. On return, n == len(buf) if and only if err == nil. If r returns an error having read at least len(buf) bytes, the error is dropped.


    // 第一次读没问题
    head := make([]byte, 20)
    _, err =io.ReadFull(r,head)
    // 这时候 ReadFull 就全部 512 字节读取完了,然后只保留了 20 字节给你的 head 变量,后面全部舍弃了

    // 第二次读一直阻塞
    body := make([]byte, 6)
    _, err =io.ReadFull(r,body)
    // 当然就阻塞了,因为已经全部读完了都 EOF 了
    ma6254
        8
    ma6254  
       2020-02-27 15:36:39 +08:00
    @monkeyWie 你就用普通的 read 就可以了

    https://tour.golang.org/methods/21

    假设你的 head 长度固定,body 长度存在 head 的某个字段里,那就先 make 一个和 head 等长的[]byte
    然后 read,得到长度,再 make 一个对应长度的 body []byte,就可以了,如果末尾还有定长校验字就再 read 就好了
    ma6254
        9
    ma6254  
       2020-02-27 15:42:24 +08:00
    @monkeyWie #6

    n, err =io.ReadFull(r,head)

    如果你把这个 n 打出来看看,会发现是 512 而不是 20,表示他实际读了 512 字节,而不是你想要的只读 20 字节
    monkeyWie
        10
    monkeyWie  
    OP
       2020-02-27 15:55:25 +08:00
    @ma6254 谢谢老哥耐心解答,我找到原因了,原来一直有 err 返回,只是 io.ReadFull()在读取到指定字节会把 err 置空,

    错误信息:`wsarecv: A message sent on a datagram socket was larger than the internal message buffer or some other network limit, or the buffer used to receive a datagram into was smaller than the datagram itself.`

    看样子 upd 接收的缓冲区一定要大于要接收的这次报文,所以你 8L 提供的那种方法也是行不通的,现在我是直接开辟一个大点的 buf 用 conn.Read()接收,只能这样做了
    monkeyWie
        11
    monkeyWie  
    OP
       2020-02-27 15:58:29 +08:00
    @ma6254 n 我打印出来过了,都是 20 😂,其实是因为有 err,io.ReadFull()里面把 err 忽略掉了。
    ```
    func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error) {
    if len(buf) < min {
    return 0, ErrShortBuffer
    }
    for n < min && err == nil {
    var nn int
    nn, err = r.Read(buf[n:])
    // nn=20,err!=nil
    n += nn
    }
    if n >= min {
    //读到了指定的字节数就置空 err
    err = nil
    } else if n > 0 && err == EOF {
    err = ErrUnexpectedEOF
    }
    return
    }
    ```
    BBCCBB
        12
    BBCCBB  
       2020-02-27 16:17:47 +08:00
    @ma6254 你说返回 512 吓的我又去翻了一遍资料...

    @monkeyWie 你可以直接用 ioutil.ReadAll
    monkeyWie
        13
    monkeyWie  
    OP
       2020-02-27 16:25:14 +08:00
    @BBCCBB 用 ioutil.ReadAll 也不对呀,不是读完这次就 EOF,后面还有呢
    BBCCBB
        14
    BBCCBB  
       2020-02-27 16:30:48 +08:00   ❤️ 1
    udp 是一次一个包的吧. tcp 才是流式的, 按理说可以知道这次这个 udp 包的大小,

    ioutil.ReadAll 能一次性读完, 他里面也是调用你说的 Read 方法啊, 省去了你自己在 buf 不够的时候扩容 buf 这些代码. 它里面都处理好了.
    monkeyWie
        15
    monkeyWie  
    OP
       2020-02-27 16:36:05 +08:00
    @BBCCBB 我现在只能确定 udp 响应包的最大长度,但是真实的响应可能没这么长,我看了 ioutil.ReadAll 源码,是要遇到 io.EOF 或者其它异常(比如超时)才能返回,但是 udp 协议并不存在关闭连接也就不会有 io.EOF ,所以我这里调用的话应该是一直阻塞着直到超时
    BBCCBB
        16
    BBCCBB  
       2020-02-27 16:46:49 +08:00
    嗷, ioutil.ReadAll udp 我也没试过...

    你知道了 udp 包最大字节数, 先就申请一个最大字节数这么大的 buf, 然后 io.ReadFull, 再看 err 是否时 io.EOF, 这样不知是否可行? 只调用 conn.Read() 它的语义不保证一次能读完吧, 虽然一般都没出现过问题.
    ma6254
        17
    ma6254  
       2020-02-27 16:51:55 +08:00
    @BBCCBB #12 我说的 512 是楼主帖子里写的”一次性读满 512 个字节没问题” 推测为他的一个包为 512 字节然后 flush 一次


    @monkeyWie #10 为什么一定要用 ReadFull(),用标准 Reader 的 Read 就可以了,当作流式用也一样没问题
    rio
        18
    rio  
       2020-02-27 17:04:17 +08:00
    PacketConn 第一次返回的就是整个包的长度,你还要从哪里去找 body 的长度?
    monkeyWie
        19
    monkeyWie  
    OP
       2020-02-27 17:06:59 +08:00
    @BBCCBB udp 读估计每次都是一个完整的 udp 报文,不存在和 tcp 那样用 Read()一次性读不完,所以我现在只要用最大字节数的 buf 去 Read()一次就行了,不用 io.ReadFull 了,毕竟响应的报文不一定有这么长,这个方法肯定阻塞。

    @ma6254 现在没有 ReadFull()了,可能是 TCP 写多了,之前以为 UDP 和 TCP 一样可以这样读。 🤓
    BBCCBB
        20
    BBCCBB  
       2020-02-27 17:17:31 +08:00
    看了一圈,没找到啥可以知道要接收的 udp 包大小的... 只能先 new 一个足够大的 buf 去接收了...


    @rio 这个返回的 n 应该也是读取到 buf 里的字节数.
    BBCCBB
        21
    BBCCBB  
       2020-02-27 17:33:06 +08:00
    话说你这个 io.ReadFull(r, buf) 中的 r 是哪里来的呢.
    rio
        22
    rio  
       2020-02-27 17:35:42 +08:00
    瞎猜有什么用,UDP 协议基本特性都不知道,回去补习网络基础知识。
    monkeyWie
        23
    monkeyWie  
    OP
       2020-02-27 17:36:58 +08:00
    @BBCCBB r 就是一个*net.UDPConn
    XiaoxiaoPu
        24
    XiaoxiaoPu  
       2020-02-27 17:52:09 +08:00
    UDP 是面向报文(packet)的协议,一次读取对应一个网络包(packet),不存在一次读取不完或多次读取一个报文。理论上 UDP 报文最大长度是 65507 字节,实际一般不会这么多,要看具体的应用层协议。
    BBCCBB
        25
    BBCCBB  
       2020-02-27 17:53:20 +08:00
    我 google.. 不是调用 UDPConn 的 ReadFromUDP 方法吗.
    monkeyWie
        26
    monkeyWie  
    OP
       2020-02-27 18:00:01 +08:00
    @BBCCBB 都一样的,read()底层也是调用的 ReadFrom(),这个 @rio 就是个睿智,说了半天就一直在那秀优越,前面说的也全是错的,真的是 talk is cheap
    mightofcode
        27
    mightofcode  
       2020-02-27 18:53:34 +08:00
    一次读 1M,绝不会有问题
    reus
        28
    reus  
       2020-02-27 19:02:21 +08:00
    这个是 ReadFull 的一个局限,它调用 Read 时,如果返回的数据比缓冲区还长,那就会丢弃超出的

    所以第二次就读不到了

    可以套一个 bufio.Reader,这样超出的部分会缓存起来
    tairan2006
        29
    tairan2006  
       2020-02-27 19:22:41 +08:00
    udp 一般不都是缓冲区 1024 直接读么…udp 分包特别麻烦,你要用 quic 这种高级一点的协议才行。
    whoami9894
        30
    whoami9894  
       2020-02-27 19:46:39 +08:00
    @rio #18 你这逻辑,先后次序都颠倒了
    rio
        31
    rio  
       2020-02-27 19:48:13 +08:00
    @monkeyWie Excuse me? 给你指了方向不去想自己哪里没搞明白,解决问题全靠瞎猜?
    rio
        32
    rio  
       2020-02-27 19:50:27 +08:00
    @whoami9894 你又想说啥
    back0893
        33
    back0893  
       2020-02-27 23:22:28 +08:00
    我看别人的实现都是初始化一个最大长度的包
    直接读取一个包
    p2p
        34
    p2p  
       2020-02-28 01:03:48 +08:00
    buf := make([]byte, 1500)
    p2p
        35
    p2p  
       2020-02-28 01:06:47 +08:00
    ```go

    buf := make([]byte, 1500)

    for {
    n, src, err := packet.ReadFrom(buf)

    ... buff[:n]
    }

    ```
    dawniii
        36
    dawniii  
       2020-02-28 10:05:59 +08:00
    @reus 刚试了下并不会这样,buf 给多大,就只读多大的,有多余的下次能读到。
    dawniii
        37
    dawniii  
       2020-02-28 10:19:10 +08:00
    @reus 我试的是 tcp 的情况,楼主说的 udp 应该是要给一个足够的 buf,一次性读完所有内容。
    ydongd
        38
    ydongd  
       2020-02-28 10:35:29 +08:00
    最大传输单元( MTU )大概是 1500,具体记不清了。所以在底层读的这里用这么大的 buffer 就够了。如果一个数据大于 1500,在传输层也会分包,所以在接收的地方还需要将数据重组。根据 n, src, err := packet.ReadFrom(buf) 的 src 判断来源。即 @p2p 的方式。
    reus
        39
    reus  
       2020-02-28 10:59:25 +08:00
    @dawniii 那你试的和这里讨论的,根本就不一样啊。现在说的是读 UDP 时,缓冲区不够大的情况。请重新试验。
    mengzhuo
        40
    mengzhuo  
       2020-02-28 11:01:55 +08:00
    @ydongd 这个 src 不可信的,很多 NAT 只要回过一次包就会 DROP rule。
    dawniii
        41
    dawniii  
       2020-02-28 12:03:40 +08:00
    @reus 确实是不一样,但是楼主读 udp 消息为什么要用 ReadFull 呢,buf 给少了会丢数据(就算是加了 bufio.Reader 也是不符合场景的,造成一次消息多次读取,本来都是用 udp 了还要自己做消息分隔符,而且消息的内容长度必须是预设 buf 的整数倍,不然会后那点消息会被卡住),如果一开始预设 buf 给多了也会卡住。

    所以读 udp 直接用 ReadFrom 给一个满足业务需求的 buf 大小就行了。
    whoami9894
        42
    whoami9894  
       2020-02-28 15:00:06 +08:00
    @ydongd 应该不用考虑 ether 的 MTU 吧,给的 buffer 大于 1500 应该也会在内核缓冲区拼接的(我猜的)

    @rio 楼主问当不知道 UDP packet 长度时怎么分配 buffer 大小,buffer 给小了导致后面数据被 drop,你答 UDPConn.ReadFrom 第一个返回值就是 body 长度?你听明白他问啥了?你答的又是啥?

    @dawniii 一个是 stream 一个 packet,TCP 一次读不完还存在协议栈缓冲区,UDP 按报文划分,buffer 过小后面数据直接 drop 了
    reus
        43
    reus  
       2020-02-28 15:45:50 +08:00
    @dawniii 读出来之后,解析内容时,就要用到 ReadFull 了,例如 header 固定是 16 字节,那用 16 字节的缓冲区去读,也是正常做法。当然也可以直接用下标去处理,但如果协议发生变化,下标就可能全部变化,基于 io.Reader 的代码,就不需要调整下标,只需要插入多一些读的代码。
    rio
        44
    rio  
       2020-03-01 12:21:16 +08:00
    @whoami9894 因为他的根本问题是他对 UDP 的基本原理不清楚,才会问出这么奇怪的问题。如果理解 UDP 的语义,自然就会知道不能分多次去读同一个包,也不会出现什么不知道包的长度还需要去读一个 header 来判断 body 长度的问题。举个例子,为什么 DNS over TCP 需要两个字节的头部而 DNS over UDP 不需要?因为 UDP 的包长度在接受的时候就是已知的,根本就不可能会使用 io.ReadFull 这个调用。你不去找他的根本问题在哪里,只给一个解决表面问题的答案,他也不会意识到自身的问题在哪里,以后继续犯类似的错误。

    你觉得你听明白了问题?你自己说的「 不知道 UDP packet 长度时怎么分配 buffer 大?」你自己想想正确答案应该是啥。
    rio
        45
    rio  
       2020-03-01 12:25:05 +08:00
    @whoami9894 说白了,这个问题和本站之前出现的「 TCP 粘包问题」如出一辙:不去研究底层原理,一切全靠瞎猜。
    whoami9894
        46
    whoami9894  
       2020-03-01 13:55:59 +08:00
    @rio
    你的回复在说`net.PacketConn.ReadFrom`返回值的第一个就是 body 长度。假设我给个 1024 bytes 的 buffer,实际 packet 有 1400 bytes,ReadFrom 给我返回的一定是 1024。packet 长度确实是确定的,但 ReadFrom 传进去的 buffer 该分配的大小在事先是不知道的(在没有提前协商的前提下)。就好比我需要知道 length 才能调用 Read,你告诉我你调用 Read 就知道 length 了一样。楼主确实看起来没有 UDP 编程经验,所以直接告诉他分配一个足够大的 buffer,不需要像 stream 一样去做额外的上层分包就行了
    rio
        47
    rio  
       2020-03-01 14:32:04 +08:00
    @whoami9894 你还是没理解这里的问题到底在哪。

    「假设我给个 1024 bytes 的 buffer,实际 packet 有 1400 bytes,ReadFrom 给我返回的一定是 1024。」
    「我需要知道 length 才能调用 Read,你告诉我你调用 Read 就知道 length 了一样。」

    如果知道 UDP 的原理,就根本不会出现用 1024-byte buffer 去读 1400-byte packet 这种情况,也不会需要在这个包头加一个 header 记录 body 的长度,更不会出现不合时宜的用 io.ReadFull 去读取 body 全文。

    你前面其实也自己把这个问题总结出来了,「当不知道 UDP packet 长度时怎么分配 buffer 大小?」你这里给的答案是「分配一个足够大的 buffer,不需要像 stream 一样去做额外的上层分包」,但其实并没有真的回答问题:多大才是足够大?为什么不需要做分层?这两个问题都需要对 UDP 底层有基本的理解才能解释。这才是楼主的根本问题。
    5thcat
        48
    5thcat  
       2020-03-01 14:57:20 +08:00
    stackoverflow 上有几乎一模一样的问题,read data from a udp socket with an unknown length,解释得更清楚哈哈
    5thcat
        49
    5thcat  
       2020-03-01 15:05:55 +08:00
    我觉得可以理解成,缓冲区长度不够,read 再读就读不到, 这是 UDP 协议的行为,跟 go 语言以及 go 语言的库函数没有关系;所以只能和发送方先约定好 包的大小
    5thcat
        50
    5thcat  
       2020-03-01 15:16:52 +08:00
    又看到一个回答,recv (或 recvfrom) 系统调用有 flag (MSG_TRUNC 或 MSG_PEEK) ,是可以知道 packet 大小的
    whoami9894
        51
    whoami9894  
       2020-03-01 15:52:25 +08:00
    @rio
    我明白你的意思,楼主用 io.ReadFull 处理 packet,用 header 记录 body length 确实是不理解 UDP,也肯定没有看过 io.ReadFull 的实现,所以楼主的问题是需要补一下计网的知识
    至于多大才是足够大,如果是收发自己设计的应用协议,那就按约定的格式来,设计尽量处于 ethernet MTU 的大小范围内。但假如是收发未协商大小的包,比如做 UDP 端口转发,每个 buffer 需要给到 65507
    rio
        52
    rio  
       2020-03-01 18:15:33 +08:00
    @whoami9894 所以你看这个帖子下最开始的那些「讨论」,根本就是在盲人摸象。
    rio
        53
    rio  
       2020-03-01 18:21:02 +08:00
    @whoami9894 我最开始是心情好给他点一下,稍微聪明点的人会去想自己哪里理解错了。但显然楼主意识不到自己的问题,那就……随他去吧~ 再也不指点这些萌新了。
    hankai17
        54
    hankai17  
       2020-03-01 21:26:14 +08:00
    golang.google.cn/src/io/io.go?h=io 看了一下 ReadAtLeast 意思是 读的总字节数超过了你定的阀值 也正常返回
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2848 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 41ms · UTC 11:20 · PVG 19:20 · LAX 04:20 · JFK 07:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.