Python HTTP 协议与 web 静态服务器

2018-07-13  本文已影响25人  jovelin

HTTP 超文本传输协议

超文本传输协议(HyperText Transfer Protocol)是一种应用层协议。

HTTP是万维网的数据通信的基础。设计HTTP最初的目的是为了提供一种发布和接收HTML页面<网页>的方法。

浏览器请求的基本流程

mini-web服务器工作流程

浏览器请求的 URL

# www.baidu.com: 网站(网址)! 太长就不太称呼为网站了;
#   url(统一资源定位符):
#   完整版: http://www.baidu.com:80/aaa/bbb/index.html?username=aaa&password=123
#       http/https: https是http加密后进行传输;(https收费...)
#       端口: http: 80;    https: 443;
#       /aaa/bbb/index.html: 请求的资源路径;
#       username=aaa&password=123: 传输的内容;(请求体...GET)

请求报文格式总结


# 总结: 请求报文格式
# 1.请求行;
#     GET / HTTP/1.1\r\n
# 2.请求头;
#     头属性: 属性值\r\n
#     Host: www.baidu.com\r\n
# 3.空行;
#     \r\n
# 4.请求体;
#     username=jovelin&password=123456


# 请求报文格式分析:
# 1.请求行(request line)
#   格式: 请求方式 资源路径 协议及版本号\r\n
GET / HTTP/1.1\r\n
#   GET: 常用请求方式GET/POST;   (GET/POST/PUT/DELETE...)
#       GET:  获取(从服务器获取信息的时候用...)
#       POST: 发送(向服务器存储信息的时候用...)
#   /: /aaa/bbb/index.html; 想要访问的页面/图片/音频...(明天要用...)
#   HTTP/1.1: 协议及版本号
#   空行: \r\n

# 2.请求头(request header)
#   格式: 头属性: 头信息\r\n
Host: www.baidu.com\r\n
#       Host: 主机;(记住...)
Connection: keep-alive\r\n
#       Connection: 链接;(长连接)
Upgrade-Insecure-Requests: 1\r\n
#       提示服务端我可以解析https;
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.117 Safari/537.36\r\n
#       User-Agent: 用户代理;(浏览器及系统版本...)
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8\r\n
#       Accept: 接收!
Accept-Encoding: gzip, deflate, br\r\n
#       压缩: 数据压缩算法;
Accept-Language: zh-CN,zh;q=0.9\r\n
#       语言: 中文;

# 3.空行;
#     \r\n

# 4.请求体;
#     username=jovelin&password=123456

请求报文格式总结

响应报文格式总结


# 总结: 响应报文格式
# 1.响应行;
#   HTTP/1.1 200 OK\r\n
# 2.响应头;
#   头属性: 头信息\r\n
#   Server: BWS/1.1\r\n
# 3.换行
#   \r\n
# 4.响应体;
#   文本/图片/音频/视频/网页...


# 响应报文格式分析:
# 1.响应行(response line)
#   格式: 协议及版本号 状态码 英文解释\r\n
HTTP/1.1 200 OK\r\n
#       HTTP/1.1: 协议及版本号
#       200 OK: 状态码 英文解释

# 2.响应头(response header)
#   格式: 头属性: 属性值\r\n
Connection: Keep-Alive\r\n
#   长连接
Content-Encoding: gzip\r\n
#   压缩格式
Content-Type: text/html; charset=utf-8\r\n
#   请求体的文本类型;
Date: Wed, 14 Mar 2018 09:52:48 GMT\r\n
#   更新时间
Server: BWS/1.1\r\n
#   服务器名:(记住,因为简单,以后用)

# 3.空行;
#     \r\n

# 4.响应体(response body)
#     文本/图片/音频/视频/网页...

响应报文格式总结

网络响应状态码

2xx 成功  200 OK  (发送成功)
3xx 重定向 
    302 Moved Temporarily/302 Found   解释作用(暂时跳转)  301/2/3/4/7
    307 Internal Redirect(内部重定向)
    Location: https://www.baidu.com
