青训营Lesson-1 | 青训营笔记

126 阅读17分钟

青训营Lesson-1 | 青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记

「Go 语言上手-基础语言」

1.Go语言受众公司

1.png

2.基本语法

  • Golang只有for循环一种循环.

  • 可以在 switch 后面不加任何的变量,然后在 case 里面写条件分支。这样代码相比用多个 if else 代码逻辑会更为清晰。

  • 在真实业务代码里面,我们很少直接使用数组,因为它长度是固定的,我们用的更多的是切片。

  • slice 拥有像 python 一样的切片操作,比如s[2:5]代表取出第二个到第五个位置的元素,不包括第五个元素。不过不同于python,这里不支持负数索引.

  • golang的map是完全无序的,遍历的时候不会按照字母顺序,也不会按照插入顺序输出,而是随机顺序。

  • 对于一个 slice 或者一个 map 的话,我们可以用 range 来快速遍历,这样代码能够更加简洁。 range 遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,我们可以用下划线来忽略.

  • Golang 里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真正的返回结果,第二个值是一个错误信息。

  • go里面也支持指针。当然,相比 C 和 C++ 里面的指针,支持的操作很有限。指针的一个主要用途就是对于传入参数进行修改。

  • 同样的结构体我们也能支持指针,这样能够实现对于结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销。

  • 在实现结构体的方法的时候有两种写法,一种是带指针,一种是不带指针。这个它们的区别的话是说如果你带指针的话,那你那么你就可以对这个结构体去做修改。如果你不带指针的话,那你实际上操作的是一个拷贝,你就无法对结构体进行修改。

  • 在函数里面,我们可以在那个函数的返回值类型里面,后面加一个 error, 就代表这个函数可能会返回错误。自定义的error可以用New方法来定义。

    errors.New("not found")
    
  • string字符串方法

    contains 判断一个字符串里面是否有包含另一个字符串 , count 字符串计数, index 查找某个字符串的位置。 join 连接多个字符串 repeat 重复多个字符串 replace 替换字符串。

    func main() {
    	a := "hello"
    	fmt.Println(strings.Contains(a, "ll"))                // true
    	fmt.Println(strings.Count(a, "l"))                    // 2
    	fmt.Println(strings.HasPrefix(a, "he"))               // true
    	fmt.Println(strings.HasSuffix(a, "llo"))              // true
    	fmt.Println(strings.Index(a, "ll"))                   // 2
    	fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
    	fmt.Println(strings.Repeat(a, 2))                     // hellohello
    	fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
    	fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
    	fmt.Println(strings.ToLower(a))                       // hello
    	fmt.Println(strings.ToUpper(a))                       // HELLO
    	fmt.Println(len(a))                                   // 5
    	b := "你好"
    	fmt.Println(len(b)) // 6
    }
    
  • fmt.Println用 %v 来打印任意类型的变量,而不需要区分数字字符串。你也可以用 %+v 打印详细结果,%#v 则更详细。%.2f打印包含两个小数的浮点数。

  • 对于一个已有的结构体,只要保证每个字段的第一个字母是大写,也就是是公开字段。那么这个结构体就能用 JSON.marshaler 去序列化,变成一个 JSON 的字符串。

    序列化之后的字符串也能够用 JSON.unmarshaler 去反序列化到一个空的变量里面。 这样默认序列化出来的字符串的话,它的风格是大写字母开头,而不是下划线。我们可以在后面用 json tag 等语法来去修改输出 JSON 结果里面的字段名。

    type userInfo struct {
    	Name  string
    	Age   int `json:"age"` //去序列化之后Age就不是大写 而是age
    	Hobby []string
    }
    
  • time包的基本使用

    time.now() 来获取当前时间,time.date 去构造一个带时区的时间,

    通过构造完的时间来获取这个时间点的年月日小时分钟秒,然后也能用点 sub 去对两个时间进行减法,得到一个时间段。

    时间段又可以去得到它有多少小时,多少分钟、多少秒。 在和某些系统交互的时候,我们经常会用到时间戳,可以用 .UNIX 来获取时间戳。 time.format time.parse ["2006-01-02 15:04:05"]规范时间格式

    func main() {
    	now := time.Now()
    	fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
    	t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
    	t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
    	fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
    	fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
    	fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
    	diff := t2.Sub(t)
    	fmt.Println(diff)                           // 1h5m0s
    	fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
    	t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(t3 == t)    // true
    	fmt.Println(now.Unix()) // 1648738080
    }
    
  • 字符串和数字之间的转换都在 strconv 这个包下 我们可以用 parseInt 或者 parseFloat 来解析一个字符串。 parseint 参数 我们可以用 Atoi 把一个十进制字符串转成数字。可以用 itoA 把数字转成字符串。 如果输入不合法,那么这些函数都会返回error

    func main() {
    	f, _ := strconv.ParseFloat("1.234", 64)
    	fmt.Println(f) // 1.234
    
    	n, _ := strconv.ParseInt("111", 10, 64) // 10进制,返回64进制
    	fmt.Println(n) // 111
    
    	n, _ = strconv.ParseInt("0x1000", 0, 64) // 0表示自动推测进制,返回64进制
    	fmt.Println(n) // 4096
    
    	n2, _ := strconv.Atoi("123")
    	fmt.Println(n2) // 123
    
    	n2, err := strconv.Atoi("AAA")
    	fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax
    }
    
  • 在 go 里面,我们能够用 os.argv 来得到程序执行的时候的指定的命令行参数。 比如我们编译的一个 二进制文件,command。 后面接 abcd 来启动,输出就是 os.argv 会是一个长度为 5 的 slice ,第一个成员代表二进制自身的名字。 我们可以用 so.getenv来读取环境变量。 exec.Command来快速启动子进程然后获取输入输出。

    func main() {
    	// go run example/20-env/main.go a b c d
    	fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
    	fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
    	fmt.Println(os.Setenv("AA", "BB"))
    
    	buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(string(buf)) // 127.0.0.1       localhost
    }
    

