Python视频学习(五、re模块,六、★自己写web服务器)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_30162859/article/details/83154422

感谢传智dongge大神

1. re 正则表达式

1.1 匹配规则

单个字符

规则 含义
. 任意字符,换行符除外
[] 匹配 []中的单个字符
\d 任意一个数字
\D 任意一个非数字
\s 任意一个空白符: 空格,tab, \n
\S 任意一个非空白
\w 任意一个: 大小写字母,数字,_ , 还有别的语言的单一字符(比如中文)
\W 任意一个非文字
\b 单词边界
\B 非单词边界

多个字符

规则 含义
* 匹配前一个字符出现0次或者无限次
+ 匹配前一个字符出现1次或者无限次
匹配前一个字符出现1次或者0次,即要么有1次,要么没有
{n,m} 匹配前一个字符出现从m到n次
{n} 匹配前一个字符出现m次
{n,} 匹配前一个字符至少出现n次

开头结尾

规则 含义
^ 匹配字符串开头
$ 匹配字符串结尾

分组

规则 含义
| 匹配左右任意一个表达式,这个优先级很低,最后计算;所有有时要加上括号
(ab) 将括号中字符作为一个分组
\num 引用分组num匹配到的字符串
(?P) 分组起别名, 比如(?P<head>\w*)
(?P=name) 引用别名为name分组匹配到的字符串,比如 (?P=head)

1.2 方法

match —— 返回match对象

从开头匹配,指导匹配出正确结果,返回一个Match对象,否则返回None:

import re

result = re.match(正则, 匹配字符串)  # result 是一个match对象
# result = re.match( 正则,匹配字符串, re.S ) # re.S 代表 . 也能匹配换行
print(result.group())  # 打印完整匹配内容, 也是 group(0)
print(result.groups())  # 打印分组,从1分组开始,返回的是一个 元祖
match对象的主要方法
.group() 打印第0组,也就是整个匹配对象
.group(n) 打印第n组
.groups() 从第一组开始的各个组,打包返回一个tuple

search —— 返回match对象

搜索整个字符串,直到找到一个匹配,返回Match对象,否则返回None:

#coding=utf-8
import re

ret = re.search(r"\d+", "阅读次数为 9999")
ret.group()

findall —— 返回list

搜索整个字符串,根据匹配返回一个结果 list

sub —— 替换,或者批量操作

替换:

#coding=utf-8
import re

ret = re.sub(r"\d+", '998', "python = 997")
print(ret)

方法二:

import re

def add(temp):
    strNum = temp.group()
    num = int(strNum) + 1
    return str(num)

ret = re.sub(r"\d+", add, "python = 997")
print(ret)

ret = re.sub(r"\d+", add, "python = 99")
print(ret)

split —— 返回list

ret = re.split(r":| ","info:xiaoZhang 33 shandong")
print(ret)
# ['info', 'xiaoZhang', '33', 'shandong']

1.3 贪婪模式

*, ?, +, {m,n}, {m, }后面加上 ?能够关闭贪婪模式。

1.4 r字符串

Python中字符串前面加上 r 表示原生字符串,

与大多数编程语言相同,正则表达式里使用"“作为转义字符,这就可能造成反斜杠困扰。假如你需要匹配文本中的字符”",那么使用编程语言表示的正则表达式里将需要4个反斜杠"\":前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。

Python里的原生字符串很好地解决了这个问题,有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。

网络思维: 先圈地,后赚钱

2. HTTP协议和自己写的简单web服务器

使串口调试助手+浏览器,演示了上网原理

2.1 HTTP消息的格式

a. HTTP请求

    GET /path HTTP/1.1
    HOST: 127.0.0.1:8080
    Connection: keep-alive
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    User-Agent: ....
    Header1: Value1
    Header2: Value2
    Header3: Value3
  • 方法:GET还是POST,GET仅请求资源,POST会附带用户数据;
  • 路径:/full/url/path;
  • 域名:由Host头指定:Host: www.sina.com
  • 如果是POST,那么请求还包括一个Body,包含用户数据
    HTTP请求和响应的结构

