实现一个静态web服务器、http server

前言

不管是哪一种语言的web框架,其核心都是一致的,那就是以http协议为核心,围绕着http请求和http响应这两方面做文章。至于衍生的数据持久化(cookie session 数据库 等)只是存储手段罢了。

如果要理解好http协议,没有什么比实现一个自定义的http client客户端和http server服务器更好的方法了。

这篇博客博主就打算使用多种语言来做同一件事情:实现一个Http server,并且可以像apache那样实现静态资源的访问。


1、Http的请求过程

1.1 HTTP是一个基于TCP/IP通信协议来传递数据

这意味着可以使用建立socket的方式来监听某一端口,比如像tomcat一样监听8080端口,来实现这个web服务器。基于socket来实现client 和 server的交流。

1.2 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,通过http请求可以访问服务器上的HTML 文件, 图片文件, 查询结果等。

可以放几个简单的静态网页作为访问使用

1.3 HTTP三点注意事项:

  • HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
  • HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
  • HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

socket在处理完一个请求之后应该主动断开连接,然后方便处理下一请求
服务器端向客户端发送数据时,要通过MiME-type指定数据的类型
http不保存事务的处理状态,当然具体的业务逻辑可以使用session或者cookie来保存状态

1.4 HTTP请求图示
在这里插入图片描述

这是相对复杂的结构,HTTPServer、CGI我们可以合在一起,为了简单起见,就不需要访问database。数据可以用cache暂存。

大概就是这种结构:
在这里插入图片描述
Http server兼任 web server和CGI program的作用。


2、HTTP请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
在这里插入图片描述

这个数据是很有格式的,我们可以通过对\r\n的切分就可以简单的在server端取出client端的数据


3、HTTP响应数据

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
在这里插入图片描述

2、3 引用自菜鸟教程


使用Python实现一个Http server

第一步,实现一个监听8080端口的socket server,这个server只会连接一次就断开

# -*- coding:utf-8 -*-
import socket

if __name__ == '__main__':
    # family: 套接字家族可以使AF_UNIX或者AF_INET
    # type: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
    s.bind(('localhost', 8080))
    while(True):
        # 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。
        # 该值至少为1,大部分应用程序设为5就可以了。
        s.listen(3)
        # 被动接受TCP客户端连接,(阻塞式)等待连接的到来
        conn, addr = s.accept()
        # 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。
        # flag提供有关消息的其他信息,通常可以忽略。
        request = conn.recv(1024)
        # 打印请求内容
        print request
        # 给客户端返回内容
        conn.sendall('welcome!')
        # 关闭连接
        conn.close()

运行:
在这里插入图片描述
测试一下是否跑起来了:
在这里插入图片描述

上面是一个简单的脚本,模拟了客户端

服务器成功返回了信息welcome

同时服务器也接收到了信息:
在这里插入图片描述

接下来继续扩展

使用浏览器访问server尝试:这是打印出来的request数据
在这里插入图片描述

按照
在这里插入图片描述
对数据进行切分,新的代码:

# -*- coding:utf-8 -*-
import socket

if __name__ == '__main__':
    # family: 套接字家族可以使AF_UNIX或者AF_INET
    # type: 套接字类型可以根据是面向连接的还是非连接分为SOCK_STREAM或SOCK_DGRAM
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 绑定地址(host,port)到套接字, 在AF_INET下,以元组(host,port)的形式表示地址。
    s.bind(('localhost', 8080))
    while(True):
        # 开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。
        # 该值至少为1,大部分应用程序设为5就可以了。
        s.listen(3)
        # 被动接受TCP客户端连接,(阻塞式)等待连接的到来
        conn, addr = s.accept()
        # 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。
        # flag提供有关消息的其他信息,通常可以忽略。
        request_content = conn.recv(1024)

        # 没有内容的连接,防止keep-alive导致错误断开
        if not request_content:
            conn.close()

        request_split = request_content.split('\r\n')

        # 请求行
        method, url, http_version = request_split[0].split(' ')

        # 请求头
        request_headers = {}
        for i in range(1, len(request_split)):
            if request_split[i] == '':
                break
            else:
                key, value = request_split[i].split(': ')
                request_headers[key] = value

        # 请求数据
        request_body = []
        for i in range(2+len(request_headers), len(request_split)):
            request_body.append(request_split[i])

        request_body = '\r\n'.join(request_body)

        # 打包成请求字典
        request = {
            'addr': addr,
            'method': method,
            'url': url,
            'http_version': http_version,
            'headers': request_headers,
            'body': request_body
        }

        print request

        # 给客户端返回内容
        conn.sendall('welcome!')
        # 关闭连接
        conn.close()

为了模拟客户端post数据,使用postman进行测试:
在这里插入图片描述
结果:
在这里插入图片描述

可以看到一个简单的request就被切分出来了

返回response

在这里插入图片描述
这个简单,按照格式写就是了:

common_response = '''
HTTP/1.1 200 OK
Content-Type: text/html

<head>
<title>Hello world!</title>
</head>
<html>
<p>this is a easy python http server~</p>
<p>welcome~<p>
</html>
'''.replace('\n', '\r\n')

