go版本shadowsocks源码-终章

本节我们来看一下shadowsocks-server端的源码。server端相比local端要简单不少,其所做的主要工作就是解码请求并返回结果。

main

照例我们从main函数开始分析其大致流程。

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
func main() {
log.SetOutput(os.Stdout)

var cmdConfig ss.Config
var printVer bool
var core int

flag.BoolVar(&printVer, "version", false, "print version")
flag.StringVar(&configFile, "c", "config.json", "specify config file")
flag.StringVar(&cmdConfig.Password, "k", "", "password")
flag.IntVar(&cmdConfig.ServerPort, "p", 0, "server port")
flag.IntVar(&cmdConfig.Timeout, "t", 300, "timeout in seconds")
flag.StringVar(&cmdConfig.Method, "m", "", "encryption method, default: aes-256-cfb")
flag.IntVar(&core, "core", 0, "maximum number of CPU cores to use, default is determinied by Go runtime")
flag.BoolVar((*bool)(&debug), "d", false, "print debug message")
flag.BoolVar((*bool)(&sanitizeIps), "A", false, "anonymize client ip addresses in all output")
flag.BoolVar(&udp, "u", false, "UDP Relay")
flag.StringVar(&managerAddr, "manager-address", "", "shadowsocks manager listening address")
flag.Parse()

if printVer {
ss.PrintVersion()
os.Exit(0)
}

ss.SetDebug(debug)

var err error
config, err = ss.ParseConfig(configFile)
if err != nil {
if !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "error reading %s: %v\n", configFile, err)
os.Exit(1)
}
config = &cmdConfig
ss.UpdateConfig(config, config)
} else {
ss.UpdateConfig(config, &cmdConfig)
}
if config.Method == "" {
config.Method = "aes-256-cfb"
}
if err = ss.CheckCipherMethod(config.Method); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err = unifyPortPassword(config); err != nil {
os.Exit(1)
}
if core > 0 {
runtime.GOMAXPROCS(core)
}
for port, password := range config.PortPassword {
go run(port, password)
if udp {
go runUDP(port, password)
}
}

if managerAddr != "" {
addr, err := net.ResolveUDPAddr("udp", managerAddr)
if err != nil {
fmt.Fprintln(os.Stderr, "Can't resolve address: ", err)
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Fprintln(os.Stderr, "Error listening:", err)
os.Exit(1)
}
log.Printf("manager listening udp addr %v ...\n", managerAddr)
defer conn.Close()
go managerDaemon(conn)
}

waitSignal()
}

从以上代码中可以看到,server端main函数大体也分成三个部分,第一个部分负责处理一系列的配置文件解析,参数校验等。第二部分处理连接请求。第三部分为对server启动后进行的实时线上管理进行操作。

参数解析

1
2
3
4
5
6
7
8
9
10
11
12
   flag.BoolVar(&printVer, "version", false, "print version")
flag.StringVar(&configFile, "c", "config.json", "specify config file")
flag.StringVar(&cmdConfig.Password, "k", "", "password")
flag.IntVar(&cmdConfig.ServerPort, "p", 0, "server port")
flag.IntVar(&cmdConfig.Timeout, "t", 300, "timeout in seconds")
flag.StringVar(&cmdConfig.Method, "m", "", "encryption method, default: aes-256-cfb")
flag.IntVar(&core, "core", 0, "maximum number of CPU cores to use, default is determinied by Go runtime")
flag.BoolVar((*bool)(&debug), "d", false, "print debug message")
flag.BoolVar((*bool)(&sanitizeIps), "A", false, "anonymize client ip addresses in all output")
flag.BoolVar(&udp, "u", false, "UDP Relay")
flag.StringVar(&managerAddr, "manager-address", "", "shadowsocks manager listening address")
flag.Parse()

这一部分个人感觉其实没什么,设置项目基本和local端保持一致。其中对于c pu的设置这里需要提一下,这是对于goroutine的设置,表示对于go逻辑处理器的分配。以及增加对于server运行时的管理设置。

处理连接请求

1
2
3
4
5
6
for port, password := range config.PortPassword {
go run(port, password)
if udp {
go runUDP(port, password)
}
}

正如代码1-6行所示,根据配置的情况,server会处理多个端口的请求。由于支持了udp的socks代理加入udp判断,如果设置了udp转发,则进行udp的代理。

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
func run(port, password string) {
ln, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Printf("error listening port %v: %v\n", port, err)
os.Exit(1)
}
passwdManager.add(port, password, ln)
var cipher *ss.Cipher
log.Printf("server listening port %v ...\n", port)
for {
conn, err := ln.Accept()
if err != nil {
// listener maybe closed to update password
debug.Printf("accept error: %v\n", err)
return
}
// Creating cipher upon first connection.
if cipher == nil {
log.Println("creating cipher for port:", port)
cipher, err = ss.NewCipher(config.Method, password)
if err != nil {
log.Printf("Error generating cipher for port: %s %v\n", port, err)
conn.Close()
continue
}
}
go handleConnection(ss.NewConn(conn, cipher.Copy()), port)
}
}

run()函数主要负责监听本地接口等待连接,随后启动一个goroutine 来执行handleConnection()处理到来的连接请求。

handleConnection

server对于请求的处理流程与local几乎相同,除了记录的日志信息不同。首先获取请求的目标地址,然后进行连接请求,最后创建两个PipeThenClose进行上行下行数据的传输,同时进行对数据的解密操作。整体流程如下:

manager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if managerAddr != "" {
addr, err := net.ResolveUDPAddr("udp", managerAddr)
if err != nil {
fmt.Fprintln(os.Stderr, "Can't resolve address: ", err)
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Fprintln(os.Stderr, "Error listening:", err)
os.Exit(1)
}
log.Printf("manager listening udp addr %v ...\n", managerAddr)
defer conn.Close()
go managerDaemon(conn)
}

对于shadowsocks的管理如果进行了设置,则会使用一个goroutine执行managerDaemon()进行守护相关端口的监听,返回shadowsocks的运行情况。其中包括其流量连接情况:

1
2
3
4
5
6
7
for _, addr := range reportconnSet {
res := reportStat()
if len(res) == 0 {
continue
}
conn.WriteToUDP(res, addr)
}

可以使用的管理命令包括增加端口,删除端口,ping远程地址,停止ping等。相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
switch {
case strings.HasPrefix(command, "add:"):
res = handleAddPort(bytes.Trim(data[4:], "\x00\r\n "))
case strings.HasPrefix(command, "remove:"):
res = handleRemovePort(bytes.Trim(data[7:], "\x00\r\n "))
case strings.HasPrefix(command, "ping"):
conn.WriteToUDP(handlePing(), remote)
reportconnSet[remote.String()] = remote // append the host into the report list
case strings.HasPrefix(command, "ping-stop"): // add the stop ping command
conn.WriteToUDP(handlePing(), remote)
delete(reportconnSet, remote.String())
}

最后

在main函数的最后,设置了一个waitSignal()用来等待Signal信号从而进行程序结束�前的收尾操作,当捕捉到该信号后进行updatePasswd等收尾操作。
至此整个shadowsocks的源码,大体上就分析完了。由于想基于一种全局的观点,所以一些极其细节的代码没有去阅读,重点其实还是在理解go语言在实际项目中的一些应用。

go版本shadowsocks源码(二)

本节来分析下,在上次开启本地服务等待连接后,handleConnection()函数是怎样处理拿到的请求的。

socks5协议客户端连接要求

在看具体的代码之前,我们首先来看看官方标准中是如何规定本地与客户端之间的交互的。
协议规定当客户端连到服务器后,然后就发送请求来协商版本和认证方法:
|VER|NMETHODS|METHODS|
:-:|:-:
|1|1|1 to 255|
其中ver表示协议版本(固定长度为一个字节),nmethods表示第三个字段的长度(即有几种认证方法),methods表示客户端支持的验证方式,长度1-255字节。
支持的验证方式官方制定了以下几种:

  • 0x00:NO AUTHENTICATION REQUIRED(不需要验证)
  • 0x01:GSSAPI (通用安全服务应用程序接口)
  • 0x02:USERNAME/PASSWD(用户名密码)
  • 0x03:IANA ASSIGNED(至 0x’7F’ IANA 分配)
  • 0x80:RESERVED FOR PRIVATE METHODS(至 0x’FE’ 私人方法保留)
  • 0xff:NO ACCEPTABLE METHODS(没有可接受的方法)

当服务端收到客户端的验证信息后,就要回应客户端提供哪种验证方式的信息。回应格式如下:
|VER|METHOD|
:-:|:-:
|1|1|

handleConnect函数

现在我们来看hendleConnect函数是如何处理�连接的:

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
func handleConnection(conn net.Conn) {
if debug {
debug.Printf("socks connect from %s\n", conn.RemoteAddr().String())
}
closed := false
defer func() {
if !closed {
conn.Close()
}
}()

var err error = nil
if err = handShake(conn); err != nil {
log.Println("socks handshake:", err)
return
}
rawaddr, addr, err := getRequest(conn)
if err != nil {
log.Println("error getting request:", err)
return
}
// Sending connection established message immediately to client.
// This some round trip time for creating socks connection with the client.
// But if connection failed, the client will get connection reset error.
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
if err != nil {
debug.Println("send connection confirmation:", err)
return
}

remote, err := createServerConn(rawaddr, addr)
if err != nil {
if len(servers.srvCipher) > 1 {
log.Println("Failed connect to all available shadowsocks server")
}
return
}
defer func() {
if !closed {
remote.Close()
}
}()

go ss.PipeThenClose(conn, remote, nil)
ss.PipeThenClose(remote, conn, nil)
closed = true
debug.Println("closed connection to", addr)
}

