用Go写的HTTP代理服务器
标题是《用Go写http代理服务器》但实际上更接近于用Go架设http代理服务器,因为代码实在太少了,就像在配置一样。
做这个http代理的起因是前段时间运维上遇到的一个问题:有一个内部网站架设在两台web服务器上,暂且叫机器A和机器B,DNS分别指向这两台服务器,两台服务器之间用HaProxy做软负载均衡,两个机器上的文件是自动同步的,数据库用的是同一个。访问这个网站的域名时,请求有时会分配到机器A有时候会分配到机器B。但是网站之前的设计没有考虑到这样的部署结构,于是访问机器A和访问机器B时会出现一些缓存数据重复覆盖之类的问题。
思来想去,之所以要配这样其实有两个目的,最主要的目的是双机备份,防止单点失败,间接好处才是负载均衡。并且这个内部网站负载并不高,所以负载均衡其实是可以牺牲的,进而想能不能把HaProxy配置为不管访问机器A还是机器B,只要机器A是存活的,就访问到机器A。负载运维的同事森林帮忙研究了HaProxy的配置,没有找到这样配置的办法。于是想说能不能做一个简单的http代理服务器,用Erlang应该很容易实现,之前做过一个Socket代理,没多少代码就实现了。
但实际用erlang实现起来,发现挺复杂,虽然erlang的Socket支持{packet, http}这样的设置参数,但是代理转发数据却总是遇到问题。后来想起Gol也有http包,于是到官方文档翻看了一遍,找到一个“ReverseProxy”类型,几行代码就可以架起一个http代理服务器(下面附第一次实验的代码),但是这个代理服务器有两个问题:其一是这个代理服务器不会重新设置请求的原始地址,导致代理请求以虚拟主机方式配置的网站时出错或无法代理。其二是不会复制返回的Cookie,代理请求成功了,但是网站却登录不了。这两点我在修改了ReverseProxy的代码实验成功后,提交到了Go的BUG列表里,第二点他们已经修复,第一点,他们给的反馈是没办法重置原始地址,因为作为一个反向代理,需要让服务器知道来源地址,BUG单地址
第一次实验失败的代码,实际上等于一个不支持Cookie的反向代理,获取新版Go应该就支持Cookie了,代码够少的:
package main
import ( "os"
"log"
"http" )
func main() {
targetUrl, err := http.ParseURL("http://www.baidu.com")
if err != nil { panic("bad url")
}
proxy := http.NewSingleHostReverseProxy(targetUrl)
http.Handle("/", proxy)
log.Println("Start serving on port 1234")
http.ListenAndServe(":1234", nil)
os.Exit(0)
}
用上面这个代码代理请求google是可以的,但是请求baidu就会出错,因为来源URL的原因。
下面是我复制ReverseProxy的代码修改后的结果,实测过可以正常代理和登录网站:
package main
import ( "os"
"io"
"log"
"http"
"strings" )
var targetURL *http.URL
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/") switch { case aslash && bslash: return a + b[1:] case !aslash && !bslash: return a + "/" + b
} return a + b
}
func handler(w http.ResponseWriter, r *http.Request) {
o := new(http.Request)
*o = *r
o.Host = targetURL.Host
o.URL.Scheme = targetURL.Scheme
o.URL.Host = targetURL.Host
o.URL.Path = singleJoiningSlash(targetURL.Path, o.URL.Path)
if q := o.URL.RawQuery; q != "" {
o.URL.RawPath = o.URL.Path + "?" + q
} else {
o.URL.RawPath = o.URL.Path
}
o.URL.RawQuery = targetURL.RawQuery
o.Proto = "HTTP/1.1"
o.ProtoMajor = 1
o.ProtoMinor = 1
o.Close = false
transport := http.DefaultTransport
res, err := transport.RoundTrip(o)
if err != nil {
log.Printf("http: proxy error: %v", err)
w.WriteHeader(http.StatusInternalServerError) return
}
hdr := w.Header()
for k, vv := range res.Header { for _, v := range vv {
hdr.Add(k, v)
}
}
for _, c := range res.SetCookie {
w.Header().Add("Set-Cookie", c.Raw)
}
w.WriteHeader(res.StatusCode)
if res.Body != nil {
io.Copy(w, res.Body)
}
}
func main() {
url, err := http.ParseURL("http://www.baidu.com")
if err != nil {
log.Println("Bad target URL")
}
targetURL = url
http.HandleFunc("/", handler)
log.Println("Start serving on port 1234")
http.ListenAndServe(":1234", nil)
os.Exit(0)
}
我觉得Go可以把内置的代理模块声明为HttpProxy然后通过设置Proxy的实例是ReverseProxy还是OutGoingProxy来决定要不要修改请求的来源地址。
当这个http代理服务器代码初步实现的时候,运维上的那个需求已经没有了。。。于是就没有继续把这个http代理实现下去,就当作一次练习吧 :)
做完这个程序我的感受是:接触Go的时间并不长,没有像erlang那样实际用于项目。但是Go却给我以前做.net开发时候的感觉,.net虽然是闭源的,但是通过Reflector可以很容易的看到内部机制的设计和实现,让你在开发的时候可以更确定自己在做什么,平台又会为你做什么,甚至可以做一些Hack。相较于erlang,Go让我觉得更容易触摸到它的内部,通过阅读系统包的代码你可以知道它的Socket包是怎么实现的,erlang也是开源项目,我也曾尝试深入阅读底层的代码,但是总是没找到那种感觉。我想这跟Go的项目结构和文档组织方式有很大关系吧。
- Flask入门笔记(一)
- 刷脸还是指纹识别,that's a question
- c#:使用using关键字自动释放资源未必一定就会有明显好处
- MongoDB 学习笔记(原创)
- Silverlight:ScorllViewer随Tab键自动跟随子控件的Focus滚动
- 老域名做新站如何能快速得上首页?
- Silverlight:分包下载及SEO优化方案
- jQuery调用RESTful WCF示例(GET方法/POST方法)
- "RDLC"报表-参数传递及主从报表
- 初探小程序之运营及未来方向
- 微信小程序深夜开放NFC等连接能力,未来可直接刷公交?
- Silverlight:获取ContentTemplate中的命名控件
- silverlight向wcf传递大于8192字节(8k)的字符串
- vim使用笔记
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- (最新 9000 字 )Spring Boot 配置特性解析
- JavaWeb——HTTP响应协议及Response对象使用方法一点通及案例实战(重定向、输出字符/字节数据到浏览器、验证码实现)
- drf序列化器之反序列化的数据验证
- yum仅下载不安装---保留rpm包
- Java——数据库编程JDBC之JDBCTemplate的使用简化数据库操作步骤(含增删改查练习实例源码)
- Java——数据库编程JDBC之数据库连接池技术(C3P0与Druid,提供了Druid的工具类)
- MySQL数据库——多表查询之内连接查询、外连接查询、子查询
- MySQL数据库——表的约束(非空约束、唯一约束、主键约束、外键约束)
- 如何取消rxjs Observable的订阅
- 使用Angular rxjs打印鼠标点击事件的x坐标之和
- js变量提升 和函数提升
- JavaWeb——HTTP请求协议及request对象使用方法一点通与request登录实战案例(含BeanUtils类的使用)
- redis学习(二十一)
- Linux下查看文件和文件夹大小
- 使用Angular rxjs进行优雅限流