【python进阶 笔记】web静态服务器(含epoll实现)

目录

1.http协议(了解)

1.1.  HTTP请求

1.2.  HTTP格式

2. Web静态服务器

2.1 py实现简单的http服务器

2.2. py实现返回浏览器需要的页面http服务器

3. Web静态服务器(多任务)

3.1. 多进程实现http服务器

3.2. 多线程实现http服务器

3.3. gevent实现http服务器

3.4. 非堵塞模式

    3.4.1 单任务非堵塞

    3.4.2 单任务非堵塞(长链接)

    3.4.3 epoll实现http服务器


1.http协议(了解)

HTTP(Hyper Text Transfer Protocol)<超文本传输协议>。

1.1.  HTTP请求

1). 浏览器首先向服务器发送HTTP请求,请求包括:

方法:GET还是POST,GET仅请求资源,POST会附带用户数据;

路径:/full/url/path;

域名:由Host头指定:Host: www.sina.com

以及其他相关的Header;

如果是POST,那么请求还包括一个Body,包含用户数据

2). 服务器向浏览器返回HTTP响应,响应包括:

响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误;

响应类型:由Content-Type指定;

以及其他相关的Header;

通常服务器的HTTP响应会携带内容,也就是有一个Body,包含响应的内容,网页的HTML源码就在Body中。

3). 如果浏览器还需要继续向服务器请求其他资源,比如图片,就再次发出HTTP请求,重复步骤1、2。

Web采用的HTTP协议采用了非常简单的请求-响应模式,从而大大简化了开发。当我们编写一个页面时,我们只需要在HTTP请求中把HTML发送出去,不需要考虑如何附带图片、视频等,浏览器如果需要请求图片和视频,它会发送另一个HTTP请求,因此,一个HTTP请求只处理一个资源(此时就可以理解为TCP协议中的短连接,每个链接只获取一个资源,如需要多个就需要建立多个链接)

1.2.  HTTP格式

每个HTTP请求和响应都遵循相同的格式,一个HTTP包含Header和Body两部分,其中Body是可选的。

HTTP协议是一种文本协议,所以,它的格式也非常简单。

1). HTTP GET请求的格式:

    GET /path HTTP/1.1
    Header1: Value1
    Header2: Value2
    Header3: Value3

2). HTTP POST请求的格式:

    POST /path HTTP/1.1
    Header1: Value1
    Header2: Value2
    Header3: Value3

    body data goes here...

注:当遇到连续两个\r\n时(一个空行),Header部分结束,后面的数据全部是Body。

3). HTTP响应的格式:

    200 OK
    Header1: Value1
    Header2: Value2
    Header3: Value3

    body data goes here...

注:HTTP响应若包含body,也通过\r\n\r\n分隔。Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。

2. Web静态服务器

2.1 py实现简单的http服务器

import socket


def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    request = new_socket.recv(1024)
    print(request)

    # 2. 返回http格式的数据,给浏览器
    # 2.1 准备发送给浏览器的数据---header
    response = "HTTP/1.1 200 OK\r\n"
    response += "\r\n"
    # 2.2 准备发送给浏览器的数据---boy
    response += "hello world"
    new_socket.send(response.encode("utf-8"))  # python3发送的必须是字节类型

    # 关闭套接字
    new_socket.close()
    

def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7788端口
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)

    while True:
        # 4. 等待新客户端的链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5. 为这个客户端服务
        service_client(new_socket)

    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

运行后,在浏览器输入 http://127.0.0.1:7890/ ,服务器即会返回响应,浏览器显示 hello world。

注:其过程最好了解TCP的三次握手,4次挥手(详细参考计网知识)。

简单参考以下:

一般client先调close。

 2.2. py实现返回浏览器需要的页面http服务器

在2.1 中,http服务器返回的是固定的字符串,本节实现返回浏览器需要的页面的http服务器。

实例:先实现返回index.html

运行前先在html文件夹准备一个index.html文件,用于返回给客户端。

import socket


