04.(六星教育)TCP问题解决

tcp数据发送问题

tcp在发送数据的时候因为存在数据缓存的关系,对于数据在发送的时候在 短时间内 如果连续发送很多小的数据的时候就会有可能一次性一起发送,还有就是对于大的数据就会分开连续发送多次。

1.一起发

就是多份数据变成了一份然后进行发送给了客服端,这种情况用一种专业的词:粘包。
所谓粘包就是,一个数据在发送的时候跟上了另一个数据的信息,另一个数据的信息可能是完整的也可能是不完整的;
出现的原因:
出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
发送方引起的粘包是由TCP协议本身造成的, TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。 若连续几次发送
的数据都很少,通常TCP会根据优化算法把这些数据合成- -包后- 次发送出去,这样接收方就收到了粘包数据。
接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一-包数据到达时前一-包数据尚未被用户进程取
走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一-次取到了 多包数据。

2.多次发
<?php
// swoole_tcp_server.php
// 监听数据接收事件
$serv->on('Receive', function ($serv, $fd, $from_id, $data) {
// 接收客户端的想你想
// echo "接收到".$fd."信息的".$data."\n";
echo "接收到".$fd."信息"."\n";
$serv->send($fd, "Server: ");
});

// swoole_tcp_client.php
$client->send(str_repeat('hello World', 100000));
?>

对于这样的问题就会造成的现象就是一次就有多次的请求;对于这个问题的处理就是我们可以采用分包的方式
处理;
分包操作主要是由程序处理而并非tcp处理。

解决方案

1.1粘包-特殊字符方式

  • 当时短连接的情况下,不用考虑粘包的情况
  • 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
  • 如果双方建立长连接,需要在连接后一段时间内发送不同结构数据接收方创建预处理线程,对接收到的数据包进行预处理,将粘连的包分开

分包是指在出现粘包的时候,我们的接收方要进行分包处理。(在长连接中都会出现) 数据包的边界发生错位, 导致读出错误的数据分包,进而曲解原始数据含义。

粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。
通过定义一个特殊的符号,注意是客户端与服务端相互约定的;然后下一步就是客户端每次发送的时候都会在后面添加这个数据,服务端做数据的字符串处理;

<?php
// swoole_tcp_client
for ($i=0; $i < 100; $i++) {
$client->send("hello world\i");
}


// swoole_tcp_server
// 监听数据接收事件
$serv->on('Receive', function ($serv, $fd, $from_id, $data) {
// 接收客户端的想你想
var_dump(explode('\i', $data));
// echo "接收到".$fd."信息的".$data."\n";
// echo "接收到".$fd."信息"."\n";
$serv->send($fd, "Server: ");
});
?>


当然会存在一些"空数据" , 实际上这个只是explode函数的分割问题,自行处理即可。

swoole的解决方式:

swoole的解决办法就是通过EOF的方式处理;在swoole中提供了一个open_eof_split的选项: http://wiki.swoole.com./wiki/page/421.html

操作方式

<?php
$swoole->set([
'open_eof_split' => true,
'package_eof' => "\r\n",
]);
?>

不过这种方式会存在着一个问题,那就是在内容中不能存在于package_eof的指定的字符串,因为对于swoole来说它就是通过遍历整个数据包的内容,查找EOF然后进行分割,同时也会对于CPU的资源消耗比较大。
其实这种方式并不是特别的推荐,但是也可以了解。

1.2 统一粘包标准(固定包头+包体协议)
对于这样的问题情况,实际上我们可以通过定义好发送消息的tcp数据包的格式,而这个对于服务端和客户端相互之间就遵守这个规范。
这种方式就是通过与在数据传输之后会在tcp的数据包中携带上数据的长度,然后服务端就可以根据这个长度,对于数据进行截取。
对于pack的解释 -》https://php.golaravel.com/function.pack.html
对于unpack的解释 -》https://php.golaravel.com/function.unpack.html
须知:一个字节 = 8个二进制位

<?php
$client = new swoole_client(SWOOLE_SOCK_TCP | SWOOLE_KEEP);

// $client->set([
//     'open_eof_split' => true, //打开EOF_SPLIT检测
//     'package_eof' => "\r\n", //设置EOF
// ]);

//连接到服务器
if (!$client->connect('127.0.0.1', 9501, 0.5))
{
    die("connect failed.");
}

//准备要发送的数据
$content = "我太帅气了";
$len = pack('N',strlen($content));
//向服务器发送数据


for ($i = 0; $i < 100; $i++) {
    // sleep(1);
    // $client->send("oooo");
     $send = $len.$content;
     $client->send($send);
}

// $client->send(str_repeat('0oo',1024 * 1024 * 1));

//从服务器接收数据
$data = $client->recv();
if (!$data)
{
    die("recv failed.");
}
// echo $data;
//关闭连接
// $client->close();
//1.
// sleep(4);
// echo "这是同步\n";

//2.
echo "订单生成成功\n";
<?php
// 1. 创建swoole 默认创建的是一个同步的阻塞tcp服务
$host = "0.0.0.0"; // 0.0.0.0 代表接听所有
// 创建Server对象,监听 127.0.0.1:9501端口
// 默认是tcp
$serv = new Swoole\Server($host, 9501);

