Web服务器

本文围绕Web服务器展开,介绍了HTTP协议、请求与响应。还阐述了用Python实现简单HTTP服务器的多种方式,包括多进程、多线程、协程。此外,探讨了并发服务器的非堵塞模式和epoll机制,提及可结合IO多路复用与多进程/线程实现高性能并发。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Web服务器

HTTP

HTTP协议

在Web应用中,服务器把网页传给浏览器,实际上就是把网页的HTML代码发送给浏览器,让浏览器显示出来。而浏览器和服务器之间的传输协议是HTTP

  • HTML是一种用来定义网页的文本,会HTML,就可以编写网页
  • HTTP是在网络上传输HTML的协议,用于浏览器和服务器的通信
HTTP请求

  • 这就是浏览器发送给服务器的数据,而这些数据的格式,就是 HTTP协议

说明:

 GET / HTTP/1.1

GET表示一个读取请求, / 表示URL的路径,URL总是以 / 开头, / 就表示首页,最后的HTTP/1.1指示采用的HTTP协议版本是1.1,目前HTTP协议的版本就是1.1

HTTP响应

  • HTTP响应分为Header和Body两部分(Body是可选项)

说明:

HTTP/1.1 200 OK

200表示一个成功的响应,后面的OK是说明。

如果返回的不是200,那么往往有其他的功能,例如:

  • 失败的响应有404 Not Found:网页不存在
  • 500 Internal Server Error:服务器内部出错
  • …等等…

响应格式

200 OK
    Header1: Value1
    Header2: Value2
    Header3: Value3

    body data goes here...

注意header 空一行后,就意味着header 的结束,下面是 body HTTP里换行是用 \r\n

也就是说headerbody之间隔了 两个 \r\n


简单的HTTP服务器

Web静态服务器-2-显示需要的页面
import socket

def service_client(new_socket):
    """为这个客户端返回数据"""
    # 1.接受浏览器发送来的请求,即http请求
    request = new_socket.recv(1024)
    print(request)
    # 2.返回http格式的数据给浏览器
    # >>>>准备发送----header
    respeonse = "HTTP/1.1 200 OK\r\n"
    respeonse += "\r\n"
    # >>>>准备发送----body
    respeonse += "<h1>hello world</h1>"
    new_socket.send(respeonse.encode("utf-8"))
    # 关闭套接字
    new_socket.close()

def main():
    """用来完整体的控制"""
    # 1.创建套接字
    tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 重复使用端口,以便服务器先关闭导致等待,端口被占用
    tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 2.绑定
    tcp_server.bind(("", 7080))
    # 3.变为监听套接字
    tcp_server.listen(128)

    while True:
        # 4.等待客户端的链接
        new_socket, clien_addr = tcp_server.accept()
        # 5.为之歌客户端服务
        service_client(new_socket)

if __name__ == '__main__':
    main()
Web静态服务器-2-显示需要的页面
import socket
import re

def service_client(new_socket):
    """为这个客户端返回数据"""
    # 1.接受浏览器发送来的请求,即http请求
    request = new_socket.recv(1024).decode("utf-8")
    # print(request)
    request_lines = request.splitlines()
    print("")
    print(request_lines)

    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    file_name = ""
    if ret:
        file_name = ret.group(1)
        if file_name == "/":
            file_name = "/index.html"

    # 2.返回http格式的数据给浏览器
    # >>>>----获取----body----内容
    try:
        f = open("." + file_name, "rb")
    except:
        respeonse = "HTTP/1.1 404 NOT FOUND\r\n"
        respeonse += "\r\n"
        respeonse += "-----找不到该网页-----"
        new_socket.send(respeonse.encode("gbk"))
    else:
        html_content = f.read()
        f.close()
        # >>>>----header内容
        respeonse = "HTTP/1.1 200 OK\r\n"
        respeonse += "\r\n"
        # >>>>发送----header
        new_socket.send(respeonse.encode("utf-8"))
        # >>>>发送----body
        new_socket.send(html_content)

    # 关闭套接字
    new_socket.close()

def main():
    """用来完整体的控制"""
    # 1.创建套接字
    tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 重复使用端口,以便服务器先关闭导致等待,端口被占用
    tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 2.绑定
    tcp_server.bind(("", 7080))
    # 3.变为监听套接字
    tcp_server.listen(128)

    while True:
        # 4.等待客户端的链接
        new_socket, clien_addr = tcp_server.accept()
        # 5.为之歌客户端服务
        service_client(new_socket)

if __name__ == '__main__':
    main()
多进程http服务器
import socket
import re
import multiprocessing

def service_client(new_socket):
    """为这个客户端返回数据"""

    # 1.接受浏览器发送来的请求,即http请求
    request = new_socket.recv(1024).decode("gbk")
    request_lines = request.splitlines()
    print("")
    print(request_lines)
    ret = re.match(r"[^/]+(/[^ ]*)", request_lines[0])
    file_name = ""
    if ret:
        file_name = ret.group(1)
        if file_name == "/":
            file_name = "/index.html"

    # 2.返回http格式的数据给浏览器
    # >>>>----获取----body----内容
    try:
        f = open("." + file_name, "rb")
    except:
        respeonse = "HTTP/1.1 404 NOT FOUND\r\n"
        respeonse += "\r\n"
        respeonse += "-----找不到该网页-----"
        new_socket.send(respeonse.encode("gbk"))
    else:
        html_content = f.read()
        f.close()
        # >>>>----header内容
        respeonse = "HTTP/1.1 200 OK\r\n"
        respeonse += "\r\n"
        # >>>>发送----header
        new_socket.send(respeonse.encode("utf-8"))
        # >>>>发送----body
        new_socket.send(html_content)
    # 关闭套接字
    new_socket.close()