首先我们可以看到在代码13行-16行,对连接进行请求的协商,也就是第一部分介绍的交互协商认证的流程。
handShake代码如下

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
func handShake(conn net.Conn) (err error) {
const (
idVer = 0
idNmethod = 1
)

buf := make([]byte, 258)

var n int
ss.SetReadTimeout(conn)
if n, err = io.ReadAtLeast(conn, buf, idNmethod+1); err != nil {
return
}
if buf[idVer] != socksVer5 {
return errVer
}
nmethod := int(buf[idNmethod])
msgLen := nmethod + 2
if n == msgLen {
} else if n < msgLen {
if _, err = io.ReadFull(conn, buf[n:msgLen]); err != nil {
return
}
} else {
return errAuthExtraData
}
_, err = conn.Write([]byte{socksVer5, 0})
return
}

在代码11行读取了本地客户端发来的版本信息以及认证方法信息,如果协议版本不是socks5则立马返回。由于socks规定的字段ver与nmethods都是两个字节所以最后一个字段加上2个字节就是需要读取的信息总长度,最后第27行代码返回给客户端协议版本信息与选择的方法(此处不需要验证,所以传递0)。


1
2
3
4
5
rawaddr, addr, err := getRequest(conn)
if err != nil {
log.Println("error getting request:", err)
return
}

然后让我们把视角在回到handleConnection函数,在完成handshake的认证后客户端会向local发送一个带有目的地址和端口的请求包,由request函数完成获取操作。以下是发送的包内容:
|VER|CMD|RSV|ATYP|DST.ADDR|DST.PORT|
:-:|:-:|:-:|
|1|1|0x00|1|Variable|2|

  • VER:socks的版本
  • CMD:代表客户端请求的类型,值长度1个字节,有三种类型:
    • Connect:0x01
    • Bind:0x02
    • UDP:0x03
  • RSV:保留字段,默认0x00,长度1个字节
  • ATYP:代表请求的远程服务器地址类型,长度1个字节,三种类型:
    • IPV4:0X01
    • IPV6:0X04
    • DOMAINNAME:0x03
  • DST.ADDR:代表远程服务器的地址,根据ATYP进行解析,值长度不定
  • DST.PORT:代表远程服务器的端口,值长度2个字节

当loca接收到该信息,接下来应当向客户端返回一个结果,在代码中默认返回了success。如下所示:

1
2
3
4
5
_, err = conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x08, 0x43})
if err != nil {
debug.Println("send connection confirmation:", err)
return
}

当这些步骤完成后,接下来local端和server端建立连接。然后local负责把client的数据包加密后发送给ss-server。把收到的server数据包在发回给client。完成这些操作的函数就是PipeThenClose。

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
func PipeThenClose(src, dst net.Conn, addTraffic func(int)) {
defer dst.Close()
buf := leakyBuf.Get()
defer leakyBuf.Put(buf)
for {
SetReadTimeout(src)
n, err := src.Read(buf)
if addTraffic != nil {
addTraffic(n)
}
// read may return EOF with n > 0
// should always process n > 0 bytes before handling error
if n > 0 {
// Note: avoid overwrite err returned by Read.
if _, err := dst.Write(buf[0:n]); err != nil {
Debug.Println("write:", err)
break
}
}
if err != nil {
// Always "use of closed network connection", but no easy way to
// identify this specific error. So just leave the error along for now.
// More info here: https://code.google.com/p/go/issues/detail?id=4373
/*
if bool(Debug) && err != io.EOF {
Debug.Println("read:", err)
}
*/
break
}
}
return
}

在PipeThenClose代码中,其申请一块缓冲区然后不停的从src中读取数据,将数据转发到dst中。当然,读取的时候需要对数据进行解密,写的时候需要加密。解密加密操作分别由项目中的Conn负责,其重写了read与write函数,在read时会根据加密方法对数据进行解密。write时则会进行加密。

1
2
3
4
go ss.PipeThenClose(conn, remote, nil)
ss.PipeThenClose(remote, conn, nil)
closed = true
debug.Println("closed connection to", addr)

在handleConnection代码的最后,其使用两个PipeThenClose,新开启的goroutine和本身的goroutine病发的从本地到远程、远程到本地的上下行进行数据传输。

综上,handleConnection函数整体流程如下:

结语

local端的代码到此就算结束了,由于是按照整体流程进行分析,有些过于细节的地方几句就略过去了,重点还是在学习整体的流程以及socks协议的使用。server端的思路其实和local端比较类似,除了没有local中握手(handShake)的步骤。整体看来这个还是不太复杂的,整体代码也就2000行左右。

go语言读书笔记-aciton系列(并发)

终于到并发了,阅读本章总有种go对并发的处理简洁高效的感觉。action系列举的实战例子也很通俗易懂,感觉运用一些实际的项目实践应该理解会更加深刻。

goroutine

操作系统会在物理处理器上调度线程来运行程序,而Go语言运行时会在逻辑处理器上调用goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。
em。。goroutine可以理解为携程,就像这样。
image_1d6fdftt11ti71crn1qn2c791uls9.png-230.1kB
携程运行于线程之上,比线程更加轻量级。goroutine运行于逻辑处理器,而逻辑处理器就对应一个操作系统线程,其可以并发调度无数个groutine。

goroutine也可以并行运行,只要使用超过一个的逻辑处理器。但是只要底层硬件层面只有一个处理器,即使创建了多个逻辑处理器也依然是并发运行。
image_1d6felqb512qn1r251k38110tchqm.png-183.9kB
并发并行的区别正如书上的图所示。
通过设置参数可以给每个可用的物理处理器分配一个逻辑处理器,从而达到并行的目的。

1
2
3
import "runtime"

runtime.GOMAXPROCS(runtime.NumCpu())

同步goroutine

在写并发代码时,往往都会遇到竞争状态的问题。这就需要采用一些同步手段来得到正确的结果。go语言提供了锁的机制来同步。

原子函数

go sync包提供了一些常用的操作的原子函数。当使用这些函数来读,写时,其都会自动根据所引用的变量做同步处理。已addint64函数示例:

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
package main

import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)

func main() {
wg.Add(2)

go incCounter(1)
go incCounter(2)

wg.Wait()

fmt.Println("Final Counter:",counter)
}


func incCounter(id int){
defer wg.Done()
for count :=0; count<2;count++{
atomic.AddInt64(&counter,1)
runtime.Goshed()
}
}

程序一开始创建两个goroutine形成对counter变量的竞争条件。而AddInt64函数则强制同一时刻 只能有一个goroutine运行并完成这个加法操作。当goroutine试图取d去调用任何原子函数时,这些goroutine都会自动根据所引用的变量做同步处理。

互斥锁

互斥锁就和我们通常使用的许多语言中的锁机制一样,在代码中创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区的代码。
对刚刚的程序使用锁机制同步就是这样的:

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
package main

import (
"fmt"
"runtime"
"sync"
"sync/atomic"
)
var (
counter int64
wg sync.WaitGroup
)

func main() {
wg.Add(2)

go incCounter(1)
go incCounter(2)

wg.Wait()

fmt.Println("Final Counter:",counter)
}


func incCounter(id int){
defer wg.Done()
for count :=0; count<2;count++{
mutex.Lock()
{
value := counter
runtime.Gosched()
value++
counter = value
}
mutex.UnLock()
}
}

同一时刻只能有一个goroutine可以进入临界区。之后,直到调用Unlock()函数之后,其他goroutine才能进入临界区。

通道

go语言中提供了通道,通过发送和接收需要共享的资源,在goroutine之间做同步。
通道分为无缓冲通道和有缓冲的通道。无缓冲的通道是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。

有缓冲的通道是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求goroutine之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值的时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间有一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换了;有缓冲的通道没有这种保证。

感悟

至此,对于goaction系列中对于go语言语法特性的部分,就算是阅读完毕了。书中举了很多这些语法的实际应用场景例子,但是实际掌握还是需要自己动手写以及多看看别人项目的源代码,个人感觉这样才能得到最快的提升。所以接下来的action章节读书笔记,就主要记录书中实际的demo吧。

go版本shadowsocks源码(一)

shadowsocks简介

Shadowsocks(简称SS)是一种基于Socks5代理方式的加密传输协议,也可以指实现这个协议的各种开发包。当前包使用Python、C、C++、C#、Go语言等编程语言开发,大部分主要实现(iOS平台的除外)采用Apache许可证、GPL、MIT许可证等多种自由软件许可协议开放源代码。Shadowsocks分为服务器端和客户端,在使用之前,需要先将服务器端部署到服务器上面,然后通过客户端连接并创建本地代理。

而socks5协议则是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。根据OSI七层模型来划分,SOCKS属于会话层协议,位于表示层与传输层之间。

当防火墙后的客户端要访问外部的服务器时,就跟socks代理服务器连接。该协议设计之初是为了让有权限的用户可以穿过过防火墙的限制,使得高权限用户可以访问外部资源。经过10余年的时间,大量的网络应用程序都支持socks5代理。


客户端

首先我们来看下客户端的代码。

main函数

go语言和大多数语言一样都是从main函数开始的,在源码阅读的过程我们也从main函数开始逐步往下分析。
mian函数整体我认为可以分为三个部分,第一部分读取相关的配置信息,第二部分处理服务器配置信息,第三部分启动客户端,监听本地端口。

读取客户端启动的配置信息

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
                       ......
flag.BoolVar(&printVer, "version", false, "print version")
flag.StringVar(&configFile, "c", "config.json", "specify config file")
flag.StringVar(&cmdServer, "s", "", "server address")
flag.StringVar(&cmdConfig.LocalAddress, "b", "", "local address, listen only to this address if specified")
flag.StringVar(&cmdConfig.Password, "k", "", "password")
flag.IntVar(&cmdConfig.ServerPort, "p", 0, "server port")
flag.IntVar(&cmdConfig.Timeout, "t", 300, "timeout in seconds")
flag.IntVar(&cmdConfig.LocalPort, "l", 0, "local socks5 proxy port")
flag.StringVar(&cmdConfig.Method, "m", "", "encryption method, default: aes-256-cfb")
flag.BoolVar((*bool)(&debug), "d", false, "print debug message")
flag.StringVar(&cmdURI, "u", "", "shadowsocks URI")
......

config, err := ss.ParseConfig(configFile)
if err != nil {
config = &cmdConfig
if !os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "error reading %s: %v\n", configFile, err)
os.Exit(1)
}
} else {
ss.UpdateConfig(config, &cmdConfig)
}
if config.Method == "" {
config.Method = "aes-256-cfb"
}

从上述代码中我们可以看到,除了从命令行读取配置信息外,还可以指定json文件读取配置信息。ss的配置信息包括这样几个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   Server       interface{} `json:"server"`
ServerPort int `json:"server_port"`
LocalPort int `json:"local_port"`
LocalAddress string `json:"local_address"`
Password string `json:"password"`
Method string `json:"method"` // encryption method

// following options are only used by server
PortPassword map[string]string `json:"port_password"`
Timeout int `json:"timeout"`

// following options are only used by client

// The order of servers in the client config is significant, so use array
// instead of map to preserve the order.
ServerPassword [][]string `json:"server_password"`

从上往下分为客户端配置和服务端配置,其中客户端配置包括远程服务器ip,服务器端口,本地端口,本地地址访问密码,加密方法,服务端配置包括设定端口密码等。
对于go语言来说从命令行读取相应的参数感觉是一件十分方便的事情
,只要调用flag包相关函数即可读取并规定想要的格式参数。

处理服务器配置信息

服务器配置信息这里使用了两个结构体来存储读取的配置信息。

1
2
3
4
5
6
7
8
9
type ServerCipher struct {
server string
cipher *ss.Cipher
}

var servers struct {
srvCipher []*ServerCipher
failCnt []int // failed connection count
}

其实阅读这里我是不太理解,为什么要在用一个匿名结构体将server密码信息包起来的,而且servercipher这个名字也怪怪的,因为里面不仅仅包含server的密码信息呀。可能是因为单独记录shi失败次数有利于后续扩展?这个后面想到在补吧。使用数组就很显然了,为了支持配置多个服务。

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
func parseServerConfig(config *ss.Config) {
hasPort := func(s string) bool {
_, port, err := net.SplitHostPort(s)
if err != nil {
return false
}
return port != ""
}

if len(config.ServerPassword) == 0 {
// only one encryption table
cipher, err := ss.NewCipher(config.Method, config.Password)
if err != nil {
log.Fatal("Failed generating ciphers:", err)
}
srvPort := strconv.Itoa(config.ServerPort)
srvArr := config.GetServerArray()
n := len(srvArr)
servers.srvCipher = make([]*ServerCipher, n)

for i, s := range srvArr {
if hasPort(s) {
log.Println("ignore server_port option for server", s)
servers.srvCipher[i] = &ServerCipher{s, cipher}
} else {
servers.srvCipher[i] = &ServerCipher{net.JoinHostPort(s, srvPort), cipher}
}
}
} else {
// multiple servers
n := len(config.ServerPassword)
servers.srvCipher = make([]*ServerCipher, n)

cipherCache := make(map[string]*ss.Cipher)
i := 0
for _, serverInfo := range config.ServerPassword {
if len(serverInfo) < 2 || len(serverInfo) > 3 {
log.Fatalf("server %v syntax error\n", serverInfo)
}
server := serverInfo[0]
passwd := serverInfo[1]
encmethod := ""
if len(serverInfo) == 3 {
encmethod = serverInfo[2]
}
if !hasPort(server) {
log.Fatalf("no port for server %s\n", server)
}
// Using "|" as delimiter is safe here, since no encryption
// method contains it in the name.
cacheKey := encmethod + "|" + passwd
cipher, ok := cipherCache[cacheKey]
if !ok {
var err error
cipher, err = ss.NewCipher(encmethod, passwd)
if err != nil {
log.Fatal("Failed generating ciphers:", err)
}
cipherCache[cacheKey] = cipher
}
servers.srvCipher[i] = &ServerCipher{server, cipher}
i++
}
}
servers.failCnt = make([]int, len(servers.srvCipher))
for _, se := range servers.srvCipher {
log.Println("available remote server", se.server)
}
return
}