def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    request = new_socket.recv(1024)
    print(">>>"*50)
    print(request)

    # 2. 返回http格式的数据,给浏览器
    # 2.1 准备发送给浏览器的数据---header
    response = "HTTP/1.1 200 OK\r\n"
    response += "\r\n"
    # 2.2 准备发送给浏览器的数据---boy
    # response += "hahahhah"
    
    f = open("./html/index.html", "rb")
    html_content = f.read()
    f.close()

    # 因为是以二进制打开的文件,不能直接response+=html_content

    # 将response header发送给浏览器
    new_socket.send(response.encode("utf-8"))
    # 将response body发送给浏览器
    new_socket.send(html_content)

    # 关闭套接
    new_socket.close()
    

def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)

    while True:
        # 4. 等待新客户端的链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5. 为这个客户端服务
        service_client(new_socket)

    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

运行后,在浏览器输入 http://127.0.0.1:7890/ ,服务器即会返回响应,浏览器显示 index.html对应的前端内容。

实例改进实现返回浏览器需要的页面。这时要先对client发过来的请求进行解码。解码后提取出用户请求的页面文件名。

import socket
import re


def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    request = new_socket.recv(1024).decode("utf-8")  # decode进行解码
    # print(">>>"*50)
    # print(request)  # 会打印解码后的请求

    request_lines = request.splitlines()  # 对请求按行(请求的每行内容都有一个含义)分割,返回一个列表
    print("")
    print(">"*20)
    print(request_lines)

    # GET /index.html HTTP/1.1   (这是请求的第一行)
    # get post put del
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])  # 依据请求第一行解析文件名([^/]+匹配多个非/,[^ ]*匹配0或多个非空格)
    if ret:
        file_name = ret.group(1)
        # print("*"*50, file_name)
        if file_name == "/":
            file_name = "/index.html"  # 设置请求没指定文件名时,默认的请求页面

    # 2. 返回http格式的数据,给浏览器
    try:
        f = open("./html" + file_name, "rb")  # 上面匹配不到时,没有file_name会异常
    except:
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "------file not found-----"
        new_socket.send(response.encode("utf-8"))
    else:
        html_content = f.read()
        f.close()
        # 2.1 准备发送给浏览器的数据---header
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        # 2.2 准备发送给浏览器的数据---boy
        # response += "hahahhah"

        # 将response header发送给浏览器
        new_socket.send(response.encode("utf-8"))
        # 将response body发送给浏览器
        new_socket.send(html_content)


    # 关闭套接
    new_socket.close()
    

def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)

    while True:
        # 4. 等待新客户端的链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5. 为这个客户端服务
        service_client(new_socket)

    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

运行后,在浏览器输入 http://127.0.0.1:7890/ 指定的文件名(如:http://127.0.0.1:7890/index.html),服务器即会返回响应,文件存在浏览器显示对应的内容。

3. Web静态服务器(多任务)

3.1. 多进程实现http服务器

accept依旧用一个进程来实现,而服务的过程用一个新的进程来实现。

(注:运行前先准备好资源)

import socket
import re
import multiprocessing


def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    request = new_socket.recv(1024).decode("utf-8")
    # print(">>>"*50)
    # print(request)

    request_lines = request.splitlines()
    print("")
    print(">"*20)
    print(request_lines)

    # GET /index.html HTTP/1.1
    # get post put del
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    if ret:
        file_name = ret.group(1)
        # print("*"*50, file_name)
        if file_name == "/":
            file_name = "/index.html"




    # 2. 返回http格式的数据,给浏览器
    
    try:
        f = open("./html" + file_name, "rb")
    except:
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "------file not found-----"
        new_socket.send(response.encode("utf-8"))
    else:
        html_content = f.read()
        f.close()
        # 2.1 准备发送给浏览器的数据---header
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        # 2.2 准备发送给浏览器的数据---boy
        # response += "hahahhah"

        # 将response header发送给浏览器
        new_socket.send(response.encode("utf-8"))
        # 将response body发送给浏览器
        new_socket.send(html_content)


    # 关闭套接
    new_socket.close()
    

