-
Notifications
You must be signed in to change notification settings - Fork 161
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
Comments
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:
There should be more details for real business that we need to handle TCP and UDP in a different way. It is easy for |
Usually, we should use some goroutine pool. |
I vote no state, no OnOpen/OnClose; UDP should be stateless
…On Mon, Sep 20, 2021 at 9:37 AM lesismal ***@***.***> wrote:
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.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#93 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAJSWEXOS2XNBAX7PD66NKLUC42KFANCNFSM5ELY2Q4Q>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
I also guess so - no OnOpen/OnClose. 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. |
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. |
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. |
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? |
Let's see how easy it is to implement a full example that use the same // 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()
} |
So, I really suggest any poller frameworks of golang give up to support UDP. |
Usually processing takes some time and code should look like this:
to avoid influence different UDP packets processing time each other. |
I know this, I use 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. |
Here is some benchmark: But for the normal scenario without too large num of online connections, poller frameworks do not perform better than std. |
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. |
Any plans to add UDP? I have seen lines in gopher.go
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()
insideonData()
enough or need to use some goroutine pool (which one actually)?The text was updated successfully, but these errors were encountered: