Skip to content

before

想要学好Django,我们需要了解:

  • 网络底层相关概念。
  • 到底什么是web框架?
  • 常见web框架的对比。
  • 同步和异步。
  • Django快速上手,创建项目、虚拟环境、多app应用、纯净版的Django。
  • 逐步深入:路由、模板、视图、静态文件引入、orm、其它内置组件.....

web框架底层

网络通信

局域网:

1832669333575172096.png

广域网:

1832669334355312640.png

个人一般写程序,想要让别人访问:阿里云、腾讯云。

  • 去云平台租服务器(含公网IP)
  • 程序放在云服务器

socket实现

我们自己写时,通过socket模块可以实现网络上的两端进行通信。

同一局域网下

只要你们的电脑在同一个局域网下,都可以跑起来这个示例。

python
import socket

# 1.监听本机的IP和端口
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('192.168.10.3', 8001))  # 我自己的电脑IP,端口8001

# 2.让多少人等待
sock.listen(5)

while True:
    # 3.等待连接请求的申请,有人来连接(阻塞)
    print("waiting.....")
    conn, addr = sock.accept()
	print(f"有客户端连进来了,{conn}")

    # 4.连接成功后立即发送
    conn.sendall("欢迎使用xx系统".encode("utf-8"))

    # 5.断开连接
    conn.close()
# 6.停止服务端程序
sock.close()
python
import socket

# 1. 向指定IP发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.10.3', 8001))

# 2. 接收你发的消息
message = client.recv(1024)
print(message.decode("utf-8"))

# 3.断开连接
client.close()
python
import socket

# 1. 向指定IP发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('192.168.10.3', 8001))

# 2. 接收你发的消息
message = client.recv(1024)
print(message.decode("utf-8"))

# 3.断开连接
client.close()

广域网下

服务端在云服务器,各个客户端可以在全球的任意电脑上。

python
import socket

# 1.监听本机的IP和端口
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('0.0.0.0', 8001))  # 监听云服务器的8001端口,注意云服务器的安全组放开8001端口

# 2.让多少人等待
sock.listen(5)

while True:
    # 3.等待连接请求的申请,有人来连接(阻塞)
    print("waiting.....")
    conn, addr = sock.accept()
	print(f"有客户端连进来了,{conn}")
    # 4.连接成功后立即发送
    conn.sendall("欢迎使用xx系统".encode("utf-8"))

    # 5.断开连接
    conn.close()
# 6.停止服务端程序
sock.close()
python
import socket

# 1. 向云服务器发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('www.neeo.cc', 8001))

# 2. 接收云服务器返回的消息
message = client.recv(1024)
print(message.decode("utf-8"))

# 3.断开连接
client.close()
python
import socket

# 1. 向云服务器发送连接请求
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('www.neeo.cc', 8001))

# 2. 接收云服务器返回的消息
message = client.recv(1024)
print(message.decode("utf-8"))

# 3.断开连接
client.close()

常见软件架构

bs架构

浏览器充当客户端。

服务端就是咱们自己写的web应用程序。

cs架构

客户端就是常见桌面软件,电脑桌面软甲、各种app也算是客户端、小程序也算是客户端。

服务端也是对应软件厂家自己的云服务器上跑的各种web应用程序。

从socket到Django

通过之前讲解的web框架的底层实现,我们可以发现,Web框架本质上就是一个socket服务端,而用户的浏览器、移动设备等就是一个个个socket客户端。

接下来,我们通过手撸一个web框架,能让你再次加深web框架的本质和其工作原理。

最简单的web框架:

python
import socket

# 1. 创建socket对象
sk = socket.socket()

# 2. 绑定IP和端口
sk.bind(('127.0.0.1', 8888))

# 3. 监听
sk.listen(5)

# 4. 循环监听

