High-performance batch read and write network packets Addendum

Original link: https://colobu.com/2023/05/21/batch-read-and-write-udp-packets-in-Go-2/

Some time ago, I wrote an article on high-performance batch reading and writing network packets , which introduced the sendmmsg system call, which can send network packets in batches. Some readers asked what is the difference between this and writev .

In fact, look at their definitions, you will know where the difference is:


1
2
3

int sendmmsg( int sockfd, struct mmsghdr *msgvec, unsigned int vlen, int flags);
ssize_t writev( int fd, const struct iovec *iov, int iovcnt);

sendmmsg() system call is an extension of sendmsg(2) that allows a caller to transmit multiple messages on a socket using a single system call. (This has performance benefits for some applications.)
The sockfd parameter is the file descriptor of the socket on which to transfer data.
The msgvec parameter is a pointer to an array of mmsghdr structures. The size of this array is specified in vlen.
sendmmsg is mainly used for network batch writing. If you want to perform file IO batch operations, you can use writev and readv .

writev() system call writes the iovcnt cache data to the file descriptor fd (“gather output”). The data it processes is “atomic” and will not be disturbed by other concurrent read and write operations. This means that when using these functions in a multi-threaded or multi-process environment, data transfer can be done safely without worrying about fragmentation or confusion of data.
Under normal circumstances, the writev() function does not write only partial data. It will either write all the data as a single operation, or return an error indicating that the write failed.
However, there is a special case where only partial data may be written, that is, when using a non-blocking socket for writing, and the write buffer is full. In this case, writev() may only be able to write part of the data, and the remaining data will be returned for later writing. At this point, further processing is required based on the returned results to ensure that all data is written correctly.
Therefore, when using the writev() function, it is recommended to check the returned number of bytes written to ensure that all data was written correctly, and handle partial writes by retrying the operation if necessary.

In fact, using sendmmsg and writev to send data is boring and anti-human. It needs to construct specific data structures such as mmsghdr and iovec. In the previous article, we introduced Go’s packaging of sendmmsg , which simplifies the call to sendmmsg . In this article, we introduce Go’s encapsulation of writev system. Go does not encapsulate readv . Even if someone contributes the implemented code, the main reason is that everyone feels that they have not seen the performance improvement and benefits brought by readv .

Inside Go is Writev encapsulated in src/internal/poll/writev.go , and the wrtev system call in Linux environment is implemented in fd_writev_unix.go .

You can also use Writev and Readv directly, which are relatively low-level.

If you think the Writev encapsulated by Go is still troublesome, you can use Buffers , which optimizes the conn that implements writev, and calls writeBuffers first:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two

func (v *Buffers) WriteTo(w io. Writer) (n int64 , err error) {
if wv, ok := w.(buffersWriter); ok {
return wv.writeBuffers(v)
}
for _, b := range *v {
nb, err := w.Write(b)
n += int64 (nb)
if err != nil {
v. consume(n)
return n, err
}
}
v. consume(n)
return n, nil
}
// buffersWriter is the interface implemented by Conns that support a
// “writev”-like batch write optimization.
// writeBuffers should fully consume and write all chunks from the
// provided Buffers, else it should report a non-nil error.
type buffersWriter interface {
writeBuffers(*Buffers) ( int64 , error)
}

The following is an example of sending bulk messages using Buffers:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
twenty one
twenty two
twenty three
twenty four
25
26
27
28
29
30
31
32
33
34

package main
import (
“fmt”
“net”
“strconv”
)
func main() {
conn, err := net.Dial( “udp” , “192.168.0.1:8972” )
if err != nil {
panic (err)
}
var buf net.Buffers
for i := 0 ; i < 10 ; i++ {
buf = append (buf, [] byte ( “hello world: “ +strconv.Itoa(i)))
}
_, err = buf.WriteTo(conn) // as a datagram packet
if err != nil {
panic (err)
}
var data = make ([] byte , 1024 )
for {
n, err := conn. Read(data)
if err != nil {
panic (err)
}
fmt.Println( string (data[:n]))
}
}

It adds 10 messages to Buffers and writes them at once. In fact, these ten messages are sent to the server as a UDP packet. If you have a lot of sending requests and need to send them in batches, you can consider this method.

This article is transferred from: https://colobu.com/2023/05/21/batch-read-and-write-udp-packets-in-Go-2/
This site is only for collection, and the copyright belongs to the original author.