1.1 guessing-game
值得记忆的:
1、input = strings.Trim(input, "rn")
去掉用户输入中的回车和换行符,确保输入的数字不带有多余的空格。
2、guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue }
使用 strconv.Atoi(input) 将用户输入的字符串转换为整数 guess。如果转换出错,说明用户输入的不是有效的整数,程序会提示用户重新输入并进入下一次循环。 guess, err := strconv.Atoi(input)是一种结构,就是假如说strconv.Atoi的值正常,就正常赋值给guess,否则的话,执行出错语句。
3、在go中,if-else判断语句中的条件判断都是不加括号的。
4、reader := bufio.NewReader(os.Stdin)
NewReader:从所给的字符串中读取数据,在1.2内存优化。
1.2 simple-dictionary在线词典——用go语言来发送http请求
假设查询handle这个单词的中文:
抓包
随便查询个单词,然后找到dict,post请求,服务器返回内容如下:
复制curl时候以cmd格式复制和以bash格式复制区别在于cmd格式是windows命令格式,而bash是linux格式,转义符不一样,下面的代码生成网址需要用bash格式。
结果返回一串json(bash格式):
curl 'https://api.interpreter.caiyunai.com/v1/dict'
-H 'authority: api.interpreter.caiyunai.com'
-H 'accept: application/json, text/plain, */*'
-H 'accept-language: zh-CN,zh;q=0.9,en;q=0.8'
-H 'app-name: xy'
-H 'cache-control: no-cache'
-H 'content-type: application/json;'
-H 'device-id: 530bc22ecab916f3ff3b535ad8002137'
-H 'origin: https://fanyi.caiyunapp.com'
-H 'os-type: web'
-H 'os-version;'
-H 'pragma: no-cache'
-H 'referer: https://fanyi.caiyunapp.com/'
-H 'sec-ch-ua: "Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"'
-H 'sec-ch-ua-mobile: ?0'
-H 'sec-ch-ua-platform: "Windows"'
-H 'sec-fetch-dest: empty'
-H 'sec-fetch-mode: cors'
-H 'sec-fetch-site: cross-site'
-H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36'
-H 'x-authorization: token:qgemv4jr1y38jyq6vhvi'
--data-raw '{"trans_type":"en2zh","source":"handle"}'
--compressed
打开代码生成网址curlconverter.com/#go
实际上读下来就是首先创建一个请求,并且加上请求头,之后就是发起请求,接收。
值得记住:
1、内存优化
var data = strings.NewReader(`{"trans_type":"en2zh","source":"handle"}`)
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
data实际上不是字符串,而是流,所以strings.NewReader实际上是将字符串转化成流,因为有时候读取的字符串可能会很大,就会很占用内存空间。
2、resp, err := client.Do(req) //发起请求
client是前面创建的client := &http.Client{},作为一个客户端实例可以来发起http请求。
但实际上,输出的一连串json数据我们需要易于人类阅读的情况下,就要对它进行序列化,也就是转换为json字符串。
在序列化的时候还有一个区别就是
var data = strings.NewReader(`{"trans_type":"en2zh","source":"handle"}`) //未序列化
var data = bytes.NewReader(buf) //有序列化
注意未序列化之前读取的是字符串,所以NewReader使用的是strings,序列化之后是使用读取字节的方式,所以用bytes。
值得记住:
1、JSON序列化
首先需要先定义结构体,用于对应JSON数据,如:
type Person struct {
Name string
Age int
Email string
}
通过将上述结构体实例化并填充字段值后,可以使用Go语言的标准库中的 json.Marshal()
函数将其序列化为JSON格式的序列化数据。
package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string
Age int
Email string
}
func main() {
person := Person{
Name: "John",
Age: 30,
Email: "john@example.com",
}
jsonBytes, err := json.Marshal(person)
if err != nil {
fmt.Println("Error:", err)
return
}
jsonString := string(jsonBytes)
fmt.Println(jsonString)
}
最后再重新转换成字符串:
jsonString := string(jsonBytes)
输出结果:
{"Name":"John","Age":30,"Email":"john@example.com"}
回到代码中来,我们序列化后的内容是人能够看懂的,但是要机器能够操作,还需要把它反序列化。
因为我们人为来写非常麻烦,所以我们可以借助机器。把浏览器中的JSON数据复制到JSON转Golang Struct这个网站中:
先到这吧,剩下的太难了。
1.3 SOCKS5代理
比较有疑问的就是第一阶段协商阶段是代理要求用户是否需要加密认证,这部分我们简单以未加密为例;仔细看这个图,接下来所有步骤都会围绕它来展开。
下面是一个简单的建立TCP链接,一旦客户端链接成功,会将接收到的数据原样返回给客户端。
使用一个无限循环 for
来接受客户端连接。一旦有客户端连接成功,server.Accept()
将会阻塞并返回一个新的 net.Conn
实例 client
,(注意这里是net.Conn实例client)用于处理与该客户端的通信。如果接受连接时出现错误,它将打印错误日志并继续等待下一个连接。假设监听到客户端连接时,就会继续执行go process(client);
3、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
}
}
}
使用一个无限循环来不断读取客户端发送的数据。在循环中,通过 reader.ReadByte()
读取一个字节,并将其原样写回给客户端,使用 conn.Write()
将字节切片 {b}
发送回客户端。
在Go语言中,下划线
_
是一个特殊的标识符,通常用作占位符。在这个上下文中,_
被用作匿名变量,用于接收函数返回值中不需要的部分。代码执行了conn.Write([]byte{b})
函数调用,并通过_
来接收函数返回值中的写入的字节数。由于我们不关心写入的字节数,所以使用_
来忽略这个值。
实际上,因为server和client是TCP连接相互通信的,所以它的原理:首先reader读取conn中的每一个字节,然后再重新写回到conn中,由于我们不关心变量是什么,所以用_来代表协会的值,也就是说,写回的值同样会显示出来,表示双方之间的通信。
4、reader.ReadByte()函数,表示一个字节一个字节读。
1.3.1 auth认证阶段
大概讲明白了彼此之间建立通信的原理之后,从第一个阶段认证阶段开始来完善代码。
值得记住(先看auth()函数) :
1、method := make([]byte, methodSize)
内置函数make( ):用于创建切片、映射和通道的函数。它的语法为 make(T, length)
,其中 T
是切片、映射或通道的类型,length
是切片的长度、映射的初始容量或通道的缓冲区大小。
这句代码可以理解为在Go语言中创建一个字节切片([]byte
)变量 method
,并分配了一个长度为 methodSize
的内存空间给这个切片,其中因为methodSize对应NMETHODS,所以methodSize的值为多少,METHODS就有几个字节。
2、_, err = io.ReadFull(reader, method)
io.ReadFull( )函数:从输入流(注意是流,不是字符串,前面我们讲过用流能更好地优化内存)中读取指定长度的数据,因为method的内存空间之前被分配为methodSize,所以这句代码的含义是从输入流 reader
中读取 methodSize
个字节,并将这些字节填充到字节切片 method
中。
并且,因为只关心method,所以我们可以用一个占位符_来代替我们并想创建的新变量。
3、_, err = conn.Write([]byte{socks5Ver, 0x00})
_, err = conn.Write([]byte{b})
对比一下,返回包到连接中,前者是返回socks5Ver, 0x00,后者返回b所对应的内容
4、func auth(reader *bufio.Reader, conn net.Conn)
我们使用 bufio.Reader
作为参数传递给函数时,如果不加上 *
号,函数接收到的将是 bufio.Reader
类型的一份副本,而不是原始的 bufio.Reader
类型变量。我们希望在函数内部能够直接操作原始的 bufio.Reader
变量,而不是副本。为了实现这一点,我们需要将 bufio.Reader
声明为指针类型,并在函数参数中使用 *bufio.Reader
。一般来说,使用指针调用的目的最主要是方便函数调用的时候能够对所指向的变量进行修改,但是在此处暂时还看不到修改什么。
接下来回到process( )函数:
与前面的process( )函数不同的是,这里在读取这里开始引入auth( )函数的内容,开始要读取认证,要返回是否认证成功的结果。
实际上现在的代码用curl命令运行还是运行不起来的,因为它只包含了认证,但是没有具体的请求体。
1.3.2 Request请求阶段
继续来完善代码,其中有变化的为:process( )函数,以及创建的connect( )函数:
值得记住(先看connect( )函数) :
1、
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]
创建一个四字节的缓冲区,然后用io.ReadFull填满;接下来按顺序读取其中0,1,3的字段,因为前四个字段都是定长的。
2、_, err = io.ReadFull(reader, buf[:2]) //此处为了读端口号的固定2个字节
关于buf[:2]的解释:表示对 buf
字节切片的前两个字节进行切片操作,这样我们创建了一个长度为 2 的新字节切片。
回到process( )函数:
它比先前的多了请求部分:
err = connect(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
在这里,使用 =
而不是 :=
是因为 err
已经在函数开始时用 :=
进行了声明和初始化。在 Go 语言中,:=
用于声明并初始化一个新的变量。如果变量已经存在,则应该使用 =
进行赋值操作。
1.3.3 relay连接阶段
其中有变化的函数为connect( ):
值得记住:
1、dest, err := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
if err != nil {
return fmt.Errorf("dial dst failed:%w", err)
}
使用 net.Dial
建立与目标服务器的连接 dest
,目标地址为 addr:port
。
2、ctx, cancel := context.WithCancel(context.Background())
defer cancel() //等待后面的调用,不用着急,在go func()中就有调用了
使用 context.WithCancel
创建一个新的上下文 ctx
,同时返回一个取消函数 cancel
。context.Background()
是 context
包中的一个默认上下文,它是最基本的上下文,不包含任何值和超时。它通常用作整个请求处理链中的根上下文。
通过调用 cancel()
函数,可以手动触发上下文的取消,这将导致与该上下文相关联的所有 Goroutine 都收到取消信号,并有机会进行清理和退出。在这里,该上下文用于控制数据转发的两个 Goroutine(协程),当一个 Goroutine 完成数据转发后,调用 cancel()
来取消上下文,从而终止另一个 Goroutine,实现数据转发的完整结束。
3、
go func() {
_, _ = io.Cop y(dest, reader) //从用户浏览器拷贝数据到底层服务器
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest) //从底层服务器拷贝数据到用户浏览器
cancel()
}()