Web框架、Web框架的本质(socket)以及自定义Web框架

框架的定义

在生活中就像我们想到的一样,框架指的我们在做一件事情的时候搭的骨架来完成基础的功能。

例如盖楼,开发商建的毛坯房就是楼的骨架,毛坯房的基础功能就是能住。
Web框架、Web框架的本质(socket)以及自定义Web框架
如果想住的舒服用户根据自己的需求来摆放物品。例如客厅放沙发,电视。主卧放床和衣柜等等。

Web框架、Web框架的本质(socket)以及自定义Web框架

例如明星开演唱会,舞台的搭建就是一个骨架,舞台基础的功能就是明星能有地方唱歌跳舞,如果想要气氛,可以在舞台上摆放不同的物品。
Web框架、Web框架的本质(socket)以及自定义Web框架
Web框架、Web框架的本质(socket)以及自定义Web框架

程序中的框架和生活中搭建的框架的功能是相同的,框架来完成一些基础的工作,程序员在此基础上开发实现自己业务功能的代码;把程序员从繁琐的重复性的代码中解脱出来,提交开发效率。

WEB框架的本质

所有的Web应用框架本质上就是一个socket服务端,而用户的浏览器就是一个socket客户端。 一些常用框架(Django、Tornado、Flask)是对socket服务端进行的封装,使得基础功能更加完善。这样我们就可以自己实现Web框架了。

自定义web框架

基于TCP的套接字编程实现流程:

  1. 服务器端流程简介:
    (1)创建套接字(socket)
    (2)将套接字绑定到一个本地地址和端口上(bind)
    (3)将套接字设定为监听模式,准备接受客户端请求(listen)
    (4)阻塞等待客户端请求到来。当请求到来后,接受连接请求,返回一个新的对应于此客户端连接的套接字clinet_sk(accept)
    (5)用返回的套接字clinet_sk和客户端进行通信(send/recv);
    (6)返回,等待另一个客户端请求(accept)
    (7)关闭套接字(close)
  2. 客户端流程简介:
    (1) 创建套接字(socket)
    (2) 向服务器发出连接请求(connect)
    (3) 和服务器进行通信(send/recv)
    (4) 关闭套接字(close)
    Web框架、Web框架的本质(socket)以及自定义Web框架
    Web框架、Web框架的本质(socket)以及自定义Web框架
  3. 自定义web框架(不完整版)
import socket
server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9994))
server_sk.listen(128)#最多可以接受128个客户端
while True:
    print('等待客户端的连接:')
    clinet_sk,addr=server_sk.accept() #等待传入连接
    # 接收数据此时指定接收的大小1024字节,为了防止发送的数据过大,造成数据丢失,那么进行循环recv()
    #recv本身是阻塞模式的,即设置接收最多缓冲大小字节,设置阻塞模式
    content=clinet_sk.recv(1024)#默认是二进制内容
    print(content)#接受到的内容是请求报文
    content=content.decode('utf-8')
    print(content)  #接受到的内容是请求报文
    #给浏览器发生内容
    clinet_sk.send('HTTP/1.1 200 OK\r\n'.encode('utf-8'))#设置响应首行
    clinet_sk.send()
    clinet_sk.send('\r\n'.encode('utf-8'))#设置响应空行
    clinet_sk.send('ok'.encode('utf-8')) 设置响应体内容  ok变为中文会乱码
    clinet_sk.close()

  • 设置为listen模式时(服务器端模式),发送缓冲区不再使用,接收缓冲区只存放客户端的连接请求。
  • accpet函数返回的新建套接字sockfd会再生成两个新缓冲区,发送和接收缓冲区。
  • 当调用recv时,recv先等待 clinet_sk的发送缓冲区中数据按协议传送完毕,再检查 clinet_skd的接收缓冲区,如果接收缓冲区没有数据或正在传送,则recv等待;否则recv将接收缓冲区中的数据拷贝到用户定义的buff中(ps:当接收缓冲区中数据长度大于buff长度时,recv要调用多次才能完全拷贝完成)。recv返回的是每次实际拷贝的数据长度,若拷贝出错则返回SOCKET_ERROR,若网络中断则返回0。
  • send和recv只是从发送/接收缓冲区中拷贝数据,真正的读写数据是由TCP/IP协议完成的。
  • 当调用socket创建套接字时,同时在内核中生成发送和接收缓冲区。