3.实战部分

3.1猜谜游戏

  • 使用rand需要先布置seed

    func main() {
    	maxNum := 100
    	rand.Seed(time.Now().UnixNano())  
    	secretNumber := rand.Intn(maxNum)
    	fmt.Println("The secret number is ", secretNumber)
    }
    
  • 读取用户输入

    把数据读到一个bufio中,strings.TrimSuffix()去除换行符,atoi转换为数字

    可用scanf实现读取输入

    func main() {
    	maxNum := 100
    	rand.Seed(time.Now().UnixNano())
    	secretNumber := rand.Intn(maxNum)
    	fmt.Println("The secret number is ", secretNumber)
    
    	fmt.Println("Please input your guess")
    	reader := bufio.NewReader(os.Stdin)
    	input, err := reader.ReadString('\n')
    	if err != nil {
    		fmt.Println("An error occured while reading input. Please try again", err)
    		return
    	}
    	input = strings.TrimSuffix(input, "\n")
    
    	guess, err := strconv.Atoi(input)
    	if err != nil {
    		fmt.Println("Invalid input. Please enter an integer value")
    		return
    	}
    	fmt.Println("You guess is", guess)
    }
    

3.2在线词典

2.png

  • dict Copy as curl 之后采用curlconverter.com/#go 在线转化为go码(有几个header比较复杂,生成代码有转义导致的编译错误,删掉这几行即可。)

    打印出json格式的结果

    func main() {
    	client := &http.Client{}  // timeout参数可设定超时,此处不限制
    	var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)  //data是由string转为流
    	req, err := http.NewRequest("POST", "<https://api.interpreter.caiyunai.com/v1/dict>", data)  // method url data(是个bufio,避免body很大会占用太大内存)
    	if err != nil {
    		log.Fatal(err)
    	}
    	//请求头
    	req.Header.Set("Connection", "keep-alive")
    	req.Header.Set("DNT", "1")
    	req.Header.Set("os-version", "")
    	req.Header.Set("sec-ch-ua-mobile", "?0")
    	req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36")
    	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", "")
    	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")
    	req.Header.Set("Cookie", "_ym_uid=16456948721020430059; _ym_d=1645694872")
    	resp, err := client.Do(req) //发送请求
    	if err != nil {
    		log.Fatal(err)
    	}
    	defer resp.Body.Close() // body也是一个流,防止资源泄露需要手动关闭
    	bodyText, err := ioutil.ReadAll(resp.Body) //读到内存里作为byte数组
    	if err != nil {
    		log.Fatal(err)
    	}
    	fmt.Printf("%s\n", bodyText)
    }
    
  • 构造结构体字段(输出一样是json格式的结果)

    type DictRequest struct {
    	TransType string `json:"trans_type"`
    	Source    string `json:"source"`
    	UserID    string `json:"user_id"`
    }
    
    client := &http.Client{}
    	request := DictRequest{TransType: "en2zh", Source: "good"}
    	buf, err := json.Marshal(request) //序列化变为byte数组
    	if err != nil {
    		log.Fatal(err)
    	}
    	var data = bytes.NewReader(buf) // byte数组转化为bytes.NewReader
    
  • 解析response body,常用方式是写一个结构体对应response,然后反序列化。由于返回的结果很复杂,运用oktools.net/json2go来解析(转化-嵌套)

