Python网络编程及WebServer

网络编程

  • 复习:Linux基本操作
    • ctrl A 到命令行首
    • ctrl E到命令行末
    • ifconfig查看网络状态
    • mv 文件重命名
    • cp 拷贝文件到
  • vim基本操作
    • 命令模式下:
      • 直接跳转到某行:行号+G
      • 复制光标所在行粘贴到下一行:yyp
      • 跳到行末并进入编辑模式:A
      • 跳到行首:I
      • 选中后剪切:d
      • 粘贴:p
      • 选中后左移:<
      • 在光标行前面插入一行:O
      • 后插入一行:o
    • vim xxx.py +4 打开文件后光标在第四行
  • 基础知识

socket通信

  • 完成网络通信必备的东西

     import socket
    # 创建套接字
     socket.socket(AddressFamily, Type)	 
    # 使用套接字收/发数据
     s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   # tcp
     s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)	# udp
    # 关闭套接字
     s.close()
    
  • 在Linux中使用sublime编程,相关Linux操作看“Linux基础巩固”,需要总结“面试常考点

  • 常见的通信协议有两种:TCP和UDP

  • UDP Socket发送数据:

    # 要发送数据,必须确定对方端口
    
    from socket import *
    
    # 1. 创建udp套接字
    udp_socket = socket(AF_INET, SOCK_DGRAM)
    
    while True:
        if send_data == "exit":
            break
        # 2. 准备接收方的地址
        # '192.168.1.103'表示目的ip地址
        # 8080表示目的端口
        dest_addr = ('192.168.1.103', 8080)  # 注意 是元组,ip是字符串,端口是数字
    
        # 3. 从键盘获取数据(准备数据)
        send_data = input("请输入要发送的数据:")
    
        # 4. 发送数据
        udp_socket.sendto(send_data.encode('utf-8'), dest_addr)
    
    # 5. 关闭套接字
    udp_socket.close()
    
    # ubuntu下运行程序使用:	roy@ubuntu:$ python3 xxx.py 
    
  • 在发送之前ping一下,看网络通不通;虚拟机要改成桥接模式,如果此时IP还是不在同一网段,使用命令sudo dhclient,静静等待;

  • 错误提示TypeError: need is object not str意思就是别发字符串;解决方案:在字符串前面写个b,即编程字节对象;或者如代码所示,encode

  • 在ubuntu中python3ipython3都是交互模式,后者类似jupyter;

  • UDP接收数据:

    # 要接收数据,必须确定自身端口,需要绑定,这个IP也得是自己的(可以不写)
    # 总之,确定端口和IP就万事大吉
    
    # 程序不是从第一行开始写的
    
    from socket import *
    
    def main():
        # 1. 创建套接字
        udp_socket = socket(AF_INET, SOCK_DGRAM)
        # 2. 绑定端口(本地信息)
        local_addr = ('', 7788)
        udp_socket.bind(local_addr)
        # 3. 接收数据
        recv_data = udp_socket.recvfrom(1024)  # 1024表示本次接收的最大字节数
        	# 如果没有收到数据会在此处 阻塞
        send_addr = recv_data[0]
        recv_msg = recv_data[1]
        # 4. 打印接收到的数据
        print("%s:%s"%(str(send_addr), recv_msg.decode("gbk")))	# 从Windows来的数据要用gbk解码
        # 5. 关闭套接字
        udp_socket.close()
    # 从这开始写代码
    if __name__ == "__main__":
        main()
    
  • 小结:

    • 发送方和接收方各自搞清楚必要参数,发送方是对方的(IP,端口),接收方是绑定端口(一个应用至少占据一个端口用于通信);套接住咯,诶~
    • 为何说“必要”参数呢?因为无论是作为发送方还是接收方,都需要有自己的端口才能发送数据的(tcp和udp是端对端的),但在这里发送方没有bind,所以OS会随机分配一个端口;
    • 因为端口是套接字的对接目标,所以在同一电脑(IP)上的不同程序(端口)可以互发数据,“互发”意思就是创建一个套接字即可,可收可发;
    • 套接字是全双工的;
  • 聊天器

    • 程序在接收数据时若没有缓存消息,一闪而过,即OS会暂存收到的消息;这也存在弊端,可能会缓存过多信息,占用内存导致死机;(由于现在是单任务,只能实现半双工)
      1
    • 这里发送IP可以写127.0.0.1回环地址,自己发自己收;也可以查看ubuntu自身IP,实现自娱自乐;

    TCP通信

  • UDP不安全,类似写信;TCP类似打电话,需要链接,有确认机制

  • TCP有拥塞控制和可靠传输机制

    • 拥塞控制:指数递增线性增长、快开始
    • 可靠传输包括:超时重传、错误校验
  • TCP是严格的客户服务器模式

    • 客户端:需要链接
      from socket import *
      
      # 创建socket
      tcp_client_socket = socket(AF_INET, SOCK_STREAM)	# TCP
      
      # 服务器信息
      server_ip = input("请输入服务器ip:")
      server_port = int(input("请输入服务器port:"))
      
      # 链接服务器
      tcp_client_socket.connect((server_ip, server_port))
      
      # 提示用户输入数据
      send_data = input("请输入要发送的数据:")
      
      # 先发(请求)
      tcp_client_socket.send(send_data.encode("gbk"))
      
      # 接收对方发送过来的数据,最大接收1024个字节
      recvData = tcp_client_socket.recv(1024)
      print('接收到的数据为:', recvData.decode('gbk'))
      
      # 关闭套接字
      tcp_client_socket.close()
      
    • 服务器:要绑定,再运行

      from socket import *
      
      # 创建socket
      tcp_server_socket = socket(AF_INET, SOCK_STREAM)
      
      # 本地信息
      address = ('', 7788)	# tuple
      # 绑定
      # 绑不绑定要看是否接收数据,如果只发送,不绑也行
      tcp_server_socket.bind(address)
      
      # 使用socket创建的套接字默认是主动的,
      # 使用listen将其变为被动的,接收别人的链接,可套接
      tcp_server_socket.listen(128)	# 监听套接字负责等待有新客户链接
      
      # accept()负责产生一个新的套接字 client_socket 专门为这个客户端服务
      # clientAddr 即“来电显示”
      client_socket, clientAddr = tcp_server_socket.accept()	# 默认阻塞,等待客户端connect()
      
      # 接收对方发送过来的数据(先收)
      recv_data = client_socket.recv(1024)  # 函数的写法和UDP不同
      print('接收到的数据为:', recv_data.decode('gbk'))
      
      # 发送一些数据到客户端
      client_socket.send("thank you !".encode('gbk'))
      
      # 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
      client_socket.close()
      
      tcp_server_socket.close()
      

      区别在于,服务器端会产生两个套接字,分别用于监听和收发数据;
      服务器端要先收数据(响应),客户端要先发数据(请求);然后互相收发;

  • 和UDP的主要区别:

    • 严格的客户服务器模式,分离
    • 需要链接,而不是简单的告知(IP+端口)
      2

