從零開始搭建一個簡易的服務(wù)器(一)


前言

其實大家大可不必被服務(wù)器這三個字嚇到冬殃,一個入門級后端框架,所需的僅僅是HTTP相關(guān)的知識與應(yīng)用這些知識的編程工具。據(jù)本人的經(jīng)驗辐怕,絕大多數(shù)人擁有搭建后端所涉及到的基礎(chǔ)理論知識,但是缺乏能將之應(yīng)用出去的工具从绘,而本文即是交給讀者這樣一個工具寄疏,并能夠運用之來實現(xiàn)一個可用的后端。

本文以基礎(chǔ)理論知識的運用為主僵井,并不會在服務(wù)器的穩(wěn)定性安全性上做探究陕截,同時為了避免大家在實現(xiàn)中被各種編程語言的獨有特性所困擾,本文選用選Python作為編程語言批什,并會附上詳細(xì)的代碼农曲。

一、最初的嘗試

超文本傳輸協(xié)議HyperText Transfer Protocol)是迄今為止互聯(lián)網(wǎng)應(yīng)用最為廣泛的協(xié)議渊季,平時大家在瀏覽器上瀏覽網(wǎng)頁朋蔫,逛淘寶,刷博客却汉,上知乎均是基于這種協(xié)議驯妄。

在互聯(lián)網(wǎng)七層架構(gòu)中HTTP位于TCP/UDP之上,這意味著我們我們可以在TCP/UDP層收發(fā)HTTP層的數(shù)據(jù)合砂,而能夠幫助我們在TCP/UDP層收發(fā)數(shù)據(jù)的最原始的一個工具------套接字青扔。

幾乎每一門編程語言都會原生支持套接字,所以本文選用套接字講解,而非python語言本身拿手的第三方庫微猖,套接字與基礎(chǔ)知識之間直接對接谈息,這樣不僅簡化學(xué)習(xí)成本,同時易于讀者從底層了解學(xué)習(xí)HTTP凛剥,也便于理解各種第三方庫的實現(xiàn)機理侠仇,可謂一舉三得。

在套接字的幫助下犁珠,我們可以寫下第一個服務(wù)器端的框架:

#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.余素,如圖:

怎么樣,是不是很有成就感炊昆,你的代碼“成功”響應(yīng)了瀏覽器的請求并回復(fù)了一個你設(shè)定好的字符串桨吊。

或許新入門的你對上述代碼有所疑惑,不著急凤巨,我們來慢慢過一遍上述代碼视乐。

s = socket(AF_INET, SOCK_STREAM)創(chuàng)建一個流式套接字用于TCP通信

s.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)設(shè)定當(dāng)前套接字,使其允許被復(fù)用

s.bind(('127.0.0.1', 8080))將當(dāng)前套接字綁定到ip地址為127.0.0.1磅甩,端口號為8080的連接上

注:雖然HTTP默認(rèn)端口為80炊林,但在linux下,監(jiān)聽80號端口需要root權(quán)限卷要。

s.listen(10)監(jiān)聽當(dāng)前套接字渣聚,設(shè)定并發(fā)數(shù)為10,即在多客戶端并發(fā)請求時僧叉,第11個及其以后的連接請求會被拒絕

conn,addr = s.accept()響應(yīng)一個連接請求

recv_data = conn.recv(64*1024)接收來自客戶端的數(shù)據(jù)奕枝,并設(shè)置緩沖區(qū)大小為64KB

resp_data = handle_request(recv_data)處理請求內(nèi)容,并生成回復(fù)字串

conn.sendall(resp_data)發(fā)送回復(fù)字串

conn.close()關(guān)閉與當(dāng)前客戶端的連接

二瓶堕、加入HTTP header

有了上述demo的基礎(chǔ)隘道,或許很多人會想,我是不是只要將自己的東西填入handle_request中就行了呢郎笆?誠然如此谭梗,但我們似乎還缺一點:如何區(qū)分瀏覽器申請的資源,即怎么知道瀏覽器要的是a.png還是b.txt宛蚓。

不著急激捏,我們先來普及一下url基本知識:

首先一個url通常有這樣的結(jié)構(gòu):http[s]://domain-name/path?query-string,例如:http://a.somesite.com/login.do?username=wierton&passwd=123456