def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)

    while True:
        # 4. 等待新客户端的链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5. 为这个客户端服务(服务过程用新的进程实现)
        p = multiprocessing.Process(target=service_client, args=(new_socket,))  # 进程执行service_client,参数new_socket
        p.start()

        new_socket.close()  # 通俗理解:由于创建了新的进程(复制了父进程的socket)


    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

3.2. 多线程实现http服务器

代码上与多进程的主要区别,把multiprocessing改成threading。注意子线程不会复制主线程的socket,所以只用一次new_socket.close()即可。

import socket
import re
import threading


def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    request = new_socket.recv(1024).decode("utf-8")
    # print(">>>"*50)
    # print(request)

    request_lines = request.splitlines()
    print("")
    print(">"*20)
    print(request_lines)

    # GET /index.html HTTP/1.1
    # get post put del
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    if ret:
        file_name = ret.group(1)
        # print("*"*50, file_name)
        if file_name == "/":
            file_name = "/index.html"




    # 2. 返回http格式的数据,给浏览器
    
    try:
        f = open("./html" + file_name, "rb")
    except:
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "------file not found-----"
        new_socket.send(response.encode("utf-8"))
    else:
        html_content = f.read()
        f.close()
        # 2.1 准备发送给浏览器的数据---header
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        # 2.2 准备发送给浏览器的数据---boy
        # response += "hahahhah"

        # 将response header发送给浏览器
        new_socket.send(response.encode("utf-8"))
        # 将response body发送给浏览器
        new_socket.send(html_content)


    # 关闭套接
    new_socket.close()
    

def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)

    while True:
        # 4. 等待新客户端的链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5. 为这个客户端服务
        p = threading.Thread(target=service_client, args=(new_socket,))
        p.start()

        # new_socket.close()  # 线程不会复制主线程的socket,所以只用一次new_socket.close()即可。


    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

更新:用 面向对象实现多进程web服务器

import socket
import re
import multiprocessing


class WSGIServer(object):
    def __init__(self):
        # 1. 创建套接字
        self.tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        # 2. 绑定
        self.tcp_server_socket.bind(("", 7890))

        # 3. 变为监听套接字
        self.tcp_server_socket.listen(128)

    def service_client(self, new_socket):
        """为这个客户端返回数据"""

        # 1. 接收浏览器发送过来的请求 ,即http请求  
        # GET / HTTP/1.1
        # .....
        request = new_socket.recv(1024).decode("utf-8")
        # print(">>>"*50)
        # print(request)

        request_lines = request.splitlines()
        print("")
        print(">"*20)
        print(request_lines)

        # GET /index.html HTTP/1.1
        # get post put del
        file_name = ""
        ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
        if ret:
            file_name = ret.group(1)
            # print("*"*50, file_name)
            if file_name == "/":
                file_name = "/index.html"


        # 2. 返回http格式的数据,给浏览器
        
        try:
            f = open("./html" + file_name, "rb")
        except:
            response = "HTTP/1.1 404 NOT FOUND\r\n"
            response += "\r\n"
            response += "------file not found-----"
            new_socket.send(response.encode("utf-8"))
        else:
            html_content = f.read()
            f.close()
            # 2.1 准备发送给浏览器的数据---header
            response = "HTTP/1.1 200 OK\r\n"
            response += "\r\n"
            # 2.2 准备发送给浏览器的数据---boy
            # response += "hahahhah"

            # 将response header发送给浏览器
            new_socket.send(response.encode("utf-8"))
            # 将response body发送给浏览器
            new_socket.send(html_content)


        # 关闭套接
        new_socket.close()
        

    def run_forever(self):
        """用来完成整体的控制"""

        while True:
            # 4. 等待新客户端的链接
            new_socket, client_addr = self.tcp_server_socket.accept()

            # 5. 为这个客户端服务
            p = multiprocessing.Process(target=self.service_client, args=(new_socket,))
            p.start()

            new_socket.close()


        # 关闭监听套接字
        self.tcp_server_socket.close()


