1行Go代码实现反向代理的示例
作者:alfred-zhong 发布时间:2024-04-28 09:15:26
暂且放下你的编程语言来瞻仰下我所见过的最棒的标准库。
为项目选择编程语言和挑选你最爱的球队不一样。应该从实用主义出发,根据特定的工作选择合适的工具。
在这篇文章中我会告诉你从何时开始并且为什么我认为 Go 语言如此闪耀,具体来说是它的标准库对于基本的网络编程来说显得非常稳固。更具体一点,我们将要编写一个反向代理程序。
Go 为此提供了很多,但真正支撑起它的在于这些低级的网络管道任务,没有更好的语言了。
反向代理是什么? 有个很棒的说法是流量转发 。我获取到客户端来的请求,将它发往另一个服务器,从服务器获取到响应再回给原先的客户端。反向的意义简单来说在于这个代理自身决定了何时将流量发往何处。
为什么这很有用?因为反向代理的概念是如此简单以至于它可以被应用于许多不同的场景:负载均衡,A/B 测试,高速缓存,验证等等。
当读完这篇文章之后,你会学到:
如何响应 HTTP 请求
如何解析请求体
如何通过反向代理将流量转发到另一台服务器
我们的反向代理项目
我们来实际写一下项目。我们需要一个 Web 服务器能够提供以下功能:
获取到请求
读取请求体,特别是 proxy_condition 字段
如果代理域为 A,则转发到 URL 1
如果代理域为 B,则转发到 URL 2
如果代理域都不是以上,则转发到默认的 URL
准备工作
Go 语言环境。
http-server 用来创建简单的服务。
环境配置
我们要做的第一件事是将我们的配置信息写入环境变量,如此就可以使用它们而不必写死在我们的源代码中。
我发现最好的方式是创建一个包含所需环境变量的 .env
文件。
以下就是我为特定项目编写的文件内容:
export PORT=1330
export A_CONDITION_URL="http://localhost:1331"
export B_CONDITION_URL="http://localhost:1332"
export DEFAULT_CONDITION_URL=http://localhost:1333
这是我从 12 Factor App 项目中获得的技巧。
保存完 .env
文件之后就可以运行:
source .env
在任何时候都可以运行该指令来将配置加载进环境变量。
项目基础工作
接着我们创建 main.go
文件做如下事情:
将
PORT
,A_CONDITION_URL
,B_CONDITION_URL
和DEFAULT_CONDITION_URL
变量通过日志打印到控制台。在
/
路径上监听请求:
package main
import (
"bytes"
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
)
// Get env var or default
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
// Get the port to listen on
func getListenAddress() string {
port := getEnv("PORT", "1338")
return ":" + port
}
// Log the env variables required for a reverse proxy
func logSetup() {
a_condtion_url := os.Getenv("A_CONDITION_URL")
b_condtion_url := os.Getenv("B_CONDITION_URL")
default_condtion_url := os.Getenv("DEFAULT_CONDITION_URL")
log.Printf("Server will run on: %s\n", getListenAddress())
log.Printf("Redirecting to A url: %s\n", a_condtion_url)
log.Printf("Redirecting to B url: %s\n", b_condtion_url)
log.Printf("Redirecting to Default url: %s\n", default_condtion_url)
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
// We will get to this...
}
func main() {
// Log setup values
logSetup()
// start server
http.HandleFunc("/", handleRequestAndRedirect)
if err := http.ListenAndServe(getListenAddress(), nil); err != nil {
panic(err)
}
}
现在你就可以运行代码了。
解析请求体
有了项目的基本骨架之后,我们需要添加逻辑来处理解析请求的请求体部分。更新 handleRequestAndRedirect
函数来从请求体中解析出 proxy_condition
字段。
type requestPayloadStruct struct {
ProxyCondition string `json:"proxy_condition"`
}
// Get a json decoder for a given requests body
func requestBodyDecoder(request *http.Request) *json.Decoder {
// Read body to buffer
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Error reading body: %v", err)
panic(err)
}
// Because go lang is a pain in the ass if you read the body then any susequent calls
// are unable to read the body again....
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
return json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body)))
}
// Parse the requests body
func parseRequestBody(request *http.Request) requestPayloadStruct {
decoder := requestBodyDecoder(request)
var requestPayload requestPayloadStruct
err := decoder.Decode(&requestPayload)
if err != nil {
panic(err)
}
return requestPayload
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
requestPayload := parseRequestBody(req)
// ... more to come
}
通过 proxy_condition 判断将流量发往何处
现在我们从请求中取得了 proxy_condition
的值,可以根据它来判断我们要反向代理到何处。记住上文我们提到的三种情形:
如果
proxy_condition
值为A
,我们将流量发送到A_CONDITION_URL
如果
proxy_condition
值为B
,我们将流量发送到B_CONDITION_URL
其他情况将流量发送到
DEFAULT_CONDITION_URL
// Log the typeform payload and redirect url
func logRequestPayload(requestionPayload requestPayloadStruct, proxyUrl string) {
log.Printf("proxy_condition: %s, proxy_url: %s\n", requestionPayload.ProxyCondition, proxyUrl)
}
// Get the url for a given proxy condition
func getProxyUrl(proxyConditionRaw string) string {
proxyCondition := strings.ToUpper(proxyConditionRaw)
a_condtion_url := os.Getenv("A_CONDITION_URL")
b_condtion_url := os.Getenv("B_CONDITION_URL")
default_condtion_url := os.Getenv("DEFAULT_CONDITION_URL")
if proxyCondition == "A" {
return a_condtion_url
}
if proxyCondition == "B" {
return b_condtion_url
}
return default_condtion_url
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
requestPayload := parseRequestBody(req)
url := getProxyUrl(requestPayload.ProxyCondition)
logRequestPayload(requestPayload, url)
// more still to come...
}
反向代理到 URL
最终我们来到了实际的反向代理部分。在如此多的语言中要编写一个反向代理需要考虑很多东西,写大段的代码。或者至少引入一个复杂的外部库。
然而 Go 的标准库使得创建一个反向代理非常简单以至于你都不敢相信。下面就是你所需要的最关键的一行代码:
httputil.NewSingleHostReverseProxy(url).ServeHTTP(res, req)
注意下面代码中我们做了些许修改来让它能完整地支持 SSL 重定向(虽然不是必须的)。
// Serve a reverse proxy for a given url
func serveReverseProxy(target string, res http.ResponseWriter, req *http.Request) {
// parse the url
url, _ := url.Parse(target)
// create the reverse proxy
proxy := httputil.NewSingleHostReverseProxy(url)
// Update the headers to allow for SSL redirection
req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Host = url.Host
// Note that ServeHttp is non blocking and uses a go routine under the hood
proxy.ServeHTTP(res, req)
}
// Given a request send it to the appropriate url
func handleRequestAndRedirect(res http.ResponseWriter, req *http.Request) {
requestPayload := parseRequestBody(req)
url := getProxyUrl(requestPayload.ProxyCondition)
logRequestPayload(requestPayload, url)
serveReverseProxy(url, res, req)
}
全部启动
代码是开源的,你可以在 Github 上找到。 :heart: 在 Twitter 上我只聊关于编程和远程工作相关的东西。如果关注我,你不会后悔的。
来源:https://studygolang.com/articles/14246