b. HTTP响应

	HTTP/1.1 200 OK
	Connection: keep-alive
	Content-Type: text/html; charset=utf-8
	Content-Encoding: gzip
    Header1: Value1
    Header2: Value2
    Header3: Value3

    body data goes here...
  • 响应代码:200表示成功,3xx表示重定向,4xx表示客户端发送的请求有错误,5xx表示服务器端处理时发生了错误;
  • 响应类型:由Content-Type指定;
  • HTTP响应如果包含body,也是通过\r\n\r\n来分隔的。
  • 请再次注意,Body的数据类型由Content-Type头来确定,如果是网页,Body就是文本,如果是图片,Body就是图片的二进制数据。
  • 当存在Content-Encoding时,Body数据是被压缩的,最常见的压缩方式是gzip,所以,看到Content-Encoding: gzip时,需要将Body数据先解压缩,才能得到真正的数据。压缩的目的在于减少Body的大小,加快网络传输。

1688网站进货–>淘宝卖
阿里妈妈领取广告
根据用户的cookie,记录用户数据,分析用户的profile

2.2 返回静态页面的程序

import socket


serverlistener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7788端口
serverlistener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
serverlistener.bind( ("",8080) )
serverlistener.listen(100)

while True:

    connectin_sk, client_addr = serverlistener.accept()

    print("连接来了,地址为", client_addr)

    # 如果不读取只发送的话,没有效果
    headers = connectin_sk.recv(2048).decode("utf-8")

    # 打印头部
    headers = headers.splitlines()
    for h in headers:
        print(h)

	# 准备数据
    content = "HTTP/1.1 200\r\n"
    content += "Content-Type: text/html;charset=utf-8\r\n\r\n"
    content += "<h1>哈哈哈哈</h1>"

    connectin_sk.send(content.encode("utf-8"))

    connectin_sk.close()

serverlistener.close()

注意这一句

# 设置当服务器先close 即服务器端4次挥手之后资源能够立即释放,这样就保证了,下次运行程序时 可以立即绑定7788端口
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

主要是因为TCP关闭的四次挥手阶段,主动结束方在最后需要等待一定时间,防止没有接收到 被动结束方发过来的FIN。

2.3 动态服务器

import socket
import os


def readpage(pagename) -> bytes:
    """
    用来读取静态文件并且返回

    """
    with open("html/%s"%pagename,"rb") as f:
        return f.read()


def communication(conn_sk):
    """
    处理连接的socket的收发

    """

    headers = conn_sk.recv(1024).decode('utf-8')
    # 获取请求的地址
    route = headers.splitlines()[0].split()[1]
    if route== '/':
        body = readpage("index.html")
    else:
        body = readpage(route[1:])

    # 发送响应头部
    response = "HTTP/1.1 200 OK\r\n\r\n"

    conn_sk.send(response.encode("utf-8"))
    # 发送页面内容
    conn_sk.send(body)

    conn_sk.close()


def main():

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 设置 socket关闭后马上可以使用
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    local_addr = "", 8080
    server.bind(local_addr)
    server.listen(128)
    while True:
        conn_sk, client_addr = server.accept()
        communication(conn_sk)

    server.close()


if __name__ == '__main__':
    main()
import socket
import re


def handle_client(client_socket):
    "为一个客户端进行服务"
    recv_data = client_socket.recv(1024).decode('utf-8', errors="ignore")
    request_header_lines = recv_data.splitlines()
    for line in request_header_lines:
        print(line)

    http_request_line = request_header_lines[0]
    get_file_name = re.match("[^/]+(/[^ ]*)", http_request_line).group(1)
    print("file name is ===>%s" % get_file_name)  # for test

    # 如果没有指定访问哪个页面。例如index.html
    # GET / HTTP/1.1
    if get_file_name == "/":
        get_file_name = DOCUMENTS_ROOT + "/index.html"
    else:
        get_file_name = DOCUMENTS_ROOT + get_file_name

    print("file name is ===2>%s" % get_file_name) #for test

    try:
        f = open(get_file_name, "rb")
    except IOError:
        # 404表示没有这个页面
        response_headers = "HTTP/1.1 404 not found\r\n"
        response_headers += "\r\n"
        response_body = "====sorry ,file not found===="
    else:
        response_headers = "HTTP/1.1 200 OK\r\n"
        response_headers += "\r\n"
        response_body = f.read()
        f.close()
    finally:
        # 因为头信息在组织的时候,是按照字符串组织的,不能与以二进制打开文件读取的数据合并,因此分开发送
        # 先发送response的头信息
        client_socket.send(response_headers.encode('utf-8'))
        # 再发送body
        client_socket.send(response_body)
        client_socket.close()


