php socket 编程

php socket 编程

1.实验预习:tcp协议

TCP协议的建立:
建立流程:1.客户端主动调用connect发送SYN分节;2.服务器端必须回复一个ACK分节来确认客户端的SYN分节,并发送一个SYN分节给客户端;3.客户端对服务器端发送SYN分节进行ACK分节的确认
php socket 编程php

TCP协议的拆除(TCP为全双工的传输协议,因此须要4次分节的交换):
拆除流程:1.首先申请拆除的一端调用close发送一个FIN分节;2.另外一端接收到FIN分节时,发送一个ACK分节进行确认;3.另外一端要申请拆除链接时,也要发送一个FIN分节;4.接收端发送一个ACK分节进行确认
php socket 编程html

TCP的状态转换图
链接:[1.SYN_SENT主动打开,SYN分节已发送;2.SYN_RCVD被动打开,SYN分节已接收;3.ESTABLISHED已经创建链接]编程

关闭:[1.FIN_WAIT_1发起主动关闭,FIN分节已发送;2.CLOSE_WAIT被动关闭,FIN分节已接收,ACK分节已发送;3.FIN_WAIT_2成功实现半关闭,ACK分节已接收;4.LAST_ACK最终的ACK,FIN分节已发送;5.TIME_WAIT FIN分节已接收,ACK分节已发送;6.CLOSE ACK分节已接收,成功拆除链接]
php socket 编程浏览器

2.SOCKET 编程

咱们能够简单的把 Socket 理解为一个能够连通网络上不一样计算机应用程序之间的管道,把一堆数据从管道的 A 端扔进去,则会从管道的 B 端(同时还能够从C、D、E、F……端冒出来)(Socket 的官方解释: 在网络编程中最经常使用的方案即是Client/Server(客户机/服务器)模型。在这种方案中客户应用程序向服务器程序请求服务。一个服务程序一般在一个众所周知的地址监听对服务的请求,也就是说,服务进程一 直处于休眠状态,直到一个客户向这个服务的地址提出了链接请求。在这个时刻,服务程序被"惊醒"而且为客户提供服务-对客户的请求做出适当的反应。)
php socket 编程服务器

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

<?php
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($sock, '127.0.0.1', 8080);
socket_listen($sock);
for(;;){
$conn = 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';
socket_write($conn, $output_buffer);
socket_close($conn);
}

tcp

<?php
$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, $write_buffer);
fclose($conn);
}

控制台运行
sudo php-fpm7.2 start && php sertver.php

运行成功以后,打开浏览器输入 ‘127.0.0.1:8080’

3.多进程编程

多进程简介:就是多个进程同时工做,这样的进程通常属于亲属关系,一般由一个父进程fork获得的. 注意这里所说的同时工做,是宏观上的,同一时刻在单个单核CPU上

 示例 multiProcess.php

<?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 $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:80", $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服务器的并发处理量再次提高。

4 I/O复用

涉及知识点:阻塞/非阻塞,同步/异步,I/O多路复用,轮询,epoll

1.阻塞/非阻塞:这两个概念是针对 IO 过程当中进程的状态来讲的,阻塞 IO 是指调用结果返回以前,当前线程会被挂起;相反,非阻塞指在不能马上获得结果以前,该函数不会阻塞当前线程,而会马上返回;

2.同步/异步:这两个概念是针对调用若是返回结果来讲的,所谓同步,就是在发出一个功能调用时,在没有获得结果以前,该调用就不返回;相反,当一个异步过程调用发出后,调用者不能马上获得结果,实际处理这个调用的部件在完成后,经过状态、通知和回调来通知调用者;

3.阻塞与非阻塞:在介绍IO复用技术以前,先介绍一下阻塞和非阻塞,在咱们前几节的WEB服务器中,调用socket_accept函数会使整个进程阻塞,直到有新链接,操做系统才唤醒进程继续执行。而非阻塞模式, stream_socket_accept的行为就不同了,若是没有新链接,不会阻塞进程,而是立刻返回false;

4.I/O 多路复用:多路复用(IO/Multiplexing):为了提升数据信息在网络通讯线路中传输的效率,在一条物理通讯线路上创建多条逻辑通讯信道,同时传输若干路信号的技术就叫作多路复用技术。对于 Socket 来讲,应该说能同时处理多个链接的模型都应该被称为多路复用,目前比较经常使用的有 select/poll/epoll/kqueue 这些 IO 模型(目前也有像 Apache 这种每一个链接用单独的进程/线程来处理的 IO 模型,可是效率相对比较差,也很容易出问题,因此暂时不作介绍了)。在这些多路复用的模式中,异步阻塞/非阻塞模式的扩展性和性能最好;

5.select轮询:使用select会轮询链接池,当有链接可读或可写时,select函数返回可读写的链接数,而后再轮询一遍链接池,查找活动链接进行读写操做。比较尴尬的是,socket_select只支持socket类型的资源,而不支持stream类型的资源,因此这里须要使用socket_create建立socket资源;

建立文件select.php:

<?php
$sock = socket_create(AF_IINIT, SOCK_STREAM,0);
socket_bind($sock, '127.0.0.1', 80);
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, $write_buffer);
socket_close($client);
break;
}
}
}
}

select虽然能够监听多个链接,可是它最多只能监听1024个链接。这虽然在poll中获得了改进,可是select和poll本质上都是经过轮询的方式进行监听,这意味着当监听了上万链接时,就算只有一个链接是活动的,依然要把上万链接都遍历一次。显然,这无疑是极大的性能浪费,而epoll的出现完全地解决了这个问题

6.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

<?php
$fd = stream_socket_server("tcp://127.0.0.1:80", $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服务器进程脱离控制终端,变为守护进程

7信号通讯以及守护进程

进程的几个ID[pid:进程ID,ppid:父进程ID,pgid:进程组ID,sid:会话组ID],能够用命令去查看ps -axj,通常PPID为0的,都是内核态进程。通常PPID为1的,而且pid == pgid == sid的,都是守护进程

守护进程建立的标准流程,让WEB服务器进程变为守护进程,成为守护进程有几个标准的步骤:

1.设置文件建立掩码,通常设置为0,umask(0)
2.pcntl_fork一个子进程,并立刻退出,这样作的目的是让子进程继承进程组ID并获取一个新的进程ID,这样就能够确保子进程必定不是进程组组长,由于进程组组长不能建立新会话
3.posix_setsid建立新会话和新进程组,并成为会话组长和进程组组长,并和原来的控制终端脱离关系,这样该进程就不会被原来终端的控制信号中断
4.pcntl_fork,再fork一次并非必须的,只是在基于System-V的系统上,有人建议再fork一次,避免打开终端设备,使程序的通用性更强。

示例:daemon.php:

<?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进行改写:

<?php
unction 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:

<?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改写为:

<?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

相关文章
相关标签/搜索