swoole server

server,顾名思义,就是服务端。我们平时接触比较多的无非就是nginx和apache。作为webServer,二者都是通过监听某端口对外提供服务,swoole的server也不例外同样需要绑定端口,同时能够提供给客户端相关的服务。

创建一个server对象

创建server的步骤:

1、实例化Server对象
2、设置运行时参数
3、注册事件回调函数
4、启动服务器
 1$serv=new Swoole\Server('0.0.0.0',9800);
 2$serv->set([
 3    "worker_num"=>2,//设置进程 CPU核数的1-4倍。
 4]);
 5$serv->on('connect',function($serv,$fd){
 6    echo '新的客户端链接, 链接标志为'.$fd.PHP_EOL;
 7});
 8//监听数据接收事件,server 接收到客户端的数据后,worker进程内发该回调
 9$serv->on("receive",function(swoole_server $server, int $fd, int $reactor_id, string $data){
10   echo '消息发送过了'.PHP_EOL;
11    $server->send($fd,"服务器给你发送消息了".$data);
12});
13//监听链接关闭事件,客户端关闭或者服务器主动关闭
14$serv->on('close',function($serv,$fd){
15    echo '编号'.$fd.'连接结束了'.PHP_EOL;
16});
17//启动服务器
18$serv->start();

server的创建,只需要绑定要监听的ip和端口,如果ip指定为127.0.0.1,则表示客户端只能位于本机才能连接,其他计算机无法连接,如果需要所有的客户端都能连接则可以设置0.0.0.0

配置

在享受swoole的server之前,同样我们需要配置一下server,比如调几个人来提供服务(几个进程),以及是否是后台执行(守护进程)等等一些其它的配置。

这个就需要我们做一些配置了,但是并非像fpm直接在文件内配置,我们可以在server创建后,通过set方法指定配置项。

同时,这个配置项也有很多,比如说我们可以指定日志文件记录具体的错误信息等等

这里我们首要说明一下worker进程数的配置,因为swoole是多进程的异步服务器所以需要设置工作进程数,提升服务器性能。

1$serv=new Swoole\Server('0.0.0.0',9800);
2$serv->set([
3    "worker_num"=>2,//设置进程 CPU核数的1-4倍。 相同cpu数即可
4]);

默认该参数的值等于你机器的CPU核数。

事件驱动

swoole另外一个比较吸引人的地方,就是swoole_server是事件驱动的。我们在使用的过程中不需要关注底层是怎么实现的,底层是C写的php只是做了个传递的作用,所以只需要对底层相应的动作注册相应的回调,在回调函数中处理业务逻辑即可。

什么意思呢?我举个例子:

你启动了一个server,当客户端连接的时候(触发事件),你不需要关心它是怎么连接的,你就单纯的注册一个connect函数,做一些连接后的业务处理即可(执行业务)。类似于js的事件监听,比如触发了click事件,就会执行相应的闭包。

1Swoole监听的事件:
2参数$serv是我们一开始创建的swoole_server对象,
3参数$fd是唯一标识,用于区分不同的客户端,同时该参数是1-1600万之间可以复用的整数。简单解释下复用:假设现在客户端1、2、3处于连接中,客户端4要连接的话$fd就是4,但是不巧的是客户端3连接不稳定,断掉了,客户端4连接到server的话,$fd就是3,这样看的话。
41600W个连接够用吗?单机业务百万连接,已经是很厉害了,不用担心

客户端连接事件

1$serv->on('connect', function ($serv, $fd) {   
2			echo '新的客户端链接, 链接标志为' . $fd . PHP_EOL;
3});

监听客户端数据发送,触发回调

1//监听数据接收事件,server 接收到客户端的数据后,worker进程内发该回调
2$serv->on("receive", function (swoole_server $server, int $fd, int $reactor_id, string $data) {
3    echo '消息发送过了' . PHP_EOL;
4    $server->send($fd, "服务器A给你发送服务器B消息了" . $data);
5});

worker进程内触发的。第三个参数$reactor_id指的是哪一个reactor线程。

