章节

17.10 练习

本篇通过若干分级练习巩固 TCP、HTTP、RPC、WebSocket 与基础调试方式,并能独立写出小型网络通信程序。

练习

学完本章你应该掌握

  • 能使用 net.Listenlistener.Acceptnet.Dialconn.Readconn.Write 写出基础 TCP 服务端和客户端,并正确处理 buffer[:n]、连接关闭与地址一致性。
  • 能使用 http.HandleFunchttp.ListenAndServehttp.Getresp.Body.Closeio.ReadAll 写出基础 HTTP 接口和客户端请求代码。
  • 能理解 net/rpc 的服务端与客户端配合方式,写对“导出类型 + 导出方法 + 请求参数 + 响应指针 + error 返回值”这组核心规则。
  • 能使用 github.com/gorilla/websocket 完成 WebSocket 服务端升级连接、客户端连接、消息循环收发,并区分 ws://http://
  • 能识别本章常见错误,例如服务端未启动、地址或路径不一致、忘记关闭连接、把整个 buffer 当有效数据、reply 没传指针、响应体未关闭等问题。
  • 能把前面学过的函数、结构体、切片、map、判断、循环、goroutine、错误处理与超时控制自然放进网络题目里,而不是只会照着 API 写最小示例。

简单

第 1 题:搭一个最小 TCP 服务端

第 1 题 简单

编写一个 Go 程序,完成下面要求:

  • 监听地址 127.0.0.1:9100
  • 每当有客户端连接进来时,读取一条消息
  • 在服务端打印:
1
receive: 客户端消息
  • 再向客户端回复固定文本 ack
  • 每个连接处理完成后关闭连接

要求:

  • 使用 net.Listen
  • 使用 listener.Accept
  • 使用单独的 handleConn(conn 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
33
34
35
36
37
38
39
40
41
42
43
44
package main

import (
	"fmt"
	"net"
)

func handleConn(conn net.Conn) {
	defer conn.Close()

	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("read error:", err)
		return
	}

	fmt.Println("receive:", string(buffer[:n]))

	_, err = conn.Write([]byte("ack"))
	if err != nil {
		fmt.Println("write error:", err)
	}
}

func main() {
	listener, err := net.Listen("tcp", "127.0.0.1:9100")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	fmt.Println("tcp server listen on 127.0.0.1:9100")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			continue
		}

		go handleConn(conn)
	}
}

说明:这里顺手复习了前面函数和 goroutine 的知识,但主角仍然是 TCP 服务端的监听、接收连接和读写流程。

第 2 题:编写对应的 TCP 客户端

第 2 题 简单

请基于上一题的 TCP 服务端,编写一个客户端程序,完成下面要求:

  • 连接 127.0.0.1:9100
  • 发送文本 hello tcp
  • 读取服务端返回内容
  • 在控制台输出返回结果

要求:

  • 使用 net.Dial
  • 使用 conn.Write 发送数据
  • 使用 conn.Read 读取响应
  • 读取时只处理本次真正读到的数据
查看参考答案
 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
package main

import (
	"fmt"
	"net"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:9100")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	_, err = conn.Write([]byte("hello tcp"))
	if err != nil {
		panic(err)
	}

	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(buffer[:n]))
}

输出结果:

1
ack

说明:运行客户端前要先启动上一题的服务端,否则会出现连接失败。

第 3 题:修正 TCP 读取函数里的两个问题

第 3 题 简单

下面这个连接处理函数有两个明显问题:

  • 没有关闭连接
  • 打印消息时直接使用了整个 buffer

请改正它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func handleConn(conn net.Conn) {
	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("read error:", err)
		return
	}

	fmt.Println("receive:", string(buffer))
	conn.Write([]byte("pong"))
	_ = n
}
查看参考答案

修正后的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func handleConn(conn net.Conn) {
	defer conn.Close()

	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("read error:", err)
		return
	}

	fmt.Println("receive:", string(buffer[:n]))

	_, err = conn.Write([]byte("pong"))
	if err != nil {
		fmt.Println("write error:", err)
	}
}

两个关键点分别是:

  • 连接处理结束后要 defer conn.Close(),避免资源泄漏。
  • 读取时只能使用 buffer[:n],否则可能把未写入的空字节或旧数据一起打印出来。