def main():
    "作为程序的主控制入口"
    server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server_socket.bind(("", 7788))
    server_socket.listen(128)
    while True:
        client_socket, clien_cAddr = server_socket.accept()
        handle_client(client_socket)


#这里配置服务器
DOCUMENTS_ROOT = "./html"

if __name__ == "__main__":
    main()

2.4 TCP三次握手,四次挥手

在这里插入图片描述

3. 并发web服务器

3.1 多线程动态服务器

import socket
import threading
import re


class MultiThreadServer(object):
    """多进程服务器的类"""

    DOCUMENT_PATH = "./html"

    def read_file(self, route) -> bytes:
        """
        根据传入的路径名, 从html文件夹中获取对应文件
        """

        # 判断是否存在
        try:
            with open("%s/%s" % (MultiThreadServer.DOCUMENT_PATH, route), "rb") as f:
                return f.read()
        except Exception as e:
            return

    def resolve_route(self, raw_route):
        if raw_route == "/":
            return "index.html"
        else:
            return raw_route[1:]

    def worker(self, conn_sk):
        headers = conn_sk.recv(1024).decode("utf-8")

        # 获取请求头列表
        headers = re.split(r"\r\n", headers)

        for header in headers:
            if re.match(r"(GET|POST).*", header):
                # 如果第一行是GET或者POST开始,则获取路径
                route = re.split(r"\s", header)[1]
                route = self.resolve_route(route)
                content = self.read_file(route)

                # 准备发送数据
                if content:
                    # 正确返回结果
                    response_header = "HTTP/1.1 200 OK"
                    conn_sk.send(response_header.encode("utf-8"))
                    conn_sk.send("\r\n\r\n".encode("utf-8"))
                    conn_sk.send(content)

                else:
                    # 显示错误页面
                    response_header = "HTTP/1.1 404 NOT FOUND"
                    conn_sk.send(response_header.encode("utf-8"))
                    conn_sk.send("\r\nContent-Type: text/html; charset=utf-8".encode("utf-8"))
                    conn_sk.send("\r\n\r\n".encode("utf-8"))
                    conn_sk.send("找不到页面".encode("utf-8"))

        conn_sk.close()

    def serve_forever(self):
        """创建监听套接字,开始服务器"""

        # 使用资源管理来关闭socket
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
            # 设置地址重用
            server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            local_addr = "", 8080
            server.bind(local_addr)
            server.listen(128)

            while True:
                client_sk, client_addr = server.accept()
                print("有新连接,地址为%s:%s" % (client_addr[0], client_addr[1]))
                t = threading.Thread(target= self.worker, args=(client_sk,))
                t.start()


if __name__ == '__main__':
    server = MultiThreadServer()
    server.serve_forever()

3.2 多进程动态服务器

和多线程几乎一样,最大的区别是在创建了新进程并且start之后,要关闭主线程的这个连接套接字, 因为子线程已经有了一份赋值,他们都指向的是同一个socket,如果主线程的不关闭,就算子线程发送完了数据之后,由于连接还在,所以浏览器那边还在等待数据,无法显示内容

import socket
import multiprocessing
import re


