Socket PHP

在网络编程的客户端/服务器模型中,客户端应用程序向服务器程序请求服务,一个服务器程序通常在指定地址的端口上监听对服务的请求,也就是说,服务进程会一直处于休眠状态,直到有一个客户端向这个服务器的地址端口上发出连接请求。此时,服务器程序被“惊醒”并未客户端提供服务,也就是为客户端的请求作出适当的反应。

为了方便这种客户端/服务器模型的网络编程,90年代初期由微软联合多家公司共同制定了一套Windows下的网络编写接口即WindowsSockets规范,WindowsSockets并非一种网络协议,而是一套开放的、支持多种协议的Windows下的网络编程接口。

Socket是应用层与TCP/IP协议簇通信的中间软件抽象层,是一组接口 。在设计模式中,Socket是一个门面模式,它将复杂的TCP/IP协议簇隐藏在Socket接口后面,对用户而言一组简单的接口就是它的全部。

Socket抽象层

简单来说,Socket是封装好的TCP/IP通信协议的接口,提供网络通讯的能力,更加方便地使用协议栈。Socket赋予用户控制传输层和网络层的能力,从而得到更强的性能和更高的效率。Socket编程是解决高并发网络服务器中最常见和最成熟的解决方案。

Socket

网络上两个应用程序会通过一个双向的通信连接来实现数据的交换,此时连接的一端会被成为一个Socket。所以,Socket的运行至少具有两个端,比如一个服务端多个客户端。

Socket的运行流程

套接字运行流程
  1. 服务器初始化Socket后与端口进行绑定bind,接着会对端口进行监听listen,并调用accept进入阻塞状态以等待客户端的连接。
  2. 如果此时客户端初始化Socket后连接connect到服务器
  3. 如果连接建立成功,客户端就可以发送数据请求到服务器,服务端会接收请求并处理,最后将回应数据返回给客户端。
  4. 客户端读取服务器返还的数据后关闭连接,完成一次交互。
Socket

为什么说 WebSocket是一个持久化的协议呢?

WebSocket是一个持久化的协议是相对于HTTP非持久化来说的

  • HTTP1.0的生命周期是以request请求作为界定。即一个request请求对应一个response响应,对于HTTP来说一次客户端与服务器的会话会到此结束。
  • HTTP1.1中加入了keep-alive,也就是在一个HTTP连接中可以对多个请求和多个响应结束操作。然而在实时通讯方式并没太多的作用。因为HTTP只能由客户端发起请求,服务器才能返回消息,也就是说服务器不能主动向客户端推送信息,无法满足实时通讯的要求。WebSocket支持持久化连接,也就是客户端只需要一次握手,成功后即可进行数据通信。

PHP中可以操作Socket的函数分为两种,一种是以socket_开头的系列函数,另一种是以stream_开头的系列函数。socket_函数是PHP将C语言中的Socket移植过来的实现,而stream_开头的系列则是PHP使用流stream的概念将其进行的一层封装。

例如:

$ vim server.php

服务器开发流程

  1. 根据协议簇或地址簇、套接字类型、协议创建套接字
  2. 将创建好的套接字绑定到指定主机的端口上
  3. 服务器开启监听
  4. 使服务器进入无线循环不退出的状态,当没有客户的连接时程序会阻塞在accept上,当有连接进入时才会往下执行,然后再次循环,为客户端提供持久服务。
<?php
//设置脚本超时事件
set_time_limit(60);//保证在连接客户端时脚本不会因为超时而中断执行

//创建socket返回socket句柄
$domain = AF_INET;//域名
$type = SOCK_STREAM;//Socket类型
$protocol = SOL_TCP;//协议类型
$socket = socket_create($domain, $type, $protocol);
if($socket < 0){
    $errmsg = socket_strerror($socket);
    echo "[create] {$errmsg}".PHP_EOL;
    exit;
}

