代理是网络中的一项重要的功能,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站,对于客户端来说,代理扮演的是服务器的角色,接收请求报文,返回响应报文;对于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.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协议交互过程如图:

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协议交互过程如图:

代码实现

首先,创建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)
}

添加代理,然后运行:

运行成功!

参考文档

HTTP 代理原理及实现(一):https://imququ.com/post/web-proxy.html

Http 请求头中的 Proxy-Connection:https://imququ.com/post/the-proxy-connection-header-in-http-request.html

golang实现一个简单的http代理的更多相关文章

  1. 一个简单的tcp代理实现

    There are a number of reasons to have a TCP proxy in your tool belt, bothfor forwarding traffic to b ...

  2. 使用 TUN 设备实现一个简单的 UDP 代理隧道

    若要实现在 Linux 下的代理程序,方法有很多,比如看着 RFC 1928 来实现一个 socks5 代理并自行设置程序经过 socks5 代理等方式,下文是使用 Linux 提供的 tun/tap ...

  3. 一个简单JDK动态代理的实例

    动态代理的步骤: 创建一个实现了InvocationHandler接口的类,必须重写接口里的invoke()方法. 创建被代理的类和接口 通过Proxy的静态方法 newProxyInsatance( ...

  4. 一个简单 JDK 动态代理的实例

    动态代理的步骤: 创建一个实现了 InvocationHandler 接口的类,必须重写接口里的 invoke()方法. 创建被代理的类和接口 通过 Proxy 的静态方法 newProxyInsat ...

  5. 参考MySQL Internals手册,使用Golang写一个简单解析binlog的程序

    GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. MySQL作为最流行的开源关系型数据库,有大量的拥趸.其生态已经相当完善,各项特性在圈内都有大量研究.每次新特性发布,都会 ...

  6. golang开发一个简单的grpc

    0.1.索引 https://waterflow.link/articles/1665674508275 1.什么是grpc 在 gRPC 中,客户端应用程序可以直接调用不同机器上的服务器应用程序上的 ...

  7. golang实现一个简单的websocket聊天室

    基本原理: 1.引入了 golang.org/x/net/websocket 包. 2.监听端口. 3.客户端连接时,发送结构体: {"type":"login" ...

  8. golang 创建一个简单的连接池,减少频繁的创建与关闭

    一.连接池的描述图片如下: 二.连接池代码如下: package main; import ( "time" "sync" "errors" ...

  9. golang 创建一个简单的资源池,重用资源,减少GC负担

    package main; import ( "sync" "errors" "fmt" ) //代码参考<Go语言实战>中第7 ...

随机推荐

  1. 【多服务场景化解决方案】智能家居(UrbanHome)

    ​ 介绍 UrbanHome是一款提供房屋维修服务的移动应用.如有维修需求,用户可通过该应用联系所在城市的管道工,电工,保洁,漆匠,木匠,修理工等,或是搜寻导航附近的维修商店. 通过构建UrbanHo ...

  2. KingbaseES 全局索引是否因为DDL操作而变为Unusable ?

    前言 Oracle 在对分区做DDL操作时,会使分区全局索引失效,需要加上关键字update global indexes.KingbaseES 同样支持全局索引.那么,如果对分区表进行DDL操作,那 ...

  3. KingbaseES 数据库连接断开问题排查思路

    用户在使用数据库过程中,经常会发现如果会话空闲一段时间,会话有可能断开,需要重连.这个问题影响因素很多,包括数据库参数设置.操作系统参数.防火墙等.以下介绍KingbaseES针对该问题的排查思路. ...

  4. KingbaseES OUT 类型参数过程与函数的调用方法

    对于含有 out 类型参数的过程或者函数,只能通过块方式调用,这是因为,ksql 还不支持类似 Oracle 那样通过 var 定义变量. 一.带OUT的procedure 调用 创建过程: crea ...

  5. SpringBoot使用自定义注解+AOP+Redis实现接口限流

    为什么要限流 系统在设计的时候,我们会有一个系统的预估容量,长时间超过系统能承受的TPS/QPS阈值,系统有可能会被压垮,最终导致整个服务不可用.为了避免这种情况,我们就需要对接口请求进行限流. 所以 ...

  6. 华南理工大学 Python第3章课后小测-2

    1.(单选)给出如下代码 s = 'Hello scut' print(s[4::-1]) 上述代码的输出结果是(本题分数:2)A) HelloB) olleHC) scutD) tucs您的答案:B ...

  7. Html飞机大战(十七): 优化移动端

    好家伙,继续优化,   好家伙,我把我的飞机大战发给我的小伙伴们玩 期待着略微的赞赏之词,然后他们用手机打开我的给他们的网址 然后点一下飞机就炸了. 游戏体验零分 (鼠标点击在移动端依旧可以生效) 好 ...

  8. css文字超出后显示...

    多行 overflow: hidden; //超出的文本隐藏 text-overflow: ellipsis; //溢出用省略号显示 display: -webkit-box; -webkit-lin ...

  9. RabbitMQ原理和架构图解(附6大工作模式)

    为什么要使用RabbitMQ? 1.解耦 系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦. 2.异步 将消息写入消息队列,非必要的业务逻辑以异步的方式运 ...

  10. 洛谷P1496 火烧赤壁 (模拟/离散化+差分)

    分析可知:将起点和终点按照从小到大的顺序排序,对答案不会产生影响 所以此时我们得到一种模拟做法: 1 #include<bits/stdc++.h> 2 using namespace s ...