Skip to content

Commit

Permalink
修改文件: 从网络编程基础到epoll.md
Browse files Browse the repository at this point in the history
  • Loading branch information
jiangbaiyan committed Aug 26, 2019
1 parent 7fe6326 commit a36d060
Showing 1 changed file with 16 additions and 17 deletions.
33 changes: 16 additions & 17 deletions 从网络编程基础到epoll.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ while (1) {
> - socket_listen():我们观察到只有一个函数参数就是之前创建的套接字。有些同学之前可能认为这一步函数调用完全没有必要。但是它告诉内核,我是一个服务器,将套接字转换为一个被动实体,其实是有很大的作用的。
> - socket_accept():接收客户端发来的请求。因为服务器启动之后,是不知道客户端什么时候有连接到来的。所以,需要在一个while循环中不断调用这个函数,如果有连接请求到来,那么就会返回一个新的套接字,我们可以通过这个新的套接字进行与客户端的数据通信,如果没有,就只能不断地进行循环,直到有请求到来为止。
注意,在这里我将套接字分为两类,一个是监听套接字,一个是连接套接字。注意这里对两种套接字的区分,在下面的讨论中会用到:
注意,在这里我将套接字分为两类,一个是**监听套接字**,一个是**连接套接字**。注意这里对两种套接字的区分,在下面的讨论中会用到:

> - 监听套接字:服务器对某个端口进行监听,这个套接字用来表示这个端口($listenSocket)
> - 连接套接字:服务器与客户端已经建立连接,所有的读写操作都要在连接套接字上进行($connSocket)
Expand Down Expand Up @@ -91,7 +91,7 @@ for ($i = 0; $i < 10; $i++) { //初始创建10个子进程
}
```
我们主要关注这个for循环,一共循环了10次代表初始的子进程数量我们设置为10。接着我们调用了pcntl_fork()函数创建子进程。由于一个客户端的connect就对应一个服务端的accept。所以在每个fork之后的10个子进程中,我们均进行accept的系统调用,等待客户端的连接。这样,就可以通过10个服务器进程,同时接受10个客户端的连接、同时为10个客户端提供读写数据服务。
注意这样一个细节,由于所有子进程都是预先创建好的,那么请求到来的时候就不用创建子进程,也提高了每个连接请求的处理效率。同时也可以借助进程池的概念,这些子进程在处理完连接请求之后并不立即回收,可以继续服务下一个客户端连接请求,就不用重复的进行fork()的系统调用,也能够提高服务器的性能。这些小技巧在PHP-FPM的实现中都有所体现。其实这种进程创建方式是其三种运行模式中的一种,被称作static:
注意这样一个细节,由于所有子进程都是预先创建好的,那么请求到来的时候就不用创建子进程,也提高了每个连接请求的处理效率。同时也可以借助进程池的概念,这些子进程在处理完连接请求之后并不立即回收,可以继续服务下一个客户端连接请求,就不用重复的进行fork()的系统调用,也能够提高服务器的性能。这些小技巧在PHP-FPM的实现中都有所体现。其实这种进程创建方式是其三种运行模式中的一种,被称作static(静态进程数量)模式

> - ondemand:按需启动。PHP-FPM启动的时候不会启动任何一个子进程(worker进程),只有客户端连接请求到达时才启动
> - dynamic:在PHP-FPM启动时,会初始启动一些子进程,在运行过程中视情况动态调整worker数量
Expand Down Expand Up @@ -120,7 +120,7 @@ IO多路复用的核心就是添加了一个**套接字集合管理员**,它
function socket_select (array &$read, array &$write, array &$except, $tv_sec, $tv_usec = 0) {}
```
举个例子,如果某个客户单通过调用connect()连接到了服务器的**监听套接字**($listenSocket)上,这个监听套接字的状态就会从不可读变为可读。由于监听套接字只有一个,select()对于监听套接字上的处理仍然是阻塞的。一个监听套接字,存在于整个服务器的生命周期中,所以在select()的实现中并不能体现出其对监听套接字的优化管理。
在当一个服务器使用accept()接受多个客户端连接,并生成了多个**连接套接字**之后,select()的管理才能就会体现出来。这个时候,select()的监听列表中有**一个监听套接字**、和与**一堆**客户端建立连接后新创建的**连接套接字**。在这个时候,可能这一堆已建立连接的客户端,都会通过这个连接套接字发送数据,等待服务端接收。假设同时有5个连接套接字都有数据发送,那么这5个连接套接字的状态都会变成可读状态。由于已经有套接字变成了可读状态,select()函数解除阻塞,立即返回。具体哪一个套接字或哪些套接字变为可读或可写我们是不知道的,所以我们需要遍历所有select()返回的套接字来判断哪些套接字可以进行处理。遍历完毕之后,就知道有5个连接套接字可以进行读写处理,这样就实现了对多个套接字的管理。我们注意到,上面php中的select实现使用了引用传递,所以会对原始的监听数组进行修改,修改之后的数组存储的就是所有状态变化之后的套接字集合。使用PHP实现select()的代码如下:
在当一个服务器使用accept()接受多个客户端连接,并生成了多个**连接套接字**之后,select()的管理才能就会体现出来。这个时候,select()的监听列表中有**一个监听套接字**、和与**一堆**客户端建立连接后新创建的**连接套接字**。在这个时候,可能这一堆已建立连接的客户端,都会通过这个连接套接字发送数据,等待服务端接收。假设同时有5个连接套接字都有数据发送,那么这5个连接套接字的状态都会变成可读状态。由于已经有套接字变成了可读状态,select()函数解除阻塞,立即返回。具体哪一个套接字或哪些套接字变为可读或可写我们是不知道的,所以我们需要遍历所有select()返回的套接字,来判断哪些套接字已经就绪,可以进行读写处理。遍历完毕之后,就知道有5个连接套接字可以进行读写处理,这样就实现了同时对多个套接字的管理。使用PHP实现select()的代码如下:
```php
<?php
if (($listenSocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP))=== false) {
Expand Down Expand Up @@ -158,16 +158,15 @@ while (1) {
$data = socket_read($read, 1024); //从客户端读取数据, 此时一定会读到数据,不会产生阻塞
if ($data === '') { //已经无法从连接套接字中读到数据,需要移除对该socket的监听
foreach ($read_socks as $key => $val) {
if ($val == $read) unset($read_socks[$key]);
if ($val == $read) unset($read_socks[$key]); //移除失效的套接字
}
foreach ($write_socks as $key => $val) {
if ($val == $read) unset($write_socks[$key]);
}
socket_close($read);
} else { //能够从连接套接字读到数据。此时$read是连接套接字
if (in_array($read, $tmp_writes)) {
//如果该客户端可写 把数据回写给客户端
socket_write($read, $data);
socket_write($read, $data);//如果该客户端可写 把数据写回到客户端
}
}
}
Expand All @@ -179,7 +178,7 @@ socket_close($listenSocket);
总结一下,select()的过人之处有以下几点:

> - 实现了对多个套接字的同时、集中管理
> - 能够一次性返回多个就绪的套接字,对这些就绪的套接字进行操作不会阻塞
> - 通过遍历所有的套接字集合,能够获取所有已就绪的套接字,对这些就绪的套接字进行操作不会阻塞
但是,select()仍存在几个问题:

Expand All @@ -196,7 +195,7 @@ poll的fds参数集合了select的read、write和exception套接字数组,合
我们可以总结一下,select和poll这两种实现,都需要在返回后,通过遍历所有的套接字描述符来获取已经就绪的套接字描述符。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
为了解决不知道返回之后究竟是哪个或哪些描述符已经就绪的问题,同时避免遍历所有的套接字描述符,聪明的开发者们又发明出了epoll机制,完美解决了select和poll所存在的问题。
#### epoll
epoll将一个阻塞的select、poll系统调用拆分成了三个步骤。一次select或poll可以看作是由一次 epoll_create、若干次 epoll_ctl、若干次 epoll_wait构成:
epoll是最先进的套接字们的管理员,解决了上述select和poll中所存在的问题。它将一个阻塞的select、poll系统调用拆分成了三个步骤。一次select或poll可以看作是由一次 epoll_create、若干次 epoll_ctl、若干次 epoll_wait构成:
```c
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
Expand All @@ -212,12 +211,12 @@ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout
- fd:上面op操作的套接字描述符对象(之前在PHP中是$listenSocket与$connSocket两种套接字描述符)例如将某个套接字**添加**到监听列表中
- event:告诉内核需要监听该套接字描述符的什么事件(如读写、连接等)、
最后我们调用epoll_wait()等待连接或读写等事件,在某个套接字描述符上准备就绪。当有事件准备就绪之后,会存到第二个参数epoll_event结构体中。通过访问这个结构体就可以得到所有已经准备好事件的套接字描述符。这里就不用再像之前select和poll那样,遍历所有的套接字描述符之后才能知道究竟是哪个描述符已经准备就绪了,这样减少了一次O(n)的遍历,大大提高了效率。
在最后返回的所有套接字描述符中,同样存在之前说过的两种描述符:**监听套接字描述符****连接套接字描述符**。那么我们需要遍历所有准备就绪的描述符,然后去判断究竟是监听还是连接套接字描述符,然后视情况做做出accept(监听套接字)或者是read(连接套接字)的处理。一个使用C语言编写的epoll服务器的伪代码如下:
在最后返回的所有套接字描述符中,同样存在之前说过的两种描述符:**监听套接字描述符****连接套接字描述符**。那么我们需要遍历所有准备就绪的描述符,然后去判断究竟是监听还是连接套接字描述符,然后视情况做做出accept(监听套接字)或者是read(连接套接字)的处理。一个使用C语言编写的epoll服务器的伪代码如下(重点关注代码注释)
```c
int main(int argc, char *argv[]) {
listenSocket = socket(AF_INET, SOCK_STREAM, 0); //创建一个监听套接字
bind(listenSocket) //绑定地址与端口
listen(listenSocket) //转换为被动套接字
listenSocket = socket(AF_INET, SOCK_STREAM, 0); //同上,创建一个监听套接字
bind(listenSocket) //同上,绑定地址与端口
listen(listenSocket) //同上,转换为被动套接字
epfd = epoll_create(EPOLL_SIZE); //创建一个epoll实例
ep_events = (epoll_event*)malloc(sizeof(epoll_event) * EPOLL_SIZE); //创建一个epoll_event结构存储套接字集合
event.events = EPOLLIN;
Expand All @@ -232,12 +231,12 @@ int main(int argc, char *argv[]) {
event.data.fd = connSocket;
epoll_ctl(epfd, EPOLL_CTL_ADD, connSocket, &event); //添加对新建立的连接套接字描述符的监听,以监听后续在连接描述符上的读写事件
} else { //如果是连接套接字描述符事件就绪,则可以进行读写
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if (str_len == 0) { //无法从连接描述符中读取到数据
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); //从连接套接字描述符中读取数据, 此时一定会读到数据,不会产生阻塞
if (str_len == 0) { //已经无法从连接套接字中读到数据,需要移除对该socket的监听
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); //删除对这个描述符的监听
close(ep_events[i].data.fd);
} else {
write(ep_events[i].data.fd, buf, str_len);
} else { =
write(ep_events[i].data.fd, buf, str_len); //如果该客户端可写 把数据写回到客户端
}
}
}
Expand All @@ -247,6 +246,6 @@ int main(int argc, char *argv[]) {
return 0;
}
```
我们看代码的结构,除了由一个函数拆分成三个函数,其余的执行流程基本同select、poll相似。只是epoll会只返回已经就绪的套接字描述符集合,而不是所有描述符的集合,IO的效率不会随着监视fd的数量的增长而下降,大大提升了效率。此外,它监听的套接字描述符是没有限制的,这样,之前select、poll的遗留问题就全部解决啦。关于epoll的两种工作工作模式有LT(水平触发)和ET(边缘触发)并不是我们此次的重点,并且我们在讲述的过程中省略了描述符在内核空间与与用户空间的拷贝过程,以简化我的表述。有兴趣的同学可以搜索其他博客进行扩展学习。
我们看这个通过epoll实现一个IO多路复用服务器的代码结构,除了由一个函数拆分成三个函数,其余的执行流程基本同select、poll相似。只是epoll会只返回已经就绪的套接字描述符集合,而不是所有描述符的集合,IO的效率不会随着监视fd的数量的增长而下降,大大提升了效率。同时它细化并规范了对每个套接字描述符的管理(如增删改的过程)。此外,它监听的套接字描述符是没有限制的,这样,之前select、poll的遗留问题就全部解决啦。关于epoll的两种工作工作模式有LT(水平触发)和ET(边缘触发)并不是我们此次的重点,并且我们在讲述的过程中省略了描述符在内核空间与与用户空间的拷贝过程,以简化我的表述。有兴趣的同学可以搜索其他博客进行扩展学习。
## 总结
我们从最基本网络编程说起,开始从一个最简单的同步阻塞服务器到一个IO多路复用服务器,我们从头到尾了解到了一个服务器性能提升的思考与实现过程。而提升服务器的并发性能的方式远不止这几种,还包括协程等新的概念需要我们去对比与分析,大家加油。

0 comments on commit a36d060

Please sign in to comment.