//绑定socket句柄绑定到对应主机的端口
$host = "127.0.0.1";
$port = 1901;
$ret = socket_bind($socket, $host, $port);
if($ret < 0){
    $errmsg = socket_strerror($socket);
    echo "[bind] {$errmsg}".PHP_EOL;
    exit;
}

//监听外部连接
$backlog = 4;
$ret = socket_listen($socket, $backlog);
if($ret < 0){
    $errmsg = socket_strerror($socket);
    echo "[listen] {$errmsg}".PHP_EOL;
    exit;
}


$count = 0;
$max = 5;
do{
    //接收来自客户端的请求,调用另一个socket来处理通信。
    $spawn = socket_accept($socket);
    if($spawn < 0){
        $errmsg = socket_strerror($spawn);
        echo "[accept] {$errmsg}".PHP_EOL;
        break;
    }
    //写入客户端
    $message = "success";
    $message = mb_convert_encoding($message, "GBK", "UTF-8");//处理中文乱码
    socket_write($spawn, $message, strlen($message));
    //获取客户端的输入
    $buffer = socket_read($spawn, 8192);
    echo "[read] {$buffer}".PHP_EOL;

    if(++$count > $max){
        break;
    }
    //关闭客户端socket
    socket_close($spawn);
}while(true);

//关闭服务端socket
socket_close($socket);

运行服务器

$ php server.php

客户端

$ vim client.php
<?php
error_reporting(E_ALL);
set_time_limit(0);

$domain = AF_INET;
$type = SOCK_STREAM;
$protocol = SOL_TCP;
$socket = socket_create($domain, $type, $protocol);
if($socket < 0){
    $errmsg = socket_strerror($socket);
    echo "[create] {$errmsg}".PHP_EOL;
    exit;
}

//设置接收套接字流的最大超时时间为1秒
$level = SOL_SOCKET;
$optname = SO_RCVTIMEO;
$optval = ["sec"=>1, "usec"=>0];
socket_set_option($socket, $level, $optname, $optval);

//设置发送套接字流的最大超时时间为6秒
$level = SOL_SOCKET;
$optname = SO_SNDTIMEO;
$optval = ["sec"=>6, "usec"=>0];
socket_set_option($socket, $level, $optname, $optval);

//连接服务器的套接字流
$address = "127.0.0.1";
$port = 1901;
$ret = socket_connect($socket, $address, $port);
if($ret < 0){
    $errmsg = socket_strerror($socket);
    echo "[create] {$errmsg}".PHP_EOL;
    exit;
}

//向服务器写入字符串信息
$buffer = "hello world";
$buffer = mb_convert_encoding($buffer, "GBK", "UTF-8");//统一编码处理乱码
$ret = socket_write($socket, $buffer, strlen($buffer));
if(!$ret){
    $errmsg = socket_strerror($socket);
    echo "[write] {$errmsg}".PHP_EOL;
    exit;
}

//读取服务器返还的套接字流
while($recv = socket_read($socket, 8192)){
    echo "[read] {$recv}".PHP_EOL;
}

//关闭套接字流
socket_close($socket);

运行客户端

$ php client.php

代码缺陷:

  1. 一次只能为一个客户端提供服务,如果正在为第一个客户端发送消息期间有第二人个客户端来连接,那么第二个客户端就必须等待片刻后才行。
  2. 容易受到攻击,造成拒绝服务。

解决方案:

使用多进程方式,在accept到一个请求后fork处一个子进程来出来客户端的请求。

<?php
//创建TCP套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

//将套接字绑定到指定主机的端口上
$host = "0.0.0.0";
$port = 9501;
socket_bind($socket, $host, $port);

//监听客户端的套接字连接
socket_listen($socket);