def main():
    """用来完整体的控制"""
    # 1.创建套接字
    tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # 重复使用端口,以便服务器先关闭导致等待,端口被占用
    tcp_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # 2.绑定
    tcp_server.bind(("", 7080))
    # 3.变为监听套接字
    tcp_server.listen(128)

    while True:
        # 4.等待客户端的链接
        new_socket, clien_addr = tcp_server.accept()

        # 5.为多个客户服务 多进程
        p = multiprocessing.Process(target=service_client, args=(new_socket,))
        p.start()
        new_socket.close()

    # 关闭监听套接字
    tcp_server.socket.close()

if __name__ == '__main__':
    main()
多线程http服务器
  • 多线程只需把上面的模块换一下,线程创建改一下即可
p = threading.Thread(target=service_client, args=(new_socket,))
        p.start()

值得注意的是,因为多进程会复制全部资源,服务结束使需要new_socket.close()

而多线程就不能关闭,不然无法正常工作

gevent(协程)实现http服务器
  • 协程实现的服务器,相比于进程和线程,非常的节省资源,访问人数越多,相比于前两种,协程就越占优势~
  • 这个也只需简单修改即可,把模块换一下
gevent.spawn(service_client, new_socket)
  • 记得在前面加上这句话
monkey.patch_all()

并发服务器

非堵塞模式
  • 想让多人同时访问一个服务器,并不是只有多进程、线程、协程才能完成
  • 单进程-单线程同样可以完成多人访问,只需把套接字手动设为非堵塞状态
import socket

tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server.bind(("", 7890))
tcp_server.listen(128)
tcp_server.setblocking(False)  # 设置套接字为非堵塞方式

client_socket_list = list()

while True:
    try:
        new_socket, new_addr = tcp_server.accept()
    except Exception as ret:
        print("----没有新用户的到来---")
    else:
        print("---只要没有异常,那么就相当于一个新的客户端到来")
        new_socket.setblocking(False)  # 设置套接字为非堵塞方式
        client_socket_list.append(new_socket)

    for client_socket in client_socket_list:
        try:
            recv_data = client_socket.recv(1024)
        except Exception as ret:
            print("----这个客户端没有发送来数据----")
        else:
            if recv_data:
                # 对方发送来数据
                print("---客户端发送过来了数据---")
            else:
                # 对方调用了 close 导致了 recv返回
                client_socket_list.remove(client_socket)
                client_socket.close()
                print("客户端已经关闭")
epoll
  • epoll多用于Linux、Ulinix、Mac,故一般大型服务器都是用他们的
  • IO 多路复用

就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。

select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。

它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

  • epoll简单模型
import socket
import select

# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置可以重复使用绑定的信息
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
# 绑定本机信息
s.bind(("",7788))
# 变为被动
s.listen(10)
# 创建一个epoll对象
epoll = select.epoll()

# 测试,用来打印套接字对应的文件描述符
# print(s.fileno())
# print(select.EPOLLIN|select.EPOLLET)

# 注册事件到epoll中
# epoll.register(fd[, eventmask])
# 注意,如果fd已经注册过,则会发生异常
# 将创建的套接字添加到epoll的事件监听中
epoll.register(s.fileno(), select.EPOLLIN|select.EPOLLET)

connections = {}
addresses = {}

# 循环等待客户端的到来或者对方发送数据
while True:
    # epoll 进行 fd 扫描的地方 -- 未指定超时时间则为阻塞等待
    epoll_list = epoll.poll()
    # 对事件进行判断
    for fd, events in epoll_list:
        # print fd
        # print events

        # 如果是socket创建的套接字被激活
        if fd == s.fileno():
            new_socket, new_addr = s.accept()

            print('有新的客户端到来%s' % str(new_addr))

            # 将 conn 和 addr 信息分别保存起来
            connections[new_socket.fileno()] = new_socket
            addresses[new_socket.fileno()] = new_addr

            # 向 epoll 中注册 新socket 的 可读 事件
            epoll.register(new_socket.fileno(), select.EPOLLIN|select.EPOLLET)

        # 如果是客户端发送数据
        elif events == select.EPOLLIN:
            # 从激活 fd 上接收
            recvData = connections[fd].recv(1024).decode("utf-8")
            if recvData:
                print('recv:%s' % recvData)
            else:
                # 从 epoll 中移除该 连接 fd
                epoll.unregister(fd)
                # server 侧主动关闭该 连接 fd
                connections[fd].close()
                print("%s---offline---" % str(addresses[fd]))
                del connections[fd]
                del addresses[fd]
  • I/O 多路复用的特点:

通过一种机制使一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,epoll()函数就可以返回。 所以, IO多路复用,本质上不会有并发的功能,因为任何时候还是只有一个进程或线程进行工作,它之所以能提高效率是因为select\epoll 把进来的socket放到他们的 ‘监视’ 列表里面,当任何socket有可读可写数据立马处理,那如果select\epoll 手里同时检测着很多socket, 一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理高效率。

当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换页会耗掉更多的系统资源。 所以我们可以结合IO多路复用和多进程/多线程 来高性能并发,IO复用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑。

  • 关于epoll的更多资料请参考:

http://blog.youkuaiyun.com/xiajun07061225/article/details/9250579

END…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值