多任务静态web服务器的实现

一、前言

《基于python 自己动手写一个简单的web服务器》https://blog.csdn.net/weixin_44485643/article/details/87807191
本文为上篇文章的续篇,想要深究的朋友可以移步上文

  1. 上次我们从零开始,简单介绍了介绍了http协议的前世今生,也了解和演示了http协议的表现形式 ,同时也实现了一个单任务web服务器。虽然上个版本实现了我们预期的功能,但是因为单任务的原因在遇到大量的访问时,效率会地到令人发指。因为单任务程序在同一时间只能为一个连接服务,而若是在这一刻同时有大量的请求发过来,由于服务器只能一个一个的处理,那么除了被处理的幸运儿,别的请求只能在原地等待。所以这种情况在客户端的体现就是,访问网页后小圈圈转了半天都没反应。设身处地想想,如果是自己在浏览网页时遇到网页半天打反应迟钝的情况会怎么办,如果是我,3-4秒打不开,绝对会送几个叉叉过去。

  2. 为此我们可以用多任务来解决中并发请求,提高访处理效率。
    而实现多任务并发的操作有很多种,这里会介绍常见的四种:多进程、多线程、协程 和 单进程单线程实现高并发操作。因为后两种涉及到一些底层实现和原理,所以我们放到下篇文章中在为大家详细介绍

二、并行与并发

  • 因为涉及到大量 操作系统 的内容,在此处就不复述,想要深究的朋友可以去书本中找到详解,在这里只进行简单的区分。

  • 并行和并发均指在同一时刻多个程序被cpu调度的情况

    并发:同一时刻多个程序交替执行

    并行:同一时刻多个程序同时执行

  • 并行和并发还有如下特点需要了解:

    1. 并发交替执行程序时,每一个程序占有cpu的时间非常短,这个时间可能比1微秒,1纳秒还要短,所以多个程序在我们看起来是同时运行,比如说你可以同时聊着QQ并且听着音乐。你以为这两个程序是在同时运行,其实对cpu来说,是交替执行的。所以并发只是 “看上去同时执行”
    2. 并行,在很早以前电脑还是单核的时代,并没有并行这个概念,随着技术进步,由于 cpu和内存速度差越来越大后,cpu厂商不得不放弃在单核cpu主频的提升,转向了多核心cpu研发。由于有多个cpu 的存在,自然而然的,并行这种真正的同时执行技术就诞生了。由此我们可以得到一个结论:并行程序的个数等于你cpu 的核数,如果还有其他程序,则按照并发执行。

三、多进程和线程

  • 此处同样不深究,只需知道这句话即可:
    进程是资源分配的最小单位,线程是CPU调度的最小单位,线程依赖于进程的存在。

  • python 中 使用 多进程:

    1. 多进程:
      在python中有两种常见的实现多进程的方法,一种是使用封装好的multiprocessing模块 ,第二种是使用os.fork()方法来创建子进程。后者只能在Unix/Linux环境下使用,而前者是通用的,所以我们介绍前者
    2. 关键代码:
      #需要导入的模块
      import mutiprocessing
      #创建单个子进程p 参数target为函数名的引用,args参数是一个元组,为该函数所需参数
      p = multiprocessing.Process(target=,args=)
      #启动进程
      p.start() #启动该进程
      #创建进程池,在任务多时,重复利用,节省资源 。参数为同时执行程序个数,此参数默认为cpu核数,若超出后超出的任务以并发执行
      po = multiprocessing.Pool()
      #向进程池中添加一个异步非阻塞式进程,参数同上
      po.apply_async(target,args)
      #向进程池中添加一个阻塞式进程,参数同上
      po.apply(target,args)
      #关闭进程池,不能再向池中添加进程
      po.close()
      #将池中进程加入执行队列,开始执行,执行前须先调用close()关闭
      po.join()
  • python 中 使用 多线程:

  1. 多线程:
    在python中我们使用threading模块,因为历时遗留问题(GIL)在python中,线程虽然可以实现多任务,但是效率并不高,所以在这里只介绍最基本的使用。
  2. 关键代码:
    #需要导入的模块
    import threading
    #创建单个子线程p 参数target为函数名的引用,args参数是一个元组,为该函数所需参数
    t = threading.Thread(target=,args=)
    #启动线程
    p.start() #启动该线程

四、代码实现

注:因为无论是进程还是线程,在服务器端所用的服务方法一样,只是在控制流程上有着不同,故为了减少冗余,将服务方法代码只列出一次:

def service(new_client):
    '''为新连接的客户端提供f服务'''
    
    # 1.接受客户端的请求
    request = new_client.recv(1024).decode('utf8')

    if request == '':
        print('客户端关闭链接')
        new_client.close()
    
    else:

        # 2.判断出客户端访问的页面

        request_lines = request.splitlines()
        print()
        print('-'*20)
        print(request)
        print('-'*20)
        print()
        
        pat = '.* (/.*) '
        res = re.match(pat,request_lines[0])
        if res:
            path = res.group(1)
        else:
            path = 'notfount.html'
        
        # 3. 读取客户端请求的数据
        
        if path == '/':
            path = '/index.html'

        try:
            f = open('./html'+path,'rb')
        except Exception:
            html_content = 'HTTP/1.1 404 NOT FOUND\r\n'
            html_content += '\r\n'
            html_body = '404 NOT FOUND'
        else:
            html_content = 'HTTP/1.1 200 OK\r\n'
            html_content += '\r\n'
            html_body = f.read()
            f.close()

        # 4. 发送数据给客户端
        new_client.send(html_content.encode('utf8'))
        new_client.send(html_body)



    # 5.关闭t客户端套接字
    new_client.close()

1.多进程版实现:

import socket
import re
import multiprocessing 


def main():
    '''http 服务器j控制流程'''
    
    # 1. 创建套接字
    tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    tcp_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 当服务器先调用close()时,可以保证在 c四次挥手后立刻释放资源,这样就h可以保证下次运行时,端口等资源不会被占用

    # 2. 绑定本地信息
    localaddr = ('',7788)
    tcp_socket.bind(localaddr)

    # 3. 设置为监听状态
    tcp_socket.listen(128)

    while True:
        # 4. 将套接字阻塞,等待链接
        new_client,client_addr = tcp_socket.accept()

        # 5. 开辟新进程为客户端服务
        p = multiprocessing.Process(target=service,args=(new_client,))
        p.start()
        
        # 5.1 因为子进程会拷贝主进程的资源,所以 主、子 进程中都存在指向new_client的链接,所以子进程中关闭new_client时,并不会真正的关闭,只有 主、子进程都无指向new_client的链接时才会真正关闭,接着做c四次挥手的动作 
        new_client.close()

    # 6. 关闭套接字
    tcp_socket.close()

if __name__ == '__main__':
    main()

2.多线程版实现:

import socket
import re
import threading 

def main():
    '''http 服务器控制流程'''
    
    # 1. 创建套接字
    tcp_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    tcp_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 当服务器先调用close()时,可以保证在 c四次挥手后立刻释放资源,这样就h可以保证下次运行时,端口等资源不会被占用

    # 2. 绑定本地信息
    localaddr = ('',7788)
    tcp_socket.bind(localaddr)

    # 3. 设置为监听状态
    tcp_socket.listen(128)

    while True:
        # 4. 将套接字阻塞,等待链接
        new_client,client_addr = tcp_socket.accept()

        # 5. 开辟新进程为客户端服务
        t = threading.Thread(target=service,args=(new_client,))
        t.start()
        

    # 6. 关闭套接字
    tcp_socket.close()

if __name__ == '__main__':
    main()

3.代码解读:
虽然使用线程和进程控制非常相似,代码重合度高,但是因为进程和线程本质上就不同,所以还是有不同之处比如:

  • 多进程版中存在 #5.1 步,而 多线程中没有:
    这一步也体现了线程和进程的本质区别,还记得那句话吗?进程是资源分配的最小单位,线程是CPU调度的最小单位,线程依赖于进程的存在。

  • 这是因为进程是资源分配的单位,当创建一个子进程时,子进程会将主进程中所有存在的资源都拷贝一份到自己的进程中,而线程只是cpu调度的单位,线程没有自己的资源,线程的资源在它的父进程中(这也是造成资源竞争的罪魁祸首,也是GIL存在的原因)。所以这也是进程虽然效率要高于线程,但是开辟一个进程耗费的资源确比开辟一个线程耗费的要多,
    所以在此例的多进程版本中,当主进程产生一个new_client套接字,并且让子进程去为这个新连接的客户端服务时, 主、子 进程中都存在指向new_client的链接,所以子进程中关闭new_client时,并不会真正的关闭,只有 主、子进程都无指向new_client的链接时才会真正关闭,接着做c四次挥手的动作。

五、结语

至此,多任务web服务器的两种实现就结束了,比起上一版本单任务服务器,无论是使用多线程版还是多进程版,效率都是指数及的增长。but 在实际使用中除了效率我们其实更关心的是稳定性。那么此版本的稳定性如何呢,答案是否定的。虽然多线程版多进程版解决了不能同时为多为客户服务的问题,但是也带来了新的问题,如果同时访问的人数过多呢? 1000次 10000次甚至更多,那么这个服务器是必挂无疑,道理很简单,你在电脑上打开1000个程序试试?也许50个电脑就已经很卡了,不到100个电脑可能都挂了。纵使是性能多强的cpu 也应付不过来,那么这个问题如何解决呢?使用协程来完成多任务是一种有效的方法,单线程其实也可以实现多任务,也是一种解决此问题的方法。这两种方法将在下篇中实现,请期待

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值