Go的入门笔记(一)|青训营笔记
Go的入门笔记(一)|青训营笔记
这是我参与「第三届青训营 -后端场」笔记创作活动的的第一篇笔记。
以上是青训营的主要内容。
青训营第一节课的代码地址
hello world
首先是hello word,所有的语言学习都从这里开始嘛。
import (
"fmt"
)
func main() {
fmt.Println("hello world")
}
我们可以通过命令 go run xx.go来进行go文件的运行
当然可以同时进行编译,通过go build xx.go 可以生成 xx.exe 然后即可运行
学习Go的语法
当然笔者之前主要是写java的,所以难免会将两种语言进行比较,这里就减少对代码的粘贴,因为代码在github上前面的已经给出了地址,读者可以自行下载运行,与该文章进行对比观看。
这里主要是对菜鸟教程的进行了总结。 如果基础好一些的可以直接跳到正文部分
打印
这里的输出并非是调用系统静态函数,而是通过引入fmt包,通过包中的fmt.Println("")进行输出。
变量类型
这里的初始化很像前端或者jdk10的新特性,可以通过var来新建变量,格式为 var 变量名1,变量名2... 变量类型
这里的变量可以不用初始化输出,会是默认的,int是0,字符串是"",bool是false。下列的类型默认值是nil
var a *int //指针
var a []int //数组
var a map[string] int //map键值对
var a chan int //管道
var a func(string) int //函数声明
var a error // error 是接口
f := "Runoob"等价于 var f string = "Runoob" 且初始化后不能再次:=
对于基础数据类型,int float bool string这几种,b=a是创建了一个新的变量 里面的值和a一样但是地址不一样,让b指向这个地址,而引用数据类型诸如结构体之类的就会创建一个对象,不去重新开辟内存空间而是直接指向a所在的地址,此时a和b的地址是相同的。
var声明的变量必须使用,不然会报错,而const即全局声明的变量是可以不去使用的。
lota 是一个对常量进行计算的 初始是0,每有一行就+1;
运算符
和java的区别大概只有&和*
&是取地址,*是指针变量,可以拿着这个地址获得对象。
var a int = 4
/* & 和 * 运算符实例 */
ptr = &a /* 'ptr' 包含了 'a' 变量的地址 */
fmt.Printf("a 的值为 %d\n", a); 4
fmt.Printf("*ptr 为 %d\n", *ptr); 4
条件语句
//if
if true {
fmt.Printf("asds")
if true {
fmt.Printf("asds")
}
} else{
/* 定义局部变量 */
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
fmt.Printf("你的等级是 %s\n", grade );
}
switch可以判断类型
package main
import "fmt"
func main() {
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
//输出 x 的类型 :<nil>
}
fallthrough可以继续执行下一个case
package main
import "fmt"
func main() {
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true") // ✔
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")// ✔
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")// ✔
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
}
结果为
2、case 条件语句为 true
3、case 条件语句为 false
4、case 条件语句为 true
select
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
Go 编程语言中 select 语句的语法如下:
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
以下描述了 select 语句的语法:
-
每个 case 都必须是一个通信
-
所有 channel 表达式都会被求值
-
所有被发送的表达式都会被求值
-
如果任意某个通信可以进行,它就执行,其他被忽略。
-
如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:
- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
select 语句应用演示:
package main
import "fmt"
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
以上代码执行结果为:
no communication
注意:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。
channel 详解 https://blog.csdn.net/xixihahalelehehe/article/details/104787831/
循环语句
Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。
和 C 语言的 for 一样:
for init; condition; post { }
和 C 的 while 一样:
for condition { }
和 C 的 for(;😉 一样:
for { }
- init: 一般为赋值表达式,给控制变量赋初值;
- condition: 关系表达式或逻辑表达式,循环控制条件;
- post: 一般为赋值表达式,给控制变量增量或减量。
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
以上代码中的 key 和 value 是可以省略。
如果只想读取 key,格式如下:
for key := range oldMap
或者这样:
for key, _ := range oldMap
如果只想读取 value,格式如下:
for _, value := range oldMap
所以在map类型的遍历中,直接:= range赋值的是key。在数组类型的时候,第一位是索引位置,第二位是值。
break可以直接使用,和java一样,但是多了一个标记,可以对应的跳出该标记
// 使用标记
fmt.Println("---- break label ----")
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
break re
}
}
这里就是标记了外层for循环,因此直接跳出两层for循环外面,正常的话应该是跳出一层。
continue同理,可以跳过指定标记处的循环的当前一次循环。
goto 语句可以无条件地转移到过程中指定的行。goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 10
/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}
结果为跳过了15
函数
这里需要定义函数名(形参1,形参2,形参3... int) (string,string) 举例:
package main
import "fmt"
func swap(x, y string) (string, string) {//这里因为x y 都为string 所以一起标记了
return y, x
}
func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b)
}
当你声明了一个对象,然后想给它添加函数
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
变量作用域
全局变量,局部变量和形式参数的概念和java一样。需要注意的是初始化的数据类型,int和float32默认是0;pointer的默认是nil。
数组
以下演示了数组初始化:
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
我们也可以通过字面量在声明数组的同时快速初始化数组:
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度:
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
或
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
如果设置了数组的长度,我们还可以通过指定下标来初始化元素:
// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}
初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小
多维数组的使用和java一样,声明略有区别
var a = [5][2]int{ {0,0}, {1,2}, {2,4}, {3,6},{4,8}}//五个长度为2的数组
指针
取地址
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a ) //变量的地址: 20818a220
}
指针声明
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
使用指针利用地址访问值
package main
import "fmt"
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
/*
a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20
*/
函数传递指针
通过指针交换两个数的值 这是java无法做到的
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);
fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}
这里因为传入的是指针,通过指针对值进行赋值,改变了函数外的值。
而java只能传入变量的副本,因此无法改变值。
结构体
结构体的定义和初始化
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
var Book2 Books /* 声明 Book2 为 Books 类型 */
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}
输出结果
{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com 0}
go的结构体和java的对象很像,但是不需要定义构造函数就可以直接赋值。
获取元素的话 也是对象.属性。
切片(Slice)
从本质上看就是动态数组的意思,长度不固定。所以和数组在声明上的区别是不需要说大小。
切片初始化
默认值为nil
arr :=[] int {1,2,3 } //直接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3。
s := arr[:] //初始化切片 s,是数组 arr 的引用。
s := arr[startIndex:endIndex] //将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片。
s := arr[startIndex:] //默认 endIndex 时将表示一直到arr的最后一个元素。
s := arr[:endIndex] //默认 startIndex 时将表示从 arr 的第一个元素开始。
s :=make([]int,len,cap) //通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片。
len()和cap()函数
len() 方法获取长度。cap() 可以测量切片最长可以达到多少。
append() 和 copy() 函数
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
/* 允许追加空切片 记得赋值回去*/
numbers = append(numbers, 0)
printSlice(numbers)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
*/
范围(Range)
range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
对数组和切片而言,第一个位置元素是索引i,第二个位置是指。对于map来说,第一个是key第二个是value。
package main
import "fmt"
func main() {
//这是我们使用 range 去求一个 slice 的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用 range 将传入索引和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range 也可以用在 map 的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举 Unicode 字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}
/* 结果:
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111
*/
Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
map使用示例
package main
import "fmt"
func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)
/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "巴黎"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"
/*使用键输出地图值 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}
/*查看元素在集合中是否存在 */
capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(capital) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("American 的首都是", capital)
} else {
fmt.Println("American 的首都不存在")
}
}
/*
France 首都是 巴黎
Italy 首都是 罗马
Japan 首都是 东京
India 首都是 新德里
American 的首都不存在*/
delete() 函数
delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。实例如下:
package main
import "fmt"
func main() {
/* 创建map */
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
fmt.Println("原始地图")
/* 打印地图 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
/*删除元素*/ delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")
fmt.Println("删除元素后地图")
/*打印地图*/
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
}
/*
原始地图
India 首都是 New delhi
France 首都是 Paris
Italy 首都是 Rome
Japan 首都是 Tokyo
法国条目被删除
删除元素后地图
Italy 首都是 Rome
Japan 首都是 Tokyo
India 首都是 New delhi*/
视频课的正文部分
前面讲解源码比较简单 https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fwangkechun%2Fgo-by-example
源码里面写的比较详细 我这里记录感觉比较重要的部分。
实战项目--猜数游戏
- 随机数种子 rand.Seed(time.Now().UnixNano()) 不去设定的话会一直生成固定的数字81
- rand.Intn(100) 生成 0-100大小的随机数
- 通过bufio.NewReader()将输入os.Stdin转换成流
- 通过ReadString函数读取的一行中 里面有一个换行符需要通过strings.TrimSuffix(input, "\n")去掉
- strconv.Atoi(input) 需要接受err参数 如果不为nil,需要输出问题并panic
最终的实现效果:
实战项目--在线词典
目的就是在彩云翻译这个网站上,通过后端访问,达到一个单词翻译的效果。
-
首先需要打开F12检查网页,点开Network,在网页中输入good并点击翻译。
-
在All下面的Name中找到dict请求,并注意右侧应该是post请求而不是optional(这个是跨域时的预访问)。如下图
且点击Payload和Preview应该有下面画面
-
Payload中的source代表要查询的内容,即"good";trans_type代表翻译类型,即"en2zh",英文到中文。
-
由于Headers中还有很多请求头信息,因此我们需要网站的辅助,首先将该请求复制(在Windows谷歌中,先右键,然后选择Copy,然后选择Copy as cURL(bash)),然后粘贴到代码生成的网站中,即可自动生成进行网络请求的go代码。
-
接下来会对代码进行解释。
- client := &http.Client{} 新建一个http的client,里面可以指定timeout即多久未响应就断开。
- data我们提供的是一个字符串,但是有些情况下这个字符串可能会很大,占用很多内存,因此我们通过之前用过的strings.NewReader()将字符串转换成流,这样下面建立的请求将会是流式请求,这就会占用比较少的内存。
- resp,err = client.Do(req)会真正的发送请求,其中err代表了各种网络错误。请求头中的内容少一两个无所谓。
- 在确认err为空后,应当第一时间进行defer,关闭resp.Body这一流
- 对结果流通过ioutil读入,然后%s进行输出。
-
这时我们输入的查询数据是写死的字符串,我们为了后面方便的输入,将字符串抽象成结构体,并修改输入流的代码。
//结构体 type DictRequest struct { TransType string `json:"trans_type"` Source string `json:"source"` UserID string `json:"user_id"` } //用下面的三行代码 替代注释的这行代码 来抽象出对象 应当注意这里是bytes流,因为json格式化后是byte数组 // var data = strings.NewReader(`{"trans_type":"en2zh","source":"good"}`) request := DictRequest{TransType: "en2zh", Source: "good"} buf, err := json.Marshal(request) var data = bytes.NewReader(buf)
-
我们可以看到Preview里面的Response非常的冗长,我们可以通过map获取,但是goLang是个强类型语言,这种方式不是最佳实践,我们应当写一个结构体,然后将字符串反序列化到结构体里。我们可以通过代码生成网站,复制Response中的内容为Object,输入网站获取结构体。选择转换-嵌套会让结构体更加紧凑一些。
var dictresponse DictResponse //新建对象
err = json.Unmarshal(bodyText, &dictresponse) //将json字符串反序列化到dictresponse对象中
if err != nil {
fmt.Println("error")
}
// fmt.Printf("%s\n", bodyText)
fmt.Printf("%#v\n", dictresponse) //将输出字符串改为输出对象 #附带详细信息
输出效果如下:
-
对response的返回码进行检验,如果不是200打印报文
if resp.StatusCode != 200 { log.Fatal("bad StatusCode:", resp.StatusCode, "body", string(bodyText)) }
-
通过os包,判断输入是否为运行语句和要查询的单词
if len(os.Args) != 2 { //获取输入的命令个数 fmt.Fprintf(os.Stderr, `usage: simpleDict WORD example: simpleDict hello`) os.Exit(1) } word := os.Args[1] //获取第二个元素 即要查询的单词 query(word) //查询 dictResponse.Dictionary.Explanations遍历输出Response中的这一内容
实战项目--Socket5代理服务器
要实现一个代理服务器,可以先尝试运行给出代码。
下载好curl这一软件后,可以打开终端进行使用
这一部分用到了网络的三次握手的部分知识,可以先查看这篇文章进行铺垫--深入浅出TCP三次握手 (多图详解),在无代理服务器的情况下就是正常的三次握手,但是有了代理服务器后,如下图:
- 第一步应当是客户端与代理服务器建立连接,首先发送一个报文到代理服务器,一般这个报文包括协议版本号 即v5,和支持的认证的种类,例如密码认证或不需要认证,代理服务器会选择一个支持的认证方式告诉客户端,这里我们用的是不加密的代理,因此跳过认证阶段。
- 客户端与代理服务器建立连接后,向其发送请求访问服务器的ip和端口,代理服务器与真正的服务器建立连接,并告知客户端。
- 建立好所有准备后,客户端发送数据,代理服务器就是一直转发两者的数据。
实现的版本迭代
- 版本1:我们接下来尝试实现它,先实现一个简单版的TCP echo sever,就发送给它啥 它就返回啥。代码如下
package main
import (
"bufio"
"log"
"net"
)
func main() {
server, err := net.Listen("tcp", "127.0.0.1:1082") //建立一个sever
if err != nil {
//panic(err)
}
for {
client, err := server.Accept() //获取客户端
if err != nil {
log.Printf("Accept failed %v", err)
continue
}
go process(client)//新建一个goruntine 可以类比为java的多线程(其实是协程) 来处理client
}
}
func process(conn net.Conn) { //协程要处理的内容
defer conn.Close()//首先defer流 在文件执行结束后,自下向上的关闭指定的流
reader := bufio.NewReader(conn) //获取reader对象 bufio是带缓冲的一个流
for {
b, err := reader.ReadByte() //每次只读一个字节的内容
if err != nil { //看起来是每次只读取一个字节,但其实底层有优化,每次读1KB然后一起返回
break
}
_, err = conn.Write([]byte{b})//write需要写入一个slice,就将读入的字节组装成slice写回去
if err != nil {
break
}
}
}
运行效果如上图,输入一个hello会由服务器返回一个hello。nc命令类似于curl,需要下载netcat的windows版本才能使用。
- 版本2:这里我们省略了认证的过程,在第二版中加上后process函数被修改如下:
func process(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
err := auth(reader, conn) //这里将获取到的读取流和连接传入到auth即认证过程中
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 | 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:%w", err)
}
if ver != socks5Ver { //先读到的内容是版本号 判断协议版本
return fmt.Errorf("not supported ver:%v", ver)
}
methodSize, err := reader.ReadByte()//第二个字节读到的是methodSize 即支持认证的方法数量
if err != nil {
return fmt.Errorf("read methodSize failed:%w", err)
}
method := make([]byte, methodSize)//有几个方法 就创建多大的slice 即缓冲区
_, err = io.ReadFull(reader, method)//通过ReadFull填充满 method缓冲区
if err != nil {
return fmt.Errorf("read method failed:%w", err)
}
log.Println("ver", ver, "method", method) //输出版本号和方法
// +----+--------+
// |VER | METHOD |
// +----+--------+
// | 1 | 1 |
// +----+--------+
_, err = conn.Write([]byte{socks5Ver, 0x00})//代理服务器将选择的版本号和方法打成包返回给客户端
if err != nil {
return fmt.Errorf("write failed:%w", err)
}
return nil
}
运行结果如下:
这里可以看到curl命令的建成方式是有两种,因为数组中代表方法编码的有0和1,因为我们刚完成整个流程的认证,还没与客户端建立连接,再转发,因此这种一定是会失败的。
-
版本3:新增代理服务器对curl进行返回包,并获得要去访问的ip和端口号,auth函数没有变化,只展示process和新的connect函数
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 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) //读取前面的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 {//对atype进行判定 目标地址不同就对应不同的处理方式 解析获得ip地址 case atypIPV4://ipv4 _, 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)//用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") } _, err = io.ReadFull(reader, buf[:2])//通过切片 复用之前的buf数组 if err != nil { return fmt.Errorf("read port failed:%w", err) } port := binary.BigEndian.Uint16(buf[:2])//利用大数 解析出端口号 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 地址类型 1最简单的一个 // BND.ADDR 服务绑定的地址 不用 写4个0 // BND.PORT 服务绑定的端口DST.PORT 不用 写2个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 }
这里可以看到连接在客户端和代理服务器已经建立,还欠缺代理服务器与真正要访问的服务器建立连接。 -
版本4:通过net.Dial建立tcp连接,然后通过两个协程 建立客户端和目标服务器的双向数据流,通过cancel()函数控制关闭。
func connect(reader *bufio.Reader, conn net.Conn) (err error) { //上面是一样的 //建立连接 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())//通过WithCancel函数创建一个ctx,当一个方向的拷贝出现问题就调用cancel() defer cancel() //建立两个gorountine 从两个方向死循环进行拷贝 go func() { _, _ = io.Copy(dest, reader) //从reader 即用户的浏览器 拷贝到 dest 即目标服务器 cancel() }() go func() { _, _ = io.Copy(conn, dest) //dest 即目标服务器 拷贝到 conn 即用户的浏览器 cancel() }() <-ctx.Done()//出现问题时 cancel函数被调用 结束执行 所有的 Goroutine 都会同步收到这一取消信号 return nil }
执行情况:
还可以通过在执行完go文件后,在谷歌浏览器下载SwichyOmega插件
应用选项后,在右上角选择自己新建的情景模式,
这样浏览器中所有访问的网址和端口号都会在代理服务器中显示出来,当然这样浏览器会无法正常访问,
这样就完成了代理服务器的搭建。