|
@@ -0,0 +1,98 @@
|
|
|
|
|
+package main
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "io"
|
|
|
|
|
+ "net/http"
|
|
|
|
|
+ "net/http/httptest"
|
|
|
|
|
+ "net/url"
|
|
|
|
|
+ "os"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "time"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// Greeting 是服务端返回的 JSON 结构。
|
|
|
|
|
+type Greeting struct {
|
|
|
|
|
+ Message string `json:"message"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// greetingHandler 是 HTTP 服务端的处理函数。
|
|
|
|
|
+func greetingHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
+ // 本课只接受 GET。状态码不是 Go 函数的 error 返回值。
|
|
|
|
|
+ if r.Method != http.MethodGet {
|
|
|
|
|
+ w.Header().Set("Allow", http.MethodGet)
|
|
|
|
|
+ http.Error(w, "只支持 GET", http.StatusMethodNotAllowed)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ name := strings.TrimSpace(r.URL.Query().Get("name"))
|
|
|
|
|
+ if name == "" {
|
|
|
|
|
+ http.Error(w, "name 不能为空", http.StatusBadRequest)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
+ // 首次写入响应体时会默认发送 200 状态码。
|
|
|
|
|
+ if err := json.NewEncoder(w).Encode(Greeting{Message: "你好," + name + "!"}); err != nil {
|
|
|
|
|
+ // 响应可能已开始,不能保证此时还能改状态码。
|
|
|
|
|
+ fmt.Fprintln(os.Stderr, "写入响应失败:", err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// fetchGreeting 是客户端:发送请求、检查状态、读取并解析 JSON。
|
|
|
|
|
+func fetchGreeting(client *http.Client, baseURL, name string) (Greeting, error) {
|
|
|
|
|
+ params := url.Values{}
|
|
|
|
|
+ params.Set("name", name) // 对中文、空格和特殊字符做 URL 编码。
|
|
|
|
|
+ response, err := client.Get(baseURL + "/hello?" + params.Encode())
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return Greeting{}, fmt.Errorf("发送请求:%w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ defer response.Body.Close() // 只在请求成功后关闭响应体。
|
|
|
|
|
+
|
|
|
|
|
+ if response.StatusCode != http.StatusOK {
|
|
|
|
|
+ return Greeting{}, fmt.Errorf("服务端返回状态码 %d", response.StatusCode)
|
|
|
|
|
+ }
|
|
|
|
|
+ // 为示例设置大小上限,避免无限读取响应体。
|
|
|
|
|
+ const maxBody = 64 * 1024
|
|
|
|
|
+ data, err := io.ReadAll(io.LimitReader(response.Body, maxBody+1))
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return Greeting{}, fmt.Errorf("读取响应:%w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(data) > maxBody {
|
|
|
|
|
+ return Greeting{}, fmt.Errorf("响应内容超过 %d 字节", maxBody)
|
|
|
|
|
+ }
|
|
|
|
|
+ var result Greeting
|
|
|
|
|
+ if err := json.Unmarshal(data, &result); err != nil {
|
|
|
|
|
+ return Greeting{}, fmt.Errorf("解析 JSON:%w", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ return result, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func run() error {
|
|
|
|
|
+ // 新建路由器:只将 /hello 交给我们的处理函数。
|
|
|
|
|
+ mux := http.NewServeMux()
|
|
|
|
|
+ mux.HandleFunc("/hello", greetingHandler)
|
|
|
|
|
+
|
|
|
|
|
+ // 本课使用本机临时测试服务器,自动选择可用端口,不访问外网。
|
|
|
|
|
+ server := httptest.NewServer(mux)
|
|
|
|
|
+ defer server.Close()
|
|
|
|
|
+ fmt.Println("本地临时服务:", server.URL)
|
|
|
|
|
+
|
|
|
|
|
+ // 设置超时,避免请求无限等待。
|
|
|
|
|
+ client := &http.Client{Timeout: 3 * time.Second}
|
|
|
|
|
+ result, err := fetchGreeting(client, server.URL, "小朱")
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return err
|
|
|
|
|
+ }
|
|
|
|
|
+ fmt.Println("客户端收到:", result.Message)
|
|
|
|
|
+ fmt.Println("演示结束,临时服务即将关闭")
|
|
|
|
|
+ return nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func main() {
|
|
|
|
|
+ if err := run(); err != nil {
|
|
|
|
|
+ fmt.Fprintln(os.Stderr, "请求失败:", err)
|
|
|
|
|
+ os.Exit(1)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|