处理服务器配置信息主要由parseServerConfig函数完成,老实说我不太喜欢这个函数,写的又臭又长看起来也很费劲,应该很有优化的空间的。
函数整体应该分为两个部分,用一个if分支隔离开来。if直接跟的语句负责处理单个服务器配置的信息,else跟的部分负责多个服务器配置信息的解析。整体流程如下:

开启go携程监听本地端口

run方法是比较简单的,使用tcp协议监听指定地址端口,采用轮询机制等待连接请求,一旦等到连接后开启一个goroutine调用 handleConnection()进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func run(listenAddr string) {
ln, err := net.Listen("tcp", listenAddr)
if err != nil {
log.Fatal(err)
}
log.Printf("starting local socks5 server at %v ...\n", listenAddr)
for {
conn, err := ln.Accept()
if err != nil {
log.Println("accept:", err)
continue
}
go handleConnection(conn)
}
}

##结语
至此,main函数的整体就差不多,接下来我们需要看下在拿到请求后,handleConnection函数是怎样对其进行处理的。

go语言读书笔记-action系列(方法与接口)

方法简单概述

go语言中,方法是用来给用户定义的类型添加新的行为。要想给类型添加方法,只需要在函数关键字func和函数名之间加一个参数,这个参数就被称为接收者,将函数与接收者的类型绑定在一起。函数有接收者就被称为方法。

1
2
3
4
5
6
7
8
type user struct {
name string
email string
}

func (u user) notify() {
fmt.Printf("hello world!")
}

ok,其实关于这一篇读书笔记我重点想记录的是接口,关于方法大概声明就是这样,使用的套路和java、php等各种面向对象的语言都是差不多。


接口

go语言中的接口,个人感觉和java,php中这类相比,与实现者之间的联系真的是十分薄弱,其不需要使用任何显式的关键字指明要实现该接口。只要类型绑定了同样的方法就认为其实现了该接口,就可以赋值给该接口类型。也就是说go中需要实现某个接口不需要显式的指明实现哪一个,想实现哪个接口直接编写该方法就可以,不想实现后就删除该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type notifier inteface {
notify()
}

type user struct{
name string
email string
}

func (u *user) notify(){
fmt.Printf("Sending user email to %s<%s>\n",u.name,u.email)
}
//这就可以说user类型实现了notifier 接口
func main(){
var n notifier
n = user{"bill"}
}


接口的内部实现

image_1d5llurtkgeq6kr1oln1gj0igdm.png-126.9kB

上图展示了在user类型值赋值后接口变量的值的内部布局。接口值是一个两个字长度的数据结构,第一个字包含一个指向内部表的指针。这个内部表叫iTable,包含了所存储的值的类型信息。iTable包含了已存储的值的类型信息以及与这个值相关联的一组方法。第二个字是一个指向所存储值的指针。

image_1d5lmui17tig1l7p1ec91bv31m4k2g.png-128.5kB

当把指针赋值给接口之后,类型信息会存储一个指向保存类型的指针,而接口值第二个字依旧保存指向实体值的指针。


方法集

方法集定义了接口的接受规则。当使用指针接收者来实现接口时,值类型则无法实现该接口。这是因为值类型的方法集只包括值接收者声明的方法,所以值类型无法实现指针接收者声明的方法。、

  • 以下是go语言规范里描述的方法集:
Values Methods
T (t T)
*T (t T) and (t *T)

T类型的值的方法集只包含值接收者声明的方法。而指向T类型的指针的方法集既包含值接收者声明的方法,也包含指针接收者声明的方法。

  • 从接收者类型角度来看方法集
Methods Receivers Values
T (t T)
*T (t T) and (t *T)

这个实际上和上面那个说的是同一件事,只不过换了个视角。如果使用指针接收者来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。如果使用值接收者来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。

总结

这一篇主要记录了,go中方法的简单使用以及初步了解了下接口内部的存储方式。对于接口其实我当初在看go圣经时就一直很疑惑,为什么要有值和指针方法集这种限制,为什么不可以值也传递给指针接收者。action 中也给出了回答—编译器并不是总能自动获得一个值的地址。这就好像不可能把值传递给一个行参为指针的函数一样,编译器并不总会帮我们主动获取到值的地址。

go语言读书笔记-action系列(类型)

用户定义的类型

实际上这里指的就是结构体,go语言中创建结构体的方式与c中基本大同小异,但是go会每次用上一个type 相当于给这个结构体一个命名,这一点不同于c中的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//go语言写法
type user strut {
name string
email string
ext int
privileged bool
}
//c语言写法
struct user {
char *name;
char *email;
int ext;
bool privileged;
}

当然c也可以用typedef这个关键字 给予别名使用。

但是go中的type 定义的类型会被认定为一个新的类型,比如:

1
type blade int

这个blade会被认为是全新的类型,将int类型赋值给它时会报错,c语言中就仅仅只是个别名的意思,赋值依然可以。