TCP文件下载器

  • 客户端

    from socket import *
    
    def main():
        # 创建socket
        tcp_client_socket = socket(AF_INET, SOCK_STREAM)
    
        # 目的信息
        server_ip = input("请输入服务器ip:")	# 这个可以绑死
        server_port = int(input("请输入服务器port:"))
    
        # 链接服务器
        tcp_client_socket.connect((server_ip, server_port))
    
        # 输入需要下载的文件名
        file_name = input("请输入要下载的文件名:")
    
        # 发送文件下载请求(先建立连接,发请求,再收!)
        tcp_client_socket.send(file_name.encode("utf-8"))
    
        # 接收对方发送过来的数据,最大接收1024个字节(1K)
        recv_data = tcp_client_socket.recv(1024)
        # print('接收到的数据为:', recv_data.decode('utf-8'))
        # 如果接收到数据再创建文件,否则不创建
        if recv_data:
            # 由于读写期间可能会出异常,需要捕获,with的作用是不用手动捕获并close
            with open("[接收]"+file_name, "wb") as f:
                f.write(recv_data)
                # with一般用于'w'模式,若要读取文件,加上try...except...捕获
    
        # 关闭套接字
        tcp_client_socket.close()
        
    if __name__ == "__main__":
        main()
    
  • 服务端:

    from socket import *
    import sys
    
    def get_file_content(file_name):
        """获取文件的内容"""	# 函数注释
        try:	# 可能文件不存在,这是读写文件的标准写法
            with open(file_name, "rb") as f:
                content = f.read()
            return content
        except:
            print("没有下载的文件:%s" % file_name)
    
    # 运行程序即启动服务器,输入参数:[0]是此程序文件名,[1]是端口号
    def main():
        # sys.argv[]其实就是一个列表,里边的项为用户输入的参数,且参数是从程序外部输入的,例如:python3 test.py a b c		# abc就是外部输入参数,用列表接收
        if len(sys.argv) != 2:
            # 保证输入了端口参数
            print("请按照如下方式运行:python3 xxx.py 7890")
            return
        else:
            # 运行方式为python3 xxx.py 7890
            port = int(sys.argv[1])
    
        # 创建socket
        tcp_server_socket = socket(AF_INET, SOCK_STREAM)
        # 本地信息
        address = ('', port)
        # 绑定本地信息(需要接收客户端信息)
        tcp_server_socket.bind(address)
        # 将主动套接字变为被动套接字
        tcp_server_socket.listen(128)	# 决定可以有多少个客户端连接,涉及到高并发了
    
        while True:	# 服务端继续运行
            # 等待客户端的链接,即为这个客户端发送文件
            client_socket, clientAddr = tcp_server_socket.accept()  # 阻塞
            # 接收对方发送过来的数据
            recv_data = client_socket.recv(1024)  # 接收1024个字节; 阻塞
            file_name = recv_data.decode("utf-8")
            print("对方请求下载的文件名为:%s" % file_name)
            file_content = get_file_content(file_name)
            # 发送文件的数据给客户端
            # 因为获取打开文件时是以rb方式打开,因此不需要encode编码
            if file_content:
                client_socket.send(file_content)
            # 关闭这个套接字
            client_socket.close()
    
        # 关闭监听套接字
        tcp_server_socket.close()
    
    if __name__ == "__main__":
        main()
    
  • TCP注意点:
    3

  • 关闭监听套接字,已经建立连接的accept套接字不会断的哦;

  • 服务端recv套接字解阻塞有两种方式:客户端关闭(挂电话)、服务端收到数据

  • 结合多任务可以实现服务多个用户