class MultiProcessServer(object):
    """多进程服务器的类"""

    DOCUMENT_PATH = "./html"

    def read_file(self, route) -> bytes:
        """
        根据传入的路径名, 从html文件夹中获取对应文件
        """

        # 判断是否存在
        try:
            with open("%s/%s" % (MultiThreadServer.DOCUMENT_PATH, route), "rb") as f:
                return f.read()
        except Exception as e:
            return

    def resolve_route(self, raw_route):
        if raw_route == "/":
            return "index.html"
        else:
            return raw_route[1:]

    def worker(self, conn_sk):
        headers = conn_sk.recv(1024).decode("utf-8")

        # 获取请求头列表
        headers = re.split(r"\r\n", headers)

        for header in headers:
            if re.match(r"(GET|POST).*", header):
                # 如果第一行是GET或者POST开始,则获取路径
                route = re.split(r"\s", header)[1]
                route = self.resolve_route(route)
                content = self.read_file(route)

                # 准备发送数据
                if content:
                    # 正确返回结果
                    response_header = "HTTP/1.1 200 OK"
                    conn_sk.send(response_header.encode("utf-8"))
                    conn_sk.send("\r\n\r\n".encode("utf-8"))
                    conn_sk.send(content)

                else:
                    # 显示错误页面
                    response_header = "HTTP/1.1 404 NOT FOUND"
                    conn_sk.send(response_header.encode("utf-8"))
                    conn_sk.send("\r\nContent-Type: text/html; charset=utf-8".encode("utf-8"))
                    conn_sk.send("\r\n\r\n".encode("utf-8"))
                    conn_sk.send("找不到页面".encode("utf-8"))

        conn_sk.close()

    def serve_forever(self):
        """创建监听套接字,开始服务器"""

        # 使用资源管理来关闭socket
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
            # 设置地址重用
            server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            local_addr = "", 8080
            server.bind(local_addr)
            server.listen(128)

            while True:
                client_sk, client_addr = server.accept()
                print("有新连接,地址为%s:%s" % (client_addr[0], client_addr[1]))
                t = multiprocessing.Process(target= self.worker, args=(client_sk,))
                t.start()

                # 这一个一定要关闭,因为新创建的线程已经维护了一份赋值
                # 如果不关闭的话,那边发送完,连接还在等待 关闭
                client_sk.close()


if __name__ == '__main__':
    server = MultiProcessServer()
    server.serve_forever()

3.3 单进程单线程非阻塞服务器

利用的是 setblocking(False)使套接字编程非阻塞,意思是如果没有 数据/连接,则会抛出异常,这样就能继续别的处理

  • python中不是使用查看socket有没有数据的函数,而是直接使用非阻塞的recv来收,如果没有就捕获异常
  • 要捕获 监听socket的accept方法
  • 如果accept方法连接成功,把创建的socket设置为 非阻塞,并且加入列表
  • 将连接的socket加入一个列表中,遍历查看是否有数据
import socket
import time


def main():
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    local_addr = "", 8080
    server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    server.bind(local_addr)
    server.listen(128)

    server.setblocking(False)  # 设置为非阻塞

    client_list = list()
    while True:

        try:
            client_sk, client_addr = server.accept()  # 没有接受到连接,会抛出异常
        except Exception:
            print("暂时没有连接到来")

        else:
            # client_sk.setblocking(True)  # 设置非阻塞
            client_list.append(client_sk)
            print("有新连接到来,地址为%s:%s" % (client_addr[0], client_addr[1]))

        for client in client_list:
            try:
                request = client.recv(1024)  # 如果没有数据,也会抛出异常
                if request:
                    print("数据发来了哦!%s" % request)
                else:
                    print("有一个连接断开了")
                    client.close()
                    client_list.remove(client)

            except Exception as ex:
                # print(ex)
                print("没有数据发送过来")
        time.sleep(0.5)


if __name__ == '__main__':
    main()

3.4 长连接服务器

在之前的代码中,每次将客户端请求的页面发送完毕后,服务器这边的连接socket就关闭了。但是现在HTTP1.1中,客户端(浏览器)会使用同一个socket发送多个请求。

  1. 发送一个新的请求的前提是,上一个发送的请求已经收到了响应
  2. 服务器响应,是根据设置 Content-Type来表示响应长度,这样才能让浏览器确定已经收到了所有数据,继而发送下一个请求
  3. **负责发送内容的函数中,不要断开连接,交给 遍历函数来负责执行 **
  4. 等浏览器请求全部完毕后,等浏览器主动关闭连接,服务器之后断开连接
