Golang 實現一個簡單的 http 代理
本文詳細介紹了Golang 實現 http 代理的實現,在實際業務中有需求的同學可以學起來了!
代理是網絡中的一項重要的功能,其功能就是代理網絡用戶去取得網絡信息。形象的說:它是網絡信息的中轉站,對于客戶端來說,代理扮演的是服務器的角色,接收請求報文,返回響應報文;對于 web 服務器來說,代理扮演的是客戶端的角色,發送請求報文,接收響應報文。
代理具有多種類型,如果是根據網絡用戶劃分的話,可以劃分為正向代理和反向代理:
- 正向代理:將客戶端作為網絡用戶。客戶端訪問服務端時,先訪問代理服務器,隨后代理服務器再訪問服務端。此過程需客戶端進行代理配置,對服務端透明。
- 反向代理:將服務端作為網絡用戶。訪問過程與正向代理相同,不過此過程對客戶端透明,需服務端進行代理配置(也可不配置)。
針對正向代理和反向代理,分別有不同的代理協議,即代理服務器和網絡用戶之間通信所使用的協議:
- 正向代理:
- http
- https
- socks4
- socks5
- 反向代理:
- tcp
- udp
- http
- https
接下來我們就說說 http 代理。
http 代理概述
http 代理是正向代理中較為簡單的代理方式,它使用 http 協議作為客戶端和代理服務器的傳輸協議。
http 代理可以承載 http 協議,https 協議,ftp 協議等等。對于不同的協議,客戶端和代理服務器間的數據格式略有不同。
http 協議
我們先來看看 http 協議下客戶端發送給代理服務器的 HTTP Header:
- // 直接連接
- GET / HTTP/1.1
- Host: staight.github.io
- Connection: keep-alive
- // http 代理
- GET http://staight.github.io/ HTTP/1.1
- Host: staight.github.io
- Proxy-Connection: keep-alive
可以看到,http 代理比起直接連接:
- url 變成完整路徑,/->http://staight.github.io/
- Connection字段變成Proxy-Connection字段
- 其余保持原樣
為什么使用完整路徑?
為了識別目標服務器。如果沒有完整路徑,且沒有 Host 字段的話,代理服務器將無法得知目標服務器的地址。
為什么使用 Proxy-Connection 字段代替 Connection 字段?
為了兼容使用 HTTP/1.0 協議的過時的代理服務器。HTTP/1.1 才開始有長連接功能,直接連接的情況下,客戶端發送的 HTTP Header 中如果有Connection: keep-alive字段,表示使用長連接和服務端進行 http 通信,但如果中間有過時的代理服務器,該代理服務器將無法與客戶端和服務端進行長連接,造成客戶端和服務端一直等待,白白浪費時間。因此使用Proxy-Connection字段代替Connection字段,如果代理服務器使用 HTTP/1.1 協議,能夠識別Proxy-Connection字段,則將該字段轉換成Connection再發送給服務端;如果不能識別,直接發送給服務端,因為服務端也無法識別,則使用短連接進行通信。
http 代理 http 協議交互過程如圖:
http 代理 http 協議
https 協議
接下來我們來看看 https 協議下,客戶端發送給代理服務器的 HTTP Header:
- CONNECT staight.github.io:443 HTTP/1.1
- Host: staight.github.io:443
- Proxy-Connection: keep-alive
如上,https 協議和 http 協議相比:
- 請求方法從GET變成CONNECT
- url 沒有 protocol 字段
實際上,由于 https 下客戶端和服務端的通信除了開頭的協商以外都是密文,中間的代理服務器不再承擔修改 http 報文再轉發的功能,而是一開始就和客戶端協商好服務端的地址,隨后的 tcp 密文直接轉發即可。
http 代理 https 協議交互過程如圖:
http 代理 https 協議
代碼實現
首先,創建 tcp 服務,并且對于每個 tcp 請求,均調用 handle 函數:
- // tcp 連接,監聽 8080 端口
- l, err := net.Listen("tcp", ":8080")
- if err != nil {
- log.Panic(err)
- }
- // 死循環,每當遇到連接時,調用 handle
- for {
- client, err := l.Accept()
- if err != nil {
- log.Panic(err)
- }
- go handle(client)
- }
- 然后將獲取的數據放入緩沖區:
- // 用來存放客戶端數據的緩沖區
- var b [1024]byte
- //從客戶端獲取數據
- n, err := client.Read(b[:])
- if err != nil {
- log.Println(err)
- return
- }
從緩沖區讀取 HTTP 請求方法,URL 等信息:
- var method, URL, address string
- // 從客戶端數據讀入 method,url
- fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &URL)
- hostPortURL, err := url.Parse(URL)
- if err != nil {
- log.Println(err)
- return
- }
http 協議和 https 協議獲取地址的方式不同,分別處理:
- // 如果方法是 CONNECT,則為 https 協議
- if method == "CONNECT" {
- address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
- } else { //否則為 http 協議
- address = hostPortURL.Host
- // 如果 host 不帶端口,則默認為 80
- if strings.Index(hostPortURL.Host, ":") == -1 { //host 不帶端口, 默認 80
- address = hostPortURL.Host + ":80"
- }
- }
用獲取到的地址向服務端發起請求。如果是 http 協議,將客戶端的請求直接轉發給服務端;如果是 https 協議,發送 http 響應:
- //獲得了請求的 host 和 port,向服務端發起 tcp 連接
- server, err := net.Dial("tcp", address)
- if err != nil {
- log.Println(err)
- return
- }
- //如果使用 https 協議,需先向客戶端表示連接建立完畢
- if method == "CONNECT" {
- fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
- } else { //如果使用 http 協議,需將從客戶端得到的 http 請求轉發給服務端
- server.Write(b[:n])
- }
最后,將所有客戶端的請求轉發至服務端,將所有服務端的響應轉發給客戶端:
- //將客戶端的請求轉發至服務端,將服務端的響應轉發給客戶端。io.Copy 為阻塞函數,文件描述符不關閉就不停止
- go io.Copy(server, client)
- io.Copy(client, server
完整的源代碼:
- package main
- import (
- "bytes"
- "fmt"
- "io"
- "log"
- "net"
- "net/url"
- "strings"
- )
- func main() {
- // tcp 連接,監聽 8080 端口
- l, err := net.Listen("tcp", ":8080")
- if err != nil {
- log.Panic(err)
- }
- // 死循環,每當遇到連接時,調用 handle
- for {
- client, err := l.Accept()
- if err != nil {
- log.Panic(err)
- }
- go handle(client)
- }
- }
- func handle(client net.Conn) {
- if client == nil {
- return
- }
- defer client.Close()
- log.Printf("remote addr: %v\n", client.RemoteAddr())
- // 用來存放客戶端數據的緩沖區
- var b [1024]byte
- //從客戶端獲取數據
- n, err := client.Read(b[:])
- if err != nil {
- log.Println(err)
- return
- }
- var method, URL, address string
- // 從客戶端數據讀入 method,url
- fmt.Sscanf(string(b[:bytes.IndexByte(b[:], '\n')]), "%s%s", &method, &URL)
- hostPortURL, err := url.Parse(URL)
- if err != nil {
- log.Println(err)
- return
- }
- // 如果方法是 CONNECT,則為 https 協議
- if method == "CONNECT" {
- address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
- } else { //否則為 http 協議
- address = hostPortURL.Host
- // 如果 host 不帶端口,則默認為 80
- if strings.Index(hostPortURL.Host, ":") == -1 { //host 不帶端口, 默認 80
- address = hostPortURL.Host + ":80"
- }
- }
- //獲得了請求的 host 和 port,向服務端發起 tcp 連接
- server, err := net.Dial("tcp", address)
- if err != nil {
- log.Println(err)
- return
- }
- //如果使用 https 協議,需先向客戶端表示連接建立完畢
- if method == "CONNECT" {
- fmt.Fprint(client, "HTTP/1.1 200 Connection established\r\n\r\n")
- } else { //如果使用 http 協議,需將從客戶端得到的 http 請求轉發給服務端
- server.Write(b[:n])
- }
- //將客戶端的請求轉發至服務端,將服務端的響應轉發給客戶端。io.Copy 為阻塞函數,文件描述符不關閉就不停止
- go io.Copy(server, client)
- io.Copy(client, server)
- }
添加代理,然后運行:
添加代理
運行