Web服务器

  • 实现一个web服务器,可以参考

HTTP协议

  • 浏览器和服务器之间的传输协议;什么是协议?为服务而制定的规范
  • 问题:在Chrome中输入http://www.baidu.com 会发生什么?
    4
    5
    • 经典问题,可以搜索资料,可以回答的简单,可以回答的详细,很考验水平
  • GET表示向服务器要数据POST表示给服务器提交数据

Chrome

  • 使用Chrome->检查功能,其中的Network会将整个交互过程记录
    6

    • 这里对消息分块(chunked),长连接使用,方便确定啥时候断开
  • 要具体看浏览器请求和服务器回送的数据,点击 view source

    // 服务器回送信息
    HTTP/1.1 200 OK
    Cache-Control: no-cache
    Connection: keep-alive
    Content-Encoding: gzip
    Content-Type: text/html;charset=utf-8
    Coremonitorno: 0
    Date: Fri, 08 Jan 2021 09:05:20 GMT
    Server: apache
    Set-Cookie: H_WISE_SIDS=107320_110085_127969_128698_131424_144966_151532_154619_155932_156289_156849_160573_161395_161840_162152_162156_163233_163321_163567_163805_163837_164020_164108_164163_164219_164697_164940_164955_164963_165070_165087_165236_165328_165523_165552_165564_165652_165698_165716_165736_165813_165848_166056_166143_166147_166167_166177_166180_166184_166209_166277_166282_166312_166449_166852_167107; path=/; expires=Sat, 08-Jan-22 09:05:20 GMT; domain=.baidu.com
    Set-Cookie: bd_traffictrace=081705; expires=Thu, 08-Jan-1970 00:00:00 GMT
    Set-Cookie: rsv_i=0d3ejV%2BeAsqCMFZjR8yzjtJZF3%2Fo6FYGIBXa%2F50wTW7hXSVtbGWWSXntA4sywlJiaUx1y4ZjyUXUJDkXU%2Bl4cy9fMIfE8Rs; path=/; domain=.baidu.com
    Set-Cookie: BDSVRTM=568; path=/
    Set-Cookie: eqid=deleted; path=/; domain=.baidu.com; expires=Thu, 01 Jan 1970 00:00:00 GMT
    Set-Cookie: __bsi=; max-age=3600; domain=m.baidu.com; path=/
    Strict-Transport-Security: max-age=172800
    Traceid: 161009672003635589229344073941350584633
    Vary: Accept-Encoding
    Transfer-Encoding: chunked
    
    // 浏览器回送信息
    0 GET /index.html HTTP/1.1
    1 Host: 127.0.0.1:7890
    2 User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:84.0) Gecko/20100101 Firefox/84.0
    3 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
    4 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    5 Accept-Encoding: gzip, deflate
    6 Connection: keep-alive
    7 Upgrade-Insecure-Requests: 1
    

