Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UDP support #93

Closed
UladzimirTrehubenka opened this issue Sep 20, 2021 · 13 comments
Closed

UDP support #93

UladzimirTrehubenka opened this issue Sep 20, 2021 · 13 comments

Comments

@UladzimirTrehubenka
Copy link

UladzimirTrehubenka commented Sep 20, 2021

Any plans to add UDP? I have seen lines in gopher.go

	// tcp* supported only by now, there's no plan for other protocol such as udp,
	// because it's too easy to write udp server/client.

but well, e.g. I want to implement DNS proxy which should respond on UDP/TCP port 53,
it is ridiculous to implement another one flow instead using nbio itself.

Side question: as I understand onData() have to return ASAP to avoid blocking the event loop,
is call go handler() inside onData() enough or need to use some goroutine pool (which one actually)?

@UladzimirTrehubenka UladzimirTrehubenka changed the title SO_REUSEPORT UDP support Sep 20, 2021
@lesismal
Copy link
Owner

Any plans to add UDP? I have seen lines in gopher.go

For TCP, we aim to solve 1000k problem, we support poller to avoid serving each connection with 1 or more goroutines which cost lots of goroutines then memory/gc/schedule.

There are some details of differences we need to consider between TCP and UDP if we both support them in nbio:

  1. Shall we support OnOpen/OnClose for UDP?
  2. If we did 1, we need to maintain UDP connection info, then we may need use timer to do some alive management for each UDP socket.

There should be more details for real business that we need to handle TCP and UDP in a different way.

It is easy for golang to wrap UDP using std and no need to make that many goroutines.
Since std+UDP is easy, what can we benefit from the support for UDP? I think we can get nothing but the cost is greater than the benefit.

@lesismal
Copy link
Owner

Side question: as I understand onData() have to return ASAP to avoid blocking the event loop,
is call go handler() inside onData() enough or need to use some goroutine pool (which one actually)?

Usually, we should use some goroutine pool.
In the implementation of nbio-http, we accept the user's argument, if it's nil, we use a goroutine pool:
https://github.com/lesismal/nbio/blob/master/nbhttp/server.go#L261

@acgreek
Copy link
Collaborator

acgreek commented Sep 20, 2021 via email

@UladzimirTrehubenka
Copy link
Author

UladzimirTrehubenka commented Sep 20, 2021

I also guess so - no OnOpen/OnClose.
And regarding There should be more details for real business that we need to handle TCP and UDP in a different way
https://github.com/tidwall/evio has separated flows for UDP and TCP (but has no TLS support). And the idea is to use nbio as a base for a DNS service with performance comparable with Unbound DNS resolver written on C lang.

And regarding hack TLS from golang - probably need to propose appropriate changes in golang itself because I don't know who would use the lib with such kind of tricks except original repo authors.

@lesismal
Copy link
Owner

lesismal commented Sep 20, 2021

I list the differences is to say that I don't want to support UDP, but not to say I want to support UDP and thinking about what we want to do for UDP:joy:.

Unlike TCP, the UDP protocol does not promise the packets comes by order, if you want to support TLS on UDP, you need to implement reliability as TCP, then why not use KCP or UTP directly?

For DNS like services, one request packet, one response packet, we don't need to worry about the packets' order and even don't care if the packet is lost on the way, we don't need to implement features about the reliability such as ack, window size, and retransmission. I think it's much easier for std+UDP, and no need to implement a poller that supports UDP in golang.

Also, I think evio, or other poller frameworks which have already supported UDP, are doing jobs that are meaningless on UDP.

@lesismal
Copy link
Owner

For TCP, we aim to solve 1000k problem, we support poller to avoid serving each connection with 1 or more goroutines which cost lots of goroutines then memory/gc/schedule.

But for UDP, it's much easier to control the num of goroutines, and the std+UDP is better than a poller-like framework.

@lesismal
Copy link
Owner

lesismal commented Sep 20, 2021

Since we don't need OnOpen/OnClose, std+UDP is so easy:

socket, err := net.ListenUDP("udp4", &net.UDPAddr{
	IP:   net.IPv4(0, 0, 0, 0),
	Port: 8888,
})
if err != nil {
	log.Fatal(err)
}
defer socket.Close()

// GoroutineNum like the num of poller goroutine
for i := 0; i < GoroutineNum; i++ {
	go func() {
		data := make([]byte, YourMaxUDPPacketLength)
		for {
			read, remoteAddr, err := socket.ReadFromUDP(data)
			if err != nil {
				log.Fatal(err)
				continue
			}

			// handle the request
			senddata := []byte("some data")
			_, err = socket.WriteToUDP(senddata, remoteAddr)
			if err != nil {
				log.Fatal(err)
			}
		}
	}()
}

So, why we must handle UDP with another poller framework that is much more complex?

@lesismal
Copy link
Owner

Let's see how easy it is to implement a full example that use the same onData handler for both TCP(nbio) and UDP(std, without OnOpen/OnClose):

// server.go
package main

import (
	"errors"
	"fmt"
	"log"
	"net"
	"os"
	"os/signal"
	"runtime"
	"time"

	"github.com/lesismal/nbio"
)

var (
	port = 8888

	gopher      *nbio.Gopher
	udpListener *net.UDPConn
)

// echo handler
func onDataBothForTcpAndUdp(c net.Conn, data []byte) {
	c.Write(data)
}

func main() {
	startUDPServer()
	defer stopUDPServer()

	startTCPServer()
	defer stopTCPServer()

	interrupt := make(chan os.Signal, 1)
	signal.Notify(interrupt, os.Interrupt)
	<-interrupt
}

func startTCPServer() {
	gopher = nbio.NewGopher(nbio.Config{
		Network:            "tcp",
		Addrs:              []string{fmt.Sprintf(":%v", port)},
		MaxWriteBufferSize: 6 * 1024 * 1024,
	})

	gopher.OnData(func(c *nbio.Conn, data []byte) {
		onDataBothForTcpAndUdp(c, data)
	})

	err := gopher.Start()
	if err != nil {
		log.Fatal(err)
	}
}

func stopTCPServer() {
	gopher.Stop()
}

func startUDPServer() {
	var err error
	udpListener, err = net.ListenUDP("udp4", &net.UDPAddr{
		IP:   net.IPv4(0, 0, 0, 0),
		Port: port,
	})
	if err != nil {
		log.Fatal(err)
	}

	for i := 0; i < runtime.NumCPU(); i++ {
		go func() {
			data := make([]byte, 4096)
			for {
				read, remoteAddr, err := udpListener.ReadFromUDP(data)
				if err != nil {
					log.Println("udp read failed:", err)
					continue
				}
				onDataBothForTcpAndUdp(&UDPConn{udpSocket: udpListener, remoteAddr: remoteAddr}, data[:read])
			}
		}()
	}
}

func stopUDPServer() {
	udpListener.Close()
}

type UDPConn struct {
	remoteAddr *net.UDPAddr
	udpSocket  *net.UDPConn
}

func (c *UDPConn) Read(b []byte) (n int, err error) {
	return 0, errors.New("unsupported")
}

func (c *UDPConn) Write(b []byte) (n int, err error) {
	return c.udpSocket.WriteToUDP(b, c.remoteAddr)
}

func (c *UDPConn) Close() error {
	return errors.New("unsupported")
}

func (c *UDPConn) LocalAddr() net.Addr {
	return c.udpSocket.LocalAddr()
}

func (c *UDPConn) RemoteAddr() net.Addr {
	return c.remoteAddr
}

func (c *UDPConn) SetDeadline(t time.Time) error {
	return errors.New("unsupported")
}

func (c *UDPConn) SetReadDeadline(t time.Time) error {
	return errors.New("unsupported")
}

func (c *UDPConn) SetWriteDeadline(t time.Time) error {
	return errors.New("unsupported")
}
// client.go
package main

import (
	"fmt"
	"log"
	"net"
	"time"
)

