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

    django_day08_项目相关 定义数据库表 from django.db import models # Create your models here. class User(models.M ...

  2. STC15 串口通信

    串口1选择定时器2产生波特率 串口1相关寄存器 1.选择串口1所放的管脚 2.串口1配置步骤 3.选择串口工作模式 4.确定定时器2工作速度 代码配置 void Uart1_Tim2_Config(i ...

  3. 如何查找并简单分析core文件

    当系统发生coredump时,通常需要通过分析core文件来定位问题所在,但实际工作中,有时却发现core 文件找不到,或者core文件被删除了. 一.core文件没有生成 KINGBASE core ...

  4. KingbaseES R6 集群“双主”故障解决案例

    实际工作中,可能会碰到集群脑裂的情况,在脑裂时,会出现双 primary情况.这时,需要用户介入,人工判断哪个节点的数据最新,减少数据丢失. 一.测试环境信息 操作系统: [kingbase@node ...

  5. The 19th Zhejiang Provincial Collegiate Programming Contest

    目录 A.JB Loves Math B.JB Loves Comma C. JB Wants to Earn Big Money G. Easy Glide I. Barbecue L. Candy ...

  6. PPR管及管件的类型、规格与选用

    1. PPR管的类型及参数识读 2. 常用的PPR管件及规格 3. 住宅给水管的PPR管及管件的需求量

  7. 使用Docker方式部署Mongodb多副本集(replSet)

    linux主机ip:192.168.0.253 1. 创建网络与容器 docker pull mongo docker network create mongo-rs docker run --nam ...

  8. 从应用访问Pod元数据-DownwardApi的应用

    对于某些需要调度之后才能知道的数据,比如 pod 的 ip,主机名,或者 pod 自身的名称等等,k8s 依旧很贴心的提供了 Downward API 的方式来获取此类数据,并且可以通过环境变量或者文 ...

  9. Mysql 数据库SQL脚本导入

    1.进入mysql命令行窗口 mysql -uroot -p123456 2.展示所有数据库 show databases: 3.选择数据库 use 数据库名 4.展示选择的数据库中所有表 show ...

  10. 「国产系统」Tubian 0.2,兼容Windows和Android的GNU/Linux系统!

    0.3版已发布:https://www.cnblogs.com/tubentubentu/p/16733005.html Sourceforge.net主页(提供下载):https://sourcef ...