感谢传智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发送多个请求。
- 发送一个新的请求的前提是,上一个发送的请求已经收到了响应
- 服务器响应,是根据设置
Content-Length
来表示响应长度,这样才能让浏览器确定已经收到了所有数据,继而发送下一个请求 - **负责发送内容的函数中,不要断开连接,交给 遍历函数来负责执行 **
- 等浏览器请求全部完毕后,等浏览器主动关闭连接,服务器之后断开连接
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-Length: %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多路复用的几种:
- select: 采用遍历轮询,并且还有同时能维护的数量
- poll:取消了数量问题,还是轮询
- 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,这是允许的
暂时省略