//配置
// $serv->set([
    // 'open_eof_split' => true, //打开EOF_SPLIT检测
 //   'package_eof' => "\r\n", //设置EOF
    // 'heartbeat_idle_time' => 10,
 //   'heartbeat_check_interval' => 3,
// ]);

// 2. 注册事件
$serv->on('Start', function($serv){
    echo "启动swoole 监听的信息tcp:$host:9501\n";
});
//监听连接进入事件
$serv->on('Connect', function ($serv, $fd) {
    echo "Client: Connect.\n";
});

//监听数据接收事件
$serv->on('Receive', function ($serv, $fd, $from_id, $data) {
    // echo "接收到的信息".$fd."\n";
    // echo "接收到的信息".$data."\n";
    $pack = unpack('N',$data);
    var_dump($pack);
    $content = $pack[1];
    var_dump($content);
    var_dump(substr($data,2,$content));
    // var_dump(explode("\r\n",$data));
    $serv->send($fd, "Server: ".$data);
});

//监听连接关闭事件
$serv->on('Close', function ($serv, $fd) {
    echo "QQ离线.\n";
});
// 3. 启动服务器
// 阻塞
$serv->start(); // 阻塞与非阻塞
?>

不过如上的服务端还需要负责对于不同的数据进行解包的处理。

swoole的处理方式:

相关配置:

  1. open_length_check: 打开包长检测特性
  2. package_length_type: 长度字段的类型,固定包头中用一个4字节或2字节表示包体长度。
  3. package_length_offset:从第几个字节开始是长度,比如包头长度为120字节,第10个字节为长度值,这里填入9(从0开始计数)
  4. package_body_offset: 从第几个字节开始计算长度,比如包头为长度为120字节,第10个字节为长度值,包体长度为1000。如果长度包含包头,这里填入0,如果不包含包头,这里填入120
  5. package_max_length: 最大允许的包长度。因为在一个请求包完整接收前,需要将所有数据保存在内存中,所以需要做保护。避免内存占用过大。

package_length_type 的参考 https://segmentfault.com/a/1190000008305573

<?php
// 1. 创建swoole 默认创建的是一个同步的阻塞tcp服务
$host = "0.0.0.0"; // 0.0.0.0 代表接听所有
// 创建Server对象,监听 127.0.0.1:9501端口
// 默认是tcp
$serv = new Swoole\Server($host, 9501);

//配置
$serv->set([
    'open_length_check' => true,
    'package_max_length' => 81920,
    'package_length_type' => 'N',
    'package_length_offset' => 0,
    'package_body_offset' => 4,
]);

// 2. 注册事件
$serv->on('Start', function($serv){
    echo "启动swoole 监听的信息tcp:$host:9501\n";
});
//监听连接进入事件
$serv->on('Connect', function ($serv, $fd) {
    echo "Client: Connect.\n";
});

//监听数据接收事件
$serv->on('Receive', function ($serv, $fd, $from_id, $data) {
    // echo "接收到的信息".$fd."\n";
    // echo "接收到的信息".$data."\n";
    $pack = unpack('N',$data);
    var_dump($pack);
    $content = $pack[1];
    var_dump($content);
    var_dump(substr($data,4,$content));
    //var_dump($data);
    // var_dump(explode("\r\n",$data));
    $serv->send($fd, "Server: ".$data);
});

//监听连接关闭事件
$serv->on('Close', function ($serv, $fd) {
    echo "QQ离线.\n";
});
// 3. 启动服务器
// 阻塞
$serv->start(); // 阻塞与非阻塞
?>

$serv->on('Receive', function ($serv, $fd, $from_id, $data) {
    // echo "接收到的信息".$fd."\n";
    // echo "接收到的信息".$data."\n";
    //$pack = unpack('N',$data);
    //var_dump($pack);
    //$content = $pack[1];
    //var_dump($content);
    //var_dump(substr($data,4,$content));
    var_dump($data);
    // var_dump(explode("\r\n",$data));
    $serv->send($fd, "Server: ".$data);
});

服务端向客户端发送消息也要在客户端加上配置

<?php
$client = new swoole_client(SWOOLE_SOCK_TCP | SWOOLE_KEEP);

$client->set([
    'open_length_check' => true,
    'package_max_length' => 81920,
    'package_length_type' => 'N',
    'package_length_offset' => 0,
    'package_body_offset' => 4,
]);

//连接到服务器
if (!$client->connect('127.0.0.1', 9501, 0.5))
{
    die("connect failed.");
}

//准备要发送的数据
$content = "我太帅气了";
$len = pack('N',strlen($content));
//向服务器发送数据


for ($i = 0; $i < 10; $i++) {
    // sleep(1);
    // $client->send("oooo");
     $send = $len.$content;
     $client->send($send);
}

// $client->send(str_repeat('0oo',1024 * 1024 * 1));

//从服务器接收数据
$data = $client->recv();
if (!$data)
{
    die("recv failed.");
}
echo $data;
//关闭连接
// $client->close();
//1.
// sleep(4);
// echo "这是同步\n";

//2.
echo "订单生成成功\n";

王如棋博客
请先登录后发表评论
  • 最新评论
  • 总共0条评论