Linux.中国 - 开源社区

 找回密码
 骑士注册

QQ登录

微博登录


搭个 Web 服务器(三)

2016-10-9 14:42    评论: 3 收藏: 4    

如何并发处理多个请求

现在,我可以开始回答第二部分中的那个问题了:“你该如何让你的服务器在同一时间处理多个请求呢?”或者换一种说法:“如何编写一个并发服务器?”

在 UNIX 系统中编写一个并发服务器最简单的方法,就是使用系统调用 fork()

下面是全新出炉的并发服务器 webserver3c.py 的代码,它可以同时处理多个请求(和我们之前的例子 webserver3b.py 一样,每个子进程都会休眠 60 秒):

#######################################################
# 并发服务器 - webserver3c.py                          #
#                                                     #
# 使用 Python 2.7.9 或 3.4                             #
# 在 Ubuntu 14.04 及 Mac OS X 环境下测试通过            #
#                                                     #
# - 完成客户端请求处理之后,子进程会休眠 60 秒             #
# - 父子进程会关闭重复的描述符                           #
#                                                     #
#######################################################
import os
import socket
import time

SERVER_ADDRESS = (HOST, PORT) = '', 8888
REQUEST_QUEUE_SIZE = 5


def handle_request(client_connection):
    request = client_connection.recv(1024)
    print(
        'Child PID: {pid}. Parent PID {ppid}'.format(
            pid=os.getpid(),
            ppid=os.getppid(),
        )
    )
    print(request.decode())
    http_response = b"""\
HTTP/1.1 200 OK

Hello, World!
"""
    client_connection.sendall(http_response)
    time.sleep(60)


def serve_forever():
    listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    listen_socket.bind(SERVER_ADDRESS)
    listen_socket.listen(REQUEST_QUEUE_SIZE)
    print('Serving HTTP on port {port} ...'.format(port=PORT))
    print('Parent PID (PPID): {pid}\n'.format(pid=os.getpid()))

    while True:
        client_connection, client_address = listen_socket.accept()
        pid = os.fork()
        if pid == 0:  ### 子进程
            listen_socket.close()  ### 关闭子进程中复制的套接字对象
            handle_request(client_connection)
            client_connection.close()
            os._exit(0)  ### 子进程在这里退出
        else:  ### 父进程
            client_connection.close()  ### 关闭父进程中的客户端连接对象,并循环执行

if __name__ == '__main__':
    serve_forever()

在深入研究代码、讨论 fork 如何工作之前,先尝试运行它,自己看一看这个服务器是否真的可以同时处理多个客户端请求,而不是像轮询服务器 webserver3a.pywebserver3b.py 一样。在命令行中使用如下命令启动服务器:

$ python webserver3c.py

然后,像我们之前测试轮询服务器那样,运行两个 curl 命令,来看看这次的效果。现在你可以看到,即使子进程在处理客户端请求后会休眠 60 秒,但它并不会影响其它客户端连接,因为他们都是由完全独立的进程来处理的。你应该看到你的 curl 命令立即输出了“Hello, World!”然后挂起 60 秒。你可以按照你的想法运行尽可能多的 curl 命令(好吧,并不能运行特别特别多 ^_^),所有的命令都会立刻输出来自服务器的响应 “Hello, World!”,并不会出现任何可被察觉到的延迟行为。试试看吧。

如果你要理解 fork(),那最重要的一点是:你调用了它一次,但是它会返回两次 —— 一次在父进程中,另一次是在子进程中。当你创建了一个新进程,那么 fork() 在子进程中的返回值是 0。如果是在父进程中,那 fork() 函数会返回子进程的 PID。

我依然记得在第一次看到它并尝试使用 fork() 的时候,我是多么的入迷。它在我眼里就像是魔法一样。这就好像我在读一段顺序执行的代码,然后“砰!”地一声,代码变成了两份,然后出现了两个实体,同时并行地运行相同的代码。讲真,那个时候我觉得它真的跟魔法一样神奇。

当父进程创建出一个新的子进程时,子进程会复制从父进程中复制一份文件描述符:

