TCP 服务器处理粘包

粘包问题

由于tcp的特性,可能会出现数据粘包情况,例如

  • A连接Server
  • A发送 hello
  • A又发送了一条 hello
  • Server可能会一次性收到一条”hellohello”的数据
  • Server也可能收到”he” ,”llohello”类似这样的中断数据

粘包解决

  • 通过标识EOF,例如http协议,通过\r\n\r\n 的方式去表示该数据已经完结,我们可以自定义一个协议,例如当接收到 “结尾666” 字符串时,代表该字符串已经结束,如果没有获取到,则存入缓冲区,等待结尾字符串,或者如果获取到多条,则通过该字符串剪切出其他数据
  • 定义消息头,通过特定长度的消息头进行获取,例如我们定义一个协议,前面10位字符串都代表着之后数据主体的长度,那么我们传输数据时,只需要000000000512346(前10位为协议头,表示了这条数据的大小,后面的为数据),每次我们读取只先读取10位,获取到消息长度,再读取消息长度那么多的数据,这样就可以保证数据的完整性了.(但是为了不被混淆,协议头也得像EOF一样标识)
  • 通过pack二进制处理,相当于于方法2,将数据通过二进制封装拼接进消息中,通过验证二进制数据去读取信息,sw采用的就是这种方式

可查看swoole官方文档:https://wiki.swoole.com/wiki/page/287.html

实现粘包处理

服务端:

  1. <?php
  2. $subPort2 = $server->addlistener('0.0.0.0', 9503, SWOOLE_TCP);
  3. $subPort2->set(
  4. [
  5. 'open_length_check' => true,
  6. 'package_max_length' => 81920,
  7. 'package_length_type' => 'N',
  8. 'package_length_offset' => 0,
  9. 'package_body_offset' => 4,
  10. ]
  11. );
  12. $subPort2->on('connect', function (\swoole_server $server, int $fd, int $reactor_id) {
  13. echo "tcp服务2 fd:{$fd} 已连接\n";
  14. $str = '恭喜你连接成功服务器2';
  15. $server->send($fd, pack('N', strlen($str)) . $str);
  16. });
  17. $subPort2->on('close', function (\swoole_server $server, int $fd, int $reactor_id) {
  18. echo "tcp服务2 fd:{$fd} 已关闭\n";
  19. });
  20. $subPort2->on('receive', function (\swoole_server $server, int $fd, int $reactor_id, string $data) {
  21. echo "tcp服务2 fd:{$fd} 发送原始消息:{$data}\n";
  22. echo "tcp服务2 fd:{$fd} 发送消息:" . substr($data, '4') . "\n";
  23. });

客户端:

  1. <?php
  2. /**
  3. * Created by PhpStorm.
  4. * User: Tioncico
  5. * Date: 2019/3/6 0006
  6. * Time: 16:22
  7. */
  8. include "../vendor/autoload.php";
  9. define('EASYSWOOLE_ROOT', realpath(dirname(getcwd())));
  10. \EasySwoole\EasySwoole\Core::getInstance()->initialize();
  11. //::: warning
  12. //在3.3.7版本后,initialize事件调用改为:`EasySwoole\EasySwoole\Core::getInstance()->initialize()->globalInitialize();`
  13. //:::
  14. /**
  15. * tcp 客户端2,验证数据包,并处理粘包
  16. */
  17. go(function () {
  18. $client = new \Swoole\Client(SWOOLE_SOCK_TCP);
  19. $client->set(
  20. [
  21. 'open_length_check' => true,
  22. 'package_max_length' => 81920,
  23. 'package_length_type' => 'N',
  24. 'package_length_offset' => 0,
  25. 'package_body_offset' => 4,
  26. ]
  27. );
  28. if (!$client->connect('127.0.0.1', 9503, 0.5)) {
  29. exit("connect failed. Error: {$client->errCode}\n");
  30. }
  31. $str = 'hello world';
  32. $client->send(encode($str));
  33. $data = $client->recv();//服务器已经做了pack处理
  34. var_dump($data);//未处理数据,前面有4 (因为pack 类型为N)个字节的pack
  35. $data = decode($data);//需要自己剪切解析数据
  36. var_dump($data);
  37. // $client->close();
  38. });
  39. /**
  40. * 数据包 pack处理
  41. * encode
  42. * @param $str
  43. * @return string
  44. * @author Tioncico
  45. * Time: 9:50
  46. */
  47. function encode($str)
  48. {
  49. return pack('N', strlen($str)) . $str;
  50. }
  51. function decode($str)
  52. {
  53. $data = substr($str, '4');
  54. return $data;
  55. }