net 包是 Go 语言标准库中网络编程的核心基石,它提供了跨平台的网络 I/O 抽象,支持 TCP/IP、UDP、域名解析、Unix 域套接字等几乎所有主流网络协议。无论是构建简单的客户端 / 服务端,还是开发复杂的分布式系统,net 包都是最底层的支撑工具。
本文档将从基础到高级,全面解析 net 包的核心功能,针对常用功能进行详细展开,并提供可直接运行的实战代码示例。
1. 包概述
net 包的设计理念是统一接口,分层抽象:
-
高层接口:Dial、Listen、Accept 等通用函数,屏蔽底层协议差异
-
中层接口:Conn、Listener、PacketConn 等接口,定义通用的读写、监听行为
-
底层实现:针对 TCP、UDP、IP、Unix 等不同协议的具体实现,提供协议特有的配置能力
大多数场景下,开发者只需要使用高层接口即可完成开发;而在需要精细控制网络行为的场景下,可以深入到底层实现进行调优。
2. 核心基础类型
网络编程的第一步是处理地址,net 包提供了多种地址类型来适配不同的协议。
2.1 IP 地址(IP 类型)
IP 类型是一个字节切片,用于表示 IPv4 或 IPv6 地址,它提供了一系列方法来判断地址的属性。
核心方法与示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package main
import (
"fmt"
"net"
)
func main() {
// 1. 解析IP地址
ip := net.ParseIP("192.168.1.100")
if ip == nil {
fmt.Println("无效的IP地址")
return
}
// 2. 判断地址类型
fmt.Println("是否为回环地址:", ip.IsLoopback()) // false
fmt.Println("是否为私有地址:", ip.IsPrivate()) // true
fmt.Println("是否为IPv4地址:", ip.To4() != nil) // true
// To4方法会返回IPv4地址的4字节表示,如果不是IPv4地址则返回nil;To16方法会返回IPv6地址的16字节表示,如果不是IPv6地址则返回nil。
// 3. 构造IPv4地址
ipv4 := net.IPv4(127, 0, 0, 1)
fmt.Println("回环地址:", ipv4.String()) // 127.0.0.1
// 4. 掩码计算
ip2 := net.ParseIP("192.168.1.100")
mask := net.IPv4Mask(255, 255, 255, 0)
networkIP := ip2.Mask(mask)
fmt.Println("网段地址:", networkIP.String()) // 192.168.1.0
}
|
2.2 IP 网段(IPNet 类型)
IPNet 用于表示一个 IP 网段,常用于判断某个 IP 是否属于某个子网,这在防火墙、路由匹配等场景中非常常用。
核心方法与示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 1. 解析CIDR格式的网段
ip, ipNet, err := net.ParseCIDR("192.168.1.50/24")
if err != nil {
log.Fatal(err) // 解析失败,CIDR格式不正确,或者IP地址无效。
// log.Fatal会导致程序崩溃,适合在示例代码中使用,但在生产环境中应该使用更优雅的错误处理方式,如log.Printf或返回错误给调用者。
}
fmt.Println("IP地址:", ip.String()) // 192.168.1.0
fmt.Println("网段:", ipNet.String()) // 192.168.1.0/24
// 2. 判断IP是否属于该网段
testIP := net.ParseIP("192.168.1.100")
fmt.Println(testIP.String(), "是否在网段内:", ipNet.Contains(testIP)) // true
testIP2 := net.ParseIP("8.8.8.8")
fmt.Println(testIP2.String(), "是否在网段内:", ipNet.Contains(testIP2)) // false
}
|
2.3 传输层地址(TCPAddr/UDPAddr)
TCP 和 UDP 地址都包含 IP 和端口两部分,用于标识网络中的一个端点。
地址解析示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 解析TCP地址
tcpAddr, err := net.ResolveTCPAddr("tcp", "localhost:8080")
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("TCP地址:", tcpAddr.String()) // 127.0.0.1:8080
// 解析UDP地址
udpAddr, err := net.ResolveUDPAddr("udp", "localhost:8080")
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("UDP地址:", udpAddr.String()) // 127.0.0.1:8080
}
|
2.4 地址处理工具函数
处理 host:port 格式的地址时,JoinHostPort 和 SplitHostPort 是最常用的工具,它们能正确处理 IPv6 地址的括号格式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
"fmt"
"log"
"net"
)
func main() {
// 拼接主机和端口
host := "127.0.0.1"
port := "8080"
addr := net.JoinHostPort(host, port)
fmt.Println("拼接后地址:", addr) // 127.0.0.1:8080
// 拆分地址
host2, port2, err := net.SplitHostPort(addr)
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("拆分后主机:", host2, "端口:", port2)
// 正确处理IPv6地址
ipv6Addr := "[::1]:8080"
host3, port3, _ := net.SplitHostPort(ipv6Addr)
fmt.Println("IPv6拆分后主机:", host3, "端口:", port3) // ::1 8080
}
|
3. TCP 网络编程(面向连接可靠传输)
TCP 是面向连接的可靠传输协议,是互联网中最常用的传输层协议。net 包通过 TCPListener 和 TCPConn 来实现 TCP 通信。
3.1 基础 TCP 服务端
TCP 服务端的工作流程是:监听端口 -> 接受连接 -> 处理连接。Go 中通过 goroutine 可以轻松实现高并发的服务端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
package main
import (
"bufio" // 提供了缓冲读取和写入功能,适用于网络连接的读写操作,可以按行读取数据。
"fmt"
"net"
"log"
)
func handleConnection(conn *net.TCPConn) {
// 确保连接关闭
defer conn.Close()
log.Printf("客户端 %s 已连接", conn.RemoteAddr())
// 读取客户端数据
reader := bufio.NewReader(conn)
for {
// 按行读取数据
data, err := reader.ReadString('\n')
if err != nil {
log.Printf("客户端 %s 已断开", conn.RemoteAddr())
return
}
log.Printf("收到来自 %s 的消息: %s", conn.RemoteAddr(), data)
// 回显消息
_, err = conn.Write([]byte("Echo: " + data))
if err != nil {
log.Printf("发送数据失败: %v", err)
return
}
}
}
func main() {
// 解析监听地址
addr, err := net.ResolveTCPAddr("tcp", ":8080") // 监听8080端口
if err != nil {
log.Println(err.Error())
return
}
// 监听端口
listener, err := net.ListenTCP("tcp", addr)
if err != nil {
log.Println(err.Error())
return
}
defer listener.Close()
log.Println("TCP服务端已启动,监听 :8080")
for {
// 接受新连接
conn, err := listener.AcceptTCP()
if err != nil {
log.Printf("接受连接失败: %v", err)
continue
}
// 启动goroutine处理连接,实现并发
go handleConnection(conn)
}
}
|
3.2 基础 TCP 客户端
TCP 客户端通过主动连接服务端来建立连接,然后进行数据读写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
package main
import (
"bufio"
"fmt"
"net"
"os"
"log"
)
func main() {
// 连接服务端,中间这个nil表示本地地址,通常设置为nil让系统自动分配一个可用的端口。
conn, err := net.DialTCP("tcp", nil, &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080})
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
log.Println("已连接到服务端")
// 从标准输入读取消息
scanner := bufio.NewScanner(os.Stdin) // 从标准输入读取用户输入,os.Stdin表示标准输入设备,通常是键盘。
writer := bufio.NewWriter(conn)
// 创建一个缓冲写入器,用于向TCP连接写数据,它会将数据先写入内存缓冲区,只有在调用Flush方法时才会真正发送到网络上,这样可以提高效率,减少系统调用次数。
// conn是之前建立的TCP连接,bufio.NewWriter(conn)创建了一个新的缓冲写入器,将TCP连接作为底层的写入目标。
for scanner.Scan() {
text := scanner.Text()
// 发送消息
_, err = writer.WriteString(text + "\n")
if err != nil {
log.Println(err.Error())
return
}
writer.Flush()
// 读取响应
resp, err := bufio.NewReader(conn).ReadString('\n') // 以换行符为分隔符读取服务端响应,直到遇到换行符为止。
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("服务端响应:", resp)
}
}
|
如此即可实现一个简单的 TCP 客户端,能够向服务端发送消息并接收响应。
3.3 TCP 连接高级配置
TCPConn 提供了一系列方法来调整 TCP 协议的参数,以适配不同的业务场景。
3.3.1 禁用 Nagle 算法(SetNoDelay)
Nagle 算法是 TCP 协议中的一个优化机制,它会将小数据包合并成一个大数据包发送,以减少网络开销。但在某些实时性要求高的场景下(如在线游戏、即时通信),这种合并会引入不必要的延迟,因此需要禁用 Nagle 算法。
Nagle 算法默认会合并小数据包以减少网络开销,但会引入延迟。对于实时性要求高的场景(如游戏、即时通信),需要禁用它。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 建立连接后
conn, err := net.DialTCP("tcp", nil, serverAddr)
if err != nil {
log.Println(err.Error())
return
}
// 禁用Nagle算法,小数据包立即发送
err = conn.SetNoDelay(true)
if err != nil {
log.Printf("设置NoDelay失败: %v", err)
}
|
3.3.2 TCP 保活机制(SetKeepAlive)
TCP 保活机制可以自动检测死连接,避免长时间占用资源。Go 1.13+ 还支持更精细的保活配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 启用保活机制
err = conn.SetKeepAlive(true)
if err != nil {
log.Printf("启用保活失败: %v", err)
}
// 方式1:简单设置保活间隔(旧API)
err = conn.SetKeepAlivePeriod(30 * time.Second)
if err != nil {
log.Printf("设置保活间隔失败: %v", err)
}
// 方式2:精细配置保活参数(Go 1.13+ 新API)
err = conn.SetKeepAliveConfig(net.KeepAliveConfig{
Enable: true,
Idle: 30 * time.Second, // 空闲30秒后开始发送探针
Interval: 10 * time.Second, // 探针间隔10秒
Count: 3, // 连续3次失败则断开连接
})
if err != nil {
log.Printf("设置保活配置失败: %v", err)
}
|
3.3.3 连接半关闭
TCP 支持半关闭,即可以单独关闭读或写通道,这在某些场景下非常有用(比如发送完数据后告诉服务端不再发送,但还能接收响应)。
1
2
3
4
5
6
7
8
9
10
11
|
// 发送完所有数据后,关闭写通道
err = conn.CloseWrite()
if err != nil {
log.Printf("关闭写通道失败: %v", err)
}
// 此时还可以继续读取服务端的响应
data, err := conn.Read(buf)
// 读取完后,关闭读通道
err = conn.CloseRead()
|
4. UDP 网络编程(无连接数据报传输)
UDP 是无连接的传输协议,不需要建立连接,直接发送数据包,适用于对实时性要求高、可容忍少量丢包的场景(如音视频、心跳检测)。
4.1 基础 UDP 服务端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package main
import (
"fmt"
"net"
"log"
)
func main() {
// 解析UDP地址
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
log.Println(err.Error())
return
}
// 监听UDP端口
conn, err := net.ListenUDP("udp", addr)
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
log.Println("UDP服务端已启动,监听 :8080")
buf := make([]byte, 1024)
for {
// 读取UDP数据包,同时获取客户端地址
n, clientAddr, err := conn.ReadFromUDP(buf)
if err != nil {
log.Printf("读取数据失败: %v", err)
continue
}
data := string(buf[:n])
log.Printf("收到来自 %s 的消息: %s", clientAddr, data)
// 向客户端发送响应
resp := []byte("UDP Ack: " + data)
_, err = conn.WriteToUDP(resp, clientAddr)
if err != nil {
log.Printf("发送响应失败: %v", err)
}
}
}
|
4.2 基础 UDP 客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package main
import (
"fmt"
"net"
"log"
)
func main() {
// 服务端地址
serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
if err != nil {
log.Println(err.Error())
return
}
// 建立UDP连接(UDP的连接只是逻辑绑定,不发送网络包),中间这个nil表示本地地址,通常设置为nil让系统自动分配一个可用的端口。
conn, err := net.DialUDP("udp", nil, serverAddr)
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
// 发送数据
_, err = conn.Write([]byte("Hello UDP Server!"))
if err != nil {
log.Println(err.Error())
return
}
// 读取响应
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("服务端响应:", string(buf[:n]))
}
|
如此即可实现一个简单的 UDP 客户端,能够向服务端发送消息并接收响应。
4.3 实战:UDP 心跳检测
UDP 非常适合做心跳检测,因为它轻量、无连接开销。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
package main
import (
"net"
"time"
"log"
)
func main() {
serverAddr, _ := net.ResolveUDPAddr("udp", "127.0.0.1:8080")
conn, _ := net.DialUDP("udp", nil, serverAddr)
defer conn.Close()
// 定时器,每秒发送一次心跳
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for range ticker.C {
// 发送心跳包
_, err := conn.Write([]byte("HEARTBEAT"))
if err != nil {
log.Printf("发送心跳失败: %v", err)
continue
}
// 设置读取超时,检测服务端是否存活
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
log.Printf("心跳检测失败,服务端可能已下线: %v", err)
continue
}
log.Printf("心跳正常,服务端响应: %s", string(buf[:n]))
}
}
|
如图所示,UDP 心跳检测可以快速判断服务端的存活状态,适用于分布式系统中的健康检查和服务发现。
5. DNS 域名解析
net 包提供了完整的 DNS 解析能力,从基础的域名转 IP,到高级的 MX、SRV、TXT 记录查询。
5.1 基础域名解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package main
import (
"fmt"
"net"
"log"
)
func main() {
// 解析域名对应的IP列表(同时返回IPv4和IPv6)
ips, err := net.LookupIP("google.com")
if err != nil {
log.Println(err.Error())
return
}
for _, ip := range ips {
fmt.Println("IP地址:", ip.String())
}
// 反向解析:根据IP查找域名
names, err := net.LookupAddr("8.8.8.8")
if err != nil {
log.Println(err.Error())
return
}
for _, name := range names {
fmt.Println("8.8.8.8对应的域名:", name)
}
}
|
解析结果如下图所示:
5.2 高级 DNS 记录查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package main
import (
"fmt"
"net"
"log"
)
func main() {
// 1. 查询MX记录(邮件服务器记录)。MX记录是Mail Exchanger Record的缩写,邮件交换记录,用于指定处理该域邮件的服务器地址和优先级。
mxs, err := net.LookupMX("gmail.com")
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("Gmail的邮件服务器:")
for _, mx := range mxs {
fmt.Printf(" 优先级: %d, 服务器: %s\n", mx.Pref, mx.Host)
}
// 2. 查询SRV记录(服务发现记录,常用于微服务)。SRV记录是Service Record的缩写,用于指定提供特定服务的服务器地址、端口和优先级,常用于微服务架构中的服务发现。
cname, srvs, err := net.LookupSRV("xmpp-server", "tcp", "gmail.com")
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("\nXMPP服务的SRV记录:")
fmt.Println("CNAME:", cname)
for _, srv := range srvs {
fmt.Printf(" 目标: %s:%d, 优先级: %d, 权重: %d\n", srv.Target, srv.Port, srv.Priority, srv.Weight)
}
// 3. 查询TXT记录(常用于验证域名、SPF、DKIM等)。TXT记录是Text Record的缩写,用于存储任意文本信息,常用于域名验证、SPF(Sender Policy Framework)和DKIM(DomainKeys Identified Mail)等邮件安全机制。
txts, err := net.LookupTXT("gmail.com")
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("\nTXT记录:")
for _, txt := range txts {
fmt.Println(" ", txt)
}
}
|
查询结果如下图所示:
5.3 自定义 DNS 解析器
默认情况下,net 包使用系统的 DNS 配置,但我们可以自定义 DNS 服务器,比如使用 Google DNS 或 Cloudflare DNS。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package main
import (
"context"
"fmt"
"net"
"time"
)
func main() {
// 自定义DNS解析器,使用Google的8.8.8.8
resolver := &net.Resolver{
// 强制使用Go原生的DNS解析器,不使用系统的cgo解析
PreferGo: true,
// 自定义DNS服务器的连接
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: 3 * time.Second,
}
// 连接到自定义的DNS服务器
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}
// 使用自定义解析器解析域名
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ips, err := resolver.LookupIP(ctx, "ip4", "google.com")
if err != nil {
log.Println(err.Error())
return
}
for _, ip := range ips {
fmt.Println("通过8.8.8.8解析到的IP:", ip.String())
}
}
|
解析结果如下图所示:
6. 错误处理与超时控制
网络编程中,错误处理和超时控制是保证服务稳定性的关键。
6.1 网络错误类型(net.Error)
net 包的所有网络错误都实现了 net.Error 接口,它提供了 Timeout() 和 Temporary() 方法来区分错误类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"fmt"
"net"
"time"
)
func main() {
// 这里用google.com:80作为测试地址,设置一个非常短的超时时间来触发错误
conn, err := net.DialTimeout("tcp", "google.com:80", 1*time.Second)
if err != nil {
// 判断错误是否为网络错误
if ne, ok := err.(net.Error); ok {
fmt.Println("是否为超时错误:", ne.Timeout())
fmt.Println("是否为临时错误:", ne.Temporary())
}
fmt.Println("错误信息:", err)
return
}
defer conn.Close()
fmt.Println("连接成功")
}
|
之所以 Timeout() 和 Temporary() 都返回 true,是因为在这个场景下,连接超时被认为是一个临时错误,可能是网络状况不佳导致的,重试可能会成功。
6.2 超时控制详解
net 包提供了多层级的超时控制:
1. 连接超时
在建立连接时设置超时,避免长时间卡在连接阶段。
1
2
3
4
5
6
7
8
|
// 方式1:使用DialTimeout
conn, err := net.DialTimeout("tcp", "google.com:80", 5*time.Second)
// 方式2:使用Dialer+Context
d := net.Dialer{Timeout: 5*time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := d.DialContext(ctx, "tcp", "google.com:80")
|
2. 读写超时
连接建立后,为读写操作设置超时,避免单个读写操作阻塞。
1
2
3
4
5
6
7
8
|
// 统一设置读写超时
conn.SetDeadline(time.Now().Add(30 * time.Second))
// 单独设置读超时
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
// 单独设置写超时
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
7. 高级连接配置:Dialer
Dialer 是 net 包中用于配置连接的核心结构体,它可以统一配置连接的超时、保活、DNS 解析器等参数。
7.1 Dialer 核心配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package main
import (
"context"
"fmt"
"log"
"net"
"time"
)
func main() {
// 配置Dialer
dialer := &net.Dialer{
// 连接超时:如果在指定时间内无法建立连接,则返回错误
Timeout: 5 * time.Second,
// 保活间隔:每隔30秒发送一次TCP保活探针
KeepAlive: 30 * time.Second,
// 本地地址,用于指定出站网卡
LocalAddr: nil,
// 自定义DNS解析器
Resolver: &net.Resolver{
// 强制使用Go原生的DNS解析器,不使用系统的cgo解析
PreferGo: true,
// 自定义DNS服务器的连接
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
return net.Dial("udp", "8.8.8.8:53")
},
},
}
// 使用Dialer建立连接
conn, err := dialer.DialContext(context.Background(), "tcp", "google.com:80")
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
fmt.Println("连接成功")
}
|
如果连不上会得到以下输出:
如果连接成功会得到以下输出:
8. Unix 域套接字(本地进程间通信)
Unix 域套接字是用于同一主机上进程间通信的机制,比 TCP 更快,不需要经过网络协议栈,延迟更低。
8.1 流式 Unix 域套接字(类似 TCP)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
// 服务端
package main
import (
"net"
"os"
"log"
)
func main() {
sockPath := "/tmp/test.sock"
// 清理旧的sock文件
os.Remove(sockPath)
// 监听Unix域套接字
listener, err := net.Listen("unix", sockPath)
if err != nil {
log.Println(err.Error())
return
}
defer listener.Close()
defer os.Remove(sockPath)
log.Println("Unix服务端已启动")
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go func(c net.Conn) {
defer c.Close()
// 处理连接,和TCP完全一样
buf := make([]byte, 1024)
n, _ := c.Read(buf)
c.Write([]byte("Echo: " + string(buf[:n])))
}(conn)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 客户端
package main
import (
"fmt"
"net"
)
func main() {
// 连接Unix域套接字
conn, err := net.Dial("unix", "/tmp/test.sock")
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
conn.Write([]byte("Hello IPC!"))
buf := make([]byte, 1024)
n, _ := conn.Read(buf)
fmt.Println(string(buf[:n]))
}
|
9. 原始 IP 与 ICMP:实现 Ping 工具
通过 net 包的原始 IP 连接,我们可以实现 ICMP 协议,也就是常用的 ping 工具。
注意:ICMP 原始套接字在部分系统上需要 root 权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
package main
import (
"fmt"
"net"
"time"
)
// 计算ICMP校验和
func checkSum(msg []byte) uint16 {
sum := 0
for i := 0; i < len(msg)-1; i += 2 {
sum += int(msg[i])*256 + int(msg[i+1])
}
sum = (sum >> 16) + (sum & 0xffff)
sum += sum >> 16
return uint16(^sum)
}
func ping(host string) error {
// 建立ICMP连接
conn, err := net.Dial("ip4:icmp", host)
if err != nil {
return err
}
defer conn.Close()
// 构造ICMP Echo请求包
msg := make([]byte, 64)
msg[0] = 8 // Type: Echo Request
msg[1] = 0 // Code: 0
// Checksum占位
msg[2] = 0
msg[3] = 0
// Identifier
id := uint16(1234)
msg[4] = byte(id >> 8)
msg[5] = byte(id & 0xff)
// Sequence
seq := uint16(1)
msg[6] = byte(seq >> 8)
msg[7] = byte(seq & 0xff)
// 计算校验和
sum := checkSum(msg)
msg[2] = byte(sum >> 8)
msg[3] = byte(sum & 0xff)
start := time.Now()
// 发送ICMP包
_, err = conn.Write(msg)
if err != nil {
return err
}
// 设置超时
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
recv := make([]byte, 1024)
n, err := conn.Read(recv)
if err != nil {
return err
}
duration := time.Since(start)
// 解析响应,ICMP响应的Type为0
if recv[20] == 0 { // 跳过20字节IP头
fmt.Printf("%s is up, 延迟: %v\n", host, duration)
return nil
}
return fmt.Errorf("收到非预期的响应")
}
func main() {
err := ping("8.8.8.8")
if err != nil {
fmt.Println("Ping失败:", err)
}
}
|
10. 网络接口与本机信息获取
net 包还提供了获取本机网卡、IP 信息的能力。
获取本机所有非回环 IP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
"fmt"
"net"
)
func main() {
// 获取所有网络接口的地址
addrs, err := net.InterfaceAddrs()
if err != nil {
log.Println(err.Error())
return
}
for _, addr := range addrs {
// 转换为IPNet
if ipNet, ok := addr.(*net.IPNet); ok {
// 排除回环地址
if !ipNet.IP.IsLoopback() {
// 只打印IPv4地址
if ipNet.IP.To4() != nil {
fmt.Println("本机IP:", ipNet.IP.String())
}
}
}
}
}
|
11. 其他常用工具示例
11.1 简单端口扫描
通过 DialTimeout 我们可以快速实现一个简单的端口扫描工具,检测目标主机的端口是否开放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
package main
import (
"fmt"
"net"
"sync"
"time"
)
func main() {
target := "localhost"
var wg sync.WaitGroup
// 扫描1-1024端口
for port := 1; port <= 1024; port++ {
wg.Add(1)
go func(p int) {
defer wg.Done()
address := fmt.Sprintf("%s:%d", target, p)
// 用500ms超时检测端口
conn, err := net.DialTimeout("tcp", address, 500*time.Millisecond)
if err == nil {
fmt.Printf("端口 %d 开放\n", p)
conn.Close()
}
}(port)
}
wg.Wait()
fmt.Println("扫描完成")
}
|
11.2 内存管道 Pipe
Pipe 函数可以创建一对内存中的连接,用于进程内的通信,它实现了 net.Conn 接口,所以可以像网络连接一样进行读写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package main
import (
"fmt"
"net"
)
func main() {
// 创建内存管道
r, w := net.Pipe()
// 写入端goroutine
go func() {
defer w.Close()
// 写入数据
_, err := w.Write([]byte("Hello from Pipe!"))
if err != nil {
log.Println(err.Error())
return
}
}()
// 读取端
buf := make([]byte, 1024)
n, err := r.Read(buf)
if err != nil {
log.Println(err.Error())
return
}
fmt.Println("读取到的数据:", string(buf[:n]))
}
|
11.3 Go 1.26 新特性:Dialer 新方法
Go 1.26 为 Dialer 新增了针对特定协议的方法,比如 DialTCP、DialUDP,这些方法支持预解析的地址,兼顾了预解析的效率和 context 的可取消性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
package main
import (
"context"
"fmt"
"net"
"net/netip"
"time"
)
func main() {
// 预解析地址,避免每次连接都解析
raddr := netip.MustParseAddrPort("127.0.0.1:8080")
dialer := &net.Dialer{
Timeout: 5 * time.Second,
}
// 使用新的DialTCP方法,支持context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := dialer.DialTCP(ctx, "tcp", netip.AddrPort{}, raddr)
if err != nil {
log.Println(err.Error())
return
}
defer conn.Close()
fmt.Println("连接成功")
}
|
11.4 自定义 DNS 在 HTTP 客户端中的应用
我们可以将自定义的 DNS 解析器和 HTTP 客户端结合,让 HTTP 请求也使用自定义的 DNS 服务器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
package main
import (
"context"
"fmt"
"net"
"net/http"
"io/ioutil"
"time"
)
func main() {
// 自定义DNS解析器,使用Cloudflare的1.1.1.1
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: 3 * time.Second}
return d.DialContext(ctx, "udp", "1.1.1.1:53")
},
}
// 自定义Transport的DialContext
transport := &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// 拆分主机和端口
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
// 用自定义解析器解析主机
ips, err := resolver.LookupHost(ctx, host)
if err != nil {
return nil, err
}
// 逐个尝试连接解析到的IP
for _, ip := range ips {
dialer := &net.Dialer{Timeout: 5 * time.Second}
conn, err := dialer.DialContext(ctx, network, net.JoinHostPort(ip, port))
if err == nil {
return conn, nil
}
}
return nil, fmt.Errorf("所有IP连接失败")
},
}
// 创建使用自定义DNS的HTTP客户端
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
// 发送请求
resp, err := client.Get("https://example.com")
if err != nil {
log.Println(err.Error())
return
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println("响应状态:", resp.Status)
fmt.Println("响应长度:", len(body))
}
|
12. 避坑指南
-
始终处理错误:网络操作的错误必须处理,不能忽略,尤其是 Accept、Read、Write 这些核心操作。
-
资源释放:使用 defer conn.Close() 确保连接关闭,避免文件描述符泄漏。
-
超时控制:所有网络操作都要设置超时,避免阻塞导致服务不可用。
-
并发处理:TCP 服务端必须用 goroutine 处理每个连接,才能实现高并发。
-
参数调优:
-
DNS 缓存:默认的 DNS 解析没有缓存,高并发场景下建议自己实现 DNS 缓存,避免频繁解析。
-
Unix 域套接字:本地进程间通信优先使用 Unix 域套接字,比 TCP 性能更好。
通过以上内容,你已经基本掌握了 Go net 包的核心用法,从基础的 TCP/UDP 通信,到高级的 DNS 解析、ICMP 实现,都可以基于此进行开发。无论是构建简单的网络工具,还是开发复杂的分布式系统,net 包都是你不可或缺的利器。希望这份文档能帮助你更好地理解和使用 Go 的网络编程能力!