var (
	port = 8888
)

func udpEchoClient() {
	socket, err := net.DialUDP("udp4", nil, &net.UDPAddr{
		IP:   net.IPv4(127, 0, 0, 1),
		Port: port,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer socket.Close()

	sendBuf := []byte("hello from udp")
	recvBuf := make([]byte, 1024)
	for i := 0; true; i++ {
		_, err = socket.Write(sendBuf)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("udp send %v: %v\n", i, string(sendBuf))

		nread, _, err := socket.ReadFromUDP(recvBuf)
		if err != nil {
			return
		}
		fmt.Printf("udp recv %v: %v\n", i, string(recvBuf[:nread]))
		time.Sleep(time.Second)
	}
}

func tcpEchoClient() {
	socket, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%v", port))
	if err != nil {
		log.Fatal(err)
	}
	defer socket.Close()

	sendBuf := []byte("hello from tcp")
	recvBuf := make([]byte, 1024)
	for i := 0; true; i++ {
		_, err = socket.Write(sendBuf)
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("tcp send %v: %v\n", i, string(sendBuf))

		nread, err := socket.Read(recvBuf)
		if err != nil {
			return
		}
		fmt.Printf("tcp recv %v: %v\n", i, string(recvBuf[:nread]))
		time.Sleep(time.Second)
	}
}

func main() {
	go udpEchoClient()
	tcpEchoClient()
}

@lesismal
Copy link
Owner

So, I really suggest any poller frameworks of golang give up to support UDP.

@UladzimirTrehubenka
Copy link
Author

Usually processing takes some time and code should look like this:

		for {
			read, remoteAddr, err := socket.ReadFromUDP(data)
			if err != nil {
				log.Fatal(err)
				continue
			}
                        go func(data []byte, remoteAddr *net.UDPAddr) {
			        // handle the request
			        senddata, err := handle(data)
                                ...
			        _, err = socket.WriteToUDP(senddata, remoteAddr)
			        ...
			}(data, remoteAddr)
		}

to avoid influence different UDP packets processing time each other.
And ReadFromUDP() costs some CPU time, where as with events flow a data is read when it is exactly getting.

@lesismal
Copy link
Owner

lesismal commented Sep 20, 2021

Usually processing takes some time and code should look like this:

I know this, I use go or goroutine pool in lots of other places, users should control the onData handler themselves.
In my example, I just want to show the point that how to handle TCP and UDP with the same handler.
And furthermore, Read and parse operations which are cpu cost, and to reuse the read buffer and avoid copy and more buffer allocation, we should usually not use go directly, but should go when you get a full message. We need avoid dirty buffer too:

data := make([]byte, 4096)
for {
	read, remoteAddr, err := udpListener.ReadFromUDP(data)
	if err != nil {
		log.Println("udp read failed:", err)
		continue
	}

	go func(data []byte, remoteAddr *net.UDPAddr) {
		// befor you have handled the data, it may have been changed by next ReadFromUDP
		senddata, err := handle(data)
		_, err = socket.WriteToUDP(senddata, remoteAddr)
	}(data, remoteAddr)
}

BTW, even for TCP, poller frameworks are not faster than std for normal scenarios that do not have a huge num of online connections, only when there are too many connections that cost lots of goroutines will make std slow.

@lesismal
Copy link
Owner

Here is some benchmark:
https://github.com/lesismal/go-net-benchmark
lesismal/go-net-benchmark#1 (comment)

But for the normal scenario without too large num of online connections, poller frameworks do not perform better than std.
Because we need to implement async-streaming-parser, solve problems of buffer life cycle management, buffer pool and object pool, goroutine pool, which are stack unfriendly and much more difficult than std.
If your online num is smaller than 100k, std should be better.

@lesismal
Copy link
Owner

Summary: The standard library udp is simpler and easier to use, and the support for udp will have a bad impact on tcp (many functions that support tcp need to be left blank for udp), we will not plan to support udp.

Thank you for your feedback, I'll close this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants