在数据变动比较频繁时对实时显示有一定要求、存在多个客户端的一对多的业务模式时可以考虑使用socket解决问题。
socket的概念和应用场景相信你一定可以通过搜索引擎检索到更多、更详细的信息,本文就不重复了。
socket通信是由两部分组成,一是server,二是client。一个server可以对多个client同时提供服务,以解决高并发和实时通信的问题。下面是demo code。
client.php
php模拟socket客户端,与socket服务端进行信息交互。
/*
+-------------------------------
* @socket连接整个过程
+-------------------------------
* @socket_create
* @socket_connect
* @socket_write
* @socket_read
* @socket_close
+--------------------------------
*/
$port = 9999;
$ip = "127.0.0.1";
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if ($socket < 0) {
echo "socket_create() failed: reason: " . socket_strerror($socket) . "\n";
}else {
echo "socket已创建.\n";
}
echo "试图连接 '$ip' 端口 '$port'...\n";
$result = socket_connect($socket, $ip, $port);
if ($result < 0) {
echo "socket_connect() failed.\nReason: ($result) " . socket_strerror($result) . "\n";
}else {
echo "已连接\n";
}
$in = "hello\r\n";
$in .= "world\r\n";
$out = '';
if(!socket_write($socket, $in, strlen($in))) {
echo "socket_write() failed: reason: " . socket_strerror($socket) . "\n";
}else {
echo "发送的内容为:$in";
}
$out = socket_read($socket, 9999);
echo "接受的内容为:",$out;
socket_close($socket);
echo "关闭SOCKET...\n";
server01.php
最简单socket服务器
$host = '127.0.0.1';
$port = 9999;
// 创建一个tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 将socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 开始监听socket
socket_listen( $listen_socket );
// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上
while( true ){
// 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的
// 所以你不用担心while循环会将机器拖垮,不会的
$connection_socket = socket_accept( $listen_socket );
// 向客户端发送一个helloworld
$msg = "helloworld\r\n";
socket_write( $connection_socket, $msg, strlen( $msg ) );
socket_close( $connection_socket );
}
socket_close( $listen_socket );
现在使用 server01.php 作为服务端提供socket服务,client.php 作为客户端与 server01.php 进行信息交互。下面使用ssh的方式演示。
一个简易的demo已经完成了,但上面的 server01.php 存在缺陷。
一次只可以为一个客户端提供服务,如果正在为第一个客户端发送helloworld期间有第二个客户端来连接,那么第二个客户端就必须要等待片刻才行。
很容易受到攻击,造成拒绝服务。
好的,我们继续改造server。
server02.php
我们可以在accpet到一个请求后就fork一个子进程来处理这个客户端的请求,这样当accept了第二个客户端后再fork一个子进程来处理第二个客户端的请求,这样问题就解决了。
需要注意的是:在 UNIX / Linux 系统中,一个进程结束了,但是他的父进程没有等待(调用 wait / waitpid )他, 那么他将变成一个僵尸进程。 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程。
$host = '127.0.0.1';
$port = 9999;
// 创建一个tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 将socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 开始监听socket
socket_listen( $listen_socket );
// 进入while循环,不用担心死循环死机,因为程序将会阻塞在下面的socket_accept()函数上
while( true ){
// 此处将会阻塞住,一直到有客户端来连接服务器。阻塞状态的进程是不会占据CPU的
// 所以你不用担心while循环会将机器拖垮,不会的
$connection_socket = socket_accept( $listen_socket );
// 当accept了新的客户端连接后,就fork出一个子进程专门处理
$pid = pcntl_fork();
// 在子进程中处理当前连接的请求业务
if( 0 == $pid ){
// 向客户端发送一个helloworld
$msg = "helloworld\r\n";
socket_write( $connection_socket, $msg, strlen( $msg ) );
echo time().' : a new client'.PHP_EOL;
socket_close( $connection_socket );
exit;
}
// 回收子进程,释放资源,避免出现僵尸进程
pcntl_waitpid($pid, $status, WNOHANG);
}
没错, server02.php 仍然存在2个问题。
客户端数量较多时,会fork出请求量对应的进程数。fork本身就是一个很浪费系统资源的系统调用,大量的fork足以让系统崩溃。
依然没解决拒绝服务攻击。
继续改造…
server03.php
可以预估一下业务量,然后在服务启动的时候就fork出固定数量的子进程,每个子进程处于无限循环中并阻塞在accept上,当有客户端连接挤进来就处理客户请求,当处理完成后仅仅关闭连接但本身并不销毁,而是继续等待下一个客户端的请求。这样,不仅避免了进程反复fork销毁巨大资源浪费,而且通过固定数量的子进程来保护系统不会因无限fork而崩溃。
$host = '127.0.0.1';
$port = 9999;
// 创建一个tcp socket
$listen_socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP );
// 将socket bind到IP:port上
socket_bind( $listen_socket, $host, $port );
// 开始监听socket
socket_listen( $listen_socket );
// 给主进程换个名字
cli_set_process_title( 'phpserver master process' );
// 按照数量fork出固定个数子进程
for( $i = 1; $i <= 1; $i++ ){
$pid = pcntl_fork();
if( 0 == $pid ){
cli_set_process_title( 'phpserver worker process' );
while( true ){
$conn_socket = socket_accept( $listen_socket );
$msg = "helloworld\r\n";
socket_write( $conn_socket, $msg, strlen( $msg ) );
socket_close( $conn_socket );
}
}
}
// 主进程不可以退出,代码演示比较粗暴,为了不保证退出直接走while循环,休眠一秒钟
// 实际上,主进程真正该做的应该是收集子进程pid,监控各个子进程的状态等等
while( true ){
sleep( 1 );
}
socket_close( $connection_socket );
pcntl_waitpid($pid, $status, WNOHANG);
附赠两条命令
//找出僵死进程
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
//查找僵死进程,然后将父进程杀死
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]' | awk '{print $2}' | xargs kill -9
参考资料