TCP协议的创建:
创建流程:1.客户端主动调用connect发送SYN分节;2.服务器端必须回复一个ACK分节来确认客户端的SYN分节,并发送一个SYN分节给客户端;3.客户端对服务器端发送SYN分节进行ACK分节的确认
TCP协议的拆除(TCP为全双工的传输协议,所以需要4次分节的交换):
拆除流程:1.首先申请拆除的一端调用close发送一个FIN分节;2.另一端接收到FIN分节时,发送一个ACK分节进行确认;3.另一端要申请拆除连接时,也要发送一个FIN分节;4.接收端发送一个ACK分节进行确认
TCP的状态转换图
我们可以简单的把 Socket 理解为一个可以连通网络上不同计算机应用程序之间的管道,把一堆数据从管道的 A 端扔进去,则会从管道的 B 端(同时还可以从C、D、E、F……端冒出来)(Socket 的官方解释: 在网络编程中最常用的方案便是Client/Server(客户机/服务器)模型。在这种方案中客户应用程序向服务器程序请求服务。一个服务程序通常在一个众所周知的地址监听对服务的请求,也就是说,服务进程一 直处于休眠状态,直到一个客户向这个服务的地址提出了连接请求。在这个时刻,服务程序被"惊醒"并且为客户提供服务-对客户的请求作出适当的反应)
Socket 通信依次会进行 Socket 创建、Socket 监听、Socket 收发、Socket 关闭几个阶段。
常用函数1(创建的是socket资源):[socket_create() | socket_bind() | socket_listen() | socket_accept() | socket_write() | socket_read() | socket_close()]
常用函数2(创建的是stream资源):[stream_socket_server() | fwrite() | fread() | fclose()]
示例 server.php(并发量只有1);
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '127.0.0.1', 8080);
socket_listen($socket);
for(;;){
$conn = socket_accept($socket);
$output_buffer = 'HTTP/1.0 200 OK\r\nServer: this is my server\r\nContent-Type:text/html;charset:utf-8\r\nthis is my frist socket program';
socket_write($conn, $output_buffer);
socket_close($conn);
}
或
$sock = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
for(;;){
$conn = stream_socket_accept($sock);
$output_buffer = 'HTTP/1.0 200 OK\r\nServer: this is my server\r\nContent-Type:text/html;charset:utf-8\r\nthis is my frist socket program';
fwrite($conn, $output_buffer);
fclose($conn);
}
控制台运行
sudo php-fpm7.2 start && php sertver.php
运行成功之后,打开浏览器输入 ‘127.0.0.1:8080’
多进程简介:就是多个进程同时工作,这样的进程一般属于亲属关系,通常由一个父进程fork得到的. 注意这里所说的同时工作,是宏观上的,同一时刻在单个单核CPU上
示例 multiProcess.php
$pid = pcntl_fork();
if($pid){
echo "this is parent process\n";
pcntl_waitpid($pid, $status);
} elseif($pid == 0){
echo "this is child process\n";
} else {
die("fork faild\n");
}
运行 php multiProcess.php
函数介绍:
int pcntl_fork(void);
执行该函数,会复制当前进程产生另一个进程,称之为当前进程的子进程,该函在父进程和子进程的返回值不相同,在父进程中返回的是fork出的子进程的进程ID,在子进程中返回值为0。要注意的是在复制进程时,会复制该进程的数据(堆数据、栈数据和静态数据),包括在父进程打开的文件描述符,在子进程中也是打开的,这意味着当你在父进程使用了大量内存时,fork出来的子进程必须拥有等量的内存资源,否则可能会导致fork失败.
int pcntl_waitpid(int KaTeX parse error: Expected 'EOF', got '&' at position 10: pid, int &̲status [,int $options=0]);
pid: 进程ID;status: 子进程的退出状态;option: 取决于操作系统是否提供wait3函数,如果提供该函数,则该选项参数才生效.
为什么父进程要调用 pcntl_waitpid() 函数呢?这是因为子进程在结束时,不管是主动结束(调用exit或main函数返回)还是被动结束(被发出的信号打断),都会保存退出状态供父进程调用,所以还会在操作系统的进程表中占用一项。如果不调用pcntl_waitpid清除子进程的退出状态,回收该表项,那么子进程虽然已经死亡,但依然占用着宝贵的资源,就变成了“僵尸进程”)
leader-follower模型
一个非常简单的leader-follower模型,创建一个进程池,随机选出一个进程作为leader进程,该进程监听是否有新连接,如果有则提升另一个follower为leader进程来继续监听,而原leader进程则去处理新连接的请求,在/home/shiyanlou/目录下创建文件leader.php:
$sock = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
$pids = [];
for($i=0;$i<10;$i++){
$pid = pcntl_fork();
$pids[] = $pid;
if($pid == 0){
for(;;){
$conn = stream_socket_accept($sock);
$out_buffer = "HTTP/1.0 200 OK\r\nServer: my_server\r\nContent-Type:text/html; charset=utf-8\r\n\r\n this is $i process";
fwrite($conn, $out_buffer);
fclose($conn);
}
exit(0);
}
}
foreach($pids as $pid){
pcntl_waitpid($pid, $status);
}
这样,我们的WEB服务器的处理能力又上了一个台阶,可以同时处理10个并发,当然这个能力还会随着你的进程池中进程的数量提升。那是不是意味着只要我们无限加大进程的数量,就可以处理无限的并发呢?遗憾的是,事实并不是这样。首先,系统创建进程的开销是大的,系统并不能无限地创建进程,因为每一个进程都占用一定的系统资源,而系统的资源是有限的,不可能无限地创建。 其次,大量进程带来的上下文切换,也会带来巨大的资源消耗和性能浪费。所以使用大量地创建进程的方式来提升并发,是不可行的。那么,没有办法了么?难道没有一种技术在单进程里就可以维持成千上万的连接么?下一个实验我们将介绍IO复用技术,使我们WEB服务器的并发处理量再次提升。
涉及知识点:阻塞/非阻塞,同步/异步,I/O多路复用,轮询,epoll
阻塞/非阻塞:这两个概念是针对 IO 过程中进程的状态来说的,阻塞 IO 是指调用结果返回之前,当前线程会被挂起;相反,非阻塞指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回;
同步/异步:这两个概念是针对调用如果返回结果来说的,所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回;相反,当一个异步过程调用发出后,调用者不能立刻得到结果,实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者;
阻塞与非阻塞:在介绍IO复用技术之前,先介绍一下阻塞和非阻塞,在我们前几节的WEB服务器中,调用socket_accept函数会使整个进程阻塞,直到有新连接,操作系统才唤醒进程继续执行。而非阻塞模式, stream_socket_accept的行为就不一样了,如果没有新连接,不会阻塞进程,而是马上返回false;
I/O 多路复用:多路复用(IO/Multiplexing):为了提高数据信息在网络通信线路中传输的效率,在一条物理通信线路上建立多条逻辑通信信道,同时传输若干路信号的技术就叫做多路复用技术。对于 Socket 来说,应该说能同时处理多个连接的模型都应该被称为多路复用,目前比较常用的有 select/poll/epoll/kqueue 这些 IO 模型(目前也有像 Apache 这种每个连接用单独的进程/线程来处理的 IO 模型,但是效率相对比较差,也很容易出问题,所以暂时不做介绍了)。在这些多路复用的模式中,异步阻塞/非阻塞模式的扩展性和性能最好;
select轮询:使用select会轮询连接池,当有连接可读或可写时,select函数返回可读写的连接数,然后再轮询一遍连接池,查找活动连接进行读写操作。比较尴尬的是,socket_select只支持socket类型的资源,而不支持stream类型的资源,所以这里需要使用socket_create创建socket资源;
创建文件select.php:
$sock = socket_create(AF_INET, SOCK_STREAM,0);
socket_bind($sock, '127.0.0.1', 8080);
socket_listen($sock);
$reads = $clients = [];
$writes = $exceptions = NULL;
socket_set_nonblock($sock);
$out_buffer = "HTTP/1.0 200 OK\r\nServer:server\r\nContent-Type:text/html;chartset=utf-8\r\n\r\nHello!world";
for(;;){
$reads = array_merge(array($sock), $clients);
$activity_counts = @socket_select($reads, $writes, $exceptions, 0);
if($activity_counts>0){
if(($conn=socket_accept($sock))!= false){
$clients[] = $conn;
}
$length = count($clients);
for($i=0; $i<$length;$i++){
$client = $clients[$i];
if(($rad_buffer = @socket_read($client, 1024)) != false){
socket_write($client, $out_buffer);
socket_close($client);
break;
}
}
}
}
select虽然可以监听多个连接,但是它最多只能监听1024个连接。这虽然在poll中得到了改进,但是select和poll本质上都是通过轮询的方式进行监听,这意味着当监听了上万连接时,就算只有一个连接是活动的,依然要把上万连接都遍历一次。显然,这无疑是极大的性能浪费,而epoll的出现彻底地解决了这个问题
epoll:epoll并不是只有一个函数来实现,而是多个函数。我们这里并不讨论epoll相关的函数,因为PHP并不提供相关的函数,但它提供了基于libevent库的libevent扩展,以及基于libevent库的event扩展。libevent库实现了Reactor模型,关于Reactor模型,这里只作简单的介绍(Reactor模型,包含了几个组件:句柄,事件分发器,事件处理器。句柄:就是文件描述符,在Socket编程中,就是使用socket_create创建的socket资源.事件分发器:通过事件循环,事件循环是通过诸如epollSelectPoll
等IO复用技术实现的,监听句柄期待的事件是否发生,发生了则将事件分发给事件处理器。事件处理器:当事件发生时,处理相关的逻辑)。
而libevent库已经实现了Reactor模型,我们可以开箱即用。下面,我们将通过libevent对我们的WEB服务器再次改造,使它的处理并发的能力再次提高在此之前,我们需要安装event扩展,安装php的event扩展必须安装libevent库,php -m|grep event
确保我们已经安装好了event库;
示例:epoll.php
$fd = stream_socket_server("tcp://127.0.0.1:8080", $errno, $errstr);
stream_set_blocking($fd, 0);
$event_base = new EventBase();
$event = new Event($event_base, $fd, Event::READ | Event::PERSIST, function($fd) use (&$event_base){
$conn = stream_socket_accept($fd);
fwrite($conn, 'HTTP/1.0 200 OK\r\nContent-Length:2\r\r\r\rHi');
fclose($conn);
}, $fd);
$event->add();
$event_base->loop();
流程和创建Reactor模型一致:创建句柄->创建事件循环器->创建事件,并指定事件监听的事件类型及注册事件处理器->向循环器中添加事件
这里我们主要看Event类,看看它的构造函数原型:
public Event::__construct ( EventBase base , mixed base,mixedfd , int what , callable what,callablecb [, mixed $arg = NULL ] )
base: EventBase类的实例;fd: 要监听的句柄;what: 要监听的事件类型;cb: 事件处理器,在PHP中就是回调函数;arg: 事件处理器的参数列表
通过我们进一步的改造,我们的WEB服务器现在处理并发的能力已经非常强劲,但是要用于生产环境,还有一些需要解决的问题,下一章我们将探讨如何让WEB服务器进程脱离控制终端,变为守护进程
进程的几个ID[pid:进程ID,ppid:父进程ID,pgid:进程组ID,sid:会话组ID],可以用命令去查看ps -axj
,一般PPID为0的,都是内核态进程。一般PPID为1的,并且pid == pgid == sid的,都是守护进程
守护进程创建的标准流程,让WEB服务器进程变为守护进程,成为守护进程有几个标准的步骤:
示例:daemon.php:
function daemon(){
umask(0);
if(pcntl_fork()){
exit(0);
}
posix_setsid();
if(pcntl_fork()){
exit(0);
}
sleep(100);
}
daemon();
在终端运行php daemon.php && ps axj|grep daemon.php
,观察一下ppid、pid、pgid、sid,结果显示:ppid确实为1,这证明进程已经被init1号进程收养。但是为什么pid、pgid、sid这三个值不一样呢?是不是弄错了?我们再看看代码,在调用posix_setsid之后,这三个值其实是一样的,只是我们又fork了一次,所以pid变了。有兴趣的同学把第二次fork的代码注释点,再观察一下,是不是一样了?
现在我我们对上节的server.php进行改写:
function daemon(){
umask(0);
if(pcntl_fork()){
exit(0);
}
posix_setsid();
if(pcntl_fork()){
exit(0);
}
sleep(100);
}
daemon();
$fd = stream_socket_server('tcp://127.0.0.1:8080', $errno, $errstr);
stream_set_blocking($fd, 0);
$event_base = new EventBase();
$event = new Event($event_base, $fd, Event::READ | Event::PERSIST, function($fd) use(&$event_base){
$conn = stream_socket_accept($fd);
fwrite($conn, 'HTTP/1.0 200 OK\r\nContent-Length:2\r\n\r\nHi');
fclose($conn);
}, $fd);
$event->add();
$event_base->loop();
运行成功之后,关闭当前终端,打开另一终端,输入 ps axj | grep server.php观察pid、pgid、sid、ppid,并打开浏览器输入127.0.0.1:8080,看是否输出结果到这儿,我们的WEB服务器才相对完善一些了,那有的同学就又要问了,变成了守护进程,那我要怎么控制它重启,暂停呢?接下来的一节我们将介绍如何使用信号与守护进程进行通信。
信号: 我们在使用控制终端的时候,在上面键入各种各样的子程序,比如sudo apt-get安装程序,但有的时候子程序运行时间过长,我们没有耐心等下去时,我们经常会按Ctrl+c结束当前进程的运行,Ctrl+c实质上就是发送一个SIGINT信号给子程序,子程序的信号处理器接收到该信号之后,就会按预先编好的程序进行处理,这样的话即使我们脱离终端,无法进行直接的手动操作也可以利用信号控制我们编写程序的状态,那在PHP中我们如何调用函数发送信号呢?
相关函数1 posix_kill
函数原型: bool posix_kill ( int pid , int pid,intsig )
pid: 进程ID
sig: 系统预定义的信号常量
相关函数2 pcntl_signal
函数原型: bool pcntl_signal ( int signo , callback signo,callbackhandler [, bool $restart_syscalls = true ] )
signo: 系统预定义的信号常量
handler: 信号处理器,一个回调函数
restart_syscalls: 当进程在进行系统调用时,被信号中断时,系统调用是否重新调用,一般默认为true
示例:signal.php:
declare(ticks=1);
pcntl_signal(SIGINT, function(){
file_put_content("signal.txt", "signal recevied\n")
})
sleep(30);
编辑完成之后,我们在终端执行php signal.php
在进程返回结果之前,我们按下Ctrl+c,此时系统会自动调用kill发送信号 SIGINT 我们编写的信号处理器进行信号的处理执行回调函数。除了使用pcntl_signal安装信号处理器,我们在上一章说过的Event类,也可以监听信号事件,将signal.php改写为:
$event_base = new EventBase();
$event = new Event($event_base, SIGINT, Event::SIGNAL, function() use(&$event_base){
file_put_content("signal2.txt", "signal recevied\n")
})
$event->add();
$event_base->loop();
使用守护进程和信号再次重构我们的WEB服务器,让它更像一个真正的能用在生产环境的在此感谢实验楼提供的实验帮助
扩展阅读php手册之socket