本篇源码及Ctrl+C+V的来源参考这个

使用socket编程实现代理服务器,首先它得是一个服务器,因此我们有第一篇参考代码:

server = socket.socket()
server.bind(('127.0.0.1',8000))
server.listen(3)
conn, addr = server.accept()
data = True
while data :data = conn.recv(1024)msg = raw_input()if msq=="any code you mean to exit": breakconn.sendall(msg)
conn.close()
server.close()

它做了这几件事:

1.启动服务,监听端口8000,并设置为允许3个客户端排队(虽然实际上只支持一个客户端进行访问)

2.接受请求,在连接中接收和返回数据

3.当客户端关闭时,recv会得到空字符串,因此退出循环、结束程序

不妨就用上面的这个程序接收请求,看一看我们的代理服务器究竟要处理什么:

GET http://www.sina.com/ HTTP/1.1
Host: www.sina.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1

注意到,最下面有两个空行,这是约定,请求头与请求体之间用\r\n\r\n来分割

为了看的更清楚,我们可以让它以unicode显示

['GET http://www.sina.com/ HTTP/1.1\r\nHost: www.sina.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n']

其中的第二行Host就是我们要获取的目标服务器地址,当然http的默认端口号是80

只要得到目标服务器的地址和端口号,我们就可以将这个请求原封不动的丢给目标服务器了,至于怎么获取这个目标地址,反正看起来也不难,我们可以假装它已经实现了。

与上述服务器代码不同,我们不需要input,也不需要循环处理数据,只需要接受完数据、把它丢给服务器就可以了,然后从目标服务器返回数据的过程恰好相反,需要从target中recv,向conn中sendall,因此:

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('127.0.0.1', 8000))
server.listen(3)
conn, addr = server.accept()
data = conn.recv(1024)
print data
# 假装已经实现了getHost
host, port = getHost(data)
target = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
target.connect((host, port))
target.sendall(data)data = target.recv(1024)
print data
conn.sendall(data)target.close()
conn.close()
server.close()

对www.sina.com的测试得到了这样的报文:

HTTP/1.1 302 Moved Temporarily
Server: nginx
Date: Thu, 14 Mar 2019 11:25:58 GMT
Content-Type: text/html
Content-Length: 154
Connection: keep-alive
Location: https://www.sina.com.cn/
X-Via-CDN: f=edge,s=cmcc.shandong.ha2ts4.82.nb.sinaedge.com,c=223.72.94.28;
X-Via-Edge: 15525627584351c5e48df7d53c0784e9a7612

<html>
<head><title>302 Found</title></head>
<body bgcolor="white">
<center><h1>302 Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

报文称,这个网站已经搬家了,不再使用http协议进行访问了,以后要上新浪网应该使用https://www.sina.com.cn/这个网址

显然,这是因为我太落伍了,在https大行其道的年代连传统的http代理都没学会

无论如何,这样的结果至少表明我们正常的接收了客户端与服务器端的响应,并且测试会发现,浏览器可以正常访问到新浪网(因为它跳转到https协议上不再经过http代理)


至此,http代理服务器的核心代码已经完成,接下来的任务是对这部分代码进行优化。

首先我们假装这个服务器启动命令中可以接收一个整数作为端口号,然后假装我们的服务器可以服务于多个不同的客户端,这意味着对于每个客户端需要分别启动新线程,因此:

def main(_, port=8000):myserver = socket.socket()myserver.bind(('127.0.0.1', port))myserver.listen(1024)while True:conn, addr = myserver.accept()thread_p = threading.Thread(target=thread_proxy, args=(conn, addr))thread_p.setDaemon(True)thread_p.start()if __name__ == '__main__':main(*sys.argv)sys.exit(0)

当然了,我们的服务器很流氓,不提供退出方法,所以这是一个死循环

对每一个thread_proxy,我们需要完成三件事:1.找到目标服务器。2.转发请求报文。3.转发响应报文。

注意到我们在之前的简易服务器代码中写的recv参数固定为1024,这是不是意味着我们只能对请求长度小于1024的请求进行代理,超出长度概不负责?这当然是不合适的!因此我们需要将它设置的非常大循环读取直到读完。

于是一个非常令人尴尬的问题就出现了,在某一次读取完毕之后,我怎么知道我读完了呢?