import socket
import time
import re


class NonBlockingLongConnectionServer(object):
    """多进程服务器的类"""

    DOCUMENT_PATH = "./html"

    def read_file(self, route) -> bytes:
        """
        根据传入的路径名, 从html文件夹中获取对应文件
        """

        # 判断是否存在
        try:
            with open("%s/%s" % (NonBlockingLongConnectionServer.DOCUMENT_PATH, route), "rb") as f:
                return f.read()
        except Exception as e:
            return

    def resolve_route(self, raw_route):
        if raw_route == "/":
            return "index.html"
        else:
            return raw_route[1:]

    def worker(self, conn_sk, headers):
        headers = headers.decode("utf-8")

        # 获取请求头列表
        headers = re.split(r"\r\n", headers)

        for header in headers:
            if re.match(r"(GET|POST).*", header):
                # 如果第一行是GET或者POST开始,则获取路径
                route = re.split(r"\s", header)[1]
                route = self.resolve_route(route)
                content = self.read_file(route)

                # 准备发送数据
                if content:
                    # 正确返回结果

                    response_body = content

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

                else:
                    # 显示错误页面

                    response_body = "找不到页面".encode("utf-8")

                    response_header = "HTTP/1.1 404 NOT FOUND\r\n"
                    response_header += "Content-Type: %d\r\n" % len(response_body)
                    response_header += "\r\n"

                conn_sk.send(response_header.encode("utf-8"))
                conn_sk.send(response_body)


        # 这里就不关闭了,只是完成一次的请求响应而已
        # conn_sk.close()

    def serve_forever(self):
        """创建监听套接字,开始服务器"""

        # 使用资源管理来关闭socket
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
            # 设置地址重用
            server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            local_addr = "", 8080
            server.bind(local_addr)
            server.listen(128)
            server.setblocking(False)

            client_list = []
            while True:
                try:
                    client_sk, client_addr = server.accept()
                except:
                    print("暂时没有新连接到来")
                else:
                    print("有新连接,地址为%s:%s" % (client_addr[0], client_addr[1]))
                    client_sk.setblocking(False)
                    client_list.append( (client_sk, client_addr))

                for client_sk, (client_ip,client_port) in client_list:
                    try:
                        request = client_sk.recv(1024)
                    except:
                        print("暂时没有接收%s:%s的数据"%(client_ip, client_port))
                    else:
                        if request:
                            print("接受到一次数据: %s"%request.decode("utf-8"))
                            self.worker(client_sk, request)
                        else:
                            # 关闭连接
                            print("一个连接结束了!")
                            client_sk.close()
                            client_list.remove( (client_sk,(client_ip,client_port)   ) )

                time.sleep(0.5)


if __name__ == '__main__':
    server = NonBlockingLongConnectionServer()
    server.serve_forever()

3.5 协程web服务器

大致更改:

from gevent import monkey

monkey.patch_all()

...
def run_forever(self):
        """运行服务器"""

	# 等待对方链接
	while True:
		new_socket, new_addr = self.server_socket.accept()
		# 将每一个连接socket交给 处理函数即可
		gevent.spawn(self.deal_with_request, new_socket)  # 创建一个协程准备运行它