while 1:
    conn, addr = sk.accept()  # 等待连接
    received_data = conn.recv(8192)  # 接收数据
    print(received_data)  # b'GET / HTTP/1.1\r\nHost: 127.0.0.1:8888\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.103 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\nCookie: csrftoken=FQW7H4SEVPRqdqIr4vSeiP3EdjX2C0JY7VwCdmXevoXcPjiAMBtNptkiZLHqXo4d\r\n\r\n'

    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')  # 接收数据
    conn.send(b'<h1>Successful</h1>')
    conn.close()  # 关闭连接

1832669335798153216.png

可以说Web服务本质上都是在这十几行代码基础上扩展出来的。这段代码就是它们的祖宗。

用户在浏览器中输入网址,浏览器会向服务端发送数据,那浏览器会发送什么数据?怎么发?这个谁来定? 你这个网站是这个规定,他那个网站按照他那个规定,那互联网还能玩么?

所以,必须有一个统一的规则,让大家发送消息、接收消息的时候都有个格式依据,不能随便写。

这个规则就是HTTP协议,以后浏览器发送请求信息也好,服务器回复响应信息也罢,都要按照这个规则来。

HTTP协议主要规定了客户端和服务器之间的通信格式,那HTTP协议是怎么规定消息格式的呢?

我们可以分析下上面代码中print(received_data)这行代码。

get请求

稍微整下后就这样了:

# 请求头首行
GET /xxx/xxx/?name=xxx&age=111 HTTP/1.1\r\n

# 请求头
Host: 192.168.0.6:9000\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7\r\n\r\n'

这里需要了解的是http协议,简单了来说:

http协议特点:无状态的短连接。
http的应用:浏览器向服务端发送请求,就是按照http协议来的。
	- 请求头+请求体 ;请求头和请求体之间用 \r\n\r\n ;请求头之间用 \r\n
	- 一次请求和一次响应后,断开连接。  -> 短连接。  ->无状态如何体现?
	- 后期记住 请求头+cookie

http get请求的格式:

1832669336603459584.png

http get响应的格式:

1832669336754454528.png

post请求

# 请求头首行
POST /xxx/xxx/ HTTP/1.1\r\n

# 请求头
Host: 192.168.0.6:9000\r\n
Connection: keep-alive\r\n
Upgrade-Insecure-Requests: 1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\n
Accept-Encoding: gzip, deflate\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7\r\n\r\n
# 请求体
username=zhangkai&password=123

浏览器本质上发送请求时,包含请求头和请求体(可选的)。

  • GET请求

    • 只有请求头 + 没有请求体
    • 请求头之间用 \r\n
    • 请求头和请求体之间用 \r\n\r\n
  • POST请求

    • 只有请求头 + 有请求体
    • 请求头之间用 \r\n
    • 请求头和请求体之间用 \r\n\r\n

按照http协议我们给浏览器返回了结果,但是这只是最简单的,接下来,我们通过不同的示例,进一步完善我们的web框架。

根据不同的路径返回不同的内容

如何让我们的Web服务根据用户请求的URL不同而返回不同的内容呢?来看示例。 注意,这里在了解一个概念,

python
"""
根据URL中不同的路径返回不同的内容 
"""
import socket

# 创建socket对象
sk = socket.socket()

# 绑定IP和端口
sk.bind(('127.0.0.1', 8888))

# 监听
sk.listen()

while True:
    # 等待连接
    conn, addr = sk.accept()
    # 接收数据
    data = conn.recv(8192)
    data = data.decode('utf-8')
    url = data.split()[1]
    print(url)
    # 返回状态行
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')
    if url == '/oumei':
        conn.send(b'<h1>oumei</h1>')
    elif url == '/rihan':
        conn.send(b'<h1>rihan</h1>')
    else:
        conn.send(b'<h1>404</h1>')
    # 关闭连接
    conn.close()
python
"""
前一个示例我们返回的内容是最简单的几个字符,接下来我们对代码进一步封装。
根据URL中不同的路径返回不同的内容--函数版 
"""
import socket