Cookie

  • 存储用户浏览记录,用户画像
  • 服务器可以向浏览器发送cookie值并保存,Set-Cookie,判断用户是否登录成功之类的;

简单实现

分析需求

  • TCP服务器
  • 给浏览器回送简单信息即可

代码验证

  • 启动下面的程序(作为服务器)
  • 在浏览器(客户端)中输入:https://127.0.0.1:7890
    7
  • 收到的客户端的request就是view source中的内容
  • 在这里换行要用 \r\n,服务器总是要先绑定端口
    8

TCP握手挥手

9

  • 本来建立链接也是四次握手,形式上类似于四次挥手,但服务端返回的时候可以直接带上自己的seq,让客户端返回ack=seq+1
    10

  • 原则:seq看本方上一次,ack看对方seq,都是加1;

  • 客户端发送FIN信号之后不再发送数据,但仍能接受数据;客户端回完最后的ACK之后双方还会等待至少一个RTT,防止消息丢失需要重发

  • 四次挥手之后服务器仍占用着其端口,这也是客户端(先关闭)一般不绑定端口的原因,避免占用;

  • 注:绑定和确认不是一回事

    # 设置服务器四次挥手之后能立即释放资源,保证下次运行端口不被占用
    server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    

返回页面

  • 使用课件05python和linux高级编程阶段 / 1-6代码和截图 / 07-http协议、http服务器的实现-1

  • html文件夹放到服务器程序所在目录

    import socket
    import re
    
    def service_client(new_socket):
        # 接收浏览器请求
        request = new_socket.recv(1024).decode("utf-8")
        lines = request.splitlines()	# 拆分成行方便解析
        # print(lines)
        # 使用正则解析请求并返回
        file_name = ""
        # 如下第一行要匹配index.html
        # GET /index.html HTTP/1.1 
        # 斜杠开始,空格停
        ret = re.match(r"[^/]+(/[^ ]*)", lines[0])	# * 代表0个或多个,可能无文件名
    	if ret:	# /index.html
            file_name = ret.group(1)
            if file_name == "/":
                file_name = "/index.html"
                
        try:
            # 尝试打开file_name文件,也可以用with结构
            f = open("./html" + file_name, "rb")
            # with open(file_name, "rb") as f:	content = f.read()	send()
        except:
            # 未找到文件
            response = "HTTP/1.1 404 \r\n"	# 给后台的
            response += "\r\n"
            response += "File Not Found"	# 给前端的;固定信息格式
            new_socket.send(response.encode("utf-8"))
        else:
            # 成功打开文件
            html_content = f.read()
            f.close()
            response = "HTTP/1.1 200 OK\r\n"
            response += "\r\n"
            
            new_socket.send(response.encode("utf-8"))
            new_socket.send(html_content)	# rb,无需编码
            
        new_socket.close()
            
    def main():
        tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 解除占用
        tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        tcp_socket.bind("",7890)	# 必须绑定
        tcp_socket.listen()
        
        while True:
            new_socket, client_addr = tcp_socket.accept()
    		service_client(new_socket)
            
        tcp_socket.close()
    if __name__=="__main__":
        main()
    

多进程服务器

  • 都是放在while循环创建

