从零开始搭建一个简易的服务器(一)
前言
其实大家大可不必被服务器这三个字吓到,一个入门级后端框架,所需的仅仅是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/https
与domain-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/version
,method
可为GET、POST、PUT、HEAD、DELETE、CONNECT、TRACE、OPTIONS
,不过一般常用的只有两个GET
和POST
,path
表示申请服务器资源的完整路径名,路径名之后有时会附带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)
将原始数据以空白行分割为header
与body
两块
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()
将正则表达式匹配到的分组内容提取出来,分别为method
与path[?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。
本文如有不当或错误之处,欢迎在评论区指出(但拒绝回复攻击性、侮辱性言论),转载时请注明出处。
从零开始搭建一个简易的服务器(一)相关推荐
- 【Python】快速创建一个简易 HTTP 服务器(http.server)
引言 http.server 是 socketserver.TCPServer 的子类,它在 HTTP 套接字上创建和监听,并将请求分派给处理程序.本文是关于如何使用 Python 的 http.se ...
- 快速搭建一个简易的HTTP服务器用于文件分享与下载
需要快速搭建一个简易的HTTP服务器进行文件的分享与下载.主要有以下两种方法: 1. 使用python 来实现 import http.server import socketserverPORT = ...
- python -m http.server 搭建一个简易web下载服务器(可用于快速发送大文件)
在工作过程中需要经常发送安装包等大文件给同事,亲测共享文件夹拷贝.QQ.微信等聊天工具,大文件传输速度效率不高. 然而局域网内使用python搭建的简易下载服务器速度比较快,百兆网卡下载速度随便达到1 ...
- github项目怎么运行_利用 GitHub 从零开始搭建一个博客
"NightTeam",一个值得加星标的公众号. 趁着周末,搭建了一下 NightTeam 的官方博客和官方主页,耗时数个小时,两个站点终于完工了. 由于 NightTeam 的域 ...
- 利用 GitHub 从零开始搭建一个博客
"NightTeam",一个值得加星标的公众号. 趁着周末,搭建了一下 NightTeam 的官方博客和官方主页,耗时数个小时,两个站点终于完工了. 由于 NightTeam 的域 ...
- 如何利用 GitHub 从零开始搭建一个博客
趁着周末,搭建了一下 NightTeam 的官方博客和官方主页,耗时数个小时,两个站点终于完工了. 由于 NightTeam 的域名是 nightteam.cn,所以这里官方博客使用了二级域名 blo ...
- 如何搭建一个简易的Web框架
Web框架本质 什么是Web框架, 如何自己搭建一个简易的Web框架?其实, 只要了解了HTTP协议, 这些问题将引刃而解. 简单的理解: 所有的Web应用本质上就是一个socket服务端, 而用户 ...
- 从零开始实现一个简易的Java MVC框架(九)--优化MVC代码
前言 在从零开始实现一个简易的Java MVC框架(七)--实现MVC中实现了doodle框架的MVC的功能,不过最后指出代码的逻辑不是很好,在这一章节就将这一部分代码进行优化. 优化的目标是1.去除 ...
- 从零开始搭建你的Web服务器
http://www.hello-code.com/blog/architecture/201506/5147.html 有天一个女士出门散步,路过一个建筑工地,看到三个男人在干活.她问第一个男人,& ...
最新文章
- RelativeLayout不能居中的解决的方法
- 计算机网络离不开光缆,九年级物理全册 第二十一章 第四节 越来越宽的信息之路习题课件 新人教版.ppt...
- 【数据结构与算法】之深入解析“不同的二叉搜索树”的求解思路与算法示例
- Timer定时器Demo
- 宁夏警官学院计算机专业,宁夏警官职业学院毕业设计模板.docx
- 无心剑《英语学习漫谈》
- 一些实用的Chrome插件
- 3dmax详细讲解全套攻略在线视频教程
- paip. 'QObject::QObject(const QObject)' is private问题的解决.
- 参考 雷霄骅https://blog.csdn.net/leixiaohua1020/article/list/28
- 一位基金经理13年的期货感悟
- 理想主义者与现实主义者的差别
- 电子邮箱的工作原理以及SMTP、POP3、IMAP之间的联系和区别
- 超神之路-MySQL
- websocketpp wss
- 大数据产业链包括哪几个环节,具体包含哪些内容
- UTC、GMT、时间戳之间的关系
- aiwi国内最大体感游戏平台 领跑体感游戏第一线
- MySQL8免安装版下载安装与配置(linux)
- HTMLCSS基础学习笔记1.30-选择器是什么