4xx 客户端错误 404 Not Found(客户端发送的页面没找打)
    http://help.xunlei.com/online/stat_inst.php?pid=0000&thunderver=5.8.14.706&thundertype=4&peerid=000C294E4AE1J3J4    
    http://video.baomihua.com/play_error/-30001
5xx 服务器错误 503 Service Unavailable(服务器不能使用)

长连接和短连接

TCP长/短连接好比地铁卡/单程票

在HTTP/1.0中, 默认使用的是短连接.也就是说, 浏览器和服务器每进行一次HTTP操作, 就建立一次连接, 但任务结束就中断连接.如果客户端浏览器访问的某个HTML或其他类型的 Web 页中包含有其他的Web资源,如js文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话。

但从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:

Connection:keep-alive

在真正的读写操作之前,server与client之间必须建立一个连接,

当读写操作完成后,双方不再需要这个连接时它们可以释放这个连接,

连接的建立通过三次握手,释放则需要四次握手,

所以说每个连接的建立都是需要资源消耗和时间消耗的。

TCP 短连接

短连接一般只会在 client/server 间传递一次读写操作!

  1. client 向 server 发起连接请求
  2. server 接到请求,双方建立连接
  3. client 向 server 发送消息
  4. server 回应 client
  5. 一次读写完成,此时双方任何一个都可以发起 close 操作 (一般都是 client 先发起 close 操作。当然也不排除有特殊的情况。)

TCP 长连接

  1. client 向 server 发起连接
  2. server 接到请求,双方建立连接
  3. client 向 server 发送消息
  4. server 回应 client
  5. 一次读写完成,连接不关闭
  6. 后续读写操作...
  7. 长时间操作之后 client 发起关闭请求

TCP长/短连接的优点和缺点

长连接可以省去较多的TCP建立和关闭的操作,节约时间。但是如果用户量太大容易造成服务器负载过高最终导致服务不可用。

短连接对于服务器来说实现起来较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但是如果用户访问量很大, 往往可能在很短时间内需要创建大量的连接,造成服务器响应速度过慢。

总之:

小的WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源来让套接字 保持存活-keep alive,

对于中大型WEB网站一般都采用长连接,好处是响应用户请求的时间更短,用户体验更好,虽然更耗硬件资源一些,但这都不是事儿。另外,数据库的连接用长连接,如果用短连接频繁的通信会造成socket错误。

案例

1.模拟服务器(服务端)


# 需求: 获取请求报文的格式;

import socket

if __name__ == '__main__':
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
    tcp_socket.bind(("127.0.0.1", 8888))
    tcp_socket.listen(128)
    print("服务已开启...")
    while True:
        service_client_socket, ip_port = tcp_socket.accept()
        print(ip_port, "已连接...")
        data_bin = service_client_socket.recv(5000)
        print("二进制数据:", data_bin)
        print("解析后数据:", data_bin.decode())
        service_client_socket.close()

2.模拟浏览器(客户端)


# 需求: 获取响应报文的格式内容并保存;

import socket

if __name__ == '__main__':
    # 创建TCP连接
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # DNS解析 和 连接HTTP服务器
    tcp_socket.connect(("www.baidu.com", 80))

    # 组包 发送HTTP请求报文

    # 请求行
    request_line = "GET / HTTP/1.1\r\n"

    # 请求头
    request_header = "Host: www.baidu.com\r\n"
    request_data = request_line + request_header + "\r\n"

    # 发送请求
    tcp_socket.send(request_data.encode())

    # 接收响应报文
    response_data = tcp_socket.recv(4096)

    # 对响应报文进行解析 -- 切割
    response_str_data = response_data.decode()
    # print(response_data)

    # '\r\n\r\n'之后的数据就是响应体数据
    index = response_str_data.find("\r\n\r\n")

    # 切割出的数据就是文件数据
    html_data = response_str_data[index + 4:]

    # data_file = open("index.html", "wb")
    # data_file.write(html_data.encode())
    # data_file.close()
    with open("index.html", "wb") as file:
        file.write(html_data.encode())
        # 如果是长连接,还有很多内容没有收到,需要死循环接收
        while True:
            # 后面在获取到的响应内容,就不包含响应行和响应头了
            data_bin = tcp_socket.recv(4096)
            if data_bin:
                file.write(data_bin)
            else:
                break

    # 关闭套接字
    tcp_socket.close()

  1. web 静态服务器