# 1. 创建socket对象
sk = socket.socket()

# 2. 绑定IP和端口
sk.bind(('127.0.0.1', 8888))
# 3. 监听
sk.listen(5)

def func(url):
    return f"this is {url} page!".encode('utf8')


# 4. 循环监听
while True:
    # 等待连接
    conn, add = sk.accept()
    data = conn.recv(8096)  # 接收客户端发来的消息
    # 从data中取到路径
    data = str(data, encoding="utf8")  # 把收到的字节类型的数据转换成字符串
    # 按\r\n分割
    data1 = data.split("\r\n")[0]
    url = data1.split()[1]  # url是我们从浏览器发过来的消息中分离出的访问路径
    print(url)
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')  # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同内容,response是具体的响应体
    if url == "/index/":
        response = func(url)
    elif url == "/home/":
        response = func(url)
    else:
        response = b"404 not found!"

    conn.send(response)
    conn.close()
python
"""
看起来前面的代码写了一个函数,那肯定可以写多个函数,不同的路径对应执行不同的函数拿到结果,但是我们要一个个判断路径,是不是很麻烦?我们有简单的办法来解决。
"""
import socket

# 1. 创建socket对象
sk = socket.socket()

# 2. 绑定IP和端口
sk.bind(('127.0.0.1', 8888))
# 3. 监听
sk.listen(5)


# 将返回不同的内容部分封装成不同的函数
def index(url):
    return f"{url} page".encode('utf8')


def home(url):
    return f"{url} page".encode('utf8')


# 定义一个url和实际要执行的函数的对应关系
list1 = [
    ("/index/", index),
    ("/home/", home),
]

while True:
    # 等待连接
    conn, add = sk.accept()
    data = conn.recv(8096)  # 接收客户端发来的消息
    # 从data中取到路径
    data = str(data, encoding="utf8")  # 把收到的字节类型的数据转换成字符串
    # 按\r\n分割
    data1 = data.split("\r\n")[0]
    url = data1.split()[1]  # url是我们从浏览器发过来的消息中分离出的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')  # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同内容
    func = None  # 定义一个保存将要执行的函数名的变量
    for item in list1:
        if item[0] == url:
            func = item[1]
            break
    if func:
        response = func(url)
    else:
        response = b"404 not found!"

        # 返回具体的响应消息
    conn.send(response)
    conn.close()

返回HTML页面

上面的示例完美解决了不同URL返回不同内容的问题。 但是我不想仅仅返回几个字符串,我想给浏览器返回完整的HTML内容,这又该怎么办呢?

没问题,不管是什么内容,最后都是转换成字节数据发送出去的。 我们可以打开HTML文件,读取出它内部的二进制数据,然后再发送给浏览器。

python
"""
Python脚本文件和html文件在同一个目录下即可。
"""

import socket

sk = socket.socket()
sk.bind(('127.0.0.1', 8888))
sk.listen()

# 将返回不同的内容部分封装成不同的函数
def index(url):
    # 读取index.html页面的内容
    with open("index.html", "rb") as f:
        return f.read()


def home(url):
    with open("home.html", "rb") as f:
        return f.read()
# 定义一个url和实际要执行的函数的对应关系
list1 = [
    ("/index/", index),
    ("/home/", home),
]

while True:
    # 等待连接
    conn, add = sk.accept()
    data = conn.recv(8096)  # 接收客户端发来的消息
    # 从data中取到路径
    data = str(data, encoding="utf8")  # 把收到的字节类型的数据转换成字符串
    # 按\r\n分割
    data1 = data.split("\r\n")[0]
    url = data1.split()[1]  # url是我们从浏览器发过来的消息中分离出的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')  # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同内容
    print(111, url)
    func = None  # 定义一个保存将要执行的函数名的变量
    for item in list1:
        if item[0] == url:
            func = item[1]
            break
    if func:
        response = func(url)
    else:
        response = b"404 not found!"
    conn.send(response)
    conn.close()
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>index 页面</h1>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>home 页面</h1>
</body>
</html>

