自己动手写web服务器

feimat 发布于 2016/03/02 10:58
阅读 2K+
收藏 12

 项目主要实现一个脚本启动即可当web服务器使用,目前封装web服务器常用功能。

欢迎加群一起学习进步: 339711102 (it民工群)

 项目代码 https://git.oschina.net/feimat/fastpy

本项目尽量少使用语言原生库,期望网友一起开发不同语言版本

第一步: 首先要有个处理网络异步io的模块

这一步相信大部分做后台开发的程序员都做过,模式大同小异,处理流程如下

 

第二步: 支持多平台

第一步中的 epoll_fd用一个跨平台的事件通知类ev_fd代替

Linux unix可使用epoll,win使用Select,freebsd使用kqueue

这里拿epoll举例 类提供poll、register、unregi、modify函数

事件类型统一为EV_DIS、EV_IN、EV_OUT

 

第三步:http分包

对第一步中的3,tcp收到包后,由于是粘包的,需要进行http分包

服务器可以不考虑chunk模式,根据content-lenth来进行分包即可

 

第四步: http解析

拆分出一个完整的http包后接着就要解析这个http

这里其实可以使用python原生http解析类,不过为了以后扩展语言,自己再封装而且速度比原生快

对于k=v&k2=v2类似的内容通过&、=号分割解析

对multipart格式的带文件form内容用boundary分割解析

解析后存放到:

headers: 头部 (字典)

form:  post参数,包括form表单 (字典)

getdic: url参数 (字典)

filedic: form表单中文件 (字典)

rfile: 原始http content内容  (字符串)

action: url/最后一个单词

command:  (get or post or put or delete)

path: url (字符串)

http_version: http版本号 (http 1.1)

 

第五步:支持大文件上传

如果用户是上传大文件几百M甚至几G怎么办,socket收到的数据,要落地磁盘缓存

然后解析时,使用boundary分割并用文件句柄指向内容的开头即可,不需读进内存,使用时再读

另外每次当前socket关闭时要删除之前缓存文件,节省磁盘.

 

第六步:支持静态文件下载

一般静态文件下载都是使用sendfile系统调用实现,这样减少内存拷贝(sendfile是磁盘直接到socket缓冲区,调用send的话,用户程序还要从磁盘read到内存,多了一次拷贝)

然而

1、考虑到以后扩展语言有些不支持sendfile,python 3.5之后才支持sendfile系统调用,网上开源的sendfile功能不全,不支持自己控制每次send大小

2、另外实际上下载文件的瓶颈在网络上,多一次内存拷贝性能也不会有很大下降

3、对于频繁下载的文件加速,还得加载进内存缓存和压缩

这里我们直接使用send支持静态文件下载。

每次读100k去send,send完了再读。直到egain或全部发送完。

 

第七步:支持Gzip压缩、Etag客户端缓存、断点续传等小功能

Web服务器有好多小功能,这里不会全部覆盖,挑选了几个必须用到的功能举例

1、Gzip压缩 对于指定类型的文件或者http 头部指定要gzip压缩的返回结果使用gzip压缩

2、etag客户端缓存 last-modified检测

检查缓存有

时间戳

last-modified 服务器返回的最后修改时间

 last-modified 客户端请求到的上次修改时间

文件hash

Etag 服务器返回文件hash

If-none-match 客户端请求到的上次文件hash

如果以上两对都为发生改变 则返回304 not modified

对于etag由于计算hash较慢,在只有修改时间戳不一致的条件后再计算文件hash

然后计算hash还有好多优化方法 例如分块计算hash遇到不一致马上返回,用修改后的时间戳做etag。

3、断点续传。就是http头带range:byte=100-499指定要下载文件哪部分,然后

HTTP/1.1 206 Partial Content

Content-Range: bytes 100-499/102400 返回那部分内容和总大小给客户端即可

 

第八步:支持http/https正向代理

大家应该会有时需要搭建Squid或apache翻墙看看新闻或者下...此处省略若干字,因为nginx不支持正向https代理,squid安装又比较麻烦,这里直接提供几十行脚本快速编写自己的http/https正向代理

代理原理很简单

1、如果是http,在代理进程proxy里,使用http头部的host(真实远程服务端)的地址建立个socket连接,然后把proxy recv到的数据send给远端,再把从远端recv到的数据send回client即可

2、如果是https,client会发来

CONNECT XXX HTTP1.1

HOST XXX:443    格式的内容包,我们取到host然后创建socket连接,然后返回给client

HTTP/1.1 200 Connection Established,然后就和http一样的流程继续走了

这里要注意的是connect是阻塞的,proxy需要使用多线程或起协程来connect呦

另外如果接受完数据远程服务器关闭proxy要记得等数据发送给client成功再关闭呦

 

第九步: 支持cgi编写和运行时更新

上面完成了支持静态服务器常用功能,下面提供支持动态cgi编写功能。这个实现方式太多了,这里就是简单的把解析后的http内容扔到多线程或协程然后去加载脚本来执行即可。

运行时加载就是每收到一个请求要查看下py脚本的修改时间有没和内存中的一样,不一样就重新加载

Python动态加载脚本

 

到此一个简单的web服务器和webcgi框架就写完了,不到800行的一个脚本fastpy.py,启动就能使用:

1、启动:

   指定监听端口即可启动 ,可在linux win freebsd使用

   python fastpy.py 8998

 

2、编写cgi (默认多线程模式,安装gevent后自动切换为协程模式)

   在fastpy.py同一目录下

   随便建一个python 文件

   例如:

   example.py:

   #定义一个同名example类

   #定义一个test函数:

   FastpyAutoUpdate=True #表示支不支持动态更新

   class example():

       def test(self, request, response_head):

           #print request.form

           #print request.getdic

           #fileitem = request.filedic["upload_file"]

           #fileitem.filename

           #fileitem.file.read()

           request.ret(200,"ccb"+request.path)

 

   则访问该函数的url为 http://ip:port/example.test

  

   test函数必须带两个参数

   request:表示请求的数据 默认带以下属性

      headers: 头部 (字典)

      form:  post参数,包括form表单 (字典)

      getdic: url参数 (字典)

      filedic: form表单中文件 (字典)

      rfile: 原始http content内容  (字符串)

      action: python文件名 (这里为example)

      method: 函数方法    (这里为tt)

      command:  (get or post or put or delete or head...)

      path: url (字符串)

      http_version: http版本号 (http 1.1)

   response_head: 表示response内容的头部

      例如如果要返回用gzip压缩

      则增加头部

      response_head["Content-Encoding"] = "gzip"

 

3、静态文件下载

   虚拟目录默认放在fastpy同目录的static目录下

   访问  http://ip:port/static/ 即可查看该文件夹

 

4、使用正向代理功能

   python proxy.py 8997

指定端口启动后,浏览器配置使用即可

 

欢迎加群一起学习进步: 339711102 (it民工群)

 项目代码 https://git.oschina.net/feimat/fastpy

 

 

 

加载中
返回顶部
顶部