php 进程通信系列 (五)socket unix域套接字

php 进程通信系列 (五)socke unix域套接字

  • 常见进程通信方式
  • Unix socket (套接字)介绍
  • 一些理论基础:
  • 我们来看看php创建 无命名本地域 socket 的函数:
  • 命名Unix域【本地域】套接字
    • 平常使用到的命名Unix 域套接字
      • 编写Unix 域 tcp 类型进程通信案例
      • 编写Unix 域 udp 类型进程通信案例
  • 结语

常见进程通信方式

php 进程通信系列 (五)socket unix域套接字_第1张图片

Unix socket (套接字)介绍

现实世界中两个人进行信息交流的整个过程被称作一次通信(Communication),通信的双方被称为端点(Endpoint)。

工具通讯环境的不同,端点之间可以选择不同的工具进行通信,距离近可以直接对话,距离远可以选择打电话、微信聊天。这些工具就被称为 Socket。
php 进程通信系列 (五)socket unix域套接字_第2张图片

同理,在计算机中也有类似的概念:
在 Unix 中,一次通信由两个端点组成,例如 HTTP 服务端和 HTTP 客户端。
端点之间想要通信,必须借助某些工具,Unix 中端点之间使用 Socket 来进行通信。

Socket 原本是为网络通信而设计的,但后来在 Socket 的框架上发展出一种 IPC 机制,就是 UDS。
Unix Domain Socket(UDS,Unix 域套接字),它还有另一个名字叫 IPC(inter-process communication,进程间通信)。

使用 UDS 的好处显而易见:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。这是因为,IPC 机制本质上是可靠的通讯,而网络协议是为不可靠的通讯设计的

UDS 与网络 Socket 最明显的区别在于,网络 Socket 地址是 IP 地址加端口号,而 UDS 的地址是一个 Socket 类型的文件在文件系统中的路径,一般名字以 .sock 结尾。

这个 Socket 文件可以被系统进程引用,两个进程可以同时打开一个 UDS 进行通信,而且这种通信方式只会发生在系统内核里,不会在网络上进行传播。

特别说明:本文中提到的“Socket”、“网络套接字”、“套接字”,如无特殊指明,指的都是同一个东西。

实际上,Socket 通信不仅可以跨网络与不同主机的进程间通信,还可以在同主机上进程间通信,这就是今天介绍的 unix 域套接字。

一些理论基础:

  1. 套接字是什么?

套接字也就是socket实际上是一种连接

  1. 套接字通信域类型:

ipv4[AF_INET] 是跨机器,通信需要经过网卡,需要绑定IP,端口(绑定的原因是便于寻址,ip主要用于确认通信的机器,端口用于确定与哪个进程通信)

ipv6[AF_INET6] 是跨机器,通信需要经过网卡,需要绑定IP,端口

UNIX[AF_UNIX,AF_LOCAL] unix域也叫本地域,通信的时候不需要绑定ip端口不需要网卡,不经过网络

unix 域套接字有两种与管道类似,分为有命名的【创建好的unix域套接字需要绑定地址,它是一个比较特殊文件,socket 文件】,既能实现父子进程通信,也能实现不同进程间通信

无命名的【创建好的unix域套接字不需要绑定地址】,只能实现血缘关系进程通信,比如父子进程通信

  1. 比较常用套接字类型:tcp,udp

tcp: 需要连接(三次握手),是可靠的,有序的,错误丢包可重传的,字节流服务

udp: 不需要连接,不可靠,数据长度固定的,数据报服务

注意: 本地 socket 的编程接口和 IPv4 、IPv6 套接字编程接口是一致的,可以支持「字节流」和「数据报」两种协议,也就是说unix 域套接字也支持 tpc,upd 两种套接字类型,不过对于unix 域的 upd是可靠的,有序的这个由内核实现

我们来看看php创建 无命名本地域 socket 的函数:

//创建一对无命名的,相互连接的UNIX 域套接字
stream_socket_pair(int $domain, int $type, int $protocol): array|false

三个参数分别代表:

  • domain 参数用来指定协议族,比如 STREAM_PF_INET 用于 IPV4、STREAM_PF_INET6 用于 IPV6、STREAM_PF_UNIX 用于本机;
  • type 参数用来指定通信特性,比如 STREAM_SOCK_STREAM 表示的是字节流,对应 TCP、STREAM_SOCK_DGRAM 表示的是数据报,对应 UDP、STREAM_SOCK_RAW 表示的是原始套接字;
  • protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可;

