本文詳細(xì)介紹了Golang 實(shí)現(xiàn) http 代理的實(shí)現(xiàn),在實(shí)際業(yè)務(wù)中有需求的同學(xué)可以學(xué)起來(lái)了!
代理是網(wǎng)絡(luò)中的一項(xiàng)重要的功能,其功能就是代理網(wǎng)絡(luò)用戶去取得網(wǎng)絡(luò)信息。形象的說(shuō):它是網(wǎng)絡(luò)信息的中轉(zhuǎn)站,對(duì)于客戶端來(lái)說(shuō),代理扮演的是服務(wù)器的角色,接收請(qǐng)求報(bào)文,返回響應(yīng)報(bào)文;對(duì)于 web 服務(wù)器來(lái)說(shuō),代理扮演的是客戶端的角色,發(fā)送請(qǐng)求報(bào)文,接收響應(yīng)報(bào)文。
代理具有多種類型,如果是根據(jù)網(wǎng)絡(luò)用戶劃分的話,可以劃分為正向代理和反向代理:
- 正向代理:將客戶端作為網(wǎng)絡(luò)用戶。客戶端訪問(wèn)服務(wù)端時(shí),先訪問(wèn)代理服務(wù)器,隨后代理服務(wù)器再訪問(wèn)服務(wù)端。此過(guò)程需客戶端進(jìn)行代理配置,對(duì)服務(wù)端透明。
- 反向代理:將服務(wù)端作為網(wǎng)絡(luò)用戶。訪問(wèn)過(guò)程與正向代理相同,不過(guò)此過(guò)程對(duì)客戶端透明,需服務(wù)端進(jìn)行代理配置(也可不配置)。
針對(duì)正向代理和反向代理,分別有不同的代理協(xié)議,即代理服務(wù)器和網(wǎng)絡(luò)用戶之間通信所使用的協(xié)議:
-
正向代理:
- http
- https
- socks4
- socks5
- vpn:就功能而言,vpn 也可以被認(rèn)為是代理
-
反向代理:
- tcp
- udp
- http
- https
接下來(lái)我們就說(shuō)說(shuō) http 代理。
http 代理概述
http 代理是正向代理中較為簡(jiǎn)單的代理方式,它使用 http 協(xié)議作為客戶端和代理服務(wù)器的傳輸協(xié)議。
http 代理可以承載 http 協(xié)議,https 協(xié)議,ftp 協(xié)議等等。對(duì)于不同的協(xié)議,客戶端和代理服務(wù)器間的數(shù)據(jù)格式略有不同。
http 協(xié)議
我們先來(lái)看看 http 協(xié)議下客戶端發(fā)送給代理服務(wù)器的 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
keep-alive :
可以看到,http 代理比起直接連接:
- url 變成完整路徑,/->http://staight.github.io/
- Connection字段變成Proxy-Connection字段
- 其余保持原樣
為什么使用完整路徑?
為了識(shí)別目標(biāo)服務(wù)器。如果沒(méi)有完整路徑,且沒(méi)有 Host 字段的話,代理服務(wù)器將無(wú)法得知目標(biāo)服務(wù)器的地址。
為什么使用 Proxy-Connection 字段代替 Connection 字段?
為了兼容使用 HTTP/1.0 協(xié)議的過(guò)時(shí)的代理服務(wù)器。HTTP/1.1 才開(kāi)始有長(zhǎng)連接功能,直接連接的情況下,客戶端發(fā)送的 HTTP Header 中如果有Connection: keep-alive字段,表示使用長(zhǎng)連接和服務(wù)端進(jìn)行 http 通信,但如果中間有過(guò)時(shí)的代理服務(wù)器,該代理服務(wù)器將無(wú)法與客戶端和服務(wù)端進(jìn)行長(zhǎng)連接,造成客戶端和服務(wù)端一直等待,白白浪費(fèi)時(shí)間。
因此使用Proxy-Connection字段代替Connection字段,如果代理服務(wù)器使用 HTTP/1.1 協(xié)議,能夠識(shí)別Proxy-Connection字段,則將該字段轉(zhuǎn)換成Connection再發(fā)送給服務(wù)端;如果不能識(shí)別,直接發(fā)送給服務(wù)端,因?yàn)榉?wù)端也無(wú)法識(shí)別,則使用短連接進(jìn)行通信。
http 代理 http 協(xié)議交互過(guò)程如圖:
https 協(xié)議
接下來(lái)我們來(lái)看看 https 協(xié)議下,客戶端發(fā)送給代理服務(wù)器的 HTTP Header:
CONNECT staight.github.io:443 HTTP/1.1
Host: staight.github.io:443
Proxy-Connection: keep-alive
如上,https 協(xié)議和 http 協(xié)議相比:
- 請(qǐng)求方法從GET變成CONNECT
- url 沒(méi)有 protocol 字段
實(shí)際上,由于 https 下客戶端和服務(wù)端的通信除了開(kāi)頭的協(xié)商以外都是密文,中間的代理服務(wù)器不再承擔(dān)修改 http 報(bào)文再轉(zhuǎn)發(fā)的功能,而是一開(kāi)始就和客戶端協(xié)商好服務(wù)端的地址,隨后的 tcp 密文直接轉(zhuǎn)發(fā)即可。
http 代理 https 協(xié)議交互過(guò)程如圖:
代碼實(shí)現(xiàn)
首先,創(chuàng)建 tcp 服務(wù),并且對(duì)于每個(gè) tcp 請(qǐng)求,均調(diào)用 handle 函數(shù):
// tcp 連接,監(jiān)聽(tīng) 8080 端口
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}
// 死循環(huán),每當(dāng)遇到連接時(shí),調(diào)用 handle
for {
client, err := l.Accept()
if err != nil {
log.Panic(err)
}
go handle(client)
}
然后將獲取的數(shù)據(jù)放入緩沖區(qū):
// 用來(lái)存放客戶端數(shù)據(jù)的緩沖區(qū)
var b [1024]byte
//從客戶端獲取數(shù)據(jù)
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
從緩沖區(qū)讀取 HTTP 請(qǐng)求方法,URL 等信息:
var method, URL, address string
// 從客戶端數(shù)據(jù)讀入 method,url
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], ' ')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil {
log.Println(err)
return
}
http 協(xié)議和 https 協(xié)議獲取地址的方式不同,分別處理:
// 如果方法是 CONNECT,則為 https 協(xié)議
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else { //否則為 http 協(xié)議
address = hostPortURL.Host
// 如果 host 不帶端口,則默認(rèn)為 80
if strings.Index(hostPortURL.Host, ":") == -1 { //host 不帶端口, 默認(rèn) 80
address = hostPortURL.Host + ":80"
}
}
用獲取到的地址向服務(wù)端發(fā)起請(qǐng)求。如果是 http 協(xié)議,將客戶端的請(qǐng)求直接轉(zhuǎn)發(fā)給服務(wù)端;如果是 https 協(xié)議,發(fā)送 http 響應(yīng):
//獲得了請(qǐng)求的 host 和 port,向服務(wù)端發(fā)起 tcp 連接
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
//如果使用 https 協(xié)議,需先向客戶端表示連接建立完畢
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established ")
} else { //如果使用 http 協(xié)議,需將從客戶端得到的 http 請(qǐng)求轉(zhuǎn)發(fā)給服務(wù)端
server.Write(b[:n])
}
最后,將所有客戶端的請(qǐng)求轉(zhuǎn)發(fā)至服務(wù)端,將所有服務(wù)端的響應(yīng)轉(zhuǎn)發(fā)給客戶端:
//將客戶端的請(qǐng)求轉(zhuǎn)發(fā)至服務(wù)端,將服務(wù)端的響應(yīng)轉(zhuǎn)發(fā)給客戶端。io.Copy 為阻塞函數(shù),文件描述符不關(guān)閉就不停止
go io.Copy(server, client)
io.Copy(client, server
完整的源代碼:
package main
import (
"bytes"
"fmt"
"io"
"log"
"net"
"net/url"
"strings"
)
func main() {
// tcp 連接,監(jiān)聽(tīng) 8080 端口
l, err := net.Listen("tcp", ":8080")
if err != nil {
log.Panic(err)
}
// 死循環(huán),每當(dāng)遇到連接時(shí),調(diào)用 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 ", client.RemoteAddr())
// 用來(lái)存放客戶端數(shù)據(jù)的緩沖區(qū)
var b [1024]byte
//從客戶端獲取數(shù)據(jù)
n, err := client.Read(b[:])
if err != nil {
log.Println(err)
return
}
var method, URL, address string
// 從客戶端數(shù)據(jù)讀入 method,url
fmt.Sscanf(string(b[:bytes.IndexByte(b[:], ' ')]), "%s%s", &method, &URL)
hostPortURL, err := url.Parse(URL)
if err != nil {
log.Println(err)
return
}
// 如果方法是 CONNECT,則為 https 協(xié)議
if method == "CONNECT" {
address = hostPortURL.Scheme + ":" + hostPortURL.Opaque
} else { //否則為 http 協(xié)議
address = hostPortURL.Host
// 如果 host 不帶端口,則默認(rèn)為 80
if strings.Index(hostPortURL.Host, ":") == -1 { //host 不帶端口, 默認(rèn) 80
address = hostPortURL.Host + ":80"
}
}
//獲得了請(qǐng)求的 host 和 port,向服務(wù)端發(fā)起 tcp 連接
server, err := net.Dial("tcp", address)
if err != nil {
log.Println(err)
return
}
//如果使用 https 協(xié)議,需先向客戶端表示連接建立完畢
if method == "CONNECT" {
fmt.Fprint(client, "HTTP/1.1 200 Connection established ")
} else { //如果使用 http 協(xié)議,需將從客戶端得到的 http 請(qǐng)求轉(zhuǎn)發(fā)給服務(wù)端
server.Write(b[:n])
}
//將客戶端的請(qǐng)求轉(zhuǎn)發(fā)至服務(wù)端,將服務(wù)端的響應(yīng)轉(zhuǎn)發(fā)給客戶端。io.Copy 為阻塞函數(shù),文件描述符不關(guān)閉就不停止
go io.Copy(server, client)
io.Copy(client, server)
}
添加代理,然后運(yùn)行:
原文標(biāo)題:Golang 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 http 代理
文章出處:【微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
-
服務(wù)器
+關(guān)注
關(guān)注
13文章
9795瀏覽量
88009 -
HTTP
+關(guān)注
關(guān)注
0文章
525瀏覽量
33534 -
代理
+關(guān)注
關(guān)注
1文章
44瀏覽量
11343
原文標(biāo)題:Golang 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 http 代理
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
評(píng)論