1.golang的优点
- 天生支持高并发,适合电商平台等网页后端的开发
- 功能上有内存安全、GC(垃圾回收)、结构形态以及CSP-style并发计算
- 内存runtime,支持垃圾回收
- 可以直接编译为机器码,而不依赖其他库
丰富的标准库
- 可以跨平台编译
2.go语言结构
以下用hello_world例程来解释
//程序的第一部分,这一行代码定义了包名,必须在源文件中非注释的第一行指明这个文件属于哪个包
//package main表示一个可独立执行的程序,每个Go应用都包含一个名为main的包
package main
//告诉go编译器,将来这个go应用会引用到哪个包,fmt包实现了格式化I/O的各种API
import "fmt"
//是程序开始执行的函数,main函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数
//注意,如果有init函数,那么该应用会先执行init函数
func main() {
fmt.Print("Hello world!")
}
//需要注意的是 { 不能单独放在一行
当标识符(包括常量
、变量
、类型
、函数名
、结构字段
等等)以一个大写字母开头
,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用
(客户端程序需要先导入这个包),这被称为导出
(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的
,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的protected
)。
3.go基本语法
3.1 关于标识符命名
第一个字符必须是字母或下划线而不能是数字。
3.2 字符串命名
Go的字符串可以通过+
来连接
3.3 go语言空格
//Go 语言中变量的声明必须使用空格隔开
var age int
3.4 格式化字符串
package main
import (
"fmt"
)
func main() {
var stockCode = 123
fmt.Println(fmt.Sprintf("%d", stockCode))
}
4. golang语言变量类型
4.1 数字类型
整形int和浮点型float32和float64,Go语言支持整形和浮点型数字,并且支持复数,其中位的运算采用补码
4.2字符串类型
字符串就是一串固定的字符连接起来的字符序列,Go的字符串是由单个字节连接起来的,Go语言的字符串的字节使用UTF-8
编码标识UNICODE
文本
4.3派生类型
(a)指针
类型(pointer)
(b)数组类型
©结构化类型(struct)
(d)Channel类型
(e)函数类型
(f)切片类型
(g)接口类型(interface)
(h)map类型
5.变量声明类型
5.1 第一种,指定变量类型,如果没有初始化,则变量默认为零值
其中,零值所对应的值为:
数值类型(包括complex64/128)为 0
布尔类型为 false
字符串为 “”(空字符串)
以下几种类型为 nil:
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口
第二种,根据赋值类型自行判断变量类型
var a = 123//自动判断为整形123
第三种,使用 := 赋值符对已经赋值的变量再次赋值则编译会报错
s := "string"
fmt.Print(fmt.Sprintf("%v", s))
s := 123//编译报错
第四种,多变量声明
//类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断
vname1, vname2, vname3 := v1, v2, v3 // 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误
// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
关于值类型和引用类型
(1)所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
:
(2)当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝
:
(3)与C++类似的,golang也支持通过&运算符来计算变量的内存地址,值类型的变量存储在堆
中
6.语言常量
常量的定义格式
const arrLength = 155//隐式
const arrLength int = 155//显式
可以用函数/方法来计算常量的值,如
const (
arrLength = len("abc")
brrLength = unsafe.Sizeof("abs")
)
要注意的是用来计算常量的值必须是在标准库中已经存在的了
iota
iota被认为是一个可以被编译器修改的常量,iota
中每新增一行常量将使iota计数一次, 可理解为const语句块中的行索引
const (
a = iota
b = iota
c = iota
)
fmt.Println(a, b, c)
const (
d = iota
e
f
)
fmt.Println(d, e, f)
输出:
0 1 2
0 1 2
7.函数定义
7.1函数定义的基本形式
func function_name([parameterlist])[return types]{
//do something
}
7.2函数传参的类型
值传递
:值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,也不会影响到上一层函数中的变量引用传递
:引用传递是指在调用函数时将实际参数的地址传递到函数中去,那么在函数中对参数所进行的修改,将会影响到实际的参数
默认情况下,go语言所使用的是值传递,也就是在调用过程中不会影响到实际参数
7.3函数用法介绍
函数作为另一个函数的实参
(lambda):函数定义后可以作为另一个函数的实参传入闭包
:闭包是匿名函数,可以在动态编程中使用方法
:方法就是一个包含了接受者的函数
8.数组
var variable_name [SIZE] variable_type//定义的基本格式
var balance[10] float32//定义的例子
balance1 := [5]float32{1000.0, 1.0, 2.0, 3.0, 4.0} //使用赋值运算符来进行快速初始化
var balance2 = [5]float32{1.0, 2.0, 3.0, 4.0, 5.0} //使用var运算符来进行初始化
fmt.Println(balance1)
fmt.Println(balance2)
//如果数组的长度不确定,[]内可以改成...
//如果只是需要初始化指定索引,写法为
balance3 := [...]float32{1: 3.0, 2: 5.0}
fmt.Println("balance3", balance3)
9.切片(重要)
9.1 切片简介
切片
:切片是对数组的抽象,go的数组长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片,(“动态数组”),与数组相比,切片的长度不是固定的,可以追加
元素,在追加时可能使切片的容量增大
9.2 切片的定义
var identifier []float32 = make([]float32, 5,6)
//其中[]float32是创建的切片类型,5是创建的切片长度,6是创建的切片容量
9.3 切片的初始化
s := [] int{1,2,3}
[]
表示s是一个切片类型的变量,{1,2,3}初始化值为{1,2,3}
,其中容量 = 长度 = 3
s := arr[:]
arr := [5]float64{1.0, 2.0, 3.0}
s1 := arr[:] //切片s1代表着对arr的引用
s2 := s1[0:len(s1)]//从startIndex访问到endIndex-1
9.4 append与copy函数
如果想要增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝
过来
s1 := make([]float32, 5)
s1 = append(s1, 1, 2, 3, 4, 5)
fmt.Println(s1)
s2 := make([]float32, 2*len(s1))
copy(s2, s1)
s2 = append(s2, 0, 1, 0, 1)
fmt.Println(s2)
首先我们需要知道,为什么需要容量这个东西,容量首先是有关于切片间的数据共享的,在容量允许的情况下,切片中的元素在底层中都是存在连续的内存空间的,如果底层数组没有足够的容量,这时候append会再创建一个新的底层数组。
10.范围Range
range 关键字用于 for 循环中迭代数组(array)
、切片(slice)
、通道(channel)
或集合(map)
的元素
for key, value := range oldMap {
newMap[key] = value
}//遍历map
for key,_ := range nums{
fmt.print(key)
}//遍历普通数组,只取key
for i,v := range nums{
fmt.print("i:",i,"v:"v)
}
11.接口
定义接口的方法
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct {
}
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
12.go并发
12.1 goroutine
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:
package main
//例如
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
12.2 channel
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
创建通道的实例
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
12.3 通道缓冲区
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态
,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
12.4 Go 遍历通道与关闭通道
v, ok := <-ch
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Println(i)
}
}
13.错误处理
package main
import (
"errors"
"fmt"
)
type user struct { //定义结构体
userName string
password string
}
//定义结构体方法
func (u user) getUserName() string {
return u.userName
}
//定义结构体方法,带指针,能够直接修改到内存
func (u *user) setUserPassword(password string) bool {
u.password = password
return u.password == password
}
func findUser(users []user, userName string) (u *user, err error) {
for _, u := range users {
if u.userName == userName {
return &u, nil
}
}
return nil, errors.New("404")
}
func main() {
var users []user
name := "Tom"
findUser(users, name)
_, err := findUser(users, name)
if err != nil {
fmt.Println("not found user", name)
fmt.Println(err)
return
} else {
fmt.Println(name)
}
}
14. JSON操作
将结构体序列化
package main
import (
"encoding/json"
"fmt"
)
type user struct { //定义结构体,并且使得其能够被JSON序列化
//变量名首字母要大写
UserName string
PassWord string
Age int `json:"age"`
Hobby []string
}
func main() {
a := user{UserName: "Tom", PassWord: "123456", Age: 18, Hobby: []string{"basketball", "music"}}
buf, err := json.Marshal(a) //对a变量进行序列化
if err != nil {
panic(err)
} else {
//fmt.Println(buf),直接打印会打印出16进制的编码
fmt.Println(string(buf))
}
buf, err = json.MarshalIndent(a, "", " \t") //多做了一些格式化的处理
if err != nil {
panic(err)
} else {
fmt.Println(string(buf))
}
var b user
err = json.Unmarshal(buf, &b) //将解析之后的byte数组注入回原数组类型中
if err != nil {
panic(err)
} else {
fmt.Println(b)
}
}
青训营作业-1 猜谜游戏
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
/*Description:
生成随机数+猜数demo.go
*/
func main() {
maxNum := 100 //随机数的最大值
rand.Seed(time.Now().UnixNano()) //利用时间戳来生成随机数种子
secretNumber := rand.Intn(maxNum) //生成随机数,在100以内,注意,在生成随机数之前需要设置随机数种子,否则产生的随机数一直都是同一个
fmt.Println("请输入你要猜的数字:")
reader := bufio.NewReader(os.Stdin)//这里可以将OS的标准输入流缓冲看做为文件
input, err := reader.ReadString('\r') //以"\n为界限读取这一行",我的机子上跑'\n'是有问题的
if err != nil {
fmt.Println("产生了错误", err)
return
}
input = strings.TrimSuffix(input, "\r") //删除字符串的后缀
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("产生了错误", err)
return
}
if guess > secretNumber {
fmt.Println("你猜的数字太大了")
} else if guess < secretNumber {
fmt.Println("你猜的数字太小了")
} else if guess == secretNumber {
fmt.Println("你猜中了!数字是", secretNumber)
}
return
}
//改用for ; ; 来写比较合适
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
/*Description:
生成随机数+猜数demo.go
*/
func main() {
maxNum := 100 //随机数的最大值
rand.Seed(time.Now().UnixNano()) //利用时间戳来生成随机数种子
secretNumber := rand.Intn(maxNum) //1.生成随机数,在100以内,注意,在生成随机数之前需要设置随机数种子,否则产生的随机数一直都是同一个
for {
fmt.Println("请输入你要猜的数字:")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\r') //以"\n为界限读取这一行"
if err != nil {
fmt.Println("产生了错误", err)
continue
}
input = strings.TrimSuffix(input, "\r") //删除字符串的后缀
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("产生了错误", err)
continue
}
if guess > secretNumber {
fmt.Println("你猜的数字太大了")
} else if guess < secretNumber {
fmt.Println("你猜的数字太小了")
} else if guess == secretNumber {
fmt.Println("你猜中了!数字是", secretNumber)
break
}
}
}
上述代码的问题在于,读取用户输入的数据太麻烦了,可以改用fmt.scanfln)
api来简化代码
package main
import (
"fmt"
"math/rand"
"time"
)
/*Description:
生成随机数+猜数demo.go
*/
func main() {
maxNum := 100 //随机数的最大值
rand.Seed(time.Now().UnixNano()) //利用时间戳来生成随机数种子
secretNumber := rand.Intn(maxNum) //1.生成随机数,在100以内,注意,在生成随机数之前需要设置随机数种子,否则产生的随机数一直都是同一个
for {
var guess int
var c string
fmt.Println("请输入你要猜的数字:")
_, err := fmt.Scanf("%d", &guess)
fmt.Scanf("%s", &c)
if err != nil {
fmt.Println("产生了某种错误!", err)
continue
}
if guess > secretNumber {
fmt.Println("你猜的数字太大了")
} else if guess < secretNumber {
fmt.Println("你猜的数字太小了")
} else if guess == secretNumber {
fmt.Println("你猜中了!数字是", secretNumber)
break
}
}
}
青训营作业-2 使用go语言编写获取后台接口信息的小程序
1. 抓包教程
- 首先是用浏览器打开彩云小译,网址:https://fanyi.caiyunapp.com/#/
- 进入到网页后,按
F12
,审查html - 输入good,点击翻译,找到如下的请求
- 让我们来看一下抓包到的请求的具体信息,要找到的属性页有两个
Preview
和Header
这两个属性页中包含了翻译网页请求接口的绝大多数信息 - 再将curl复制下来,得到一大串的字符(注意复制的时候要选
复制为curl(bash)
),然后打开网址https://curlconverter.com/
将刚刚的curl复制过去,转换为代码,得到
package main
import (
"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("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)
}
这时候这段代码就可以运行了,请求主体完成,接下来我们来封装这段请求,并进入代码编写阶段
2. 代码编写
- 封装请求
func requestDict(source string) []byte {
client := &http.Client{} //创建客户端
request := requestDataInfo{
TransType: "en2zh",
Source: source,
}
buf, err := json.Marshal(request) //序列化结构体
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("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("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) //debug
return bodyText
}
该接口返回的是请求某个单词的翻译后的json数据,那么我们的思路应该是要设置一个结构体,来接受该数据
2. 得到结构体
已知该请求返回得到的结构体非常复杂繁琐,所以需要用到结构体生成工具https://oktools.net/json2go,只需要将preview返回的元素粘贴进去选择嵌套
即可
type AutoGenerated 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"`
}
3.封装函数,写人机交互,完整代码:
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
)
type requestDataInfo struct { //请求数据结构体
TransType string `json:"trans_type"`
Source string `json:"source"`
}
type dictRes 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 requestDict(source string) dictRes {
client := &http.Client{} //创建客户端
request := requestDataInfo{
TransType: "en2zh",
Source: source,
}
buf, err := json.Marshal(request) //序列化结构体
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("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("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)
}
if resp.StatusCode != 200 { //处理请求不成功的情形
log.Fatal("bad status Code:", resp.StatusCode, "body", string(bodyText))
}
var res dictRes
err = json.Unmarshal(bodyText, &res)
return res
}
func query(word string) {
//要求获取该单词的音标、语言类型以及解释
res := requestDict(word)
fmt.Println("你查询的单词是:", word, "UK", res.Dictionary.Prons.En, "US", res.Dictionary.Prons.En)
for _, exp := range res.Dictionary.Explanations {
fmt.Println(exp)
}
}
func main() {
var word string
fmt.Scanf("%s", &word)
strings.TrimSuffix(word, "\n")
query(word)
}
3. 实战 - 实现火山翻译的抓包
根据我们第二点开始实现火山翻译的抓包:
定位到请求,我们在这个请求上开始,要拿到三个东西
1.发送请求的包头
拿到url即可
2.返回的json数据
在priview
3.发送请求的数据
在Request payload中
最终编码封装得到
type requestDataInfoFireMount struct {
Text string `json:"text"`
Language string `json:"language"`
}
type dictResFireMount struct {
Words []struct {
Source int `json:"source"`
Text string `json:"text"`
PosList []struct {
Type int `json:"type"`
Phonetics []struct {
Type int `json:"type"`
Text string `json:"text"`
} `json:"phonetics"`
Explanations []struct {
Text string `json:"text"`
Examples []struct {
Type int `json:"type"`
Sentences []struct {
Text string `json:"text"`
TransText string `json:"trans_text"`
} `json:"sentences"`
} `json:"examples"`
Synonyms []interface{} `json:"synonyms"`
} `json:"explanations"`
Relevancys []interface{} `json:"relevancys"`
} `json:"pos_list"`
} `json:"words"`
Phrases []interface{} `json:"phrases"`
BaseResp struct {
StatusCode int `json:"status_code"`
StatusMessage string `json:"status_message"`
} `json:"base_resp"`
}
func requestFireMount(source string) dictResFireMount {
client := &http.Client{}
request := requestDataInfoFireMount{
Language: "en",
Text: source,
}
buf, err := json.Marshal(request)
var data = bytes.NewReader(buf)
//var data = strings.NewReader(`{"text":"hello\n\n","language":"en"}`)
req, err := http.NewRequest("POST", "https://translate.volcengine.com/web/dict/match/v1/?msToken=&X-Bogus=DFSzswVOQDaKSfvtSWqkwOizRCs0&_signature=_02B4Z6wo00001ShekuAAAIDA.5i.mxIIxsUoXpZAAChtJQ1fYIB5IufTjBM1xCFC1hk3Q36Izh43743m4lvY451AeNcLTE.xA-xHY1hWaz0vhCT12BnAM6gYjkY863HRQd.vv90dwOjP48.738", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("authority", "translate.volcengine.com")
req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"`)
req.Header.Set("accept", "application/json, text/plain, */*")
req.Header.Set("content-type", "application/json")
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
req.Header.Set("origin", "https://translate.volcengine.com")
req.Header.Set("sec-fetch-site", "same-origin")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("referer", "https://translate.volcengine.com/translate?category=&home_language=zh&source_language=detect&target_language=zh&text=good%0A")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
req.Header.Set("cookie", "x-jupiter-uuid=16521717160593380; i18next=zh-CN; s_v_web_id=verify_9ee5cfbc7da4332702124644a695e0e7; _tea_utm_cache_2018=undefined; ttcid=3bea2de0171748ff8afafa24de02c4c615; tt_scid=X-WQFPZGdMMILVTs.Vl6C-W7n6ZTe6R5rRA3.cjgNzb.7pA9LfkZnyJSOGxmrAdX3664")
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)
}
var res dictResFireMount
err = json.Unmarshal(bodyText, &res)
return res
}
func queryFireMount(word string) {
res := requestFireMount(word)
for _, words := range res.Words {
for _, poslist := range words.PosList {
for _, explanations := range poslist.Explanations {
fmt.Println(explanations.Text)
}
}
}
}
即可完成火山翻译的抓包
接着要完成并发
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"sync"
"time"
)
type requestDataInfoColor struct { //请求数据结构体
TransType string `json:"trans_type"`
Source string `json:"source"`
}
type requestDataInfoFireMount struct {
Text string `json:"text"`
Language string `json:"language"`
}
type dictResColor 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"`
}
type dictResFireMount struct {
Words []struct {
Source int `json:"source"`
Text string `json:"text"`
PosList []struct {
Type int `json:"type"`
Phonetics []struct {
Type int `json:"type"`
Text string `json:"text"`
} `json:"phonetics"`
Explanations []struct {
Text string `json:"text"`
Examples []struct {
Type int `json:"type"`
Sentences []struct {
Text string `json:"text"`
TransText string `json:"trans_text"`
} `json:"sentences"`
} `json:"examples"`
Synonyms []interface{} `json:"synonyms"`
} `json:"explanations"`
Relevancys []interface{} `json:"relevancys"`
} `json:"pos_list"`
} `json:"words"`
Phrases []interface{} `json:"phrases"`
BaseResp struct {
StatusCode int `json:"status_code"`
StatusMessage string `json:"status_message"`
} `json:"base_resp"`
}
func requestFireMount(source string, wg *sync.WaitGroup) dictResFireMount {
startTime := time.Now()
client := &http.Client{}
request := requestDataInfoFireMount{
Language: "en",
Text: source,
}
buf, err := json.Marshal(request)
var data = bytes.NewReader(buf)
//var data = strings.NewReader(`{"text":"hello\n\n","language":"en"}`)
req, err := http.NewRequest("POST", "https://translate.volcengine.com/web/dict/match/v1/?msToken=&X-Bogus=DFSzswVOQDaKSfvtSWqkwOizRCs0&_signature=_02B4Z6wo00001ShekuAAAIDA.5i.mxIIxsUoXpZAAChtJQ1fYIB5IufTjBM1xCFC1hk3Q36Izh43743m4lvY451AeNcLTE.xA-xHY1hWaz0vhCT12BnAM6gYjkY863HRQd.vv90dwOjP48.738", data)
if err != nil {
log.Fatal(err)
}
req.Header.Set("authority", "translate.volcengine.com")
req.Header.Set("sec-ch-ua", `" Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"`)
req.Header.Set("accept", "application/json, text/plain, */*")
req.Header.Set("content-type", "application/json")
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
req.Header.Set("origin", "https://translate.volcengine.com")
req.Header.Set("sec-fetch-site", "same-origin")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("referer", "https://translate.volcengine.com/translate?category=&home_language=zh&source_language=detect&target_language=zh&text=good%0A")
req.Header.Set("accept-language", "zh-CN,zh;q=0.9")
req.Header.Set("cookie", "x-jupiter-uuid=16521717160593380; i18next=zh-CN; s_v_web_id=verify_9ee5cfbc7da4332702124644a695e0e7; _tea_utm_cache_2018=undefined; ttcid=3bea2de0171748ff8afafa24de02c4c615; tt_scid=X-WQFPZGdMMILVTs.Vl6C-W7n6ZTe6R5rRA3.cjgNzb.7pA9LfkZnyJSOGxmrAdX3664")
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)
}
var res dictResFireMount
err = json.Unmarshal(bodyText, &res)
finishTime := time.Now()
fmt.Println("火山翻译用的时间是:", finishTime.Sub(startTime))
wg.Done()
return res
}
func requestDictColor(source string, wg *sync.WaitGroup) dictResColor {
startTime := time.Now()
client := &http.Client{} //创建客户端
request := requestDataInfoColor{
TransType: "en2zh",
Source: source,
}
buf, err := json.Marshal(request) //序列化结构体
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("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("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)
}
if resp.StatusCode != 200 { //处理请求不成功的情形
log.Fatal("bad status Code:", resp.StatusCode, "body", string(bodyText))
}
finishTime := time.Now()
fmt.Println("彩云翻译用的时间是:", finishTime.Sub(startTime))
wg.Done()
var res dictResColor
err = json.Unmarshal(bodyText, &res)
return res
}
func queryColor(word string, wg *sync.WaitGroup) {
//要求获取该单词的音标、语言类型以及解释
res := requestDictColor(word, wg)
fmt.Println("你查询的单词是:", word, "UK", res.Dictionary.Prons.En, "US", res.Dictionary.Prons.En)
for _, exp := range res.Dictionary.Explanations {
fmt.Println(exp)
}
}
func queryFireMount(word string, wg *sync.WaitGroup) {
res := requestFireMount(word, wg)
for _, words := range res.Words {
for _, poslist := range words.PosList {
for _, explanations := range poslist.Explanations {
fmt.Println(explanations.Text)
}
}
}
}
func main() {
var word string
fmt.Scanf("%s", &word)
strings.TrimSuffix(word, "\n")
wg := sync.WaitGroup{}
wg.Add(2)
go queryFireMount(word, &wg)
go queryFireMount(word, &wg)
wg.Wait()
}
青训营作业-3 代理服务器
1. SOCKS5代理原理
第一阶段,首先是client
与Socks5 Server
进行协商,通过协商后进入第二阶段
第二阶段,client
发送请求到Socks5 Server,Server与Host三次握手后建立TCP连接,建立完成后返回响应给server,Server再返回状态给client
第三阶段,client
发送数据给Socke5 Server,接着relay数据到Host,接着一层层回传数据到client
2. 代码实现-TCP echo server
package main
import (
"bufio"
"log"
"net"
)
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
}
}
}
func main() {
//1.建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
server, err := net.Listen("tcp", "127.0.0.1:1080")
//2.处理异常
if err != nil {
panic(err)
}
//3.死循环,相当于一个压力测试(操作系统),接收服务端给来的信息
for {
client, err := server.Accept() //去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
if err != nil {
log.Printf("Accepted failed!", err)
continue
}
//4.将接收到的client信息传入process函数中进行处理
go process(client) //使用go 协程处理
}
}
如何测试该函数?
首先启动go run该程序
如果是在Windows下的话,要事先安装NetCat并且配置环境变量后执行nc 127.0.0.1 1080
,即可监听
如果是在Linux下NetCat是内置的,因此直接输入指令即可
3.代码实现-auth
package main
import (
"bufio"
"fmt"
"io"
"log"
"net"
)
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.Printf("auth success")
}
const (
socks5Ver = 0x05
cmdBind = 0x01
atypIPV4 = 0x01
atypeHOST = 0x03
atypeIPV6 = 0x04
)
/**
*/
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, err := reader.ReadByte() //读取报文的第一个字节,读取版本
if err != nil { //如果读取产生了错误,优先处理错误
return fmt.Errorf("read ver failed!%v", err)
}
if ver != socks5Ver { //如果版本协议是不支持的
return fmt.Errorf("not supported ver!%v", ver)
}
/*读取报文的第二个字节*/
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read method size failed!%v", err)
}
/*创建合适的缓冲区大小,并用reader剩余的内容填满method*/
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("full method failed!%v", err)
}
/*获取得到了三个报文,打印得到*/
log.Println("ver:", ver, "method", method)
//解析该包后,将该包返回给浏览器,告诉它我们用了什么版本协议和什么方法
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("Write connection failed!%v", err)
}
return nil
}
func main() {
//1.建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
server, err := net.Listen("tcp", "127.0.0.1:1080")
//2.处理异常
if err != nil {
panic(err)
}
//3.死循环,相当于一个压力测试(操作系统),接收服务端给来的信息
for {
client, err := server.Accept() //去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
if err != nil {
log.Printf("Accepted failed!", err)
continue
}
//4.将接收到的client信息传入process函数中进行处理
go process(client) //使用go 协程处理
}
}
运行代码,首先启动main函数,启动监听,然后启动终端,输入curl --socks5 127.0.0.1:1080 -v http://www.qq.com
代理进程输出:ver: 5 method [0 1]
请求代理进程输出:
- Trying 127.0.0.1:1080…
- SOCKS5 connect to IPv4 121.14.77.221:80 (locally resolved)
- Failed to receive SOCKS5 connect request ack.
- Closing connection 0
curl: (97) Failed to receive SOCKS5 connect request ack
这是正常的,因为我们还没有编写请求函数,因此没办法正常完成代理
4. 代码实现-connection
connection函数的逻辑是接收来自浏览器发送过来的一个报文。解析请求后返回
package main
import (
"bufio"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)
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 = connection(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
log.Printf("auth success")
}
const (
socks5Ver = 0x05
cmdBind = 0x01
atypIPV4 = 0x01
atypeHOST = 0x03
atypeIPV6 = 0x04
)
/**
*/
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, err := reader.ReadByte() //读取报文的第一个字节,读取版本
if err != nil { //如果读取产生了错误,优先处理错误
return fmt.Errorf("read ver failed!%v", err)
}
if ver != socks5Ver { //如果版本协议是不支持的
return fmt.Errorf("not supported ver!%v", ver)
}
/*读取报文的第二个字节*/
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read method size failed!%v", err)
}
/*创建合适的缓冲区大小,并用reader剩余的内容填满method*/
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("full method failed!%v", err)
}
/*获取得到了三个报文,打印得到*/
log.Println("ver:", ver, "method", method)
//解析该包后,将该包返回给浏览器,告诉它我们用了什么版本协议和什么方法
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("Write connection failed!%v", err)
}
return nil
}
func connection(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个字节
/**
请求函数的逻辑,我们的第一步是解析来自浏览器的报文
*/
//对于定长的字节,我们是很好处理的,这里采取的方法是创建一个4字节切片来接收前4个字节
data := make([]byte, 4)
_, err = io.ReadFull(reader, data)
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, aytp := data[0], data[1], data[3]
//验证前三个字段的合法性
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd)
}
//aytp的合法性验证比较特殊,要与后面的地址结合起来看
addr := ""
switch aytp {
case atypIPV4:
//如果是IPV4的类型的话,就是要读取后面4个字节的数据,我们直接利用上面的data切片
_, err = io.ReadFull(reader, data)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3])
case atypeHOST:
//如果是HOST域名类型,由于域名是不定长的,因此要读取不定长的变量
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, data[:2]) //用前两个位置来接受端口,切片语法
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(data[:2]) //按照大端字节序解析byte数组
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)
// +----+-----+-------+------+----------+----------+
// |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})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
return nil
}
func main() {
//1.建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
server, err := net.Listen("tcp", "127.0.0.1:1080")
//2.处理异常
if err != nil {
panic(err)
}
//3.死循环,相当于一个压力测试(操作系统),接收服务端给来的信息
for {
client, err := server.Accept() //去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
if err != nil {
log.Printf("Accepted failed!", err)
continue
}
//4.将接收到的client信息传入process函数中进行处理
go process(client) //使用go 协程处理
}
}
5. 代码实现-relay阶段
双向拷贝
package main
import (
"bufio"
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"log"
"net"
)
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 = connection(reader, conn)
if err != nil {
log.Printf("client %v auth failed:%v", conn.RemoteAddr(), err)
return
}
log.Printf("auth success")
}
const (
socks5Ver = 0x05
cmdBind = 0x01
atypIPV4 = 0x01
atypeHOST = 0x03
atypeIPV6 = 0x04
)
/**
*/
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, err := reader.ReadByte() //读取报文的第一个字节,读取版本
if err != nil { //如果读取产生了错误,优先处理错误
return fmt.Errorf("read ver failed!%v", err)
}
if ver != socks5Ver { //如果版本协议是不支持的
return fmt.Errorf("not supported ver!%v", ver)
}
/*读取报文的第二个字节*/
methodSize, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read method size failed!%v", err)
}
/*创建合适的缓冲区大小,并用reader剩余的内容填满method*/
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("full method failed!%v", err)
}
/*获取得到了三个报文,打印得到*/
log.Println("ver:", ver, "method", method)
//解析该包后,将该包返回给浏览器,告诉它我们用了什么版本协议和什么方法
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("Write connection failed!%v", err)
}
return nil
}
func connection(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个字节
/**
请求函数的逻辑,我们的第一步是解析来自浏览器的报文
*/
//对于定长的字节,我们是很好处理的,这里采取的方法是创建一个4字节切片来接收前4个字节
data := make([]byte, 4)
_, err = io.ReadFull(reader, data)
if err != nil {
return fmt.Errorf("read header failed:%w", err)
}
ver, cmd, aytp := data[0], data[1], data[3]
//验证前三个字段的合法性
if ver != socks5Ver {
return fmt.Errorf("not supported ver:%v", ver)
}
if cmd != cmdBind {
return fmt.Errorf("not supported cmd:%v", cmd)
}
//aytp的合法性验证比较特殊,要与后面的地址结合起来看
addr := ""
switch aytp {
case atypIPV4:
//如果是IPV4的类型的话,就是要读取后面4个字节的数据,我们直接利用上面的data切片
_, err = io.ReadFull(reader, data)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%d.%d.%d.%d", data[0], data[1], data[2], data[3])
case atypeHOST:
//如果是HOST域名类型,由于域名是不定长的,因此要读取不定长的变量
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, data[:2]) //用前两个位置来接受端口,切片语法
if err != nil {
return fmt.Errorf("read port failed:%w", err)
}
port := binary.BigEndian.Uint16(data[:2]) //按照大端字节序解析byte数组
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)
// +----+-----+-------+------+----------+----------+
// |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})
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_, err = io.Copy(dest, reader)
cancel()
}()
go func() {
_, err = io.Copy(conn, dest)
cancel()
}()
<-ctx.Done()
return nil
}
func main() {
//1.建立一个TCP监听,去监听一个端口,和设置监听之后的状态是否正确
server, err := net.Listen("tcp", "127.0.0.1:1080")
//2.处理异常
if err != nil {
panic(err)
}
//3.死循环,相当于一个压力测试(操作系统),接收服务端给来的信息
for {
client, err := server.Accept() //去接受一个请求,接受了之后会得到一个连接,然后通过process函数来处理该连接
if err != nil {
log.Printf("Accepted failed!", err)
continue
}
//4.将接收到的client信息传入process函数中进行处理
go process(client) //使用go 协程处理
}
}