第 4 题:编写最小 HTTP 服务端

第 4 题 简单

编写一个 Go 程序,完成下面要求:

  • 注册路由 /hello
  • 当浏览器或客户端访问这个路径时,返回文本 hello http
  • 服务端监听 :8080

要求:

  • 使用 http.HandleFunc
  • 使用 http.ListenAndServe
  • 处理函数签名写成 func(w http.ResponseWriter, r *http.Request)
查看参考答案
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello http")
}

func main() {
	http.HandleFunc("/hello", helloHandler)
	fmt.Println("http server listen on :8080")

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

访问地址:

1
http://127.0.0.1:8080/hello

第 5 题:使用 HTTP 客户端读取一条公告

第 5 题 简单

编写一个 Go 程序,完成下面要求:

  • 使用 httptest.NewServer 启动一个临时 HTTP 服务
  • 这个临时服务返回文本 network ready
  • 再使用 http.Get 发起请求
  • 读取响应体并输出

要求:

  • 读取完响应后关闭 resp.Body
  • 使用 io.ReadAll 一次性读取响应内容
查看参考答案
 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"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "network ready")
	}))
	defer server.Close()

	resp, err := http.Get(server.URL)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		panic(err)
	}

	fmt.Print(string(data))
}

输出结果:

1
network ready

说明:这里用 httptest.NewServer 是为了让这道题可以单独运行,不需要你手动先起一个 HTTP 服务。

一般

第 6 题:实现一个带查询参数的 HTTP 欢迎接口

第 6 题 一般

编写一个 Go 程序,完成下面要求:

  • 注册两个路由: /ping /welcome
  • 访问 /ping 时返回 pong
  • 访问 /welcome?name=阿斌 时返回 hello, 阿斌
  • 如果访问 /welcome 时没有传 name,返回 hello, guest
  • 服务端监听 :8081

要求:

  • /welcome 处理函数里从 *http.Request 读取查询参数
  • 把路由处理逻辑拆成两个独立函数
查看参考答案
 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/http"
)

func pingHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "pong")
}

func welcomeHandler(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name")
	if name == "" {
		name = "guest"
	}

	fmt.Fprintf(w, "hello, %s\n", name)
}

func main() {
	http.HandleFunc("/ping", pingHandler)
	http.HandleFunc("/welcome", welcomeHandler)

	fmt.Println("http server listen on :8081")
	err := http.ListenAndServe(":8081", nil)
	if err != nil {
		panic(err)
	}
}

访问示例:

1
2
3
http://127.0.0.1:8081/ping
http://127.0.0.1:8081/welcome?name=阿斌
http://127.0.0.1:8081/welcome

说明:这道题看起来还是 HTTP 基础题,但已经开始自然用到前面函数、判断和字符串处理的知识了。

第 7 题:编写一个 RPC 服务端 Multiply

第 7 题 一般

编写一个 Go 程序,完成下面要求:

  • 定义请求结构体 Args,字段包括: A int B int
  • 定义服务对象 Calculator
  • 给它绑定方法 Multiply(args Args, reply *int) error
  • 这个方法把 A * B 的结果写入 reply
  • 把服务注册到 RPC 框架
  • 监听 127.0.0.1:9101

要求:

  • 使用 rpc.Register
  • 使用 rpc.ServeConn
  • 每个连接都交给新的 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
package main

import (
	"fmt"
	"net"
	"net/rpc"
)

type Args struct {
	A int
	B int
}

type Calculator struct{}

func (Calculator) Multiply(args Args, reply *int) error {
	*reply = args.A * args.B
	return nil
}

func main() {
	err := rpc.Register(Calculator{})
	if err != nil {
		panic(err)
	}

	listener, err := net.Listen("tcp", "127.0.0.1:9101")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	fmt.Println("rpc server listen on 127.0.0.1:9101")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			continue
		}

		go rpc.ServeConn(conn)
	}
}

说明:reply 必须是指针,方法最后也必须返回 error,这两个点是 net/rpc 最容易写错的地方。

第 8 题:编写对应的 RPC 客户端

第 8 题 一般

请基于上一题的 RPC 服务端,编写一个客户端程序,完成下面要求:

  • 连接 127.0.0.1:9101
  • 调用 Calculator.Multiply
  • 传入参数 Args{A: 6, B: 7}
  • 输出远程调用结果