我们看第四个参数,这个参数就是服务端接受到的数据,注意是字符串或者二进制内容,注意我们在Receive回调内,调用了$serv的send方法,我们可以使用send方法,向client发起通知。

监听客户端关闭,触发回调

1//监听链接关闭事件,客户端关闭或者服务器主动关闭
2$serv->on('close', function ($serv, $fd) {
3    echo '编号' . $fd . '连接结束了' . PHP_EOL;
4});

这个很简单,当客户端关闭,或者服务端主动关闭连接的时候会触发。 到此呢,我们基本上已经搭建到了一个高性能的server。当然,非常简单,下面我们只需要调用start方法启动server即可。

1//启动服务器
2$serv->start();

swoole_server只能运行在CLI模式下

1#php swoole_server.php     // 运行server
2netstat -an | grep 9800    // 监测是否有9800端口

同步client跟异步client

默认的swoole的server是可以提供tcp/udp socket请求协议,然后根据请求数据,执行相应的逻辑

在PHP中,我们常用socket函数来创建TCP连接,用CURL库来创建Http连接。同样的,为了简化操作,Swoole也提供了同样的Client类用于实现客户端的功能,并且增加了异步非阻塞的模式,让用户在客户端也能使用事件循环。

swoole_client的构造函数如下所示:

 1swoole_client->__construct(int $sock_type, int $is_sync = SWOOLE_SOCK_SYNC, string $key);
 2第一个参数:
 3SWOOLE_SOCK_TCP 创建tcp socket
 4SWOOLE_SOCK_TCP6 创建tcp ipv6 socket
 5SWOOLE_SOCK_UDP 创建udp socket
 6SWOOLE_SOCK_UDP6 创建udp ipv6 socket
 7第二个参数表示是同步还是异步
 8SWOOLE_SOCK_SYNC 同步客户端
 9SWOOLE_SOCK_ASYNC 异步客户端
10第三个参数(暂不了解)
11用于长连接的Key,默认使用IP:PORT作为key。相同key的连接会被复用

新建一个同步客户端

代码如下:

 1$client = new swoole_client(SWOOLE_SOCK_TCP);
 2//建立连接
 3if (!$client->connect('127.0.0.1', 9800, -1)) {
 4    exit("connect failed. Error: {$client->errCode}\n");
 5}
 6//想服务端发送数据
 7$client->send("hello world\n");
 8//接受服务端发过来的数据
 9$reponse = $client->recv();
10echo $reponse;
11//关闭连接
12$client->close();

同步client是同步阻塞的。一整套connect->send()->rev()->close()是同步进行的。如果需要大量的数据处理,后台不能在规定的时间内返回数据会导致接收超时,并且因为是同步执行所以需要等待后台数据的返回。

异步客户端

当设定swoole_client为异步模式后,swoole_client就不能使用recv方法了,而需要通过on方法提供指定的回调函数,然后在回调函数当中处理

 1<?php
 2//第二个参数表示是同步还是异步
 3//SWOOLE_SOCK_SYNC 同步客户端
 4//SWOOLE_SOCK_ASYNC 异步客户端
 5$client = new Swoole\Client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
 6$client->on("connect", function(swoole_client $cli) {
 7		$cli->send("GET / HTTP/1.1\r\n\r\n");
 8});
 9$client->on("receive", function(swoole_client $cli, $data){
10		echo "Receive: $data";
11});
12$client->on("error", function(swoole_client $cli){
13		echo "error\n";
14});
15$client->on("close", function(swoole_client $cli){
16   		echo "Connection close\n";
17});
18$client->connect('127.0.0.1', 9800) ||  exit("connect failed. Error: {$client->errCode}\n");

心跳检测

顾名思义,心跳是判断一个事物生还是死的一个标准,在swoole里,心跳是指用来判断一个连接是正常还是断开的

为什么要心跳?

在从客户端到服务器的一条巨大的链路中会经过无数的路由器,其中每一个路由器都有可能会有检测到多少秒时间内无数据包则自动关闭连接的这种节能机制,为了让这个可能会出现的节能机制失效,客户端可以设置一个定时器,每隔固定的时间都发一个随机字符的一字节的数据包,通常我们把这种数据包就叫做心跳包。