def main():
    """控制整体,创建一个web 服务器对象,然后调用这个对象的run_forever方法运行"""
    wsgi_server = WSGIServer()
    wsgi_server.run_forever()


if __name__ == "__main__":
    main()

3.3. gevent实现http服务器

疑问:为何本例子gevent.spawn创建对象后,不需要调用 对象.join() 或 gevent.joinall() 使线程执行??

import socket
import re
import gevent
from gevent import monkey

monkey.patch_all()


def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    request = new_socket.recv(1024).decode("utf-8")
    # print(">>>"*50)
    # print(request)

    request_lines = request.splitlines()
    print("")
    print(">"*20)
    print(request_lines)

    # GET /index.html HTTP/1.1
    # get post put del
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    if ret:
        file_name = ret.group(1)
        # print("*"*50, file_name)
        if file_name == "/":
            file_name = "/index.html"

    # 2. 返回http格式的数据,给浏览器
    try:
        f = open("./html" + file_name, "rb")
    except:
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "------file not found-----"
        new_socket.send(response.encode("utf-8"))
    else:
        html_content = f.read()
        f.close()
        # 2.1 准备发送给浏览器的数据---header
        response = "HTTP/1.1 200 OK\r\n"
        response += "\r\n"
        # 2.2 准备发送给浏览器的数据---boy
        # response += "hahahhah"

        # 将response header发送给浏览器
        new_socket.send(response.encode("utf-8"))
        # 将response body发送给浏览器
        new_socket.send(html_content)

    # 关闭套接
    new_socket.close()
    

def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)

    while True:
        # 4. 等待新客户端的链接
        new_socket, client_addr = tcp_server_socket.accept()

        # 5. 为这个客户端服务
        gevent.spawn(service_client, new_socket)  # 协程在service_client执行,参数new_socket

        # new_socket.close()

    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

3.4. 非堵塞模式

在python中,套节字可以被设置为阻塞模式或者非阻塞模式。在非阻塞模式下,调用API 后,例如send() 或recv()方法,如果遇到问题就会抛出异常。在阻塞模式下,通俗说就是每次轮询监听一下,遇到错误并不会阻止操作。

对象.setblocking(False)  ,设置套接字为非堵塞的方式。

3.4.1 单任务非堵塞

while内的关键代码,等待客户端到来的socket和列表内等待数据的socket互不影响。

注:该种方式实际应用少,但过程要理解,此种方式是并发而不是并行。

import socket
import time

tcp_server_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 创建套接字
tcp_server_tcp.bind(("", 7899))  # 绑定
tcp_server_tcp.listen(128)
tcp_server_tcp.setblocking(False)  # 设置套接字为非堵塞的方式

client_socket_list = list()  # 列表,有新的客户端链接时,将其客户端添加到列表中

while True:

    # time.sleep(0.5)  # 延时方便看效果

    try:  # 收不到值时产生异常
        new_socket, new_addr = tcp_server_tcp.accept()
    except Exception as ret:
        print("---没有新的客户端到来---")
    else:
        print("---只要没有产生异常,那么也就意味着 来了一个新的客户端----")  # 此时还不能调用recv,因为调用后又会堵塞
        new_socket.setblocking(False)  # 设置套接字为非堵塞的方式
        client_socket_list.append(new_socket)  # 有新的客户端连接,将其socket添加到列表
        
    for client_socket in client_socket_list:
        try:
            recv_data = client_socket.recv(1024)  # 检测列表的socket是否有数据
        except Exception as ret:
            print(ret)
            print("----这个客户端没有发送过来数据----")  # client没发送数据,产生异常
        else:
            print("-----没有异常-----")
            print(recv_data)
            if recv_data:
                # 对方发送过来数据
                print("----客户端发送过来了数据-----")
            else:
                # 对方调用close 导致了有数据,但recv返回空,recv_data为空
                client_socket.close()
                client_socket_list.remove(client_socket)
                print("---客户端已关闭----")

