用python写服务器的几点思考与总结(epoll服务器)
用python写服务器相对而言比较简单,个人学习中,总结了几下几点需要注意的地方。仅供参考,欢迎大家给予指导。
-
1
简易服务器:每次浏览器请求数据的时间,服务器都会建立一个套字节,当执行一次后,套节字关闭,然而,一个浏览器的页面请求都是多次以上,频繁的生成和关闭套字节,会造成资源的浪费,经不起压力测试。(因此,该方法不可取)。 -
2
进程、线程、和协程(gevent)建立的服务器。虽然把套节字分配到进程、线程和协程中,但进程占用的资料较大,当多个客户端进入服务器时,也经不起压力测试。单进程、多线程模式,或单进程、单线程、多协程模式,也不是最好的方法。因为没有解决套字节监听阻塞和套节字获取数据accept()阻塞的问题,因此也不是最好的方法。 -
3
在对套节字采取解阻的方法后,对监听到的套节字存放到列表当中,而且建立长连接机制,运用并发的方式建立服务器。由于需要对列表中的套节字进行遍历,当访问服务器的客户端较多时,存放列表的套节字也多,遍历需要的时间也就更多,也提高不了服务器运行的效率,因此,此法建立服务器也不可取。 -
4
目前普遍的服务器建立方法采取epoll服务器。其基本思想是:把遍历方法变成事件触发的方法。在服务器和系统间建立一个边缘水平轮询对象(可以把他看成一个空间,或一个列表),存放套节字引用标识 和 触发事件,当对这个对个对象进行监听时(poll())方法,如果有触发事件,把产生触发事件的套节字标识 和 事件类型 生成一个二位元素的元组存放到列表当中。再遍历这个列表,从定义的字典中找出套节字(当监听对象产生触发事件的时候,把生成的对应客户端套节字分别存入到字典当中和边缘水平轮询对象当中,可以通过引用标识,快速的从字典当中找到对应的子套节字),由于遍历的次数较少,因此,有效提高了服务器的性能。此方法是目前大部分服务器采取的方法。
def main():
server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 在首先关闭套接字的情况下,不设置时间等待。设置这个,在服务器重启不会出现端口占用的情况
server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server_socket.bind(('',7789))
server_socket.listen(128)
# 设置套接字为非阻塞状态
server_socket.setblocking(False)
# epoll服务器,建立一个边缘水平轮询对象,可以用来存放套接字的识及事件触发的类型
socket_epoll = select.epoll()
# 把套节字放放对象中,参数分别为套接字标识,和触发事件类型
socket_epoll.register(server_socket.fileno(), select.EPOLLIN)
# 建立一个字典,主要用途是存放由监听套接字生成的套接字,键值对分别为(套字字标识:套节字)
# 从epoll()对象中调用poll()方法,会形成一个列表,列表的元素是二位的元组,其中第一们存放的
# 是放入epoll()对象的标识,这样,可以通过标识符,从字典中取出对应的套节字
new_socket_dict = dict()
while True:
# 监听是否在套节字触发事件,如果有,会返回一个存放二维元组的列表,对列表进行遍历
evnet_poll = socket_epoll.poll()
for socket_fileno,event in evnet_poll:
# 监听套字节的标识只有一个不重复,所以偏历出来的标识符如果和监听套节字标识一样
# 说明监听套字节有一个客户端访问,调用accept()方法,并对新产生的套节字进行一
# 系统的设设置
if socket_fileno == server_socket.fileno():
new_socket,new_client_ip = server_socket.accept()
new_socket.setblocking(False)
# 存放到边缘水平轮询对像中
socket_epoll.register(new_socket.fileno(), select.EPOLLIN)
# 存放到字典中
new_socket_dict[new_socket.fileno()] = new_socket
elif event == select.EPOLLIN:
# 从字典当中找出套字节
client_socket = new_socket_dict[socket_fileno]
request = client_socket.recv(1204)
if request:
client_cutdate(request,client_socket)
else:
# 如果客户端关闭,服务器对应客户端的套节字也关闭,并从边缘水平轮询对象和字典中删除
new_socket_dict[socket_fileno].close()
socket_epoll.unregister(socket_fileno)
new_socket_dict.pop(socket_fileno)
def client_cutdate(request,new_socket):
# 得到浏览器给服务器的第一行数据,通过正则表达式,取出文件名
request_date_list = request.splitlines()
text_header = request_date_list[0].decode('utf-8')
request_re = re.match(r'[^/]+(/[^ ]*).*', text_header)
file_name = ''
if request:
file_name = request_re.group(1)
if file_name == '/':
file_name = '/index.html'
print(file_name) #测试用
# 读取文件,采用异常处理机制,如果出错返回出错内容,正确读取信息,传给浏览器
try:
f = open('./html' + file_name, 'rb')
except Exception as ret:
response = 'HTTP/1.1 404 NOT FOUNd\r\n'
response += '\r\n'
response += '-----------file not found--------------'
new_socket.send(response.encode('utf-8'))
else:
html_date = f.read()
print(f'this is OK')
f.close()
responner_body = html_date
print(len(responner_body))
responser_header = 'HTTP/1.1 200 OK\r\n'
# 这个地方是设置长连接机制,只有body的字节长度大于或等于给定数值的长度时候,浏览器才不会打圈圈
responser_header += 'Content-Length:%d\r\n' % len(responner_body) #这个地方是建立长连接的关键
responser_header += '\r\n'
response_all = responser_header.encode('utf-8') + responner_body
new_socket.send(response_all)
感受:多学多思考,多找资源
整体代码如下:
import time
import socket
import re
import select
import multiprocessing
def client_cutdate(request,new_socket):
# 得到浏览器给服务器的第一行数据,通过正则表达式,取出文件名
request_date_list = request.splitlines()
text_header = request_date_list[0].decode('utf-8')
request_re = re.match(r'[^/]+(/[^ ]*).*', text_header)
file_name = ''
if request:
file_name = request_re.group(1)
if file_name == '/':
file_name = '/index.html'
print(file_name) #测试用
# 读取文件,采用异常处理机制,如果出错返回出错内容,正确读取信息,传给浏览器
try:
f = open('./html' + file_name, 'rb')
except Exception as ret:
response = 'HTTP/1.1 404 NOT FOUNd\r\n'
response += '\r\n'
response += '-----------file not found--------------'
new_socket.send(response.encode('utf-8'))
else:
html_date = f.read()
print(f'this is OK')
f.close()
responner_body = html_date
print(len(responner_body))
responser_header = 'HTTP/1.1 200 OK\r\n'
# 这个地方是设置长连接机制,只有body的字节长度大于或等于给定数值的长度时候,浏览器才不会打圈圈
responser_header += 'Content-Length:%d\r\n' % len(responner_body) #这个地方是建立长连接的关键
responser_header += '\r\n'
response_all = responser_header.encode('utf-8') + responner_body
new_socket.send(response_all)
def main():
server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
# 在首先关闭套接字的情况下,不设置时间等待。设置这个,在服务器重启不会出现端口占用的情况
server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
server_socket.bind(('',7789))
server_socket.listen(128)
# 设置套接字为非阻塞状态
server_socket.setblocking(False)
# epoll服务器,建立一个边缘水平轮询对象,可以用来存放套接字的识及事件触发的类型
socket_epoll = select.epoll()
# 把套节字放放对象中,参数分别为套接字标识,和触发事件类型
socket_epoll.register(server_socket.fileno(), select.EPOLLIN)
# 建立一个字典,主要用途是存放由监听套接字生成的套接字,键值对分别为(套字字标识:套节字)
# 从epoll()对象中调用poll()方法,会形成一个列表,列表的元素是二位的元组,其中第一们存放的
# 是放入epoll()对象的标识,这样,可以通过标识符,从字典中取出对应的套节字
new_socket_dict = dict()
while True:
# 监听是否在套节字触发事件,如果有,会返回一个存放二维元组的列表,对列表进行遍历
evnet_poll = socket_epoll.poll()
for socket_fileno,event in evnet_poll:
# 监听套字节的标识只有一个不重复,所以偏历出来的标识符如果和监听套节字标识一样
# 说明监听套字节有一个客户端访问,调用accept()方法,并对新产生的套节字进行一
# 系统的设设置
if socket_fileno == server_socket.fileno():
new_socket,new_client_ip = server_socket.accept()
new_socket.setblocking(False)
# 存放到边缘水平轮询对像中
socket_epoll.register(new_socket.fileno(), select.EPOLLIN)
# 存放到字典中
new_socket_dict[new_socket.fileno()] = new_socket
elif event == select.EPOLLIN:
# 从字典当中找出套字节
client_socket = new_socket_dict[socket_fileno]
request = client_socket.recv(1204)
if request:
client_cutdate(request,client_socket)
else:
# 如果客户端关闭,服务器对应客户端的套节字也关闭,并从边缘水平轮询对象和字典中删除
new_socket_dict[socket_fileno].close()
socket_epoll.unregister(socket_fileno)
new_socket_dict.pop(socket_fileno)
server_socket.close()
if __name__ == '__main__':
main()