本篇源码及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": break
conn.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
302 Found
302 Found
nginx
报文称,这个网站已经搬家了,不再使用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分割符,形成这样一组手段:
def splitHeader(string):
i, l = 3, len(string)
while i
def getHeader(header, name):
name = name.upper()
base, i, l = 0, 0, len(header)
while i
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)
有了这些给力的手段做支撑,现在可以写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 -1
request = 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 -1
response = 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协议?