Go 中的 UDP 服务器和客户端:下篇

举报
宇宙之一粟 发表于 2022/06/30 10:04:44 2022/06/30
【摘要】 从客户端中的 UDP“连接”接收从 UDPConn 接收的行为可以看作与“写入路径”几乎相同,只是此时提供了一个缓冲区(以便它可以填充到达的内容),而我们没有真的知道我们要等多久才能收到内容。例如,我们可以有以下从已知地址读取的代码路径:buf := make([]byte, *bufSize)_, err = conn.Read(buf)这将在后台变成 read(2) 系统调用,然后...

从客户端中的 UDP“连接”接收

UDPConn 接收的行为可以看作与“写入路径”几乎相同,只是此时提供了一个缓冲区(以便它可以填充到达的内容),而我们没有真的知道我们要等多久才能收到内容。

例如,我们可以有以下从已知地址读取的代码路径:

buf := make([]byte, *bufSize)
_, err = conn.Read(buf)

这将在后台变成 read(2) 系统调用,然后通过 vfs 并变成从套接字读取 read

22313   22313   read            __skb_recv_udp
        __skb_recv_udp+0x1 
        inet_recvmsg+0x51 
        sock_recvmsg+0x43 
        sock_read_iter+0x90 
        new_sync_read+0xe4 
        __vfs_read+0x29 
        vfs_read+0x8e 
        sys_read+0x55 
        do_syscall_64+0x73 
        entry_SYSCALL_64_after_hwframe+0x3d 

需要记住的重要一点是,当从套接字读取时,这将是一个阻塞操作。

鉴于消息可能永远不会从这样的套接字返回,我们可能会永远等待。

为了避免这种情况,我们可以设置一个读取时间截止时间 SetReadDeadline ,以防我们等待太久,这会杀死整个进程:

buf := make([]byte, *bufSize)

// Sets the read deadline for now + 15seconds.
// If you plan to read from the same connection again,
// make sure you expand the deadline before reading
// it.
conn.SetReadDeadline(time.Now().Add(15 * time.Second))
_, err = conn.Read(buf)

现在,如果另一端需要很长时间才能响应:

read udp 10.0.2.15:41745->1.1.1.1:53: i/o timeout

从服务器中的 UDP“连接”接收

虽然这对客户端来说很好(我们知道我们在从谁那里读取数据),但它不适用于服务器。

原因是在服务器端,我们不知道我们从谁那里读取(地址未知)。

与 TCP 服务器的情况不同,我们有 accept(2) 将服务器可以写入的连接返回给服务器实现者,在 UDP 的情况下,没有“要写入的连接”之类的东西。只有一个“写给谁”,可以通过检查到达的数据包来检索。

WITH READ

  "Hmmm, let me write something to
   my buddy at 1.1.1.1:53"

   client --.
            |
            | client: n, err := udpConn.Write(buf)
            | server: n, err := udpConn.Read(buf)
            |
            *---> server
                  "Oh, somebody wrote me something!
                   I'd like to write back to him/her,
                   but, what's his/her address?
                   
                   I don't have a connection... I need
                   an address to write to! I can't to
                   a thing now!"


WITH READFROM

   client --.
            |
            | client: n, err := udpConn.Write(buf)
            | server: n, address, err := udpConn.Read(buf)
            |
            *---> server
                  "Oh, looking at the packet, I can
                   see that my friend Jane wrote to me,
                   I can see that from `address`!
                   
                   Let me answer her back!"

因此,在服务器上,我们需要专门的连接:UDPConn .

这种专门的连接能够为我们提供 ReadFrom ,这种方法不仅可以读取文件描述符并将内容添加到缓冲区,还可以检查数据包的标头并为我们提供有关谁发送了数据包的信息。

它的用法如下所示:

buffer := make([]byte, 1024)

// Given a buffer that is meant to hold the
// contents from the messages arriving at the
// socket that `udpConn` wraps, it blocks until
// messages arrive. 
//
// For each message arriving, `ReadFrom` unwraps
// the message, getting information about the
// sender from the protocol headers and then
// fills the buffer with the data.
n, addr, err := udpConn.ReadFrom(buffer)
if err != nil {
        panic(err)
}

尝试了解事物如何在幕后工作的一种有趣方式是查看 plan9 实现 (net/udpsock_plan9.go)。

这是它的代码(带有我自己的注释):

func (c *UDPConn) readFrom(b []byte) (n int, addr *UDPAddr, err error) {
        // creates a buffer a little bit bigger than
        // the one we provided (to account for the header of
        // the UDP headers)
	buf := make([]byte, udpHeaderSize+len(b))

        // reads from the underlying file descriptor (this might
        // block).
	m, err := c.fd.Read(buf)
	if err != nil {
		return 0, nil, err
	}
	if m < udpHeaderSize {
		return 0, nil, errors.New("short read reading UDP header")
	}

        // strips out the parts that were not readen
	buf = buf[:m]

        // interprets the UDP header
	h, buf := unmarshalUDPHeader(buf)
        
        // copies the data back to our supplied buffer
        // so that we only receive the data, not the header.
	n = copy(b, buf)
	return n, &UDPAddr{IP: h.raddr, Port: int(h.rport)}, nil
}

