这是我参与「第五届青训营」伴学笔记创作活动的第1天
go基础
go语言的特点
1.高性能,高并发
2.语法简单,学习曲线平滑
3.丰富的标准库
4.完善的工具链
5.静态编译
6.快速编译
7.跨平台
8.垃圾回收
go的if条件语句
条件不用带括号,条件前可以加表达式
go的for循环
for循环中的三部分都可以省略,不带条件进入死循环,break退出循环,continue进入下一次循环,通过和if的组合可以实现while的功能,go中没有while语句.
go的switch分支
case的条件可以是任意的变量类型,字符串,结构体,int类型等等,还可以取代多条件的if语句,每一个case不会自动下坠,要使用fallthrough进行穿透
结构体的使用
在对结构体的使用中,结构体可以作为参数使用,就会涉及到值拷贝,我们使用结构体的指针,可以对结构体的值进行修改,同时还可以避免一些大结构体的拷贝.
字符串的一些操作
标准库中包含许多可以对字符串进行操作的函数,可以对字符串进行一些常见的操作
获取进程信息
os.Args()可以获取进程执行时的一些命令行参数
os.Getenv(),os.Setenv()可以获取和写入环境变量
exec.Command()可以快速启动子进程并且获取其输入输出
猜数字游戏
1.设置随机数种子,保证每次rand的随机数都不一样,然后生成一个随机数
scss
复制代码
rand.Seed(time.Now().UnixNano())//里面的参数,当前的时间纳秒戳
secretNumber := rand.Intn(maxNum)
2.读取玩家的键盘输入
2.1 fmt包
读取键盘的方式,可以用fmt包下的Scan(),Scanln()或者Scanf()来扫描键盘输入。
2.1.1 fmt.Scan
读取以空白符分割的值返回到地址中进行修改,换行也视为空白符,换行和空格是不能存储到变量内的。
也就是说,我们可以连续输入多个字符串,存放在不同的地址中,但是要以空白符进行分割这些字符串。
ag1, err := fmt.Scan(&name,&age,&married)
返回的ag1是正确扫描的项目数量
2.1.2 fmt.Scanln
和Scan类似,在换行的地方停止扫描。也就是说,空白符也可以做分割,空白和换行都不能存储到变量内,碰到空白停止扫描。
2.1.3 fmt.Scanf
将连续的空格分隔值存储到由格式确定的连续参数中。它返回成功扫描的项目数。如果它小于参数的数量,err 将报告原因。输入中的换行符必须与格式中的换行符匹配。一个例外:动词 %c 总是扫描输入中的下一个符文,即使它是空格(或制表符等)或换行符。
还可以使用os.Stdin来读取
2.2 os包
2.2.1 直接读取
将系统输入直接读取在一个创建好的buffer切片中
arduino
复制代码
var buffer [512]byte
n, err := os.Stdin.Read(buffer[:])
#2.2.2 通过bufio来读取
先通过bufio创建一个reader,参数就是os.stdin
再通过reader的函数来读取字符串
css
复制代码
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
我们这里使用的是这一种读取键盘输入的方式,该方式会把我们输入的换行和空格也保留。需要使用strings包的Trim()函数把换行符去掉。
ini
复制代码
input = strings.Trim(input, "\r\n")
3.将获取的用户输入字符串转换成整数
拿到的用户输入是字符串,需要转换成整数,使用strconv包的Atoi函数来转换。
css
复制代码
guess, err := strconv.Atoi(input)
4.用户输入与生成数相比较
现在就是比大小,用户输入大了或者小了给出相应提示,让用户重新输入,用死循环,知道用户输入正确。
简易单词翻译
1.前置信息
我们利用彩云小译来翻译我们想要翻译的单词,在浏览器打开彩云小译界面,再点开开发者工具。接下来在搜索框,我们搜索单词之后,点击翻译,我们就可以在开发者工具界面看到我们刚刚的dict的post请求。
请求的json里面包含了两个字段,source和trans_type,这两个字段很重要,我们等会写程序要用上,其中的trans_type字段,en2zh表示英译中。
下面的截图,是查询后返回的一些东西,包括了发音,音标,释义还有其他的信息,我们等会需要的是释义。所以要对返回的信息进行处理,只拿我们需要的。
我们需要在我们的go的ide里去完成这些请求,而且接收网页传递过来的信息并作出相应的处理,如果我们直接去敲击代码,构造一些Headers会十分麻烦,课程里介绍了一个很简单的方式来生成请求。
2.生成post请求
现在尝试使用网页工具来生成我们的post请求。
首先右键点击我们刚刚的dict请求,找到Copy里的Copy as cURL (bash),然后点击,注意不要点错了,
接下来需要打开一个转换curl命令的网页,网页链接:curlconverter.com/go/,这个网页工具可以把我们curl转换成其他的语言,会直接帮我们生成对应语言的代码,把刚刚复制的代码粘贴进去,点击go,就会生成go语言的代码。由于curl语义优点复杂,转换过程中会出现一些转义导致的错误,删掉那部分就可以了。
1.版本v1
在前置内容里,最后我们生成了对彩云小译网站发起请求的代码,直接可以运行一下
erlang
复制代码
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
func main() {
//1.创建一个客户对象
client := &http.Client{}
//将这个字符串转换成一个流,作为后面的输入参数,字符串里包含我们要查的单词和转换方式
var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`)
//2.创建一个post请求
req, err := http.NewRequest("POST", "https://api.interpreter.caiyunai.com/v1/dict", data)
if err != nil {
log.Fatal(err)
}
//3.设置请求的一些Header
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")
//4.客户执行这个请求,也就是发起请求,会返回一个response结构体
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close() //body是一个流,为了防止资源泄露,要defer延迟关闭一下这个流
//5.读取res的body,得到一个byte数组
bodyText, err := ioutil.ReadAll(resp.Body) //读取body中的内容转换成字符串输出
if err != nil {
log.Fatal(err)
}
//6.转成字符串输出
fmt.Printf("%s\n", bodyText)
}
输出了很长的一段json,我们应该要对这个json处理一下,需要构造结构体,让结构体的字段和json结构相对应,然后反序列化,就可以拿到我们需要的内容。
2.版本v2
在上面生成的代码中,data流是直接生成的我们实例中查询的单词和翻译方式,接下来我们要换成自己想要查询的内容。首先构建结构体,将上面的信息填充到结构体中,然后序列化成json格式就可以了。
go
复制代码
type DictRequest struct {//构建结构体
TransType string `json:"trans_type"`
Source string `json:"source"`
UserID string `json:"user_id"`
}
request := DictRequest{TransType: "en2zh", Source: "good"}
buf, err := json.Marshal(request)//序列化
if err != nil {
log.Fatal(err)
}
var data = bytes.NewReader(buf)//注意序列化后返回的是byte数组,所以使用bytes包
执行后的输出不会发生改变。
3.版本v3
接下来解析response body的内容,获取我们需要的内容。我们需要构建与response body里面的内容一一对应的结构体,可以看到输出结果中的json结构体非常复杂,直接去构建很容易出错,所以课程中也介绍了一种代码生成的方式来生成我们所需要的结构体。
网页工具链接:oktools.net/json2go
复制彩云小译中我们查询单词的response中的json,粘贴在我们的工具网页中
转换-展开:可以让我们生成的代码更容易读懂。
转换-嵌套:可以让我们的代码更紧凑。
将代码拷贝,并创建一个结构体对象,然后把拿到的bodyText反序列化到我们构建的结构体对象中。
go
复制代码
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
版本v4
刚刚所有的代码都在我们的main函数中,显得函数十分冗长,我们将这些代码放在另一个函数query中,后面使用main函数来调用。
前面拿到的response,虽然我们拿到了结果,但不代表拿到的结果就一定正确,我们再验证一下response.StatusCode也就是状态码,看是否是200,是的话再往下执行。
c
复制代码
if resp.StatusCode != 200 {
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
在该函数中只打印我们需要的中文释义就可以了
go
复制代码
fmt.Println(word, "UK:", dictResponse.Dictionary.Prons.En, "US:", dictResponse.Dictionary.Prons.EnUs)
for _, item := range dictResponse.Dictionary.Explanations {
fmt.Println(item)
}
另外修改一下我们的函数主体,先判断命令的长度是不是2,不是2就提示用户输入单词,否则就提取要查询的单词,调用query函数查询。
scss
复制代码
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
//os.Args[]中第0个值是前置的命令
word := os.Args[1]
query(word)
}
结果展示:
socks5 proxy实现步骤总结
代理逻辑
首先我们要搞懂,服务器代理的逻辑,我们使用浏览器与我们想要访问的服务器进行交互时,浏览器与服务器需要进行tcp连接,而使用代理服务器,是浏览器和代理服务器建立连接,然后代理服务器再与我们想要访问的服务器进行tcp连接.
两边都连接完成后,此时代理服务器会把我们想要发送给的内容转发给目标服务器,而目标服务器回复的内容代理服务器也会转发给我们,相当于,是代理服务器代理我们访问目标服务器.
下面是代理的一个流程图
版本v1
首先我们先实现一个简单的逻辑版本的代理器,创建一个监听服务,持续接收来自其他地方的访问,当收到访问时就创建一个连接,并且起一个goroutine来为这个连接提供一个简单的服务.
1.创建一个tcp协议的监听服务,对本地端口1080进行监听.
go
复制代码
server, err := net.Listen("tcp", "127.0.0.1:1080")
if err != nil {
panic(err)
}
2.使用死循环来持续监听,当监听到访问时就建立一个conn连接,并且启动一个协程来为这个连接服务,并且继续监听.
go
复制代码
for {
client, err := server.Accept()
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client) //启动go routine
}
3.在服务的协程中,我们首先要对该连接进行延时关闭,当服务结束时,系统会以出栈的顺序关闭我们的defer对象,我们这里只是简单的起了一个输入流,来读取客户发送过来的数据,并且原封不动的使用输出流转发给客户端.
go
复制代码
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
}
}
}
版本v2
在这个版本中,我们开始第二步,认证阶段,客户与我们进行认证,我们在process中对客户发送过来的认证tcp报文进行解析,并作出回复就可以完成认证,我们首先需要了解发送过来的报文中包含哪些字段
- 报文的第一个字段是版本号,socks5显示为0x05,占一个字节
- 第二个字段NMETHODS,是支持认证的方法数量,占一个字节,它的值是多少,METHODS的长度就是多少个字节,我们可以根据它的大小来设置一个缓冲字节数组,用于接收METHODS数据.
- 第三个字段METHODS,对应NMETHODS。至于该字段的值的含义,我们暂时不清楚.
arduino
复制代码
// +----+----------+----------+
// |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
1.在我们创建了一个输入流的信息后,调用一个auth方法去解析该输入流的报文
go
复制代码
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
}
log.Println("auth success")
}
2.auth方法,对输入流先读取一个字节,比较一下是否是socks5,我们提前定义一个常量来进行比较,再读取第二个字节,就可以定义一个缓冲数组接收剩下的字节了,缓冲数组的长度前面说过,就是第二个字节的数值.打印一下就行.之后再给该连接写入一些数据,写入的内容不重要.
go
复制代码
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
//这里使用ReadByte()一个字节一个字节的读取
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)
}
log.Println("ver", ver, "method", method)
//回复内容
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
版本v3
以上我们已经完成了与客户建立连接认证.但是没有与目标服务器建立连接。接下来我们需要解读客户传过来的与目标服务器建立连接的报文的具体内容。内面包含了用户要访问的url或者ip+端口号。下面是具体的内容:
arduino
复制代码
// +----+-----+-------+------+----------+----------+
// |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个字节
1.后面需要对比字段的16进制的内容,我们预先设置了一些常量用于比较
ini
复制代码
const socks5Ver = 0x05
const cmdBind = 0x01
const atypIPV4 = 0x01
const atypeHOST = 0x03
const atypeIPV6 = 0x04
2.在上面的认证好了之后,在调用一个connect()函数来解析连接报文
go
复制代码
func connect(reader *bufio.Reader, conn net.Conn) (err error) {
//这里一次性读取前面4个字段
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 := ""
//判断atyp是变长域名还是ipv4地址
switch atyp {
case atypIPV4://ipv4就直接用刚刚的buf接收
_, 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://ipv6暂时不进行操作
return errors.New("IPv6: no supported yet")
default:
return errors.New("invalid atyp")
}
//最后两个字节是端口号,用刚刚的buf切片的方式来接收
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
//这个函数可以把字节数组转换成无符号16位整数
port := binary.BigEndian.Uint16(buf[:2])
log.Println("dial", addr, port)
//这里有一些字段不重要,就直接使用0来代替,回复报文,写了版本号和目标地址类型
_, 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
}
版本v4
上面的版本已经对报文进行了解析,拿到了目标的url或者ip地址和端口号,我们现在开始对目标服务器进行连接
在上面的基础上添加代码
1.在拿到地址和端口号之后,我们使用标准库net中的函数Dial()进行拨号,如果成功会返回一个Conn连接
go
复制代码
_, 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)
2.拿到和服务器的连接后,我们需要将客户后续发给我们的内容持续转发给服务器,并且同样将服务器回复的内容持续转发给客户,这里我们使用io包里面的Copy()函数,另起两个协程来完成这两个工作,但是协程不会影响主进程的进行,所以不加干涉的话,主进程结束了协程也会被动结束,
我们使用标准库中Context中的WithCancel()函数,这是一个很重要的内容
func WithCancel
scss
复制代码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel returns a copy of parent with a new Done channel. The returned context's Done channel is closed when the returned cancel function is called or when the parent context's Done channel is closed, whichever happens first.
Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.
大意是 :WithCancle返回一个由parent对象拷贝的context对象,里面包含一个新的Done管道,返回的上下文的Done管道会在返回的cancle函数被调用或者他的父亲上下文的Done管道关闭时关闭,无论哪一个先发生。
取消该上下文会释放它所关联的上下文,所以代码应该在上下文完成后立刻调用cancel。
看不懂对吧,我也看不懂哈哈哈哈哈哈,我去查了一下其他资料
Context 也叫作“上下文”,一般理解为程序单元的一个运行状态、环境、快照等信息。其中上下是指存在上下层的传递,上会把内容传递给下,程序单元则指的是 Goroutine。context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
Done(): 返回一个 channel,可以表示 context 被取消的信号 。(PS:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(nil)后,就可以做一些收尾工作,尽快退出。)
知道原理后就可以使用啦
scss
复制代码
//该函数返回一个context和一个cancel对象,传入context.BackGround()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()//这是一个防御性编程,避免内存泄漏
go func() {
_, _ = io.Copy(dest, reader)//从reader向dest拷贝内容,连接没断开就会持续拷贝
cancel()//一旦连接断开就调用cancel()
}()
go func() {
_, _ = io.Copy(conn, dest)//从dest向reader拷贝内容,连接没断开就会持续拷贝
cancel()//一旦连接断开就调用cancel()
}()
<-ctx.Done()//如果从Done管道中取出了对象就结束,Done管道关闭的时候会读到nil值
到这里所有的工作就完成了,我们已经写了一个socks代理服务器
测试
尝试一下在浏览器中更换我们的代理服务器,
- 在谷歌浏览器中下载SwitchyOmega插件
- 新建一个情景模式,如下设置,再点击应用选项
接下来我们访问网页就会走我们的代理了,最后记得把代理改回来,改成系统代理。
总结
现在我们对整个过程进行一个梳理,下次就可以直接实现版本v4了
-
首先创建一个持续的监听,当接收到访问请求就创建一个Conn
-
使用一个协程来为该连接服务
-
先与客户进行认证
-
再解析客户想要访问目标服务器的报文,拿到地址和端口号后,与服务器建立连接
-
使用两个匿名协程,用于转发报文,使用WithCancle()来防止主线程跑路
作者:半妖
链接:juejin.cn/post/718927…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。