【Go语言-Day 49】网络编程入门:用 net 包亲手打造你的第一个 TCP 服务器
Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
Python系列文章目录
Go语言系列文章目录
01-【Go语言-Day 1】扬帆起航:从零到一,精通 Go 语言环境搭建与首个程序
02-【Go语言-Day 2】代码的基石:深入解析Go变量(var, :=)与常量(const, iota)
03-【Go语言-Day 3】从零掌握 Go 基本数据类型:string, rune 和 strconv 的实战技巧
04-【Go语言-Day 4】掌握标准 I/O:fmt 包 Print, Scan, Printf 核心用法详解
05-【Go语言-Day 5】掌握Go的运算脉络:算术、逻辑到位的全方位指南
06-【Go语言-Day 6】掌控代码流:if-else 条件判断的四种核心用法
07-【Go语言-Day 7】循环控制全解析:从 for 基础到 for-range 遍历与高级控制
08-【Go语言-Day 8】告别冗长if-else:深入解析 switch-case 的优雅之道
09-【Go语言-Day 9】指针基础:深入理解内存地址与值传递
10-【Go语言-Day 10】深入指针应用:解锁函数“引用传递”与内存分配的秘密
11-【Go语言-Day 11】深入浅出Go语言数组(Array):从基础到核心特性全解析
12-【Go语言-Day 12】解密动态数组:深入理解 Go 切片 (Slice) 的创建与核心原理
13-【Go语言-Day 13】切片操作终极指南:append、copy与内存陷阱解析
14-【Go语言-Day 14】深入解析 map:创建、增删改查与“键是否存在”的奥秘
15-【Go语言-Day 15】玩转 Go Map:从 for range 遍历到 delete 删除的终极指南
16-【Go语言-Day 16】从零掌握 Go 函数:参数、多返回值与命名返回值的妙用
17-【Go语言-Day 17】函数进阶三部曲:变参、匿名函数与闭包深度解析
18-【Go语言-Day 18】从入门到精通:defer、return 与 panic 的执行顺序全解析
19-【Go语言-Day 19】深入理解Go自定义类型:Type、Struct、嵌套与构造函数实战
20-【Go语言-Day 20】从理论到实践:Go基础知识点回顾与综合编程挑战
21-【Go语言-Day 21】从值到指针:一文搞懂 Go 方法 (Method) 的核心奥秘
22-【Go语言-Day 22】解耦与多态的基石:深入理解 Go 接口 (Interface) 的核心概念
23-【Go语言-Day 23】接口的进阶之道:空接口、类型断言与 Type Switch 详解
24-【Go语言-Day 24】从混乱到有序:Go 语言包 (Package) 管理实战指南
25-【Go语言-Day 25】从go.mod到go.sum:一文彻底搞懂Go Modules依赖管理
26-【Go语言-Day 26】深入解析error:从errors.New到errors.As的演进之路
27-【Go语言-Day 27】驾驭 Go 的异常处理:panic 与 recover 的实战指南与陷阱分析
28-【Go语言-Day 28】文本处理利器:strings 包函数全解析与实战
29-【Go语言-Day 29】从time.Now()到Ticker:Go语言time包实战指南
30-【Go语言-Day 30】深入探索Go文件读取:从os.ReadFile到bufio.Scanner的全方位指南
31-【Go语言-Day 31】精通文件写入与目录管理:os与filepath包实战指南
32-【Go语言-Day 32】从零精通 Go JSON:Marshal、Unmarshal 与 Struct Tag 实战指南
33-【Go语言-Day 33】告别“能跑就行”:手把手教你用testing包写出高质量的单元测试
34-【Go语言-Day 34】告别凭感觉优化:手把手教你 Go Benchmark 性能测试
35-【Go语言-Day 35】Go 反射核心:reflect 包从入门到精通
36-【Go语言-Day 36】构建专业命令行工具:flag 包入门与实战
37-【Go语言-Day 37】深入C世界:Go与C语言交互的桥梁——Cgo入门指南
38-【Go语言-Day 38】编写地道Go代码:Go语言官方代码规范与最佳实践深度解析
39-【Go语言-Day 39】Go 工具链深度游:掌握 build, vet, pprof 和交叉编译四大神器
40-【Go语言-Day 40】项目实战:从零到一打造一个功能完备的命令行 Todo List 应用
41-【Go语言-Day 41】并发编程的基石:Goroutine 从入门到精通
42-【Go语言-Day 42】并发通信的艺术:深入理解 Channel 的创建、使用与死锁
43-【Go语言-Day 43】Channel 进阶:解锁有缓冲、关闭、遍历与单向通道的并发神技
44-【Go语言-Day 44】并发神器 select:从入门到精通,解锁 Channel 多路复用
45-【Go语言-Day 45】从互斥锁到原子操作:全面掌握 Go 传统并发同步原语 sync
46-【Go语言-Day 46】并发模式揭秘:轻松实现高性能 Worker Pool
47-【Go语言-Day 47】并发模式王者:深入解析 Go Pipeline 流水线模型
48-【Go语言-Day 48】掌握 Goroutine 生命周期管理:context 包实战指南
49-【Go语言-Day 49】网络编程入门:用 net 包亲手打造你的第一个 TCP 服务器
文章目录
- Langchain系列文章目录
- Python系列文章目录
- PyTorch系列文章目录
- 机器学习系列文章目录
- 深度学习系列文章目录
- Java系列文章目录
- JavaScript系列文章目录
- Python系列文章目录
- Go语言系列文章目录
- 摘要
- 一、网络编程与 TCP/IP 协议栈回顾
- 1.1 什么是网络编程?
- 1.2 TCP/IP 协议栈简介
- 1.3 为何是 TCP?
- 二、Go `net` 包核心组件解析
- 2.1 `net` 包概览
- 2.2 关键类型介绍
- 2.2.1 `net.Conn` 接口
- 2.2.2 `net.Listener` 接口
- 2.2.3 `net.Addr` 接口
- 三、实战:构建一个基础的 TCP 服务器
- 3.1 服务器的工作流程
- 3.2 步骤一:监听端口 (`net.Listen`)
- 3.3 步骤二:接受连接 (`listener.Accept`)
- 3.4 步骤三:处理连接 (I/O 操作)
- 3.5 步骤四:整合与并发处理
- 四、完整代码示例与测试
- 4.1 Echo 服务器完整代码
- 4.2 编写一个简单的 TCP 客户端
- 4.3 如何测试
- 五、常见问题与注意事项 (FAQ)
- 5.1 关于 `Read` 的阻塞和 EOF
- 5.2 粘包问题简介
- 5.3 优雅关闭服务器
- 六、总结
摘要
欢迎来到 Go 语言并发与实战阶段!从本篇开始,我们将踏入激动人心的网络编程世界。Go 语言因其出色的并发性能和简洁的网络库,在云原生、微服务和后端开发领域大放异彩。本文是您进入 Go 网络编程的第一站,我们将从网络编程的基础概念 TCP 协议入手,深入剖析 Go 标准库 net 包的核心组件,并手把手带您从零开始,构建一个功能完整、支持并发的 TCP 服务器。您将学习到如何监听端口、接受客户端连接、并发处理数据,并最终掌握 Go 进行底层网络开发的基本功,为后续学习 HTTP 开发及更复杂的网络应用打下坚实的基础。
一、网络编程与 TCP/IP 协议栈回顾
在深入代码之前,我们有必要快速回顾一下网络编程的基本概念,这有助于我们更好地理解 Go net 包的设计哲学。
1.1 什么是网络编程?
简单来说,网络编程就是让两台或多台计算机上的程序能够通过网络进行数据交换。我们日常使用的浏览器、聊天软件、在线游戏等,都离不开网络编程。其核心目标是解决进程间通信(Inter-Process Communication, IPC)的跨主机问题。
1.2 TCP/IP 协议栈简介
计算机网络通信依赖于一套复杂的规则,这套规则被称为协议栈。其中,TCP/IP 协议栈是事实上的工业标准。我们可以将其简化为四层模型,每一层都负责不同的功能:
graph TD
A[应用层 (Application Layer)] --> B(传输层 (Transport Layer));
B --> C(网络层 (Network Layer));
C --> D(网络接口层 (Link Layer));
subgraph "协议示例"
P1(HTTP, FTP, SMTP, DNS)
P2(TCP, UDP)
P3(IP, ICMP)
P4(Ethernet, Wi-Fi)
end
A --- P1;
B --- P2;
C --- P3;
D --- P4;
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#ccf,stroke:#333,stroke-width:2px
style C fill:#9cf,stroke:#333,stroke-width:2px
style D fill:#c9c,stroke:#333,stroke-width:2px
- 应用层:我们程序员最常打交道的一层,决定了应用程序如何交互,例如 HTTP(网页)、FTP(文件传输)。
- 传输层:为应用程序之间提供端到端的通信。主要协议是 TCP 和 UDP。Go 的
net包主要是在这一层面上提供了强大的抽象。 - 网络层:负责数据包在网络中的路由和寻址,核心协议是 IP 协议。
- 网络接口层:负责在物理网络(如以太网)上传输数据。
1.3 为何是 TCP?
在传输层,TCP(Transmission Control Protocol,传输控制协议)是我们今天的主角。它具有以下关键特性,使其成为大多数网络应用的首选:
- 面向连接 (Connection-Oriented):在数据传输前,通信双方必须先建立一个连接,就像打电话前需要先拨号接通一样。
- 可靠传输 (Reliable):TCP 通过序列号、确认应答、重传机制等,确保数据能够完整、有序地到达目的地。如果数据包丢失或出错,TCP 会负责重传。
- 面向字节流 (Stream-Oriented):TCP 不关心你发送的数据块的边界。它将数据视为一串连续的字节流。接收方可以一次性读取任意大小的数据,这可能导致“粘包”问题(我们稍后会提及)。
与之相对的是 UDP(User Datagram Protocol),它无连接、不可靠,但开销小、速度快,适用于直播、在线游戏等对实时性要求高但能容忍少量丢包的场景。
二、Go net 包核心组件解析
Go 语言将复杂的网络底层操作封装在 net 包中,提供了简洁、易用且高效的 API。
2.1 net 包概览
net 包是 Go 语言标准库中负责网络 I/O 的核心部分。它提供了对 TCP/IP、UDP、域名解析和 Unix 域套接字等功能的封装。其设计充分利用了 Go 的接口和并发特性,使得编写高性能网络服务变得异常简单。
2.2 关键类型介绍
要构建一个 TCP 服务器,我们必须了解以下几个核心类型:
2.2.1 net.Conn 接口
net.Conn 是 Go 网络编程中对网络连接的通用抽象。它代表了两个网络端点之间的一条通信链路。无论你是 TCP 连接、UDP 连接还是 Unix Socket 连接,都可以通过 net.Conn 接口进行统一的操作。
它内嵌了 io.Reader 和 io.Writer 接口,因此一个 net.Conn 对象天然就具备了读写能力:
type Conn interface {
// Read 从连接中读取数据。
Read(b []byte) (n int, err error)
// Write 将数据写入连接。
Write(b []byte) (n int, err error)
// Close 关闭连接。
Close() error
// LocalAddr 返回本地网络地址。
LocalAddr() Addr
// RemoteAddr 返回远程网络地址。
RemoteAddr() Addr
// ... 其他方法
}
这种设计体现了 Go 语言组合优于继承的哲学,非常优雅。
2.2.2 net.Listener 接口
net.Listener 是对网络监听器的抽象。对于服务器而言,它需要在一个特定的网络地址上“监听”,等待客户端的连接请求。
type Listener interface {
// Accept 等待并返回下一个连接到该监听器的连接。
Accept() (Conn, error)
// Close 关闭监听器。
Close() error
// Addr 返回监听器的网络地址。
Addr() Addr
}
Accept() 方法是一个阻塞方法,它会一直等待,直到有一个新的客户端连接请求到来,然后返回一个代表该连接的 net.Conn 对象。
2.2.3 net.Addr 接口
net.Addr 代表一个网络终端的地址。它是一个接口,具体的实现有 *net.TCPAddr、*net.UDPAddr 等。
type Addr interface {
Network() string // e.g., "tcp", "udp"
String() string // e.g., "192.0.2.1:25", "[2001:db8::1]:80"
}
三、实战:构建一个基础的 TCP 服务器
理论学习完毕,让我们卷起袖子,一步步构建一个 TCP 服务器。这个服务器的功能很简单:接收客户端发来的消息,并将其原样返回,即一个 “Echo Server”。
3.1 服务器的工作流程
一个典型的 TCP 服务器工作流程如下:
graph TD
A(启动程序) --> B{监听指定端口 `net.Listen`};
B -- 成功 --> C[进入无限循环, 等待客户端连接];
B -- 失败 --> D[打印错误, 退出];
C --> E{调用 `listener.Accept()`};
E -- 新连接到达 --> F[获得 `net.Conn` 对象];
F --> G(启动一个 Goroutine 处理该连接);
G --> C;
subgraph "Goroutine (处理单个连接)"
direction LR
H[读取客户端数据 `conn.Read()`] --> I{处理数据};
I --> J[向客户端写回数据 `conn.Write()`];
J --> K[关闭连接 `conn.Close()`];
end
G --> H;
3.2 步骤一:监听端口 (net.Listen)
第一步是让服务器在某个地址上开始监听。我们使用 net.Listen 函数。
// Listen 宣布在本地网络地址 laddr 上监听。
// network 参数必须是 "tcp", "tcp4", "tcp6", "unix" 或 "unixpacket"。
// address 参数的格式取决于 network 参数。对于TCP,格式是 "host:port"。
func Listen(network, address string) (Listener, error)
示例代码:
package main
import (
"fmt"
"net"
)
func main() {
// 在 "127.0.0.1:8888" 这个地址上使用 TCP 协议进行监听
listener, err := net.Listen("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("Listen failed, err:", err)
return // 如果监听失败,程序直接退出
}
defer listener.Close() // 确保程序退出时关闭监听器
fmt.Println("Server is listening on 127.0.0.1:8888")
// ... 后续代码
}
"tcp"表示使用 TCP 协议。"127.0.0.1:8888"表示监听本地回环地址的 8888 端口。如果写成":8888",则会监听本机所有网络接口的 8888 端口。- 错误处理是必须的。如果端口被占用或没有权限,
net.Listen会返回错误。 defer listener.Close()是一个好习惯,确保在main函数结束时能正确关闭监听,释放端口。
3.3 步骤二:接受连接 (listener.Accept)
监听成功后,服务器需要在一个循环中不断调用 Accept 方法来接收客户端的连接。
// ... 接上文
for {
// Accept() 会阻塞,直到有客户端连接进来
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept failed, err:", err)
continue // 出现错误,继续等待下一个连接
}
// 当获取到新连接后,需要处理这个连接
// ...
}
listener.Accept() 会返回两个值:一个 net.Conn 接口类型的变量和一个 error。成功的连接会返回一个 conn 对象,我们可以用它与客户端进行通信。
3.4 步骤三:处理连接 (I/O 操作)
拿到 conn 对象后,我们就可以和客户端收发数据了。这里我们实现一个简单的 Echo 功能。
func process(conn net.Conn) {
// 处理完连接后,要关闭连接
defer conn.Close()
// 打印客户端地址
fmt.Printf("Client connected: %s
", conn.RemoteAddr().String())
// 循环读取客户端发送的数据
for {
buf := make([]byte, 1024) // 创建一个缓冲区
n, err := conn.Read(buf) // 从 conn 中读取数据
if err != nil {
fmt.Printf("Read from client failed, err: %v
", err)
break // 读取失败,退出循环
}
// 打印接收到的数据
recvStr := string(buf[:n])
fmt.Printf("Received from client: %s
", recvStr)
// 将接收到的数据原样写回给客户端 (Echo)
_, err = conn.Write(buf[:n])
if err != nil {
fmt.Printf("Write to client failed, err: %v
", err)
break
}
}
}
defer conn.Close()至关重要,确保与该客户端的连接在处理函数结束时被关闭,释放资源。- 我们创建了一个字节切片
buf作为缓冲区,conn.Read()会将读取到的数据填充到这个缓冲区中。它返回读取的字节数n和一个错误err。 conn.Write()将数据写回客户端。
3.5 步骤四:整合与并发处理
如果我们将 process(conn) 直接放在 for 循环里,服务器将是串行的:它必须处理完一个客户端的所有请求后,才能去 Accept 下一个客户端。这在实际应用中是不可接受的。
Go 语言的 goroutine 完美地解决了这个问题。我们只需在调用处理函数时,在前面加上 go 关键字,即可为每个连接创建一个新的 goroutine 来独立处理。
// ...
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept failed, err:", err)
continue
}
// 启动一个 goroutine 来处理连接
go process(conn)
}
这样,主 goroutine 在启动一个新的 process goroutine 后,不会等待它执行完毕,而是立刻返回到 for 循环的开始,继续调用 listener.Accept(),准备接收下一个客户端连接。这就是 Go 语言编写高并发网络服务如此简洁的奥秘所在。
四、完整代码示例与测试
现在,我们将所有部分组合起来,并提供一个客户端用于测试。
4.1 Echo 服务器完整代码
server.go
package main
import (
"fmt"
"io"
"net"
)
// process 函数负责处理单个客户端连接
func process(conn net.Conn) {
// 函数退出时,自动关闭连接,释放资源
defer conn.Close()
// 获取并打印客户端的网络地址
clientAddr := conn.RemoteAddr().String()
fmt.Printf("Accepted connection from %s
", clientAddr)
// 使用一个无限循环来持续处理来自该客户端的数据
for {
// 创建一个 1024 字节的缓冲区
buf := make([]byte, 1024)
// 从连接中读取数据到缓冲区
// Read 会阻塞,直到有数据可读或发生错误
n, err := conn.Read(buf)
if err != nil {
// 如果读取到文件末尾(EOF),表示客户端已关闭连接
if err == io.EOF {
fmt.Printf("Client %s closed the connection
", clientAddr)
} else {
fmt.Printf("Read from %s failed, err: %v
", clientAddr, err)
}
return // 发生错误或连接关闭,结束该 goroutine
}
// 将读取到的数据转换成字符串
recvStr := string(buf[:n])
fmt.Printf("Received %d bytes from %s: %s
", n, clientAddr, recvStr)
// 将接收到的数据原样写回给客户端 (Echo)
_, err = conn.Write(buf[:n])
if err != nil {
fmt.Printf("Write to %s failed, err: %v
", clientAddr, err)
return // 写入失败,结束该 goroutine
}
}
}
func main() {
// 在所有网络接口的 8888 端口上启动 TCP 监听
listener, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("Listen failed, err:", err)
return
}
// 主函数退出时,确保监听器被关闭
defer listener.Close()
fmt.Println("Server is listening on :8888...")
// 无限循环,等待并接受新的客户端连接
for {
// Accept 方法会阻塞,直到有新的连接请求
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept failed, err:", err)
continue // 如果接受连接失败,继续等待下一个
}
// 为每一个新的客户端连接启动一个独立的 goroutine 进行处理
go process(conn)
}
}
4.2 编写一个简单的 TCP 客户端
为了测试我们的服务器,我们需要一个客户端。客户端的主要工作是使用 net.Dial 连接服务器。
client.go
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
// 使用 Dial 连接到 TCP 服务器
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
fmt.Println("Dial failed, err:", err)
return
}
// 程序退出时关闭连接
defer conn.Close()
// 从标准输入读取数据
reader := bufio.NewReader(os.Stdin)
for {
fmt.Print("Enter text to send: ")
// 读取用户输入,直到遇到换行符
input, _ := reader.ReadString('
')
// 去掉输入字符串末尾的换行符
input = strings.TrimSpace(input)
// 如果用户输入 "exit",则退出循环
if input == "exit" {
break
}
// 将用户输入的数据发送到服务器
_, err = conn.Write([]byte(input))
if err != nil {
fmt.Println("Write to server failed, err:", err)
return
}
// 创建一个缓冲区,准备接收服务器的回显
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("Read from server failed, err:", err)
return
}
// 打印服务器返回的数据
fmt.Printf("Response from server: %s
", string(buf[:n]))
}
fmt.Println("Connection closed.")
}
4.3 如何测试
-
启动服务器:
打开一个终端,进入代码所在目录,运行服务器程序。go run server.go您将看到输出:
Server is listening on :8888... -
启动客户端:
打开另一个终端,运行客户端程序。go run client.go您将看到提示:
Enter text to send: -
交互:
- 在客户端终端输入
hello, go!并按回车。 - 客户端会显示:
Response from server: hello, go! - 同时,服务器端会显示类似
Accepted connection from 127.0.0.1:xxxxx和Received 11 bytes from 127.0.0.1:xxxxx: hello, go!的日志。 - 您可以打开多个客户端终端,它们都可以同时与服务器通信,验证了我们服务器的并发能力。
- 在客户端输入
exit即可关闭连接。
- 在客户端终端输入
-
使用
netcat测试
您也可以使用netcat(或nc)这个强大的网络工具来测试服务器,无需编写客户端代码。# 在新终端中执行 nc 127.0.0.1 8888 # 然后输入任何文本并回车,服务器会将其回显 hello from netcat # 服务器响应: hello from netcat
五、常见问题与注意事项 (FAQ)
5.1 关于 Read 的阻塞和 EOF
conn.Read() 是一个阻塞函数。如果连接上没有数据,它会一直等待。如果客户端关闭了连接,conn.Read() 会立刻返回一个 io.EOF (End Of File) 错误。我们的代码中正确地处理了这种情况,这是编写健壮网络程序的关键。
5.2 粘包问题简介
TCP 是一个字节流协议,它不保证你的一次 Write 操作会被对方一次 Read 操作完整接收。例如,你连续两次 Write,每次 10 字节,对方可能一次 Read 就读到了 20 字节,这就是“粘包”。反之,你一次 Write 100 字节,对方可能需要两次 Read 才能读完。
对于简单的 Echo Server,这不是问题。但在真实应用中,你需要定义应用层协议来区分消息边界。常见的方法有:
- 固定长度消息:每个消息都是固定的大小。
- 分隔符:在消息末尾添加特殊字符(如
)。 - 长度前缀:在每个消息的头部用固定字节数(如4个字节)来表示该消息的长度。
5.3 优雅关闭服务器
我们当前的服务器通过 Ctrl+C 强制终止,这会导致正在处理的连接被粗暴地断开。一个生产级的服务器需要实现“优雅关闭”:
- 停止接受新的连接 (
listener.Close())。 - 等待所有已建立的连接处理完毕。
- 然后才退出程序。
这通常需要使用 context 包和 sync.WaitGroup 来协调,我们将在后续文章中探讨更复杂的并发模式时详细介绍。
六、总结
恭喜你,成功构建并运行了你的第一个 Go TCP 服务器!通过本文的学习,我们完成了从理论到实践的跨越。
核心要点回顾:
- TCP 协议:我们了解了 TCP 是一个面向连接、可靠的字节流协议,是构建大多数网络服务的基础。
net包:掌握了 Gonet包的三大核心组件:net.Listener用于监听,net.Conn代表连接,net.Addr代表地址。- 服务器开发流程:我们遵循了
Listen->Accept循环 ->go handleConnection的标准模式,这是构建 Go 并发服务器的黄金法则。 - 并发处理:深刻体会了 Go 语言使用
go关键字实现高并发是多么的简单和自然,这也是 Go 在网络编程领域的最大优势之一。
今天我们用 net 包搭建了底层的 TCP 服务。在接下来的文章中,我们将更进一步,学习如何在其之上,使用 net/http 包来构建更常见的 Web 服务器。敬请期待!