我们通过十几行代码简单地演示了web 框架的本质。接下来就让我们继续完善我们的自定义web框架吧!
Web框架、Web框架的本质(socket)以及自定义Web框架
扩展:
解决返回给浏览器中文乱码问题:

import socket
server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9997))
server_sk.listen(128)
while True:
    print('等待客户端的连接:')
    clinet_sk,addr=server_sk.accept()
    content=clinet_sk.recv(1024).decode('utf-8')
    print(content)

    msg1='HTTP/1.1 200 OK \r\n'.encode('utf-8')
    msg2='Content-Type:text/html;charaset=utf-8\r\n'.encode('utf-8')
    msg3='\r\n'.encode('utf-8')
    msg4='你好'.encode('utf-8')
    clinet_sk.send(msg1)
    clinet_sk.send(msg2)
    clinet_sk.send(msg3)
    clinet_sk.send(msg4)
    clinet_sk.close()

Web框架、Web框架的本质(socket)以及自定义Web框架查看浏览器请求时的路径位置
Web框架、Web框架的本质(socket)以及自定义Web框架
Web框架、Web框架的本质(socket)以及自定义Web框架
2.根据不同的路径返回不同的内容
如何让我们的Web服务根据用户请求的URL不同而返回不同的内容呢?
我们可以从请求相关数据里面拿到请求URL的路径,然后拿路径做一个判断…

import socket
server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9996))
server_sk.listen(128)
while True:
    clinet_sk,addrs=server_sk.accept()
    content=clinet_sk.recv(1024).decode('utf-8')
    print('客户端发来贺电')
    print(content)
    header_lst=content.split('\r\n')
    print(header_lst)
    title_lst=header_lst[0].split(' ')
    print(title_lst)
    path=title_lst[1]
    print(path)
    if path=='/home':
        msg='这是{}页面'.format(path).encode('utf-8')
    elif path=='/index':
        msg='这是{}页面'.format(path).encode('utf-8')
    else:
        msg='sorry {} 404 not found'.format(path).encode('utf-8')

    msg1='HTTP/1.1 200 ok\r\n'.encode('utf-8')
    msg2='Content-Type:text/html;charaset=utf-8\r\n'.encode('utf-8')
    msg3='\r\n'.encode('utf-8')

    clinet_sk.send(msg1)
    clinet_sk.send(msg2)
    clinet_sk.send(msg3)
    clinet_sk.send(msg)
    clinet_sk.close()

Web框架、Web框架的本质(socket)以及自定义Web框架
Web框架、Web框架的本质(socket)以及自定义Web框架
Web框架、Web框架的本质(socket)以及自定义Web框架
3.根据不同的路径返回不同的内容–函数版
上面的代码解决了不同URL路径返回不同内容的需求。
但是问题又来了,如果有很多很多路径要判断怎么办?难道要挨个写if判断? 当然不用

import socket

server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9995))
server_sk.listen(128)
def home(path):
    msg = '这是{}页面'.format(path).encode('utf-8')
    return msg
def index(path):
    msg = '这是{}页面'.format(path).encode('utf-8')
    return msg
def error(path):
    msg = 'sorry {} 404 not found'.format(path).encode('utf-8')
    return msg
while True:
    clinet_sk,addrs=server_sk.accept()
    content=clinet_sk.recv(1024).decode('utf-8')
    print('客户端发来贺电')
    print(content)
    header_lst=content.split('\r\n')
    title_lst=header_lst[0].split(' ')
    path=title_lst[1]
    if path=='/home':
        msg=home(path)
    elif path=='/index':
        msg=index(path)
    else:
        msg=error(path)
    msg1='HTTP/1.1 200 ok\r\n'.encode('utf-8')
    msg2='Content-Type:text/html;charaset=utf-8\r\n'.encode('utf-8')
    msg3='\r\n'.encode('utf-8')

    clinet_sk.send(msg1)
    clinet_sk.send(msg2)
    clinet_sk.send(msg3)
    clinet_sk.send(msg)
    clinet_sk.close()

如果出现这种报错:OSError: [WinError 10048] 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。就要换端口号

4.根据不同的路径返回不同的内容–函数进阶版
看起来上面的代码还是要挨个写if判断,怎么办?我们还是有办法!

import socket

server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9998))
server_sk.listen(128)
def home(path):
    msg = '这是{}页面'.format(path).encode('utf-8')
    return msg
def index(path):
    msg = '这是{}页面'.format(path).encode('utf-8')
    return msg
def error(path):
    msg = 'sorry {} 404 not found'.format(path).encode('utf-8')
    return msg
path_list=[('/index',index),('/home',home)]
while True:
    clinet_sk,addrs=server_sk.accept()
    content=clinet_sk.recv(1024).decode('utf-8')
    print('客户端发来贺电')
    print(content)
    header_lst=content.split('\r\n')
    title_lst=header_lst[0].split(' ')
    path=title_lst[1]
    func=None
    for path_tup in path_list:
        if path_tup[0]==path:
            func=path_tup[1]
    if func:
        msg=func(path)
    else:
        msg=error(path)

    msg1='HTTP/1.1 200 ok\r\n'.encode('utf-8')
    msg2='Content-Type:text/html;charaset=utf-8\r\n'.encode('utf-8')
    msg3='\r\n'.encode('utf-8')

    clinet_sk.send(msg1)
    clinet_sk.send(msg2)
    clinet_sk.send(msg3)
    clinet_sk.send(msg)
    clinet_sk.close()

5.返回具体的HTML文件
给浏览器返回完整的HTML内容,这又该怎么办呢?不管是什么内容,最后都是转换成字节数据发送出去的。 我们可以打开HTML文件,读取出它内部的二进制数据,然后再发送给浏览器。

import socket

server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9999))
server_sk.listen(128)
def home(path):
    with open('home.html',mode='rb') as f:
        msg=f.read()
    return msg
def index(path):
    with open('index.html',mode='rb') as f:
        msg=f.read()
    return msg
def error(path):
    with open('error.html',mode='rb') as f:
        msg=f.read()
    return msg
path_list=[('/index',index),('/home',home)]
while True:
    clinet_sk,addrs=server_sk.accept()
    content=clinet_sk.recv(1024).decode('utf-8')
    print('客户端发来贺电')
    print(content)
    header_lst=content.split('\r\n')
    title_lst=header_lst[0].split(' ')
    path=title_lst[1]
    func=None
    for path_tup in path_list:
        if path_tup[0]==path:
            func=path_tup[1]
            break
    if func:
        msg=func(path)
    else:
        msg=error(path)

    msg1='HTTP/1.1 200 ok\r\n'.encode('utf-8')
    msg2='Content-Type:text/html;charaset=utf-8\r\n'.encode('utf-8')
    msg3='\r\n'.encode('utf-8')

    clinet_sk.send(msg1)
    clinet_sk.send(msg2)
    clinet_sk.send(msg3)
    clinet_sk.send(msg)
    clinet_sk.close()

这个程序执行前要先建三个HTML文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index.html</title>
</head>
<body>
<h2 style="color:blue;">这是index页面</h2>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>home.html</title>
</head>
<body>
<h2 style="color:blue;">这是home页面</h2>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>error.html</title>
</head>
<body>
<h2 style="color:blue;">这是error页面</h2>
</body>
</html>

(6).让网页动态起来
这网页能够显示出来了,但是都是静态的啊。页面的内容都不会变化的,我想要的是动态网站。
没问题,我也有办法解决。我选择使用字符串替换来实现这个需求。(这里使用时间戳来模拟动态的数据)

import socket,time

server_sk=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
server_sk.bind(('127.0.0.1',9999))
server_sk.listen(128)
def home(path):
    with open('home.html',mode='r',encoding='utf-8') as f:
        msg=f.read()
        msg=msg.replace('home',str(time.time()))
        msg=bytes(msg,encoding='utf-8')
    return msg
def index(path):
    with open('index.html',mode='rb') as f:
        msg=f.read()
    return msg
def error(path):
    with open('error.html',mode='rb') as f:
        msg=f.read()
    return msg
path_list=[('/index',index),('/home',home)]
while True:
    clinet_sk,addrs=server_sk.accept()
    content=clinet_sk.recv(1024).decode('utf-8')
    print('客户端发来贺电')
    print(content)
    header_lst=content.split('\r\n')
    title_lst=header_lst[0].split(' ')
    path=title_lst[1]
    func=None
    for path_tup in path_list:
        if path_tup[0]==path:
            func=path_tup[1]
            break
    if func:
        msg=func(path)
    else:
        msg=error(path)

    msg1='HTTP/1.1 200 ok\r\n'.encode('utf-8')
    msg2='Content-Type:text/html;charaset=utf-8\r\n'.encode('utf-8')
    msg3='\r\n'.encode('utf-8')

    clinet_sk.send(msg1)
    clinet_sk.send(msg2)
    clinet_sk.send(msg3)
    clinet_sk.send(msg)
    clinet_sk.close()