网络协议详解之 HTTP 协议
概述
HTTP(HyperText Transfer Protocal)超文本传输协议, 是一个基于请求与响应模式的、无状态的应用层协议。它是 WEB 上应用最广泛的协议。它一般基于 TCP 的连接方式。其主要特点有:
- 支持 C/S 通信模式
- 简单快速。HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信快。
- 灵活。HTTP 协议允许客户端和服务端传输任意类型任意格式的数据。不同的类型由 Content-Tyoe 标记
- 面向无连接。无连接是指每次建立的连接只处理一个文请求。
- 无状态。无状态是指协议对于事务处理没有记忆能力。如果后续处理需要前面的信息,则它必须重传。这样可有导致每次连接传送的数据量增大。
HTTP URL
URL(Uniform Resource Locator) 统一资源定位符,它包含了查找某个资源的信息。其格式如下:http://host[:port][abs_path]
http指定协议的名称,表示要通过HTTP协议来定位网络资源。host是一个合法的网络域名 或 IP 地址。port指定使用的网络端口,缺省值为80。abs_path指定请求的资源的URI,如果URL中没有给出URI, 则必须以 "/" 符号结束(这个工作通常由浏览器完成)。
连接
浏览器与服务器联系的最常用方法是与服务器的 80 端口建立 TCP 连接。使用 TCP 的意义在于,浏览器和服务器都不需要担心如何处理长消息、可靠性与拥塞控制,这些事将由 TCP 来处理。 在早期的 HTTP1.0 中,连接建立起来后会在一个请求和一个响应后立即释放。因为那时的HTML很简单,基本只有文本,使用这种模式就够了。但是随着时代的发展,HTML里包含了太多的东西,使用单独的TCP来传递每个资源代价太大。于是 HTTP1.1 诞生了,它支持持续连接(persistent connection
),它可以在一个 TCP 连接上进行多次请求响应,还可以发送流水线请求。这种做法减少建立多个 TCP 连接所用的时间,减少服务器的空闲时间,提高了性能。
HTTP 协议的请求
每个HTTP Request(请求) 由一行或多行ASCII文本组成。其中第一行的第一个词为请求方法的名称,然后是请求资源的URI,再后是协议的版本。这几个部分使用空格分开:Method Request-URI HTTP-Version CRLF
Method为请求方法,必须为大写。 Request-URI 是一个统一资源标识符。 HTTP-Version 表示请求的HTTP协议版本。CRLF表示换行符。 例如:GET about.html HTTP/1.1
常用的请求方法如下:
类别 | 意义 |
---|---|
GET | 请求 Request-URI 所标识的资源 |
POST | 在Request-URI所标识的资源后附加数据 |
HEAD | 仅获取所标识的资源的响应消息报头 |
PUT | 请求服务器存储一个资源,并用 Request-URL 作为其标识 |
DELETE | 请求服务删除 Request-URI 所标识的资源 |
TRACE | 请求服务器回传收到的请求。一般用于调试或诊断 |
CONNECT | 保留使用 |
OPTIONS | 请求查询服务器的性能或查询与资源相关的选项或需求 |
Request 从第二行开始为请求消息头,每个消息头以 CRLF 结尾。消息头在后面会单独讲。下面是一个 Request 的实例:
GET /about/ HTTP/1.1
Host: pugqq.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:49.0) Gecko/20100101 Firefox/49.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
DNT: 1
Connection: keep-alive
Upgrade-Insecure-Requests: 1
关于 Request 的更多内容请参考这里。
HTTP 协议的响应
服务器在接收和解释请求的消息后,返回一个HTTP Response(响应)
消息。每个HTTP响应由一个状态行
、0到多个消息头
,以及消息体
组成。状态行与每个消息头均以CRLF
结束,消息体与消息头之间以一个空行(CRLF)隔开。 状态行中包括一个 3 位数的状态码,标示请求是否被满足。状态行格式如下:HTTP-Version Status-Code Readson-Phrase CRLF
其中, HTTP-Version 表示服务器 HTTP 协议的版本. Status-Code 表示服务器发回的响应状态代码. Reason-Phrase 表示状态码的文本描述.
状态码由三位数字组成,其中第一位表示类别 。
表头 | 表头 |
---|---|
1xx | 信息。请求已接收,继续处理 |
2xx | 成功。请求已被成功接收、理解、处理 |
3xx | 重定向。要完成请求需要有进一步的操作 |
4xx | 客户端错误。请求有语法错误或无法处理 |
5xx | 服务端错误。服务器未能实现请求 |
常见的状态码:
- 200: OK 请求成功
- 302: 重定向
- 400: BadRequest 客户端请求语法错误
- 401: Unauthorized 未授权
- 403: Forbidden 服务器拒绝服务
- 404: NotFound 错误的URL,资源不存在
- 500: InternalServerError 服务器发生不可预期的错误
- 503: Server Unavailable 服务器当前不能处理请求,请稍后重试
消息头(报头)将在下一节中讲到.关于 Response 的更多内容请参考这里。
HTTP 协议的报头
请求行后面可能还有附加的行,其中包含了更多信息,它们被称为请求头(request header).同样的,响应消息也有响应行(response header).每个请求和响应通常具有不同的头。 下表列出了一些常用的消息头:
Header | Type | Content |
---|---|---|
User-Agent | 请求 | 浏览器及平台的信息 |
Accept | " | 客户可处理的页面类型 |
Accept-Charset | " | 客户可处理的字符集 |
Accept-Encoding | " | 客户可处理的页面编码 |
Accept-Language | " | 客户可处理的自然语言 |
If-Modified-Since | " | 检查新鲜度的时间及日期 |
If-None-Match | " | 为检查新鲜度而发送的标签 |
Host | " | 服务器DNS名称 |
Authorization | " | 客户的信任凭据 |
Referer | " | 发出请求的先前URL |
Cookie | " | 给服务器发回Cookie的先前URL |
Set-Cookie | 响应 | 客户存储的Cookie |
Server | " | 服务器信息 |
Content-Encoding | " | 内容编码 |
Content-Language | " | 页面的自然语言 |
Content-Length | " | 页面长度(字节) |
Content-Type | " | 页面MIME类型 |
Last-Modified | " | 页面最后个性的时间 |
Excepires | " | 页面过期时间 |
Location | " | 告诉客户向谁发送请求 |
Accept-Ranges | " | 告诉客户服务器能接受的字节范围 |
Date | 请求/响应 | 发送消息的时间 |
Range | " | 标识一个页面的一部分 |
Cache-Control | " | 指示如何处理缓存 |
Upgrade | " | 发送方希望希望切换的协议 |
User-Agent
允许客户将它的浏览器信息告知服务器(如 Mozilla/5.0 ) 服务器可以据此给不同的浏览器发送不同的响应。毕竟不同浏览器和行为是各不一样的。Accept
客户端通过 4 个 Accept 头告知服务器用户端可以接受哪些信息。分别为 MIME类型(如 text/html)、字符集(如 ISO-8859-5)、压缩方法(如 gzip)、自然语言(如 zh-cn)Host
是服务器的名称,它取自URL.这个对是强制性的。因为有些 IP 地址可能对应多个 DNS 名称。Referer
根据此信息服务器可以知道浏览器是从哪个页面到达此页面的Last-Modified/Excepires
在页面缓存中有重要作用Upgrade
用来切换到一个新的通信协议
HTTP 实践
基于 Python socket 的简单 http 服务器
这里我们将使用 Python 来完成一个简单的 http 服务器,它将用来接收浏览器发出的 Request ,进行处理并返回 Response.
1.整体流程
首先建立一个 TCP 连接,在连接中我们接收来客户端数据,如果检测到是 Http 请求,则进行处理,并返回响应
#======================== config =====================
HOST = '127.0.0.1' #服务器地址
PORT = 9891 #服务端端口
BUFSZ = 10240 #支持最大 Request 长度
STATICPATH = './template/' #静态资源路径
STATUS_CODE = {200:'OK',404:'Not Found',500:'InternalServerError'} #定义状态码及状态描述
#======================= main =======================
ADDR = (HOST,PORT)
skt = socket(AF_INET,SOCK_STREAM) #建立socket
skt.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) #设置地址可复用
skt.bind(ADDR) #绑定地址
skt.listen(5)
while True:
tcpClientSkt,addr = skt.accept()
print 'accept from [%s,%s]' % (str(addr[0]),str(addr[1]))
while True:
try:
data = tcpClientSkt.recv(BUFSZ)
except Exception,e:
print 'Exception',e
tcpClientSkt.close()
break
if not data:
break
msg = '[%s] :\r\n%s' % (ctime(),data)
print msg
request = GetHttpRequest(data)
if request == None:
print 'Http request error'
buf = ''
method = request[0]
if method != 'GET':
param = ['message=method [%s] not implament yet!' % (method)]
buf = DoRenderPage('500.html',param,500)
else:
buf = DoGet(request[1])
SendResponse(tcpClientSkt,status=buf[0],body=buf[1])
tcpClientSkt.close()
skt.close()
这里使用到的几个方法:GetHttpRequest
对数据进行检测,如果是 HTTP Request 则返回方法与RUI,否则返回 NoneDoGet
对于 GET ,传入 URI ,进行相应的处理DoRenderPage
对根据参数对模板页面进行填充SendResponse
将 Response 消息体发送回客户端
在这里我们仅处理 GET 方法。
2.检测 HTTP 请求
HTTP request 是 ACSII 协议,我们可以使用文本操作的方式来处理它。这里我们只分析第一行就够了。然后根据空格与区分请求方法(Method)与URI,最后返回Method与URI:
def GetHttpRequest(data):
buf = ''
idx = 0
c = data[idx]
while c != '\n':
buf = buf + c
idx = idx + 1
c = data[idx]
if len(buf) < 3:
return None
lst = buf.split(' ')
print lst
if len(lst) < 3:
return None
return lst[0],lst[1]
3.处理 GET 请求
浏览器默认的请求方法为 GET 。GET 方法在URI 后面使用问号("?") 来分隔参数。它的参数是一个或多个键值对,每个键值对之间使用 "&" 号分隔。如:GET /about.html?Name=pugqq.com&type=blog HTTP/1.1
HTTP协议规定URL里的 "?","=" , "&" (包括其他一些符号)作为URL中的特殊符号,不能用做其它含意,否则会出现解析错误。如果需要使用使用这些符号,则需要进行转义.这些符号将转化成 "%"后接两们十六进制符号(% HEX HEX) 的形式进行表示 。详情请参考这里。
def DoGet(params):
if params == None or len(params) == 0 or params == '/':
return DoRenderPage('index.html')
lst = params.split('?')
if len(lst) == 1:
return DoRenderPage(lst[0])
elif len(lst) >= 2:
lstparam = lst[1].split('&')
return DoRenderPage(lst[0],lstparam)
else:
msg = "message=Please Check Your Input!"
return DoRenderPage('400.html',msg,404)
这里我们将无URI的请求转到 index.html页面。
4.返回响应消息
在处理完请求后,服务器将返回响应消息。根据协议,响应消息由三个部分组成,响应状态行在最前,消息体放在最后,与消息头之间有一个空行:
def GetResponseCommonHeader():
buf = 'Date:' + ctime() + '\r\n'
buf = buf + 'Server:myHttpd\r\n'
buf = buf + 'Content-Type:text/html\r\n'
return buf
def GetResponseStatusLine(status):
httpVer = 'HTTP/1.1'
return httpVer + ' ' + str(status) + ' ' + STATUS_CODE[status] + '\r\n'
def SendResponse(skt,status,body):
buf = GetResponseStatusLine(status)
buf = buf + GetResponseCommonHeader()
buf = buf + '\r\n'
buf = buf + body
SendMsg(skt,buf)
def SendMsg(skt,buf):
skt.send(buf)
到此,一个完整的响应过程就算完成了。
5.处理请求的一些细节
我们使用 html
模板来完成 response
消息体的组装。这将使页面(view)与数据模型(Model)分开。在获取请求的URI后读取模板,然后将模板中的参数一一对应地替换掉。
这里我们实现一个简单的功能:通过页面传入一个姓名,并将它在一个新的页面展示出来。
首先定义一个页面(index.html
),用于传入参数:
<html>
<head>
<title>Index ~ pyHttpd</title>
</head>
<body>
<center>
Please Enter You Name :
<form action='welcome.html' method='GET'>
<input type='text' name='name'>
<input type='submit' value='Submit'>
</form>
</center>
</body>
</html>
服务器收到请求后使用 "name" 参数对 welcome.html 进行渲染:
<html>
<head>
<title>Welcome ~ pyHttpd</title>
</head>
<body>
<center>
Hi, dear <br/>
<b> ${name} </b><br/>
welcome visit the pyHttpd Server</br>
</center>
</body>
</html>
最终效果如图: