MENU

epoll打造单线程高并发http服务器

• March 4, 2019 • Read: 116 • python

在上一篇文章中,我分别用多进程多线程和协程做了三个简单的web服务器,然后今天尝试了一下用socket的基本功能打造了一个单进程、线程、非堵塞、长连接的http服务器,紧接着又通过epoll改进。

首先,先看一下代码:

import socket
import re

def response_data(client_socket,client_socket_list):

    try:
        #接收数据
        recv_data=client_socket.recv(1024)
    except :
        pass
    else :
        if recv_data:
            #解码并打印请求头
            print('\n\n\n'+'>'*20)
            print('接收数据类型:',type(recv_data),len(recv_data))
            print(recv_data.decode('utf8'))


            #检查请求头信息
            a_file=re.split(r' | \n',str(recv_data.decode('utf8')))[1]
            if str(a_file)=='/':
                a_file+='index.html' 
            file_name='./html'+a_file
            print('文件名:',file_name)

            #二进制可读打开文件
            try:
                with open(file=file_name,mode='rb') as f:
                    content=f.read()
                #构建响应头信息
                response_header='HTTP/1.1 200 OK '+'\n'+'Content-Length:%d' % len(content)+'\n\n'
            except :
                content=("-"*20+"404  FILE NOT FOUND"+'-'*20).encode()
                #构建响应头信息
                response_header='HTTP/1.1 404 FILE NOT FOUND'+'\n'+'\n'

            #返回给浏览器信息
            #通过utf8编码为二进制
            print('响应头类型:',type(response_header))


            #socket通过二进制传输数据
            client_socket.send(response_header.encode('utf8'))
            client_socket.send(content)


            #client_socket.close()
        else :
            #recv_data为空,说明客户端关闭了链接
            client_socket.close()
            client_socket_list.remove(client_socket)


def main():
    #创建tcp_server_socket
    tcp_server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

    #绑定端口
    tcp_server_socket.bind(('',80))

    #创建监听
    tcp_server_socket.listen(128)

    #tcp_server_socket套接字设置成非堵塞
    tcp_server_socket.setblocking(False)

    client_socket_list=list()
    while True:
        try:
            #print('等待客户端链接')
            #等待连接
            client_socket,client_addr= tcp_server_socket.accept()
        except :
            pass
        else :
            #clent_socket套接字设置成非堵塞
            print('检测到客户端链接')
            client_socket.setblocking(False)
            client_socket_list.append(client_socket)
            print('同时连接的客户端数量:'+str(len(client_socket_list)))
            for client_socket in client_socket_list:
                print(client_socket)

        for client_socket in client_socket_list:

            response_data(client_socket,client_socket_list)

    tcp_server_socket.close()

if __name__ == '__main__':

    main()

关于长链接和短连接,可以参考一下知乎的相关答案:https://www.zhihu.com/question/22677800

这个版本美中不足的就是当连接的客户端非常多的时候,

for client_socket in client_socket_list:

    response_data(client_socket,client_socket_list)

这段代码就会执行很长时间,就影响了其他客户端的连接。然后我加入了linux系统的epoll功能进行优化。关于epoll已经有大神写的非常详细了:https://blog.csdn.net/xiajun07061225/article/details/9250579
此处不再重复造轮子。

说一下我自己的见解吧。

for client_socket in client_socket_list:

    response_data(client_socket,client_socket_list)

这段代码就是之前严重影响性能的代码,而epoll采用了事件通知的方式,直接返回正在活动的套接字的文件描述符等内容而忽略掉其他套接字。而且,应用和系统分别保存数据到不同的区域,epoll会构建一个共用的区域来保存套接字的文件描述符等内容。这大大缩短了系统返会被唤醒的套接字到应用的时间。

代码:

import socket
import re
import select

def response_data(client_socket,client_socket_list,fd,epl):

    try:
        #接收数据
        recv_data=client_socket.recv(1024)
    except :
        pass
    else :
        if recv_data:
            #解码并打印请求头
            print('\n\n\n'+'>'*20)
            print('接收数据类型:',type(recv_data),len(recv_data))
            print(recv_data.decode('utf8'))


            #检查请求头信息
            a_file=re.split(r' | \n',str(recv_data.decode('utf8')))[1]
            if str(a_file)=='/':
                a_file+='index.html' 
            file_name='./html'+a_file
            print('文件名:',file_name)

            #二进制可读打开文件
            try:
                with open(file=file_name,mode='rb') as f:
                    content=f.read()
                #构建响应头信息
                response_header='HTTP/1.1 200 OK '+'\n'+'Content-Length:%d' % len(content)+'\n\n'
            except :
                content=("-"*20+"404  FILE NOT FOUND"+'-'*20).encode()
                #构建响应头信息
                response_header='HTTP/1.1 404 FILE NOT FOUND'+'\n'+'\n'

            #返回给浏览器信息
            #通过utf8编码为二进制
            print('响应头类型:',type(response_header))


            #socket通过二进制传输数据
            client_socket.send(response_header.encode('utf8'))
            client_socket.send(content)


            #client_socket.close()
        else :
            #recv_data为空,说明客户端关闭了链接
            client_socket.close()
            #把死掉的客户端从共享内存中清理掉
            epl.unregister(fd)

            del client_socket_list[fd]


def main():
    #创建tcp_server_socket
    tcp_server_socket=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

    #绑定端口
    tcp_server_socket.bind(('',80))

    #创建监听
    tcp_server_socket.listen(128)

    #tcp_server_socket套接字设置成非堵塞
    tcp_server_socket.setblocking(False)


    #创建epoll对象
    #可以理解为创建了一个应用和系统共用的共享内存
    epl=select.epoll()

    #把tcp_serever_socket加载到共享内存中
    #第一个参数是socket的fd(文件描述符),第二个表示检测传入的fd是否有输入
    epl.register(tcp_server_socket.fileno(),select.EPOLLIN)



    sockets={}
    #client_socket_list=list()
    while True:
        #堵塞直到os检测到数据,然后通过事件通知告诉应用
        #返回被唤醒的套接字的fd和event列表
        fd_event_list=epl.poll()
        print(fd_event_list)

        for fd, event in fd_event_list:
            if fd == tcp_server_socket.fileno():
                #print('等待客户端链接')
                #等待连接
                client_socket,client_addr= tcp_server_socket.accept()
                epl.register(client_socket.fileno(),select.EPOLLIN)
                #epl.register(tcp_server_socket.fileno(),select.EPOLLIN)
            #判断是否有数据发过来
            elif event==select.EPOLLIN:
                
                #clent_socket套接字设置成非堵塞
                print('检测到客户端链接')
                client_socket.setblocking(False)
                #client_socket_list.append(client_socket)
                sockets[fd]=client_socket
                #print('同时连接的客户端数量:'+str(len(client_socket_list)))
                #for client_socket in client_socket_list:
                response_data(sockets[fd],sockets,fd,epl)

    tcp_server_socket.close()

if __name__ == '__main__':

    main()

然后我在一个美帝小鸡儿上跑了一下;

107.148.241.202/index.html

忽略机器的性能,本来是我搭梯子用的小鸡儿,当然是和腾讯云的机器没办法比的。

Tags: python
Archives QR Code Tip
QR Code for this page
Tipping QR Code