Contents

[后台]用select写一个HTTP代理

函数说明

18.3. select — Waiting for I/O completion — Python 3.6.0 documentation

select.select(rlist, wlist, xlist[, timeout])

Unix select()系统调用的一个简单接口。前三个参数是文件描述符(int类型)的序列。

1
2
3
    rlist:等待准备读取
    wlist:等待准备写作
    xlist:等待一个异常

参数说明

在Unix上序列可以为空,但在Windows则不可以。可选的timeout参数将超时指定为浮点数(以秒为单位)。当省略timeout参数时,该函数阻塞,直到至少一个文件描述符已准备好。该值为0时表示轮询从不停止。

返回值

准备好的对象列表的三元组(前三个参数的子集)。当超时但没有文件描述符就绪时,返回三个空列表。

序列对象

序列中可用的对象类型包括Python文件对象(例如sys.stdin或由open()或os.popen()返回的对象),由socket.socket()返回的socket对象。你也可以自己定义一个类,只要它有一个合适的fileno()方法(返回一个真正的文件描述符,而不只是一个随机整数)。

注意:
Windows上的文件对象不可接受,但socket可以。在Windows上,底层的select()函数由WinSock库提供,不处理不是源自WinSock的文件描述符。

实现HTTP代理

代理由两个部分组成:客户端-代理的socket,和代理-Web服务器的socket。
因为代理像两边接受数据时调用recv方法都会发生阻塞,
所以,在这个样例中,事件循环由两个事件组成:客户端-代理链接可以继续接受数据了,和代理-Web服务器链接可以接受数据了(指都是代理端)。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
    import select
    import socket
    import _thread
    
    BUFF_SIZE = 1024
    
    
    # get http head data from a connection
    def get_post(conn):
        res = b''
        while True:
            res += conn.recv(BUFF_SIZE)
            if b'\r\n\r\n' in res:
                break
        return res
    
    
    class Proxy:
        # init. in_conn: browser-proxy connection, out_conn: proxy-web_server connection
        def __init__(self, conn, timeout):
            self.in_conn = conn
            self.out_conn = None
            self.timeout = timeout
    
            self.run()
    
        # main function
        def run(self):
            action, url, http_ver, req, headers = self.__parse_header()
            # https
            if action in ["CONNECT"]:
                self.in_conn.send(b'HTTP/1.1 200 Connection established\r\nProxy-agent: God Proxy\r\n\r\n')
                self.__connect(headers)
            # http
            else:
                self.__connect(headers)
                self.out_conn.send(req)
            self.event_loop()
            # close connection
            self.in_conn.close()
            self.out_conn.close()
    
        # event loop
        def event_loop(self):
            count = 0
            inputs = [self.in_conn, self.out_conn]
            outputs = []
            while True:
                read_list, _, exception_list = select.select(inputs, outputs, inputs, self.timeout)
                count += 1
                for future_object in read_list:
                    # once could read, receive data
                    data = future_object.recv(1024)
                    if data:
                        # read in_conn's data, send to out_conn or out_conn to in_conn
                        if future_object is self.in_conn:
                            # print(count, 'out conn', data[:10], '...', data[-10:])
                            self.out_conn.sendall(data)
                        else:
                            # print(count, 'in conn', data[:10], '...', data[-10:])
                            self.in_conn.sendall(data)
                if exception_list:
                    break
                # time out
                if count > 100:
                    break
    
        # parse host from url and setting up out_conn socket
        def __connect(self, headers):
            host_list = headers['Host'].split(':')
            host = host_list[0]
            if host_list[-1].isdigit():
                port = int(host_list[1])
            else:
                port = 80
            socket_info = socket.getaddrinfo(host, port)[0]
            self.out_conn = socket.socket(socket_info[0])
            self.out_conn.connect(socket_info[4])
            print('WEB SITE: ', host, socket_info[4])
    
        # get headers from in_conn and parse
        def __parse_header(self):
            req = get_post(self.in_conn)
            str_req = req[:req.find(b'\r\n\r\n')].decode('utf8')
            headers = {}
            lines = str_req.split('\r\n')
            for line in lines[1:]:
                line = line.split(': ')
                headers[line[0]] = line[1]
            action, url, http_ver = lines[0].split()
            return action, url, http_ver, req, headers
    
    
    def main():
        server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    
        server_address = ('0.0.0.0', 8899)
        server.bind(server_address)
    
        server.listen(0)
        # 1 connection to 1 thread
        while True:
            conn, addr = server.accept()
            _thread.start_new_thread(Proxy, (conn, 3))
    
    if __name__ == '__main__':
        main()