前情提要:
简介
什么是 Go 语言
高性能、高并发
语法简单、学习曲线平缓
丰富的标准库
完善的工具链
静态编译
快速编译
跨平台
垃圾回收
哪些公司在使用 Go 语言
那必是很多呀~(略)
字节跳动为什么全面拥抱 Go 语言
Python 性能不好
C++ 不适合在线 Web 业务
早期团队非 Java 背景
部署简单,学习成本低
内部 RPC 和 HTTP 框架的推广
入门
开发环境
略
基础语法
这块就是查缺补漏了,仅挑选我认为有必要再温习的部分
[16] 字符串格式化
首先,最常用的是 Println()
,它的作用是打印变量并换行
Go 中也是有 Printf()
的,与 C 中的相比更加智能,你可以用 %v
来自动判断类型,而无需纠结整型用 %d
,实行用 %f
之类的
使用 %+v
可以打印详细信息,而 %#v
则可以更加详细
保留位数的浮点数与 C 中的做法相同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" type point struct { x, y int } func main () { s := "hello" n := 123 p := point{1 , 2 } fmt.Println(s, n) fmt.Println(p) fmt.Printf("s=%v\n" , s) fmt.Printf("n=%v\n" , n) fmt.Printf("p=%v\n" , p) fmt.Printf("p=%+v\n" , p) fmt.Printf("p=%#v\n" , p) f := 3.141592653 fmt.Println(f) fmt.Printf("%.2f\n" , f) }
[17] JSON 处理
Go 中的 JSON 处理极为方便,对结构体使用 json.Marshal()
函数就能自动序列化(不要忘记首字母大写)
序列化后得到一个 byte
数组,如果要打印的话需要转换为 string
类型
正常序列化之后得到的都是首字母大写的,可以在结构体处为 JSON 格式加一个 tag
序列化的 JSON 可以使用 json.Unmarshal()
来解析到一个空的变量里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "encoding/json" "fmt" ) type userInfo struct { Name string Age int `json:"age"` Hobby []string } func main () { a := userInfo{Name: "wang" , Age: 18 , Hobby: []string {"Golang" , "TypeScript" }} buf, err := json.Marshal(a) if err != nil { panic (err) } fmt.Println(buf) fmt.Println(string (buf)) buf, err = json.MarshalIndent(a, "" , "\t" ) if err != nil { panic (err) } fmt.Println(string (buf)) var b userInfo err = json.Unmarshal(buf, &b) if err != nil { panic (err) } fmt.Printf("%#v\n" , b) }
[18] 时间处理
使用 time.Now()
可以快速获取当前时间
也可以用 time.Date()
去构造一个带时区的时间,有很多方法可以使用获取年月日时分秒
可以使用 Sub()
方法得到时间段,时间段又可以使用方法去得到它有多少小时,多少分钟,多少秒
使用 Format()
方法去格式化时间,不像其他编程语言使用诸如 yyyy-M-d
之类的标识符,Go 是以一个特殊的时刻(2006 年 1 月 2 日 下午 3 点 4 分 5 秒,这个时刻是写在包里面的)为例子,来理解你想表达时间的格式
同样地,使用 Parse()
并提供这个时刻的示例,可以解析你写成字符串的时间
你可以使用 now.Unix()
来获取当前的 Unix 时间戳
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport ( "fmt" "time" ) func main () { now := time.Now() fmt.Println(now) t := time.Date(2022 , 3 , 27 , 1 , 25 , 36 , 0 , time.UTC) t2 := time.Date(2022 , 3 , 27 , 2 , 30 , 36 , 0 , time.UTC) fmt.Println(t) fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) fmt.Println(t.Format("2006-01-02 15:04:05" )) diff := t2.Sub(t) fmt.Println(diff) fmt.Println(diff.Minutes(), diff.Seconds()) t3, err := time.Parse("2006-01-02 15:04:05" , "2022-03-27 01:25:36" ) if err != nil { panic (err) } fmt.Println(t3 == t) fmt.Println(now.Unix()) }
[19] 数字解析
使用 strconv
包中的 ParseFloat()
和 ParseInt()
来快速解析字符串
如果确定是 10 进制,也可以使用 Atoi()
来快速转换成数字
同样的,可以使用 Itoa()
来把数字转换为字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "strconv" ) func main () { f, _ := strconv.ParseFloat("1.234" , 64 ) fmt.Println(f) n, _ := strconv.ParseInt("111" , 10 , 64 ) fmt.Println(n) n, _ = strconv.ParseInt("0x1000" , 0 , 64 ) fmt.Println(n) n2, _ := strconv.Atoi("123" ) fmt.Println(n2) n2, err := strconv.Atoi("AAA" ) fmt.Println(n2, err) }
[20] 进程信息
使用 os.Args
来获取执行时获取的一些命令行参数
使用 os.Getenv()
与 os.Setenv()
来获取与写入环境变量
使用 exec.Command()
来启动子进程并获取其输入输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "os" "os/exec" ) func main () { fmt.Println(os.Args) fmt.Println(os.Getenv("PATH" )) fmt.Println(os.Setenv("AA" , "BB" )) buf, err := exec.Command("grep" , "127.0.0.1" , "/etc/hosts" ).CombinedOutput() if err != nil { panic (err) } fmt.Println(string (buf)) }
实战
猜谜游戏
实现一个简单的二分猜数游戏
V1
先测试随机数生成
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" "math/rand" ) func main () { maxNum := 100 secretNumber := rand.Intn(maxNum) fmt.Println("The secret number is " , secretNumber) }
但是发现,每次生成的数都是一样的
V2
原来是没有设置种子,设置完种子之后就正常了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "math/rand" "time" ) func main () { maxNum := 100 rand.Seed(time.Now().UnixNano()) secretNumber := rand.Intn(maxNum) fmt.Println("The secret number is " , secretNumber) }
V3
接下来实现用户的输入输出,并且解析成数字
这就需要借助 os.Stdin
文件,但是直接操作很不方便,可以使用 bufio.NewReader()
转换为一个只读的流
使用 ReadString("\n")
方法来读取一行
但是读入的时候会多读入一个换行符,所以要使用 strings.TrimSuffix
来去掉换行符
最后,使用 strconv.Atoi
来转换为数字
这里用这么复杂的方式来得到输入,其实是因为后面的项目中要用到 bufio
这个包,需要提前熟悉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "bufio" "fmt" "math/rand" "os" "strconv" "strings" "time" ) func main () { maxNum := 100 rand.Seed(time.Now().UnixNano()) secretNumber := rand.Intn(maxNum) fmt.Println("The secret number is " , secretNumber) fmt.Println("Please input your guess" ) reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n' ) if err != nil { fmt.Println("An error occurred while reading input. Please try again" , err) return } input = strings.TrimSuffix(input, "\n" ) input = strings.TrimSuffix(input, "\r" ) guess, err := strconv.Atoi(input) if err != nil { fmt.Println("Invalid input. Please enter an integer value" ) return } fmt.Println("You guess is" , guess) }
V4
这个版本的改进是实现逻辑判断,下末尾添加下面的行
1 2 3 4 5 6 7 if guess > secretNumber { fmt.Println("Your guess is bigger than the secret number. Please try again" ) } else if guess < secretNumber { fmt.Println("Your guess is smaller than the secret number. Please try again" ) } else { fmt.Println("Correct, you Legend!" ) }
V5
此时程序大致已经完成了,但是还缺少循环,用户输入完成一次后程序就会退出
现在加上循环,这个游戏就完整了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "bufio" "fmt" "math/rand" "os" "strconv" "strings" "time" ) func main () { maxNum := 100 rand.Seed(time.Now().UnixNano()) secretNumber := rand.Intn(maxNum) fmt.Println("Please input your guess" ) reader := bufio.NewReader(os.Stdin) for { input, err := reader.ReadString('\n' ) if err != nil { fmt.Println("An error occured while reading input. Please try again" , err) continue } input = strings.TrimSuffix(input, "\n" ) input = strings.TrimSuffix(input, "\r" ) guess, err := strconv.Atoi(input) if err != nil { fmt.Println("Invalid input. Please enter an integer value" ) continue } fmt.Println("You guess is" , guess) if guess > secretNumber { fmt.Println("Your guess is bigger than the secret number. Please try again" ) } else if guess < secretNumber { fmt.Println("Your guess is smaller than the secret number. Please try again" ) } else { fmt.Println("Correct, you Legend!" ) break } } }
在线词典
在这个项目中,需要实现一个命令行版本的词典,输入一个单词,调用第三方 api 查询它的音标与翻译
抓包
这里以彩云科技提供的在线翻译为例(https://fanyi.caiyunapp.com )
打开开发者工具,随便输入一个单词,找到它的请求与响应
下面,我们就需要在 Go 中模拟这个请求
代码生成
首先复制为 cURL
然后打开 https://curlconverter.com/#go ,转换为 Go 代码
解读生成代码
让我们来看一看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "fmt" "io/ioutil" "log" "net/http" "strings" ) func main () { client := &http.Client{} var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}` ) req, err := http.NewRequest("POST" , "https://api.interpreter.caiyunai.com/v1/dict" , data) if err != nil { log.Fatal(err) } req.Header.Set("Accept" , "application/json, text/plain, */*" ) req.Header.Set("Accept-Language" , "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6" ) req.Header.Set("Connection" , "keep-alive" ) req.Header.Set("Content-Type" , "application/json;charset=UTF-8" ) req.Header.Set("DNT" , "1" ) req.Header.Set("Origin" , "https://fanyi.caiyunapp.com" ) req.Header.Set("Referer" , "https://fanyi.caiyunapp.com/" ) req.Header.Set("Sec-Fetch-Dest" , "empty" ) req.Header.Set("Sec-Fetch-Mode" , "cors" ) req.Header.Set("Sec-Fetch-Site" , "cross-site" ) req.Header.Set("User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36 Edg/101.0.1210.39" ) req.Header.Set("X-Authorization" , "token:qgemv4jr1y38jyq6vhvi" ) req.Header.Set("app-name" , "xy" ) req.Header.Set("os-type" , "web" ) req.Header.Set("sec-ch-ua" , `" Not A;Brand";v="99", "Chromium";v="101", "Microsoft Edge";v="101"` ) req.Header.Set("sec-ch-ua-mobile" , "?0" ) req.Header.Set("sec-ch-ua-platform" , `"Windows"` ) resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() bodyText, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatal(err) } fmt.Printf("%s\n" , bodyText) }
可以看到能成功输出一大串的 JSON
生成 request body
但是我们的程序是需要根据输入的不同来查询不同的单词,所以我们需要能自动生成出类似的请求
先手动构造一个结构体
1 2 3 4 type DictRequest struct { TransType string `json:"trans_type"` Source string `json:"source"` }
然后 data 改为通过从结构体来获取
1 2 3 4 5 6 request := DictRequest{TransType: "en2zh" , Source: "good" } buf, err := json.Marshal(request) if err != nil { log.Fatal(err) } var data = bytes.NewReader(buf)
可以看见一样可以正常响应请求
解析 response body
已经解决的发送请求的问题了,现在需要解析那个巨大的 response
但是返回的 JSON 非常复杂,如果要手写结构体一一绑定的话不知道要写多久,所以又需要借助于在线工具
打开 https://oktools.net/json2go ,把响应的字符串复制过去,再点击『转换-嵌套』,就能得到对应的结构体
把这个巨大的结构体复制到代码中,然后更名为 DictResponse
使用 json.Unmarshal
把响应体解析到结构体中,尝试打印一下
1 2 3 4 5 6 var dictResponse DictResponseerr = json.Unmarshal(bodyText, &dictResponse) if err != nil { log.Fatal(err) } fmt.Printf("%#v\n" , dictResponse)
可以看见,没毛病,十分完美
打印结果
那最后一步自然是从结构体中挑出我们需要的那部分打印出来就行了
1 2 3 4 fmt.Println(word, "UK:" , dictResponse.Dictionary.Prons.En, "US:" , dictResponse.Dictionary.Prons.EnUs) for _, item := range dictResponse.Dictionary.Explanations { fmt.Println(item) }
还可以加上一个状态码检查
1 2 3 if resp.StatusCode != 200 { log.Fatal("bad StatusCode:" , resp.StatusCode, "body" , string (bodyText)) }
最后包装成函数,得到成品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package mainimport ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "os" ) type DictRequest struct { TransType string `json:"trans_type"` Source string `json:"source"` UserID string `json:"user_id"` } 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 query (word string ) { client := &http.Client{} request := DictRequest{TransType: "en2zh" , Source: word} buf, err := json.Marshal(request) if err != nil { log.Fatal(err) } var data = bytes.NewReader(buf) req, err := http.NewRequest("POST" , "https://api.interpreter.caiyunai.com/v1/dict" , data) if err != nil { log.Fatal(err) } 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" ) resp, err := client.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close() bodyText, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatal(err) } if resp.StatusCode != 200 { log.Fatal("bad StatusCode:" , resp.StatusCode, "body" , string (bodyText)) } var dictResponse DictResponse err = json.Unmarshal(bodyText, &dictResponse) if err != nil { log.Fatal(err) } fmt.Println(word, "UK:" , dictResponse.Dictionary.Prons.En, "US:" , dictResponse.Dictionary.Prons.EnUs) for _, item := range dictResponse.Dictionary.Explanations { fmt.Println(item) } } func main () { if len (os.Args) != 2 { fmt.Fprintf(os.Stderr, `usage: simpleDict WORD example: simpleDict hello ` ) os.Exit(1 ) } word := os.Args[1 ] query(word) }
不得不说,看上去挺好用的
SOCKS5 代理
这一节的内容是手动写一个 SOCKS5 代理,代码量比较大
原视频讲得很详细,代码的每个部分都有解析
介绍
(略)
原理
如图,要先协商鉴权,然后转发数据
TCP echo server
先从简单的开始,写一个原路转发所有 TCP 请求的服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "bufio" "log" "net" ) 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) for { b, err := reader.ReadByte() if err != nil { break } _, err = conn.Write([]byte {b}) if err != nil { break } } }
这东西在 Windows 上需要管理员,我是 build 了之后手动以管理员权限运行的
跑起来之后,我们使用 nc
命令来测试是否正常运行
这个命令 Linux 是自带的,Windows 可以去网上下载
可以看见成功返回了数据
认证阶段
接下来开始实现协议的第一步:认证阶段,这里就开始变得复杂了
总体思想就是在缓冲区读取发送过来的 VER
、NMETHODS
和 METHODS
三个字段,鉴别好是这个协议之后,返回 VER
和 METHOD
这两个字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package mainimport ( "bufio" "fmt" "io" "log" "net" ) const socks5Ver = 0x05 const cmdBind = 0x01 const atypIPV4 = 0x01 const atypeHOST = 0x03 const 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 } log.Println("auth success" ) } func auth (reader *bufio.Reader, conn net.Conn) (err error ) { 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 }
使用 curl
命令测试一下,连接肯定是不成功的(毕竟只写了第一步),但已经成功打印出了 VER
和 METHOD
两个字段,这说明我们当前的实现正确
请求阶段
现在代码开始很长了,实现一个 connect
函数,用来代替客户端连接目标服务器(获取用户的需求,然后返回成功)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 package mainimport ( "bufio" "encoding/binary" "errors" "fmt" "io" "log" "net" ) const socks5Ver = 0x05 const cmdBind = 0x01 const atypIPV4 = 0x01 const atypeHOST = 0x03 const 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, 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) } _, 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 ) { 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 := "" switch atyp { case atypIPV4: _, 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 ]) if err != nil { return fmt.Errorf("read port failed:%w" , err) } port := binary.BigEndian.Uint16(buf[:2 ]) log.Println("dial" , addr, 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) } return nil }
到这一步必定还是连接失败的,但我们已经能获得到客户想访问的目标服务器
响应阶段
现在是最后一步:真正地和目标服务器建立连接,并转发数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 package mainimport ( "bufio" "context" "encoding/binary" "errors" "fmt" "io" "log" "net" ) const socks5Ver = 0x05 const cmdBind = 0x01 const atypIPV4 = 0x01 const atypeHOST = 0x03 const 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, 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) } _, 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 ) { 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 := "" switch atyp { case atypIPV4: _, 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 ]) 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) _, 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() go func () { _, _ = io.Copy(dest, reader) cancel() }() go func () { _, _ = io.Copy(conn, dest) cancel() }() <-ctx.Done() return nil }
可以看见已经成功完成了转发