def deal_with_request(self, client_socket):
    """为这个浏览器服务器"""
    while True:
        # 接收数据
        request = client_socket.recv(1024).decode('utf-8')
        # print(gevent.getcurrent())
        # print(request)

        # 当浏览器接收完数据后,会自动调用close进行关闭,因此当其关闭时,web也要关闭这个套接字
        if not request:
            new_socket.close()
            break

        request_lines = request.splitlines()
        for i, line in enumerate(request_lines):
            print(i, line)

        # 提取请求的文件(index.html)
        # GET /a/b/c/d/e/index.html HTTP/1.1
        ret = re.match(r"([^/]*)([^ ]+)", request_lines[0])
        if ret:
            print("正则提取数据:", ret.group(1))
            print("正则提取数据:", ret.group(2))
            file_name = ret.group(2)
            if file_name == "/":
                file_name = "/index.html"

        file_path_name = self.documents_root + file_name
        try:
            f = open(file_path_name, "rb")
        except:
            # 如果不能打开这个文件,那么意味着没有这个资源,没有资源 那么也得需要告诉浏览器 一些数据才行
            # 404
            response_body = "没有你需要的文件......".encode("utf-8")

            response_headers = "HTTP/1.1 404 not found\r\n"
            response_headers += "Content-Type:text/html;charset=utf-8\r\n"
            response_headers += "Content-Length:%d\r\n" % len(response_body)
            response_headers += "\r\n"

            send_data = response_headers.encode("utf-8") + response_body

            client_socket.send(send_data)

        else:
            content = f.read()
            f.close()

            # 响应的body信息
            response_body = content
            # 响应头信息
            response_headers = "HTTP/1.1 200 OK\r\n"
            response_headers += "Content-Type:text/html;charset=utf-8\r\n"
            response_headers += "Content-Length:%d\r\n" % len(response_body)
            response_headers += "\r\n"
            send_data = response_headers.encode("utf-8") + response_body
            client_socket.send(send_data)

3.6 epoll(了解)

高性能服务器现在很多用epoll,是linux和unix底层功能。 gevent底层用的是epoll, nginx底层也是epoll. 它的性能比刚才写的单进程单线程非阻塞性能好非常多。

  • epoll使用了内存映射mmap,不是让应用程序本身维护列表,而是和kernel一起使用一处内存,能让kernel更快速的响应 file descriptor的情况
  • epoll是基于事件驱动,而不是轮询

在这里插入图片描述

I/O 多路复用的特点:

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

当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换页会耗掉更多的系统资源。 所以我们可以结合IO多路复用和多进程/多线程 来高性能并发,IO复用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑

IO多路复用的几种:

  1. select: 采用遍历轮询,并且还有同时能维护的数量
  2. poll:取消了数量问题,还是轮询
  3. epoll事件驱动,哪个有file descriptor有反应了就触发

epoll详解: https://blog.csdn.net/xiajun07061225/article/details/9250579
《单台服务器并发TCP连接数到底可以有多少》 http://www.52im.net/thread-561-1-1.html
《上一个10年,著名的C10K并发连接问题》 http://www.52im.net/thread-566-1-1.html

案例:

import select
...



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

# 注册这个socket的 file descriptor, 并且表明是输入模式
epl.register( server.fileno(), select.EPOLLIN)  

# 维护一个空字典,键为 file descriptor, 值为socket
# 负责根据epoll响应的 fd,找到对应的socket
fd_socket_dict = {} 
while True:
	# 阻塞,直到它监听的那些 file descriptor触发了响应的事件
	# 返回列表,将所有可用的fd返回
	# 格式是 [(fd,event), (fd, event) ... ] 
	fd_event_list = epl.poll()

	for fd, event in fd_event_list:
		if fd == server.fileno(): # 判断如果是这个监听套接字
			client_sk, client_addr = server.accept()
			client_sk.setblocking(False)
			#  把返回的连接套接字注册到字典中
			fd_socket_dict = {client_sk.fileno() : client_sk}
			#  把返回的连接套接字,也注册到epoll监听列表
			epl.register( client_sk.fileno(), select.EPOLLIN) 
		elif event == select.EPOLLIN:
			# 客户端有数据发送,则处理
			client_sk = fd_socket_dict[fd]   # 从字典中根据fd取出对应的 socket
			request = client_sk.recv(1024)

			if request:
				# ....处理client_sk
			else:
				# 当连接断开
				client_sk.close()   #关闭
				del fd_event_list[fd]   # 移除字典
				epl.unregister(fd)

文件描述符:

  • EPOLLIN (可读)
  • EPOLLOUT (可写)
  • EPOLLET (ET模式) (默认)

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

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