然后返回回去:

        # 给客户端返回内容
        conn.sendall(common_response)

使用浏览器访问127.0.0.1:8080
在这里插入图片描述

成功了!

写到现在,一个最基础的,能获取请求,能返回信息的httpServer就做好了。但是它现在的功能还是太简单,下一阶段,我们要再加个功能:
  • 静态资源的访问

在继续写之前,先把解析请求信息的部分抽离成函数,方便使用:

def parseRequest(request_content):


    request_split = request_content.split('\r\n')

    # 请求行
    method, url, http_version = request_split[0].split(' ')

    # 请求头
    request_headers = {}
    for i in range(1, len(request_split)):
        if request_split[i] == '':
            break
        else:
            key, value = request_split[i].split(': ')
            request_headers[key] = value

    # 请求数据
    request_body = []
    for i in range(2+len(request_headers), len(request_split)):
        request_body.append(request_split[i])

    request_body = '\r\n'.join(request_body)

    # 打包成请求字典
    request = {
        'addr': addr,
        'method': method,
        'url': url,
        'http_version': http_version,
        'headers': request_headers,
        'body': request_body
    }

    return request

建立一个static目录用来存储静态文件:
在这里插入图片描述
index.html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>test</title>
    <link rel="stylesheet" href="all.css">
</head>
<body>
    <img src="img.jpg" alt="">
    <h1 class="red">首页</h1>
</body>
</html>

all.css:

.red {
    color: red;
}

img.jpg
在这里插入图片描述

我们首先要能把这些static目录下的文件都读取:

def get_files(files_dir='.'):
    """ 获取某个路径所有的文件路径
    """
    files_dir = os.path.join(os.getcwd(), files_dir)
    files_all = []
    def get_files_(files_dir='.', r_path=''):
        if files_dir[-1:] != '/':
            files_dir += '/'
        files = os.listdir(files_dir)
        for file in files:
            file_path = os.path.join(files_dir, file)
            if os.path.isdir(file_path):
                get_files_(file_path, file)
            else:
                if r_path:
                    files_all.append('%s/%s' % (r_path, file))
                else:
                    files_all.append(file)
    get_files_(files_dir)
    return files_all

def loadStatic(static_path='static'):
    statics = get_files(static_path)
    static_path = os.path.join(os.getcwd(), static_path)
    statics_dict = {}
    # 设置下列文件后缀使用二进制读取
    byte_files_suf = ('jpg', 'png')
    for file_name in statics:
        file_suf = file_name.split('.')[-1]
        print file_suf
        file_path = os.path.join(static_path, file_name)
        if file_suf in byte_files_suf:
            file = open(file_path, 'rb')
        else:
            file = open(file_path, 'r')
        statics_dict['/'+file_name] = file.read()
        file.close()
    return statics_dict

这边做了一个判断,将图片类型的文件使用二进制来进行了读写。
然后会将stataic目录下的所有文件读取,并按照文件相对static的路径-文件内容的键值对返回。
在这里插入图片描述
这样我们要做的就很简单了,根据之前获取的request.url,来匹配路由,如果匹配到了静态文件路径,就将其内容返回。

在浏览器中访问http://127.0.0.1:8080/index.html
控制台中打印出了request信息:
在这里插入图片描述
根据要求修改__main__

if __name__ == '__main__':
    # 加载静态文件
    statics = loadStatic()

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(('localhost', 8080))
    while(True):
        s.listen(3)
        conn, addr = s.accept()
        request_content = conn.recv(1024)

        # 没有内容的连接,防止keep-alive导致错误断开
        try:
            request = parseRequest(request_content)
        except e:
            conn.close()
            break

        mime_type = {
            'jpg': 'image/jpeg',
            'png': 'image/png',
            'html': 'text/html'
        }
        file_suf = request['url'].split('.')[-1]
        if file_suf in mime_type:
            content_type = mime_type[file_suf]
        else:
            content_type = 'text/html'

        response = 'HTTP/1.1 200 OK\r\nContent-Type:%s\r\n\r\n' % content_type

        print request['url']

        if request['url'] in statics:
            print 'match static'
            response += statics[request['url']]

        # 给客户端返回内容
        conn.sendall(response)
        # 关闭连接
        conn.close()

再次访问http://127.0.0.1:8080/index.html
浏览器显示:
在这里插入图片描述
查看f12
在这里插入图片描述

到这里一个静态服务器就大功告成啦,哈哈哈哈

我们可以随便往static目录下放文件,然后重启server, 并访问对应的路径,比如,再新建个/img/img_1.jpg:
在这里插入图片描述
这张图片长这样:
在这里插入图片描述

重新运行easy_http_server_1.py,浏览器访问http://127.0.0.1:8080/img/img_1.jpg
在这里插入图片描述
访问成功。


总结

洋洋洒洒,一篇博客接近一万字了。

事实证明,一个简单的http server的实现并不复杂(当然是不追求性能和安全的前提下)。

代码仅仅100行左右。

下一篇博主将会进一步实现一个动态服务器。敬请期待。

博客代码github 链接:https://github.com/numb-men/easy-http-server/blob/master/easy_http_server_1.py


ps:请随便转载
ps:发现错误请评论
ps:python 2.7.15

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值