猜你喜欢
- 目录一、pyecharts绘制词云图WordCloud.add()方法简介二、绘制词云图对应轮廓按diamond显示三、对应完整代码如下所示
- 代理模式的优点代理模式可以保护原对象,控制对原对象的访问;代理模式可以增强原对象的功能,通过代理对象来添加一些额外的功能;代理模式可以提高系
- 前言这个系列的文章我们使用以下的顺序进行讲解:Pattern 详解;Matcher 详解;正则表达式语法详解。接下来先来介绍 Pattern
- 高可用架构对于互联网服务基本是标配,无论是应用服务还是数据库服务都需要做到高可用。虽然互联网服务号称7*24小时不间断服务,但多多少少有一些
- 新建一个项目之后,我们来看一下项目的目录结构几个主要文件的内容index.html文件(入口文件,系统进入之后先进入index.html)&
- 本章是前一章的延续,我们使用RSA算法逐步实现加密,并详细讨论它.用于解密密文的函数是as跟随 :def decrypt(ciph
- 前言Django处理json也是一把好手,有时候在工作中各个部门都会提供自己的相关接口,但是信息也只是单方的信息,这时候需要运维将各个部门的
- AERGO SHIP:用于开发智能合约的包管理器用于构建、测试和部署分布式应用程序的客户端框架和开发环境构建大型分布式应用程序是很困难的,因
- 本文实例讲述了MySQL切分查询用法。分享给大家供大家参考,具体如下:对于大查询有时需要‘分而治之',将大查询切分为小查询: 每个查
- sql exist的妙用create table b(a varchar(10),b varchar(10),c varchar(10))i
- 什么是python的迭代如果给定一个list或tuple,我们可以通过for循环来遍历这个list或tuple,这种遍历我们称为迭代(Ite
- 废话不多说了直接给大家贴代码了。代码如下:<script language="JavaScript"><
- 本文实例讲述了Python实现求最大公约数及判断素数的方法。分享给大家供大家参考。具体实现方法如下:#!/usr/bin/env pytho
- 前言近来chatGPT挺火的,也试玩了一下,确实挺有意思。这里记录一下在Python中如何去使用chatGPT。本篇文章的实现100%基于
- 本文为大家分享了Eclipse开发python脚本的具体方法,供大家参考,具体内容如下一、安装python1.访问网址,可以看到如下图所示界
- 一:安装PyQt5pip install pyqt5如果你的系统没有安装pip请阅读我们的另一篇文章 windows下python安装pip
- replace() 方法用于将字符串用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。 需要注意的是:如果用正则表达式替换时, r
- 从实时视频流中识别出人脸区域,从原理上看,其依然属于机器学习的领域之一,本质上与谷歌利用深度学习识别出猫没有什么区别。程序通过大量的人脸图片
- 1. 使用 fileinput 进行迭代fileinput 模块可以对一个或多个文件中的内容进行迭代、遍历等操作。该模块的 input()
- 说明:本例改编自《Python编程快速上手》。例子很简单我就不多说了 直接上代码,给初学python练手用。给你6次机会猜对一个预先生成好的