网络协议详解之 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指定使用的网络端口,缺省值为80abs_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).每个请求和响应通常具有不同的头。 下表列出了一些常用的消息头:

HeaderTypeContent
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,否则返回 None
DoGet 对于 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>

最终效果如图:

request

response

end

本文链接:

https://pugqq.com/archives/http-network-protocol.html
网络协议详解之 HTTP 协议 - I/O
快来做第一个评论的人吧~

# 最近更新

Nginx的proxy_pass指令完全拆解2021-03-21

Nginx配置Jenkins域名访问2021-01-03

设计模式 - 浅谈备忘录模式2020-12-02

设计模式 - 浅谈中介者模式2020-11-23

设计模式 - 浅谈迭代器模式2020-11-02

MySQL5.7 字符集设置2020-10-26

设计模式 - 浅谈状态模式2020-10-23

设计模式 - 浅谈访问者模式2020-10-13

设计模式 - 浅谈观察者模式2020-10-12

设计模式 - 浅谈命令模式2020-09-21