其中http/httpsdomain-name含義自不用說凄吏,path指申請資源的完整路徑名远舅,query-string格式一般是數(shù)個鍵值對闰蛔,鍵值對之間用&連接,鍵與值之間用=連接图柏,例如:?username=wierton&password=123456序六,那如果鍵或值中需要使用&、=這兩個特殊符號呢蚤吹?這時候就要動用url編碼了例诀,其中=號對應(yīng)編碼%3D,&號對應(yīng)編碼%26裁着,因此我們只要在鍵值對中需要這兩個符號的地方將其替換為對應(yīng)的url編碼即可余佃。

有些url中還會有特殊符號#,其具體用途參見這里跨算。

上述內(nèi)容如何對應(yīng)到TCP連接中收到的數(shù)據(jù)呢?我們可以做如下一個簡單的實驗椭懊,只需將之前的代碼略作修改诸蚕,在函數(shù)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背犯,查看代碼的輸出,我們可以看到如下內(nèi)容:

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

容易發(fā)現(xiàn)盅抚,url中域名之后的內(nèi)容被原封不動的放在第一行GET字符串之后漠魏。那么代碼收到的除第一行外的這么多數(shù)據(jù)又是什么?有何用處妄均?

一個完整的HTTP請求應(yīng)至少包含一個完整的HTTP header柱锹,有時header后面還會附上data段(如POST請求中),上面代碼收到的即是一個HTTP header丰包,而一個HTTP header的第一行一般形如method path[?query-string] HTTP/version禁熏,method可為GET、POST邑彪、PUT瞧毙、HEAD、DELETE寄症、CONNECT宙彪、TRACE、OPTIONS有巧,不過一般常用的只有兩個GETPOST释漆,path表示申請服務(wù)器資源的完整路徑名,路徑名之后有時會附帶query-string剪决,兩者之間以符號?分隔灵汪,version表示協(xié)議的版本檀训,目前常用的是HTTP/1.1

第一行結(jié)束后享言,會跟上一個\r\n作為換行符(注意:是\r\n而非\n)峻凫,然后緊接著便是一行行由冒號分割開的鍵值對(關(guān)于這些鍵值對的較為詳細(xì)的含義可以參見這里),其中本文關(guān)注的字段有Host览露、Connection荧琼、User-Agent,同樣差牛,這些鍵值對之間也是以\r\n作為分隔符(換行符)命锄。當(dāng)然鍵值對的末尾還得加上一個空白行(\r\n),以區(qū)分開HTTP頭與主體數(shù)據(jù)偏化。

\r\n英文縮略為CRLF脐恩,在早期顯示器中,光標(biāo)移動\r\n是兩個分開的操作\r代表光標(biāo)移回行首侦讨,\n代表光標(biāo)移動到下一行水平坐標(biāo)不變的位置驶冒,也就是說現(xiàn)在的一個字符\n其實在早期是由兩個字符\r\n組成的,同時windows下至今沿用\r\n作為換行符韵卤。

作為服務(wù)器骗污,在拿到這一串header之后,首先要做的無疑是解析header沈条,分割開鍵與值需忿,并最好能將鍵值對存到Python的字典中去,如下便是將這些信息提取出來的代碼:

#coding=utf-8
import re

def parse_header(raw_data):
    if not '\r\n\r\n' in raw_data:
        print('Unable to parse the data:{}.'.format(raw_data))
        return False
    proto_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 False
    method, 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))

直接甩出這么一堆代碼蜡歹,或許你有點懵逼屋厘,不著急,我們來慢慢分析一下這段代碼月而,也許分析完擅这,你就能寫出比這更優(yōu)的代碼。

首先我們對客戶端傳來的數(shù)據(jù)做如下標(biāo)準(zhǔn)化假設(shè):

  • 換行符:在正式數(shù)據(jù)之前景鼠,換行符均為\r\n
  • 數(shù)據(jù)格式:first-line + key-value-pairs + \r\n + body
  • 首行:(GET|POST) path?params HTTP/1.1
  • 即只接受GET和POST兩種方法仲翎,同時只接受1.1版的HTTP協(xié)議。
  • 鍵值對:key : value + \r\n
  • 數(shù)據(jù)主體:body可為空

