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函数创建的资源流上下文
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342