Golang实现一个简单的http代理

本文详细介绍了golang 实现 http 代理的实现,在实际业务中有需求的同学可以学起来了!
代理是网络中的一项重要的功能,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站,对于客户端来说,代理扮演的是服务器的角色,接收请求报文,返回响应报文;对于 web 服务器来说,代理扮演的是客户端的角色,发送请求报文,接收响应报文。
代理具有多种类型,如果是根据网络用户划分的话,可以划分为正向代理和反向代理:
正向代理:将客户端作为网络用户。客户端访问服务端时,先访问代理服务器,随后代理服务器再访问服务端。此过程需客户端进行代理配置,对服务端透明。
反向代理:将服务端作为网络用户。访问过程与正向代理相同,不过此过程对客户端透明,需服务端进行代理配置(也可不配置)。
针对正向代理和反向代理,分别有不同的代理协议,即代理服务器和网络用户之间通信所使用的协议:
正向代理:
http
https
socks4
socks5
vpn:就功能而言,vpn 也可以被认为是代理
反向代理:
tcp
udp
http
https
接下来我们就说说 http 代理。
http 代理概述
http 代理是正向代理中较为简单的代理方式,它使用 http 协议作为客户端和代理服务器的传输协议。
http 代理可以承载 http 协议,https 协议,ftp 协议等等。对于不同的协议,客户端和代理服务器间的数据格式略有不同。
http 协议
我们先来看看 http 协议下客户端发送给代理服务器的 http header:
// 直接连接get / http/1.1host: staight.github.ioconnection: keep-alive// http 代理get http://staight.github.io/ http/1.1host: staight.github.ioproxy-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.1host: staight.github.io:443proxy-connection: keep-alive  
如上,https 协议和 http 协议相比:
请求方法从get变成connect
url 没有 protocol 字段
实际上,由于 https 下客户端和服务端的通信除了开头的协商以外都是密文,中间的代理服务器不再承担修改 http 报文再转发的功能,而是一开始就和客户端协商好服务端的地址,随后的 tcp 密文直接转发即可。
http 代理 https 协议交互过程如图:
代码实现
首先,创建 tcp 服务,并且对于每个 tcp 请求,均调用 handle 函数:
// tcp 连接,监听 8080 端口l, err := net.listen(tcp, :8080)if err != nil { log.panic(err)}// 死循环,每当遇到连接时,调用 handlefor { 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,urlfmt.sscanf(string(b[:bytes.indexbyte(b[:], '')]), %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)} else { //如果使用 http 协议,需将从客户端得到的 http 请求转发给服务端 server.write(b[:n])   }  
最后,将所有客户端的请求转发至服务端,将所有服务端的响应转发给客户端:
//将客户端的请求转发至服务端,将服务端的响应转发给客户端。io.copy 为阻塞函数,文件描述符不关闭就不停止go io.copy(server, client)   io.copy(client, server  
完整的源代码:
package mainimport ( 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, 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[:], '')]), %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) } else { //如果使用 http 协议,需将从客户端得到的 http 请求转发给服务端  server.write(b[:n]) } //将客户端的请求转发至服务端,将服务端的响应转发给客户端。io.copy 为阻塞函数,文件描述符不关闭就不停止 go io.copy(server, client) io.copy(client, server)}  
添加代理,然后运行:


季丰电子荣膺2023上海交大“未来领军”企业
智能楼宇快速发展背后凸显蓝牙Mesh解决方案
FPGA各电源定义:VCCINT、VCCIO、VCCAUX
我国迎来“5G引领”的历史性跨越,如何更好促进5G发展应用
华为新专利公布!将实现两台手机之间的屏幕共享,增强设备协同性
Golang实现一个简单的http代理
三星Galaxy S8/S8 Plus齐曝光 这么大的屏占比心动吗?
氧化镓产业化更进一步,本土初创企业成果涌现
黑鲨游戏手机2Pro开启预约 7月30日正式发布
华为鸿蒙一向死而生!
三星eUFS 3.1芯片开始量产 写入速度快了三倍
AI行业新风口下 早教机器人产业掀起了一波热潮
多页式或堆叠式分形面板分组
Admatis正在研发用于卫星的红外和磷光导航标记
热继电器的关键参数与选择准则
家用净水器哪个品牌的比较好?冷热即饮净水机,使用更方便
索尼ps5插图曝光 或为V型散热设计
虹科Dimetix激光测距传感器的优势
诺基亚第四款WP7新机曝光 定价350欧元
北交所4家公司发布业绩预告 有米科技华成智云都有两位数增长