根据创建 socket 类型的不同,通信的方式也就不同:

  • 实现 TCP 字节流通信: socket 类型是 STREAM_PF_INET 和 STREAM_SOCK_STREAM;
  • 实现 UDP 数据报通信:socket 类型是 STREAM_PF_INET 和 STREAM_SOCK_DGRAM;
  • 实现本地进程间通信:
    「本地字节流 socket 」类型是 STREAM_PF_UNIX 和 STREAM_SOCK_STREAM。
    「本地数据报 socket 」类型是 STREAM_PF_UNIX 和 STREAM_SOCK_DGRAM。
  1. 下面我们通过stream_socket_pair() 函数创建父子进程通信例子

<?php
//创建一对连接的、不可区分的套接字流,成功时返回一个包含两个套接字资源的数组
$fd = stream_socket_pair(STREAM_PF_UNIX,STREAM_SOCK_STREAM,STREAM_IPPROTO_IP);
//$fd[0]  可用于读
//$fd[1]  可用于写

$pid = pcntl_fork();//fork 一个子进程
if($pid == 0){// $pid == 0 表示子进程执行逻辑

    while(1){// 循环 读取套接字为0的流,读取长度设置为128字节
        $data = fread($fd[0],128);

        if($data){// 如果读取到数据,使用格式化输出函数打印输出,STDOUT 表示标准化输出资源流
            fprintf(STDOUT,"收到数据:%s\n",$data);
        }

        if(strncasecmp(trim($data),'quit',4)==0){// strncasecmp 比较字符串是否相同,第三个参数表示用于比较的字符串的长度 ,如果输入的字符串是quit 就结束循环,子进程退出
            break;
        }
   }
    exit(0);
}
if($pid > 0){ //pid 大于0 表示父进程执行逻辑
    while(1){ // 循环读取 STDIN 表示标准输入流,接收数据的长度设置为128字节
       $data = fread(STDIN,128);
       if($data){//如果有数据,则写入$fd[1] 流,fwrite 函数第三个参数表示写入数据的长度
            fwrite($fd[1],$data,strlen($data));
       }
		// strncasecmp 比较字符串是否相同,第三个参数表示用于比较的字符串的长度,如果输入的是quit,就退出循环
       if(strncasecmp(trim($data),'quit',4)==0){
        break;
       }
    }

}
// 回收子进程预防僵尸进程
$pid = pcntl_wait($status);
// 打印 退出子进程 pid
fprintf(STDOUT,"退出进程id:%s\n",$pid);

php 进程通信系列 (五)socket unix域套接字_第3张图片

无名Unix域套接字,和管道相似以套接字对的形式创建,不同于管道的是,这对套接字都对读写开放,是全双工通信【即同一时刻两端可以相互通信类似于打电话】,管道是半双工通信【即同一时间只能有一端能发送数据,类似于对讲机】需要注意的是,匿名Unix套接字对,虽然都对读写开放,但是,通过fd[0]写入数据,再通过fd[0]读数据,会阻塞,只能通过fd[1]来读,当fd[1]中没写入数据时,通过fd[0]进行读的话,也会阻塞。

当fd[1]中没写入数据时,通过fd[0]进行读的话,会阻塞这种情况



$fd = stream_socket_pair(STREAM_PF_UNIX,STREAM_SOCK_STREAM,0);
// 当fd[1]中没写入数据时,通过fd[0]进行读的话,会阻塞
echo “one\n”;
echo fread($fd[0],128);
echo “two\n”;

php 进程通信系列 (五)socket unix域套接字_第4张图片

通过fd[0]写入数据,再通过fd[0]读数据,会阻塞这种情况


<?php
$fd = stream_socket_pair(STREAM_PF_UNIX,STREAM_SOCK_STREAM,0);
$w = fwrite($fd[0],"hello word\n",128);
echo "写入字节数:".$w.PHP_EOL;
echo fread($fd[0],128);
echo "two\n";
[root@dada unix_socket]#

php 进程通信系列 (五)socket unix域套接字_第5张图片

命名Unix域【本地域】套接字

stream_socket_pair() 可以创建一对相互连接的套接字,但是每个套接字没有名字,在无关进程中也是无法使用它们。所以要想和其他进程进行通信,就需要定一个众所周知的名字。即类似于消息队列那样的操作,创建一个特殊的文件作为地址,让其他进程通过连接地址找到服务进程

要创建命名Unix域套接字需要使用两个 socket_* 的扩展函数中的 socket_create()socket_bind(),我们挨个来介绍

//创建并返回一个Socket实例,也称为通信端点。典型的网络连接由2个套接字组成,一个执行客户端角色,另一个执行服务器角色
socket_create(int $domain, int $type, int $protocol): Socket|false

三个参数分别代表:

  • domain 参数用来指定协议族,比如 STREAM_PF_INET 用于 IPV4、STREAM_PF_INET6 用于 IPV6、STREAM_PF_UNIX 用于本机;
  • type 参数用来指定通信特性,比如 STREAM_SOCK_STREAM 表示的是字节流,对应 TCP、STREAM_SOCK_DGRAM 表示的是数据报,对应 UDP、STREAM_SOCK_RAW 表示的是原始套接字;
  • protocal 参数原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成,protocol 目前一般写成 0 即可;
//将$address中给出的名称绑定到socket描述的套接字
socket_bind(Socket $socket, string $address, int $port = 0): bool

三个参数分别代表:

  • 填写 socket_create() 函数创建的sock 套接字
  • address 参数用来当我们将一个地址绑定到一个Unix域套接字时使用
  • port 这个参数用于绑定端口,只有ipv4,ipv6域才有用也就是跨网络通信时使用

平常使用到的命名Unix 域套接字

我们讲过命名的Unix 域套接字会创建一个绑定地址的 Socket 类型的文件,一般名字以 .sock 结尾,我们平常使用的mysql ,php-fpm 都有使用 .sock 文件通信
php 进程通信系列 (五)socket unix域套接字_第6张图片

创建命名Unix 域套接字


//定义一个sock 文件名
$sock_file = "sock_file.sock";
// 创建一个sock本地域套接字
$sock = socket_create(STREAM_PF_UNIX,STREAM_SOCK_STREAM,0);
// 将套接字绑定到文件上
socket_bind($sock,$sock_file);

php 进程通信系列 (五)socket unix域套接字_第7张图片
执行脚本我们获得了一个socket 类型的文件 sock_file.sock 它与 mysql.sock 类型相同

编写Unix 域 tcp 类型进程通信案例

1.1 编写一个服务端与客户端通信案例,服务端监听客户端连接,读取客户端发送过来的数据,并把数据在发送回客户端

servers 端


//随意定义一个文件名,有没有.sock 后缀都行
$sock_file = "sock_file.sock";
// 创建一个sock本地域套接字
$sock = socket_create(STREAM_PF_UNIX,STREAM_SOCK_STREAM,0);
// 将套接字绑定到文件上
socket_bind($sock,$sock_file);
// 监听套接字上的连接
socket_listen($sock,4);
//接受套接字上的连接 , 客户端发送连接请求,一旦成功连接,将返回一个新的Socket实例,可以使用这个socket 实例与客户端通信,如果没有客户端连接过来,服务端将阻塞在该函数上
$connfd = socket_accept($sock);

if($connfd){
    fprintf(STDOUT,"监听到客户端连接\n");
    while(1){
    	// 读取客户端过来的数据,如果客户端没有发送数据,将阻塞在该函数上
        $data = socket_read($connfd,1024);
        if($data){
            fprintf(STDOUT,"收到客户端数据:%s\n",$data);
            // 向客户端写入接收到的数据
            socket_write($connfd,$data,strlen($data));
        }
		// 当收到数据为 quit 结束服务端进程
        if(strncasecmp($data,"quit",4) == 0){
            break;
        }
    }
}
// 关闭服务端 sock 实例
socket_close($sock);
// 关闭客户端连接sock 实例
socket_close($connfd);

clients 端


//需要与服务端使用同一个sock 文件
$sock_file = "sock_file.sock";
//创建一个sock本地域套接字,
$sock = socket_create(STREAM_PF_UNIX,STREAM_SOCK_STREAM,0);
// 在套接字上发起连接,连接服务端
if(socket_connect($sock,$sock_file)){

$pid = pcntl_fork();// fork 子进程

if($pid ==0){
// 子进程负责接收服务端发来的数据
    while(1){
    	 // 从连接的套接字接收数据
        $len = socket_recv($sock,$data,1024,0);
        if($len){
            fprintf(STDOUT,"收到服务端发来数据:%s\n",$data);
        }
		// 接收数据为quit则退出循环,进程退出
        if(strncasecmp(trim($data),"quit",4) == 0){
            break;
        }

    }
    exit(0);
}
// 父进程负责发送数据给服务端
while(1){
	// 接收终端输入的数据
   $data = fread(STDIN,1024);
        if($data){
            fprintf(STDOUT,"发送数据给服务端:%s\n",$data);
            // 发送数据给服务端
            socket_send($sock,$data,strlen($data),0);
        }
		// 接收数据为quit则退出循环,进程退出
        if(strncasecmp(trim($data),"quit",4) == 0){
            break;
        }
    }

}
// 关闭客户端 sock 实例
socket_close($sock);
// 回收子进程,预防僵尸进程
$pid = pcntl_wait($status);