from gevent import monkey

monkey.patch_all()

import socket
import gevent
import sys


class WebServer(object):
    """Web 服务器类"""

    def __init__(self, ip, port):
        # 创建套接字
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 设置套接字复用地址
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 绑定IP地址和端口
        self.socket.bind((ip, port))
        # 设置被动套接字
        self.socket.listen(128)

    def startup(self):
        """等待客户端连接"""
        while True:
            # 等待被连接
            service_client_socket, ip_port = self.socket.accept()
            print(ip_port, "连接成功.", end="\n\n")
            # 处理请求
            gevent.spawn(self.client_handler, service_client_socket)

    def client_handler(self, service_client_socket):
        """处理客户端请求"""
        request_data_bin = service_client_socket.recv(4096)

        if not request_data_bin:
            print('客户端已经断开连接.', end="\n\n")
            service_client_socket.close()
            return
        else:
            print("客户端请求报文:", request_data_bin, end="\n\n")

        # 解析 HTTP 文本
        my_http = self.parse_http(request_data_bin.decode('utf-8'))

        # 读取固定页面数据
        try:
            response_line = 'HTTP/1.1 200 OK\r\n'
            response_header = 'Server: PythonWebServer1.0\r\n'
            file = open('./static' + my_http['url'], 'rb')
        except:
            response_line = 'HTTP/1.1 404 NOT FOUND\r\n'
            response_header = 'Server: PythonWebServer1.0\r\n'
            file = open('./static/404.html', 'rb')

        # 读取文件内容
        response_content = file.read()
        file.close()
        # 拼接响应报文
        response_data = (response_line + response_header + '\r\n').encode('utf-8') + response_content
        # 发送响应报文
        service_client_socket.send(response_data)
        # 关闭套接字
        service_client_socket.close()

        print("\n", "-" * 100, "\n")

    @staticmethod
    def parse_http(request_data):
        """解析 HTTP 文本"""

        my_http = {}

        # 分成多行
        request_headers = request_data.split('\r\n')
        request_lines = request_headers[0].split(' ')
        print("request_lines: ", request_lines, end="\n\n")

        my_http['method'] = request_lines[0]
        my_http['url'] = request_lines[1]
        my_http['version'] = request_lines[2]

        # 未指定页面时 默认访问 index.html
        if my_http['url'] == "/":
            my_http['url'] = "/index.html"

        for header in request_headers[1:]:

            if not header:
                continue

            # Host: www.baidu.com
            lines = header.split(':')
            my_http[lines[0]] = lines[1][1:]

        print("my_http: ", my_http, end="\n\n")

        return my_http


def port_handler():
    """指定端口"""

    # 默认设置端口为 8888
    port = 8888

    # # 获取外部传递过来的参数;
    # # 1.尽量值传递一个参数过来
    # #   ctrl+z: 退出页面,但是程序没有退出;(该端口还可以使用)
    # #   ctrl+c: 退出页面,也退出程序;
    # # print(sys.argv[1])
    # if not len(sys.argv) == 2:
    #     print('输入的格式错误,正确的格式应该是: python3 文件名.py 端口号')
    #     return
    #
    # # 2.如果传递过来端口号,里面有非数字;(也不行)
    # if not sys.argv[1].isdigit():
    #     print('端口号, 必须是整数!!!')
    #     return
    #
    # # 3.取值范围: [0-65535]
    # if not 0 <= int(sys.argv[1]) <= 65535:
    #     print('端口号必须在: [0-65535]之间!!!')
    #     return
    #
    # # 4.如果全部通过,那么要把端口号,传递到程序中
    # # 获取用户指定的绑定端口
    # port = int(sys.argv[1])

    return port


def main():
    # 服务器 IP,默认为本机 IP
    server_ip = ""
    # 服务器 端口
    server_port = port_handler()
    web_server = WebServer(server_ip, server_port)
    web_server.startup()


if __name__ == '__main__':
    main()

上一篇下一篇

猜你喜欢

热点阅读