3.4.2 单任务非堵塞(长链接)

通俗来说,短连接每获取一个数据建立一次链接(三次握手连接四次挥手断开)。操作步骤:建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接;

长链接在一次链接中,通过同一个套接字获取好几个需要的数据,这样可以节省服务器资源。操作步骤:建立连接——数据传输...(保持连接)...数据传输——关闭连接。

注:http1.1版本采用的是长链接。之前的code虽然用了1.1版本的http协议,但是每次连接后手动调用了 socket对象.close() ,导致还是短链接。

单任务非堵塞(长链接)code:在header中添加 Content-Length:,用来记下内容的长度,告诉浏览器包有多长,浏览器依此判断是否传完。这样就不用像短链接一样通过断开连接来判断传完了。

import socket
import re


def service_client(new_socket, request):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    # request = new_socket.recv(1024).decode("utf-8")
    # print(">>>"*50)
    # print(request)

    request_lines = request.splitlines()
    print("")
    print(">"*20)
    print(request_lines)

    # GET /index.html HTTP/1.1
    # get post put del
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    if ret:
        file_name = ret.group(1)
        # print("*"*50, file_name)
        if file_name == "/":
            file_name = "/index.html"

    # 2. 返回http格式的数据,给浏览器
    
    try:
        f = open("./html" + file_name, "rb")
    except:
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "------file not found-----"
        new_socket.send(response.encode("utf-8"))
    else:
        html_content = f.read()
        f.close()

        response_body = html_content

        response_header = "HTTP/1.1 200 OK\r\n"
        response_header += "Content-Length:%d\r\n" % len(response_body)  # 记下内容的长度,告诉浏览器包有多长
        response_header += "\r\n"

        response = response_header.encode("utf-8") + response_body  # 读到的response_body是二进制,response_header是字符串

        new_socket.send(response)  # 发送二进制的数据
   
    # # 关闭监听套接字
    # new_socket.close()


def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)
    tcp_server_socket.setblocking(False)  # 将套接字变为非堵塞

    client_socket_list = list()
    while True:
        # 4. 等待新客户端的链接
        try:
            new_socket, client_addr = tcp_server_socket.accept()
        except Exception as ret:
            pass
        else:  # 没有异常
            new_socket.setblocking(False)
            client_socket_list.append(new_socket)

        for client_socket in client_socket_list:
            try:
                recv_data = client_socket.recv(1024).decode("utf-8")
            except Exception as ret:
                pass
            else:
                if recv_data:  # 有数据
                    service_client(client_socket, recv_data)
                else:  # 空数据,即浏览器断开连接
                    client_socket.close()
                    client_socket_list.remove(client_socket)

    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()

3.4.3 epoll实现http服务器

一般linux服务器都是用epoll实现的(注:epoll是单进程、单线程),可以实现高并发。

epoll详解可参考:【Linux学习】epoll详解 。

select/epoll采用IO多路复用(有时也称event driven IO),当某个socket有数据到达,则通知用户进程。好处为单个process可同时处理多个网络连接的IO。

参考特点如下:通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,epoll()函数就可以返回。 所以, IO多路复用,本质上不会有并发的功能,因为任何时候还是只有一个进程或线程进行工作,它之所以能提高效率是因为select\epoll 把进来的socket放到他们的 '监视' 列表里面,当任何socket有可读可写数据立马处理,那如果select\epoll 手里同时检测着很多socket, 一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理高效率。

 

code:epoll实现HTTP(理解实现过程):

  1. 导入:import select
  2. 创建epoll对象:epl = select.epoll()
  3. 将监听套接字相应的FD(File Discriptor 文件描述符)注册到epoll中:epl.register(tcp_server_socket.fileno(), select.EPOLLIN)  :参数二:select.EPOLLIN | select.EPOLLET,前者检测是否有输入,后者为ET模式
  4. fd_event_list = epl.poll()  # epl.poll()默认会堵塞,直到 os监测到数据到来 通过事件通知方式 告诉这个程序,此时才会解堵塞。返回event和fd组成的列表。(列表格式:[(fd, event), (套接字对应的文件描述符, 这个文件描述符到底是什么事件 例如 可以调用recv接收等)...]

  5. 对数据处理......

  6. 注销:epl.unregister(fd)。

