Golang 实现一个简单的 http 代理

推荐大家关注一个公众号

点击上方 "编程技术圈"关注, 星标或置顶一起成长

后台回复“大礼包”有惊喜礼包!

日英文

Alter what is changeable, and accept what is mutable, don't let yourself become unacceptable.

改变能改变的,接受不能改变的,别让自己成为不可接受的。

每日掏心话

岁月缠绕在指缝里,温和的流走,一季的悲伤,徜徉在心湖,如若,仰望,便如七月明媚忧伤。
责编:乐乐 | 来自:staight | 链接:http://t.hk.uy/B4p

编程技术圈(ID:study_tech)第 1298次推文

往日回顾:在 Win11 上打开 C 盘后:界面全变了

     

   正文   

本文详细介绍了Golang 实现 http 代理的实现,在实际业务中有需求的同学可以学起来了!

代理是网络中的一项重要的功能,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站,对于客户端来说,代理扮演的是服务器的角色,接收请求报文,返回响应报文;对于 web 服务器来说,代理扮演的是客户端的角色,发送请求报文,接收响应报文。
代理具有多种类型,如果是根据网络用户划分的话,可以划分为正向代理和反向代理:
正向代理:将客户端作为网络用户。客户端访问服务端时,先访问代理服务器,随后代理服务器再访问服务端。此过程需客户端进行代理配置,对服务端透明。反向代理:将服务端作为网络用户。访问过程与正向代理相同,不过此过程对客户端透明,需服务端进行代理配置(也可不配置)。针对正向代理和反向代理,分别有不同的代理协议,即代理服务器和网络用户之间通信所使用的协议:
正向代理:httphttpssocks4socks5vpn:就功能而言,vpn 也可以被认为是代理反向代理:tcpudphttphttps接下来我们就说说 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变成CONNECTurl 没有 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)
}
添加代理,然后运行:

添加代理
运行你还有什么想要补充的吗?
PS:欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

欢迎加入后端架构师交流群,在后台回复“学习”即可。

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。
别找了,想获取史上最简单的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了


猜你还想看
阿里、腾讯、百度、华为、京东最新面试题汇集
软件外包公司入职须知

彻底搞懂 Nginx 的五大应用场景

感受 lambda 之美!

嘿,你在看吗?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值