心跳的目的其实是通过判断客户端是否存活,从而回收fd(文件描述符),系统为什么要回收fd,因为fd资源是有限的,所以必需重复利用

心跳作用主要有两个:

1、客户端定时给服务端发送点数据,防止连接由于长时间没有通讯而被某些节点的防火墙关闭导致连接断开的情况。

2、服务端可以通过心跳来判断客户端是否在线,如果客户端在规定时间内没有发来任何数据,就认为客户端下线。这样可以检测到客户端由于极端情况(断电、断网等)下线的事件。

配置建议

建议 heartbeat_idle_time(TCP连接的最大闲置时间) 为 heartbeat_check_interval(每隔多少秒检测一次) 的两倍多一点。

心跳在swoole里的实现

swoole会在主进程独立起一个心跳线程,通过定时轮询所有的连接,来判断连接的生死,所以swoole的心跳不会堵塞任何业务逻辑。

server端心跳

设置完成了之后,你会发现设置了定时检测之后,如果客户端没在规定的时间之内发送数据就会关闭。

heartbeat_check_interval: 服务器定时检测在线列表的时间

heartbeat_idle_time: 连接最大的空闲时间 (如果最后一个心跳包的时间与当前时间之差超过这个值,则认为该连接失效)

 1<?php
 2
 3$serv=new Swoole\Server('0.0.0.0',9800);
 4$serv->set([
 5    "worker_num"=>2,//设置进程 CPU核数的1-4倍。
 6    'heartbeat_check_interval'=>3, //心跳检测机制  //每隔多少秒检测一次,
 7     //单位秒,Swoole会轮询所有TCP连接,将超过心跳时间的连接关闭掉
 8    'heartbeat_idle_time' => 10 //TCP连接的最大闲置时间,单位s ,
 9    // 如果某fd最后一次发包距离现在的时间超过heartbeat_idle_time会把这个连接关闭。
10
11
12]);
13$serv->on('connect',function($serv,$fd){
14    echo '新的客户端链接, 链接标志为'.$fd.PHP_EOL;
15});
16
17
18//监听数据接收事件,server 接收到客户端的数据后,worker进程内发该回调
19$serv->on("receive",function(swoole_server $server, int $fd, 
20                     int $reactor_id, string $data){
21   echo '消息发送过了'.PHP_EOL;
22    $server->send($fd,"服务器给你发送消息了".$data);
23});
24
25
26//监听链接关闭事件,客户端关闭或者服务器主动关闭
27$serv->on('close',function($serv,$fd){
28    echo '编号'.$fd.'连接结束了'.PHP_EOL;
29});
30
31
32//启动服务器
33$serv->start();

异步客户端心跳

 1<?php
 2$client = new Swoole\Client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
 3$client->on("connect", function(swoole_client $cli) {
 4$cli->send("GET / HTTP/1.1\r\n\r\n");
 5});
 6$client->on("receive", function(swoole_client $cli, $data){
 7echo "Receive: $data";
 8//$cli->send(str_repeat('A', 100)."\n");
 9//sleep(14);
10});
11$client->on("error", function(swoole_client $cli){
12echo "error\n";
13});
14$client->on("close", function(swoole_client $cli){
15echo "Connection close\n";
16});
17$client->connect('127.0.0.1', 9800);
18
19//定时器,保持长连接
20swoole_timer_tick(9000,function () use($client){
21    $client->send('1');  //9秒自动发送消息给服务端,
22});
23
24echo "写日志";

tcp断粘包问题及swoole处理方案

tcp断粘包处理

TCP通信特点

  • 1.TCP 是流式协议没有消息边界,客户端向服务器端发送一次数据,可能会被服务器端分成多次收到。客户端向服务器端发送多少数据。服务器端可能一次全部收到。
  • 2.保证传输的可靠性,顺序。
  • 3.TCP拥有拥塞控制,所以数据包可能会延后发送。