多进程

  • 子进程复制父进程的全局和局部变量

    import socket
    import re
    import multiprocessing
    import threading
    
    def service_client(new_socket):
        # 接收浏览器请求
        request = new_socket.recv(1024).decode("utf-8")
        lines = request.splitlines()	# 拆分成行方便解析
        # print(lines)
        # 使用正则解析请求并返回
        file_name = ""
        # 如下第一行要匹配index.html
        # GET /index.html HTTP/1.1 
        # 斜杠开始,空格停
        ret = re.match(r"[^/]+(/[^ ]*)", lines[0])	# * 代表0个或多个,可能无文件名
    	if ret:	# /index.html
            file_name = ret.group(1)
            if file_name == "/":
                file_name = "/index.html"
                
        try:
            # 尝试打开file_name文件,也可以用with结构
            f = open("./html" + file_name, "rb")
            # with open(file_name, "rb") as f:	content = f.read()	send()
        except:
            # 未找到文件
            response = "HTTP/1.1 404 \r\n"	# 给后台的
            response += "\r\n"
            response += "File Not Found"	# 给前端的;固定信息格式
            new_socket.send(response.encode("utf-8"))
        else:
            # 成功打开文件
            html_content = f.read()
            f.close()
            response = "HTTP/1.1 200 OK\r\n"
            response += "\r\n"
            
            new_socket.send(response.encode("utf-8"))
            new_socket.send(html_content)	# rb,无需编码
            
        new_socket.close()
            
    def main():	    # 主进程
        tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 释放后解除占用
        tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        tcp_socket.bind("",7890)	# 必须绑定
        tcp_socket.listen()
        
        while True:
            new_socket, client_addr = tcp_socket.accept()
            
    		p = multiprocessing.Process(target=service_client,args=(new_socket,))
            p.start()
            
            # 多线程
            # t = threading.Thread(target=service_client,args=(new_socket,))
            # t.start()
            # 可以使用vim全局替换:命令模式下
            	# :%s/multiprocessing/threading/g
            # 不需要关闭 new_socket,线程共享进程资源,内部解决即可
            
            new_socket.close()	# 进程复制,相当于使用硬链接指向同一socket文件,所以子进程关闭后实际上new_socket还未关闭,所以在此调用close(),外部协商
            
        tcp_socket.close()
    if __name__=="__main__":
        main()
    

协程

  • 【注】html文件夹通过共享文件夹到VM中的mnt/hgfs/shareDocument

    from gevent import monkey
    import gevent
    import socket
    import sys
    import re
    
    monkey.patch_all()
    
    class WSGIServer(object):
        """定义一个WSGI服务器的类"""
    
        def __init__(self, port, documents_root):	# 7890 ./html
            # 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
    
        def run_forever(self):
            """运行服务器"""
            # 等待对方链接
            while True:
                new_socket, new_addr = self.server_socket.accept()
                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')	# 阻塞
                # 当浏览器接收完数据后,会自动调用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"
                    # header不是二进制文件,编码一下相加
                    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)
    
    # 设置服务器服务静态资源时的路径
    DOCUMENTS_ROOT = "./html"
    
    def main():
        """控制web服务器整体"""
        # python3 xxxx.py 7890
        if len(sys.argv) == 2:
            port = sys.argv[1]	# xxx.py 后面为外部参数
            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()
    

单进程非阻塞

  • 多进程(多任务)可以实现非阻塞,再开一个进程/线程/协程服务即可

  • 单进程阻塞则会导致后续用户无法连接,影响用户体验;这里用单进程实现非阻塞

  • 无论单进程还是多进程,都是考虑并发,因为CPU单核;多核仅是可以并行

  • 监听模式下 accept() 处会阻塞,recv() 处也会阻塞;可都设置为非阻塞模式,但代码的写法如下:

    from socket import *
    import time
    
    # 用来存储所有的新链接的socket
    g_socket_list = list()
    
    def main():
        server_socket = socket(AF_INET, SOCK_STREAM)
        server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR  , 1)
        server_socket.bind(('', 7890))
        server_socket.listen(128)
        # 将监听套接字设置为非堵塞
        server_socket.setblocking(False)
    
        while True:
            # 用来测试
            time.sleep(0.5)
            try:
                newClientInfo = server_socket.accept()	# 没收到请求必报异常
            except Exception as result:
                pass
            else:
                print("一个新的客户端到来:%s" % str(newClientInfo))
                # 新客户端套接字设置为非堵塞
                newClientInfo[0].setblocking(False)
                g_socket_list.append(newClientInfo)
    
    		# 使用for循环每次都处理一下已经连接的客户端套接字,看有没有数据
            # 因为不阻塞了,可能上次你抛弃人家的时候正有数据过来呢
            for client_socket, client_addr in g_socket_list:
                try:
                    recvData = client_socket.recv(1024)
                    if recvData:
                        print('recv[%s]:%s' % (str(client_addr), recvData))
                    else:	# 客户端关闭也会返回空值,不报异常
                        print('[%s]客户端已经关闭' % str(client_addr))
                        # 收到b''
                        client_socket.close()	# 只有客户端主动关闭才能关闭套接字
                        g_socket_list.remove((client_socket,client_addr))	
    			# 产生异常代表没有数据过来,并不能移除,可能在路上
                except Exception as result:	# 之前说的是这里也可以关闭,未收到数据
                    # print(result) 	# 调试信息
                    pass
            print(g_socket_list)
            
    # 操作系统会将recv数据缓存,即有缓冲区
    
    if __name__ == '__main__':
        main()
    
    • Ubuntu安装了网络调试助手,默认在opt/目录下
    • Ubuntu安装Sublime,Linux可以使用subl fileName打开文件