自然,在 Linux 下,这不是 readFrom 采用的路径。它使用了 recvfrom,它在底层完成了整个“UDP 标头解释”,但想法是一样的(除了 plan9,它都是在用户空间中完成的)。

为了验证在 Linux 下我们使用 recvfrom 的事实,我们跟踪 UDPConn.ReadFrom (你可以使用 delve):

0  0x00000000004805b8 in syscall.recvfrom
   at /usr/local/go/src/syscall/zsyscall_linux_amd64.go:1641
1  0x000000000047e84f in syscall.Recvfrom
   at /usr/local/go/src/syscall/syscall_unix.go:262
2  0x0000000000494281 in internal/poll.(*FD).ReadFrom
   at /usr/local/go/src/internal/poll/fd_unix.go:215
3  0x00000000004f5f4e in net.(*netFD).readFrom
   at /usr/local/go/src/net/fd_unix.go:208
4  0x0000000000516ab1 in net.(*UDPConn).readFrom
   at /usr/local/go/src/net/udpsock_posix.go:47
5  0x00000000005150a4 in net.(*UDPConn).ReadFrom
   at /usr/local/go/src/net/udpsock.go:121
6  0x0000000000526bbf in main.server.func1
   at ./main.go:65
7  0x000000000045e1d1 in runtime.goexit
   at /usr/local/go/src/runtime/asm_amd64.s:1333

在内核层面,我们还可以查看涉及到哪些方法:

24167   24167   go-sample-udp   __skb_recv_udp
        __skb_recv_udp+0x1 
        inet_recvmsg+0x51 
        sock_recvmsg+0x43 
        SYSC_recvfrom+0xe4 
        sys_recvfrom+0xe 
        do_syscall_64+0x73 
        entry_SYSCALL_64_after_hwframe+0x3d 

Go 中的 UDP 服务器

现在,转到服务器端实现,代码如下所示(大量注释):

// maxBufferSize specifies the size of the buffers that
// are used to temporarily hold data from the UDP packets
// that we receive.
const maxBufferSize = 1024

// server wraps all the UDP echo server functionality.
// ps.: the server is capable of answering to a single
// client at a time.
func server(ctx context.Context, address string) (err error) {
	// ListenPacket provides us a wrapper around ListenUDP so that
	// we don't need to call `net.ResolveUDPAddr` and then subsequentially
	// perform a `ListenUDP` with the UDP address.
	//
	// The returned value (PacketConn) is pretty much the same as the one
	// from ListenUDP (UDPConn) - the only difference is that `Packet*`
	// methods and interfaces are more broad, also covering `ip`.
	pc, err := net.ListenPacket("udp", address)
	if err != nil {
		return
	}

	// `Close`ing the packet "connection" means cleaning the data structures
	// allocated for holding information about the listening socket.
	defer pc.Close()

	doneChan := make(chan error, 1)
	buffer := make([]byte, maxBufferSize)

	// Given that waiting for packets to arrive is blocking by nature and we want
	// to be able of canceling such action if desired, we do that in a separate
	// go routine.
	go func() {
		for {
			// By reading from the connection into the buffer, we block until there's
			// new content in the socket that we're listening for new packets.
			//
			// Whenever new packets arrive, `buffer` gets filled and we can continue
			// the execution.
			//
			// note.: `buffer` is not being reset between runs.
			//	  It's expected that only `n` reads are read from it whenever
			//	  inspecting its contents.
			n, addr, err := pc.ReadFrom(buffer)
			if err != nil {
				doneChan <- err
				return
			}

			fmt.Printf("packet-received: bytes=%d from=%s\n",
				n, addr.String())

			// Setting a deadline for the `write` operation allows us to not block
			// for longer than a specific timeout.
			//
			// In the case of a write operation, that'd mean waiting for the send
			// queue to be freed enough so that we are able to proceed.
			deadline := time.Now().Add(*timeout)
			err = pc.SetWriteDeadline(deadline)
			if err != nil {
				doneChan <- err
				return
			}

			// Write the packet's contents back to the client.
			n, err = pc.WriteTo(buffer[:n], addr)
			if err != nil {
				doneChan <- err
				return
			}

			fmt.Printf("packet-written: bytes=%d to=%s\n", n, addr.String())
		}
	}()

	select {
	case <-ctx.Done():
		fmt.Println("cancelled")
		err = ctx.Err()
	case err = <-doneChan:
	}

	return
}

您可能已经注意到,它与客户端并没有什么不同!原因是不涉及实际连接(如在 TCP 中),客户端和服务器最终都会通过相同的路径:准备一个套接字来读取和写入,然后检查数据包中的内容并执行同样的事情一遍又一遍。

结束的想法

很高兴通过这种探索,检查 Go 源代码以及内核中的幕后情况。

我想在使用 Delve 进行调试和使用 bcc 验证内核函数时,我终于有了一个很棒的工作流程,也许我很快会写到这 - 如果这很有趣,请告诉我!

参考链接:A UDP server and client in Go | OpsTips

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。