Golang中net.DialTCP指定本地IP地址报错:address already in use

Golang中net.DialTCP指定本地IP地址报错:address already in use 编写了一个TCP客户端,用于创建10个TCP并发连接(长时间保持),并且只启用10个临时端口使用(49001 - 49010,通过 ip_local_port_range 文件设置)。

func createConnection(c int, desAddr, desPort string) (brokerCon net.Conn, err error) {
    localips := GetLocalIP()
    maxRetry := len(localips)
    retry := 0
    for retry < maxRetry {
        lIPPort := fmt.Sprintf("%s:0", strings.Split(localips[retry].String(), "/")[0])
        fmt.Println("\n---", lIPPort)
        laddr, lerr := net.ResolveTCPAddr("tcp4", lIPPort)
        if lerr != nil {
            fmt.Println("Getting Error ResolveTCPAddr for local ", lerr)
        }
        raddr, rerr := net.ResolveTCPAddr("tcp4", desAddr+desPort)
        if rerr != nil {
            fmt.Println("Getting Error ResolveTCPAddr for local ", rerr)
        }

        brokerCon, err = net.DialTCP("tcp", laddr, raddr)
        if err != nil {
            fmt.Printf("Failed to connect connetion %d , %d retrying with seconday IP, err:\n",
                c, retry, err)
            retry = retry + 1
            time.Sleep(1 * time.Second)
            continue

        } else {
            //fmt.Println("successfull ")
            break
        }
    }
    return
}

场景 1:

如果 DialTCP 方法中的 laddr 值为 nil,它能够成功创建10个连接。DialTCP 能够使用已经被其他不同目标连接占用的端口 49009 和 49007(因为一个TCP连接由五元组决定:[本地IP, 本地端口, 远程IP, 远程端口, 协议])。

sudo netstat -anpl --tcp --udp | grep 490 
tcp        0      0 10.50.1.245:49005       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49010       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49008       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49009       XXX.XXX.XXX.X44:443     ESTABLISHED 3250/XXXX          
tcp        0      0 10.50.1.245:49006       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49002       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49004       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49001       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49007       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49003       10.50.1.41:9999         ESTABLISHED 23408/client        
tcp        0      0 10.50.1.245:49007       XXX.XXX.XXX.X29:443        ESTABLISHED 2806/XXXX 
tcp        0      0 10.50.1.245:49009       10.50.1.41:9999         ESTABLISHED 23408/client  

场景 2:

如果我传入本地IP详情(10.50.1.245)并指定端口为0,它只能创建8个连接,对于剩余的2个连接会抛出 bind: address already in use 错误。如果本地地址不为 nil,为什么 dialTCP 无法使用已经被其他不同目标连接占用的端口 49005 和 49007?

sudo netstat -anpl --tcp --udp | grep 490 
tcp        0      0 10.50.1.245:49010       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49005       XXX.XXX.XXX.X66:443        ESTABLISHED 2510/XXX 
tcp        0      0 10.50.1.245:49008       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49005       XXX.XXX.XXX.X44:443     ESTABLISHED 3250/XXX         
tcp        0      0 10.50.1.245:49006       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49002       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49004       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49001       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49003       10.50.1.41:9999         ESTABLISHED 25841/client        
tcp        0      0 10.50.1.245:49007       XXX.XXX.XXX.X29:443        ESTABLISHED 2806/XXX 
tcp        0      0 10.50.1.245:49009       10.50.1.41:9999         ESTABLISHED 25841/client  

更多关于Golang中net.DialTCP指定本地IP地址报错:address already in use的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中net.DialTCP指定本地IP地址报错:address already in use的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这个问题的根本原因是当指定本地IP地址时,Go的TCP连接行为发生了变化。当laddr为nil时,系统会自动选择可用的本地端口,即使该端口已被其他连接使用(只要五元组不同)。但当指定了具体的本地IP地址时,系统会尝试绑定到该IP地址的特定端口(即使端口为0),这时如果端口已被占用就会失败。

