linux下用python搭建web服务器

参考文章:https://linux.cn/article-6817-1.html

简易web服务器

  • web服务器是指在物理服务器上搭建的网络连接服务器,时刻等待客户端的请求,并作出响应。

  • 客户端与服务器的通信,是以HTTP协议进行的,客户端可以是任意支持HTTP协议的软件

  • 客户端在向服务器发送HTTP请求前,先建立TCP连接,通过TCP连接发送HTTP请求,客户端和服务器建立TCP连接用到套接字(socket)
  • 套接字:可以理解为通信端点的抽象形式,可以让一个程序通过文件描述符与另一个程序进行通信。
  • TCP套接字对:4元组:本地ip,本地端口,外部ip,外部端口
  • 服务器套接字创建流程:
Created with Raphaël 2.1.2created socketbindacceptconnected socket
  • 客户端套接字创建流程
Created with Raphaël 2.1.2created socketconnect

代码:

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()

实现并发

  • 进程:在unix系统下,每个用户进程都会有一个父进程,父进程id为ppid,当启动服务器时,系统会创建一个服务器进程,指定一个pid,在bash中创建则其父进程为bash的id

  • 文件描述符:当系统打开一个现有文件、创建一个新文件或者创建一个新的套接字之后,返回给进程的那个正整数。系统内核通过文件描述符来追踪一个进程所打开的文件。

  • 套接字对象的listen方法中BACKLOG参数的意义:BACKLOG参数决定了内核中外部连接请求的队列大小。当服务器睡眠时,你运行的第二个命令之所以能够连接服务器,是因为连接请求队列仍有足够的位置。

  • fork函数:unix系统函数, fork()调用一次返回两次,在子进程中返回0,在父进程中返回子进程的pid。
    image

当父进程fork一个新的子进程时,子进程会得到父进程文件描述符的副本
image

父进程:fork一个子进程处理客户端连接,然后回到循环起点准备接受其他的客户端连接

导致的问题:
- 如果不关闭重复(父进程)的文件描述符,客户端不会结束,且服务器会逐渐消耗尽文件描述符资源(最大打开文件数)
- 若子进程在父进程之前结束,会出现僵尸进程,消耗内存资源。因为父进程是不监听客户端结束的,故子进程结束后看到父进程未关闭,会保留信息供父进程之后调用。不处理好僵尸进程会逐渐消耗完可用的进程数(最大用户进程数)

  • 系统内核通过描述符计数来决定是否关闭文件/套接字

  • linux下Python可以用os.fork()创建新进程,windows下是不行的

僵尸进程

  • 如果你fork一个子进程,却不等待进程结束,该进程就会变成僵尸进程。

  • 使用SIGCHLD时间处理函数来异步等待进程结束,获取其结束状态。

  • 使用事件处理函数时,需要牢记系统函数调用可能会被中断,要做好这类情况发生的准备。

最终代码:

import errno
import os
import signal
import socket

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 1024


def grim_reaper(signum, frame):
    while True:
        try:
            pid, status = os.waitpid(
                -1,          # Wait for any child process
                 os.WNOHANG  # Do not block and return EWOULDBLOCK error
            )
        except OSError:
            return

        if pid == 0:  # no more zombies
            return


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))

    signal.signal(signal.SIGCHLD, grim_reaper)

    while True:
        try:
            client_connection, client_address = listen_socket.accept()
        except IOError as e:
            code, msg = e.args
            # restart 'accept' if it was interrupted
            if code == errno.EINTR:
                continue
            else:
                raise

        pid = os.fork()
        if pid == 0:  # child
            listen_socket.close()  # close child copy
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)
        else:  # parent
            client_connection.close()  # close parent copy and loop over

if __name__ == '__main__':
    serve_forever()
阅读更多
换一批

没有更多推荐了,返回首页