用Golang仅需50行代码实现百万级并发连接处理

用Golang仅需50行代码实现百万级并发连接处理 我正在浏览这个页面:单机百万并发,golang 50行代码 - Go语言中文网 - Golang中文社区,它声称用Go语言编写的50行代码,在一台4核CPU、16G内存的单台服务器上,可以处理来自网络客户端的100万个并发连接。

net 包内部是否已经利用了IO多路复用技术,比如 epoll(epoll_create, epoll_ctl, epoll_wait)?所以我只需使用 net 包,就能自动获得 epoll 的能力,而无需在 Go 中手动调用 epoll 的 API?

谢谢

服务器端代码:

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

var array []byte = make([]byte, 10)

func checkError(err error, info string) (res bool) {

    if err != nil {
        fmt.Println(info + "  " + err.Error())
        return false
    }
    return true
}

func Handler(conn net.Conn) {
    for {
        _, err := conn.Write(array)
        if err != nil {
            return
        }
        time.Sleep(10 * time.Second)
    }
}

func main() {

    for i := 0; i < 10; i += 1 {
        array[i] = 'a'
    }

    service := ":8888"
    tcpAddr, _ := net.ResolveTCPAddr("tcp4", service)
    l, _ := net.ListenTCP("tcp", tcpAddr)

    for {
        conn, err := l.Accept()
        if err != nil {
            fmt.Printf("accept error, err=%s\n", err.Error())
            os.Exit(1)
        }
        go Handler(conn)
    }

}

客户端代码:

package main

import (
    "flag"
    "fmt"
    "net"
    "os"
    "time"
)

var RemoteAddr *string
var ConcurNum *int
var LocalAddr *string

func init() {
    RemoteAddr = flag.String("remote-ip", "127.0.0.1", "ip addr of remote server")
    ConcurNum = flag.Int("concurrent-num", 100, "concurrent number of client")
    LocalAddr = flag.String("local-ip", "0.0.0.0", "ip addr of remote server")
}

func consume() {

    laddr := &net.TCPAddr{IP: net.ParseIP(*LocalAddr)}

    var dialer net.Dialer
    dialer.LocalAddr = laddr

    conn, err := dialer.Dial("tcp", *RemoteAddr+":8888")
    if err != nil {
        fmt.Println("dial failed:", err)
        os.Exit(1)
    }
    defer conn.Close()

    buffer := make([]byte, 512)

    for {
        _, err2 := conn.Read(buffer)
        if err2 != nil {
            fmt.Println("Read failed:", err2)
            return
        }

        //  fmt.Println("count:", n, "msg:", string(buffer))

    }

}

func main() {
    flag.Parse()
    for i := 0; i < *ConcurNum; i++ {
        go consume()
    }
    time.Sleep(3600 * time.Second)
}

更多关于用Golang仅需50行代码实现百万级并发连接处理的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

是的,无需调用epoll。并非net包,而是Go调度器为您进行轮询。简而言之,每当一个goroutine执行网络I/O时,调度器会将goroutine移交给netpoller,并让其他goroutine运行。

我找到了这篇文章,它提供了关于网络I/O和netpoller的更多细节。

更多关于用Golang仅需50行代码实现百万级并发连接处理的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


是的,Go语言的net包在Linux系统上确实内部使用了epoll来实现IO多路复用。Go运行时通过runtime.netpoll抽象层封装了不同操作系统的IO多路复用机制,在Linux上使用的是epoll,在BSD/macOS上使用kqueue,在Windows上使用IOCP。

以下是Go运行时中网络轮询器的简化实现原理:

// Go运行时内部的实际实现(简化示意)
func netpollinit() {
    epfd = epoll_create1(0)
    if epfd >= 0 {
        return
    }
}

func netpollopen(fd uintptr, pd *pollDesc) int32 {
    var ev epoll_event
    ev.events = _EPOLLIN | _EPOLLOUT | _EPOLLRDHUP | _EPOLLET
    ev.data.ptr = pd
    return -epoll_ctl(epfd, _EPOLL_CTL_ADD, int32(fd), &ev)
}

func netpoll(block bool) gList {
    var events [128]epoll_event
    timeout := -1
    if !block {
        timeout = 0
    }
    n := epoll_wait(epfd, &events[0], int32(len(events)), timeout)
    var toRun gList
    for i := int32(0); i < n; i++ {
        ev := &events[i]
        pd := (*pollDesc)(ev.data.ptr)
        mode := int32(0)
        if ev.events&(_EPOLLIN|_EPOLLRDHUP|_EPOLLHUP|_EPOLLERR) != 0 {
            mode += 'r'
        }
        if ev.events&(_EPOLLOUT|_EPOLLHUP|_EPOLLERR) != 0 {
            mode += 'w'
        }
        if mode != 0 {
            netpollready(&toRun, pd, mode)
        }
    }
    return toRun
}

在你的服务器代码中,每个连接都在独立的goroutine中处理,Go调度器会自动将这些goroutine映射到操作系统线程上。当conn.Write()conn.Read()阻塞时,Go运行时会挂起当前goroutine,将线程让给其他可运行的goroutine,底层通过epoll监控文件描述符的状态变化。

// 你的Handler函数中,当Write阻塞时:
func Handler(conn net.Conn) {
    for {
        // 这里Write内部会调用netFD.write,最终通过pollDesc.waitWrite等待
        _, err := conn.Write(array)
        if err != nil {
            return
        }
        time.Sleep(10 * time.Second)
    }
}

// net包内部的实现示意:
func (fd *netFD) Write(p []byte) (nn int, err error) {
    for {
        n, err := syscall.Write(fd.sysfd, p[nn:])
        if err != nil {
            n = 0
            if err == syscall.EAGAIN {
                // 等待可写事件
                if err = fd.pd.waitWrite(); err == nil {
                    continue
                }
            }
        }
        nn += n
        return nn, err
    }
}

对于百万级并发连接,每个连接消耗的主要资源是文件描述符和goroutine栈内存。Go 1.4之后goroutine栈初始大小为2KB,可以通过runtime/debug.SetMaxStack调整。要真正达到百万连接,需要调整系统限制:

// 调整系统限制的示例
import "syscall"

func main() {
    var rLimit syscall.Rlimit
    syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
    rLimit.Cur = 1000000
    rLimit.Max = 1000000
    syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
    
    // 你的服务器代码...
}

所以确实不需要手动调用epoll API,Go的net包已经提供了完整的抽象。你展示的50行代码能够处理百万并发,主要得益于Go的goroutine轻量级并发模型和runtime对epoll的自动封装。

回到顶部