这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记
前言
本文主要是记载青训营第一节课在教了go语言基础语法后
老师从易到难,由浅入深的分享了的三个小项目:
- 猜数字
- 在线词典
- socks5代理服务器
因为老师留了课后作业,所以一并把解题思路记录下来
省略了所有的异常处理和一些不太重要的逻辑
猜数字
描述
生成随机数,用户输入随机数,判断输入是否有效
如果是数字,判断与生成数大小,猜中则退出程序,否则继续交互
作业要求:使用Scanf简化用户输入读取
知识点
-
随机数的生成
- rand.Intn()
- 随机种子的设置
- 计算机伪随机数
-
流程控制语句(if、for)
-
输入输出
- fmt.Scanf、fmt.Println
- bufio.NewReader(os.Stdin).reader.ReadString()
核心代码
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
// reader := bufio.NewReader(os.Stdin)
// 输出 略
var guess int
for {
// 另一种输入写法,使用bufio,要先NewReader
// input, _ := reader.ReadString('\n')
// input = strings.TrimSuffix(input, "\r\n")
_, err := fmt.Scanf("%d ", &guess)
if err != nil {
// 输出 略
continue
}
// 略
if guess > secretNumber { // 输出 略
} else if guess < secretNumber { // 输出 略
} else { // 输出 略
break
}
}
执行效果
在线词典
描述
读取用户指令输入的单词,调用第三方翻译api,爬取翻译的关键信息输出
作业要求:
1. 使用两种搜素引擎实现结果
2. 使用并发处理两种搜索引擎的请求过程,提高程序性能
知识点
-
启动参数的读取
- os.Args获取启动参数
-
http请求的发送和响应的获取与处理
- net/http包
-
json包的配置
- Marshal 和 Unmarshal
-
如何模拟浏览器上的一个http网络请求?
- 打开开发者工具,点击NetWork栏
- 在Name栏中找到目标的请求(需要自主筛选,有些请求需要判断)
- 右键 Copy As cURL(window需要选择bash格式)
- 使用脚本转Go代码的工具,自动生成代码
- 截取返回的json字符串,使用json转go结构体的工具,生成结构体
- 在代码中编写json解码相关内容,同时选择需要的属性进行处理即可
-
并发编程入门 goroutine + sync.WaitGroup 实现并发调用
- 使用 goroutine 协程
- 使用 sync.WaitGroup 阻塞等待
核心代码
// 第一种搜索引擎
// 利用返回的json生成,使用工具根据 json 转 go struct
type HuoShanDictRequest struct {}
type HuoShanDictResponse struct {}
// step0: 代码生成器生成请求模拟函数,封装成函数
func HuoShanQuery(word string) {
// step1: 生成http客户端
client := &http.Client{}
// step2: 使用结构体封装请求
request := HuoShanDictRequest{Language: "en", Text: word}
buf, _ := json.Marshal(request)
// step3: 初始化请求(自动生成)
data := bytes.NewReader(buf)
req, _ := http.NewRequest("POST", url, data)
// step4 :设置请求头(自动生成略)
req.Header.Set("Connection", "keep-alive")
// ...
// step5: 执行请求,获取相应(自动生成略)
resp, _ := client.Do(req)
// step6: 读出响应体(自动生成略)
bodyText, _ := ioutil.ReadAll(resp.Body)
// step7 : 异常分支,校验状态码
if resp.StatusCode != 200 {
log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText))
}
// step8 : 结构体封装响应
var huoShanDictResponse HuoShanDictResponse
_ = json.Unmarshal(bodyText, &huoShanDictResponse)
// step9: 输出略
}
// 第二种搜索引擎,实现过程参照上一种即可
type CaiYunDictRequest struct {}
type CaiYunDictResponse struct {}
func CaiYunQuery(word string) {}
func main() {
if len(os.Args) != 2 {
// 获取启动参数
fmt.Fprintf(os.Stderr, `usage: simpleDict WORD
example: simpleDict hello
`)
os.Exit(1)
}
word := os.Args[1]
// 用于阻塞等待的结构,Add添加任务数,Done减少任务数,查看源码发现Done的本质是Add(-1)
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
HuoShanQuery(word)
wg.Done()
}()
go func() {
CaiYunQuery(word)
wg.Done()
}()
// 等待任务数清零结束
wg.Wait()
}
执行结果
SOCKS5代理
描述
实现一个简易不需要认证的socks5代理服务器
知识点
soks5协议的工作原理
正常浏览器不经过代理访问网站,会想和对方服务器建立TCP连接,执行三次握手建立HTTP请求,然后交互
设置代理之后,流程变为用户先和代理服务器建立连接,然后代理服务器再和实际服务器建立连接,然后用户和代理进行交互,代理和实际服务器交互,代理再负责转发用户和实际服务器的数据。
可以分成四个阶段:握手阶段、认证阶段、请求阶段、relay阶段。
第一阶段 - 握手
浏览器会向socks5代理发送请求,包的内容包括一个协议的版本号,支持的认证种类,socks5服务器会选中一个认证方式,返回给浏览器。
第二阶段 - 认证
如果socks5服务器返回的是00的话就代表不需要认证,返回其他类型的话会开始认证流程。
本次简单实现一个不需要认证的代理,就不对如认证赘述了。
第三阶段 - 请求
认证过后浏览器会和对代理发送请求,主要信息包扣版本号,请求类型,主机,ip地址端口号等等。
这代表着如果代理截取到这些数据,就可以确定实际请求服务器,代理就可以和实际服务器建立TCP连接,返回响应。
第四个阶段 - relay
此时浏览器会发送一些请求获取数据,代理收到请求会转发到实际的服务器上,实际服务器返回响应也会发送到代理上,由代理转发到浏览器。
核心代码
package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)
// 常量,
const (
socks5Ver = 0x05
cmdBind = 0x01
atypIPV4 = 0x01
atypeHOST = 0x03
atypeIPV6 = 0x04
)
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1080")
// 异常处理略
for {
client, err := server.Accept()
// 异常处理略
go process(client)
}
}
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn)
// 异常处理略
err = connect(reader, conn)
// 异常处理略
}
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, _ := reader.ReadByte()
// 异常处理略
methodSize, _ := reader.ReadByte()
method := make([]byte, methodSize)
_, _ = io.ReadFull(reader, method)
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, _ = conn.Write([]byte{socks5Ver, 0x00})
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)
_, _ = io.ReadFull(reader, buf)
ver, cmd, atyp := buf[0], buf[1], buf[3]
// 异常处理,略
addr := ""
switch atyp {
case atypIPV4:
_, _ = io.ReadFull(reader, buf)
addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
case atypeHOST:
hostSize, _ := reader.ReadByte()
host := make([]byte, hostSize)
_, _ = io.ReadFull(reader, host)
addr = string(host)
default:
return errors.New("invalid atyp or no supported yet")
}
_, _ = io.ReadFull(reader, buf[:2])
port := binary.BigEndian.Uint16(buf[:2])
dest, _ := net.Dial("tcp", fmt.Sprintf("%v:%v", addr, port))
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
_, _ = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 从用户读数据到host
go func() {
_, _ = io.Copy(dest, reader)
cancel()
}()
// 从host读数据到用户
go func() {
_, _ = io.Copy(conn, dest)
cancel()
}()
// 阻塞等待上述两个协程任意一个触发cancel,结束程序
<-ctx.Done()
return nil
}
小结
三个小实验,知识面覆盖到了golang基础语法,io处理,http请求,json解析,并发编程等多个知识面。
对于刚接触go语言的人来说,有不小的挑战,也非常的有意义,希望能给看到的人带来一定的帮助。
同时,在整理的代码中,省略了一些不必要的流程,还需要读者自己具体实验。