要求:

  • 使用 rpc.Dial
  • 使用 client.Call
  • reply 必须传指针
查看参考答案
 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
package main

import (
	"fmt"
	"net/rpc"
)

type Args struct {
	A int
	B int
}

func main() {
	client, err := rpc.Dial("tcp", "127.0.0.1:9101")
	if err != nil {
		panic(err)
	}
	defer client.Close()

	var reply int
	err = client.Call("Calculator.Multiply", Args{A: 6, B: 7}, &reply)
	if err != nil {
		panic(err)
	}

	fmt.Println(reply)
}

输出结果:

1
42

说明:运行客户端前要先启动上一题的 RPC 服务端,否则会连接失败。

第 9 题:实现一个带条件回显的 WebSocket 服务端

第 9 题 一般

从这一题开始,如果你是在一个全新的空目录里练习,请先初始化模块并安装依赖;如果你已经在现成的 Go 模块里,可以跳过第 1 步:

1
2
go mod init go-network-practice
go get github.com/gorilla/websocket

请编写一个 WebSocket 服务端,完成下面要求:

  • 监听 :8082
  • 注册路由 /ws
  • 当客户端发送 ping 时,返回 pong
  • 当客户端发送其他文本时,返回 echo: 原消息
  • 服务端要持续读取消息,而不是只读一次
查看参考答案
 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
package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Println("upgrade error:", err)
		return
	}
	defer conn.Close()

	for {
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("read error:", err)
			return
		}

		text := string(message)
		reply := "echo: " + text
		if text == "ping" {
			reply = "pong"
		}

		err = conn.WriteMessage(messageType, []byte(reply))
		if err != nil {
			fmt.Println("write error:", err)
			return
		}
	}
}

func main() {
	http.HandleFunc("/ws", wsHandler)
	fmt.Println("websocket server listen on :8082")

	err := http.ListenAndServe(":8082", nil)
	if err != nil {
		panic(err)
	}
}

连接地址:

1
ws://127.0.0.1:8082/ws

说明:这道题虽然还是回显模型,但已经把前面学过的 if、循环和字符串处理都自然带进来了。

第 10 题:编写 WebSocket 客户端连续发送两条消息

第 10 题 一般

请基于上一题的 WebSocket 服务端,编写一个客户端程序,完成下面要求:

  • 连接 ws://127.0.0.1:8082/ws
  • 依次发送两条消息: ping lesson
  • 每发送一条消息后,立刻读取并输出服务端响应

要求:

  • 使用切片保存两条待发送消息
  • 使用 for range 循环发送
查看参考答案
 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"

	"github.com/gorilla/websocket"
)

func main() {
	conn, _, err := websocket.DefaultDialer.Dial("ws://127.0.0.1:8082/ws", nil)
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	messages := []string{"ping", "lesson"}

	for _, text := range messages {
		err = conn.WriteMessage(websocket.TextMessage, []byte(text))
		if err != nil {
			panic(err)
		}

		_, reply, err := conn.ReadMessage()
		if err != nil {
			panic(err)
		}

		fmt.Println(string(reply))
	}
}

一种可能的输出结果:

1
2
pong
echo: lesson

说明:这道题的主考点还是 WebSocket 客户端的连接、发送和读取,但你在实现时会自然复习切片和循环。

进阶

下面 5 题会把网络编程和前面学过的函数、结构体、map、错误处理、goroutine、字符串处理与超时控制自然组合起来,更接近真实的小型网络程序。

第 11 题:实现一个并发 TCP 课程查询服务

第 11 题 进阶

请完成一个稍微完整一点的小场景:

  • 监听地址 127.0.0.1:9102
  • 准备一张课程说明表:
1
2
3
4
5
6
map[string]string{
	"tcp":       "面向连接传输",
	"http":      "请求响应协议",
	"rpc":       "远程过程调用",
	"websocket": "双向实时通信",
}
  • 每个客户端发送一个课程关键字,例如 tcp
  • 服务端根据关键字返回对应说明
  • 如果找不到,返回 not found
  • 服务端要支持多个客户端并发连接

要求:

  • 把“根据关键字生成回复”的逻辑拆成单独函数
  • 读取消息后使用 strings.TrimSpace 去掉首尾空白
  • 每个连接使用 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