##内置类型
内置类型指的就是数值类型、字符串类型和布尔类型,也就是常说的int,string这类的。因为其是原始类型,所以对其进行增加或者删除都会创建新值,因为其底层结构不共享。
action中举了这样一个例子:

1
2
3
4
5
6
func Trim(s string, cutset string) string{
if s == "" || cutset == ""nc{
return s
}
return TrimFunc(s,makeCutsetFunc(cutset))
}

这个函数对调用者原始的string值的一个副本进行操作,并且返回一个新的string值做副本。字符串就像整数、浮点数和布尔值一样,本质上是一种很原始的数据值,所以在函数或方法内外传递时,要传递字符串的一份副本。

引用类型

go语言中的引用类型有:切片、映射、通道、接口、和函数类型。创建上述类型的变量是共享底层数据结构的。
�由于其底层共享结构,所以不需要通过指针来共享引用类型的值,而通过复制来传递引用。

1
2
3
4
5
6
7
8
9
func (ip IP) MarshalText() ([]byte,error){
if len(ip) == 0{
return []byte(""),nil
}
if len(ip) != IPV4len && len(ip) !=IPv6len{
return nil,erroes.New("invalid IP address")
}
return []byte(ip.String()),nil
}

以上代码,正如预期的那样通过复制来传递引用,从而不需要通过指针来共享引用类型的值。

总结

go语言中的类型大体上就分为这几类,在其作为方法的参数时,创建新的值就用值接收者,否则就用指针。这就是保持传递的一致性。内置类型与引用类型传递的时候,就需要把握其特性再根据传递原则决定如何使用,比如引用类型因为其底层共享结构,就不用传递指针,仅仅传递引用的副本就可以完成修改值的操作,而内置类型就需要指针。

go语言读书笔记-action系列(映射)

对于习惯了map这种表述方式的我,现在看来还是go中使用映射这个词,表达这一结构感觉更加精准点。

映射内部实现方式

映射的内部是无序的,因为其使用了散列表。
image_1d4rk5af2rkt1taiego1ied6jv9.png-156.1kB

映射的散列结构包含一组桶,所有的操作都要先选择一个桶。把操作映射时指定的键传递给映射的散列函数就可以选中对应的桶。
go语言生成散列键的过程如下:

  1. 这些字符串会转换为一个数值(散列值)。
  2. 这个数值落在映射已有桶的序号范围内表示一个可以用于存储的桶的序号。
  3. 最后这个数值被用来选择桶,用于存储或者查找指定的键值对。

在此需要强调的是,散列值的低位用来选择桶,高位用来区分不同的项。
对于桶的内部实现,感觉action中解释的并不是很好,尤其这个散列值高位的作用,解释感觉很难理解。
桶的内部实现使用两个数据结构实现。第一个是数组,内部存储的是用于选择桶的散列键的高位值(在对key/value对增删查的时候,先比较key的hash值高八位是否相等,然后再比较具体的key值,主要用来帮助区分寻找对应的key,不用每次都对key做全等判断)。第二个数据结构是一个字节数组,用于存储键值对。该字节数组存储了这个桶里所有的键,之后依次存储了这个桶里所有的值。

映射的扩容

action没有提到这一点,但是我觉得应该去探索下。
在桶中插入元素时,当桶填满后,将通过overflow指针来mallocgc一个新的bucket出来形成链表。
随着元素的增长,在桶链中寻找特定的key会变得效率低下,所以再插入的元素个数/桶达到阙值时(貌似设置为6.5),map会扩容,创建新的桶数组,长度为之前长度的两倍。

go语言读书笔记-action系列(切片)

go语言中的切片,感觉就像动态数组一样,自由的增删元素,更改大小。相比数组的固定大小,在编程中提供了不小的方便。不同于java中数据集合的各种抽象(map,set,list),以及三大基础结构底下的各个容器,go的集合表示很简单,就只有数组、切片、映射(当然比起php一切皆array逊色很多,但是我go快啊),感觉go应该是总结了各个语言的特性,总结除了这三种最具有普适性的结构,从而可以适应各种需要。

切片的内部实现

image_1d4rgl7v3tf2110v19hd80t1q1dp.png-66.2kB
在go中,切片可以看作对底层数组所做的抽象,切片的数据包含三个字段:

  • 指向数组的指针
  • 切片访问元素的个数
  • 允许增长到的元素个数(就是切片的容量,占用的内存空间)

通过这种结构,明显可以发现在使用切片时需要十分小心,因为其共享底层数组,可能一不小心就会干扰到其它切片的元素。
但是这种结构也使得切片在函数间传递时不用像数组那样传递过大的数据量,无论多大的切片都只有24个字节(指针8个,元素个数8个,容量8个),极大的提升了效率。

append

使用append可以向切片中增加元素:

func append(slice []Type, elems …Type) []Type

