Web服务器的实现

知识点

多进程实现并发服务器

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
# 利用多进程实现服务器并发,只是将客户端处理时的语句修改为多进程的即可,其它部分无变化
import multiprocessing
import re
import socket


def client_socket(new_tcp_socket):
request = new_tcp_socket.recv(2048).decode("utf-8").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'
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)
new_tcp_socket.close() # 由于加入Content-length,故客户端接受到内容后会自动断开链接


def main():
server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_tcp_socket.bind(("", 8080))
server_tcp_socket.listen(128)
while True:
new_tcp_socket, client_addr = server_tcp_socket.accept()
p1 = multiprocessing.Process(target=client_socket, args=(new_tcp_socket,))
p1.start()
# 在多进程中,子进程会复制主进程中所有的变量一份,此时复制的内容可以理解为复制对应数据的硬连接
# 当系统中只要有一个硬链接存在,则不会销毁变量名所指向的数据
# 故不仅仅需要在子进程中关闭硬连接对象,也需要在主进程中关闭主进程的硬链接对象
new_tcp_socket.close()
server_tcp_socket.close()


# python中一切皆文件
if __name__ == '__main__':
main()

多线程实现web服务器

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
# 只是将客户端处理时的任务切换为多线程即可,其它部分无变化
import threading
import re
import socket


def client_socket(new_tcp_socket):
request = new_tcp_socket.recv(2048).decode("utf-8").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'
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)
new_tcp_socket.close() # 由于加入Content-length,故客户端接受到内容后会自动断开链接


def main():
server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_tcp_socket.bind(("", 8080))
server_tcp_socket.listen(128)
while True:
new_tcp_socket, client_addr = server_tcp_socket.accept()
p1 =threading.Thread(target=client_socket, args=(new_tcp_socket,))
p1.start()
# 在多线程中,子线程和主线程共享全局变量
# 子线程中关闭了通信入口则相当于主线程中关闭了通信入口
server_tcp_socket.close()


if __name__ == '__main__':
main()

协程实现web服务器

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
import gevent
from gevent import monkey
import re
import socket


def client_socket(new_tcp_socket):
request = new_tcp_socket.recv(2048).decode("utf-8").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'
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)
new_tcp_socket.close() # 由于加入Content-length,故客户端接受到内容后会自动断开链接


def main():
# 设定延时切换
monkey.patch_all()
server_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_tcp_socket.bind(("", 8080))
server_tcp_socket.listen(128)
while True:
new_tcp_socket, client_addr = server_tcp_socket.accept()
gevent.joinall([
gevent.spawn(client_socket, new_tcp_socket)
])
server_tcp_socket.close()


if __name__ == '__main__':
main()

单进程、单线程、非堵塞实现多任务

  • 目的:

    便于理解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
    36
    import 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()

    长连接&短链接

    image-20200711113142345

    1. 区别:

      长连接:客户端在与服务器交互时,会与服务器进行多次数据传输后,再主动关闭通信入口

      短链接:客户端与服务器每进行一次交互,就关闭通信入口

    2. 实现源码:

      本质是通过在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
      61
      import 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实现服务器

  1. 作用:

    实现单进程、单线程、高并发的Web服务器。但是该方法仅仅适用于linux,不支持windows。

  2. 实现原理:

    image-20200711172840739

    • 操作系统运行的程序有自己的内存空间,它的变量都存放在自己的内存空间中;操作系统在运行的时候也有自己的内存空间;两个内存空间彼此不互通的,当操作系统切换到某个程序时,会将程序内容空间中的内容复制一份到操作系统运行的内存中,然后执行;这样就降低了程序的运行效率,为了解决这一个问题,就开发出epoll。
    • epoll借助内存映射技术,在内存中开辟一块独立的内存,这个内存空间是应用程序和操作系统所共享的(即不需要复制,可直接访问)。应用程序的变量信息直接存放到epoll内存中,操作系统需要读取时也直接读取epoll内存中存放的变量信息,这样就减少了复制的过程。
    • 由于epoll中变量信息不是时时刻刻都变化的,如果通过循环遍历(轮询)的方式,则也会降低程序运行的效率,所以epoll利用事件通知(即哪一个变量发生了变化,则系统就会直接通知应用程序该变量)的方式替换轮询,减少了无效遍历的过程。
  3. 实例代码

    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
    70
    import 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()

    作业

    1. (问答)如何利用多线程实现Web服务器,请描述具体代码修改之处
    2. (问答)如何利用多进程实现Web服务器,利用多进程实现时的注意点是什么,请描述具体代码修改之处
    3. (问答)如何利用协程实现Web服务器,请描述具体代码修改之处
    4. (问答)socket在监听时会引起堵塞,如何设置监听套接字不堵塞,请描述实现代码
    5. (问答)当服务端非正常关闭套接字服务时,再次重新启动套接字服务会提示端口不可用,如何解决该问题
    6. (问答)epoll诞生是为了解决什么问题?epoll的实现原理是什么?
    7. (问答)如何利用epoll实现高并发,请描述代码具体修改之处