fprintf(STDOUT,"子进程退出pid=%s\n",$pid);

php 进程通信系列 (五)socket unix域套接字_第8张图片
需要注意:已经绑定的sock 文件不能再次绑定,如果我们试图绑定同一地址,该文件已经存在,那么绑定请求就会失败,当关闭套接字时并不会自动删除该文件,所以必须确保在程序退出前,对文件执行连接解除操作。
php 进程通信系列 (五)socket unix域套接字_第9张图片
再次运行server 端脚本文件 提示地址已在使用,要怎么解决呢?要么在程序退出前使用 unlink() 函数删除文件,要么 用rm
命令手动删除

编写Unix 域 udp 类型进程通信案例

Unix 域 udp 类型通信对于tcp 差异比较大,首先 upd 是没有连接概念的,其次 Unix 域 udp 套接字相互通信需要双向绑定,如果不理解,看下面例子

Unix 域 upd server 服务端


// 定义一个文件名
$server_file = "unix_server"; // 服务端
// 创建sock 文件
$sock = socket_create(AF_UNIX,SOCK_DGRAM,0);
// 将套接字绑定到文件上
socket_bind($sock,$server_file);

while(1){
// 读取客户端发来的数据
    $len = socket_recvfrom($sock,$buf,1024,0,$unixclientFile);
    if($len){
    	
        fprintf(STDOUT,"发送数据:%s, client_file=%s\n",$buf,$unixclientFile);
        // 发送数据到客户端,使用$unixclientFile 客户端绑定的sock 文件,发送数据
        socket_sendto($sock,$buf,strlen($buf),0,$unixclientFile);

    }

	// 接收数据为quit则退出循环,进程退出
    if(strncasecmp($buf,"quit",4) == 0){
        break;
    }
}
// 移除sock 实例
socket_close($sock);
//删除sock 文件 避免下次启动,提示文件占用错误
unlink($server_file);

Unix 域 upd client 客户端,

udp 客户端不像tcp 需要连接服务端,udp是无连接概念,只要绑定sock 文件就能相互通信


$server_file = 'unix_server';//服务端

$client_file = 'unix_clients';//客户端
// 创建sock 文件  AF_UNIX 表示 Unix 本地域 SOCK_DGRAM 表示 udp 
$sock = socket_create(AF_UNIX,SOCK_DGRAM,0);
// 这里特别注意 ,udp 没有连接概念,而是直接绑定地址,为什么这里需要绑定呢? 因为服务端想发数据到客户端,需要通过这个客户端绑定的sock 文件,不然无法发送
socket_bind($sock,$client_file);

$pid = pcntl_fork();

if($pid == 0){
// 子进程负责接收服务端发过来的数据
    while(1){
    	 // 接收 服务端发来数据
        $len = socket_recvfrom($sock,$buf,1024,0,$unixserverFile);
        if($len){
            fprintf(STDOUT,"收到服务端数据:%s file = %s\n",$buf, $unixserverFile);
        }
		// 接收数据为quit则退出循环,子进程退出
        if(strncasecmp($buf,"quit",4) == 0){
            break;
        }
    }
    exit(0);
}

if($pid){//父进程
    while(1){
    // 接收终端输入内容
       $data =  fread(STDIN,128);
       if($data){
       		// 通过 $server_file 服务端绑定的sock 文件 发送数据到服务端
            socket_sendto($sock,$data,strlen($data),0,$server_file);
       }

       if(strncasecmp($data,"quit",4) == 0){
            break;
       }

    }
}
socket_close($sock);

$pid = pcntl_wait($status);

fprintf(STDOUT,"子进程退出pid=%s\n",$pid);
unlink($client_file);

结语

通过上面的学习,相信大家已经详细了解了在php 中如何使用 Unix 域套接字进行通信,上面一些函数中部分参数可能并没有详细讲解,如果不明白可以到php 官网查看文档,后期也会继续推出非常重要的网络套接字通信(网络编程)案例讲解,那时会更详细的说明每个函数参数的意思用法,敬请期待,拜拜!

你可能感兴趣的:(php,php)