注:上述3中,epoll对文件描述符的操作有两种模式:LT(level trigger)ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

  • LT模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll时,不会再次响应应用程序并通知此事件。

EPOLLIN (可读)

EPOLLOUT (可写)

EPOLLET (ET模式)

import socket
import re
import select


def service_client(new_socket, request):
    """为这个客户端返回数据"""

    # 1. 接收浏览器发送过来的请求 ,即http请求  
    # GET / HTTP/1.1
    # .....
    # request = new_socket.recv(1024).decode("utf-8")
    # print(">>>"*50)
    # print(request)

    request_lines = request.splitlines()
    print("")
    print(">"*20)
    print(request_lines)

    # GET /index.html HTTP/1.1
    # get post put del
    file_name = ""
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    if ret:
        file_name = ret.group(1)
        # print("*"*50, file_name)
        if file_name == "/":
            file_name = "/index.html"

    # 2. 返回http格式的数据,给浏览器
    
    try:
        f = open("./html" + file_name, "rb")
    except:
        response = "HTTP/1.1 404 NOT FOUND\r\n"
        response += "\r\n"
        response += "------file not found-----"
        new_socket.send(response.encode("utf-8"))
    else:
        html_content = f.read()
        f.close()

        response_body = html_content

        response_header = "HTTP/1.1 200 OK\r\n"
        response_header += "Content-Length:%d\r\n" % len(response_body)
        response_header += "\r\n"

        response = response_header.encode("utf-8") + response_body

        new_socket.send(response)


def main():
    """用来完成整体的控制"""
    # 1. 创建套接字
    tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # 2. 绑定
    tcp_server_socket.bind(("", 7890))

    # 3. 变为监听套接字
    tcp_server_socket.listen(128)
    tcp_server_socket.setblocking(False)  # 将套接字变为非堵塞

    # 创建一个epoll对象!!!
    epl = select.epoll()

    # 将监听套接字对应的fd注册到epoll中
    epl.register(tcp_server_socket.fileno(), select.EPOLLIN)

    fd_event_dict = dict()  # 定义字典用来存放fd对应的套接字

    while True:

        fd_event_list = epl.poll()  # 默认会堵塞,直到 os监测到数据到来 通过事件通知方式 告诉这个程序,此时才会解堵塞。返回event和fd组成的列表

        # [(fd, event), (套接字对应的文件描述符, 这个文件描述符到底是什么事件 例如 可以调用recv接收等)]
        for fd, event in fd_event_list:
            # 等待新客户端的链接
            if fd == tcp_server_socket.fileno():  # 接收到新的客户端链接。产生新的套接字。将其注册到epoll中
                new_socket, client_addr = tcp_server_socket.accept()
                epl.register(new_socket.fileno(), select.EPOLLIN)
                fd_event_dict[new_socket.fileno()] = new_socket  # 存放fd对应的套接字
            elif event == select.EPOLLIN:  # 事件类型为EPOLLIN,有数据
                # 判断已经链接的客户端是否有数据发送过来
                recv_data = fd_event_dict[fd].recv(1024).decode("utf-8")  # fd_event_dict[fd]根据套接字取socket
                if recv_data:
                    service_client(fd_event_dict[fd], recv_data)
                else:  # 收到空数据
                    fd_event_dict[fd].close()
                    epl.unregister(fd)
                    del fd_event_dict[fd]  # 删除字典对应的ky


    # 关闭监听套接字
    tcp_server_socket.close()


if __name__ == "__main__":
    main()
发布了50 篇原创文章 · 获赞 10 · 访问量 6615

猜你喜欢

转载自blog.csdn.net/qq_23996069/article/details/104076227