Web服务器的实现
知识点
多进程实现并发服务器
1 | # 利用多进程实现服务器并发,只是将客户端处理时的语句修改为多进程的即可,其它部分无变化 |
多线程实现web服务器
1 | # 只是将客户端处理时的任务切换为多线程即可,其它部分无变化 |
协程实现web服务器
1 | import gevent |
单进程、单线程、非堵塞实现多任务
目的:
便于理解gevent实现协程中的多任务切换,此处是利用socket里面的setblocking(False)方法将原本的堵塞监听设置为非堵塞监听,之后利用捕获异常的方式处理每次循环过程中发生的异常,直到正常监听到数据信息;同时为了实现任务切换,而将每次监听到的客户端请求都传递给一个列表,然后一直循环遍历列表中的套接字对象,监听他们是否有发送消息
实现源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36import time
import re
import socket
def main():
client_list = list()
server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_tcp_socket.bind(("", 8080))
server_tcp_socket.listen(128)
server_tcp_socket.setblocking(False) # 设置套接字属性为非堵塞,
while True:
try:
time.sleep(1)
new_tcp_socket, client_addr = server_tcp_socket.accept()
except Exception as ret:
print("没有用户链接")
pass
else:
new_tcp_socket.setblocking(False) # 设置套接字属性为非堵塞,之后每次调用都是非堵塞了
client_list.append(new_tcp_socket)
for client in client_list:
try:
recv_data = client.recv(2048)
except:
print("该客户端未发送数据")
else:
if recv_data:
pass
else:
client_list.remove(client)
client.close()
server_tcp_socket.close()
if __name__ == '__main__':
main()长连接&短链接
区别:
长连接:客户端在与服务器交互时,会与服务器进行多次数据传输后,再主动关闭通信入口
短链接:客户端与服务器每进行一次交互,就关闭通信入口
实现源码:
本质是通过在response-header中添加内容:content-length
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61import time
import re
import socket
def client_socket(new_tcp_socket, request_info):
request = request_info.splitlines()
request_page = re.match(r'.* /(.*) .*', request[0]).group(1)
print(request_page)
try:
with open('./_book/' + request_page, 'rb') as f:
page_content = f.read()
except Exception as msg:
response_header = 'HTTP/1.1 403 bad requests\r\n'
response_header += '\r\n'
page_content = "File Not Found".encode('utf-8')
else:
response_header = 'HTTP/1.1 200 OK\r\n'
# 通过content-length可以告诉浏览器回复的数据有多少
# 当浏览器接受到content-length后会自动请求断开连接,这样就不用服务器主动关闭通信入口了
response_header += 'Content-Length: {0}\r\n'.format(len(page_content))
response_header += '\r\n'
reponse = response_header.encode('utf-8') + page_content
new_tcp_socket.send(reponse)
def main():
client_list = list()
server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置当服务器先close,即服务器4次挥手后能够立即释放
server_tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_tcp_socket.bind(("", 8080))
server_tcp_socket.listen(128)
server_tcp_socket.setblocking(False) # 设置套接字属性为非堵塞,
while True:
try:
time.sleep(1)
new_tcp_socket, client_addr = server_tcp_socket.accept()
except Exception as ret:
print("没有用户链接")
pass
else:
new_tcp_socket.setblocking(False)
client_list.append(new_tcp_socket)
for client in client_list:
try:
recv_data = client.recv(2048).decode('utf-8')
except Exception as ret:
print("该客户端未发送数据")
else:
if recv_data:
client_socket(client, recv_data)
else:
client_list.remove(client)
client.close()
server_tcp_socket.close()
if __name__ == '__main__':
main()
epoll实现服务器
作用:
实现单进程、单线程、高并发的Web服务器。但是该方法仅仅适用于linux,不支持windows。
实现原理:
- 操作系统运行的程序有自己的内存空间,它的变量都存放在自己的内存空间中;操作系统在运行的时候也有自己的内存空间;两个内存空间彼此不互通的,当操作系统切换到某个程序时,会将程序内容空间中的内容复制一份到操作系统运行的内存中,然后执行;这样就降低了程序的运行效率,为了解决这一个问题,就开发出epoll。
- epoll借助内存映射技术,在内存中开辟一块独立的内存,这个内存空间是应用程序和操作系统所共享的(即不需要复制,可直接访问)。应用程序的变量信息直接存放到epoll内存中,操作系统需要读取时也直接读取epoll内存中存放的变量信息,这样就减少了复制的过程。
- 由于epoll中变量信息不是时时刻刻都变化的,如果通过循环遍历(轮询)的方式,则也会降低程序运行的效率,所以epoll利用事件通知(即哪一个变量发生了变化,则系统就会直接通知应用程序该变量)的方式替换轮询,减少了无效遍历的过程。
实例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70import time
import re
import socket
# epoll是select中的方法
import select
def client_socket(new_tcp_socket, request_info):
request = request_info.splitlines()
request_page = re.match(r'.* /(.*) .*', request[0]).group(1)
print(request_page)
try:
with open('./_book/' + request_page, 'rb') as f:
page_content = f.read()
except Exception as msg:
page_content = "File Not Found"
response_header = 'HTTP/1.1 403 bad requests\r\n'
response_header += 'Content-Length: {0}\r\n'.format(len(page_content))
response_header += '\r\n'
page_content = page_content.encode('utf-8')
else:
response_header = 'HTTP/1.1 200 OK\r\n'
response_header += 'Content-Length: {0}\r\n'.format(len(page_content))
response_header += '\r\n'
reponse = response_header.encode('utf-8') + page_content
new_tcp_socket.send(reponse)
def main():
client_list = list()
server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_tcp_socket.bind(("", 8080))
server_tcp_socket.listen(128)
server_tcp_socket.setblocking(False)
# 创建一个epoll对象
epl = select.epoll()
# 将监听套接字的文件描述符(监听套接字在运行的操作系统中的文件id)注册到epoll中
# 套接字对象.fileno():可以获取套接字在运行的操作系统中的文件描述符
# select.EPOLLIN设置监听该套接字的接受请求
epl.register(server_tcp_socket.fileno(), select.EPOLLIN)
# epoll监听的结果中没有记录下套接字的对象信息,不利于后面程序的回复,所以利用字典记录下每次生成的套接字信息
fd_event_dict = dict()
while True:
# 设置堵塞,直到操作系统检测到有数据到来,然后通过事件通知的方式告诉程序,此时才会解堵塞
# [(fd, event),(套接字对应的文件描述符, 文件描述符描述的事件)]
fd_event_list = epl.poll() # 返回一个列表,列表中是套接字对象的文件描述符和事件类型的元组
for fd, event in fd_event_list:
if fd == server_tcp_socket.fileno():
# 当监听到的文件描述符是监听套接字的,则说明是一个新客户端链接,此时记录下新客户端
new_socket, client_addr = server_tcp_socket.accept()
epl.register(new_socket.fileno(), select.EPOLLIN)
fd_event_dict[new_socket.fileno()] = new_socket
elif event == select.EPOLLIN:
# 利用此前记录的套接字字典信息获取文件描述符对应的套接字对象
recv_data = fd_event_dict[fd].recv(2048).decode('utf-8')
if recv_data:
client_socket(fd_event_dict[fd], recv_data)
else:
fd_event_dict[fd].close()
# 从epoll内存中去除关闭的内存信息
epl.unregister(fd)
# 从字典中删除掉已关闭的套接字数据
fd_event_dict.pop(fd)
server_tcp_socket.close()
if __name__ == '__main__':
main()作业
- (问答)如何利用多线程实现Web服务器,请描述具体代码修改之处
- (问答)如何利用多进程实现Web服务器,利用多进程实现时的注意点是什么,请描述具体代码修改之处
- (问答)如何利用协程实现Web服务器,请描述具体代码修改之处
- (问答)socket在监听时会引起堵塞,如何设置监听套接字不堵塞,请描述实现代码
- (问答)当服务端非正常关闭套接字服务时,再次重新启动套接字服务会提示端口不可用,如何解决该问题
- (问答)epoll诞生是为了解决什么问题?epoll的实现原理是什么?
- (问答)如何利用epoll实现高并发,请描述代码具体修改之处