以下是问题分析和解决方案:

问题分析

当指定本地IP地址时,net.DialTCP会尝试绑定到该IP地址。即使端口设为0,系统也会在绑定过程中检查该IP地址上是否有冲突。问题在于Linux的端口重用策略。

解决方案

使用SO_REUSEADDRSO_REUSEPORT套接字选项来允许端口重用:

package main

import (
    "context"
    "fmt"
    "net"
    "syscall"
    "time"
)

type Dialer struct {
    control func(network, address string, c syscall.RawConn) error
}

func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
    var dialer net.Dialer
    dialer.Control = d.control
    return dialer.DialContext(ctx, network, address)
}

func createConnectionWithReuse(c int, desAddr, desPort, localIP string) (net.Conn, error) {
    dialer := &Dialer{
        control: func(network, address string, c syscall.RawConn) error {
            var opErr error
            err := c.Control(func(fd uintptr) {
                // 设置SO_REUSEADDR和SO_REUSEPORT
                opErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
                if opErr != nil {
                    return
                }
                opErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
            })
            if err != nil {
                return err
            }
            return opErr
        },
    }

    ctx := context.Background()
    localAddr := net.JoinHostPort(localIP, "0")
    remoteAddr := net.JoinHostPort(desAddr, desPort)
    
    return dialer.DialContext(ctx, "tcp4", remoteAddr)
}

// 或者使用更简洁的方式
func createConnectionSimple(c int, desAddr, desPort, localIP string) (net.Conn, error) {
    dialer := &net.Dialer{
        LocalAddr: &net.TCPAddr{
            IP: net.ParseIP(localIP),
            Port: 0,
        },
        Control: func(network, address string, c syscall.RawConn) error {
            var opErr error
            err := c.Control(func(fd uintptr) {
                opErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
                if opErr != nil {
                    return
                }
                opErr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
            })
            if err != nil {
                return err
            }
            return opErr
        },
    }
    
    return dialer.Dial("tcp4", net.JoinHostPort(desAddr, desPort))
}

// 批量创建连接的示例
func createMultipleConnections(desAddr, desPort, localIP string, count int) ([]net.Conn, error) {
    connections := make([]net.Conn, 0, count)
    
    for i := 0; i < count; i++ {
        conn, err := createConnectionSimple(i, desAddr, desPort, localIP)
        if err != nil {
            // 关闭已创建的连接
            for _, c := range connections {
                c.Close()
            }
            return nil, fmt.Errorf("failed to create connection %d: %v", i, err)
        }
        connections = append(connections, conn)
        fmt.Printf("Created connection %d: local=%s remote=%s\n", 
            i, conn.LocalAddr(), conn.RemoteAddr())
    }
    
    return connections, nil
}

func main() {
    // 使用示例
    conns, err := createMultipleConnections("10.50.1.41", "9999", "10.50.1.245", 10)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    
    // 保持连接
    for _, conn := range conns {
        defer conn.Close()
    }
    
    // 保持程序运行
    select {}
}

关键点说明

  1. SO_REUSEADDR:允许绑定到处于TIME_WAIT状态的地址
  2. SO_REUSEPORT:允许多个套接字绑定到相同的IP地址和端口组合

这两个选项的组合允许在指定本地IP地址时重用端口,即使该端口已被其他连接使用(只要五元组不同)。

替代方案

如果不想使用套接字选项,可以考虑让系统自动选择本地地址:

func createConnectionAuto(c int, desAddr, desPort string) (net.Conn, error) {
    // 不指定LocalAddr,让系统自动选择
    dialer := &net.Dialer{
        Timeout: 30 * time.Second,
    }
    
    return dialer.Dial("tcp4", net.JoinHostPort(desAddr, desPort))
}

这种方法最简单,但无法控制使用哪个本地IP地址(在多IP系统中)。

回到顶部