PHP常用socket创建TCP连接,使用CURL创建HTTP连接,为了简化操作,Swoole提供了Client类用于实现客户端功能,并增加了异步非阻塞模式,让用户在客户端也能使用事件循环。
作为客户端使用,Swoole Client可以在FPM环境下或 Apache中使用,但不允许使用Async异步模式,只能使用同步非阻塞模式,异步非阻塞模式仅限CLI模式下使用。
Client提供了TCP/UDP socket的客户端的封装代码,使用时仅需new Swoole\Client
。
Swoole的socket客户端对比PHP提供的stream族函数有什么优势呢?
-
stream
函数存在超时设置的陷阱和Bug,一旦没有处理好将会导致服务器长时间阻塞。 -
stream
和fread
有8192长度限制,无法支持UDP大包。 -
swoole_client
客户端支持waitall
,在知道包长度的情况下可以一次取完不必循环获取。 -
swoole_client
支持UDP的connect
解决了UDP串包的问题 -
swoole_client
是纯C的代码 -
swoole_client
支持异步非阻塞回调
构造方法 constructor
Swoole的Client功能封装为一个swoole_client
类,可通过new swoole_client
来创建一个客户端实例。
原型
swoole_client->__construct(
int $sock_type,
int $is_async = SWOOLEL_SOCK_SYNC,
string $key
);
参数
参数1:int $sock_type
$sock_type
表示socket
类型,可使用swoole提供的宏来指定类型。
-
SWOOLE_TCP
创建TCP Socket -
SWOOLE_TCP6
创建IPv6 TCP Socket -
SWOOLE_UDP
创建UDP Socket -
SWOOLE_UDP6
创建IPv6 UDP Socket -
SWOOLE_SSL
开启SSL加密 -
SWOOLE_KEEP
开启连接复用
其中SWOOLE_SSL
与SWOOLE_KEEP
不能单独使用,需要与前四个选项共同作用。
// 创建并开启SSL加密的TCP客户端
$client = new swoole_client(SWOOLE_TCP | SWOOLE_SSL);
SWOOLE_KEEP
swoole_client
支持在PHP-FPM或Apache中建立一个TCP长连接到服务器,当客户端启用SWOOLE_KEEP
选项后,一个请求结束不会关闭socket
,下一次再进行connect
连接时会自动复用上次创建的连接。如果执行connect
连接时发现连接已经 被服务器断开,那么connect
将会创建新的连接。
// 创建一个可以在FPM中使用的长连接客户端
$client = new swoole_client(SWOOLE_TCP | SWOOLE_KEEP);
SWOOLE_KEEP建立TCP长连接有什么优势呢?
- TCP长连接可以减少
connect
建立连接时的三次握手和close
断开连接的四次挥手所带来的额外IO开销 - 降低服务器
connect
建立连接和close
断开连接的次数
例如:在PHP-FPM模式下可以使用同步模式的swoole_client
向后端发送数据,为了能够让不同的连接复用同一个客户端client
,可以开启SWOOLE_KEEP
选项并指定对应的key
。这样对于同一个逻辑就不需要为每个请求都创建一个新的连接了。这里需要注意的是,虽然共用了同一个连接,但是每个请求仍然会创建一个新的swoole_client
实例,只是这些实例底层会复用同一个TCP连接。因此,如果开启了SWOOLE_KEEP
选项也就不能随便调用close
关闭客户端,并且在使用时可使用isConnect
方法来判断连接是否可用。
isConnect()){
$client->connect($host, $port);
}
//发送数据
$client->send($data);
//接收打印
echo $client->recv();
参数2:int $is_sync
$is_sync
表示同步阻塞或异步非阻塞,默认为同步阻塞。模式不同决定了可以使用的API的不同。
SWOOLE_SOCK_SYNC
SWOOLE_SOCK_SYNC
表示创建一个同步阻塞客户端,默认设置。
swoole_client(SWOOLE_TCP, SWOOLE_SYNC, $key);
当设定swoole_client
为同步模式后,可以像使用PHP的sockets扩展也一样使用swoole_client
来创建socket
连接。由于是同步阻塞模式,所以connect
、recv
、send
这样的方法都会阻塞进程。相比较PHP的sockets扩展提供的方法,swoole_client
的API更为简洁,使用起来更加方便。
connect($host, $port, $timeout)){
die("[connect] failed".PHP_EOL);
}
//发送数据
$message = "hello world";
if(!$client->send($message)){
die("[send] failed".PHP_EOL);
}
//接收数据
if(!$data = $client->recv()){
die("[recv] failed".PHP_EOL);
}
echo $data.PHP_EOL;
//关闭连接
$client->close();
SWOOLE_SOCK_ASYNC
SWOOLE_SOCK_ASYNC
表示创建一个异步非阻塞客户端
- 需要通过
on
方法注册异步回调函数 - 多个
swoole_client
客户端可以嵌套回调 - 异步模式仅可用于CLI命令行模式
swoole_client(SWOOLE_TCP, SWOOLE_SOCK_ASYNC)
当设置swoole_client
为异步模式后,swoole_client
就不能再使用recv
方法了,而是需要通过on
方法提供指定的回调函数,然后在回调函数当中处理。
异步模式的swoole_client
必须设置四种回调函数connect
、receive
、error
、close
,不能够缺省,否则在调用connect
连接时会提示回调未设置。同样,因为已经有了onConnect
回调,因此异步模式的swoole_client
调用connect
方法时不再阻塞,connect
方法也只会返回true
,此时需要在onConnect
回调中确定连接成功,或者在onError
回调中确定连接失败。
on("connect", function(swoole_client $client){
//向服务器发送数据
$message = "hello world".PHP_EOL;
$client->send($message);
});
//数据接收时回调
$client->on("receive", function(swoole_client $client, $data){
if(empty($data)){
$client->close();
}else{
echo "[receive] $data".PHP_EOL;
sleep(1);
$client->send(time());
}
});
//连接失败时回调
$client->on("error", function(swoole_client $client){
echo "[error] connection failed".PHP_EOL;
});
//注册关闭连接时回调
$client->on("close", function(swoole_client $client){
echo "[close] connection close".PHP_EOL;
});
//连接到服务器
$host = "127.0.0.1";
$port = 9501;
$timeout = 0.5;//超过与服务器交互的超时秒数会自动断开
if(!$client->connect($host, $port, $timeout)){
die("[connect] failed".PHP_EOL);
}
异步模式的swoole_client跟swoole_server一样以事件作为驱动,而无法发像同步模式一样来驱动。只要提供需要的事件就能过够处理逻辑,如swoole的定时器、事件循环等。
例如:实现客户端,每秒向服务器发送一个心跳包,在接收到终端输入的内容后,该客户端会将终端输入的内容发送给服务器。
服务器
$ vim server.php
set($configs);
//Master主进程 当服务器启动时触发
$serv->on("start", function(swoole_server $serv){
echo PHP_EOL."[start] master {$serv->master_pid} manager {$serv->manager_pid}".PHP_EOL;
});
//Worker工作进程 当客户端有新TCP连接时触发
$serv->on("connect", function(swoole_server $serv, $fd, $reactor_id){
echo PHP_EOL."[connect] worker {$serv->worker_pid} reactor {$reactor_id} client {$fd}".PHP_EOL;
});
//Worker工作进程 当客户端连接向服务器发送数据时触发
$serv->on("receive", function(swoole_server $serv, $fd, $reactor_id, $data){
echo "[receive] worker {$serv->worker_pid} reactor {$reactor_id} client {$fd} data:{$data}".PHP_EOL;
if($data !== "PING"){
//投递异步任务给TaskWorker异步任务进程,程序会立即返回向下执行后续代码。
$task_id = $serv->task($data);
echo "[receive] task {$task_id} data:{$data}".PHP_EOL;
//向客户端文件描述符发送字符串信息
$message = "success";
$serv->send($fd, $message);
echo "[receive] client {$fd} send:{$message}".PHP_EOL;
}
});
//TaskWorker任务进程 处理异步任务
$serv->on("task", function(swoole_server $serv, $task_id, $reactor_id, $data){
echo "[task] task {$task_id} reactor {$reactor_id} data:{$data}".PHP_EOL;
sleep(10);//模拟异步操作执行时长10秒
//返回任务执行的结果
$serv->finish("finish");
});
//Worker进程 处理异步任务完成的结果
$serv->on("finish", function(swoole_server $serv, $task_id, $data){
echo "[finish] worker {$serv->worker_pid} task {$task_id} data:{$data}".PHP_EOL;
});
//Worker进程 监听断开连接时触发
$serv->on("close", function(swoole_server $serv, $fd, $reactor_id){
//连接断开类型
$disconnect_type = "client";//客户端主动断开
if($reactor_id < 0){
$disconnect_type = "server";//服务器主动断开
}
echo "[close] worker {$serv->manager_pid} client {$fd} disconnect {$disconnect_type}".PHP_EOL;
});
//启动服务器
$serv->start();
客户端
$ vim client.php
client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
//绑定事件回调函数
$this->client->on("Connect", [$this, "onConnect"]);
$this->client->on("Receive", [$this, "onReceive"]);
$this->client->on("Close", [$this, "onClose"]);
$this->client->on("Error", [$this, "onError"]);
//连接服务器
$this->client->connect($host, $port, $timeout=1);
}
/**连接服务器 */
public function connect($host, $port, $timeout=1)
{
//连接服务器
$fp = $this->client->connect($host, $port, $timeout);
if(!$fp){
echo "[connect] error{$fp->errCode} {$fp->errMsg}".PHP_EOL;
return;
}
}
/**连接成功回调 */
public function onConnect($client)
{
//将标准输入添加到swoole的事件监听中
swoole_event_add(STDIN, function($fp){
//读取标准输入
$message = trim(fgets(STDIN));
if(!empty($message)){
//发送数据
$this->client->send($message);
}
});
//添加定时器,每秒向服务器发送一个心跳包
$this->timer_id = swoole_timer_tick(1000, function(){
$this->client->send("PING");
});
}
public function send($message)
{
$this->client->send($message);
}
public function onReceive(swoole_client $client, $data)
{
echo "[receive] {$data}".PHP_EOL;
}
public function onClose(swoole_client $client)
{
$data = json_encode($client);
echo "[close] {$data}".PHP_EOL;
//断开连接时清除定时器
swoole_timer_clear($this->timer_id);
}
public function onError(swoole_client $client)
{
$data = json_encode($client);
echo "[error] {$data}".PHP_EOL;
}
}
$client = new Client("127.0.0.1", 9501, 1);
参数3:string $key
$key
表示用于长连接的key
,默认使用IP:PORT
作为key
,相同key
的连接会被复用。
对象属性
{
"errCode":0,
"sock":-1,
"reuse":false,
"reuseCount":0,
"type":1025,
"id":null,
"setting":null
}
错误码 int swoole_client->errCode
当connect
、send
、recv
、close
失败时会自动设置$swoole_client->errCode
的值,errCode
的值等于Linux的errno
,可以使用socket_strerror
将错误码转换为错误信息。
$client->on("close", function(swoole_client $client){
if(!$client->errCode){
$error = socket_strerror($client->errCode);
echo "error: {$error}".PHP_EOL;
}
echo "close".PHP_EOL;
});
Socket文件描述符 int swoole_client->sock
sock
属性值为整型的socket
文件描述符,可转换int
作为数组的key
。
// 在PHP代码中可以使用
$sock = fopen("php://fd/".$swoole_client->sock);
-
$client->sock
属性值仅在$client->connect
后才能取出,在未连接服务器之前此属性值为null
。 - 将
swoole_client
的socket
转换为一个stream socket
可调用fread
、fwrite
、fclose
等函数进行操作。 -
swoole_server
中的$fd
文件描述符不能使用sock()
方法转换,因为$fd
文件描述符只是一个属于主进程的数字。
是否复用 bool swoole_client->reuse
reuse
属性值表示连接是新连接的还是复用已存在的,是一个布尔值,需要与SWOOLE_KEEP
配合使用。
例如:WebSocket
客户端与服务器建立连接后需要进行握手,如果连接是复用的,就无需再次进行握手,直接发送WebSocket
数据帧即可。
if($client->reuse){
$client->send($data);
}
建立连接 connect
connect
方法用于建立连接到远程服务器
原型
bool $server_client->connect(
string $host,
int $port,
float $timeout = 0.5,
int $flag = 0
);
参数
参数1:string $host
表示远程服务器的地址,swoole1.10.0+已支持自动异步解析域名即可直接传入域名。
参数2:int $port
表示远程服务器的端口
参数3:float $timeout
表示网络IO超时秒数,包括connect
、send
、recv
,单位为秒并支持浮点数,默认0.5s即500ms。
参数4:int $flag
- 在UDP类型时表示是否启用
udp_connect
设定此选项后将绑定$host
与$port
,此UDP将会丢弃非指定host/port
的数据包。 - 在TCP类型中
$flag=1
表示设置为非阻塞socket,connect会立即返回。如果将$flag
设置为1则在send/recv
前必须使用swoole_client_select
来检测是否完成了连接。
模式
- 同步模式
connect
方法在同步模式下会发生阻塞,直到连接成功并返回true
。这个时候就可以向服务器发送数据或接收数据了。如果连接失败则会返回false
。另外,同步TCP客户端在执行close
之后,可以再次发起connect
创建新连接到服务器。
if($client->connect($host, $port)){
$client->send($message);
}else{
echo "connect failed";
}
- 异步模式
异步模式下connect
连接会立即返回true
,但实际上连接并未建立,所以不能在connect
后使用send
。此时使用fsConnected()
判断也是false
。当连接创建成功后,系统会自动回调onConnect
,此时才可以使用send
方法向服务器发送数据。
异步客户端执行connect
时会增加一次引用计数,当连接关闭时会减少引用计数。
版本
- 小于swoole1.9.11版本中
$timeout
超时设置在异步客户端中是无效的,应用层需要使用Timer::after
自行添加定时器来实现异步客户端的链接超时控制。 - 大于等于swoole1.9.11版本中,底层会自动添加定时器,在规定的时间内未连接成功时,底层会触发
onError
连接失败事件,错误码为ETIMEOUT(110)
。
失败重连
connect
失败后如果希望重连一次,必须先进行close
关闭旧的socket
,否则会返回EINPROCESS
错误,因为当前的socket正在连接服务器,客户端并不知道是否连接成功,所以无法再次执行connect
。
调用close
会关闭当前的socket
,底层会重新创建新的socket
来进行连接。
启用SWOOLE_KEEP
长连接后,close
调用的第一个参数需要设置为true
来表示强行销毁长连接socket
。
//连接失败
if($socket->connect($host, $port) === false){
$socket->close(true);//关闭旧的socket
$socket->connect($host, $port);//重连
}
UDP连接
UDP连接时默认底层并不会启用,一个UDP客户端执行连接时,底层在创建socket后会立即返回成功,此时的socket绑定的地址为0.0.0.0,任何其它对端均可向此端口发送数据包。
$client->connect("192.168.1.100", 9502)
此时操作系统会为客户端socket随机分配一个端口,其它机器可向这个端口发送数据包。
未开启UDP连接时调用getsockname
返回的host
为0.0.0.0
。
将connect
的第四项参数$flag
设置为1时将启用UDP连接。
$client->connect("192.168.1.100", 9501, 1, 1)
此时将会绑定客户端和服务器,底层会根据服务器的地址来绑定socket绑定的地址,例如连接了192.168.1.100,当前socket会被绑定到192.168.1.*的本地机器上。启用UDP连接后客户端将不再接收其它主机向此端口发送的数据包。
发送数据 send
send
方法用于建立连接后发送数据到远程服务器
原型
int $swoole_client->send(string $data);
参数
参数:string $data
发送的数据,格式为字符串,支持二进制。
返回
- 成功发送返回已发送数据的长度
- 失败返回
false
,并设置$swoole_client->errCode
错误码。
模式
- 同步
- 发送的数据没有长度闲置
- 发送的数据太多时socket缓存区塞满,底层会阻塞等待可写。
- 异步
- 发送数据的长度收到
socket_buffer_size
限制 - 如果socket缓存区已满,swoole的处理逻辑参考
swoole_event_write
。
注意
- 如果没有执行连接
connect
直接调用send
会触发PHP警告
接收数据 recv
recv
方法用于从服务器接收数据
原型
swoole1.7.22-
string $swoole_client->recv(int $size = 655335, bool $waitall = 0)
swoole1.7.22+
string $swoole_client->recv(int $size = 65535, int $flag = 0);
swoole1.7.22版本后,将原来第二个参数$waitall
参数修改为$flags
可以接收一些特殊的socket接收设置,为了兼容旧的接口,如果$flag = 1
表示 $flag = swoole_client::MSG_WAITALL
。
$client->recv(8192, swoole_client::MSG_PEEK | swoole_client::MSG_WAITALL);
参数
-
int $size
表示接收数据的缓存区最大长度,此参数设置过大会占用较大内存。 -
bool $waitall
表示是否等待所有数据到达后返回
如果设置了$waitall
就必须设定准确的$size
否则会一直等待,直到接收的数据长度达到$size
。如果未设置$waitall = true
时$size
最大未64K,如果设置了错误的$size
将会导致recv
超时而返回false
。
返回
- 成功:成功接收到数据则返回字符串
- 失败:返回
false
需设置错误码$client->errCode
属性 - 连接关闭:返回空字符串
EOL/Length
如果客户端启用了EOF/Length检测后,无需设置$size
和$waitall
参数,扩展层会返回完整的数据包或返回false
。当接收到错误的包头或包头中长度超过package_max_length
设置时,recv
会返回空字符串,PHP代码中应当关闭此连接。
关闭连接 close
close
用于关闭客户端连接,操作成功后返回true
。当swoole_client
客户端连接被close
关闭后不要再次发起连接close
。正确的做法是销毁当前的swoole_client
,然后再重新创建一个swoole_client
并发起新的连接。
bool $swoole_client->close(bool $force = false);
参数:bool $force
表示是否强制关闭连接,可用于关闭SWOOLE_KEEP
长连接。
注意:swoole_client
对象在析构时会自动关闭
异步非阻塞客户端close
关闭时会立即关闭连接,如果发送队列中仍然有数据底层会丢弃。所以请勿在大量发送数据后立即close
关闭,否则发送的数据未必能真正到达服务器端。
服务器
$ vim server.php
set($configs);
//Master主进程 当服务器启动时触发
$server->on("start", function(swoole_server $server){
echo PHP_EOL."[start] master {$server->master_pid} manager {$server->manager_pid}".PHP_EOL;
});
//Worker工作进程 当客户端有新TCP连接时触发
$server->on("connect", function(swoole_server $server, $fd, $reactor_id){
echo PHP_EOL."[connect] worker {$server->worker_pid} reactor {$reactor_id} client {$fd}".PHP_EOL;
});
//Worker工作进程 当客户端连接向服务器发送数据时触发
$server->on("receive", function(swoole_server $server, $fd, $reactor_id, $data){
echo "[receive] worker {$server->worker_pid} reactor {$reactor_id} client {$fd} data:{$data}".PHP_EOL;
if($data !== "PING"){
//投递异步任务给TaskWorker异步任务进程,程序会立即返回向下执行后续代码。
$task_id = $server->task($data);
echo "[receive] task {$task_id} data:{$data}".PHP_EOL;
//向客户端文件描述符发送字符串信息
$message = "success";
$server->send($fd, $message);
echo "[receive] client {$fd} send:{$message}".PHP_EOL;
}
});
//TaskWorker任务进程 处理异步任务
$server->on("task", function(swoole_server $server, $task_id, $reactor_id, $data){
echo "[task] task {$task_id} reactor {$reactor_id} data:{$data}".PHP_EOL;
sleep(10);//模拟异步操作执行时长10秒
//返回任务执行的结果
$server->finish("finish");
});
//Worker进程 处理异步任务完成的结果
$server->on("finish", function(swoole_server $server, $task_id, $data){
echo "[finish] worker {$server->worker_pid} task {$task_id} data:{$data}".PHP_EOL;
});
//Worker进程 监听断开连接时触发
$server->on("close", function(swoole_server $server, $fd, $reactor_id){
//连接断开类型
$disconnect_type = "client";//客户端主动断开
if($reactor_id < 0){
$disconnect_type = "server";//服务器主动断开
}
echo "[close] worker {$server->manager_pid} client {$fd} disconnect {$disconnect_type}".PHP_EOL;
});
//启动服务器
$server->start();
客户端
$ vim client.php
客户端发送4MB的数据,实际传输可能需要一段时间,此时如果立即进行close
关闭操作,可能只有小部分数据传输成功,大部分数据在发送队列中排队等待发送,close
关闭时会丢失这些数据。
on("connect", function(swoole_client $client){
echo "connect".PHP_EOL;
//将标准输入添加到事件监听中
swoole_event_add(STDIN, function($fp) use($client){
$msg = trim(fgets(STDIN));
$client->send($msg);
});
});
$client->on("receive", function(swoole_client $client, $data){
echo "receive:{$data}".PHP_EOL;
$client->send(str_repeat("X", 1024*1024*4).PHP_EOL);
$client->close();
});
$client->on("error", function(swoole_client $client){
echo "error".PHP_EOL;
});
$client->on("close", function(swoole_client $client){
echo "close".PHP_EOL;
});
$client->connect("127.0.0.1", 9501);
解决方案
- 配合使用
onBufferEmpty
等待发送队列为空时进行close
操作
on("connect", function(swoole_client $client){
echo "connect".PHP_EOL;
//将标准输入添加到事件监听中
swoole_event_add(STDIN, function($fp) use($client){
$msg = trim(fgets(STDIN));
$client->send($msg);
});
});
$client->on("receive", function(swoole_client $client, $data){
echo "receive:{$data}".PHP_EOL;
$client->send(str_repeat("X", 1024*1024*4).PHP_EOL);
$client->close();
});
$client->on("error", function(swoole_client $client){
echo "error".PHP_EOL;
});
$client->on("close", function(swoole_client $client){
echo "close".PHP_EOL;
});
//配合使用onBufferEmpty等待发送队列为空时进行关闭close操作
$client->on("bufferEmpty", function(swoole_client $client){
$client->close();
});
$client->connect("127.0.0.1", 9501);
- 协议设计为
onReceive
收到数据后主动关闭连接,发送数据时对端主动关闭连接。