1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        實戰(zhàn):150行Go實現(xiàn)高性能socks5代理

        共 7748字,需瀏覽 16分鐘

         ·

        2020-12-01 16:45


        光說不練假把式,不如上手試試,這篇來寫個有點卵用的東西。




        - TCP?Server -


        用 Go 實現(xiàn)一個 TCP Server 實在是太簡單了,什么?c10k problem、select、poll、epoll、kqueue、iocp、libevent,通通不需要(但為了通過面試你還是得去看呀),只需要這樣兩步:


        • 監(jiān)聽端口 1080(socks5的默認端口)

        • 每收到一個請求,啟動一個?goroutine 來處理它


        搭起這樣一個架子,實現(xiàn)一個 Hello world,大約需要 30 行代碼:

        func main() {  server, err := net.Listen("tcp", ":1080")  if err != nil {    fmt.Printf("Listen failed: %v\n", err)    return  }
        for { client, err := server.Accept() if err != nil { fmt.Printf("Accept failed: %v", err) continue } go process(client) }}
        func process(client net.Conn) { remoteAddr := client.RemoteAddr().String() fmt.Printf("Connection from %s\n", remoteAddr) client.Write([]byte("Hello world!\n")) client.Close()}




        - SOCKS5?-


        socks5 是?SOCKS Protocol Version?5 的縮寫,其規(guī)范定義于?RFC 1928[1],感興趣的同學可以自己去翻一翻。


        它是個二進制協(xié)議,不那么直觀,不過實際上非常簡單,主要分成三個步驟:

        • 認證

        • 建立連接

        • 轉(zhuǎn)發(fā)數(shù)據(jù)


        我們只需 16 行就能把 socks5 的架子搭起來:

        func process(client net.Conn) {  if err := Socks5Auth(client); err != nil {    fmt.Println("auth error:", err)    client.Close()    return  }
        target, err := Socks5Connect(client) if err != nil {????fmt.Println("connect?error:",?err) client.Close() return }
        Socks5Forward(client, target)}


        這樣一看是不是特別簡單?


        然后你只要把 Socks5Auth、Socks5Connect 和 Socks5Forward 給補上,一個完整的 socks5 代理就完成啦!是不是就像畫一匹馬一樣簡單?



        全文完 (不是)





        - Socks5Auth?-


        言歸正傳,socks5 協(xié)議規(guī)定,客戶端需要先開口:


        RFC 1928,首行是字段名,次行是字節(jié)數(shù)


        解釋一下:


        VER本次請求的協(xié)議版本號,取固定值 0x05(表示socks?5
        NMETHODS客戶端支持的認證方式數(shù)量,可取值 1~255
        METHODS可用的認證方式列表


        我們用如下代碼來讀取客戶端的發(fā)言:

        func Socks5Auth(client net.Conn) (err error) {  buf := make([]byte, 256)
        // 讀取 VER 和 NMETHODS n, err := io.ReadFull(client, buf[:2]) if n != 2 { return errors.New("reading header: " + err.Error()) }
        ver, nMethods := int(buf[0]), int(buf[1]) if ver != 5 { return errors.New("invalid version") }
        // 讀取 METHODS 列表 n, err = io.ReadFull(client, buf[:nMethods]) if n != nMethods { return errors.New("reading methods: " + err.Error()) } ??//TO?BE CONTINUED...


        然后服務端得選擇一種認證方式,告訴客戶端:


        VER也是0x05,對上?SOCKS 5 的暗號
        METHOD選定的認證方式;其中?0x00 表示不需要認證,0x02 是用戶名/密碼認證,……


        簡單起見我們就不認證了,給客戶端回復 0x05、0x00 即可:

          //無需認證  n, err = client.Write([]byte{0x05, 0x00})??if?n?!=?2?||?err?!=?nil?{    return errors.New("write rsp err: " + err.Error())  }
        return nil}


        以上 Socks5Auth 總共 28 行。




        - Socks5Connect?-


        在完成認證以后,客戶端需要告知服務端它的目標地址,協(xié)議具體要求為:



        VER0x05,老暗號了
        CMD
        連接方式,0x01=CONNECT, 0x02=BIND, 0x03=UDP ASSOCIATE
        RSV
        保留字段,現(xiàn)在沒卵用
        ATYP地址類型,0x01=IPv4,0x03=域名,0x04=IPv6
        DST.ADDR
        目標地址,細節(jié)后面講
        DST.PORT
        目標端口,2字節(jié),網(wǎng)絡字節(jié)序(network octec order)


        咱們先讀取前四個字段:

        func Socks5Connect(client net.Conn) (net.Conn, error) {  buf := make([]byte, 256)
        n, err := io.ReadFull(client, buf[:4]) if n != 4 { return nil, errors.New("read header: " + err.Error()) }
        ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3] if ver != 5 || cmd != 1 { return nil, errors.New("invalid ver/cmd") }
        //TO BE CONTINUED...

        注:BIND 和 UDP ASSOCIATE 這兩個 cmd 我們這里就先偷懶不支持了。


        接下來問題是如何讀取 DST.ADDR 和 DST.PORT。


        如前所述,ADDR 的格式取決于 ATYP:

        • 0x01:4個字節(jié),對應 IPv4 地址

        • 0x02:先來一個字節(jié) n 表示域名長度,然后跟著 n 個字節(jié)。注意這里不是 NUL 結(jié)尾的。

        • 0x03:16個字節(jié),對應 IPv6 地址


          addr := ""  switch atyp {  case 1:    n, err = io.ReadFull(client, buf[:4])    if n != 4 {      return nil, errors.New("invalid IPv4: " + err.Error())    }    addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])
        case 3: n, err = io.ReadFull(client, buf[:1]) if n != 1 { return nil, errors.New("invalid hostname: " + err.Error()) } addrLen := int(buf[0])
        n, err = io.ReadFull(client, buf[:addrLen]) if n != addrLen { return nil, errors.New("invalid hostname: " + err.Error()) } addr = string(buf[:addrLen])
        case 4: return nil, errors.New("IPv6: no supported yet")
        default: return nil, errors.New("invalid atyp") }

        注:這里再偷個懶,IPv6 也不管了。


        接著要讀取的 PORT 是一個 2 字節(jié)的無符號整數(shù)。


        需要注意的是,協(xié)議里說,這里用了?“network octec order” 網(wǎng)絡字節(jié)序,其實就是 BigEndian (還記得我們在 《UTF-8:一些好像沒什么用的冷知識》里講的小人國的故事嗎?)。別擔心,Golang 已經(jīng)幫我們準備了個 BigEndian 類型:

          n, err = io.ReadFull(client, buf[:2])  if n != 2 {    return nil, errors.New("read port: " + err.Error())  }  port := binary.BigEndian.Uint16(buf[:2])


        既然 ADDR 和 PORT 都就位了,我們馬上創(chuàng)建一個到 dst 的連接:

          destAddrPort := fmt.Sprintf("%s:%d", addr, port)  dest, err := net.Dial("tcp", destAddrPort)  if err != nil {    return nil, errors.New("dial dst: " + err.Error())  }


        最后一步是告訴客戶端,我們已經(jīng)準備好了,協(xié)議要求是:



        VER
        暗號,還是暗號!
        REP
        狀態(tài)碼,0x00=成功,0x01=未知錯誤,……
        RSV
        依然是沒卵用的 RESERVED
        ATYP
        地址類型
        BND.ADDR
        服務器和DST創(chuàng)建連接用的地址
        BND.PORT
        服務器和DST創(chuàng)建連接用的端口


        BND.ADDR/PORT 本應填入 dest.LocalAddr(),但因為基本上也沒甚卵用,我們就直接用 0 填充了:

          n, err = client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})  if err != nil {    dest.Close()    return nil, errors.New("write rsp: " + err.Error())  }
        return dest, nil}

        注:?ATYP =?0x01 表示?IPv4,所以需要填充 6 個 0 —— 4 for ADDR, 2 for PORT。


        這個函數(shù)加在一起有點長,整整用了 62 行,但其實也就這么回事,對吧?




        - Socks5Forward -


        萬事俱備,剩下的事情就是轉(zhuǎn)發(fā)、轉(zhuǎn)發(fā)、轉(zhuǎn)發(fā)。


        所謂“轉(zhuǎn)發(fā)”,其實就是從一頭讀,往另一頭寫。


        需要注意的是,由于 TCP 連接是雙工通信,我們需要創(chuàng)建兩個 goroutine,用于完成“雙工轉(zhuǎn)發(fā)”。


        由于 golang 有一個 io.Copy 用來做轉(zhuǎn)發(fā)的事情,代碼只要 9 行,簡單到難以形容:

        func Socks5Forward(client, target net.Conn) {  forward := func(src, dest net.Conn) {    defer src.Close()    defer dest.Close()    io.Copy(src, dest)  }  go forward(client, target)??go forward(target,?client)}

        注意:在發(fā)送完以后需要關(guān)閉連接。




        - 驗證 -


        把上面的代碼組裝起來,補上 "package main" 和必要的 import ,總共 145 行,一個能用的 socks5 代理服務器就成型了(完整代碼可參見[2])。


        上手跑起來:

        $?go?run?socks5_proxy.go


        發(fā)起代理訪問請求:

        $?curl?--proxy?"socks5://127.0.0.1:1080"?\https://job.toutiao.com/s/JxLbWby

        注:↑上面這個鏈接很有用,建議在瀏覽器里打開查看。


        代碼是沒啥問題了,不過標題里的?“高性能” 這個 flag 立得起來嗎?




        - 壓測?-


        說到壓測,自然就想到老牌工具 ab (apache benchmark),不過它只支持 http 代理,這就有點尷尬了。


        不過還好,開源的世界里什么都有,在?大型同性交友網(wǎng)站 Github 上,@cnlh 同學寫了個支持 socks5 代理的 benchmark 工具[3],馬上就可以燥起來:

        $ go get github.com/cnlh/benchmark


        由于代理本身不提供 http 服務,我們可以基于 gin 寫一個高性能的 http server:

        package main
        import "github.com/gin-gonic/gin"
        func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) r.Run(":8080")}


        跑起來

        $ go run http_server.go


        先對它進行一輪壓測,測試機是 Xeon 6130(16c32t) *2 + 376G RAM。


        簡單粗暴,直接上 c10k + 100w 請求:

        $ benchmark -c 10000 -n 1000000 \http://127.0.0.1:8080/ping
        Running?1000000?test?@?127.0.0.1:8080?by?10000?connections...1000000 requests in 10.57s, 115.59MB read, 42.38MB writeRequests/sec: 94633.20Transfer/sec: 14.95MBError : 0Percentage of the requests served within a certain time (ms)????50%???????????47????90%???????????299????95%???????????403????99%???????????608???100%???????????1722


        11 行代碼就能扛住 c10k problem,還做到了 94.6k QPS !



        不過由于并發(fā)量太大,導致 p99 需要 608ms;如果換成 1000 個并發(fā),QPS沒太大變化,p99 可以下降到 63ms。


        接下來該我們的 socks5 代理上場了:


        $ go run socks_proxy.go


        $?benchmark?-c?10000?-n?1000000?\-proxy?socks5://127.0.0.1:1080?\http://127.0.0.1:8080/ping
        Running?1000000?test?@?127.0.0.1:8080?by?10000?connections...1000000 requests in 11.47s, 115.59MB read, 42.38MB writeRequests/sec: 87220.83Transfer/sec: 13.78MBError : 0Percentage of the requests served within a certain time (ms)????50%???????????102????90%???????????318????95%???????????424????99%???????????649???100%???????????1848


        QPS 微降到 87.2k,p99 649ms 也不算顯著上漲;換成 1000 并發(fā),QPS?89.2k,p99 則下降到了 66ms —— 說明代理本身對請求性能的影響非常小(注:如果把 benchmark、http server、代理放在不同的機器上執(zhí)行,應該會看到更小的性能損耗)。


        標題里的 “高性能” 這個 flag 算是立住了。





        - 小結(jié)?-


        最后照例簡單總結(jié)下:


        • Go語言非常適合實現(xiàn)網(wǎng)絡服務,代碼短小精悍,性能強大

        • Socks 5 是一個簡單的二進制網(wǎng)絡代理協(xié)議

        • 網(wǎng)絡字節(jié)序?qū)嶋H上就是 BigEndian,大端存儲


        順便一提:實際上字節(jié)跳動早期的很多服務(比如今日頭條的Feed流服務)都是用 Python 實現(xiàn)的,由于性能的原因,我們在 2015?年開始用?Go 重構(gòu),并逐漸演化出了自研的微服務框架,感興趣的同學可以閱讀 InfoQ 的這篇《今日頭條Go建千億級微服務的實踐》。


        當然,想要進一步了解的話,最好的方式還是能直接看到這個微服務框架的源碼,并且實際上手用它?



        參考鏈接:


        1. RFC1928 - SOCKS Protocol Version 5

        https://tools.ietf.org/html/rfc1928


        2. Minimal socks5 proxy in Golang

        https://gist.github.com/felix021/7f9d05fa1fd9f8f62cbce9edbdb19253


        3. Benchmark by @cnlh

        https://github.com/cnlh/benchmark


        4.?今日頭條Go建千億級微服務的實踐

        https://mp.weixin.qq.com/s/CJL0Ttexvh7XT1zoNLOJrA



        推薦閱讀


        福利

        我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關(guān)注公眾號 「polarisxu」,回復?ebook?獲取;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。



        瀏覽 52
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品人妻人伦a 6 2v久动漫 骚虎导航 | 精品九九 | 97性潮久久久久久久久动漫 | 精品少妇一区二区三区免费观看 | 内射逼 | 五月天久久久 | 美女喷水视频 | 《色戒》未删减版在线 | 国产丝袜91久久久久久久久久久 | 瑶脱了内裤打开腿让人摸 |