一个非常直观的想法是:如果我读到的长度等于预设的长度,那就是没有读完,否则就是读完了。然而无论是客户端还是浏览器,都不知道你预设的长度是多少,因此总是存在“整倍数”的概率,而且这个概率并不太低。一旦如此,就会陷入读阻塞。

请求头中有一个字段【content-length】被用于描述请求体的长度,如果没有这样的字段,那么约定\r\n0\r\n\r\n为休止符

虽然网上查到的结论有些深奥,但简单来说就是上面这句话。再加上我们之前就掌握了的\r\n\r\n分割符,形成这样一组手段:

  1. 切取请求头

    def splitHeader(string):i, l = 3, len(string)while i<l and (string[i] != "\n" or string[i-3:i+1] !="\r\n\r\n") : i+=1return string[:i-3]
  2. 从请求头中寻找信息(host、content-length)
    def getHeader(header, name):name = name.upper()base, i, l = 0, 0, len(header)while i<l:# 行入口,寻找冒号while i<l and header[i] != ":" : i+=1# 判断信息头if i<l and header[base:i].strip().upper() == name:# 此行即为所求,从冒号后截断base = i+1while i<l and not(header[i] == "\n" and header[i-1] == "\r") : i+=1return header[base:i-1]else:# 此行非所求,跳过此行while i<l and not(header[i] == "\n" and header[i-1] == "\r") : i+=1base, i = i+1, i+1# 所求不存在return None
  3. 根据约定获取全部报文
    def recvBody(conn, base, size):if size==-1:while base[-5:] != "\r\n0\r\n\r\n" : base += conn.recv(RECV_SIZE)else:while len(base)<size:base += conn.recv(RECV_SIZE)return base

有了这些给力的手段做支撑,现在可以写thread_proxy了,为了便捷起见,事实上很多服务器也约定,报文的头信息不能太长,这给了我们一个保障:在指定的长度内一定能够获取完整的头信息,将这个长度设置为MAX_HEADER_SIZE,有:

def thread_proxy(client, addr):request = client.recv(MAX_HEADER_SIZE)requestHeader = splitHeader(request)raw_host = getHeader(requestHeader, "Host")host, port = transHost(raw_host)# body也可能是空字符串,若如此则不必处理if len(requestHeader) < len(request)-4:content_size = getHeader(requestHeader, "content-length")size = len(requestHeader) + 4 + int(content_size) if content_size else -1request = recvBody(client, request, size)server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.connect((host, port))server.sendall(request)response = server.recv(MAX_HEADER_SIZE)responseHeader = splitHeader(response)if len(responseHeader) < len(response)-4:content_size = getHeader(responseHeader , "content-length")size = len(responseHeader) + 4 + int(content_size) if content_size else -1response = recvBody(server, response , size)client.sendall(response)server.close()client.close()

其中transHost是一个异常简单的小方法,只是处于处理默认值的方便,单独提炼出来:

def transHost(raw_host):for i in range(len(raw_host)): if raw_host[i] == ":" : return raw_host[:i].strip(), int(raw_host[i+1:])else : return raw_host.strip(), 80

len(responseHeader)+4+int(content_size)是技术不足技巧来补的解决方案,目的是实现对报文长度的控制

至此,一个基本的http代理服务器就实现了,当然,出于健壮性考虑、debug方便和其它因素,实用化的代码会更长一点,完整的代码点击这里

然而https据说会更复杂,截至目前,我连示意图都还没看懂。真希望有个大佬教我SSL协议?