package main

import (
	"fmt"
	"net"
	"strings"
)

var lessonMap = map[string]string{
	"tcp":       "面向连接传输",
	"http":      "请求响应协议",
	"rpc":       "远程过程调用",
	"websocket": "双向实时通信",
}

func buildReply(keyword string) string {
	key := strings.TrimSpace(strings.ToLower(keyword))
	if text, ok := lessonMap[key]; ok {
		return fmt.Sprintf("%s -> %s", key, text)
	}
	return "not found"
}

func handleConn(conn net.Conn) {
	defer conn.Close()

	buffer := make([]byte, 1024)
	n, err := conn.Read(buffer)
	if err != nil {
		fmt.Println("read error:", err)
		return
	}

	reply := buildReply(string(buffer[:n]))
	_, err = conn.Write([]byte(reply))
	if err != nil {
		fmt.Println("write error:", err)
	}
}

func main() {
	listener, err := net.Listen("tcp", "127.0.0.1:9102")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	fmt.Println("tcp lesson server listen on 127.0.0.1:9102")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			continue
		}

		go handleConn(conn)
	}
}

说明:这道题真正练的是“网络层读到原始文本后,再交给普通函数和 map 去完成业务处理”,也就是把前面章节的基础能力接进网络场景。

第 12 题:实现一个带超时的 HTTP 获取函数

第 12 题 进阶

编写一个 Go 程序,完成下面要求:

  • 编写函数 fetchWithTimeout(url string, timeout time.Duration) (string, error)
  • 这个函数使用 http.Client 发起 GET 请求
  • 如果在超时时间内拿到响应,就返回响应文本
  • 如果超时,直接把错误返回给调用方
  • main 中使用 httptest.NewServer 启动一个临时服务
  • 这个临时服务先等待 50ms,再返回 slow hello
  • 至少测试两次: 一次超时时间是 100ms 一次超时时间是 10ms
查看参考答案
 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
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"time"
)

func fetchWithTimeout(url string, timeout time.Duration) (string, error) {
	client := http.Client{
		Timeout: timeout,
	}

	resp, err := client.Get(url)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	data, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	return string(data), nil
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(50 * time.Millisecond)
		fmt.Fprintln(w, "slow hello")
	}))
	defer server.Close()

	text, err := fetchWithTimeout(server.URL, 100*time.Millisecond)
	if err != nil {
		fmt.Println("request error:", err)
	} else {
		fmt.Print(text)
	}

	text, err = fetchWithTimeout(server.URL, 10*time.Millisecond)
	if err != nil {
		fmt.Println("request error:", err)
	} else {
		fmt.Print(text)
	}
}

一种可能的输出结果:

1
2
slow hello
request error: Get "http://127.0.0.1:xxxxx": context deadline exceeded

说明:

  • http.Client{Timeout: ...} 是本章 HTTP 客户端内容的一个自然延伸,真实项目里很常见。
  • 这道题顺手复习了前面异常处理章节里的 error 返回和调用方判断。

第 13 题:实现一个带业务错误的 RPC 成绩查询服务

第 13 题 进阶

请完成一个稍微完整一点的小场景:

  • 定义请求结构体 QueryArgs,字段为: Name string
  • 定义响应结构体 ScoreReply,字段为: Score int Level string
  • 定义服务对象 StudentService
  • 给它绑定方法 GetScore(args QueryArgs, reply *ScoreReply) error
  • 准备一张成绩表:
1
2
3
4
5
map[string]int{
	"小李": 82,
	"小王": 59,
	"小张": 91,
}
  • 如果姓名存在: 把分数和等级写入 reply
  • 如果姓名不存在: 返回错误 student xxx not found
  • 服务端监听 127.0.0.1:9103

等级规则:

  • >= 90A
  • >= 80B
  • >= 60C
  • 其他是 D
查看参考答案
 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
package main

import (
	"fmt"
	"net"
	"net/rpc"
)

type QueryArgs struct {
	Name string
}

type ScoreReply struct {
	Score int
	Level string
}

type StudentService struct{}

var scoreTable = map[string]int{
	"小李": 82,
	"小王": 59,
	"小张": 91,
}