append 在切片容量足够时,直接将元素插入底层数组并更新切片访问元素的个数。切片的容量在不足够时append会自动生成新的切片增加容量,每次增加的容量是原来的两倍。但是当元素个数超过1000时,append容量的增长因子会被设为1.25。

创建切片的三个索引

在创建切片时,原来还可以使用第三个索引,第三个索引是用来控制切片的容量。

  • 为什么需要限制切片的容量?

因为切片存在多个切片共用一个数组的情况,有时可能会由于误操作影响到其他切片,所以设置了容量的情况下,在添加元素时就可以强制使得append为了增加容量申请新的底层数组,从而保证即使误操作也不会影响到其它切片。

go语言读书笔记-action系列(一个简单的go程序)

action系列的书感觉还是挺经典的,入手后直接跳过第一章介绍,从第二章开始。在第二章中,其主要以一个搜索的小demo来整体回顾了下go的基础语法,以及鲜明特性,通道,携程之类的,顺读下来感觉对于之前读圣经积累下来的基础是一个十分不错的回顾。

快速开始一个go程序

书中的demo是一个从不同数据源中进行搜索的程序,其实现了json与rss这两种格式的读取与搜索,程序的整体架构如下:
image_1d4ku4v57nbldgti1b6eb1aok9.png-144.6kB
程序的目录结构分为data,matchers,search三个部分,main一贯作为程序的主入口执行整个程序。
从中get到的新的点,感觉就是对于init的使用,预先在各个包中初始化matcher的类型,感觉就像传统面向对象的初始化工厂对象一样。以及对于接口的绑定感觉很有意思,像php,java等对于接口都要使用关键字implements强实现,而go不同,任何一个struct只要绑定了对应方法那么就可以视为其实现了该对象就可以对其赋值,这种弱关联性,对于习惯了php,java那种写法的方式一开始kennel会感觉很不适应但是习惯了,真的感觉很nice,程序写起来就有种用最少的组件做最多的事情的感觉,唯一不爽的就还是觉得代码可读性,没java那么的高,也可能是我习惯了java。

接口的赋值

对于一个绑定方法结构体,如果其传入是值的话,则其接口引用类型无论是值还是指针都可以直接调用方法。如果传入的是指针的话,就只可以在接口引用类型是指针的情况下进行方法调用。举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 方法声明为使用指向 defaultMatcher 类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过 interface 类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = dm // 将值赋值给接口类型 matcher.Search(feed, "test") // 使用值来调用接口方法
> go build
cannot use dm (type defaultMatcher) as type Matcher in assignment
// 方法声明为使用 defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过 interface 类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = &dm // 将指针赋值给接口类型 matcher.Search(feed, "test") // 使用指针来调用接口方法

TCP连接详解

三次握手:

(1)服务器必须准备好接受连接。通常通过调用socket、bind和listen这3个函数完成,这被称为被动打开。
(2)客户通过调用connect发起主动打开。这导致客户tcp发送一个syn(同步)分节,它会告诉服务器客户将在连接中发送的数据的初始序列号。通常syn分节不携带数据,其所在ip数据报hozhi只含有一个ip首部、一个tcp首部及可能有的tcp选项。
(3)服务器必须确认客户(ack acknowledge)d的syn,同事自己也要发送一个syn分节,它含有服务器将在同一个连接中发送的数据的初始序列号。服务器将在单个分节中发送syn和对客户syn的ack(确认)。
(4)客户必须确认服务器的syn哦。
这种交换需要三个分组,所以被称为tcp的三次握手。

四次挥手:

(1)某个应用进程首先调用close,我们称该端执行主动关闭。该端的TCP于是发送一个fin分节,表示数据发送完毕。
(2)接收到这个fin的对端执行被动关闭。这个fin由tcp确认。它的接收也作为一个文件结束符传递给接收端应用进程,fin的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
(3)一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的tcp也发送一个fin。
(4)接收这个最终fin的愿发送端tcp确认这个fin。

tcp连接过程中的状态转移

tcp为一个连接定义了11种状态,且tcp规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。
在此说明下:
image.png-510.5kB
对于一个经典的断开操作其示意图如下:
image.png-108.9kB

##TTIME_WAIT状态
一般执行主动关闭的一端会经历这个状态,该端停留在该状态的持续时间是最长分节生命期的两倍,2MSL。
TIME_WAIT状态有两个存在的理由:
(1)可靠地实现TCP全双工连接的终止;
(2)允许老的重复分节在网络中消逝。
第一个理由是,假设在四次挥手最后的ack丢失,那么发起端就必须重新发送ack。第二个则是,tcp必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。而tcp不会给处于time_wait状态的连接发起新的化身。而time_wait状态的持续时间是msl的2倍,所以无论哪个方向的分组将最多存活msl秒后即被丢弃,这样就可以保证每成功建议一个tcp连接时。来自该连接先前化身的老的重复分组都已在网络中消逝了。