网络从套接字到Web服务器

Posted ydongy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络从套接字到Web服务器相关的知识,希望对你有一定的参考价值。

纸上得来终觉浅,绝知此事要躬行。

前言

今天来说说从远古套接字到现在的Web服务器的具体过程。在之前我想说说为什么会有这篇文章,其实是在我学习到了Flask框架上下文管理的的时候,我在梳理Flask请求的整个过程。但是总是很让我困惑,因为我所说的请求流程不是上来就直接url匹配而是说从网络请求开始的过程,了解过Flask的就知道,所谓的Flask框架是有Flask+Jinja2+Werkzuge三个组合在一起。今天说要说的就是Werkzeug的模块,它实现了WSGI标准的服务器来接受HTTP请求。但是我找不到werkzeug网络请求的入口,看视频讲解是调用了app__call__方法,但是也没有具体说,于是就开始了探索的过程。

OK,现在应该差不多明确了本章所要讨论的内容了吧!说实话搞清这个东西其实还挺让我头疼,前前后后查了WSGI相关的资料,HTTP协议相关的资料,以及网络编程的资料,只能说自己的基础太弱了。一步一步做吧,等彻底搞清楚之后,Flask的整体流程大概也就了解的差不多了。接下来让我们回到Socket的年代。

客户端 / 服务器架构

说之前我们先来简单回顾一下我们的C/S架构:

  • 服务器:目的就是等待客户端的请求,提供服务返回响应,然后等待更多请求

  • 客户端:请求服务器,并发送必要的数据,然后等待服务器的回应。

目前最常见的客户端/服务器架构,就是一个用户或多个客户端计算机通过因特网从一台服务器上检索信息。如图所示:

技术图片

关于客户端与服务器端想要进行通信,客户端需要做的是创建单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。

套接字

套接字是计算机网络数据结构,在任何类型的通信开始之前,网络应用程序必须创建套接字。套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信。

