HomeBlogDownloadsApps

如何实现最简单的HTTP服务

要做一个网站, 有许多Web框架可选, 但是考虑到这个破网站并没有什么高级的功能, 那么就将后端精简到一个Python脚本. 下面是代码, 这个网站最初就以此为基础.

import os
import re
import socket
import threading

content_type = {'html': 'text/html', 'htm': 'text/html', 'jsp': 'text/html', 'xhtml': 'text/html', 'js': 'application/x-javascript', 'css': 'text/css', 'tif': 'image/tiff', 'gif': 'image/giff', 'ico': 'image/icon', 'jpg': 'image/jpeg', 'png': 'image/png'}
home = '/home'  #the root directory for website
host = '192.168.25.47'
port = 80

def handle(conn):
    path = re.search('GET /(.*?) ', conn.recv(1024).decode()).group(1)
    if path == '':
        path = 'index.html'  #default page
    abs_path = os.path.join(home, path)
    if os.path.isfile(abs_path):
        with open(abs_path, 'rb') as file:
            content = file.read()
        extension = path.split('.')[-1]
        if extension in content_type.keys():
            conn.sendall(('HTTP/1.1 200 OK\r\nContent-Type: %s\n\n'%content_type[extension]).encode() + content)
        else:
            conn.sendall(('HTTP/1.1 200 OK\r\nAccept-Ranges: bytes\r\nContent-Length: %d\r\nContent-Type: application/x-download\r\nContent-Disposition: attachment;filename=%s\n\n'%(len(content), path.split('/')[-1])).encode() + content)
    else:
        conn.sendall('HTTP/1.1 404\n\n 404 Not Found'.encode())  #404 page
    conn.close()

s = socket.socket()
s.bind((host, port))
s.listen(4)
while True:
    conn, _ = s.accept()
    threading.Thread(target=handle, args=(conn,)).start()

socket

socket是一种编程接口(API), 封装的是网络通信的底层协议(TCP/IP). 大部分编程语言都有socket的实现, Python的标准库就包含socket, 下面是其中的主要方法.

socket类的构造函数

socket(family=-1, type=-1, proto=-1, fileno=None)

实例化一个socket, 其中参数可指定协议族, socket类型, 以及传输协议, 大多数情形下可忽略.

socket.bind(address)

把socket对象绑定到一个本地地址上, 地址一般是一个元组: (IP地址, 端口).

socket.listen([backlog])

开始监听. 参数backlog表示允许等待的连接数量

while True:
    conn, _ = s.accept()
    threading.Thread(target=handle, args=(conn,)).start()

socket.accept()是阻塞的方法, 此时该socket将会等待客户端的连接, 并返回一个元组(socket object, address info). 随后服务端开启一个新的线程用来处理请求(方法为handle), 主线程继续等待新的连接.

至此客户端和服务端建立好连接了, 接下来就是通信.

socket.recv(buffersize[, flags]) -> data

此方法等待接收对方的数据并返回一个字节串, 同样是阻塞的. buffersize指定返回的最大的字节数, 若某次实际接收的数据长度大于buffersize, 剩余的将会在下次recv方法中得到.

socket.sendall(data[, flags])

相对地, 此方法用于发送数据.

socket.close()

注意, 每次通信完毕都应该调用close方法释放资源.

HTTP

HTTP是应用层的协议, 规定了浏览器应该怎样向服务器提交请求, 服务器又如何响应, socket作为底层协议的接口, 可以遵循HTTP组织应用层的数据实现浏览器和服务器间的通信.

以Edge浏览器为例, 当用户在地址栏中输入域名www.example.com/index.html并按回车, 浏览器便尝试与该域名指向的服务器建立连接, 接着发送如下的消息:

GET /index.html HTTP/1.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/18.17763
Accept-Encoding: gzip, deflate
Host: www.example.com
Connection: Keep-Alive

在第一行中, GET是请求方法, "/index.html"是url(实际上就是主机名example.com后面的字符串), "HTTP/1.1"是协议版本. 后面的是请求标头, 包含客户端的一些信息, 例如"Accept-Language: zh-CN"就表示客户端期望的语言是中文. 若浏览器想发送一些数据到服务器, 比如提交表单, 那么这些数据将置于标头之后, 中间以一个空行隔开, 此时请求方法一般为POST.

服务器方面, 并不是说客户端请求/index.html, 该文件就自动返回. 实际上, 服务器如何响应完全是自定义的, 比如可以忽略所有请求信息, 返回随机字符串. 但是为了让不同的浏览器都能够正确地解析服务器发送的内容, 遵循HTTP标准是非常有必要的.

此例中, 服务器接收到浏览器的请求时, 只考虑url部分, 然后在本地上寻找相应的文件并读取文件流, 响应的数据类似:

HTTP/1.1 200 OK
cache-control: private, max-age=0
content-encoding: gzip
content-length: 1024
content-type: text/html

<html>...</html>

第一行中"HTTP/1.1"也是协议版本, "200 OK"是响应状态码. 紧接着是响应标头, 其中比较重要的属性是content-type, 浏览器将据此决定以什么方式处理响应流, 比如"text/html"表示这是一个网页. 如果希望浏览器直接下载文件, 那么只需将标头设置如下:

accept-ranges: bytes
content-length: 1024    <!--文件流长度-->
content-type: application/x-download
content-disposition: attachment;filename=filename    <!--文件名-->

至此, 把前端的网页资源放在指定目录下, 启动服务便可以正常浏览了.