GO语言实例 | 青训营笔记

77 阅读7分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天(其实不是第一天文章发的晚)

在线词典

  1. 抓包并根据curl生成代码
  2. 构造requst body/生成response body响应请求并解析代码
  3. 完成实践项目

1.抓包并根据curl生成代码

课程中词典的实现,直接调用了彩云小译 - 在线翻译 (caiyunapp.com)中的api,实现时的主要工作在于实现在我们自己的程序中构造请求和接收响应的方法。

从网页开发者模式的网络检查中找到api所在的位置,将dict保存为curl(bash)格式,随后将复制好的curl放入Convert curl commands to Go (curlconverter.com)网站进行自动代码生成。 彩云小译 - 在线翻译 和另外 6 个页面 - 个人 - Microsoft​ Edge 2023_1_15 23_40_59.png 生成的代码结果如下

package main 
import ( "fmt" "io/ioutil" "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("accept", "application/json, text/plain, */*") 
    req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-TW;q=0.6") 
    req.Header.Set("app-name", "xy") 
    req.Header.Set("content-type", "application/json;charset=UTF-8")
    req.Header.Set("device-id", "") 
    req.Header.Set("origin", "https://fanyi.caiyunapp.com") 
    req.Header.Set("os-type", "web") 
    req.Header.Set("os-version", "") 
    req.Header.Set("referer", "https://fanyi.caiyunapp.com/") 
    req.Header.Set("sec-ch-ua", `"Not_A Brand";v="99", "Microsoft Edge";v="109", "Chromium";v="109"`) 
    req.Header.Set("sec-ch-ua-mobile", "?0") 
    req.Header.Set("sec-ch-ua-platform", `"Windows"`) 
    req.Header.Set("sec-fetch-dest", "empty") 
    req.Header.Set("sec-fetch-mode", "cors") 
    req.Header.Set("sec-fetch-site", "cross-site") 
    req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.52") 
    req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi") 
    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) 
    } 
    fmt.Printf("%s\n", bodyText) }

这里自动生成了一个向服务器发送数据成功之后打印服务器响应的resp.body的功能。 后面要规范成向服务器发送一个json,并接收服务器响应的json。 由于json包括很多字段,在go的代码中我们使用struct来包装,因为字段较为复杂,课堂上教学了自动生成对应结构体的方法。

2.构造request body和response body

对于词典的request请求。可以直接构造出来。

type DictRequest struct {
	TransType string `json:"trans_type"`
	Source    string `json:"source"`
	UserID    string `json:"user_id"`
}

而服务器返回的response body则需要根据网站上暴露的信息去构造对应的Json格式。 从翻译网站的开发者模式,找到服务器响应的数据,我们可以发现响应回来的json相当之长,并且其中的嵌套较为 复杂,手动构造难度较大。

于是这里课程教我们使用了自动工具JSON转Golang Struct - 在线工具 - OKTools将json转换成嵌套类型的结构体,方便go代码的处理。 因为这里我们只是要用这个结构体接收数据,并不会对其中的某些字段进行处理,所以选用的是嵌套结构体。如果想要对json中的更多字段进行更精细的处理需要考虑对json进行展开构造。

QQ截图20230116214309.jpg

生成的response结构体如下

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"`
}

到这里我们已经拥有了request body和response body, 可以利用他们进行对翻译服务的请求和接收响应了。

3.完善实践项目

入口函数读取指令行,并进行异常处理。一切正常进入query服务,进行api调用。

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)
}

query服务中利用结构体封装成json,再设置消息头进一步封装成request,通过client的do方法将请求发送到指定的服务端。 之后从服务端读取响应,储存在bodytext中,这里是json数据。 接着,在检查完状态信息正常后对json进行反序列化储存在dictResponse结构体中。 最后对其进行打印。 代码如下:

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("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()
	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)
	}
}

socks5代理

浏览器和socks5代理服务器建立tcp连接,socks5再与后端服务器建立连接。

分为以下阶段: 1.协商阶段 2.认证阶段(握手) 3.请求阶段 4.relay阶段

协商阶段:用户浏览器向代理服务器发送报文(协议版本号,支持的认证种类),代理服务器返回合适的鉴权方式。

认证。

请求阶段:认证通过后浏览器向代理服务器发送下一个报文(协议版本号,请求的类型),一般是connection请求,命令代理服务器和某个端口(ip)建立tcp连接。建立成功后返回一个响应报文告诉浏览器tcp连接建立成功。 建立后,浏览器发送请求,代理服务器转发请求。如果服务器有响应,则由代理服务器转发响应到浏览器。

代理服务器不关注流量细节

socks5代理的原理图如下: socket5.jpg

建立tcp server

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
	}
}

auth函数的实现

这个函数实现的是协商阶段的工作,发送版本号和供协商的鉴权方式。 在函数中读出只读流发送进来的协议内容,并且返回所选择的鉴权方式。

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
}

实现请求函数

浏览器发送请求报文,由代理服务器转发。 这里使用了4个字节的缓冲区一次存取四个字节进行存储(根据实际的结构来的,在协商函数的读取是采取一个字节一个字节读取的方式)

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])

	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)

	// +----+-----+-------+------+----------+----------+
	// |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()

	go func() {
		_, _ = io.Copy(dest, reader)
		cancel()
	}()
	go func() {
		_, _ = io.Copy(conn, dest)
		cancel()
	}()

	<-ctx.Done()
	return nil
}