美文网首页
自己写一个web服务器(2)

自己写一个web服务器(2)

作者: 芒果菠萝蛋炒饭 | 来源:发表于2019-03-12 10:55 被阅读0次
    • 之前我们搭建了一个极简的WSGI服务器, 可以处理基本的HTTP请求,但是由于我们建立的服务一次只能处理一个客户端请求,在当前的请求处理完成之前,它不能接受新的连接。

    服务器的代码

    • 我们将处理请求的这块逻辑抽出来,代码如下:
    import socket
    
    SERVER_ADDRESS = (HOST, PORT) = '', 8888
    REQUEST_QUEUE_SIZE = 5
    
    def handle_request(client_connection):
        request = client_connection.recv(1024)
        print(request.decode())
        http_response = b"""\
    HTTP/1.1 200 OK
    
    Hello, World!
    """
        client_connection.sendall(http_response)
    
    def serve_forever():
        listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        listen_socket.bind(SERVER_ADDRESS)
        listen_socket.listen(REQUEST_QUEUE_SIZE)
        print('Serving HTTP on port {port} ...'.format(port=PORT))
    
        while True:
            client_connection, client_address = listen_socket.accept()
            handle_request(client_connection)
            client_connection.close()
    
    if __name__ == '__main__':
        serve_forever()
    

    仔细看,当handle_request方法还没有结束的时候,循环会阻塞在这里,无法监听后续的请求。

    客户端与服务端之间的通信

    • 为了让两个程序通过网络彼此通讯,我们用到了socket,那么socket是什么呢?

    socket

    • socket是一个通信终端的抽象概念,它允许程序通过文件描述符与另一个程序通信
    • TCP连接的socket对是一个拥有4个值的tuple,用来标识TCP连接的两个端点: 本地IP地址、本地端口、外部IP地址、外部端口。
    • socket对是网络上每个tcp连接的唯一标识。这两个成对的值标识各自端点,一个IP地址和一个端口号,通常被称为一个socket。
    • 例子
      socket pair
    • tuple {10.10.10.2:49152, 12.12.12.3:8888} 是客户端上一个唯一标识两个TCP连接终端的socket, {12.12.12.3:8888, 10.10.10.2:49152} 是服务端上一个唯一标识相同的两个TCP连接终端的socket。IP地址12.12.12.3和端口8888在TCP连接中用来识别服务器端点(同样适用于客户端)。

    Python建立socket链接

    • 服务器创建一个TCP/IP socket链接

      • 新建socket连接
      # AF_INET:服务器之间网络通信。  SOCK_STREAM: 流式socket , for TCP
      listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      
      • 设置一些socket选项(这是可选的)
      # SOL_SOCKET: 想要在套接字级别上设置选项,就必须把level设置为 SOL_SOCKET
      # SO_REUSEADDR: 打开或关闭地址复用功能。当值不等于0时,打开,否则,关闭。
      listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
      
      • 服务器绑定地址
      # 在TCP中,调用bind允许你指定端口号,IP地址,要么两个都有,要么就都没有。
      listen_socket.bind(SERVER_ADDRESS)
      
      • 开始监听
      # listen方法只供服务器调用。它告诉内核应该接受给这个socket传入的连接请求
      # REQUEST_QUEUE_SIZE代表连接请求队列的长度
      listen_socket.listen(REQUEST_QUEUE_SIZE)
      
      • 上述步骤完成后,服务器开始逐个接受客户端连接。当一个连接可用accept返回要连接的客户端socket。然后服务器读从客户端socket取请求数据,打印出响应标准输出然后给客户端socket传回消息。然后服务器关闭客户端连接,准备接受一个新的客户端连接。
    • 客户端连接服务器

      • 和服务端0建立连接类似
       import socket
      
       # create a socket and connect to a server
       sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
       sock.connect(('localhost', 8888))
      
       # send and receive some data
       sock.sendall(b'test')
       data = sock.recv(1024)
       print(data.decode())
      
      • 客户端只需提供服务器的远程地址或是主机名和远程端口号来连接。
      • 客户端没有调用bind和accept。其原因是客户端不关心本地IP地址和端口号。客户端调用connect时内核中的TCP/IP socket会自动分配本地IP地址和端口号。本地端口被称为临时端口。

    文件描述符

    • 当一个进程打开现有的文件,创建一个新的文件,或者创建一个新的socket连接的时候,内核返回给它的一个非负整数。
    • 在UNIX中,一切都是文件,内核通过文件描述符指向一个打开的文件。当你需要读写文件的时候,就是用文件描述符来识别的。
    • UNINS shell默认分配文件描述符0给标准输入进程,1是标准输出,2是标准错误

    怎么保证你的服务器能同时处理多个请求?或者换个说法,如何编写并发服务器?

    • 在UNIX下,最简单的方式是用一个fork()系统调用
      import os
      import socket
      import time
      
      SERVER_ADDRESS = (HOST, PORT) = '', 8888
      REQUEST_QUEUE_SIZE = 5
      
      
      def handle_request(client_connection):
          request = client_connection.recv(1024)
          print(
              'Child PID: {pid}. Parent PID {ppid}'.format(
                  pid=os.getpid(),
                  ppid=os.getppid(),
              )
          )
          print(request.decode())
          http_response = b"""\
      HTTP/1.1 200 OK
      Hello, World!
      """
          client_connection.sendall(http_response)
          time.sleep(60)
      
      
      def serve_forever():
          listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
          listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
          listen_socket.bind(SERVER_ADDRESS)
          listen_socket.listen(REQUEST_QUEUE_SIZE)
          print('Serving HTTP on port {port} ...'.format(port=PORT))
          print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))
      
          while True:
              client_connection, client_address = listen_socket.accept()
              pid = os.fork()
              if pid == 0:  # child
                  listen_socket.close()  # close child copy
                  handle_request(client_connection)
                  client_connection.close()
                  os._exit(0)  # child exits here
              else:  # parent
                  client_connection.close()  # close parent copy and loop over
      
      if __name__ == '__main__':
          serve_forever()
      
    • 运行多个curl命令之后,即使子进程处理一个进程之后会休眠60秒,但是这并不会影响处理其它的客户端请求,因为它们是完全独立的进程了。
    • 在调用一次fork()之后,新进程会是原进程的子进程,原进程称为父进程。 子进程会复制父进程的数据信息。而后程序就分两个进程继续运行了。在子进程内,这个方法会返回0;在父进程内,这个方法会返回子进程的编号PID。可以使用PID来区分两个进程。
    • 在上面的代码中,父进程关闭了客户端的连接,那么子进程是怎么继续读取客户端的socket连接的呢?
      • 父进程fork出一个子进程之后,这个子进程得到了一个父进程文件描述符。
      • 内核根据文件描述符的值来决定是否关闭连接socket,只有其值为0才会关闭。
      • 服务器产生一个子进程,子进程拷贝父进程文件描述符,内核增加引用描述符的值。在一个父进程一个子进程的例子中,描述符引用值就是2,当父进程关闭连接socket,它只会把引用值减为1,不会小到让内核关闭socket。
      • 子进程也关闭了父进程监听socket的重复拷贝,是因为它不关心接受新的客户端连接,而只在乎处理已连接客户端的响应:
        listen_socket.close() # close child copy

    小结

    • 服务器socket创建过程(socket,bind,listen,accept)
    • 客户端socket创建过程(socket,connect)
    • fork()函数的意义:创建子进程,复制父进程的数据信息。而后程序就分两个进程继续运行
    • 在UNIX下写并发服务器最简单的方法是用fork()系统调用。一个进程fork出一个新进程,它就变成新进程的父进程。
    • 调用fork后,父进程和子进程公用同样的文件描述符。内核用文件描述符应用值来决定关闭或打开文件/socket。
    • 服务器父进程的角色:从客户端接受新的连接,fork一个子进程去处理请求,继续接受新的连接。

    相关文章

      网友评论

          本文标题:自己写一个web服务器(2)

          本文链接:https://www.haomeiwen.com/subject/kgrgpqtx.html