Go语言入门
安装
Mac M1 Pro:https://golang.google.cn/dl/
选择Arm,PKG格式的安装,默认会安装到/usr/local/go
文件夹下面
配置环境变量~/.bash_profile
:
# 下面是配置go环境
GOPATH=/usr/local/go
export GOBIN=$GOPATH/bin
export PATH=$PATH:$GOBIN
查看版本以及是否配置环境变量成功:
go version
基础语法
HelloWorld
可以使用Jerbrain的GoLand集成开发环境进行开发
package main
import (
"fmt"
)
func main() {
s := "cxk"
fmt.Println("Hello and welcome, %s!", s)
for i := 1; i <= 5; i++ {
fmt.Println("i =", 100/i)
}
}
变量
在 Go 语言中,变量定义遵循以下规则:
- 声明变量:
- 使用
var
关键字可以声明变量。 - 可以在同一行中声明多个变量,类型可以相同或不同。
- 使用
- 初始化变量:
- 变量可以在声明时直接赋值,也可以稍后赋值。
- 如果没有显式初始化,变量会被赋予其类型的零值(例如,整数为 0,布尔值为 false,字符串为空等)。
- 简短声明:
- 使用
:=
语法可以在函数内声明并初始化变量,无需使用var
关键字。
- 使用
- 常量:
- 使用
const
关键字定义常量,常量在编译时就确定其值,不能被修改。
- 使用
package main
import (
"fmt"
"math"
)
func main() {
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64
f := float32(e)
g := a + "foo"
fmt.Println(a, b, c, d, e, f)
fmt.Println(g)
const s string = "constant"
const h = 500000000
const i = 3e20 / h
fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
分支
if-else分支:if
后面的条件表达式必须是布尔类型,返回 true
或 false
。
可以在 if
语句中进行短变量声明。在这种情况下,变量的作用域仅限于 if
语句块内
package main
import (
"fmt"
)
func main() {
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if 8%4 == 0 {
fmt.Println("8 is divisible by 4")
}
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
switch分支:
switch
语句用于根据不同的条件执行不同的代码块。
可以通过指定表达式来进行匹配,如果不指定,默认匹配 true
可以在同一个 case
中列出多个条件,用逗号分隔
可以不使用表达式,直接写多个 case
,通常用于范围判断或布尔条件。
switch 默认不会穿透到下一个 case,如果需要,可以使用 fallthrough 关键字显式地继续执行下一个 case
package main
import (
"fmt"
"time"
)
func main() {
a := 2
switch a {
case 1:
fmt.Println("a is 1")
case 2:
fmt.Println("a is 2")
case 3:
fmt.Println("a is 3")
case 4, 5:
fmt.Println("a is 4 or 5")
default:
fmt.Println("a is not 1, 2, 3, 4 or 5")
}
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
default:
fmt.Println("Good afternoon!")
}
}
循环
无限循环:可以在 for
后面不写条件,形成一个无限循环,通常与 break
配合使用。
计数循环:通过初始化语句、条件和迭代语句来实现传统的计数循环,适合已知次数的操作。
条件循环:可以直接在 for
后写条件,循环将持续执行直到条件不再满足,类似于 while
循环。
continue
语句:使用 continue
可以跳过当前迭代的剩余部分,直接进入下一次迭代。
package main
import "fmt"
func main() {
i := 1
for {
fmt.Println("loop")
break
}
for j := 7; j < 9; j++ {
fmt.Println(j)
}
for n := 0; n < 5; n++ {
if n%2 == 0 {
continue
}
fmt.Println(n)
}
for i <= 3 {
fmt.Println(i)
i = i + 1
}
}
数组
用法:
数组声明:使用 var
关键字声明数组,可以指定长度和元素类型。例如,可以声明一个固定长度的整型数组。
元素赋值:可以通过索引直接对数组的元素进行赋值,索引从 0 开始。
数组长度:使用 len()
函数可以获取数组的长度,数组长度是固定的。
数组初始化:可以在声明时通过字面量初始化数组,直接为每个元素赋值。
多维数组:Go 支持多维数组,可以通过嵌套数组的方式定义,例如二维数组,并且可以通过嵌套循环对其进行赋值。
输出:可以使用 fmt.Println()
输出数组的元素或整个数组,方便查看数组的内容和结构。
更多情况下会使用切片而不是数组
package main
import "fmt"
func main() {
var a [5]int
a[4] = 100
fmt.Println(a[4], len(a))
b := [5]int{1, 2, 3, 4, 5}
fmt.Println(b)
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
fmt.Println("2d: ", twoD)
}
切片
可以理解为可变数组,类似于Java的ArrayList
切片声明:可以使用 make()
函数创建切片,指定其类型和初始长度。切片是动态大小的数据结构,适合存储可变长度的数据。
元素赋值:可以通过索引直接对切片的元素进行赋值,索引从 0 开始,切片的长度可以在运行时变化。
获取长度:使用 len()
函数可以获取切片的长度,便于在处理数据时进行控制。
追加元素:可以使用 append()
函数向切片追加元素。append()
函数可以一次追加一个或多个元素,并返回新的切片。
复制切片:可以使用 copy()
函数将一个切片的元素复制到另一个切片,确保数据的独立性。
切片操作:通过切片的索引可以获取子切片,使用 s[start:end]
语法来截取切片的一部分。
切片字面量:可以直接声明并初始化切片,通过字面量的方式指定元素,简洁方便。
package main
import "fmt"
func main() {
s := make([]string, 3)
s[0] = "a"
s[1] = "b"
s[2] = "c"
fmt.Println("get[2]:", s[2])
fmt.Println("len:", len(s))
s = append(s, "d")
s = append(s, "e", "f")
fmt.Println("apd:", s)
c := make([]string, len(s))
copy(c, s)
fmt.Println("cpy:", c)
fmt.Println(s[2:5])
fmt.Println(s[:5])
fmt.Println(s[2:])
good := []string{"g", "o", "o", "d"}
fmt.Println("dcl:", good)
}
map
类似于Java中的HashMap
映射声明:可以使用 make()
函数创建映射,指定键值对的类型。映射是一种无序的键值对集合,适合快速查找和存储数据。
元素赋值:通过键直接对映射的元素进行赋值,如果键不存在,会自动添加。
获取长度:使用 len()
函数可以获取映射中键值对的数量。
获取元素:通过键可以获取对应的值,使用 r, ok := mp[key]
语法可以检查键是否存在。
删除元素:可以使用 delete()
函数从映射中删除指定的键及其对应的值。
映射字面量:可以通过字面量的方式直接声明并初始化映射,简洁方便。
package main
import "fmt"
func main() {
mp := make(map[string]int)
mp["one"] = 1
mp["two"] = 2
fmt.Println(mp)
fmt.Println(len(mp))
fmt.Println(mp["one"])
fmt.Println(mp["two"])
r, ok := mp["unknown"]
fmt.Println(r, ok)
delete(mp, "one")
m2 := map[string]int{"one": 1, "two": 2}
var m3 = map[string]int{"one": 1, "two": 2}
fmt.Println(m2, m3)
}
range
range
关键字的用法主要用于遍历切片、映射和其他数据结构,具体包括以下几个方面:
- 遍历切片:使用
for i, num := range nums
可以遍历切片,i
是当前元素的索引,num
是对应的值。这样可以在遍历的同时获取每个元素的索引和值。 - 计算和条件判断:在遍历切片的过程中,可以对元素进行计算(如求和)和条件判断(如查找特定元素的索引)。
- 遍历映射:使用
for k, v := range m
可以遍历映射,k
是键,v
是对应的值,这使得访问映射中的每个键值对变得简单。 - 仅遍历键:如果只需要遍历映射的键,可以使用
for k := range m
,这样只会获取键而忽略值。 - 简洁性:
range
提供了一种简洁的方式来遍历各种数据结构,避免了手动管理索引或键的复杂性。
package main
import "fmt"
func main() {
nums := []int{2, 7, 11, 15}
sum := 0
for i, num := range nums {
sum += num
if num == 7 {
println("index of 7 is ", i)
}
}
fmt.Println("sum:", sum)
m := map[string]string{"name": "zhangsan", "age": "20"}
for k, v := range m {
fmt.Println(k, v)
}
for k := range m {
fmt.Println(k) //只打印key
}
}
函数
函数定义:使用 func
关键字定义函数,后接函数名、参数列表和返回值类型。函数可以接收一个或多个参数,并返回一个或多个值。
参数类型:在参数列表中,可以为每个参数单独指定类型,也可以在同一行中为多个参数指定相同类型,例如 add2(a, b int)
。
返回值:函数可以直接返回值,也可以使用命名返回值。在命名返回值的情况下,可以在函数体内直接赋值,最后使用 return
返回即可。
调用函数:在 main
函数中可以调用定义的函数,传入参数并接收返回值。
映射作为参数:函数可以接收映射作为参数,并在函数内部访问映射的元素,这使得函数更加灵活。
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func add2(a, b int) int {
return a + b
}
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k] //v is the value, ok is a boolean
return v, ok
}
func main() {
res := add(1, 2)
fmt.Println(res)
v, ok := exists(map[string]string{"a": "b"}, "a")
fmt.Println(v, ok)
}
指针
在 Go 语言中,函数参数的传递方式主要有值传递和指针传递:
- 值传递:默认情况下,函数参数是通过值传递的。在函数内部对参数的修改不会影响到外部变量。例如,在
add
函数中对n
的修改不会改变main
函数中的n
的值。 - 指针传递:通过传递变量的指针,可以在函数内部修改外部变量的值。在
add2ptr
函数中,通过传递&n
(n
的地址),可以直接修改外部变量的值。 - 指针解引用:在指针参数中,使用
*n
来解引用,从而访问和修改指针指向的值。 - 函数调用:在
main
函数中,通过调用函数并传入相应的参数,可以观察到值传递和指针传递的效果。
package main
import "fmt"
func add(n int) {
n += 2
}
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add(n)
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}
结构体
在 Go 语言中,结构体的用法包括以下几个方面:
- 结构体定义:使用
type
关键字定义结构体,结构体由一组字段组成,可以包含不同类型的数据。字段可以指定名称和类型。 - 结构体实例化:可以通过字段名或按顺序初始化结构体。例如,可以使用
user{name: "admin", password: "admin"}
或user{"admin", "admin"}
进行实例化。 - 字段访问和修改:可以通过点(
.
)操作符访问和修改结构体的字段。例如,c.password = "123456"
修改了c
的password
字段。 - 方法定义:可以为结构体定义方法,使用接收者(receiver)来关联方法与结构体。通过方法可以对结构体进行操作或提供功能。
- 值传递与指针传递:在函数中可以通过值传递(传递结构体副本)或指针传递(传递结构体的地址)来访问和修改结构体。
- 方法调用:可以通过点操作符调用结构体的方法,如
a.checkPassword("admin")
。
package main
import "fmt"
type user struct {
name string
password string
}
func main() {
a := user{name: "admin", password: "admin"}
b := user{"admin", "admin"}
c := user{name: "cxk"}
c.password = "123456"
var d user
d.name = "cxk"
d.password = "1024"
fmt.Println(a, b, c, d)
fmt.Println(checkPassword(a, "admin"))
fmt.Println(checkPassword2(&a, "admin"))
fmt.Println(a.checkPassword("admin"))
}
func checkPassword(u user, password string) bool {
return u.password == password
}
func checkPassword2(u *user, password string) bool {
return u.password == password
}
// 结构体方法
func (u user) checkPassword(password string) bool {
return u.password == password
}
错误处理
在 Go 语言中,错误处理和指针的用法包括以下几个方面:
- 错误处理:Go 语言通过返回值来处理错误,通常在函数返回多个值时,最后一个值是一个
error
类型,用于指示函数是否成功执行。例如,findUser
函数返回一个指向user
的指针和一个错误值。 - 条件检查:在调用可能返回错误的函数后,通常使用
if err != nil
来检查是否发生了错误。如果发生错误,可以通过打印错误信息或采取其他处理措施。 - 返回指针:在
findUser
函数中,返回的是指向用户结构体的指针。在遍历用户切片时,通过return &u
返回找到用户的地址。注意,直接返回结构体的地址可能会导致问题,因为循环中的u
是局部变量。 - 局部变量作用域:在
main
函数中,使用if u, err := findUser(...);
的形式可以在条件语句内定义局部变量u
和err
,该变量在if
语句块内有效,有助于避免变量冲突。 - 简洁性:通过错误处理机制,Go 提供了一种清晰且直观的方式来管理和响应运行时错误,增强了代码的可读性和可维护性。
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
func findUser(users []user, name string) (v *user, err error) {
for _, u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("user not found")
}
func main() {
u, err := findUser([]user{{"a", "b"}}, "a")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name)
if u, err := findUser([]user{{"a", "b"}}, "b"); err != nil {
fmt.Println(err)
} else {
fmt.Println(u.name)
}
}
实战案例
猜数字游戏
输入处理的方式如下:
- 使用 bufio 包:通过
bufio.NewReader(os.Stdin)
创建一个新的读取器,专门用于从标准输入(键盘)读取数据。 - 读取输入:使用
reader.ReadString('\n')
方法读取用户输入,直到遇到换行符为止。这将返回用户输入的字符串,包括换行符。 - 处理换行符:使用
strings.TrimSuffix(input, "\n")
去除字符串末尾的换行符,以便后续处理。 - 字符串转换为整数:通过
strconv.Atoi(input)
将输入的字符串转换为整数。如果转换失败,会返回一个错误。 - 错误处理:在读取和转换过程中,程序通过检查返回的错误,判断用户输入是否合法,并给予相应提示。
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNum := rand.Intn(maxNum)
fmt.Println("the secret number is", secretNum)
fmt.Println("Please input a number between 0 and", maxNum)
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occurred while reading input. Please try again", err)
continue
}
input = strings.TrimSuffix(input, "\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer")
continue
}
fmt.Println("You guessed:", guess)
if guess > secretNum {
fmt.Println("Your guess is too high")
} else if guess < secretNum {
fmt.Println("Your guess is too low")
} else {
fmt.Println("Congratulations! You guessed the secret number ")
break
}
}
}
命令行词典
curl转go:https://curlconverter.com/
json转go:https://oktools.iokde.com/json2go
结构体后面的部分是结构体标签,用于指定在 JSON 编码和解码时字段的名称。例如,TransType
将被序列化为 "trans_type"
,Source
将被序列化为 "source"
。
- 序列化(将结构体转换为 JSON):
- 使用
json.Marshal
函数将DictRequest
结构体实例request
转换为 JSON 格式的字节切片。 buf, err := json.Marshal(request)
将request
序列化为 JSON 字符串,存储在buf
中。如果发生错误,会记录并终止程序。
- 使用
- 反序列化(将 JSON 转换为结构体):
- 使用
json.Unmarshal
函数将从 HTTP 响应中读取的 JSON 数据转换为DictResponse
结构体。 err = json.Unmarshal(bodyText, &dictResponse)
将响应的 JSON 数据解析为dictResponse
,如果发生错误,会记录并终止程序。
- 使用
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
)
type DictRequest struct {
TransType string `json:"trans_type"`
Source string `json:"source"`
}
type DictResponse struct {
Rc int `json:"rc"`
Wiki struct {
} `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: "boy",
}
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("accept", "application/json, text/plain, */*")
req.Header.Set("accept-language", "zh")
req.Header.Set("app-name", "xiaoyi")
req.Header.Set("authorization", "bearer")
req.Header.Set("content-type", "application/json;charset=UTF-8")
req.Header.Set("device-id", "40fc51033a5ba5e96b5fc956ca6175a5")
req.Header.Set("origin", "https://fanyi.caiyunapp.com")
req.Header.Set("os-type", "web")
req.Header.Set("os-version", "")
req.Header.Set("priority", "u=1, i")
req.Header.Set("referer", "https://fanyi.caiyunapp.com/")
req.Header.Set("sec-ch-ua", `"Chromium";v="130", "Google Chrome";v="130", "Not?A_Brand";v="99"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"macOS"`)
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 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36")
req.Header.Set("x-authorization", "token:qgemv4jr1y38jyq6vhvi")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
bodyText, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
//fmt.Printf("%s\n", bodyText)
var dictResponse DictResponse
err = json.Unmarshal(bodyText, &dictResponse)
if err != nil {
log.Fatal(err)
}
log.Printf("%+v\n", dictResponse)
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: %s word\n", os.Args[0])
os.Exit(1)
}
word := os.Args[1]
query(word)
}
运行结果:
Sockets5代理
echo server
具体步骤:
-
创建监听器:
- 使用
net.Listen("tcp", "127.0.0.1:1080")
创建一个 TCP 监听器,绑定到本地的1080
端口。如果创建失败,程序会调用panic
终止运行。
- 使用
-
接受连接:
- 在无限循环中,使用
server.Accept()
接受客户端的连接请求。如果发生错误,则记录错误并继续下一次循环。
- 在无限循环中,使用
-
并发处理:
- 每当接受到一个客户端连接,使用
go process(client)
启动一个新的 Goroutine 来处理该连接。这样可以同时处理多个客户端请求。
- 每当接受到一个客户端连接,使用
-
处理客户端连接:
-
在
process
函数中,使用defer conn.Close()
确保在函数结束时关闭连接。 -
创建一个
bufio.NewReader
以从连接中读取数据。 -
使用
reader.ReadByte()
循环读取字节。如果读取成功,将读取到的字节通过conn.Write
写回给客户端,实现回显功能。如果读取或写入过程中发生错误,循环将终止。
-
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 error: %v\n", 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
}
}
}
auth
认证函数: auth 函数,该函数负责处理客户端的认证请求。认证过程遵循 SOCKS5 协议的规范:
- 读取版本号并验证是否为 SOCKS5 协议(
0x05
)。 - 读取支持的认证方法的数量和具体方法。
- 打印协议版本和方法信息以供调试。
- 向客户端发送认证成功的响应(
socks5Ver, 0x00
,表示无认证)。
package main
import (
"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 error: %v\n", err)
continue
}
go process(client)
}
}
func auth(reader *bufio.Reader, conn net.Conn) (err error) {
/**
格式
ver | nmethods | methods
1 | 1 | 1-255
ver:协议版本,socket5默认为0x05
nmethods:M支持认证的方法数量
methods:支持的nmethods的值为多少,methods就有多少个字节
*/
ver, err := reader.ReadByte()
if err != nil {
return fmt.Errorf("read ver error: %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 error: %v", err)
}
method := make([]byte, methodSize)
_, err = io.ReadFull(reader, method)
if err != nil {
return fmt.Errorf("read method error: %v", err)
}
log.Println("ver", ver, "method", method)
_, err = conn.Write([]byte{socks5Ver, 0x00})
if err != nil {
return fmt.Errorf("write response error: %v", err)
}
return nil
}
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")
}
使用curl去请求
打印结果:
请求阶段
在这个 Go 程序中,添加了 SOCKS5 协议的连接处理功能,具体做了以下几点:
- 处理连接请求:
- 在
process
函数中,调用connect
函数来处理客户端的连接请求。
- 在
- 连接请求的格式:
connect
函数解析客户端发送的连接请求,根据 SOCKS5 协议的格式读取请求头信息,包括版本号(VER)、命令(CMD)、保留字段(RSV)和目标地址类型(ATYP)。
- 解析目标地址和端口:
- 根据 ATYP 的值,处理不同类型的目标地址:
- IPv4 地址:读取 4 字节的 IP 地址并格式化为字符串。
- 域名地址:首先读取域名的长度,然后读取对应长度的字节作为域名。
- IPv6 地址:当前不支持,直接返回错误。
- 读取目标端口,使用大端字节序转换为 16 位无符号整数。
- 根据 ATYP 的值,处理不同类型的目标地址:
- 建立连接:
- 打印出要连接的地址和端口。
- 构造连接响应:
- 构造并发送响应给客户端,表示连接成功。响应的格式包括 SOCKS5 版本号、命令返回码(成功为
0x00
)、保留字段、地址类型(这里固定为0x01
表示 IPv4)、绑定地址(这里为0.0.0.0
)和绑定端口(通常为0
,表示未使用)。
- 构造并发送响应给客户端,表示连接成功。响应的格式包括 SOCKS5 版本号、命令返回码(成功为
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 connect 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)
_, 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", cmd)
}
addr := ""
switch atyp {
case atypeIPV4:
_, 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)
// +----+-----+-------+------+----------+----------+
// |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
}
可以拿到对应的ip和端口
relay阶段
在 connect
函数中,添加了以下功能来处理 SOCKS5 连接请求:
- 建立与目标主机的 TCP 连接:
- 使用
net.Dial
方法与客户端请求的目标地址(addr
)和端口(port
)建立 TCP 连接,并处理连接失败的情况。
- 使用
- 发送成功响应:
- 构造并发送一个成功的 SOCKS5 响应给客户端,表示连接已经建立。这包括 SOCKS5 版本号、返回码(
0x00
表示成功)、保留字段、地址类型(固定为0x01
),以及绑定地址和端口。
- 构造并发送一个成功的 SOCKS5 响应给客户端,表示连接已经建立。这包括 SOCKS5 版本号、返回码(
- 数据转发:
- 使用两个 goroutine 实现数据转发:
- 第一个 goroutine 从客户端的连接(
reader
)读取数据,并将其写入到目标服务器(dest
)。 - 第二个 goroutine 从目标服务器读取数据,并将其写入到客户端的连接(
conn
)。
- 第一个 goroutine 从客户端的连接(
- 使用两个 goroutine 实现数据转发:
- 等待连接关闭:
- 使用
context
来管理 goroutine 的生命周期,并在其中一个方向的数据传输完成后关闭连接。
- 使用
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)
_, 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", cmd)
}
addr := ""
switch atyp {
case atypeIPV4:
_, 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)
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()
// +----+-----+-------+------+----------+----------+
// |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() {
_, _ = io.Copy(dest, reader) // 从客户端读取数据并写入到目标服务器
cancel()
}()
go func() {
_, _ = io.Copy(conn, dest) // 从目标服务器读取数据并写入到客户端
cancel()
}()
<-ctx.Done() // 等待连接关闭
return nil
}
测试: