课后作业-在线词典和SOCKS5代理实现
在线词典
猜谜游戏比较简单就不写了,大家自行实行。
首先,我们跟随老师上课讲的,先来看---下我们要用到的API,以彩云科技提供的在线翻译为例。先请打开彩云翻译的网页,然后右键检查打开浏览器的开发者工具。
1.抓包
找到network
此时我们点一下翻译按钮,浏览器会发送一系列请求,我们能很轻松地找到那个用来查询单词的请求。
找到这个 post的请求,请求的header的相的复杂,有十来个。
如果你的页面点开之后是下图,那就证明上面的都是对的
然后我们找到这个请求后,右键这个请求,copy一下,选择copy as cURL(bash)
2.生成代码
需要在 Golang 里面去发送这个请求。但是这个请求比较复杂,用代码构造很麻烦。可以用一种简单的方式来生成代码,如下
然后打开一个网址,curlconverter.com/go/,在里面把刚刚复...
获得以下代码,将这个代码粘贴到编辑器中
go
package main
import (
"fmt"
"io"
"log"
"net/http"
"strings"
)
func main() {
client := &http.Client{}
var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)//创建请求
if err != nil {
log.Fatal(err)
}
//设置请求头
req.Header.Set("authority", "api.interpreter.caiyunai.com")
req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="8"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 SLBrowser/8.0.1.4031 SLBChan/103")
req.Header.Set("app-name", "xy")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("accept", "application/json, text/plain, */*")
req.Header.Set("device-id", "19d327dca1394d0846bd1495388c4a91")
req.Header.Set("os-type", "web")
req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
req.Header.Set("origin", "https://fanyi.caiyunapp.com")
req.Header.Set("sec-fetch-site", "cross-site")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
resp, err := client.Do(req)//发起请求
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", bodyText)
}
将上述代码运行之后,我们可以得到
但是之前输入是固定的,我们是要从一个变量来输入,我们需要用到JSON序列化
3.生成解析 request body
在 Golang 里面,需要生成一段 JSON ,常用的方式是先构造出来一个结构体,这个结构体和需要生成的 JSON 的结构是一一对应的。而如何操作呢,在golang中,更常用的方式是和 request 的一样,写一个结构体,把返回的 JSON 反序列化到结构体里面。但是在浏览器里面可以看到这个 API 返回的结构非常复杂,如果要一一定义结构体字段,非常繁琐并且容易出错。
我们可以利用oktools.net/json2go来解析 response body
我们将之前网站获取的json(preview里面的)粘贴进去自动获取一个结构体,下面是我获取的json
json
{
"rc": 0,
"wiki": {},
"dictionary": {
"prons": {
"en-us": "[g\u028ad]",
"en": "[gud]"
},
"explanations": [
"a.\u597d\u7684;\u5584\u826f\u7684;\u5feb\u4e50\u7684;\u771f\u6b63\u7684;\u5bbd\u5927\u7684;\u6709\u76ca\u7684;\u8001\u7ec3\u7684;\u5e78\u798f\u7684;\u5fe0\u5b9e\u7684;\u4f18\u79c0\u7684;\u5b8c\u6574\u7684;\u5f7b\u5e95\u7684;\u4e30\u5bcc\u7684",
"n.\u5229\u76ca;\u597d\u5904;\u5584\u826f;\u597d\u4eba",
"ad.=well"
],
"synonym": [
"excellent",
"fine",
"nice",
"splendid",
"proper"
],
"antonym": [
"bad",
"wrong",
"evil",
"harmful",
"poor"
],
"wqx_example": [
[
"to the good",
"\u6709\u5229,\u6709\u597d\u5904"
],
[
"good, bad and indifferent",
"\u597d\u7684,\u574f\u7684\u548c\u4e00\u822c\u7684"
],
[
"good innings",
"\u957f\u5bff"
],
[
"good and ...",
"\u5f88,\u9887;\u5b8c\u5168,\u5f7b\u5e95"
],
[
"do somebody's heart good",
"\u5bf9\u67d0\u4eba\u7684\u5fc3\u810f\u6709\u76ca,\u4f7f\u67d0\u4eba\u611f\u5230\u6109\u5feb"
],
[
"do somebody good",
"\u5bf9\u67d0\u4eba\u6709\u76ca"
],
[
"be good for",
"\u5bf9\u2026\u6709\u6548,\u9002\u5408,\u80dc\u4efb"
],
[
"be good at",
"\u5728\u2026\u65b9\u9762(\u5b66\u5f97,\u505a\u5f97)\u597d;\u5584\u4e8e"
],
[
"as good as one's word",
"\u4fe1\u5b88\u8bfa\u8a00,\u503c\u5f97\u4fe1\u8d56"
],
[
"as good as",
"\u5b9e\u9645\u4e0a,\u51e0\u4e4e\u7b49\u4e8e"
],
[
"all well and good",
"\u4e5f\u597d,\u8fd8\u597d,\u5f88\u4e0d\u9519"
],
[
"a good",
"\u76f8\u5f53,\u8db3\u8db3"
],
[
"He is good at figures . ",
"\u4ed6\u5584\u4e8e\u8ba1\u7b97\u3002"
]
],
"entry": "good",
"type": "word",
"related": [],
"source": "wenquxing"
}
}
可以得到go里面的这样一个结构体,可以点击转换=嵌套,使这个代码更简洁化。
go
type AutoGenerated struct {
Rc int `json:"rc"`
Wiki struct {
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []string `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
获取完之后将这个相应结构体放入你的代码中,接下来我们来打印结果
先观察那个 json 可以看出需要的结果是在 Dictionary.explanations 里的,用 for range 循环来迭代它,然后直接打印结构,参照一些词典的显示方式,可以在前面打印出这个单词和它的音标。
最后放上我的代码:
go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
)
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
type DictResponse struct {
Rc int `json:"rc"`
Wiki struct {
} `json:"wiki"`
Dictionary struct {
Prons struct {
EnUs string `json:"en-us"`
En string `json:"en"`
} `json:"prons"`
Explanations []string `json:"explanations"`
Synonym []string `json:"synonym"`
Antonym []string `json:"antonym"`
WqxExample [][]string `json:"wqx_example"`
Entry string `json:"entry"`
Type string `json:"type"`
Related []interface{} `json:"related"`
Source string `json:"source"`
} `json:"dictionary"`
}
func query(word string) {
client := &http.Client{}
request := DictRequest{TransType: "en2zh", Source: word}
buf, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("authority", "api.interpreter.caiyunai.com")
req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="8"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36 SLBrowser/8.0.1.4031 SLBChan/103")
req.Header.Set("app-name", "xy")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("accept", "application/json, text/plain, */*")
req.Header.Set("device-id", "19d327dca1394d0846bd1495388c4a91")
req.Header.Set("os-type", "web")
req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
req.Header.Set("origin", "https://fanyi.caiyunapp.com")
req.Header.Set("sec-fetch-site", "cross-site")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
if resp.StatusCode != 200 {
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
fmt.Println(item)
}
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
word := os.Args[1]
query(word)
}
SOCKS5代理实现
在代码实现之前,我们先来了解一下SOCKS5是什么东西,SOCKS5 是一个代理服务器 ,正常浏览器访问一个网站,如果不经过代理服务器,就是先和对方的网站建立 TCP 连接,然后三次握手,握手完之后发起 HTTP 请求,然后服务返回 HTTP 响应。如果设置代理服务器之后,流程会变得复杂一些。首先是浏览器和 SOCKS5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。
- 这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段。
1.握手阶段
- 浏览器会向 SOCKS5 代理发送请求(SOCKS 版本号、认证方法和支持的认证方式列表等)
- SOCKS5 服务器响应握手请求,选择一种认证方法进行认证。
- 握手阶段完成。
先写一个简单的TCP echo server,为了方便测试
go
package main
import (
"bufio"
"log"
"net"
)
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080")// 监听地址和端口
if err != nil {
panic(err)
}
for {
client, err := server.Accept()// 接受客户端连接
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)// 启动协程处理客户端连接
}
}
func process(conn net.Conn) {
// 处理完连接后关闭连接
defer conn.Close()
reader := bufio.NewReader(conn)
for {
b, err := reader.ReadByte()// 读取客户端发送的数据
if err != nil {
break
}
_, err = conn.Write([]byte{b})// 将读取到的数据写入到客户端连接中,实现中继功能
if err != nil {
break
}
}
}
- 在终端输入nc命令:
yaml
nc 127.0.0.1 1080
当我们输入hello 代理服务器会返回输入的值
2.认证阶段
如果需要认证,客户端和代理服务器会进行认证过程,以验证客户端的身份。常见的认证方式包括无认证、用户名密码认证等。
go
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
// 读取协议版本(VER)
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数量
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
// X'00' NO AUTHENTICATION REQUIRED
// X'02' USERNAME/PASSWORD
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
// 检查协议版本是否为 SOCKS5
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
// 读取支持的认证方法数(NMETHODS)
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
// 读取每个认证方法的字节
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
log.Println("ver", ver, "method", method)
// 回复客户端的协议版本和认证方法(VER, METHOD)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
3.请求阶段
- 认证通过之后浏览器会 SOCKS5 服务器发起代理请求(版本号,请求的类型等)
- 代理服务器收到响应之后,会真正和后端服务器建立连接,然后返回一个响应
go
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER 版本号,socks5的值为0x05
// CMD 0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
// 0x01表示IPv4地址,DST.ADDR为4个字节
// 0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节
// 读取协议头部字段,包括协议版本(VER)、命令(CMD)、目标地址类型(ATYP)
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, atyp := buf[0], buf[1], buf[3]//读取到 VER、CMD、ATYP,并验证其合法性
// 检查协议版本是否为 SOCKS5
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
// 检查命令是否为 CONNECT 请求
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd)
}
addr := ""
switch atyp {
case atypeIPV4:
// 读取 IPv4 地址
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
// 读取域名地址
hostSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read hostSize failed:%w", err)
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read host failed:%w", err)
}
addr = string(host)
case atypeIPV6:
// 返回不支持 IPv6的信息
return errors.New("IPv6: no supported yet")
default:
return errors.New("invalid atyp")
}
// 读取目标端口
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2])//binary 实现了数字和字节序列之间的简单转换以及 varints 的编码和解码
log.Println("dial", addr, port) //只是输出日志
// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X'00' succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
// 回复客户端,表示连接成功
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
return nil
}
4.relay阶段
此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。
如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。
记得下载SwitchyOmega插件,然后在这边设置一下
保存选项之后就是开启了。之后再运行代码,然后浏览器开启这个插件之后可以浏览qq.com,结果显示如下:
通过这次实现,我了解SOCKS5代理协议的基本原理和实现方式,掌握Go语言网络编程的基础知识,同时也可以通过该项目的代码和实现思路,进行二次开发和扩展,实现更加复杂的代理功能。
完整代码如下:
go
package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080") // Listen()可以收听本地网络地址上的广播,以此来侦听一个端口
if err != nil {
panic(err) // panic() 会停止当前 goroutine 协程的正常执行
}
for {
client, err := server.Accept() // Accept() 等待并将下一个连接返回给侦听器
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client) //go 启动一个子线程或 goroutine 协程(处理并发)来处理连接
}
}
func process(conn net.Conn) { // Conn 是一个通用的面向流的网络连接。 多个 goroutine 协程可以同时调用 Conn 上的方法
defer conn.Close() // defer 会在函数结束后从后往前触发,Close() 手动关闭连接
reader := bufio.NewReader(conn) // 把输入的连接转换成只读的带缓冲的流
err := auth(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err) // RemoteAddr() 返回远程网络地址
return
}
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err) // RemoteAddr() 返回远程网络地址
return
}
}
func auth(reader *bufio.Reader, conn net.Conn) (err error) { // 用来建立授权,验证身份
// +----+----------+----------+
// |VER | NMETHODS | METHODS |
// +----+----------+----------+
// | 1 | 1 | 1 to 255 |
// +----+----------+----------+
// VER: 协议版本,socks5为0x05
// NMETHODS: 支持认证的方法数量
// METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
// X'00' NO AUTHENTICATION REQUIRED
// X'02' USERNAME/PASSWORD
ver, err := reader.ReadByte() // ReadByte() 读取并返回单个字节,读取到版本号 VER
if err != nil {
return fmt.Errorf("read ver failed:%w", err)
}
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte() // ReadByte() 读取并返回单个字节,读取到支持认证的方法数量 NMETHODS
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize) // 创建一个 method 的缓冲区
_, err = io.ReadFull(reader, method) // ReadFull() 将reader中的字节准确地读取到 method 中
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00}) // 返回协议版本号 socks5Ver,建立授权的方式 0x00
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
func connect(reader *bufio.Reader, conn net.Conn) (err error) { // 用来建立连接,进行请求
// +----+-----+-------+------+----------+----------+
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER 版本号,socks5的值为0x05
// CMD 0x01表示CONNECT请求
// RSV 保留字段,值为0x00
// ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
// 0x01表示IPv4地址,DST.ADDR为4个字节
// 0x03表示域名,DST.ADDR是一个可变长度的域名
// DST.ADDR 一个可变长度的值
// DST.PORT 目标端口,固定2个字节
buf := make([]byte, 4)
_, err = io.ReadFull(reader, buf) // ReadFull() 将 reader 中的 len(buf) 个字节准确地读取到 buf 中
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
/*读取到 VER、CMD、ATYP,并验证其合法性*/
ver, cmd, atyp := buf[0], buf[1], buf[3]
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", ver)
}
addr := ""
switch atyp {
case atypIPV4:
_, err = io.ReadFull(reader, buf)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
hostSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read hostSize failed:%w", err)
}
host := make([]byte, hostSize)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read host failed:%w", err)
}
addr = string(host)
case atypeIPV6:
return errors.New("IPv6: no supported yet")
default:
return errors.New("invalid atyp")
}
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(buf[:2]) // binary 实现了数字和字节序列之间的简单转换以及 varints 的编码和解码
dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port)) //Dial() 拨号连接到指定网络上的地址,进行 TCP 连接
if err != nil {
return fmt.Errorf("dial dst failed:%w", err)
}
defer dest.Close() // defer 会在函数结束后从后往前触发,Close() 手动关闭连接
log.Println("dial", addr, port)
// +----+-----+-------+------+----------+----------+
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
// +----+-----+-------+------+----------+----------+
// | 1 | 1 | X'00' | 1 | Variable | 2 |
// +----+-----+-------+------+----------+----------+
// VER socks版本,这里为0x05
// REP Relay field,内容取值如下 X'00' succeeded
// RSV 保留字段
// ATYPE 地址类型
// BND.ADDR 服务绑定的地址
// BND.PORT 服务绑定的端口DST.PORT
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0}) // 返回请求报文
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
/*
WithCancel() 返回具有新 Done 通道的 parent 副本。返回的上下文的完成通道在调用返回的取消函数或父上下文的完成通道关闭时关闭,以先发生者为准,
取消此上下文会释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消
*/
ctx, cancel := context.WithCancel(context.Background()) // Background() 返回一个非零、空的Context
defer cancel()
/* 启动2个协程,实现双向数值转换 */
/* 从浏览器到服务器 */
go func() {
_, _ = io.Copy(dest, reader) // 将副本从 reader 复制到 dest,直到在 reader 上达到EOF或发生错误,它返回复制的字节数和复制时遇到的第一个错误(如果有)
cancel()
}()
/* 从服务器到浏览器 */
go func() {
_, _ = io.Copy(conn, dest) // 将副本从 dest 复制到 conn,直到在 dest 上达到EOF或发生错误,它返回复制的字节数和复制时遇到的第一个错误(如果有)
cancel()
}()
<-ctx.Done() // Done() 代表此上下文完成的工作应该被取消时,Done 返回一个关闭的通道
return nil
}