sockets - 为什么在并发连接到服务器时接受两个相同的 5 元组套接字?

标签 sockets go networking tcp

server.go

package main

import (
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    _ "net/http/pprof"
    "sync"
    "syscall"
)

type ConnSet struct {
    data  map[int]net.Conn
    mutex sync.Mutex
}

func (m *ConnSet) Update(id int, conn net.Conn) error {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    if _, ok := m.data[id]; ok {
        fmt.Printf("add: key %d existed \n", id)
        return fmt.Errorf("add: key %d existed \n", id)
    }
    m.data[id] = conn
    return nil
}

var connSet = &ConnSet{
    data: make(map[int]net.Conn),
}

func main() {
    setLimit()

    ln, err := net.Listen("tcp", ":12345")
    if err != nil {
        panic(err)
    }

    go func() {
        if err := http.ListenAndServe(":6060", nil); err != nil {
            log.Fatalf("pprof failed: %v", err)
        }
    }()

    var connections []net.Conn
    defer func() {
        for _, conn := range connections {
            conn.Close()
        }
    }()

    for {
        conn, e := ln.Accept()
        if e != nil {
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                log.Printf("accept temp err: %v", ne)
                continue
            }

            log.Printf("accept err: %v", e)
            return
        }
        port := conn.RemoteAddr().(*net.TCPAddr).Port
        connSet.Update(port, conn)
        go handleConn(conn)
        connections = append(connections, conn)
        if len(connections)%100 == 0 {
            log.Printf("total number of connections: %v", len(connections))
        }
    }
}

func handleConn(conn net.Conn) {
    io.Copy(ioutil.Discard, conn)
}

func setLimit() {
    var rLimit syscall.Rlimit
    if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
        panic(err)
    }
    rLimit.Cur = rLimit.Max
    if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
        panic(err)
    }

    log.Printf("set cur limit: %d", rLimit.Cur)
}

客户端
package main

import (
    "bytes"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "strconv"
    "sync"
    "syscall"
    "time"
)

var portFlag = flag.Int("port", 12345, "port")

type ConnSet struct {
    data  map[int]net.Conn
    mutex sync.Mutex
}

func (m *ConnSet) Update(id int, conn net.Conn) error {
    m.mutex.Lock()
    defer m.mutex.Unlock()
    if _, ok := m.data[id]; ok {
        fmt.Printf("add: key %d existed \n", id)
        return fmt.Errorf("add: key %d existed \n", id)
    }
    m.data[id] = conn
    return nil
}

var connSet = &ConnSet{
    data: make(map[int]net.Conn),
}

func echoClient() {
    addr := fmt.Sprintf("127.0.0.1:%d", *portFlag)
    dialer := net.Dialer{}
    conn, err := dialer.Dial("tcp", addr)
    if err != nil {
        fmt.Println("ERROR", err)
        os.Exit(1)
    }
    port := conn.LocalAddr().(*net.TCPAddr).Port
    connSet.Update(port, conn)
    defer conn.Close()

    for i := 0; i < 10; i++ {
        s := fmt.Sprintf("%s", strconv.Itoa(i))
        _, err := conn.Write([]byte(s))
        if err != nil {
            log.Println("write error: ", err)
        }
        b := make([]byte, 1024)
        _, err = conn.Read(b)
        switch err {
        case nil:
            if string(bytes.Trim(b, "\x00")) != s {
                log.Printf("resp req not equal, req: %d, res: %s", i, string(bytes.Trim(b, "\x00")))
            }
        case io.EOF:
            fmt.Println("eof")
            break
        default:
            fmt.Println("ERROR", err)
            break
        }
    }
    time.Sleep(time.Hour)
    if err := conn.Close(); err != nil {
        log.Printf("client conn close err: %s", err)
    }
}

func main() {
    flag.Parse()
    setLimit()
    before := time.Now()
    var wg sync.WaitGroup
    for i := 0; i < 20000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            echoClient()
        }()
    }
    wg.Wait()
    fmt.Println(time.Now().Sub(before))
}

func setLimit() {
    var rLimit syscall.Rlimit
    if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
        panic(err)
    }
    rLimit.Cur = rLimit.Max
    if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
        panic(err)
    }

    log.Printf("set cur limit: %d", rLimit.Cur)
}

运行命令
go run server.go
---
go run client.go

服务器运行截图

enter image description here

客户端同时向服务器发起 20,000 个连接,服务器接受了两个完全相同的 remotePort 连接(在极短的时间内)。

我尝试使用 tcpconn.py来自 bcc(从 tcpconnect.py 通过添加 skc_num(aka: local_port) 修补)
enter image description here

tcpaccept.py
enter image description here
跟踪连接,在客户端没有重复的情况下,也发现远程端口在服务器端重复

以我的理解,socket的5元组不会重复,为什么服务器接受两个远程端口完全相同的socket?

我的测试环境:

Fedora 31,内核版本 5.3.15 x86_64

Ubuntu 18.04.3 LTS,内核版本 4.19.1 x86_64

转到版本 go1.13.5 linux/amd64

Wireshark :
服务器 TCP 保持连接到 ACK 和 PSH+ACK
dup flow

服务器 TCP Keep-Alive 仅对 PSH+ACK
no dup

最佳答案

连接已添加到 map data map[int]net.Conn当它建立时,但当连接关闭时,它不会从 map 中删除。因此,如果连接关闭,其端口将变为空闲状态,并且可以由操作系统重新使用以进行下一次连接。这就是您可以看到重复端口的原因。
尝试在关闭时从 map 中删除端口。

关于sockets - 为什么在并发连接到服务器时接受两个相同的 5 元组套接字?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59502881/

相关文章:

java - 打开 tcp InetSocketAddress 到应用程序路径(不仅仅是主机 + 端口)

c++ - 用于线程间通信的 Windows 套接字

golang 测试命令行参数

java - 如何通过 SOCKS 代理使用 URLConnection?

windows - 可以连接电脑控制鼠标或键盘吗?

java - 在 readLine() 之后使用 readLong() 进行套接字通信

Java RMI 连接池详细信息

regex - 路由 http 请求时奇怪的 Go 正则表达式不匹配

go - 在 Go 中,即使 []byte 是按值传递给方法的,但还是会修改原始值?

无法通过打开的端口访问 Docker 应用程序