第六届字节跳动青训营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)
}
实现一个简易socks5代理服务器
socks5代理
SOCKS5 代理是 Socket Secure version 5 的缩写。Socks5协议是一种通过代理服务器处理各种协议数据流量的协议。它在使用TCP/IP协议通讯的客户端和服务器之间扮演一个中介角色,使得内网中的客户端变得能够访问Internet网中的服务器,或者使C/S(Client和Server)之间的通讯更加安全。
通信过程
Socks5代理服务器通过将客户端发来的请求转发给真正的目标服务器, 模拟了一个客户端请求操作。当连接建立后,客户端就可以和正常一样访问服务端通信了,此时通信的数据除了目的地址是发往代理程序以外,所有内容都是和普通连接一模一样。对代理程序而言,后面所有收到的来自客户端的数据都会原样转发到服务端。
SOCKS 服务默认监听 1080 端口,如果连接成功,客户端需要与服务端协商认证方式并完成认证,之后便可以发送中继请求。SOCKS 服务端会执行请求,要么建立起合适的连接,要么拒绝请求。
如上图,客户端和Socks5代理服务器之间也是通过TCP/IP协议进行通讯,客户端将原本要发送给真正服务器的请求先发送给Socks5代理服务器,然后Socks5代理服务器再将请求转发给真正的服务器。
注意:SOCKS协议报文通过TCP协议传输,因此均是网络字节序。
1. 认证阶段
客户端向代理服务器发送代理请求,其中包含了代理的版本和认证方式。认证报文格式如下:
VER | NMETHODS | METHODS |
---|---|---|
1B | 1B | 1 to 255 B |
-
VER
: 协议版本:0x04
:socks4协议0x05
:socks5协议
-
NMETHODS
: 支持认证的方法数量 -
METHODS
: 支持的认证方法。对应NMETHODS
字段,NMETHODS
的值为多少,METHODS
就有多少个字节:0x00
:无需认证0x01
:GSSAPI0x02
:用户名/密码0xFF
:没有可用的方法,客户端收到此信息必须关闭连接。
随后,客户端与服务端开始协商该方法对应的后续认证,后续认证方法因方法而异,在此不进行展开。
2. 请求阶段
一旦认证方法对应的协商完成,客户端就可以发送请求细节了。如果认证方法为了完整性或者可信性的校验,需要对后续请求报文进行封装,则后续请求报文都要按照对应规定进行封装。请求报文格式如下:
VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
---|---|---|---|---|---|
1B | 1B | 0x00 | 1B | 可变 | 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. 回复阶段
客户端与服务端建立连接并完成认证之后就会发送请求信息,服务端执行对应请求并返回如下格式的回复报文:
VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
---|---|---|---|---|---|
1 | 1 | X’00’ | 1 | Variable | 2 |
-
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