套接字连接连接有两种风格,第一种是面向连接的,第二种是面向无连接

  • 面向连接:通信之前需要连接,保证传输可靠,消息拆分,能够保证每一条消息片段达到目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。实现这种连接类型的主要协议是传输控制协议(TCP为 了创建 TCP 套接字,必须使用SOCK_STREAM作为套接字类型。
  • 无连接的套接字:在通信开始之前不需要建立连接。数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,消息是以整体发送的。实现这种连接类型的主要协议是用户数据报协议( UDP为 了创建 UDP 套接字,必须使用 SOCK_DGRAM 作为套接字类型。

说了这么多理论,其实也不是我说的,是我在Python核心编程-第三版里面截取下来的,主要是Socket已经很底层了,我也不大清楚,总之就把他理解成可以关联应用层和传输层的介质,当然他还有很多功能,都是操作系统级别了。更需要清楚一点他可以建立传输层的协议,比如:TCPUPD,下面正式开始Python中Socket的使用。

Python中的Socket

下面将使用的主要模块就是 socket 模块,在这个模块中可以找到 socket()函数,该函数用于创建套接字对象。套接字有自己的方法集,这些方法可以实现基于套接字的网络通信,分别创建TCP的客户端,服务端和UDP协议客户端,服务端。

创建 TCP 服务器

socket创建TCP服务器的过程:

1.创建TCP 服务器套接字

2.把服务器地址端口绑定到到套接字

3.开启 TCP 监听器的调用,因为TCP是面向连接的,需要三次握手

4.等待连接,连接成功之后就是等待客户端发送数据

5.处理请求,返回响应

import socket

from time import ctime

HOST = ‘127.0.0.1‘
PORT = 8888
BUFFER_SIZE = 1024  # 缓冲区大小设置为 1KB
ADDR = (HOST, PORT)

#  创建TCP 服务器套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server_socket.bind(ADDR)  # 套接字绑定到服务器地址
server_socket.listen(5)  # 开启 TCP 监听器的调用。传入连接请求的最大数。

while True:
    print("waiting for connecting...")
    # 等待客户端的连接
    client_socket, addr = server_socket.accept()
    print("success connected from {}".format(addr))

    while True:
        # 等待客户端发送的消息
        data = client_socket.recv(BUFFER_SIZE)
        if not data:
            break
        # 格式化并返回相同的数据
        client_socket.send((‘[{}] {}‘.format(ctime(), data)).encode())
    client_socket.close()
server_socket.close()

创建 TCP 客户端

socket创建TCP客户端的过程:

1.创建TCP 客户端套接字

2.连接到指定服务器IP和端口

3.发送数据

4.接受响应

import socket

from time import ctime

HOST = ‘127.0.0.1‘
PORT = 8888
BUFFER_SIZE = 1024  # 缓冲区大小设置为 1KB
ADDR = (HOST, PORT)

#  创建TCP 客户端套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 连接到服务器
client_socket.connect(ADDR)

while True:
    data = input(">")
    if not data:
        break
    client_socket.send(data.encode())
    data = client_socket.recv(BUFFER_SIZE)
    if not data:
        break
    print(data.decode())

client_socket.close()

创建 UDP 服务器

socket创建UDP服务器的过程:

1.创建UDP 服务器套接字

2.把服务器地址端口绑定到到套接字

3.等待客户端发送数据

4.处理请求,返回响应

import socket

from time import ctime

HOST = ‘127.0.0.1‘
PORT = 8888
BUFFER_SIZE = 1024  # 缓冲区大小设置为 1KB
ADDR = (HOST, PORT)

#  创建UDP 服务器套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

server_socket.bind(ADDR)  # 套接字绑定到服务器地址

while True:
    print("waiting for connecting...")
    data, addr = server_socket.recvfrom(BUFFER_SIZE)
    # 格式化并返回相同的数据
    server_socket.sendto((‘[{}] {}‘.format(ctime(), data)).encode(), addr)
    print("...received from and returned to:", data)
server_socket.close()

创建 UDP 客户端

socket创建UDP客户端的过程:

1.创建UDP 客户端套接字

2.发送数据到指定IP和端口服务器

3.接受响应

import socket

from time import ctime

HOST = ‘127.0.0.1‘
PORT = 8888
BUFFER_SIZE = 1024  # 缓冲区大小设置为 1KB
ADDR = (HOST, PORT)

#  创建UDP 客户端套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
    data = input(">")
    if not data:
        break
    client_socket.sendto(data.encode(), ADDR)
    data = client_socket.recvfrom(BUFFER_SIZE)
    if not data:
        break
    print(data)

client_socket.close()

SocketServer

下面这个不是重点可以不看,主要是没用过,就随手记录下了。

SocketServer 是标准库中的一个高级模块,它的目标是简化很多样板代码,只需要创建网络客户端和服务器所必需的代码。

创建 SocketServer TCP 服务器

from socketserver import TCPServer, StreamRequestHandler
from time import ctime

HOST = ‘127.0.0.1‘
PORT = 8888
BUFFER_SIZE = 1024  # 缓冲区大小设置为 1KB
ADDR = (HOST, PORT)

class RequestHandler(StreamRequestHandler):

    def handle(self) -> None:
        """接收到一个来自客户端的消息时,它就会调用 handle()方法"""
        print("success connected from {}".format(self.client_address))

        # StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用 readline()来获取客户端消息,并利用 write()将字符串发送回客户端。
        self.wfile.write((‘[{}] {}‘.format(ctime(), self.rfile.readline())).encode())

server = TCPServer(ADDR, RequestHandler)
print("waiting for connecting...")

server.serve_forever()

创建 SocketServer TCP 客户端

import socket
from time import ctime

HOST = ‘127.0.0.1‘
PORT = 8888
BUFFER_SIZE = 1024  # 缓冲区大小设置为 1KB
ADDR = (HOST, PORT)

while True:
    #  创建TCP 客户端套接字
    client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 连接到服务器
    client_socket.connect(ADDR)
    data = input(">")
    if not data:
        break
    client_socket.send(‘{}
‘.format(data).encode())
    data = client_socket.recv(BUFFER_SIZE)
    if not data:
        break
    print(data.strip().decode())
    client_socket.close()

SocketServer 请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这
个原因,我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,
都需要创建一个新的套接字。

因为StreamRequestHandler使用的处理程序类对待套接字通信就像文件一样, 所以必须发送行终止符(回车和换行符)

HTTP协议

在基于Socket建立HTTP服务器的前提,我们需要简单了解一下HTTP协议。

HTTPHyperText Transfer Protocol)协议又称超文本传输协议,是一种通信协议。它允许将超文本标记语言(html)文档从Web服务器传送到客户端的浏览器。且HTTP是属于应用层的面向对象、无状态的协议。

接着我们需要知道HTTP协议格式,也就是HTTP报文格式,只有遵循这种格式规范,发送的数据才符合HTTP协议,才能够被浏览器所识别解析。并且一次HTTP请求结束之后连接就会断开。

HTTP 报文本身是由多行( CR+LF 回车+换行) 数据构成的字符串文本。报文大致可分为报文首部报文主体两块,其中报文又分为请求报文和响应报文,结构如下图:

技术图片

基于Socket的Web服务器

我们了解到了HTTP报文的格式,但是我们需要关注的是服务器,因为Web 应用同样遵循客户端/服务器架构,而此时的客户端就是是浏览器, 服务器端就是Web服务器。也就是说我们不太需要去编写客户端,因为我们通过浏览器去访问我们的服务器,浏览器自然会遵循HTTP请求报文的格式,而我们的服务器想要有回应就必须遵循HTTP响应报文的格式。

纯文本响应服务器

下面就是一个简单的Web服务器,利用Socket+多线程,再此不考虑技术的选型,主要是了解HTTP通信的过程:

import socket
import threading

def request_handler(client_socket):
    request_content = client_socket.recv(1024).decode("utf-8")
    print(request_content)
    client_socket.send(b"HTTP/1.1 200 OK

hello world") # 响应报文
    client_socket.close() # 关闭连接

def main():
    # 1. 创建tcp套接字
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 完成3次握手4次挥手,重复使用端口
    tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 2. 绑定
    tcp_socket.bind(("127.0.0.1", 8888))
    # 3. 监听
    tcp_socket.listen(128)
    # 4. 等待链接
    while True:
        print("----服务器已经开启----")
        client_socket, client_addr = tcp_socket.accept()
        # 开启线程
        threading.Thread(target=request_handler, args=(client_socket,)).start()
        # request_handler(client_socket)
    # tcp_socket.close()

if __name__ == ‘__main__‘:
    main()

代码的执行过程说一下:

1.创建TCP套接字

2.为套接字绑定IP和端口号

3.监听客户端连接或浏览器连接

4.等待客户端发送数据,存在一个客户端连接就创建一个线程,交给request_handler处理,然后始终返回hello world

5.断开与客户端的连接

注意:浏览器请求的过程其实也是需要三次握手和四次挥手,因为他们是TCP连接。

技术图片 技术图片 技术图片

伪静态响应服务器

目前我们实现的是无论什么HTTP请求,返回的总是hello world,这必然不是我们想要的。那么如果想要实现传输html页面的数据,那就可以把request_handler里面的逻辑更改,比如下面的这部分代码:

def response_content(title):
    if title == "/":
        title = "/index.html"
    try:
        with open(title[1:], ‘rb‘) as f: # 读取页面内容
            content = f.read()
    except Exception as e:
        with open(‘404.html‘, ‘rb‘) as f: # 异常一律返回404页面
            content = f.read()
    return content

def request_handler(client_socket):
    request_content = client_socket.recv(1024).decode("utf-8")
    print(request_content)
    ret = re.match(r"GET (/.*) HTTP/1.1", request_content) # 匹配请求URL
    if ret:
        title = ret.group(1)
        content = response_content(title) # 处理
        client_socket.send(b"HTTP/1.1 200 OK
" + b"
" + content) # 响应报文
    client_socket.close()
技术图片 技术图片

通过两个案例应该了解了HTTP协议请求响应的过程,我们其实就明白了一个Web应用的本质就是:

  1. 浏览器发送一个HTTP请求;
  2. 服务器收到请求,生成一个HTML文档;
  3. 服务器遵循HTTP协议格式,组织HTML文档作为HTTP响应的Body发送给浏览器;
  4. 浏览器收到HTTP响应,从HTTP Body取出HTML文档并显示。

符合WSGI的Web服务器

上面我们应该清楚了,只要我们的服务器遵循HTTP协议,并且按照HTTP协议格式返回数据就可以给客户端返回响应,但是有个问题就是,我们处理数据的逻辑以及组织HTTP响应报文的逻辑揉在了一起,如果是动态请求,那么我们就需要不断的组织HTTP响应报文,代码越来乱,耦合性越来越高。

于是就出现了WSGI,需要清楚的是**WSGI 不是服务器,也不是用于与程序交互的 API,更不是真实的代码,而只是定义的一个接口。目标是在 Web 服务器和 Web 框架层之间提供一个通用的 API 标准,减少之间的互操作性并形成统一的调用方式。 **

下面一张图是符合WSGI标准的请求流程:

技术图片

接下来我们来看一下WSGI的定义:

def simple_wsgi_app(environ, start_response):
    status = ‘200 OK‘
    headers = [(‘Content-type‘, ‘text/html‘)]
    start_response(status, headers)
    return [‘Hello world!‘]

上面的simple_wsgi_app函数就是符合WSGI标准的一个HTTP处理函数,它接收两个参数,返回的内容必须是可迭代的:

  • environ:一个包含所有HTTP请求信息的字典对象;
  • start_response:一个发送HTTP响应的函数,响应必须含有 HTTP 返回码,以及 HTTP 响应头。

整个simple_wsgi_app()函数本身没有涉及到任何解析HTTP的部分,也就是说,把底层web服务器解析部分和应用程序逻辑部分进行了分离,这样开发者就可以专心做一个领域了。所以simple_wsgi_app()函数必须由WSGI服务器来调用。

  • 应用程序
def index():
    return "index page"

def login():
    return "login page"

def application(environ, start_response):
    """
    提供给服务器调用的函数
    :param environ: {"xxx":"xxx"....}
    :param start_response: 服务器函数引用
    :return: 返回响应体内容
    """
    status = ‘200 OK‘
    headers = [(‘Content-type‘, ‘text/plain;charset=utf-8‘)]
    start_response(status, headers)
    print(environ)
    func = urlpatterns.get(environ.get("URL"))
    if not func:
        return [""]
    resp = func()
    return [resp]

urlpatterns = {
    "/": index,
    "/index": index,
    "/login": login
}
  • 服务器端
import socket
import threading
import re

class WSGIServer(object):
    def __init__(self, app, ip=‘127.0.0.1‘, port=8888, listen=128):
        # 1. 创建tcp套接字
        self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # 完成3次握手4次挥手,重复使用端口
        self.tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 2. 绑定
        self.tcp_socket.bind((ip, port))
        # 3. 监听
        self.tcp_socket.listen(listen)

        self.application = app

    def run(self):
        # 4. 等待链接
        while True:
            print("----服务器已经开启 IP:{} Port:{}----".format("127.0.0.1", 8888))
            client_socket, client_addr = self.tcp_socket.accept()
            # 开启线程
            threading.Thread(target=self.request_handler, args=(client_socket,)).start()
            # client_socket.close()

    def request_handler(self, client_socket):
        """
        请求处理函数
        :param client_socket: 客户端Socket
        :return: 返回响应
        """
        request_content = client_socket.recv(1024).decode("utf-8")
        content_list = request_content.split("
")
        url = re.match(r"GET (/.*) HTTP/1.1", content_list[0]).group(1)

        environ = dict()

        environ["URL"] = url

        # 调用应用框架接口
        body = self.application(environ=environ, start_response=self.start_response)

        # 组织响应头
        header = "HTTP/1.1 {status}
".format(status=self.status)
        for temp in self.headers:
            header += "{key}:{value}
".format(key=temp[0], value=temp[1])

        # 合并响应头和响应体
        response_content = header + "
" + "
".join(body)

        # 返回响应
        client_socket.send(response_content.encode())

        # 关闭客户端连接
        client_socket.close()

    def start_response(self, status, headers):
        """
        获取应用程序设置的status,header
        :param status: 200 OK / 404 Not Found
        :param headers:[("server", "my server 1.0"),("xx","xx")....]
        :return:
        """
        self.status = status
        self.headers = [("server", "my server 1.0")]
        self.headers += headers

def main():
    from my_flask import application # 导入应用程序
    # 启动http服务器
    http_server = WSGIServer(app=application)
    # 运行http服务器
    http_server.run()

if __name__ == ‘__main__‘:
    main()

整理思路:

1.实现一个简单的Web服务器

2.请求处理,组织参数(environ),调用应用框架中WSGI标准接口

# 调用应用框架接口
self.application(environ=environ, start_response=self.start_response)

3.Web需要实现一个函数作为应用传递

def start_response(self, status, headers):
    pass

4.应用程序提供接口,且函数中必须调用服务器传过来的引用,以及必须返回一个可迭代对象

def application(environ, start_response):
    pass

WSGI工具包Werkzeug

上面的代码基本的实现已经完成了WSGI的标准,当然这也太简陋了。俗话说:人生苦短,我用Python,何必重复造轮子,用现成的不香啊!香,真香啊!下面就来说说Werkzeug

import os
import redis
from werkzeug.wrappers import Request, Response
from werkzeug.wsgi import SharedDataMiddleware

class Shortly(object):

    def __init__(self, config):
        self.redis = redis.Redis(config[‘redis_host‘], config[‘redis_port‘])

    def dispatch_request(self, request):
        return Response(‘Hello World!‘)

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

def create_app(redis_host=‘localhost‘, redis_port=6379, with_static=True):
    app = Shortly({
        ‘redis_host‘: redis_host,
        ‘redis_port‘: redis_port
    })
    if with_static:
        app.wsgi_app = SharedDataMiddleware(app.wsgi_app, {
            ‘/static‘: os.path.join(os.path.dirname(__file__), ‘static‘)
        })
    return app

if __name__ == ‘__main__‘:
    from werkzeug.serving import run_simple

    app = create_app()
    run_simple(‘127.0.0.1‘, 5000, app, use_debugger=True, use_reloader=True)

程序实现思路:

1.app = create_app()内部创建了一个对象,返回对象实例

2.启动一个简单的HTTP服务器,接受一个参数app。调用run_simple方法,实际是一个闭包,执行内部的inner()方法,服务器最终启动

3.请求到来,符合WSGI标准应该执行app(),而app是一个类,所以执行__call__方法,开始处理请求,完成之后再把响应返回,再由服务器组织发送给客户端。

def run_simple(hostname,port,application,use_reloader=False,use_debugger=False......):
    .....
    def inner():
        ....
        # 内部有线程,进程,但最终实例化的都是BaseWSGIServer的对象
        srv = make_server(hostname,port,application,......)
        ......
        # 内部HTTPServer.serve_forever(self),而serve_forever最终实现的是selector.select(0.5)
        srv.serve_forever()
        
    if use_reloader:
        ......
        from ._reloader import run_with_reloader
		
        # 内部启动了一个守护线程,实际还是调用inner
        run_with_reloader(inner, extra_files, reloader_interval, reloader_type)
    else:
        inner()

下面再来看看Flask框架中的使用。

  • app.run()
def run(self, host=None, port=None, debug=None, load_dotenv=True, **options):
        .....
        from werkzeug.serving import run_simple

        try:
            run_simple(host, port, self, **options)
        finally:
            .....
  • 启动Flask程序
from flask import Flask
app = Flask(__name__)

@app.route("/index/")
def index():
    return "index"

if __name__ == ‘__main__‘:
    app.run() # 同时启动run_simple(host, port, self, **options),传入self=app

我们可以看出来,Flask中启动项目,实则是启动了Werkzeug提供的服务器,传入了自身的实例对象app,按照WSGI标准在最终接受请求的时候应该执行app(),而此时app是一个对象,所以就会调用Flask类中的__call__方法。开始处理请求,完成之后再把响应返回,再由服务器组织发送给客户端。

OK,现在应该知道Flask有这么一个流程:

app.run()====>启动服务器等待请求连接====>请求执行Flask中的__call__方法====>Flask应用中的处理(什么钩子函数,上下文,url匹配,视图函数处理,模板渲染)最终返回响应给__call__=======>然后再由__call__方法返回给WSGI服务器=====>服务器组织响应报文,返回给客户端======>断开连接

参考资料:
Python核心编程(第三版)
Werkzeug





以上是关于网络从套接字到Web服务器的主要内容,如果未能解决你的问题,请参考以下文章

将 Web 套接字请求从 Nginx 传递到 uWSGI 服务器

如何将数据从 Web 套接字服务器发送到客户端?

Java Web 实战 15 - 计算机网络之网络编程套接字

为从浏览器到 apache http 服务器到 Web 服务的 Web 套接字调用创建了多少 TCP 连接

上传文件时截断 Web 套接字块

使用 Web 套接字在浏览器中创建图形