这篇文章的最初来源是《计算机网络:自顶向下》的第7版的第2章(应用层)末尾,套接字编程作业第一题实验题。通过这个实验作业,学习 Python 中 TCP 连接的套接字编程基础知识:如何创建一个套接字,将其绑定到特定的地址和端口,以及发送和接收 HTTP 数据包。同时也学习一些 HTTP 报文格式的基础知识。
目标:开发一个一次处理一个 HTTP 请求的 Web 服务器。 这个 Web 服务器应该接受并解析 HTTP 请求,从服务器的文件系统中获取请求的文件,创建一个 HTTP 响应消息,该消息由请求的文件和标题行组成,然后将响应直接发送到客户端。 如果服务器中不存在请求的文件,则服务器应将 HTTP “404 Not Found”消息发送回客户端。
一、回顾http协议
HTTP(超文本传输协议)是在TCP/IP协议族中的一种应用层协议,除了HTTP以外,预存在TCP/IP协议族中的应用层协议还包括知名的FTP(用于文件传输服务),SMTP(简单邮件传输协议,其他的邮件协议还有POP3),DNS(域名解析系统)等等。
预备知识:URI 统一资源定位符
需要认识URI这个东西,uniform resource identification(和URL差不多,但是比URL更加具体,因为它定义了每一个网络资源单位的绝对地址路径)。HTTP 协议使用 URI 定位互联网上的资源。
这就是一个URI的格式示意图(图自《图解HTTP》)【注意:URI不区分大小写】,下面是几个不同协议的URI的示例:
ftp://ftp.is.co.za/rfc/rfc1808.txt
http://www.ietf.org/rfc/rfc23...
ldap://[2001:db8::7]/c=GB?objectClass?one
mailto:John.D[email protected]
news:comp.infosystems.www.servers.unix
tel:+1-816-555-1212
telnet://192.0.2.16:80/
urn:oasis:names:specification:docbook:dtd:xml:4.1.2
接下来讲述 HTTP的报文结构
HTTP用于客户端与服务端的通信,通过请求和响应的交换达成通信。具体过程为:由客户端发起请求,服务器接收到,然后回复响应报文。
请求报文
第1部分(第1行),请求行(request line):包括字段:方法、URI和协议版本
- 方法取值:基础的有5个,POST GET HEAD PUT DELETE 【注意区分大小写!需要用大写字母】
【辨析GET与POST】简单讲讲GET和POST的区别:GET和POST 都能够获取网络资源,GET是直接请求获取页面,POST是提交表单让服务器返回结果资源。概念上,大概区分下面几点即可(不做web开发的话)。
(1)GET把参数包含在URL中,POST通过request body传递参数。相对而言,GET比POST更不安全,因为参数直接暴露在URL上,但是由于都是http没加密,所以本质上都不安全。
(2)GET在浏览器回退时是无害的,而POST会再次提交请求。
(3)GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
(4)GET产生一个TCP数据包;POST产生两个TCP数据包。对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据)。(并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次)
(5)它们其实有时候可以混用,本质上都是通过TCP封装
【PUT和DELETE方法】HTTP/1.1 的 PUT和DELETE 方法自身不带验证机制, 任何人都可以上传或删除文件 , 存在安全性问题, 因此一般的 Web 网站不使用该方法。 - URI:上文已经给出了定义
1) 如果不是访问特定资源而是对服务器本身发起请求, 可以用一个 * 来代替请求 URI
2) 可以使用完整的域名+服务器路径作为URI,也可以把域名填在首部行的 Host 字段
* 协议版本
例子中都是HTTP/1.1
【说明:历史上的HTTP版本:(1)HTTP/0.9: HTTP 于 1990 年问世。 那时的 HTTP 并没有作为正式的标准被建立。含有 HTTP1.0 之前版本的意思, 因此被称为 HTTP/0.9。(2)HTTP/1.0:HTTP 正式作为标准被公布是在 1996年5月,版本被命名为 HTTP/1.0,并记载于 RFC1945 1。虽说是初期标准,但该协议标准至今仍被广泛使用在服务器端。(3)HTTP/1.1:1997年1月公布的 HTTP/1.1 是目前主流的 HTTP 协议版本。当初的标准是 RFC2068,之后发布的修订版 RFC2616 就是当前的最新版本 2 (4)HTTP/2.0 协议的主要目的是提高网页性能,目前尚未广泛使用】
第2部分(第2~K行),首部行(header line) 包含字段:各种首部字段(分为三类请求首部,通用首部和实体首部),每个首部字段分别占一行,格式为: 首部字段名+空格+值+CRLF(换行符)。下面是首部格式的示意图:
第3部分 空行 直接一个CRLF
第4部分 实体体(entity body)
GET方法时,实体体为空。POST方法时候才使用(将这些内容提交上去)。HEAD方法也为空,因为这个一般用于调试追踪(响应不返回请求的内容,只返回首部)。
响应报文
可以看到,响应报文由状态行,首部行,空行和实体体构成
- 状态行:包括 协议版本 状态码 状态码原因短语
协议版本:例如 HTTP/1.0
状态码:仅记录在 RFC2616 上的 HTTP 状态码就达 40 种,加上其他的就更多。
但是只要记住14种就可以:
200 OK
204 no content 没有实际内容,请求已成功处理, 但在返回的响应报文中不含实体的主体部分
206 Partial Content 客户端发出的是获取资源的部分范围请求,Content-Range指定
301 Moved Permanently 请求的资源已被分配了新的 URI
302 Found 该状态码表示请求的资源已被分配了新的 URI, 希望用户(本次) 能使用新的 URI 访问
303 See Other 由于请求对应的资源存在着另一个 URI, 应使用 GET方法定向获取请求的资源
304 Not Modified 客户端发送的是附带条件的请求,服务器端允许请求访问资源, 但没有满足条件的情况。附带条件的请求是指采用 GET方法的请求报文中包含 If-Match, If-ModifiedSince, If-None-Match, If-Range, If-Unmodified-Since 中任一首部。
307 Temporary Redirect 临时重定向 和302差不多
400 Bad Request 请求报文有语法错误
401 Unauthorized 发送的请求需要有通过 HTTP 认证(BASIC 认证、DIGEST 认证) 的认证信息
403 Forbidden 服务器拒绝访问资源,服务器可以不说明原因,如要说明原因则填写在响应报文的实体体,一般可能是例如文件权限,从未授权的发送源IP地址试图访问等原因
404 Not Found 找不到或者服务器不想告诉你他有这个资源
500 Internal Server Error 服务器内部故障
503 Service Unavailable 服务器超负荷或者停机维护,现在无法处理请求。
请求报文和响应报文的首部行
上面提到了,这两种报文的首部行有很多字段,可以分为四类 通用首部字段,实体首部字段,请求首部字段(请求报文),响应首部字段(响应报文)
接下来给出详细定义:
更详细的解释以及它们的用法需要再去查看RFC文档或者相关书籍
二、完成一个搭建web服务器的简单实例(python)
http是基于tcp连接的,因此首先要掌握一下 python中 tcp通信的基本接口用法。
服务端socket通信
- 导包
from socket import *
- 创建一个socket:
serverSocket = socket(AF_INET, SOCK_STREAM)
- 绑定本地端口
serverSocket.bind("127.0.0.1", 80)
- 设置监听,操作系统可以挂起的最大连接数量设置为5
serverSocket.listen(5)
- 监听等待一个外部进来的连接,返回一个新的socket表示这个连接,以及提取出客户端的地址
connectionSocket, addr = serverSocket.accept()
- 一旦连接成功,则从客户端处获取数据
message = connectionSocket.recv(1024)
准备一个简单的html文件
例如我准备的 hello.html 如下
Insert title here
你好啊, 天才
放到程序同目录下
然后写web server的主程序
web_server.py:
from socket import *
serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind(("127.0.0.1", 80))
serverSocket.listen(5)
while True:
print("ready to serve")
connectionSocket, addr = serverSocket.accept()
try:
# establish connection
message = connectionSocket.recv(20000)
# print(message)
filename = message.split()[1]
f = open(filename[1:], 'rb')
outputdata = f.readlines()
# send on HTTP Header line into socket
header = bytearray('HTTP/1.1 200 OK\r\n Date: Sat, 28 May 2022 18:10:01 GMT \r\n Content-Type: text/html \r\n \r\n ', 'utf-8')
connectionSocket.send(header)
# send to content of the requested file to the client
for i in range(0, len(outputdata)):
connectionSocket.send(outputdata[i])
connectionSocket.close()
except IOError:
header = bytearray('HTTP/1.1 404 Not Found\r\n Date: Tue, 03 Jul 2012 04:40:59 GMT \r\n Content-Type: text/html \r\n \r\n ', 'utf-8')
connectionSocket.send(header)
connectionSocket.close()
serverSocket.close()
程序里明确了让它运行在 本地 127.0.0.1的 80 端口上,因此运行程序以后,在浏览器打开如下网址即可获取到这个html的内容
http://127.0.0.1:80/hello.html
如果出错了,获取不到那个文件,会返回404
后记:遗留下的一些问题
1)现代的web服务,除了本文最基础的HTTP协议外,还有很多重要的实现和特性,包括:持久连接,pipeline作业, cookie, SSL加密,压缩编码传输,分块传输 这些是什么?
2)web服务如何准确响应返回多个文件内容?例如加了CSS和各种图片文件元素的网页
3)如何处理高并发的访问请求?
上述问题会在后续的系列里继续详细说。