让网页动态起来

目前网页能够显示出来了,但是都是静态的。页面的内容都不会变化的,但真正的网站页面内容都是动态的,这怎么搞呢?

下面的示例使用时间戳来模拟动态的数据。

python
import time
import socket

sk = socket.socket()
sk.bind(('127.0.0.1', 8888))
sk.listen()


# 将返回不同的内容部分封装成不同的函数
def index(url):
    # 读取index.html页面的内容
    with open("index.html", "rb") as f:
        return f.read()


def home(url):
    with open("home.html", "rb") as f:
        return f.read()


def timmer(url):
    with open("timmer.html", "r", encoding='utf8') as f:
        return bytes(f.read().replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S")), encoding="utf8")


# 定义一个url和实际要执行的函数的对应关系
list1 = [
    ("/index/", index),
    ("/home/", home),
    ("/timmer/", timmer),
]

while True:
    # 等待连接
    conn, add = sk.accept()
    data = conn.recv(8096)  # 接收客户端发来的消息
    # 从data中取到路径
    data = str(data, encoding="utf8")  # 把收到的字节类型的数据转换成字符串
    # 按\r\n分割
    data1 = data.split("\r\n")[0]
    url = data1.split()[1]  # url是我们从浏览器发过来的消息中分离出的访问路径
    conn.send(b'HTTP/1.1 200 OK\r\n\r\n')  # 因为要遵循HTTP协议,所以回复的消息也要加状态行
    # 根据不同的路径返回不同内容
    print(111, url)
    func = None  # 定义一个保存将要执行的函数名的变量
    for item in list1:
        if item[0] == url:
            func = item[1]
            break
    if func:
        response = func(url)
    else:
        response = b"404 not found!"
    conn.send(response)
    conn.close()
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>当前时间是: @@time@@ </h1>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>index 页面</h1>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>home 页面</h1>
</body>
</html>

服务器程序和应用程序

通过上面几个实例,我们发现socket代码部分基本是不变的,变得是根据需求而改动的代码部分。

所以,对于不变的socket代码部分,Python通常提供了专门的模块来处理。这就要介绍两个概念了。

对于真实开发中的python web程序来说,一般会分为两部分:服务器程序和应用程序。

服务器程序负责对socket服务端进行封装,并在请求到来时,对请求的各种数据进行整理。

应用程序则负责具体的逻辑处理。

为了方便应用程序的开发,就出现了众多的Web框架,例如:Django、Flask、web.py 等。不同的框架有不同的开发方式,但是无论如何,开发出的应用程序都要和服务器程序配合,才能为用户提供服务。

这样,服务器程序就需要为不同的框架提供不同的支持。这样混乱的局面无论对于服务器还是框架,都是不好的。对服务器来说,需要支持各种不同框架,对框架来说,只有支持它的服务器才能被开发出的应用使用。

这时候,标准化就变得尤为重要。我们可以设立一个标准,只要服务器程序支持这个标准,框架也支持这个标准,那么他们就可以配合使用,一旦标准确定,双方各自实现。这样,服务器可以支持更多支持标准的框架,框架也可以使用更多支持标准的服务器。

而WSGI(Web Server Gateway Interface)就是一种规范,它定义了使用Python编写的web应用程序与web服务器程序之间的接口格式,实现web应用程序与web服务器程序间的解耦。

常用的WSGI服务器有uWSGI、Gunicorn。而Python标准库提供的独立WSGI服务器叫wsgiref,Django开发环境用的就是这个模块来做服务器,除此之外,还有其他的比如flask框架使用的werkzeug来提供了wsgiref相同的功能。

来个最简单的示例:

python
"""
wsgiref是内置模块,无需下载
"""
from wsgiref.simple_server import make_server

def application(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [bytes('<h1>Hello, web!</h1>', encoding='utf-8'), ]


if __name__ == '__main__':
    httpd = make_server('127.0.0.1', 8888, application)
    httpd.serve_forever()
python
"""
使用前先下载
# pip install werkzeug
pip install werkzeug==3.0.1
"""
from werkzeug.wrappers import Response
from werkzeug.serving import run_simple

def application(environ, start_response):
    response = Response('<h1>Hello, web!</h1>', mimetype='text/html')
    return response(environ, start_response)


if __name__ == '__main__':
    run_simple('127.0.0.1', 8888, application)

接下来的示例,我们使用wsgiref模块来继续完善之前的代码示例。

python
import time
from wsgiref.simple_server import make_server


# 将返回不同的内容部分封装成不同的函数
def index(url):
    # 读取index.html页面的内容
    with open("index.html", "rb") as f:
        return f.read()


def home(url):
    with open("home.html", "r", encoding='utf8') as f:
        return f.read()


def timmer(url):
    with open("timmer.html", "r", encoding='utf8') as f:
        return f.read().replace('@@time@@', time.strftime("%Y-%m-%d %H:%M:%S"))


# 定义一个url和实际要执行的函数的对应关系
list1 = [
    ("/index/", index),
    ("/home/", home),
    ("/timmer/", timmer),
]


def application(environ, start_response):
    path_info = environ['PATH_INFO']
    # print(path_info)  # /timmer/
    for s, f in list1:
        if path_info == s:
            response = f(path_info)
            break
    else:
        response = "404 not found!"
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [bytes(response, encoding='utf-8'), ]


if __name__ == '__main__':
    httpd = make_server('127.0.0.1', 8888, application)
    httpd.serve_forever()
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>当前时间是: @@time@@ </h1>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>index 页面</h1>
</body>
</html>
html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>home 页面</h1>
</body>
</html>

通过wsgiref这些框架帮我们封装了socket相关的代码,让我们的精力能集中于具体的业务上。

但你以为这就结束了吗?远远没有......因为我们在开发中要考虑的还有很多很多:

  • 用户认证:来了个请求,这个请求要访问的资源是否需要登录之后才能访问。
  • 权限:这个请求是否有权限访问某个资源。
  • 限流:对于某些资源,不能任其无限的访问,比如实现投票业务,实现每日只能投一票。
  • 请求头/请求体的封装:为了业务中方便的从请求中提取想要的数据。
  • 数据库操作。
  • ........

以上种种需求,单靠wsgiref这些框架是不行的,我们倒是可以自己来处理,但是每一个项目都要搞一遍,这就是重复造轮子了,所以,Django、flask等web框架应运而生。

例如,Django框架主要集成了wsgiref,然后实现了具体业务之外的其它操作,so,你只管踩油门,剩下的交给Django.........

1832669336939003904.png

Python中主流web框架

根据框架集成功能的多少划分

  • Django,内部提供了很多组件,认证、权限、后台管理、session.....
  • flask、tornado、sanic、fastapi... 本身自己功能很少+第三方组件

异步框架/同步框架

  • 异步非阻塞:tornado、sanic、fastapi、django(从Django3开始支持,到目前Django5了还不是完全体)
  • 同步:django、flask、bottle、webpy..

个人推荐:

传统项目,非常多且复杂的业务需求场景,用Django,对于异步支持这块,可以持续关注。

对于高性能和高并发的api业务,可以选择fastapi,但fastapi对于异步这块要求较高,需要你对异步的底层实现原理、异步数据库连接操作等都要有了解,上手难度小,但深入研究就需要一些知识储备了,并且目前来说各种三方库也不多,且支持的也不够好,遇到问题你可能在网上找不到解决方案,这时就需要你自己牛逼起来了,但也是可以行上手学习并且持续关注后续发展的。