前言:在實際項目中,常常會對一些非重要非緊急的數據採起網絡爬蟲手段抓取到本地,以達到節約成本的目的,可是站點對數據訪問地來源有頻率限制。不少碼農會使用網絡上免費的,多渠道的代理來解決頻率限制的問題,因爲是免費的,因此代理不是很穩定,這樣致使每一個工程須要花大量的時間和邏輯處理代理選擇,失敗重試的問題,並最終致使了應用代碼的複雜性。本文采用多級代理的方法,第一級代理解決全部問題,用戶只須要簡單的使用第一級的代理。
基本思路:開發一個代理的代理模塊,對應用層屏蔽掉上述問題。 下面是經驗和學習的總結git
理解正向代理與反向代理原理的區別是快速編碼的關鍵github
正向代理是知道真正的目標服務器,而反向代理是不知道的,覺得代理服務器就是真正的目標服務器
Go語言的傳輸層編程代碼很是的簡單,一行代碼就搞定。下面就是建立了一個端口監聽,從傳輸層層面接受來自網絡上的各類請求。編程
net.Listen("tcp", ":7856")
proxyUrl指向上面建立的監聽地址,格式:http://ip:port服務器
proxy, _ := url.Parse(proxyUrl) tr := &http.Transport{ Proxy: http.ProxyURL(proxy), TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: true, }).DialContext, } client := &http.Client{ Transport: tr, Timeout: 10 * time.Second, } ...... client.Do(req)
client.Do就會發起http的鏈接請求,這個過程當中有兩個步驟:網絡
一、與代理服務器創建TCP的鏈接。
二、發送HTTP鏈接請求(見 HTTP協議解析)
三次握手,Wireshark抓包以下:tcp
886 47.168484 192.168.0.105 X.X.X.X TCP 78 50707 → 7856[SYN] Seq=0 Win=65535 Len=0 MSS=1460 WS=64 TSval=104140878 TSecr=0 SACK_PERM=1 887 47.187710 X.X.X.X 192.168.0.105 TCP 74 7856 → 50707 [SYN, ACK] Seq=0 Ack=1 Win=28960 Len=0 MSS=1412 SACK_PERM=1 TSval=822200229 TSecr=104140878 WS=128 888 47.187803 192.168.0.105 X.X.X.X TCP 66 50707 → 7856 [ACK] Seq=1 Ack=1 Win=131584 Len=0 TSval=104140896 TSecr=822200229
至此,TCP通道已經打通(路基已經打好,就等着修什麼樣的道路)工具
TCP/IP協議族裏面的傳輸層通道已經打通,下面就是打通HTTP應用層的初始化包(路基在上一步打好,如今咱們鋪設的是跑卡車的道路)。學習
889 47.188292 192.168.0.105 120.24.69.155 HTTP 159 CONNECT w.mmm920.com:443 HTTP/1.1 893 53.023565 120.24.69.155 192.168.0.105 HTTP 105 HTTP/1.1 200 Connection established
客戶端到HTTP的鏈接創建只有兩步:測試
CONNECT w.XXXX.com:443 HTTP/1.1
請求HTTP/1.1 200 Connection established\r\n\r\n
數據包至此,客戶端到HTTP代理的鏈接創建成功編碼
if method == "CONNECT" { fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n") } else { log.Println("server write", method) //其它協議 server.Write(b[:n]) }
代理服務器到其它網絡上代理的鏈接邏輯:
c, err := net.DialTimeout("tcp", remote proxy addr, time.Second*5) req, err := http.NewRequest(http.MethodConnect, reqURL.String(), nil) req.Write(c) resp, err := http.ReadResponse(bufio.NewReader(c), req) if resp.StatusCode != 200 { err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode) return nil, err }
經過HTTP的 CONNECT協議判斷代理服務器是否當前可用,得到 net.Conn管道 c
進階篇
在第一級代理服務器和第二級代理服務器之間的傳輸層通道能夠考慮使用TCP鏈接池。由於第二級代理的服務器都是網絡上免費的代理,創建鏈接的成本比較高、也不穩定,所以一旦創建鏈接後,應該馬上覆用。同時帶來的一些風險也須要考慮,鏈接池的維護,對遠程代理服務器的壓力等
Wireshark是個好東西,特別是問題排查和TCP/IP協議分析學習的時候很是方便
代碼很是的簡潔,短短200行代碼,就實現了多級代理的功能,並且對於學習TCP/IP協議和HTTP協議鏈接過程很是的簡單明瞭。
涉及到機密信息,因此省去了refreshProxyAddr的邏輯。refreshProxyAddr就是更新代理ip地址池的邏輯,如測試用的話,能夠手動設置幾個ip,格式如:proxyUrls["http://x.x.x.x:3128"]=''
package main import ( "bufio" "bytes" "fmt" "io" "log" "net" "net/http" "net/url" "os" "runtime/debug" "strings" "sync" "time" "github.com/robfig/cron" ) var proxyUrls map[string]string = make(map[string]string) var choiseURL string var mu sync.Mutex var connHold map[string]net.Conn = make(map[string]net.Conn) //map[代理服務器url]tcp鏈接 func init() { log.SetFlags(log.LstdFlags | log.Lshortfile) refreshProxyAddr() cronTask := cron.New() cronTask.AddFunc("@every 1h", func() { mu.Lock() defer mu.Unlock() refreshProxyAddr() }) cronTask.Start() } func main() { l, err := net.Listen("tcp", ":7856") if err != nil { log.Panic(err) } for { client, err := l.Accept() if err != nil { log.Panic(err) } go handle(client) } } func handle(client net.Conn) { defer func() { if err := recover(); err != nil { log.Println(err) debug.PrintStack() } }() if client == nil { return } log.Println("client tcp tunnel connection:", client.LocalAddr().String(), "->", client.RemoteAddr().String()) // client.SetDeadline(time.Now().Add(time.Duration(10) * time.Second)) defer client.Close() var b [1024]byte n, err := client.Read(b[:]) //讀取應用層的全部數據 if err != nil || bytes.IndexByte(b[:], '\n') == -1 { log.Println(err) //傳輸層的鏈接是沒有應用層的內容 好比:net.Dial() return } var method, host, address string fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &host) log.Println(method, host) hostPortURL, err := url.Parse(host) if err != nil { log.Println(err) return } if hostPortURL.Opaque == "443" { //https訪問 address = hostPortURL.Scheme + ":443" } else { //http訪問 if strings.Index(hostPortURL.Host, ":") == -1 { //host不帶端口, 默認80 address = hostPortURL.Host + ":80" } else { address = hostPortURL.Host } } server, err := Dial("tcp", address) if err != nil { log.Println(err) return } //在應用層完成數據轉發後,關閉傳輸層的通道 defer server.Close() log.Println("server tcp tunnel connection:", server.LocalAddr().String(), "->", server.RemoteAddr().String()) // server.SetDeadline(time.Now().Add(time.Duration(10) * time.Second)) if method == "CONNECT" { fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n") } else { log.Println("server write", method) //其它協議 server.Write(b[:n]) } //進行轉發 go func() { io.Copy(server, client) }() io.Copy(client, server) //阻塞轉發 } //refreshProxyAddr 刷新代理ip func refreshProxyAddr() { var proxyUrlsTmp map[string]string = make(map[string]string) \\獲取代理ip地址邏輯 proxyUrls = proxyUrlsTmp //能夠手動設置測試代理ip } //DialSimple 直接經過發送數據報與二級代理服務器創建鏈接 func DialSimple(network, addr string) (net.Conn, error) { var proxyAddr string for proxyAddr = range proxyUrls { //隨機獲取一個代理地址 break } c, err := func() (net.Conn, error) { u, _ := url.Parse(proxyAddr) log.Println("代理host", u.Host) // Dial and create client connection. c, err := net.DialTimeout("tcp", u.Host, time.Second*5) if err != nil { log.Println(err) return nil, err } _, err = c.Write([]byte("CONNECT w.xxxx.com:443 HTTP/1.1\r\n Host: w.xxxx.com:443\r\n User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.3\r\n\r\n"))// w.xxxx.com:443 替換成實際的地址 if err != nil { panic(err) } c.Write([]byte(`GET www.baidu.com HTTP/1.1\r\n\r\n`)) io.Copy(os.Stdout, c) return c, err }() return c, err } //Dial 創建一個傳輸通道 func Dial(network, addr string) (net.Conn, error) { var proxyAddr string for proxyAddr = range proxyUrls { //隨機獲取一個代理地址 break } //創建到代理服務器的傳輸層通道 c, err := func() (net.Conn, error) { u, _ := url.Parse(proxyAddr) log.Println("代理地址", u.Host) // Dial and create client connection. c, err := net.DialTimeout("tcp", u.Host, time.Second*5) if err != nil { return nil, err } reqURL, err := url.Parse("http://" + addr) if err != nil { return nil, err } req, err := http.NewRequest(http.MethodConnect, reqURL.String(), nil) if err != nil { return nil, err } req.Close = false req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.3") err = req.Write(c) if err != nil { return nil, err } resp, err := http.ReadResponse(bufio.NewReader(c), req) if err != nil { return nil, err } defer resp.Body.Close() log.Println(resp.StatusCode, resp.Status, resp.Proto, resp.Header) if resp.StatusCode != 200 { err = fmt.Errorf("Connect server using proxy error, StatusCode [%d]", resp.StatusCode) return nil, err } return c, err }() if c == nil || err != nil { //代理異常 log.Println("代理異常:", c, err) log.Println("本地直接轉發:", c, err) return net.Dial(network, addr) } log.Println("代理正常,tunnel信息", c.LocalAddr().String(), "->", c.RemoteAddr().String()) return c, err }