8. 网络与操作系统
8.1 核心知识点
TCP 三次握手(必考!)
目的:建立可靠的双向通信连接,确认双方的发送和接收能力都正常。
客户端 服务端
│ │
│ ① SYN=1, seq=x │
│ ────────────────────────────────────────────▶ │ 客户端:我要建立连接
│ (客户端进入 SYN_SENT 状态) │
│ │
│ ② SYN=1, ACK=1, seq=y, ack=x+1 │
│ ◀────────────────────────────────────────── │ 服务端:收到,我也准备好了
│ (服务端进入 SYN_RCVD 状态) │
│ │
│ ③ ACK=1, seq=x+1, ack=y+1 │
│ ────────────────────────────────────────────▶ │ 客户端:好的,开始传数据吧
│ (双方进入 ESTABLISHED 状态) │
│ │
═══════════════ 连接建立完成 ═══════════════════════
为什么是三次,不是两次?
两次的问题:
客户端发了个SYN,网络延迟了很久才到服务端
服务端回复SYN+ACK,以为连接建立了
但客户端可能早就超时放弃了,这个连接就"空挂"在服务端
三次的作用:
第三次ACK让服务端确认"客户端真的还想连"
面试简答版:
“三次握手是为了确认双方的收发能力都正常。 第一次:客户端发SYN,证明客户端能发。 第二次:服务端发SYN+ACK,证明服务端能收能发。 第三次:客户端发ACK,证明客户端能收。 三次不能少,否则无法防止失效的连接请求造成资源浪费。“
TCP 四次挥手(必考!)
目的:优雅地关闭连接,确保双方都发完了数据。
客户端 服务端
│ │
│ ① FIN=1, seq=u │
│ ────────────────────────────────────────────▶ │ 客户端:我发完了
│ (客户端进入 FIN_WAIT_1 状态) │
│ │
│ ② ACK=1, seq=v, ack=u+1 │
│ ◀────────────────────────────────────────── │ 服务端:收到,但我还没发完
│ (客户端进入 FIN_WAIT_2 状态) │
│ (服务端进入 CLOSE_WAIT 状态) │
│ │
│ ...服务端继续发送剩余数据... │
│ │
│ ③ FIN=1, ACK=1, seq=w, ack=u+1 │
│ ◀────────────────────────────────────────── │ 服务端:我也发完了
│ (服务端进入 LAST_ACK 状态) │
│ │
│ ④ ACK=1, seq=u+1, ack=w+1 │
│ ────────────────────────────────────────────▶ │ 客户端:好的,正式关闭
│ (客户端进入 TIME_WAIT 状态) │
│ (等待 2MSL 后关闭) │
│ │
═══════════════ 连接关闭完成 ═══════════════════════
为什么是四次,能不能变成三次?
可以!如果服务端在收到FIN时,也刚好发完了所有数据,
那么服务端可以把 ② ACK 和 ③ FIN 合并成一个包发送,
这时候就变成了"三次挥手"。
但如果服务端还有数据没发完,就必须:
- 先发 ACK 告诉客户端"收到了,等我"
- 发完数据后再发 FIN
所以是四次。
TIME_WAIT 是什么?为什么要等 2MSL?
MSL = Maximum Segment Lifetime(报文最大生存时间),通常是 2 分钟
为什么要等:
1. 确保最后一个 ACK 能到达服务端(如果丢了,服务端会重发 FIN)
2. 让网络中残留的报文消失,避免影响新连接
面试简答版:
“四次挥手是因为TCP是全双工的,双方都要独立地关闭自己的发送通道。 第一次:客户端发FIN,表示不再发送数据。 第二次:服务端发ACK,表示收到。 第三次:服务端发FIN,表示也不再发送数据。 第四次:客户端发ACK,确认关闭。 如果服务端没有数据要发了,可以把第二第三次合并成三次挥手。“
TCP 粘包/拆包
问题:TCP 是字节流协议,没有消息边界,多个应用层消息可能被合并或拆分。
应用层发送:["Hello"]["World"]["Go"]
可能收到的情况:
1. 正常:["Hello"]["World"]["Go"]
2. 粘包:["HelloWorld"]["Go"]
3. 拆包:["Hel"]["loWorld"]["Go"]
4. 混合:["HelloWorldG"]["o"]
解决方案:
| 方案 | 做法 | 示例 |
|---|---|---|
| 固定长度 | 每个消息固定 N 字节 | 消息都是 1024 字节 |
| 分隔符 | 用特殊字符分割 | Hello\nWorld\n |
| 长度前缀 | 前 4 字节表示消息长度 | [0005]Hello[0005]World |
// 长度前缀方案示例
// 发送
func send(conn net.Conn, data []byte) {
length := uint32(len(data))
binary.Write(conn, binary.BigEndian, length) // 先发4字节长度
conn.Write(data) // 再发实际数据
}
// 接收
func recv(conn net.Conn) []byte {
var length uint32
binary.Read(conn, binary.BigEndian, &length) // 先读长度
data := make([]byte, length)
io.ReadFull(conn, data) // 再读指定长度的数据
return data
}UDP 协议
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 需要三次握手 | 无连接,直接发 |
| 可靠性 | 可靠(重传、顺序) | 不可靠(可能丢、乱序) |
| 速度 | 较慢 | 快 |
| 适用场景 | HTTP、文件传输 | 视频直播、DNS、游戏 |
NAT(网络地址转换)
NAT 是什么?
NAT = Network Address Translation(网络地址转换),就是把内网 IP 翻译成外网 IP。
为什么需要 NAT?
问题:IPv4 地址只有约 43 亿个,不够给每台设备分配公网 IP
解决:家里/公司内部用"私有 IP"(如 192.168.x.x),只有路由器有公网 IP
路由器负责"翻译":内网发出去的包,换成公网 IP;外网回来的包,翻译回内网 IP
NAT 工作流程:
内网设备 路由器 (NAT) 互联网
┌─────────────────┐ ┌───────────────────┐ ┌───────────────┐
│ 192.168.1.100:3000 │ ─────────────→ │ 翻译成 │ ──────────→ │ 目标服务器 │
│ (你的电脑) │ │ 123.45.67.89:8888 │ │ 8.8.8.8 │
└─────────────────┘ │ (公网IP:随机端口) │ └───────────────┘
└───────────────────┘
返回时:目标服务器回复 123.45.67.89:8888 → 路由器翻译成 → 192.168.1.100:3000
NAT 的三种类型:
| 类型 | 说明 | 用途 |
|---|---|---|
| SNAT | 源地址转换 | 内网访问外网(私有 IP → 公网 IP) |
| DNAT | 目标地址转换 | 外网访问内网(端口映射) |
| NAPT | 端口地址转换 | 多个内网 IP 共享一个公网 IP(最常用) |
NAT 与 K8s/微服务的关系:
| 场景 | 发生的 NAT |
|---|---|
| Pod 访问外网 | Pod IP(如 10.244.1.5)通过 Node 的 NAT 转成 Node 的公网 IP |
| ClusterIP Service | 访问虚拟 IP 时,iptables/IPVS 做 DNAT 转发到真实 Pod IP |
| NodePort | 外部访问 NodeIP:30000,NAT 到内部 Pod 的 10.244.x.x:8080 |
常见私有 IP 段(RFC 1918):
10.0.0.0 ~ 10.255.255.255 (A类,大型企业)
172.16.0.0 ~ 172.31.255.255 (B类,中型企业)
192.168.0.0 ~ 192.168.255.255 (C类,家庭/小公司)
面试简答:
“NAT 是网络地址转换,主要解决 IPv4 地址不够用的问题。它让多个内网设备共享一个公网 IP 上网。路由器在内外网之间做 IP 地址的翻译。在 K8s 中,Pod 访问外网、Service 转发请求都会用到 NAT。“
HTTP客户端/服务器
HTTP 请求/响应格式:
请求:
GET /api/users/1 HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer token123
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 1, "name": "张三"}
RESTful API 设计规范:
| 方法 | 用途 | 示例 | 幂等性 |
|---|---|---|---|
| GET | 查询 | GET /users/1 | ✅ 幂等 |
| POST | 创建 | POST /users | ❌ 非幂等 |
| PUT | 全量更新 | PUT /users/1 | ✅ 幂等 |
| PATCH | 部分更新 | PATCH /users/1 | ✅ 幂等 |
| DELETE | 删除 | DELETE /users/1 | ✅ 幂等 |
HTTP vs WebSocket vs SSE:
| 特性 | HTTP | WebSocket | SSE (Server-Sent Events) |
|---|---|---|---|
| 通信模式 | 请求-响应 | 全双工(双方随时发) | 单向(服务端推送) |
| 连接 | 短连接或 Keep-Alive | 长连接 | 长连接 |
| 适用场景 | API 请求 | 聊天、游戏 | AI 流式输出、通知推送 |
| 复杂度 | 简单 | 中等 | 简单 |
| 浏览器支持 | ✅ 全部 | ✅ 全部 | ✅ 全部(IE 除外) |
SSE 就是 ChatGPT 打字效果的实现方式!
// Go 实现 SSE
func sseHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, _ := w.(http.Flusher)
// 模拟 AI 流式输出
words := []string{"你好", ",", "我是", "AI", "助手", "。"}
for _, word := range words {
fmt.Fprintf(w, "data: %s\n\n", word) // SSE 格式:data: xxx\n\n
flusher.Flush() // 立即发送
time.Sleep(200 * time.Millisecond)
}
}什么时候用哪个?
HTTP → 普通 API(查询、提交表单)
WebSocket → 双向通信(聊天室、协同编辑)
SSE → 服务端单向推送(AI 流式、通知、股票行情)
HTTP 状态码(必考!)
常用状态码:
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 创建资源成功(POST) |
| 204 | No Content | 成功但无返回内容(DELETE) |
| 301 | 永久重定向 | 旧域名跳转新域名,浏览器会缓存 |
| 302 | 临时重定向 | 登录后跳回原页面,浏览器不缓存 |
| 304 | Not Modified | 资源未修改,用缓存 |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未登录/Token 过期 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 500 | Internal Server Error | 服务端异常 |
| 502 | Bad Gateway | 网关错误(反向代理挂了) |
| 503 | Service Unavailable | 服务不可用(过载/维护) |
301 vs 302 的区别:
301 永久重定向:
- 浏览器会缓存跳转,下次直接访问新地址
- 搜索引擎会更新索引
- 适用:网站换域名
302 临时重定向:
- 浏览器不缓存,每次都问服务器
- 搜索引擎保留原地址
- 适用:登录跳转、临时维护
HTTPS 握手过程(面试高频)
HTTPS = HTTP + TLS/SSL
1. 客户端发送:支持的 TLS 版本、加密套件列表、随机数1
2. 服务端返回:选定的加密套件、证书(含公钥)、随机数2
3. 客户端验证证书,生成"预主密钥",用公钥加密发送
4. 双方根据随机数1、随机数2、预主密钥 生成"会话密钥"
5. 后续通信用"会话密钥"对称加密
非对称加密 vs 对称加密:
| 类型 | 算法 | 用途 | 速度 |
|---|---|---|---|
| 非对称加密 | RSA/ECDHE | 握手阶段,交换密钥 | 慢 |
| 对称加密 | AES | 实际数据传输 | 快 |
为什么不全用非对称加密? 因为太慢!所以只在握手时用非对称加密交换密钥,之后用快速的对称加密。
面试简答版:
“HTTPS 握手时,服务端下发证书(含公钥),客户端验证后生成随机密钥,用公钥加密发送给服务端。双方得到相同的对称密钥后,用 AES 加密通信。非对称加密用于交换密钥,对称加密用于实际传输,兼顾安全和性能。“
TLS 与 SSL 详解
概念区分:
| 名称 | 全称 | 说明 |
|---|---|---|
| SSL | Secure Sockets Layer | 网景公司1994年开发,已废弃 |
| TLS | Transport Layer Security | SSL 的继任者,现代标准 |
日常说的”SSL证书”和”TLS证书”是同一个东西。现在实际使用的都是 TLS。
版本演进:
SSL 1.0 → 从未发布(安全漏洞)
SSL 2.0 → 1995年,已废弃
SSL 3.0 → 1996年,已废弃(POODLE漏洞)
TLS 1.0 → 1999年,已废弃
TLS 1.1 → 2006年,已废弃
TLS 1.2 → 2008年,✅ 目前广泛使用
TLS 1.3 → 2018年,✅ 最新标准(更快更安全)
TLS 的三大安全特性:
| 特性 | 解决的问题 | 实现方式 |
|---|---|---|
| 加密 | 防窃听 | 对称加密(AES) |
| 认证 | 防冒充 | 数字证书(CA签发) |
| 完整性 | 防篡改 | MAC(消息认证码) |
TLS 1.2 vs TLS 1.3:
| 对比项 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手次数 | 2-RTT | 1-RTT(更快) |
| 恢复连接 | 1-RTT | 0-RTT(可选) |
| 加密套件 | 支持弱加密 | 只保留安全算法 |
Go 中使用 TLS:
// HTTPS 服务端
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", handler)
// HTTPS 客户端(默认验证证书)
resp, err := http.Get("https://example.com")
// 跳过证书验证(仅测试用!生产环境禁用)
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}TLS 面试简答:
“TLS 是 SSL 的升级版,用于传输层安全通信。它通过非对称加密交换密钥,用对称加密传输数据,同时用数字证书验证身份,保证机密性、完整性和认证。现在主流用 TLS 1.2 和 1.3。“
Socket编程
现在还需要学 Socket 吗?
工作中:几乎不直接用,HTTP 框架(Gin)已经封装好了
面试中:必考!考的是你对底层原理的理解
特殊场景:游戏服务器、物联网、自定义协议还是要用
Socket 本质:
// TCP 服务端核心流程
listener, _ := net.Listen("tcp", ":8080") // 1. 监听端口
conn, _ := listener.Accept() // 2. 等待客户端连接
conn.Read(buffer) // 3. 从连接读数据
conn.Write(response) // 4. 往连接写数据
conn.Close() // 5. 关闭连接
// TCP 客户端核心流程
conn, _ := net.Dial("tcp", "server:8080") // 连接服务器
conn.Write(request) // 发送数据
conn.Read(buffer) // 接收数据
conn.Close() // 关闭连接HTTP 框架底层就是 Socket:
用户请求 → Gin 框架 → net/http 标准库 → net.Conn (Socket) → TCP/IP → 网络
TCP vs UDP Socket:
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | net.Listen + Accept | net.ListenUDP |
| 可靠性 | 保证送达、顺序 | 不保证 |
| 适用 | HTTP、文件传输 | DNS、视频、游戏 |
SSE (Server-Sent Events) 服务端推送
考点说明:面试中常作为 WebSocket 的轻量级替代方案考察。
核心概念:
- 基于 HTTP 协议,单向持久连接(服务端 → 客户端)。
- 使用
Content-Type: text/event-stream。 - 只有文本数据,格式固定(
data: ...\n\n)。
Go 实现 SSE (满分回答):
func sseHandler(w http.ResponseWriter, r *http.Request) {
// 1. 设置标头(关键!)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// 2. 检查 Flusher 接口支持
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "SSE not supported", http.StatusInternalServerError)
return
}
// 3. 循环推送数据
for {
select {
case <-r.Context().Done():
// 客户端断开连接
return
case t := <-time.Tick(1 * time.Second):
// 4. 发送数据(必须以 \n\n 结尾)
fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339))
// 5. 立即刷新缓冲区
flusher.Flush()
}
}
}SSE vs WebSocket:
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向 (Server → Client) | 双向 (Bi-directional) |
| 协议 | 标准 HTTP | 独立 TCP 协议 (Upgrade) |
| 复发 | 自动断线重连 | 需要手动处理 |
| 数据 | 仅文本 | 文本或二进制 |
| 场景 | 股票行情、日志推送、ChatGPT流式输出 | 在线游戏、聊天室 |
8.2 常见面试题
Q1: 如何实现一个简单的HTTP服务器?
核心知识点:
http.Handler接口:核心抽象,只有一个方法ServeHTTP(w, r)- 中间件模式:装饰器模式,增强 Handler 功能
- 路由分发:根据路径和方法分发请求
代码实现(带详细注释):
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
)
// ==================== 数据模型 ====================
type User struct {
ID int `json:"id"` // JSON 序列化时用小写
Name string `json:"name"`
Email string `json:"email"`
}
// 模拟数据库(实际项目会用 MySQL/Redis)
var users = []User{
{ID: 1, Name: "Alice", Email: "alice@example.com"},
{ID: 2, Name: "Bob", Email: "bob@example.com"},
}
// ==================== 中间件 ====================
// 中间件 = 装饰器模式:接收 Handler,返回增强后的 Handler
// 执行顺序(洋葱模型):请求进 → 中间件A前 → 中间件B前 → 业务 → 中间件B后 → 中间件A后 → 响应出
// 日志中间件:记录请求信息和耗时
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[请求] %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r) // 调用下一个处理器(核心!)
log.Printf("[完成] 耗时 %v", time.Since(start))
})
}
// CORS 中间件:允许跨域请求(前后端分离必备)
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 设置跨域响应头
w.Header().Set("Access-Control-Allow-Origin", "*") // 允许所有域(生产环境要限制)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// 预检请求(浏览器先发 OPTIONS 探路)
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return // 直接返回,不进入业务逻辑
}
next.ServeHTTP(w, r)
})
}
// ==================== 业务处理器 ====================
// 获取用户列表
func getUsersHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") // 必须设置!否则浏览器可能不解析
json.NewEncoder(w).Encode(users) // 直接写入 ResponseWriter
}
// 获取单个用户
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id") // 从 URL 参数获取:/user?id=1
for _, user := range users {
if fmt.Sprintf("%d", user.ID) == id {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
return
}
}
http.Error(w, "User not found", http.StatusNotFound) // 404
}
// 创建用户
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var user User
// 从请求体解析 JSON
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) // 400
return
}
user.ID = len(users) + 1 // 简单生成 ID
users = append(users, user)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) // 201 Created
json.NewEncoder(w).Encode(user)
}
// ==================== 主函数 ====================
func main() {
// 1. 创建路由器(标准库的 ServeMux)
mux := http.NewServeMux()
// 2. 注册路由(标准库需要手动判断 Method,Gin 不用)
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
getUsersHandler(w, r)
case "POST":
createUserHandler(w, r)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) // 405
}
})
mux.HandleFunc("/user", getUserHandler)
// 3. 串联中间件(从外到内包装)
// 执行顺序:CORS → Logging → 业务路由
handler := corsMiddleware(loggingMiddleware(mux))
// 4. 配置服务器(生产环境必须设置超时!)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second, // 读请求超时(防慢速攻击)
WriteTimeout: 10 * time.Second, // 写响应超时
IdleTimeout: 60 * time.Second, // Keep-Alive 空闲超时
}
// 5. 启动服务
log.Println("Server starting on :8080")
log.Fatal(server.ListenAndServe()) // 阻塞,出错则退出
}中间件执行顺序图解:
请求 → [CORS中间件-前] → [Logging中间件-前] → [业务处理]
↓
响应 ← [CORS中间件-后] ← [Logging中间件-后] ← [业务处理完成]
实际项目用 Gin 会简单很多:
router := gin.Default() // 自带日志、Recovery 中间件
router.GET("/users", getUsers)
router.POST("/users", createUser)
router.Run(":8080")常见错误分析:
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 忘记设置 Content-Type | 浏览器可能不解析 JSON | w.Header().Set("Content-Type", "application/json") |
| 先写 Body 再设 Header | Header 不生效 | 先 w.Header().Set(),再 w.Write() |
| 不设置超时 | 慢连接拖垮服务 | 配置 ReadTimeout/WriteTimeout |
Q2: 如何实现一个简单的TCP服务器?
核心知识点:
net.Listen+Accept:监听端口,接受连接go handleConnection(conn):每个连接开一个 goroutine 并发处理bufio.Scanner:按分隔符读取,解决粘包问题
代码实现(带详细注释):
package main
import (
"bufio"
"fmt"
"log"
"net"
"time"
)
// ==================== 连接处理器 ====================
// 每个客户端连接都会启动一个 goroutine 执行这个函数
func handleConnection(conn net.Conn) {
defer conn.Close() // ⚠️ 必须关闭!否则资源泄漏
remoteAddr := conn.RemoteAddr().String()
log.Printf("[新连接] 客户端: %s", remoteAddr)
// 使用 Scanner 按行读取(\n 分割)
// 这是解决 TCP 粘包的简单方法
scanner := bufio.NewScanner(conn)
for scanner.Scan() { // 阻塞读取,直到收到 \n 或连接关闭
message := scanner.Text()
log.Printf("[收到] %s: %s", remoteAddr, message)
// 回显消息
response := fmt.Sprintf("Echo: %s\n", message)
_, err := conn.Write([]byte(response))
if err != nil {
log.Printf("[错误] 写入失败: %v", err)
break
}
}
if err := scanner.Err(); err != nil {
log.Printf("[错误] 读取失败: %v", err)
}
log.Printf("[断开] 客户端: %s", remoteAddr)
}
// ==================== TCP 服务端 ====================
func startTCPServer(addr string) {
// 1. 监听端口
listener, err := net.Listen("tcp", addr)
if err != nil {
log.Fatal("监听失败:", err)
}
defer listener.Close()
log.Printf("TCP 服务器启动: %s", addr)
// 2. 循环接受连接
for {
conn, err := listener.Accept() // 阻塞,直到有新连接
if err != nil {
log.Printf("接受连接失败: %v", err)
continue
}
// 3. 开 goroutine 处理连接(关键!实现并发)
// ❌ 不用 goroutine:处理完一个客户端才能处理下一个
// ✅ 用 goroutine:同时处理多个客户端
go handleConnection(conn)
}
}
// ==================== TCP 客户端 ====================
func startTCPClient(addr string) {
// 连接服务器
conn, err := net.Dial("tcp", addr)
if err != nil {
log.Fatal("连接失败:", err)
}
defer conn.Close()
log.Printf("已连接到服务器: %s", addr)
messages := []string{"Hello", "World", "Go TCP"}
for _, msg := range messages {
// 发送消息(注意:要加 \n,服务端按行读取)
_, err := conn.Write([]byte(msg + "\n"))
if err != nil {
log.Printf("发送失败: %v", err)
return
}
// 读取响应
response, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
log.Printf("接收失败: %v", err)
return
}
fmt.Printf("服务端回复: %s", response)
}
}
func main() {
// 启动服务端(后台运行)
go startTCPServer(":8080")
// 等待服务端启动
time.Sleep(100 * time.Millisecond)
// 启动客户端测试
startTCPClient(":8080")
}TCP 服务器核心流程图:
net.Listen(":8080") // 监听端口
↓
┌─→ listener.Accept() // 阻塞等待连接
│ ↓
│ go handleConnection() // 来一个连接,开一个 goroutine
│ ↓
└──────────────────────── // 回到 Accept,等下一个连接
// 每个 handleConnection 独立运行:
conn.Read() → 处理 → conn.Write() → 循环读写 → conn.Close()
为什么要用 goroutine?
❌ 不用 goroutine(串行):
客户端1 连接 → 处理 → 断开 → 客户端2 才能连接
如果客户端1 一直不说话,其他人都进不来!
✅ 用 goroutine(并发):
客户端1 连接 → go 处理 → 立即回到 Accept
客户端2 连接 → go 处理 → 立即回到 Accept
多个客户端同时被处理
常见错误分析:
| 错误 | 后果 | 正确做法 |
|---|---|---|
忘记 defer conn.Close() | 连接数暴涨,资源耗尽 | 处理函数第一行就 defer Close |
| 不用 goroutine | 只能同时处理一个客户端 | go handleConnection(conn) |
| 不处理粘包 | 消息边界混乱 | 用 Scanner 或长度前缀 |
常见错误分析:
- 错误1:忘记关闭连接导致资源泄漏
- 错误2:不理解并发处理
- 错误3:错误处理不当
Q3: 如何实现一个简单的WebSocket服务器?
核心知识点:
websocket.Upgrader:将 HTTP 连接升级为 WebSocket- Hub 模式:用一个中心管理器统一管理所有连接
- Channel 通信:通过 channel 避免直接操作 map 的并发问题
代码实现(带详细注释):
package main
import (
"log"
"net/http"
"sync"
"github.com/gorilla/websocket" // go get github.com/gorilla/websocket
)
// ==================== WebSocket 升级器 ====================
// 将 HTTP 连接升级为 WebSocket 连接
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // 允许所有域(生产环境要检查 Origin)
},
}
// ==================== Hub 中心管理器 ====================
// 为什么用 Hub?
// 1. 统一管理所有 WebSocket 连接
// 2. 实现广播功能(给所有连接发消息)
// 3. 通过 channel 避免并发操作 map 的问题
type Hub struct {
clients map[*websocket.Conn]bool // 所有连接的客户端
broadcast chan []byte // 广播消息队列
register chan *websocket.Conn // 新连接注册通道
unregister chan *websocket.Conn // 断开连接注销通道
mutex sync.RWMutex // 读写锁保护 clients map
}
func NewHub() *Hub {
return &Hub{
clients: make(map[*websocket.Conn]bool),
broadcast: make(chan []byte),
register: make(chan *websocket.Conn),
unregister: make(chan *websocket.Conn),
}
}
// Hub 的主循环(单独一个 goroutine 运行)
// 通过 select 监听多个 channel,避免并发问题
func (h *Hub) Run() {
for {
select {
// 有新连接注册
case client := <-h.register:
h.mutex.Lock()
h.clients[client] = true
h.mutex.Unlock()
log.Printf("[连接] 当前在线: %d", len(h.clients))
// 有连接断开
case client := <-h.unregister:
h.mutex.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
client.Close()
}
h.mutex.Unlock()
log.Printf("[断开] 当前在线: %d", len(h.clients))
// 有消息要广播
case message := <-h.broadcast:
h.mutex.RLock()
for client := range h.clients {
err := client.WriteMessage(websocket.TextMessage, message)
if err != nil {
log.Printf("[错误] 发送失败: %v", err)
client.Close()
delete(h.clients, client)
}
}
h.mutex.RUnlock()
}
}
}
// ==================== WebSocket 处理器 ====================
func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
// 1. 升级 HTTP → WebSocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("[错误] 升级失败: %v", err)
return
}
// 2. 注册到 Hub
h.register <- conn
// 3. 启动读取协程
go func() {
defer func() {
h.unregister <- conn // 断开时注销
}()
for {
// 读取客户端消息
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("[断开] 客户端离开")
break
}
log.Printf("[收到] %s", message)
// 广播给所有人
h.broadcast <- message
}
}()
}
func main() {
// 1. 创建 Hub 并启动
hub := NewHub()
go hub.Run() // Hub 在后台运行
// 2. 注册 WebSocket 路由
http.HandleFunc("/ws", hub.HandleWebSocket)
// 3. 启动 HTTP 服务(WebSocket 是 HTTP 升级来的)
log.Println("WebSocket 服务器启动: ws://localhost:8080/ws")
log.Fatal(http.ListenAndServe(":8080", nil))
}Hub 模式工作原理图:
客户端1 ──连接──→ HandleWebSocket() ─注册─→ h.register channel
客户端2 ──连接──→ HandleWebSocket() ─注册─→ h.register channel
↓
Hub.Run()
监听 select
↓
clients map 统一管理
↓
客户端1 发消息 ──→ h.broadcast channel ──→ 遍历 clients 广播
为什么用 channel 管理连接?
❌ 直接操作 map(危险):
go func() { h.clients[conn] = true } // goroutine 1
go func() { delete(h.clients, conn) } // goroutine 2
// 并发读写 map → panic: concurrent map writes
✅ 通过 channel(安全):
go func() { h.register <- conn } // 发送到 channel
go func() { h.unregister <- conn } // 发送到 channel
// Hub.Run() 串行处理,不会并发冲突
常见错误分析:
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 直接操作 clients map | 并发 panic | 用 channel + Hub.Run() 串行处理 |
| 不检查 CheckOrigin | 安全隐患 | 生产环境验证 Origin |
| 忘记 defer unregister | 连接泄漏 | 读取协程退出时发送到 unregister |