这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天.今天复习了go基础并进行了go的第一次实战案例.
一.go基础
1.slice的部分使用(append)
1.slice是go里面的可变化的数组,slice的append方法可以使sice长度增加.slice可以认为含有三个成分:cap(容量),len(长度),指针.
cap决定了在不修改底层数组的情况下,slice可以达到的最大长度.
len是当前slice的长度.
指针指向slice使用数组的第一个元素(slice的第一个元素不等于数组的第一个元素),slice通过数组的地址使用数组,对数组的元素是间接访问的.
2.对于slice的append,必须将返回的值赋给原本的slice变量,如果不处理返回值,可能会造成添加失败.
因为slice进行append时,如果没有足够的增长空间的话,append会先分配一个足够大的slice用于保存新的结果,先将原本的slice中的值复制到新的空间,然后添加新元素不处理返回值会将新的数组废弃,对于append我们不能保证他是否进行了slice的重新分配,因此我们也不能确认新的slice和原始的slice是否引用的是相同的底层数组空间,所以最好将返回值赋予slice变量.
func appendInt(slice []int, x int) []int {
var newSlice []int
newLen := len(slice) + 1
if newLen <= cap(slice) {
newSlice = slice[:newLen]
} else {
zcap := newLen
if zcap < 2*len(slice) {
zcap = 2 * len(slice)
}
newSlice = make([]int, newLen, zcap)
copy(newSlice, slice)
}
newSlice[len(slice)] = x
return newSlice
}
这是append的一种简单实现,append的扩容方法类似于此,但是他使用了一套更加复杂的扩容方法.
2.导出
只有大写字母开头的变量称为被导出,导出的变量才可以被其他包使用,同样只有导出的结构体成员才会被Json化.
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color"`
Actors []string
}
var movies = []Movie{
{Title: "Casablanca",
Year: 1942,
Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
}
func main() {
Data, err := json.MarshalIndent(movies, "", " ")
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", Data)
}
如果将结构体中的变量改为小写,那么他们将不会被josn化.
3.os/exec
在 Golang 中用于执行命令的库是 os/exec,exec.Command 函数返回一个*Cmd变量.
func main() {
cmd := exec.Command("pwd")
err := cmd.Run()
if err != nil {
log.Fatalln(err)
}
}
如果你想要得到结果,你可以使用CombinedOutput(),他会返回你需要的输出
out, err := cmd.CombinedOutput()
fmt.Println(string(out))
如果你想要得到使用*适配符号,那么会很遗憾,因为他不能使用,比如
cmd := exec.Command("ls", "./*")
它将会报错ls: 无法访问 './*': 没有那个文件或目录
二.实战案例sockts5
今天刚刚学习的socks5的实战案例,尝试用"net/http"编写一个socks5代理.
1.socks5代理的交互流程:
2.客户端和代理服务器建立socket-tcp代理连接后,客户端向代理服务器发送请求来确认协议版本和认证方式,请求格式:(单位为字节)
+----+----------+----------+
|VER | NMETHODS | METHODS |
+----+----------+----------+
| 1 | 1 | 1 to 255 |
+----+----------+----------+
VER: 协议版本,socks5为0x05
NMETHODS: 支持认证的方法数量
METHODS: 对应NMETHODS,NMETHODS的值为多少,METHODS就有多少个字节。RFC预定义了一些值的含义,内容如下:
X’00’ 不需要认证
X’01’:GSSAPI
X’02’ 用户名、密码认证
X’80’-X’FE’:为私人方法保留
X’FF’:无可接受的方法
3.代理服务从客户端收到的协议中选择一个认证方法,会发送如下格式给客户端:(单位为字节)
-------------------------
| VER | METHOD |
-------------------------
| 1 | 1 |
-------------------------
VER:socks版本号
METHOD:认证方式,如果返回的是0XFF,客户端没有认证方式,需要与服务器关闭连接
4.客户端向代理服务器发送代理请求信息,请求格式:(单位为字节)
+----+-----+-------+------+----------+----------+
|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是一个可变长度的域名
0x04: IPv6地址类型,DST.ADDR为16个字节
DST.ADDR 一个可变长度的值
DST.PORT 目标端口,固定2个字节
5.代理服务器收到客户端请求后,回复请求:(单位为字节)
+----+-----+-------+------+----------+----------+
|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
6.完成以上步骤,连接就成功了,现在客户端通过与代理建立的socket发送消息,代理会将消息发送给目标服务器.
7.编写demo
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")
host := make([]byte, 16)
_, err = io.ReadFull(reader, host)
if err != nil {
return fmt.Errorf("read atyp failed:%w", err)
}
addr = fmt.Sprintf("%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x.%02x%02x", host[0], host[1], host[2], host[3], host[4], host[5], host[6], host[7], host[8], host[9], host[10], host[11], host[12], host[13], host[14], host[15])
default:
return errors.New("invalid atyp")
}
_, err = io.ReadFull(reader, buf[:2])
if err != nil {
return fmt.Errorf("read : %w", err)
}
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()
_, 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
}
这是今天课程上的小demo,不同的是我添加了ipv6的处理方法(似乎不太对劲).
auth()函数负责客户端与代理之间的协商协议版本和认证信息, connect()函数负责客户端与代理之间的认证连接和数据读取.
在connect()的最后使用通道(<-)等待goroutine的完成,防止程序运行完毕或进行其他工作,对foroutione进行影响或者影响foroutione.
读取时使用io.ReadFull(src,buf)函数,将读取src中最多能存储在buf的byte数,但如果读的数量不够就会阻塞住,最后使用两个goroutine并发进行copy.
但是目前对于www.qq.com 无法正确处理,调用www.qq.com 时会使用ipv6,并在最后爆出so such host的错误,但对于其他其他地址的解析正常,如: juejin.cn.
ps:如果有条件可以使用两台设备进行测试.