前言

其实大家大可不必被服务器这三个字吓到,一个入门级后端框架,所需的仅仅是HTTP相关的知识与应用这些知识的编程工具。据本人的经验,绝大多数人拥有搭建后端所涉及到的基础理论知识,但是缺乏能将之应用出去的工具,而本文即是交给读者这样一个工具,并能够运用之来实现一个可用的后端。

本文以基础理论知识的运用为主,并不会在服务器的稳定性安全性上做探究,同时为了避免大家在实现中被各种编程语言的独有特性所困扰,本文选用选Python作为编程语言,并会附上详细的代码。

一、最初的尝试

超文本传输协议HyperText Transfer Protocol)是迄今为止互联网应用最为广泛的协议,平时大家在浏览器上浏览网页,逛淘宝,刷博客,上知乎均是基于这种协议。

在互联网七层架构中HTTP位于TCP/UDP之上,这意味着我们我们可以在TCP/UDP层收发HTTP层的数据,而能够帮助我们在TCP/UDP层收发数据的最原始的一个工具——套接字。

几乎每一门编程语言都会原生支持套接字,所以本文选用套接字讲解,而非python语言本身拿手的第三方库,套接字与基础知识之间直接对接,这样不仅简化学习成本,同时易于读者从底层了解学习HTTP,也便于理解各种第三方库的实现机理,可谓一举三得。

在套接字的帮助下,我们可以写下第一个服务器端的框架:

#coding=utf-8
import re
from socket import *def handle_request(request):return 'Welcome to wierton\'s site's = socket(AF_INET, SOCK_STREAM)
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)s.bind(('127.0.0.1', 8080))
s.listen(10)
while 1:conn,addr = s.accept()print("connected by {}".format(addr))recv_data = conn.recv(64*1024)resp_data = handle_request(recv_data)conn.sendall(resp_data)conn.close()
s.close()

上述框架能够干嘛呢?想要实验上述代码的效果,你只要在浏览器中输入127.0.0.1:8080,然后你就会看到一行字符串Welcome to wierton's site.,如图:

怎么样,是不是很有成就感,你的代码“成功”响应了浏览器的请求并回复了一个你设定好的字符串。

或许新入门的你对上述代码有所疑惑,不着急,我们来慢慢过一遍上述代码。

s = socket(AF_INET, SOCK_STREAM)创建一个流式套接字用于TCP通信

s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)设定当前套接字,使其允许被复用

s.bind(('127.0.0.1', 8080))将当前套接字绑定到ip地址为127.0.0.1,端口号为8080的连接上

注:虽然HTTP默认端口为80,但在linux下,监听80号端口需要root权限。

s.listen(10)监听当前套接字,设定并发数为10,即在多客户端并发请求时,第11个及其以后的连接请求会被拒绝

conn,addr = s.accept()响应一个连接请求

recv_data = conn.recv(64*1024)接收来自客户端的数据,并设置缓冲区大小为64KB

resp_data = handle_request(recv_data)处理请求内容,并生成回复字串

conn.sendall(resp_data)发送回复字串

conn.close()关闭与当前客户端的连接

二、加入HTTP header

有了上述demo的基础,或许很多人会想,我是不是只要将自己的东西填入handle_request中就行了呢?诚然如此,但我们似乎还缺一点:如何区分浏览器申请的资源,即怎么知道浏览器要的是a.png还是b.txt

不着急,我们先来普及一下url基本知识:

首先一个url通常有这样的结构:http[s]://domain-name/path?query-string,例如:http://a.somesite.com/login.do?username=wierton&passwd=123456

其中http/httpsdomain-name含义自不用说,path指申请资源的完整路径名,query-string格式一般是数个键值对,键值对之间用&连接,键与值之间用=连接,例如:?username=wierton&password=123456,那如果键或值中需要使用&、=这两个特殊符号呢?这时候就要动用url编码了,其中=号对应编码%3D,&号对应编码%26,因此我们只要在键值对中需要这两个符号的地方将其替换为对应的url编码即可。

有些url中还会有特殊符号#,其具体用途参见这里。

上述内容如何对应到TCP连接中收到的数据呢?我们可以做如下一个简单的实验,只需将之前的代码略作修改,在函数handle_request的第一行加上print(request),修改后代码如下:

#coding=utf-8
import re
from socket import *def handle_request(request):print(request)return 'Welcome to wierton\'s site's = socket(AF_INET, SOCK_STREAM)
s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)s.bind(('127.0.0.1', 8080))
s.listen(10)
while 1:conn,addr = s.accept()print("connected by {}".format(addr))recv_data = conn.recv(64*1024)resp_data = handle_request(recv_data)conn.sendall(resp_data)conn.close()
s.close()

运行代码,并在浏览器中输入127.0.0.1:8080/login.do?username=wierton&passwd=
123456,查看代码的输出,我们可以看到如下内容:

GET /login.do?username=wierton&passwd=123456 HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, l
ike Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp
,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8

容易发现,url中域名之后的内容被原封不动的放在第一行GET字符串之后。那么代码收到的除第一行外的这么多数据又是什么?有何用处?

一个完整的HTTP请求应至少包含一个完整的HTTP header,有时header后面还会附上data段(如POST请求中),上面代码收到的即是一个HTTP header,而一个HTTP header的第一行一般形如method path[?query-string] HTTP/versionmethod可为GET、POST、PUT、HEAD、DELETE、CONNECT、TRACE、OPTIONS,不过一般常用的只有两个GETPOSTpath表示申请服务器资源的完整路径名,路径名之后有时会附带query-string,两者之间以符号?分隔,version表示协议的版本,目前常用的是HTTP/1.1

第一行结束后,会跟上一个\r\n作为换行符(注意:是\r\n而非\n),然后紧接着便是一行行由冒号分割开的键值对(关于这些键值对的较为详细的含义可以参见这里),其中本文关注的字段有Host、Connection、User-Agent,同样,这些键值对之间也是以\r\n作为分隔符(换行符)。当然键值对的末尾还得加上一个空白行(\r\n),以区分开HTTP头与主体数据。

\r\n英文缩略为CRLF,在早期显示器中,光标移动\r\n是两个分开的操作\r代表光标移回行首,\n代表光标移动到下一行水平坐标不变的位置,也就是说现在的一个字符\n其实在早期是由两个字符\r\n组成的,同时windows下至今沿用\r\n作为换行符。

作为服务器,在拿到这一串header之后,首先要做的无疑是解析header,分割开键与值,并最好能将键值对存到Python的字典中去,如下便是将这些信息提取出来的代码:

#coding=utf-8
import redef parse_header(raw_data):if not '\r\n\r\n' in raw_data:print('Unable to parse the data:{}.'.format(raw_data))return Falseproto_headers, body = raw_data.split('\r\n\r\n', 1)proto, headers = proto_headers.split('\r\n', 1)ma = re.match(r'(GET|POST)\s+(\S+)\s+HTTP/1.1', proto)if not ma:print('unsupported protocol')return Falsemethod, path = ma.groups()if path[0] == '/':path = path[1:]lis = path.split('?')lis.append('')rfile, query_string = lis[0:2]params = [tuple((param+'=').split('=')[0:2])for param in query_string.split('&')]ma_headers = re.findall(r'^\s*(.*?)\s*:\s*(.*?)\s*\r?$', headers, re.M)headers = {item[0]:item[1] for item in ma_headers}print("version\t: 1.1")print("method\t: {}".format(method))print("path\t: {}".format(rfile))print("params\t: {}".format(params))print("headers\t: {}".format(headers))

直接甩出这么一堆代码,或许你有点懵逼,不着急,我们来慢慢分析一下这段代码,也许分析完,你就能写出比这更优的代码。

首先我们对客户端传来的数据做如下标准化假设:
- 换行符:在正式数据之前,换行符均为\r\n
- 数据格式:first-line + key-value-pairs + \r\n + body
- 首行:(GET|POST) path?params HTTP/1.1
* 即只接受GET和POST两种方法,同时只接受1.1版的HTTP协议。
- 键值对:key : value + \r\n
- 数据主体:body可为空

那么对于标准假设外的请求,采取一律拒绝掉的策略,基于此假设,我们再来回顾这段代码:

if not '\r\n\r\n' in raw_data:如果不存在空白行,拒绝请求

proto_headers, body = raw_data.split('\r\n\r\n', 1)将原始数据以空白行分割为headerbody两块

proto, headers = proto_headers.split('\r\n', 1)将头中的第一行与键值对分割开

ma = re.match(r'(GET|POST)\s+(\S+)\s+HTTP/1.1', proto)按标准假设匹配第一行,如果不能成功匹配,则拒绝请求

method, path = ma.groups()将正则表达式匹配到的分组内容提取出来,分别为methodpath[?query-string]

if path[0] == '/': path = path[1:]将路径首部的’/’去掉,这一步是为后期做准备,即将客户端申请的绝对路径转化为服务器工作目录的相对路径(这里为了安全起见还可以对路径进行判断,即最终路径如果不是落在工作目录内,就拒掉请求)

lis = path.split('?'); lis.append(''); rfile, query_string = lis[0:2]以?将路径与query-string分割开

params = [tuple((param+'=').split('=')[0:2]) for param in
query_string.split('&')]
这里使用生成器来简化代码,将其展开的话意思就是将query_string按&分割成若干个token,每个token按=分割成前后两部分(为了防止某些token没有=,这里将token加上=在分割),并转化为一个元组塞到列表中,最终返回这个列表

ma_headers = re.findall(r'^\s*(.*?)\s*:\s*(.*?)\s*\r?$', headers, re.M)
headers = {item[0]:item[1] for item in ma_headers}
这里用正则表达式来匹配headers数据,并利用正则表达式的分组功能,将结果用生成器打包成一个字典

运行上述代码,对如下数据进行解析:

GET /login.do?username=wierton&passwd=123456 HTTP/1.1
Host: 127.0.0.1:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, l
ike Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp
,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8

得到结果如下:

version : 1.1
method  : GET
path    : login.do
params  : [('username', 'wierton'), ('passwd', '123456')]
headers : {'Accept-Language': 'zh-CN,zh;q=0.8', 'Accept-Encoding': 'gzip, deflate, sdch', 'Connection': 'keep-alive', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp', 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, l', 'Host': '127.0.0.1:8080', 'Upgrade-Insecure-Requests': '1'}

本节到此为止,下节会介绍如何将请求回复这一过程封装,并利用正则表达式分解不同请求,将其引流至不同的handler。


本文如有不当或错误之处,欢迎在评论区指出(但拒绝回复攻击性、侮辱性言论),转载时请注明出处。

从零开始搭建一个简易的服务器(一)相关推荐

  1. 【Python】快速创建一个简易 HTTP 服务器(http.server)

    引言 http.server 是 socketserver.TCPServer 的子类,它在 HTTP 套接字上创建和监听,并将请求分派给处理程序.本文是关于如何使用 Python 的 http.se ...

  2. 快速搭建一个简易的HTTP服务器用于文件分享与下载

    需要快速搭建一个简易的HTTP服务器进行文件的分享与下载.主要有以下两种方法: 1. 使用python 来实现 import http.server import socketserverPORT = ...

  3. python -m http.server 搭建一个简易web下载服务器(可用于快速发送大文件)

    在工作过程中需要经常发送安装包等大文件给同事,亲测共享文件夹拷贝.QQ.微信等聊天工具,大文件传输速度效率不高. 然而局域网内使用python搭建的简易下载服务器速度比较快,百兆网卡下载速度随便达到1 ...

  4. github项目怎么运行_利用 GitHub 从零开始搭建一个博客

    "NightTeam",一个值得加星标的公众号. 趁着周末,搭建了一下 NightTeam 的官方博客和官方主页,耗时数个小时,两个站点终于完工了. 由于 NightTeam 的域 ...

  5. 利用 GitHub 从零开始搭建一个博客

    "NightTeam",一个值得加星标的公众号. 趁着周末,搭建了一下 NightTeam 的官方博客和官方主页,耗时数个小时,两个站点终于完工了. 由于 NightTeam 的域 ...

  6. 如何利用 GitHub 从零开始搭建一个博客

    趁着周末,搭建了一下 NightTeam 的官方博客和官方主页,耗时数个小时,两个站点终于完工了. 由于 NightTeam 的域名是 nightteam.cn,所以这里官方博客使用了二级域名 blo ...

  7. 如何搭建一个简易的Web框架

    Web框架本质 什么是Web框架, 如何自己搭建一个简易的Web框架?其实, 只要了解了HTTP协议, 这些问题将引刃而解. 简单的理解:  所有的Web应用本质上就是一个socket服务端, 而用户 ...

  8. 从零开始实现一个简易的Java MVC框架(九)--优化MVC代码

    前言 在从零开始实现一个简易的Java MVC框架(七)--实现MVC中实现了doodle框架的MVC的功能,不过最后指出代码的逻辑不是很好,在这一章节就将这一部分代码进行优化. 优化的目标是1.去除 ...

  9. 从零开始搭建你的Web服务器

    http://www.hello-code.com/blog/architecture/201506/5147.html 有天一个女士出门散步,路过一个建筑工地,看到三个男人在干活.她问第一个男人,& ...

最新文章

  1. RelativeLayout不能居中的解决的方法
  2. 计算机网络离不开光缆,九年级物理全册 第二十一章 第四节 越来越宽的信息之路习题课件 新人教版.ppt...
  3. 【数据结构与算法】之深入解析“不同的二叉搜索树”的求解思路与算法示例
  4. Timer定时器Demo
  5. 宁夏警官学院计算机专业,宁夏警官职业学院毕业设计模板.docx
  6. 无心剑《英语学习漫谈》
  7. 一些实用的Chrome插件
  8. 3dmax详细讲解全套攻略在线视频教程
  9. paip. 'QObject::QObject(const QObject)' is private问题的解决.
  10. 参考 雷霄骅https://blog.csdn.net/leixiaohua1020/article/list/28
  11. 一位基金经理13年的期货感悟
  12. 理想主义者与现实主义者的差别
  13. 电子邮箱的工作原理以及SMTP、POP3、IMAP之间的联系和区别
  14. 超神之路-MySQL
  15. websocketpp wss
  16. 大数据产业链包括哪几个环节,具体包含哪些内容
  17. UTC、GMT、时间戳之间的关系
  18. aiwi国内最大体感游戏平台 领跑体感游戏第一线
  19. MySQL8免安装版下载安装与配置(linux)
  20. HTMLCSS基础学习笔记1.30-选择器是什么

热门文章

  1. I2C协议靠这16张图彻底搞懂(超详细)
  2. Linux基础简答题八个(含答案)
  3. springmvc运行时,Failed to read candidate component class;nested exception is java.lang.IllegalArgument
  4. Windows操作系统用注册表删除启动项
  5. element popover源码
  6. 不可不知的居家风水要素
  7. centos7搭建nps实现内网穿透
  8. 相似度算法和距离算法
  9. 小甜点,RecyclerView 之 ItemDecoration 讲解及高级特性实践
  10. 【log4j】下载、安装、使用