3.png

结果作为一个DictResponse结构体,然后反序列化

```
  var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse) //&写入
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%#v\n", dictResponse) //%#输出具体结果
```
  • 完善代码,读取输入

    if resp.StatusCode != 200 {   // 防御性编程,打印错误代码(403/404)状态码为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)
    }
    

3.3 socks5代理服务器

  • 不经过代理服务器的步骤:先和网站进行tcp连接,三次握手之后正常发起http请求,服务器返回http响应

  • 经过代理服务器的步骤:

    首先是浏览器和 socks5 代理建立 TCP 连接,代理再和真正的服务器建立 TCP 连接。

    这里可以分成四个阶段,握手阶段、认证阶段、请求阶段、 relay 阶段。 第一个握手阶段,浏览器会向 socks5 代理发送请求(报文),内容包括一个协议的版本号,还有支持的认证的种类(密码或者不需要认证),socks5 代理服务器会选中一个认证方式,返回给浏览器。如果返回的是 00 的话就代表不需要认证,返回其他类型的话会开始认证流程,这里我们就不对认证流程进行概述了,因为此处实现不加密的代理。 第三个阶段是请求阶段,认证通过之后浏览器会 socks5 服务器发起请求。主要信息包括 版本号,请求的类型,一般主要是 connection 请求,就代表代理服务器要和某个域名或者某个 IP 地址某个端口建立 TCP 连接。代理服务器收到响应之后,会真正和后端服务器建立TCP连接,然后返回一个报文响应。 第四个阶段是 relay 阶段。此时浏览器会发送正常发送请求,然后代理服务器接收到请求之后,会直接把请求转换到真正的服务器上。然后如果真正的服务器以后返回响应的话,那么也会把请求转发到浏览器这边。然后实际上代理服务器并不关心流量的细节,可以是 HTTP流量,也可以是其它 TCP 流量。

