Go 中的 UDP 服务器和客户端:下篇
从客户端中的 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 验证内核函数时,我终于有了一个很棒的工作流程,也许我很快会写到这 - 如果这很有趣,请告诉我!
- 点赞
- 收藏
- 关注作者
评论(0)