<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          實戰(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é)議,不那么直觀,不過實際上非常簡單,主要分成三個步驟:

          • 認證

          • 建立連接

          • 轉發(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 結尾的。

          • 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 -


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


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


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


          由于 golang 有一個 io.Copy 用來做轉發(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ā)送完以后需要關閉連接。




          - 驗證 -


          把上面的代碼組裝起來,補上 "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 算是立住了。





          - 小結?-


          最后照例簡單總結下:


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

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

          • 網(wǎng)絡字節(jié)序實際上就是 BigEndian,大端存儲


          順便一提:實際上字節(jié)跳動早期的很多服務(比如今日頭條的Feed流服務)都是用 Python 實現(xiàn)的,由于性能的原因,我們在 2015?年開始用?Go 重構,并逐漸演化出了自研的微服務框架,感興趣的同學可以閱讀 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學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復?ebook?獲取;還可以回復「進群」,和數(shù)萬 Gopher 交流學習。



          瀏覽 52
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  微拍福利手机在线 | 特级黄色A片 | 波多野结衣无码精品一区 | 精品7777 | 婷婷丁香五月天影院亚洲综合桃花 |