//进入死循环
while(true){
    //阻塞直到有客户端连接服务器,阻塞状态进程不占用CPU
    $connection = socket_accept($socket);
    //当有新客户端连接时fork子进程专门处理
    $pid = pcntl_fork();
    //在子进程中处理当前连接的业务逻辑
    if($pid == 0){
        $msg = "success";
        socket_write($connection, $msg, strlen($msg));
        //休眠5秒用来观察同时为多个客户端提供服务
        echo date("Y-m-d H:i:s")." client".PHP_EOL;
        sleep(5);
        //关闭连接
        socket_close($connection);
        //退出程序
        exit;
    }
}
//关闭监听的socket
socket_close($socket);

代码缺陷:如果先后有1w个客户端来请求,此时服务器会fork出1w个子进程来出来每个客户端连接。fork本身是非常消耗系统资源的系统调用。

解决方案:提前预估业务量,在服务器启动时fork出固定数量的子进程,每隔子进程处于无限循环并阻塞在accept上,当有新客户端连接时处理请求,处理完毕仅仅关闭连接但本身并不销毁,继续等待下一个客户端的请求。避免进程反复fork和销毁带来的巨大资源浪费。

<?php
//创建TCP套接字
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

//将套接字绑定到指定主机的端口上
$host = "0.0.0.0";
$port = 9501;
socket_bind($socket, $host, $port);

//监听客户端的套接字连接
socket_listen($socket);

//为主进程设置别名
cli_set_process_title("master");

//按照实际业务量fork出固定个数的子进程
$max_process = 10;
for($i=1; $i<$max_process; $i++){
    $pid = pcntl_fork();
    if($pid == 0){
        cli_set_process_title("worker");
        while(true){
            $connection = socket_accept($socket);
            $msg = "success";
            socket_write($connection, $msg, strlen($msg));
            socket_close($connection);
        }
    }
}
$ php server.php

$ ps aux | grep  worker
root       545  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       546  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       547  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       548  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       549  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       550  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       551  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       552  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       553  0.0  0.5 340468  5712 pts/1    S    15:30   0:00 worker
root       556  0.0  0.0  11112   976 pts/1    S+   15:31   0:00 grep worker

$ ps -ef|grep worker|grep -v grep
root       545     1  0 15:30 pts/1    00:00:00 worker
root       546     1  0 15:30 pts/1    00:00:00 worker
root       547     1  0 15:30 pts/1    00:00:00 worker
root       548     1  0 15:30 pts/1    00:00:00 worker
root       549     1  0 15:30 pts/1    00:00:00 worker
root       550     1  0 15:30 pts/1    00:00:00 worker
root       551     1  0 15:30 pts/1    00:00:00 worker
root       552     1  0 15:30 pts/1    00:00:00 worker
root       553     1  0 15:30 pts/1    00:00:00 worker

客户端使用telnet访问

$ telnet 127.0.0.1 9501

根据进程名称批量杀死进程

$ ps -ef|grep worker|grep -v grep|awk '{print $2}'|xargs kill -9

PHP中有以socket_开头的一套函数API用于Socket编程,另外PHP5引入了流的抽象概念后,以stream_开头的一套API也可以用于网络编程,两者主要区别在于:

  • stream是PHP的核心概念,以stream_开头的函数可用。
  • sockets是PHP的一个扩展,大部分情况下都默认启用。
  • socket系列函数相对底层,stream_打头系列函数是高层的抽象。

如果有要体验原生Socket编程可用使用socket_打头的API,否则建议使用stream_流函数。

socket_create

socket_create创建一个socket套接字(通讯节点)资源,成功返回一个socket套接字,失败返回false,若参数错误则会给出E_WARNING警告。

socket_create(string $net, string $stream, string $protocol) : resource

参数列表

参数1:string $net 表示网络协议,可选项包括三种:

  • AF_INET 表示IPv4网路协议,TCP与UDP均使用AF_INET协议。
  • AF_INET6 表示IPv6网络协议,TCP与UDP均采用。
  • AF_UNIX 表示本地通讯协议,具有高性能和底层本的进程间通讯IPC

AF的全称为Address Family即地址簇,例如常见的IPv4、IPv6。

参数2:string $stream 表示套接字流即字节流的Socket类型,可选项包括五种:

  • SOCK_STREAM 表示TCP协议套接字
  • SOCK_DGRAM 表示UDP协议套接字

参数3:string $protocol 表示具体所使用的传输协议,可选项包括两种:

  • SOL_TCP 表示TCP协议
  • SOL_UDP 表示UDP协议

socket_connect

socket_connect($socket, $ip, $port):bool

socket_connect用于连接一个套接字,若成功返回true失败返回false

参数列表

参数1:$socket 表示使用socket_create函数创建的套接字
参数2:$ip 表示需要连接的套接字所属的主机地址
参数3:$port 表示端口号

socket_bind

socket_bind函数用于绑定一个套接字,也就是将创建的socket资源绑定到具体的IP地址的端口上,若成功则返回true失败返回false

socket_bind(resource $socket, string $address[, int $port]) : bool

参数列表

参数1:resource $socket 表示使用socket_create创建的socket资源,可认为是socket对应的编号。
参数2:string $address 表示主机地址
参数3:int $port 表示监听的端口号

socket_listen

socket_listen函数用于监听在指定地址下监听的套接字资源的收发操作,若成功则返回true失败返回false

socket_listen(resource $socket [, int $backlog = 0]) : bool

参数列表
参数1:resource $sockt 表示使用socket_create创建的socket资源
参数2:int $backlog 表示最大监听套接字的个数,等待处理连接队列的最大长度。

socket_accept

socket_accept表示接收套接字的资源信息,成功返回套接字的信息资源,失败返回false。监听之后接收一个即将来临的新连接,如果连接建立成功则返回一个新的socket句柄。可理解为子进程,通常父进程用来接收新连接,子进程负责具体的通信。

socket_accept(resource $socket) : resource

参数列表

参数1:resource $socket 表示使用socket_create创建的socket资源

socket_read

socket_read(resource $socket, int $length): int

socket_read函数用于读取套接字的资源信息,获取传送的数据,若读取成功则会将套接字的资源转化为字符串,失败则返回false

参数列表

  • resource $socket 表示使用socket_create创建的socket资源
  • int $length 表示socket资源中的buffer的长度

socket_write

socket_write表示将指定数据写入到对应socket管道中,若成功则返回字符串的字节长度,失败则返回false

socket_write(resource $socket, string $buffer [, int $length]) : int

参数列表

  • resource $socket 表示使用socket_create创建socket资源
  • string $buffer 表示写入到socket资源中的数据
  • int $length 表示控制写入到socket资源中的buffer的长度,如果长度大于buffer的容量则会获取buffer的容量。

socket_close

socket_close函数用于关闭套接字资源,若成功则返回true失败返回false

socket_close(resource $socket) : bool

参数列表

  • resource $socket 表示使用socket_createsocket_accept产生的socket资源,不用用于stream资源的关闭。

socket_last_error

socket_last_error($socket)

socket_last_error函数用于获取套接字最后一条错误代码,若成功则返回错误代码。

socket_strerror

socket_strerror($errcode)

socket_strerror函数用于获取错误代码所对应的错误消息字符串,若成功则返回套接字的错误信息。

参数列表

  • $errcode是由socket_last_error函数返回的结果

stream_socket_server

由于创建一个socket的流程总是create、bind、listen,因此PHP提供了一个便捷函数用于一次性创建socket、绑定端口、监听端口。

stream_socket_server(
  string $local_socket, 
  [, int &$errno 
  [, string &$errstr 
  [,int $flag = STREAM_SERVER_BIND | STREAM_SERVERR_LISTEN 
  [, resource $context ]]]]
)

参数列表

  • resource $local_socket 格式为协议名://地址:端口
  • int $errno表示错误码
  • string $errmsg 表示错误信息
  • bool $flags 表示是否只使用该函数的部分功能
  • resource $context 表示使用stream_context_create函数创建的资源流上下文
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。