# Swoole的自定义协议功能的使用 --- [TOC=3,4] --- ### **1.为什么要提供自定义协议** 熟悉TCP通信的朋友都会知道,TCP是一个流式协议。客户端向服务器发送的一段数据,可能并不会被服务器一次就完整的收到;客户端向服务器发送的多段数据,可能服务器一次就收到了全部的数据。而实际应用中,我们希望在服务器端能一次接收**一段完整的**数据,不多也不少。传统的TCP服务器中,往往需要由程序员维护一个缓存区,先将读到的数据放进缓存区中,然后再通过预先设定好的协议内容,来区分一段完整数据的开头、长度和结尾,并将一段完整的数据交给逻辑部分处理。这就是自定义协议的功能。 而在Swoole中,已经在底层实现了一个数据缓存区,并内置好了几个常用的协议类型,直接在底层做好了数据的拆分,保证了在onReceive回调函数中,一定能收到一个(或数个)完整的数据段。数据缓存区的大小可以通过配置选项[package_max_length](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#19package_max_length)来控制。下面我就将讲解如何使用这些内置协议。 ### **2.EOF标记型协议** 第一个比较常用的协议就是EOF标记协议。协议的内容是通过规定一个一定不会出现在正常数据中的字符或者字符串,用这个来标记一段完整数据的结尾。这样,只要发现这个结尾,就可以认定之前的数据已经结束,可以开始接收一个新的数据段了。<br> 在Swoole中,可以通过[open_eof_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#13open_eof_check)和[package_eof](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#14package_eof)两个配置项来开启。其中,[open_eof_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#13open_eof_check)指定开启了EOF检测,[package_eof](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#14package_eof)指定了具体的EOF标记。通过这两个选项,Swoole底层就会自动根据EOF标记来缓存和拆分收到的数据包。示例如下: ```php $this->serv->set(array( 'package_max_length' => 8192, 'open_eof_check'=> true, 'package_eof' => "\r\n" )); ``` 就这样,swoole就已经开启了EOF标记协议的解析。那么让我们来测试一下效果:<br> 服务器这边:<br> ```php // Server public function onReceive( swoole_server $serv, $fd, $from_id, $data ) { echo "Get Message From Client {$fd}:{$data}\n"; } ``` 客户端这边:<br> ```php $msg_eof = "This is a Msg\r\n"; $i = 0; while( $i < 100 ) { $this->client->send( $msg_eof ); $i ++; } ``` 然后运行一下,你会发现:哎不对啊,为什么还是一次收到了好多数据啊!<br> 这是因为,在Swoole中,采用的不是遍历识别的方法,而只是简单的检查每一次接收到的数据的末尾是不是定义好的EOF标记。因此,在开启EOF检测后,onReceive回调中还是可能会一次收到多个数据包。<br> 这要怎么办?你会发现,虽然是多个数据包,但是实际上收到的是N个完整的数据片段,那就只需要根据EOF把每个包再拆出来,一个个处理就好啦。<br> 修改后的服务器端代码如下:<br> ```php public function onReceive( swoole_server $serv, $fd, $from_id, $data ) { $data_list = explode("\r\n", $data); foreach ($data_list as $msg) { if( !empty($msg) ) { echo "Get Message From Client {$fd}:{$msg}\n"; } } } ``` 再次运行,妥了~<br> [点此查看完整实例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_eof_server.php)<br> 另外,如果担心这样运行多段数据会长时间占用Worker,可以采用把数据+fd转发给Task进程的做法。如何转发请读者自己尝试实现。<br> ### **3.固定包头类型协议** 固定包头协议是在实际应用中最常用的协议。该协议的内容是规定一个固定长度的包头,在包头的固定位置有一个指定好的字段存放了后续数据的实际长度。这样,服务器端可以先读取固定长度的数据,从中提取出长度,然后再读取指定长度的数据,即可获取一段完整的数据。<br> 在Swoole中,同样提供了固定包头的协议格式。需要注意的是,Swoole只允许二进制形式的包头,因此,需要使用pack、unpack来打包、解包。<br> 通过设置[open_length_check](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#15open_length_check)选项,即可打开固定包头协议解析功能。此外还有[package_length_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#16package_length_offset),[package_body_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#17package_body_offset)和[package_length_type](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#18package_length_type)三个配置项用于控制解析功能。[package_length_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#16package_length_offset)规定了包头中第几个字节开始是长度字段,[package_body_offset](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#17package_body_offset)规定了包头的长度,[package_length_type](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server%E9%85%8D%E7%BD%AE%E9%80%89%E9%A1%B9.md#18package_length_type)规定了长度字段的类型。<br> 具体设置如下:<br> ```php $this->serv->set(array( 'package_max_length' => 8192, 'open_length_check'=> true, 'package_length_offset' => 0, 'package_body_offset' => 4, 'package_length_type' => 'N' )); ``` 具体如何设置这些参数请参考[文档](https://github.com/LinkedDestiny/swoole-doc/blob/master/doc/01.swoole_server配置选项.md)<br> OK,废话不多讲,直接上实例:<br> 服务器端:<br> ```php public function onReceive( swoole_server $serv, $fd, $from_id, $data ) { $length = unpack("N" , $data)[1]; echo "Length = {$length}\n"; $msg = substr($data,-$length); echo "Get Message From Client {$fd}:{$msg}\n"; } ``` 客户端:<br> ```php $msg_length = pack("N" , strlen($msg_normal) ). $msg_normal; $i = 0; while( $i < 100 ) { $this->client->send( $msg_length ); $i ++; } ``` 直接运行,Perfect!<br> [点此查看完整实例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_length_check_server.php)<br> [点此查看其他相关源码](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05)<br> ### **4.特别篇:Http协议-Swoole内置的http_server** 从Swoole-1.7.7-stable开始,Swoole在内部封装并实现了一个Http服务器。是的,没错,再也不用在PHP层缓存和解析http协议了,Swoole直接内置Http服务器了。<br> 创建一个swoole_http_server的代码如下:<br> ```php $http = new swoole_http_server("127.0.0.1", 9501); $http->on('request', function (swoole_http_request $request, swoole_http_response $response) { $response->end("<h1>Hello Swoole.</h1>"); }); $http->start(); ``` 只需创建一个swoole_http_server对象并设置onRequest回调函数,即可实现一个http服务器。<br> 在onRequest回调中,共有两个参数。参数`$request`存放了来自客户端的请求,包括Http请求的头部信息、Http请求相关的服务器信息、Http请求的GET和POST参数以及HTTP请求携带的COOKIE信息。参数`$response`用于发送数据给客户端,可以通过该参数设置HTTP响应的Header信息、cookie信息和状态码。<br> 此外,swoole_http_server还提供WebSocket功能,使用此功能需要设置onMessage回调函数,如下:<br> ```php $http_server->on('message', function(swoole_http_request $request, swoole_http_response $response) { echo $request->message; $response->message(json_encode(array("data1", "data2"))); }) ``` 通过`$request->message`获取WebSocket发送来的消息,再通过`$response->message()`回复消息即可。<br> [点此查看完整实例](https://github.com/LinkedDestiny/swoole-doc/blob/master/src/05/swoole_http_server.php)<br> [点此查看swoole_http_server相关文档](server/14.swoole_http_server.md)<br> (最后做个小广告,经过尝试,已经初步将php的Yaf框架移植到了swoole_http_server上,经过测试,swoole-yaf的性能远远超过了nginx+php-fpm+yaf的性能。<br> 项目地址:https://github.com/LinkedDestiny/swoole-yaf<br> 我将继续不断完善这个项目,力争能够真正用于线上项目)