4.png

  • 第一步,我们先在 go 里面写一个简单的 TCP echo server。为了方便测试, server 的工作逻辑很简单,你给他发送啥,他就回复啥。

    bufio.NewReader 来创建一个 带缓冲的只读流,这个在前面的猜谜游戏里面也有用到, 带缓冲的流的作用是,可以减少底层系统调用的次数,比如这里为了方便是一个字节一个字节的读取,但是底层可能合并成几次大的读取操作。并且带缓冲的流会有更多的一些工具函数用来读取数据。 我们可以简单地调用那个 readbyte 函数来读取单个字节。再把这一个字节写进去连接。

    func main() {
    	server, err := net.Listen("tcp", "127.0.0.1:1080") //监听一个端口返回server
    	if err != nil {
    		panic(err)
    	}
    	for {
    		client, err := server.Accept() // 接受一个请求,成功则返回连接client,之后在process中处理
    		if err != nil {
    			log.Printf("Accept failed %v", err)
    			continue
    		}
    		go process(client) //启动goroutinue实现并发
    	}
    }
    
    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}) // 把这个字节写入,[]byte类型转化
    		if err != nil {
    			break
    		}
    	}
    }
    
  • 接下来我们是要开始实现协议的第一步,认证阶段

    我们实现一个空的 auth 函数,在 process 函数里面调用,再来编写 auth 函数的代码。 我们回忆一下认证阶段的逻辑,首先第一步的话,浏览器会给代理服务器发送一个报文,然后这个报文有三个字段, 第一个字段version也就是协议版本号,固定是 5。

    第二个字段methods,认证的方法数目。

    第三个字段每个method的编码,0代表不需要认证,2代表用户名密码认证。 我们先用 Readbyte来把版本号读出来,然后如果版本号不是 socket 5 的话直接返回报错,接下来我们再读取 methodsize ,也是一个字节。然后我们需要我们去 make 一个相应长度的一个 slice缓冲区 ,用 io.ReadFull 把它填充进去。 写到这里,我们把获取到的版本号和认证方式打印一下。 此时,代理服务器还需要返回一个response, 返回包包括 两个字段,一个是 version 一个是 method,也就是我们选中的鉴传方式,我们当前只准备实现不需要鉴传的方式,也就是00。

    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() // 读版本号
    	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() //读methodSize也是单个字节
    	if err != nil {
    		return fmt.Errorf("read methodSize failed:%w", err)
    	}
    	method := make([]byte, methodSize) //创建一个method缓冲区
    	_, err = io.ReadFull(reader, method) //用io.ReadFull填充满
    	if err != nil {
    		return fmt.Errorf("read method failed:%w", err)
    	}
    	log.Println("ver", ver, "method", method)  // 打印出三个字段
    	// +----+--------+
    	// |VER | METHOD |
    	// +----+--------+
    	// | 1  |   1    |
    	// +----+--------+
    	_, err = conn.Write([]byte{socks5Ver, 0x00}) // 返回一个报文告诉浏览器使用哪种鉴传方式 00表示不需要认证
    	if err != nil {
    		return fmt.Errorf("write failed:%w", err)
    	}
    	return nil
    }
    

    此时curl 命令肯定是不成功的,因为我们的协议还没实现完成。 但是我们看日志会发现, version和method 可以正常打印,

  • 接下来我们开始做第三步,实现请求阶段,我们试图读取到携带 URL 或者 IP 地址+端口的包,然后把它打印出来。我们实现一个和 auth 函数类似的 connect 函数,同样在 process 里面去调用。

    再来实现 connect 函数的代码。我们来回忆一下请求阶段的逻辑。浏览器会发送一个报文,报文里面包含如下6个字段:

    1. version 版本号(还是 5);
    2. command ,代表请求的类型,我们只支持 connection 请求,也就是让代理服务建立新的TCP连接;
    3. RSV 保留字段 0,不理会;
    4. atype 就是目标地址类型,可能是 IPV4、IPV6或者域名;
    5. addr, 这个地址的长度是根据 atype 的类型而不同的
    6. port 端口号,两个字节

    我们需要逐个去读取这些字段。前面这四个字段总共四个字节,我们可以一次性把它读出来。我们定义一个长度为 4 的 buffer 然后把它读满。读满之后,然后第0 个、 第1个、第3个、分别是 version、cmd 和 type。

    version 需要判断是 socket 5, cmd 需要判断是1。

    下面的 atype,可能是 ipv4 ,ipv6,或者是 host。

    1. 如果 IPV 4 的话,我们再次读满这个buffer, 因为这个buffer长度刚好也是4个字节,然后逐个字节打印成 IP 地址的格式保存到 addr变量。
    2. 如果是个 host 的话,需要先读它的长度,再 make 一个相应长度的buf 填充它。 再转换成字符串保存到 addr 变量。
    3. IPV 6 用得比较少, 我们就暂时先不支持。

    最后还有两个字节那个是 port ,我们读取它,然后按协议规定的大端字节序转换成数字。由于上面的 buffer 已经不会被其他变量使用了,我们可以直接复用之前的内存,建立一个临时的 slice ,长度是2用于读取,这样的话最多会只读两个字节回来。 接下来我们把这个地址和端口打印出来用于调试。

    收到浏览器的这个请求包之后,我们需要返回一个包,这个包有很多字段,但其实大部分都不会使用。

    1. 第一个是版本号还是 socket 5。
    2. 第二个是返回的类型,这里是成功就返回0。
    3. 第三个是保留字段 填 0 。
    4. 第四个 atype 地址类型 填 1(Ipv4)
    5. 第五个,第六个暂时用不到,都填成 0。

    一共 4 + 4 + 2 个字节,后面6个字节都是 0 填充。

    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)
    	if err != nil {
    		return fmt.Errorf("read header failed:%w", err)
    	}
    	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])
    
    	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
    }
    

    此时请求还是会失败,我们现在已经能看到正常打印出来访问的 IP 地址和端口,这说明我们当前的实现正常,这样我们就可以做最后一步,我们真正和这个端口建立连接,双向转发数据。

  • 直接用 net.dial 建立一个 TCP 连接 建立完连接之后,我们同样要加一个 defer 来关闭连接。 接下来需要建立 浏览器 和 下游服务器的双向数据转发。 标准库的 io.copy 可以实现一个单向数据转发,双向转发的话,需要启动两个 goroutinue。

      dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
    	if err != nil {
    		return fmt.Errorf("dial dst failed:%w", err)
    	}
    	defer dest.Close()
    	log.Println("dial", addr, port)
    

    现在有一个问题,connect 函数会立刻返回,返回的时候连接就被关闭了。需要等待任意一个方向copy出错的时候,再返回 connect 函数。 这里可以使用到标准库里面的一个 context 机制,用 context 连 with cancel 来创建一个context。 在最后等待 ctx.Done() , 只要 cancel 被调用, ctx.Done就会立刻返回。 然后在上面的两个 goroutinue 里面 调用一次 cancel 即可。

      ctx, cancel := context.WithCancel(context.Background()) 
      defer cancel() 
      go func() {
      _, _ = io.Copy(dest, reader) 
      cancel()
      }() 
      go func() { 
      _, _ = io.Copy(conn, dest)
      cancel() 
      }() 
      <-ctx.Done() 
      return nil
    

5.png