完整一个服务器:

import socket
import time
import sys
import re
import select


class WSGIServer(object):
    """定义一个WSGI服务器的类"""

    def __init__(self, port, documents_root):

        # 1. 创建套接字
        self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 2. 绑定本地信息
        self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.server_socket.bind(("", port))
        # 3. 变为监听套接字
        self.server_socket.listen(128)

        self.documents_root = documents_root

        # 创建epoll对象
        self.epoll = select.epoll()
        # 将tcp服务器套接字加入到epoll中进行监听
        self.epoll.register(self.server_socket.fileno(), select.EPOLLIN|select.EPOLLET)

        # 创建添加的fd对应的套接字
        self.fd_socket = dict()

    def run_forever(self):
        """运行服务器"""

        # 等待对方链接
        while True:
            # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
            epoll_list = self.epoll.poll()

            # 对事件进行判断
            for fd, event in epoll_list:
                # 如果是服务器套接字可以收数据,那么意味着可以进行accept
                if fd == self.server_socket.fileno():
                    new_socket, new_addr = self.server_socket.accept()
                    # 向 epoll 中注册 连接 socket 的 可读 事件
                    self.epoll.register(new_socket.fileno(), select.EPOLLIN | select.EPOLLET)
                    # 记录这个信息
                    self.fd_socket[new_socket.fileno()] = new_socket
                # 接收到数据
                elif event == select.EPOLLIN:
                    request = self.fd_socket[fd].recv(1024).decode("utf-8")
                    if request:
                        self.deal_with_request(request, self.fd_socket[fd])
                    else:
                        # 在epoll中注销客户端的信息
                        self.epoll.unregister(fd)
                        # 关闭客户端的文件句柄
                        self.fd_socket[fd].close()
                        # 在字典中删除与已关闭客户端相关的信息
                        del self.fd_socket[fd]

    def deal_with_request(self, request, client_socket):
        """为这个浏览器服务器"""

        if not request:
            return

        request_lines = request.splitlines()
        for i, line in enumerate(request_lines):
            print(i, line)

        # 提取请求的文件(index.html)
        # GET /a/b/c/d/e/index.html HTTP/1.1
        ret = re.match(r"([^/]*)([^ ]+)", request_lines[0])
        if ret:
            print("正则提取数据:", ret.group(1))
            print("正则提取数据:", ret.group(2))
            file_name = ret.group(2)
            if file_name == "/":
                file_name = "/index.html"


        # 读取文件数据
        try:
            f = open(self.documents_root+file_name, "rb")
        except:
            response_body = "file not found, 请输入正确的url"

            response_header = "HTTP/1.1 404 not found\r\n"
            response_header += "Content-Type: text/html; charset=utf-8\r\n"
            response_header += "Content-Length: %d\r\n" % len(response_body)
            response_header += "\r\n"

            # 将header返回给浏览器
            client_socket.send(response_header.encode('utf-8'))

            # 将body返回给浏览器
            client_socket.send(response_body.encode("utf-8"))
        else:
            content = f.read()
            f.close()

            response_body = content

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

            # 将数据返回给浏览器
            client_socket.send(response_header.encode("utf-8")+response_body)


# 设置服务器服务静态资源时的路径
DOCUMENTS_ROOT = "./html"


def main():
    """控制web服务器整体"""
    # python3 xxxx.py 7890
    if len(sys.argv) == 2:
        port = sys.argv[1]
        if port.isdigit():
            port = int(port)
    else:
        print("运行方式如: python3 xxx.py 7890")
        return

    print("http服务器使用的port:%s" % port)
    http_server = WSGIServer(port, DOCUMENTS_ROOT)
    http_server.run_forever()


if __name__ == "__main__":
    main()

4. 网络通信

在这里插入图片描述

TCP套接字本身的端口不能重复,UDP也是一样,但是对于它们2者来说,可以有一个TCP使用8080,另一个UDP也使用8080,这是允许的

暂时省略

猜你喜欢

转载自blog.csdn.net/qq_30162859/article/details/83154422