文章目录
网络编程
- 复习:Linux基本操作
ctrl A
到命令行首ctrl E
到命令行末ifconfig
查看网络状态mv
文件重命名cp
拷贝文件到
- vim基本操作
- 命令模式下:
- 直接跳转到某行:行号+G
- 复制光标所在行粘贴到下一行:yyp
- 跳到行末并进入编辑模式:A
- 跳到行首:I
- 选中后剪切:d
- 粘贴:p
- 选中后左移:<
- 在光标行前面插入一行:O
- 后插入一行:o
vim xxx.py +4
打开文件后光标在第四行
- 命令模式下:
- 基础知识
- 见本人CSDN博客
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中
python3
和ipython3
都是交互模式,后者类似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会暂存收到的消息;这也存在弊端,可能会缓存过多信息,占用内存导致死机;(由于现在是单任务,只能实现半双工)
- 这里发送IP可以写127.0.0.1回环地址,自己发自己收;也可以查看ubuntu自身IP,实现自娱自乐;
TCP通信
- 程序在接收数据时若没有缓存消息,一闪而过,即OS会暂存收到的消息;这也存在弊端,可能会缓存过多信息,占用内存导致死机;(由于现在是单任务,只能实现半双工)
-
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+端口)
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注意点:
-
关闭监听套接字,已经建立连接的accept套接字不会断的哦;
-
服务端recv套接字解阻塞有两种方式:客户端关闭(挂电话)、服务端收到数据
-
结合多任务可以实现服务多个用户
Web服务器
- 实现一个web服务器,可以参考
HTTP协议
- 浏览器和服务器之间的传输协议;什么是协议?为服务而制定的规范
- 问题:在Chrome中输入
http://www.baidu.com
会发生什么?
- 经典问题,可以搜索资料,可以回答的简单,可以回答的详细,很考验水平
- GET表示向服务器要数据,POST表示给服务器提交数据
Chrome
-
使用Chrome->检查功能,其中的
Network
会将整个交互过程记录
- 这里对消息分块(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
- 收到的客户端的request就是view source中的内容
- 在这里换行要用
\r\n
,服务器总是要先绑定端口
TCP握手挥手
-
本来建立链接也是四次握手,形式上类似于四次挥手,但服务端返回的时候可以直接带上自己的seq,让客户端返回
ack=seq+1
;
-
原则: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 操作时会释放这把锁),会对线程造成阻塞,我选择创建四个进程多任务处理