第六届字节跳动青训营1 Go原理与实践 | 豆包MarsCode AI刷题

54 阅读9分钟

第六届字节跳动青训营1 Go原理与实践 | 豆包MarsCode AI刷题

实现一个简易命令行词典

需求分析

实现一个翻译工具,显然要借助第三方API来支持翻译功能。因此涉及到HTTP协议通信。

本文出于简单期间,不采用注册API的方式,而是直接采用HTTP协议请求彩云小译的翻译URL来实现。

获取翻译API

可以像字节视频中那样通过URL抓包来获取API,也可以直接使用官方的测试API。但是官方的API貌似已经过期了。

抓包获得的API(已经去除无用Header字段),可以用curl验证一下:

curl 'https://api.interpreter.caiyunai.com/v1/dict' \
  -H 'accept-language: zh' \
  -H 'content-type: application/json;charset=UTF-8' \
  -H 'x-authorization: token:qgemv4jr1y38jyq6vhvi' \
  --data-raw '{"trans_type":"en2zh","source":"good"}'

客户端代码实现

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
	"sync"
)

const URL = "http://api.interpreter.caiyunai.com/v1/dict"
const token = "qgemv4jr1y38jyq6vhvi"

// type for json marshal/unmarshal
type DictRequest struct {
	Source    string `json:"source"`
	TransType string `json:"trans_type"`
}
type DictResponse struct {
	Rc   int `json:"rc"`
	Wiki struct {
		KnownInLaguages int `json:"known_in_laguages"`
		Description     struct {
			Source string      `json:"source"`
			Target interface{} `json:"target"`
		} `json:"description"`
		ID   string `json:"id"`
		Item struct {
			Source string `json:"source"`
			Target string `json:"target"`
		} `json:"item"`
		ImageURL  string `json:"image_url"`
		IsSubject string `json:"is_subject"`
		Sitelink  string `json:"sitelink"`
	} `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 buildRequest(req *http.Request) {
	req.Header.Set("Connection", "keep-alive")
	req.Header.Set("Content-Type", "application/json;charset=UTF-8")
	req.Header.Set("X-Authorization", "token:"+token)
}

func doTransLate(client *http.Client, word string) {
	request := DictRequest{
		Source:    word,
		TransType: "en2zh",
	}
	buf, err := json.Marshal(request)
	if err != nil {
		fmt.Printf("Marshal request failed: %v\n", err)
		return
	}
	var data = bytes.NewReader(buf)
	req, err := http.NewRequest("POST", URL, data)
	if err != nil {
		fmt.Printf("New request failed: %v\n", err)
		return
	}
	buildRequest(req)
	resp, err := client.Do(req)
	if err != nil {
		fmt.Printf("Do request failed: %v\n", err)
		return
	}
	defer resp.Body.Close()
	bodyText, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("Read response failed: %v\n", err)
		return
	}
	if resp.StatusCode != http.StatusOK {
		fmt.Printf("Bad StatusCode: %d, body: %s", resp.StatusCode, string(bodyText))
		return
	}
	var dictResponse DictResponse
	err = json.Unmarshal(bodyText, &dictResponse)
	fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
	for _, item := range dictResponse.Dictionary.Explanations {
		fmt.Println(item)
	}
}

// translate 每次调用都会建立一个新的http连接
func translate(words []string) {
	// 建立一个http client,注意这里要用指针,否则无法写入数据
	client := &http.Client{
		Timeout: 10 * time.Second,
	}
	wg := &sync.WaitGroup{}
	fmt.Println("======================================================================")
	for _, word := range words {
		wg.Add(1)
		go func (word string)  {
			defer wg.Done()
			doTransLate(client, word)
			fmt.Println("======================================================================")
		}(word)
	}
	wg.Wait()
}

func main() {
	if len(os.Args) < 2 {
		fmt.Printf("Usage: %s <word>...\n", os.Args[0])
		os.Exit(1)
	}
	words := os.Args[1:]
	fmt.Println("Input words:", words)
	translate(words)
}

image.png

实现一个简易socks5代理服务器

socks5代理

SOCKS5 代理是 Socket Secure version 5 的缩写。Socks5协议是一种通过代理服务器处理各种协议数据流量的协议。它在使用TCP/IP协议通讯的客户端和服务器之间扮演一个中介角色,使得内网中的客户端变得能够访问Internet网中的服务器,或者使C/S(Client和Server)之间的通讯更加安全。

通信过程

Socks5代理服务器通过将客户端发来的请求转发给真正的目标服务器, 模拟了一个客户端请求操作。当连接建立后,客户端就可以和正常一样访问服务端通信了,此时通信的数据除了目的地址是发往代理程序以外,所有内容都是和普通连接一模一样。对代理程序而言,后面所有收到的来自客户端的数据都会原样转发到服务端。

SOCKS 服务默认监听 1080 端口,如果连接成功,客户端需要与服务端协商认证方式并完成认证,之后便可以发送中继请求。SOCKS 服务端会执行请求,要么建立起合适的连接,要么拒绝请求。

image.png

如上图,客户端和Socks5代理服务器之间也是通过TCP/IP协议进行通讯,客户端将原本要发送给真正服务器的请求先发送给Socks5代理服务器,然后Socks5代理服务器再将请求转发给真正的服务器。

注意SOCKS协议报文通过TCP协议传输,因此均是网络字节序

1. 认证阶段

客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方式。认证报文格式如下:

VERNMETHODSMETHODS
1B1B1 to 255 B
  • VER: 协议版本:

    • 0x04:socks4协议
    • 0x05:socks5协议
  • NMETHODS: 支持认证的方法数量

  • METHODS: 支持的认证方法。对应NMETHODS字段,NMETHODS的值为多少,METHODS就有多少个字节:

    • 0x00:无需认证
    • 0x01:GSSAPI
    • 0x02:用户名/密码
    • 0xFF:没有可用的方法,客户端收到此信息必须关闭连接。

随后,客户端与服务端开始协商该方法对应的后续认证,后续认证方法因方法而异,在此不进行展开。

2. 请求阶段

一旦认证方法对应的协商完成,客户端就可以发送请求细节了。如果认证方法为了完整性或者可信性的校验,需要对后续请求报文进行封装,则后续请求报文都要按照对应规定进行封装。请求报文格式如下:

VERCMDRSVATYPDST.ADDRDST.PORT
1B1B0x001B可变2B
  • VER:socks协议版本,同认证报文

  • CMD:表示该请求的TCP/UDP请求类型

    • 0x01:表示CONNECT请求
    • 0x02:表示BIND请求
    • 0x03:表示UDP ASSOCIATE请求
  • RSV:保留字段,填充全0

  • ATYP:目标地址类型,即DST.ADDR字段的地址类型

    • 0x01:IPv4地址,DST.ADDR固定4个字节
    • 0x03:域名,DST.ADDR可变,且第一个字节为域名长度标识。其不以\0为终结符。
    • 0x04:IPv6地址,DST.ADDR固定16个字节
  • DST.ADDR:地址值,根据ATYP的值,其长度也不同

  • DST.PORT:目标端口

SOCKS 服务端会根据请求类型和源、目标地址,执行对应操作,并且返回对应的一个或多个报文信息。

3. 回复阶段

客户端与服务端建立连接并完成认证之后就会发送请求信息,服务端执行对应请求并返回如下格式的回复报文:

VERREPRSVATYPBND.ADDRBND.PORT
11X’00’1Variable2
  • VER:socks协议版本,同认证报文

  • REP:回复类型:

    • 0x00:成功
    • 0x01:常规SOCKS服务故障
    • 0x02:规则不允许的连接
    • 0x03:网络不可达
    • 0x04:主机无法访问
    • 0x05:拒绝连接
    • 0x06:连接超时
    • 0x07:不支持的命令
    • 0x08:不支持的地址类型
    • 0x09~0xFF:保留,尚未定义
  • RSV:保留字段,填充全0

  • ATYPE:同请求报文

  • BND.ADDR:客户端绑定的服务端地址。规则同请求报文。

  • BND.PORT:客户端绑定的服务端端口

如果协商的方法为了完整性、可信性的校验需要封装数据包,则返回的数据包也会进行对应的封装。

代理服务器代码实现

package main

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
)

const (
	socks5Ver = 0x05
	cmdBind = 0x01
	atypeIPV4 = 0x01
	atypeHOST = 0x03
	atypeIPV6 = 0x04
)

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)
	err := auth(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		return
	}
	err = connect(reader, conn)
	if err != nil {
		log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
		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()
	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()
	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)
	}

	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_, err = conn.Write([]byte{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) // 先解析前4个字节的字段,因为它们是固定的,同时也减少IO次数
	_, 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", cmd)
	}
	addr := ""
	switch atyp {
	case atypeIPV4:
		_, 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]) // 注意reader是流,因此这里直接就是从port字段开始读
	if err != nil {
		return fmt.Errorf("read port failed:%w", err)
	}
	port := binary.BigEndian.Uint16(buf[:2])

	dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port)) // 与指定的目标建立TCP连接
	if err != nil {
		return fmt.Errorf("dial dst via TCP failed:%w", err)
	}
	defer dest.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)
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	// 将客户端和目标服务器之间的数据进行转发(relay)
	go func() {
		_, _ = io.Copy(dest, reader) // 从客户端读取数据,写入目标服务器
		cancel()
	}()
	go func() {
		_, _ = io.Copy(conn, dest) // 从目标服务器读取数据,写入客户端
		cancel()
	}()

	<-ctx.Done()
	return nil
}

使用curl来测试:指定代理服务为127.0.0.1:1080,访问www.baidu.com

image.png