没有消息边界

可以理解为水在一个水管里的流动,我们不知道哪段数据是一个我们需要的完整数据

收发有缓冲区

比如:当水从一端流到了另一端,我们在收数据的时候,不可能每来一滴水就处理一次,这个缓冲区就相当于有了一个水桶,再接了一定的水之后内核再给数据交到用户空间,这样可以大大提升性能。

什么是 TCP 粘包

TCP 粘包是指发送方发送的若干包数据 到 接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

TCP 出现粘包的原因?

发送方:发送方需要等缓冲区满才发送出去,造成粘包.

接收方:接收方不及时接收缓冲区的包,造成多个包接收.

模拟下产生问题的场景,下面的结果确实是出现了粘包的问题

Swoole粘包方案之 EOF结束协议

通过约定结束符,来确定包数据是否发送完毕

开启open_eof_check=true,并用package_eof来设置一个完整数据结尾字符,同时设置自动拆分open_eof_split

注意:

  • 1、要保证业务数据里不能出现package_eof设置的字符,否则将导致数据错误了。
  • 2、可以手动拆包,去掉open_eof_split,自行 explode(“\r\n”, $data),然后循环发送
 1<?php
 2//server代码
 3$serv = new Swoole\Server('0.0.0.0', 9500);
 4$serv->set([
 5    "worker_num"     => 2,//设置进程 CPU核数的1-4倍。
 6    "open_eof_check" => true,//打开eof检测
 7    "package_eof"    => "\r\n",//设置eof
 8    "open_eof_split" => true, //开启自动拆分
 9]);
10$serv->on('connect', function ($serv, $fd) {
11    echo '新的客户端链接, 链接标志为' . $fd . PHP_EOL;
12});
13//监听数据接收事件,server 接收到客户端的数据后,worker进程内发该回调
14$serv->on("receive", function (swoole_server $server, int $fd, int $reactor_id, string $data) {
15  //    $data= explode("\r\n",$data);
16//    foreach($data as $v){
17//        if(empty($v)) continue;
18        $server->send($fd, "服务器给你发送消息了" . $data."\n"); //自动拆分
19//    }
20});
21//监听链接关闭事件,客户端关闭或者服务器主动关闭
22$serv->on('close', function ($serv, $fd) {
23    echo '编号' . $fd . '连接结束了' . PHP_EOL;
24});
25//启动服务器
26$serv->start();
 1<?php
 2//异步tcp客户端
 3$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
 4//自行做一下
 5$client->set([
 6]);
 7//连接服务端
 8$client->on("connect", function(swoole_client $cli) {
 9    for($i=0;$i<5;$i++){
10        $cli->send("山东队实力但是看到深刻的思考可是打开电视看收到快递时说收到快递时\r\n");
11    }
12});
13//接收到服务端发送的消息时触发的
14$client->on('receive', function ($cli, $data) {
15    //客户端接收也会有粘包的问题
16  echo $data;
17});
18$client->on('error', function ($cli) {
19});
20//监听连接关闭事件,客服端关闭,或者服务器主动关闭
21$client->on('close', function ($cli) {
22});
23//先绑定事件之后随后建立连接,连接失败直接退出并打印错误码
24$client->connect('127.0.0.1', 9500) || exit("connect failed. Error: {$client->errCode}\n");

swoole 固定包头+包体协议(推荐)

这种方式也非常常见,原理是通过约定数据流的前几个字节来表示一个完整的数据有多长,从第一个数据到达之后,先通过读取固定的几个字节,解出数据包的长度,然后按这个长度继续取出后面的数据,依次循环。

相关配置:

open_length_check:打开包长检测特性

package_length_type:长度字段的类型,固定包头中用一个4字节或2字节表示包体长度。

package_length_offset:从第几个字节开始是长度,比如包头长度为120字节,第10个字节为长度值,这里填入9(从0开始计数)

package_body_offset:从第几个字节开始计算长度,比如包头为长度为120字节,包体长度为1000。如果长度包含包头,这里填入0,如果不包含包头,这里填入120