长短连接

  • 长连接:三次握手建立连接后,返回数据后不会立即释放,等待下一次数据请求

    • 由于网页元素日益丰富,短连接意味着同一时刻有更多的请求连接服务器,从而给服务器造成很大的压力
  • 短连接:请求一次数据建立一次连接
    在这里插入图片描述
    在这里插入图片描述

  • **HTTP1.1**都是使用长连接,但是前面的程序返回数据后new_socket.close(),属于短连接;

  • 按照以前的代码,服务器回送一次数据后,浏览器并不知道服务器是否发送完毕:

    # 加上这句即可
    response_headers += "Content-Length:%d\r\n" % len(response_body)
    # 可以详细了解《大话HTTP协议》
    

单进程非阻塞长连接

  • 代码如下:实现单进程非阻塞长连接服务器

    import time
    import socket
    import sys
    import re
    
    
    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)
            # 4.非阻塞
            self.server_socket.setblocking(False)
            self.client_socket_list = list()
            self.documents_root = documents_root
    
        def run_forever(self):
            """运行服务器"""
    
            # 等待对方链接
            while True:
                try:
                    new_socket, new_addr = self.server_socket.accept()
                except Exception as ret:
                    print("-----1----", ret)  # for test
                else:
                    new_socket.setblocking(False)
                    # 添加新的客户端到列表
                    self.client_socket_list.append(new_socket)
                    
                # 循环遍历新客户端,接收请求(核心代码)
                for client_socket in self.client_socket_list:
                    try:
                        # 解码客户端请求
                        request = client_socket.recv(1024).decode('utf-8')
                    except Exception as ret:
                        print("------2------", ret)  # for test
                    else:
                        if request:
                            self.deal_with_request(request, client_socket)
                        else:
                            client_socket.close()
                            self.client_socket_list.remove(client_socket)
    
                print(self.client_socket_list)
    
    
        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"
    
                # 将header编码,加上body
                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()
    
  • 总结:单进程非阻塞:来了就连,遍历响应

  • 如果前一个请求处理时间较长?还是阻塞的

epoll

  • epoll的好处在于单个process就可以同时处理多个网络连接的IO

    • 被公认为Linux2.6下性能最好的多路I/O就绪通知方法
  • 原理:可以同时监听多个socket,当某个socket有数据到达了,就通知用户进程

  • 当今的nginx就是基于这个原理

  • 共享内存,事件通知

    • 因为for循环本质上还是把请求复制到kernel,加入到单核的进程洪流之中等待响应
    • epoll原理
      • 内存映射(mmap)即共享内存;
      • 文件描述符(fd)这里标记套接字文件(Linux一切皆文件),内核采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知
      • 可以把epoll理解为这块特殊的内存+通知机制
        在这里插入图片描述
  • 通过代码详细介绍过程:epoll版的http服务器

    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对应的套接字,每个套接字对应fd存在共享内存,省的复制socket
            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)
                        # 记录(fd,socket),后面用
                        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:	# 参数索引从1开始
            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()
    
  • 总结:为什么单进程的epoll最快?
    从多进程/线程/协程到单进程非阻塞,再到epoll,本质都需要并发;进程切换非常耗费资源(多路并发),线程协程次之(并发中的并发);单进程并发需要复制资源(套接字)给OS扫描(一路并发);而epoll采用共享内存事件通知,使用文件描述符省去了资源复制的开销,用回调实现通知,速度更快(一路并发);

  • 或者说epoll可以同时监听多个socket请求,对处理资源时分复用(单进程非阻塞模型),并发的效率更高

  • 思考题:

    • 4核CPU,使用python清洗数据,使用多进程还是多线程?
    • 答:分批处理数据,属于CPU密集型任务,由于存在GIL锁(可以认为python是thread safe;I/O 操作时会释放这把锁),会对线程造成阻塞,我选择创建四个进程多任务处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Roy_Allen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值