你可能注意到,在上面的代码中,父进程关闭了客户端连接:

else:  ### 父进程
    client_connection.close()  # 关闭父进程的副本并循环

不过,既然父进程关闭了这个套接字,那为什么子进程仍然能够从来自客户端的套接字中读取数据呢?答案就在上面的图片中。内核会使用描述符引用计数器来决定是否要关闭一个套接字。当你的服务器创建一个子进程时,子进程会复制父进程的所有文件描述符,内核中该描述符的引用计数也会增加。如果只有一个父进程及一个子进程,那客户端套接字的文件描述符引用数应为 2;当父进程关闭客户端连接的套接字时,内核只会减少它的引用计数,将其变为 1,但这仍然不会使内核关闭该套接字。子进程也关闭了父进程中 listen_socket 的复制实体,因为子进程不需要关注新的客户端连接,而只需要处理已建立的客户端连接中的请求。

listen_socket.close()  ### 关闭子进程中的复制实体

我们将会在后文中讨论,如果你不关闭那些重复的描述符,会发生什么。

你可以从你的并发服务器源码中看到,父进程的主要职责为:接受一个新的客户端连接,复制出一个子进程来处理这个连接,然后继续循环来接受另外的客户端连接,仅此而已。服务器父进程并不会处理客户端连接——子进程才会做这件事。

打个岔:当我们说两个事件并发执行时,我们所要表达的意思是什么?

当我们说“两个事件并发执行”时,它通常意味着这两个事件同时发生。简单来讲,这个定义没问题,但你应该记住它的严格定义:

如果你不能在代码中判断两个事件的发生顺序,那这两个事件就是并发执行的。(引自《信号系统简明手册 (第二版): 并发控制深入浅出及常见错误》

好的,现在你又该回顾一下你刚刚学过的知识点了。

  • 在 Unix 中,编写一个并发服务器的最简单的方式——使用 fork() 系统调用;
  • 当一个进程分叉(fork)出另一个进程时,它会变成刚刚分叉出的进程的父进程;
  • 在进行 fork 调用后,父进程和子进程共享相同的文件描述符;
  • 系统内核通过描述符的引用计数来决定是否要关闭该描述符对应的文件或套接字;
  • 服务器父进程的主要职责:现在它做的只是从客户端接受一个新的连接,分叉出子进程来处理这个客户端连接,然后开始下一轮循环,去接收新的客户端连接。
查看其它分页:

发表评论


最新评论

我也要发表评论

linux [Chrome 54.0|Mac 10.11] 2016-11-11 22:43
1
来自广东的 Chrome 54.0|Mac 10.11 用户 发表于 2016-11-11 11:32 的评论:
楼主,想问一下mac是不是无法查看僵尸进程?我在mac下运行下面这段代码:
#include <unistd.h>

int main(void)
{
        int i;
        for (i = 0; i < 10; ++i) {
                if (!fork()) {
                        return 0;
                }
        }

        sleep(100);

        return 0;
}
编译成main,然后ps -ef | grep 'main'之后根本无法查看到<defunct>的进程啊。
看 STATE 字段里面 Z 的进程,这是僵尸、
回复
来自广东的 Chrome 54.0|Mac 10.11 用户 2016-11-11 11:32
楼主,想问一下mac是不是无法查看僵尸进程?我在mac下运行下面这段代码:
#include <unistd.h>

int main(void)
{
        int i;
        for (i = 0; i < 10; ++i) {
                if (!fork()) {
                        return 0;
                }
        }

        sleep(100);

        return 0;
}
编译成main,然后ps -ef | grep 'main'之后根本无法查看到<defunct>的进程啊。
回复
来自浙江杭州的 Chrome 54.0|Mac 10.11 用户 2016-10-26 11:46
感谢分享!
2 回复

热点评论

来自浙江杭州的 Chrome 54.0|Mac 10.11 用户 2016-10-26 11:46
感谢分享!
2
返回顶部

分享到微信朋友圈

打开微信,点击底部的“发现”,
使用“扫一扫”将网页分享至朋友圈。