Python高级网络编程系列之终极篇---自己实现一个Web框架
通过前面几个小节的学习,现在我们想要把之前学到的知识点给串联起来,实现一个很小型的Web框架。虽然很小,但是用到的知识点都是比较多的。如Socket编程,装饰器传参在实际项目中如何使用。通过这一节的学习,希望能把我们以前的知识点掌握的更加牢靠!
一、客户端与服务器通信过程
在这里我们以浏览器来说明访问服务器时,会做什么处理!
说明:
TCP服务器:
1. 服务器主要是用来处理客户端的连接请求,然后把请求的url,传递给框架来处理具体的逻辑;
2. 服务器中需要定义一个处理响应头和状态码的函数,并作为参数传递给web框架的接口;
3. 服务器与web框架之间的通信主要是通过WSGI协议提供的接口,这样他们各司其职,耦合度很低
web框架:
在框架中需要解决的难题:
如何把url与相应的函数建立起关联?
浏览器发送不同的请求需要被不同的函数截取,并返回响应体。通过装饰器传递参数的方式就可以把url与函数建立对应关系。装饰器中的参数就是用来匹配url的。
二、代码实现
服务器端:
- import logging
- import socket
- import select
- import re
- import wsgi_web
- logging.basicConfig(level=logging.WARNING,
- filename='./web框架日志.txt',
- filemode='w',
- format='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s')
- class WebServer:
- """创建一个Web服务器"""
- def __init__(self):
- # 1.创建tcp socket
- self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # 2.设置地址可重用
- self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- # 3.绑定到某个端口
- self.tcp_socket.bind(('', 6060))
- # 4.设置监听队列
- self.tcp_socket.listen(128)
- def run(self):
- """运行服务器"""
- # 1.设置tcp_socket为非阻塞状态
- self.tcp_socket.setblocking(False)
- # 2.创建epoll对象,并注册服务器的监听事件
- epoll = select.epoll()
- epoll.register(self.tcp_socket.fileno(), select.EPOLLIN)
- client_dict = dict()
- # 3.不断遍历epoll列表,检查fd上有无事件发生
- while True:
- epoll_list = epoll.poll()
- for fd, event in epoll_list:
- if fd == self.tcp_socket.fileno():
- # 有客户端来连接服务器
- client_socket, client_addr = self.tcp_socket.accept()
- # 注册客户端的事件
- epoll.register(client_socket.fileno(), select.EPOLLIN)
- # 客户端与其fd要建立关联
- client_dict[client_socket.fileno()] = client_socket
- else:
- # 说明客户端发送数据过来
- # 通过fd来处理客户端的请求
- self.response_client(fd, client_dict, epoll)
- def response_client(self, client_fd, client_dict, epoll):
- """处理客户端的请求"""
- try:
- req_heads = client_dict[client_fd].recv(1024).decode('utf-8')
- except Exception as e:
- logging.warning(e)
- if not req_heads:
- # 1.关闭客户端
- client_dict[client_fd].close()
- # 2.从监听队列中移除该客户端
- client_dict.popitem()
- # 3.取消该客户端的注册事件
- epoll.unregister(client_fd)
- try:
- # print(req_heads.splitlines()[0])
- # 1.解析客户端的请求url
- match = re.match(r'[^/]+(/[^ ]*)', req_heads.splitlines()[0])
- if match:
- # 匹配成功
- filename = match.group(1)
- if filename == '/':
- filename = '/index.html'
- except Exception as e:
- logging.warning(e)
- # print('匹配数据出现了错误:{}'.format(e))
- # 2.根据文件名去动态加载,然后伪装成静态页面发送
- if filename.endswith('.html'):
- url_params = dict()
- # 1.使用WSGI接口
- # 通过不同文件构造一个字典
- url_params['filename'] = filename
- # print(url_params)
- # 定义一个函数传给框架
- body = wsgi_web.application(url_params, self.resp_heads)
- # 2.拼接数据然后发送给浏览器
- # 构造响应头信息
- # 空行
- # 响应体
- resp_head = 'HTTP/1.1 %s\r\n'%self.status_code
- for field in self.resp_fields:
- # 设置Content-Length的长度
- resp_head += '%s:%s\r\n'%(field[0], field[1] if field[0] != 'Content-Length' else len(body.encode('utf-8')))
- content = resp_head + '\r\n' + body
- client_dict[client_fd].send(content.encode('utf-8'))
- else:
- # 返回静态数据
- try:
- f = open('./static%s'%filename, 'rb')
- except Exception as e:
- # 读取失败
- body = 'Sorry! File not found!'
- resp_head = 'HTTP/1.1 404 Not Found\r\n'
- resp_head += 'Content-Length:%d\r\n'% len(body)
- resp_head += '\r\n'
- content = resp_head + body
- client_dict[client_fd].send(content.encode('utf-8'))
- # print('读取文件失败!')
- logging.warning(e)
- else:
- # 读取成功
- body = f.read()
- resp_head = 'HTTP/1.1 200 OK\r\n'
- resp_head += 'Content-Length:%d\r\n' % len(body)
- resp_head += '\r\n'
- try:
- client_dict[client_fd].send(resp_head.encode('utf-8'))
- client_dict[client_fd].send(body)
- except Exception as e:
- logging.warning(e)
- def resp_heads(self, status_code, fields):
- """在框架中使用的函数,框架用来返回响应头信息"""
- self.status_code = status_code
- self.resp_fields = fields
- def main():
- """程序入口"""
- # 1.初始化服务器
- server = WebServer()
- # 2.运行服务器
- server.run()
- if __name__ == '__main__':
- main()
框架部分:
- # 服务器给数据,返回数据给服务器
- import re
- from urllib.request import unquote #解码中文,只用在浏览器自动对中文编码
- import DBHelper
- # 定义空字典,用来存储路径跟对应的函数引用
- url_dict = dict()
- # start_response框架给服务器传响应头的数据
- # environ获取服务器传过来的文件路径
- def application(environ, start_response):
- """返回具体展示的界面给服务器"""
- start_response('200 OK', [('Content-Type', 'text/html;charset=utf-8'),
- ('Content-Length', '')]) # 返回响应头
- # 根据不同的地址进行判断
- file_name = environ['filename']
- for key, func in url_dict.items():
- match = re.match(key, file_name) # 地址跟规则一致
- if match:
- # 匹配到了
- return func(match) # 调用匹配到的函数引用,返回匹配的页面内容
- else:
- # 说明没找到
- return"不好意思,页面走丢了!"
- # 装饰器传参,用来完成路由的功能
- def route(url_address): # url_address表示页面的路径
- """目的自动添加路径跟匹配的函数到url字典中"""
- def set_fun(func):
- def call_fun(*args, **kwargs):
- return func(*args, **kwargs)
- # 根据不同的函数名称去添加到字典中
- url_dict[url_address] = call_fun
- return call_fun
- return set_fun
Python高级网络编程系列之终极篇---自己实现一个Web框架的更多相关文章
- Python高级网络编程系列之第一篇
在上一篇中我们简单的说了一下Python中网络编程的基础知识(相关API就不解释了),其中还有什么细节的知识点没有进行说明,如什么是TCP/IP协议有几种状态,什么是TCP三次握手,什么是TCP四次握 ...
- Python高级网络编程系列之第二篇
在上一篇中,我们深入探讨了TCP/IP协议的11种状态,理解这些状态对我们编写服务器的时候有很大的帮助,但一般写服务器都是使用C/Java语言,因为这些语言对高并发的支持特别好.我们写的这些简单的服务 ...
- Python高级网络编程系列之基础篇
一.Socket简介 1.不同电脑上的进程如何通信? 进程间通信的首要问题是如何找到目标进程,也就是操作系统是如何唯一标识一个进程的! 在一台电脑上是只通过进程号PID,但在网络中是行不通的,因为每台 ...
- Python高级网络编程系列之第三篇
在高级篇二中,我们讲解了5中常用的IO模型,理解这些常用的IO模型,对于编写服务器程序有很大的帮助,可以提高我们的并发速度!因为在网络中通信主要的部分就是IO操作.在这一篇当中我们会重点讲解在第二篇当 ...
- python 基础网络编程2
python 基础网络编程2 前一篇讲了socketserver.py中BaseServer类, 下面介绍下TCPServer和UDPServer class TCPServer(BaseServer ...
- 猫哥网络编程系列:HTTP PEM 万能调试法
注:本文内容较长且细节较多,建议先收藏再阅读,原文将在 Github 上维护与更新. 在 HTTP 接口开发与调试过程中,我们经常遇到以下类似的问题: 为什么本地环境接口可以调用成功,但放到手机上就跑 ...
- 猫哥网络编程系列:详解 BAT 面试题
从产品上线前的接口开发和调试,到上线后的 bug 定位.性能优化,网络编程知识贯穿着一个互联网产品的整个生命周期.不论你是前后端的开发岗位,还是 SQA.运维等其他技术岗位,掌握网络编程知识均是岗位的 ...
- python之网络编程
本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类: 消息传递(管道.FIFO.消息队列) 同步(互斥量.条件变量.读写锁.文件和写记录锁.信号量) 共享内存(匿名的和具名的) 远程过程调用 ...
- 网游中的网络编程系列1:UDP vs. TCP
原文:UDP vs. TCP,作者是Glenn Fiedler,专注于游戏网络编程相关工作多年. 目录 网游中的网络编程系列1:UDP vs. TCP 网游中的网络编程2:发送和接收数据包 网游中的网 ...
随机推荐
- js串口通信 调用MSCOMM32控件 链接电子秤(完整版实现方案)
硬件环境:RS232转USB串口线*1 电子秤*1(本人采用G&G E600Y-C型号称重仪) 电子秤原装RS232数据线*1 计算机*1 软件环境:RS232转USB串口线驱动(这个可以在串 ...
- Layui 获取 radio的值
var OutInvoiceType = $('#OutInvoiceType input[checked]').val(); 就可以获取到了.
- SqlHelper 1.0
SqlHelper类,可以简化对数据库的操作. 将程序中需要经常用到的数据库操作,如:连接字符串.对数据的增.删.改.查封装成“SqlHelper”类中的静态属性,方便在程序各部分进行调用. 增(in ...
- 8.并发容器ConcurrentHashMap#put方法解析
jdk1.7.0_79 HashMap可以说是每个Java程序员用的最多的数据结构之一了,无处不见它的身影.关于HashMap,通常也能说出它不是线程安全的.这篇文章要提到的是在多线程并发环境下的Ha ...
- Is the “*apply” family really not vectorized?
Question: So we are used to say to every R new user that "apply isn't vectorized, check out the ...
- MAC下搭建个人博客
安装homebrew ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/inst ...
- ES6学习之变量的解构赋值
前言:什么是ES6?ECMAScript 6(简称ES6)是JavaScript语言的下一代标准,已经在2015年6月正式发布了.其中相比较于ES5新增了诸多的特性,并且ES6可转换为ES5的语法.- ...
- FHQ Treap小结(神级数据结构!)
首先说一下, 这个东西可以搞一切bst,treap,splay所能搞的东西 pre 今天心血来潮, 想搞一搞平衡树, 先百度了一下平衡树,发现正宗的平衡树写法应该是在二叉查找树的基础上加什么左左左右右 ...
- 解决element-ui的el-select组件文字超过宽度时不出现横向滚动条问题
我用的element-ui是V1.4.3. 目前遇到一个问题,在用el-select组件的时候,当选项的内容很长的时候,会撑开下拉菜单的宽度,这样影响美观.具体如下图所示: 解决这个问题的思路:设置下 ...
- 2018-01-19 Xtext试用: 5步实现一个(中文)JVM语言
续上文Xtext试用: 快速实现简单领域专用语言(DSL). 基于官方教程: Five simple steps to your JVM language 达成如下语言: 它被Quan6JvmMode ...