Python实现的简易HTTP代理服务器相关推荐

  1. python制作图形化小游戏_创意编程|Python的GUI简易界面设计测测你的反应力

    Python的GUI简易界面设计案例 测测你的反应力      作为初次接触代码编程的你,是不是觉得Python程序除了"码"就是"字"即使是有趣的程序除了烧烧 ...

  2. 简单的python画图代码_python opencv如何实现简易画图板 python opencv实现简易画图板代码...

    python opencv如何实现简易画图板?本篇文章小编给大家分享一下python opencv实现简易画图板代码,小编觉得挺不错的,现在分享给大家供大家参考,有需要的小伙伴们可以来看看. 代码如下 ...

  3. [转载] python实现一个简易的计算器

    参考链接: 使用Python创建一个简单的计算器 python实现一个简易的计算器 from tkinter import from tkinter.ttk import * def my_frame ...

  4. 【新手向】Python做一个简易登录注册GUI界面(无事件绑定)

    Python做一个简易登录注册GUI界面 一,效果展示 (1)运行结果: (2)点击登录: 注:只有用户名:123 密码123456 会提示登录成功. 登录成功示范: 登录失败示范: (3)点击注册 ...

  5. python写音乐播放器_AJ Kipper:用Python写一个简易的MP3播放器

    用Python写一个简易的MP3播放器 前言 最近在学习Web.py框架的时候,了解了基本的Python连接数据库(MySQL)的方法.学完后,总想用它来干点啥,于是,就想能不能写一个MP3播放器.一 ...

  6. 用Python制作一个简易的计时器

    前言 今天又带来个小玩意 - 用Python制作一个简易的计时器 这个其实也能自定义一些东西的 就比如名字 颜色啥的 自己看着改就行 有想法的朋友也能自己再写写改改出其他的小功能 效果展示 实现代码 ...

  7. python实现电影票简易预定系统

    python实现电影票简易预定系统 一.整体结构 二.实现方式 2.1 infos.py 2.2 film_selector.py 2.3 seat_booking.py 2.4 main.py 一. ...

  8. 使用python编写一个简易的打折程序

    使用python编写一个简易的打折程序

  9. 用python写的简易黑客游戏

    "黑客"相信这个词语在IT界应该很常见,其中有些人制作了一些黑客游戏,今天我们就来用python做一个简易的黑客游戏. 我用5个小时写了一个通关类的黑客游戏,适用于python3编 ...

  10. 使用python基本函数实现简易的《原神》抽卡系统

    使用python基本函数实现简易的<原神>抽卡系统 前言 基本思路 抽卡基本函数 抽卡概率设定 卡池 保底策略 抽卡结束做什么 统计背包 抽奖操作 交互系统 开始祈愿 祈愿记录 充值系统 ...

最新文章

  1. EcoTalks预告 | Max Rietkerk:自然斑图与生态系统的恢复力
  2. OpenCV PCA提取对象的方向的实例(附完整代码)
  3. ddr4服务器内存频率_镁光出样DDR5内存;紫光发布P5160系列SSD!
  4. datetime2 数据类型
  5. HTMLCSS————CSS常用选择器及优先级
  6. C++ 求一元二次方程的根
  7. 二叉树最大深度(LeetCode 104)
  8. 数据库入门-主键和外键设置
  9. dcs world f15c教学_视频教学知识归纳 | 广东中考必备英语:中考语法知识 冠词
  10. Ubuntu22.04(Linux Mint 21)安装使用绿联USB无线网卡CM448(rtl8821CU)的方法
  11. html5 canvas图片反色
  12. echarts 闪光_echarts legend 图例文字闪烁显示
  13. 用 Python 进行多元线性回归分析(附代码)
  14. socket套接字各个接口
  15. 一般熟练盲打需要多久_如何学会盲打, 大概需要多长时间?
  16. Android Camera:从零开发一款相机APP Day01:前景
  17. iphone修改密码时服务器,iPhone 突然弹出窗口要求修改密码怎么办?
  18. Python实验及注意点总结
  19. javascript 时间日期处理相加,减操作方法js
  20. platformIO 自定义板子方法

热门文章

  1. 给广大学习单片机的同学心得,如何学好单片机
  2. 常见分布 的 数学期望以及方差公式
  3. 概率统计笔记之 “数学期望和方差”
  4. 计算机专业班级网站,计算机专业班级标语
  5. 泡泡龙游戏c语言程序,《泡泡龙》发射技巧总结_图文攻略_高分攻略_百度攻略...
  6. 计算机总是莫名其妙重启,电脑老是自动重启怎么办,电脑为什么经常自动重启_系统圣地...
  7. 【斗鱼直播源】浏览器抓取真实直播源地址(纯前端JS PHP解析源码)
  8. 三角函数计算,Cordic 算法入门
  9. 三角波c语言编程,51单片机简易波形发生器(正弦波 锯齿波 三角波)仿真+源程序+电路原理图...
  10. 腾讯云播放器TcPlayer实现网络直播