三个课内小项目| 青训营

67 阅读5分钟

把错误处理全拿掉,看的方便点 然后主要是感觉整个go中err处理太多了,而且全都是用if写的,导致看起来很繁杂 最后是感觉这些库还是得看代码引用的提示和文档, 主要课内讲的都是流程。

猜数字

很简单的随机生成和输入操作

bufio库的使用

 reader := bufio.NewReader(os.stdin)

通过对底层os库的封装实现的数据流运行

input,  err := reader.ReadString('\n')

调用方法, 以某种形式读取数据

基础标准库

strconv

主要是字符和其他类型的转化

strconv.Atoi(s string) int 

转换 string to int

strings

字符操作

strings.TrimSuffix(s string ,  suffix string) string

删除末尾字符, 如果没有就正常返回

简单字典

网络库和 json 转化

go 中需要有一个结构体接受 json 的转化, 所以 struct 需要提前准备, 课中使用了两个工具

curlconverter 根据网站的原生请求生成对应的 go 语言请求

oktools 根据 json 自动生成 struct

结构体可以用 `` 标注, 表面转化为 json 以后的名字 比如

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

}

这样转化后就是对应的标注

网络库 net/http

go 标准库

	client := &http.Client{}

初始化一个请求客户端

	req,  err := http.NewRequest("POST",  "https://api.interpreter.caiyunai.com/v1/dict",  data)

用来创建一个请求, 显而易见的结构

请求形式, url , 请求体 清晰可见

	req.Header.Set("authority",  "api.interpreter.caiyunai.com")

设置请求头, 这里是用网站生成的

	resp,  err := client.Do(req)

进行请求

	bodyText,  err := io.ReadAll(resp.Body)
	err = json.Unmarshal(bodyText,  &dictResponse)

获取 json , 反序列化

os库

被这个项目用来获取环境参数

	word := os.Args[1]

第一个是执行文件或者代码

接下去的就是携带参数

socks5代理

主要是对于 net 库的接受信息的解析, 如何取出其中的值, 并判断是否符合要求, 然后是连接, 用 context 库进行进程管理

go process(client) 用 goroutine 后台开启一个服务器交互进程

  1. 接受请求并鉴权 —— auth 函数
  2. 链接需求网址并通讯 —— connect 函数

鉴权

首先要进行的就是 auth 也就是鉴权, 在这个阶段我们要做 2 件事情

  1. 判断协议是否被支持
  2. 判断能否通过认证

下面是在鉴权阶段我们拿到的东西

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

了解通信过程, 整体就是在判断err, 然后回包告诉客户端认证成功了。

取出

	ver,  err := reader.ReadByte()
	methodSize,  err := reader.ReadByte()
	method := make([]byte,  methodSize)
	_,  err = io.ReadFull(reader,  method)

手动回包

	// +----+--------+
	// |VER | METHOD |
	// +----+--------+
	// | 1  |   1    |
	// +----+--------+
	_,  err = conn.Write([]byte{socks5Ver,  0x00})

通讯

connect阶段也就两个主要模块

  1. 判断是否这个请求服务的能接受, 判断是什么类型的目标地址并解析
  2. 通信

下面是通讯阶段拿到的东西, 和鉴权中不一样的主要是多了目标地址

	+----+-----+-------+------+----------+----------+
	|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个字节

--这个是课内部分, 但是这个域名形式的会先返回一个hostsize然后才是host--

鉴权

	buf := make([]byte,  4)
	_,  err = io.ReadFull(reader,  buf)
	ver,  cmd,  atyp := buf[0],  buf[1],  buf[3]

然后做个解析

addr := ""
	switch atyp {
	case atypeIPV4:
		_,  err = io.ReadFull(reader,  buf)
		addr = fmt.Sprintf("%d.%d.%d.%d",  buf[0],  buf[1],  buf[2],  buf[3])
	case atypeHOST:
		hostSize,  err := reader.ReadByte() //这里是host和v4地址 结构不一样的地方
		host := make([]byte,  hostSize)
		_,  err = io.ReadFull(reader,  host)
		addr = string(host)

取端口号, 这里是用了切片和数组的对应关系

	_,  err = io.ReadFull(reader,  buf[:2])
	port := binary.BigEndian.Uint16(buf[:2])

第二步, 该给目标地址发送连接请求了

	dest,  err := net.Dial("tcp",  fmt.Sprintf("%v:%v",  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})

然后将客户端和目标做互相拷贝, 这里开启两个进程, 同时用context库进行进程交互

	ctx,  cancel := context.WithCancel(context.Background())
	defer cancel()

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

	<-ctx.Done()

当有一边读取不到东西阻塞后, 用 cancel() 将两个进程都关闭

用信道保持代码不继续运行, 被关闭之后继续往下走

net库

开在指定端口开一个服务器, 并能够监听收集数据

	server,  err := net.Listen("tcp",  "127.0.0.1:1080")

开服务器, 指定协议和端口

		client,  err := server.Accept()

拿到关键 client 这是个 conn 网络连接, 读数据的

	reader := bufio.NewReader(conn)

数据取出, 开始操作

	_,  err = conn.Write([]byte{socks5Ver,  0x00})

给客户端回请求, 这里一般就是用 0x00 表示成功

context库

ctx,  cancel := context.WithCancel(context.Background())

通过默认的 Background 创建一个父节点, 并衍生一个 Cancel context

在关闭父节点就会直接关闭所有子节点

通过传递和使用 ctx 和 cancel , 就可以对不同 goruntime 进行控制

ctx.Done()

表示是否 ctx 被关闭, 关闭后返回一个被关闭的 channel