package_max_length:最大允许的包长度。因为在一个请求包完整接收前,需要将所有数据保存在内存中,所以需要做保护。避免内存占用过大。

package_length_type 长度值的类型

长度值的类型,接受一个字符参数,与php的pack函数一致。目前swoole支持10种类型:

 1c:有符号、1字节
 2C:无符号、1字节
 3s:有符号、主机字节序、2字节
 4S:无符号、主机字节序、2字节
 5n:无符号、网络字节序、2字节 (常用)
 6N:无符号、网络字节序、4字节 (常用)
 7l:有符号、主机字节序、4字节(小写L)
 8L:无符号、主机字节序、4字节(大写L)
 9v:无符号、小端字节序、2字节
10V:无符号、小端字节序、4字节

字节序的理解

计算机硬件有两种储存数据的方式:大端字节序(big endian)和小端字节序(little endian)。

大端字节序:高位字节在前,低位字节在后,这是人类读写数值的方法。

小端字节序:低位字节在前,高位字节在后,即以0x1122形式储存。

为什么要有字节序呢?

计算机电路先处理低位字节,效率比较高,因为计算都是从低位开始的。所以,计算机的内部处理都是小端字节序。

不过,人类还是习惯读写大端字节序。所以,除了计算机的内部处理,其他的场合几乎都是大端字节序,比如网络传输和文件储存。

所以我们要确定通信双方交流的信息单元应该以什么样的顺序进行传送。如果达不成一致的规则,计算机的通信与存储将会无法进行

server代码:

 1 <?php
 2$serv = new Swoole\Server('0.0.0.0', 9500);
 3$serv->set([
 4    "worker_num"            => 2,//设置进程 CPU核数的1-4倍。
 5    //固定包头跟包体协议
 6    'open_length_check'     => true,
 7    'package_length_type'   => 'N',
 8    'package_length_offset' => 0, //开始计算总长度
 9    'package_body_offset'   => 4,//包体位置
10    'package_max_length'    => 1024 * 1024 //总的请求数据大小字节为单位 1mb
11]);
12$serv->on('connect', function ($serv, $fd) {
13    echo '新的客户端链接, 链接标志为' . $fd . PHP_EOL;
14});
15//监听数据接收事件,server 接收到客户端的数据后,worker进程内发该回调
16$serv->on("receive", function (swoole_server $server, int $fd, int $reactor_id, string $data) {
17    //得到包体长度
18    $len=unpack('N',$data)[1];
19    $body=substr($data,-$len);//去除二进制数据之后,不要包头的数据
20    $server->send($fd, "服务器给你发送消息了: ".$body.PHP_EOL);
21});
22//监听链接关闭事件,客户端关闭或者服务器主动关闭
23$serv->on('close', function ($serv, $fd) {
24    echo '编号' . $fd . '连接结束了' . PHP_EOL;
25});
26//启动服务器
27$serv->start();

client代码:

 1 <?php
 2//异步tcp客户端
 3$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
 4//同理  如果 服务端发数据给客户端, 客户端也要做固定包头+包体协议处理 
 5$client->set([
 6]);
 7//连接服务端
 8$client->on("connect", function(swoole_client $cli) {
 9    $data="异步客户端数据";
10    //包头(length:是包体长度)+包体
11    $packge=pack('N',strlen($data)).$data; //二进制+数据内容
12    //var_dump($packge);
13    //连续发送5条数据
14    for ($i=0;$i<5;$i++){
15        $cli->send($packge);
16    }
17});
18//接收到服务端发送的消息时触发的
19$client->on('receive', function ($cli, $data) {
20    //粘包的问题作业
21    echo $data;
22});
23$client->on('error', function ($cli) {
24});
25//监听连接关闭事件,客服端关闭,或者服务器主动关闭
26$client->on('close', function ($cli) {
27});
28//先绑定事件之后随后建立连接,连接失败直接退出并打印错误码
29$client->connect('127.0.0.1', 9500) || exit("connect failed. Error: {$client->errCode}\n");