那么對于標(biāo)準(zhǔn)假設(shè)外的請求铛漓,采取一律拒絕掉的策略溯香,基于此假設(shè),我們再來回顧這段代碼:

if not '\r\n\r\n' in raw_data:如果不存在空白行浓恶,拒絕請求

proto_headers, body = raw_data.split('\r\n\r\n', 1)將原始數(shù)據(jù)以空白行分割為headerbody兩塊

proto, headers = proto_headers.split('\r\n', 1)將頭中的第一行與鍵值對分割開

ma = re.match(r'(GET|POST)\s+(\S+)\s+HTTP/1.1', proto)按標(biāo)準(zhǔn)假設(shè)匹配第一行玫坛,如果不能成功匹配,則拒絕請求

method, path = ma.groups()將正則表達(dá)式匹配到的分組內(nèi)容提取出來包晰,分別為methodpath[?query-string]

if path[0] == '/': path = path[1:]將路徑首部的'/'去掉湿镀,這一步是為后期做準(zhǔn)備炕吸,即將客戶端申請的絕對路徑轉(zhuǎn)化為服務(wù)器工作目錄的相對路徑(這里為了安全起見還可以對路徑進行判斷,即最終路徑如果不是落在工作目錄內(nèi)勉痴,就拒掉請求)

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加上=在分割)瀑罗,并轉(zhuǎn)化為一個元組塞到列表中,最終返回這個列表

ma_headers = re.findall(r'^\s*(.*?)\s*:\s*(.*?)\s*\r?$', headers, re.M)
headers = {item[0]:item[1] for item in ma_headers}
這里用正則表達(dá)式來匹配headers數(shù)據(jù)雏掠,并利用正則表達(dá)式的分組功能斩祭,將結(jié)果用生成器打包成一個字典

運行上述代碼,對如下數(shù)據(jù)進行解析:

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

得到結(jié)果如下:

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'}

本節(jié)到此為止乡话,下節(jié)會介紹如何將請求回復(fù)這一過程封裝摧玫,并利用正則表達(dá)式分解不同請求,將其引流至不同的handler绑青。


注:文中涉及的代碼均在python2.7下運行通過席赂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市时迫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谓晌,老刑警劉巖掠拳,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異纸肉,居然都是意外死亡溺欧,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門柏肪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姐刁,“玉大人,你說我怎么就攤上這事烦味∧羰梗” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵谬俄,是天一觀的道長柏靶。 經(jīng)常有香客問我,道長溃论,這世上最難降的妖魔是什么屎蜓? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮钥勋,結(jié)果婚禮上炬转,老公的妹妹穿的比我還像新娘辆苔。我一直安慰自己,他們只是感情好扼劈,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布驻啤。 她就那樣靜靜地躺著,像睡著了一般测僵。 火紅的嫁衣襯著肌膚如雪街佑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天捍靠,我揣著相機與錄音沐旨,去河邊找鬼。 笑死榨婆,一個胖子當(dāng)著我的面吹牛磁携,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播良风,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼谊迄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烟央?” 一聲冷哼從身側(cè)響起统诺,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎疑俭,沒想到半個月后粮呢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钞艇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年啄寡,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哩照。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡挺物,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出飘弧,到底是詐尸還是另有隱情识藤,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布次伶,位于F島的核電站蹋岩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏学少。R本人自食惡果不足惜剪个,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧扣囊,春花似錦乎折、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惕虑,卻和暖如春坟冲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溃蔫。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工健提, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人伟叛。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓私痹,卻偏偏與公主長得像,于是被迫代替她去往敵國和親统刮。 傳聞我的和親對象是個殘疾皇子紊遵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)侥蒙,斷路器暗膜,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測試 ...
    KeKeMars閱讀 6,313評論 0 6
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,367評論 0 17
  • 前幾天阿林來找我鞭衩,風(fēng)塵仆仆学搜,坐了兩天的車,從老家到城里醋旦。晚上到家的時候,他些許扭捏的坐在我們家沙發(fā)上会放,看著...
    yzy_lingo閱讀 343評論 0 0
  • 對于南鑼鼓巷咧最,當(dāng)時大抵記得那是外地人必去的一個地方捂人,因為那里有北京現(xiàn)在為數(shù)不多的胡同,并有一些過去的老建矢沿。老故事在...
    sara王閱讀 440評論 1 4