func judge(score int) string {
	switch {
	case score >= 90:
		return "A"
	case score >= 80:
		return "B"
	case score >= 60:
		return "C"
	default:
		return "D"
	}
}

func (StudentService) GetScore(args QueryArgs, reply *ScoreReply) error {
	score, ok := scoreTable[args.Name]
	if !ok {
		return fmt.Errorf("student %s not found", args.Name)
	}

	reply.Score = score
	reply.Level = judge(score)
	return nil
}

func main() {
	err := rpc.Register(StudentService{})
	if err != nil {
		panic(err)
	}

	listener, err := net.Listen("tcp", "127.0.0.1:9103")
	if err != nil {
		panic(err)
	}
	defer listener.Close()

	fmt.Println("rpc score server listen on 127.0.0.1:9103")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("accept error:", err)
			continue
		}

		go rpc.ServeConn(conn)
	}
}

说明:这道题把结构体、map、判断、错误返回和 RPC 方法规则全都串起来了。真正要练的是“远程调用不仅能返回正常结果,也能返回业务错误”。

第 14 题:实现一个 WebSocket 打卡器

第 14 题 进阶

编写一个 WebSocket 服务端,完成下面要求:

  • 监听 :8083
  • 注册路由 /ws
  • 每个客户端建立连接后,都维护自己的打卡次数
  • 当客户端发送普通文本时,例如 变量接口: 服务端返回 第1次打卡:变量 服务端返回 第2次打卡:接口
  • 当客户端发送 quit 时: 服务端返回 bye 然后结束当前连接处理

要求:

  • 使用 for 循环持续读取消息
  • 使用一个整数变量记录当前连接的打卡次数
  • 收到 quit 后要主动结束当前处理函数
查看参考答案
 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
package main

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		fmt.Println("upgrade error:", err)
		return
	}
	defer conn.Close()

	count := 0

	for {
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("read error:", err)
			return
		}

		text := strings.TrimSpace(string(message))
		if text == "quit" {
			err = conn.WriteMessage(messageType, []byte("bye"))
			if err != nil {
				fmt.Println("write error:", err)
			}
			return
		}

		count++
		reply := fmt.Sprintf("第%d次打卡:%s", count, text)
		err = conn.WriteMessage(messageType, []byte(reply))
		if err != nil {
			fmt.Println("write error:", err)
			return
		}
	}
}

func main() {
	http.HandleFunc("/ws", wsHandler)
	fmt.Println("websocket check-in server listen on :8083")

	err := http.ListenAndServe(":8083", nil)
	if err != nil {
		panic(err)
	}
}

连接地址:

1
ws://127.0.0.1:8083/ws

说明:这道题虽然还是 WebSocket,但比前面的固定回显更接近真实交互场景,因为服务端已经开始维护“当前连接的状态”了。

第 15 题:使用 Postman 验证 WebSocket 打卡器

第 15 题 进阶

请基于上一题的 WebSocket 打卡器,使用 Postman 做一次手动验证。要求:

  • 连接地址写对
  • 至少发送三次消息: 变量 接口 quit
  • 写出每次应该看到的响应
  • 再说明如果连接失败,你会优先检查哪 3 个地方
查看参考答案

一种可行的验证过程如下:

  1. 先运行第 14 题的 WebSocket 服务端。
  2. 在 Postman 中新建 WebSocket 请求。
  3. 连接地址填写:
1
ws://127.0.0.1:8083/ws
  1. 连接成功后依次发送:
1
2
3
变量
接口
quit

预期响应分别是:

1
2
3
第1次打卡:变量
第2次打卡:接口
bye

如果连接失败,我会优先检查这 3 个地方:

  1. 地址协议有没有写错,WebSocket 应该使用 ws://,不能写成 http://
  2. 服务端有没有真正启动,以及端口是不是 8083
  3. 路径是不是 /ws,如果路径写错,即使端口对了也无法按预期升级连接。

说明:这一题主考的是“会不会验证网络程序”,因为真实开发里,手动测试能力和写客户端代码同样重要。

本文禁止转载
使用 Hugo 构建
主题 StackJimmy 设计 由 Hobin 魔改
最近构建时间:2026-04-17 19:07:48 CST
载入天数...载入时分秒...
发表了 1 篇文章 · 发表了 152 篇笔记 · 总计 18 万 0 千字