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 协议

特性TCPUDP
连接需要三次握手无连接,直接发
可靠性可靠(重传、顺序)不可靠(可能丢、乱序)
速度较慢
适用场景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

特性HTTPWebSocketSSE (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 状态码(必考!)

常用状态码

状态码含义场景
200OK请求成功
201Created创建资源成功(POST)
204No Content成功但无返回内容(DELETE)
301永久重定向旧域名跳转新域名,浏览器会缓存
302临时重定向登录后跳回原页面,浏览器不缓存
304Not Modified资源未修改,用缓存
400Bad Request请求参数错误
401Unauthorized未登录/Token 过期
403Forbidden无权限
404Not Found资源不存在
500Internal Server Error服务端异常
502Bad Gateway网关错误(反向代理挂了)
503Service 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 详解

概念区分

名称全称说明
SSLSecure Sockets Layer网景公司1994年开发,已废弃
TLSTransport Layer SecuritySSL 的继任者,现代标准

日常说的”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.2TLS 1.3
握手次数2-RTT1-RTT(更快)
恢复连接1-RTT0-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

特性TCPUDP
连接net.Listen + Acceptnet.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

特性SSEWebSocket
方向单向 (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浏览器可能不解析 JSONw.Header().Set("Content-Type", "application/json")
先写 Body 再设 HeaderHeader 不生效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