TCP粘包问题和SWOOLE运行流程

上一篇文章主要讲了通过nginx反向代理,实现客户端访问服务端的请求转发,这个主要运用在传统框架tp,laravel通过swoole实现加速,本质就是运行的swoole服务,ip:端口号。网络协议及其分层,以及应用层和传输层之间进行通信是通过socket套接字实现的。以及tcp粘包问题的初体验。

 

tcp问题

tcp专业术语:包,丢包,分包,组包,拆包,包头,结尾,解包,抓包。

场景一:客户端连续而又快速的向服务端发送数据的时候,服务端接收到的数据是什么样的呢?

场景演示:

服务端

$host = "0.0.0.0";
$port = "9501";
 
//创建对象
$serv = new Swoole\Server($host,$port);
 
//注册事件
$serv->on("Start",function($serv) use($host,$port){
    echo "启动swoole 监听的信息tcp:$host:$port\n";
});

$serv->on("Connect",function ($serv,$fd){
    echo "$fd connect\n";
});

$serv->on("Receive",function($serv,$fd,$from_id,$data){
    echo "接收到客户端的数据".$data."\n";
    $serv->send($fd,$data);
});

$serv->on("Close",function($serv,$fd){
    echo "$fd close\n";
});
 
//开启服务
$serv->start();

客户端

$host = "127.0.0.1";
$port = "9501";
$timeout = 0.5;

//创建对象
$client = new swoole_client(SWOOLE_SOCK_TCP);
 
//连接服务器
if (!$client->connect($host,$port,$timeout)) {
    die("connect failed.");
}
 
//向服务器发送数据
for($i=0;$i<100;$i++){
    $client->send($i);
}
 
//接收服务器返回的数据
if(!$data = $client->recv()){
    die("recv failed.");
}
 
//关闭连接 (主动关闭 短连接)
$client->close();
 
echo "同步客户端(短连接)\n";

结果现象:客户端发送的多份小的数据,服务端接收到的数据都粘在一起了,且数据包的大小都不一样。这种现象用一种专业的词表示:粘包

所谓粘包就是,一个数据在发送的时候跟上了另一个数据的信息,另一个数据的信息可能是完整的也可能是不完整的。

tcp粘包是如何出现的呢?

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

发送方引|起的粘包是由TCP协议本身造成的, TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。 若连续几次发送 的数据都很少,通常TCP会根据优化算法把这些数据合成包后一次发送出去,这样接收方就收到了粘包数据。

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就拼接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多个数据。

tcp在数据交互的时候,会有一个 数据拥塞 的处理,这里会有一个等待的时间,但是这个等待的时间是不固定的,这会有数据的短暂的缓存,由于时间不固定,所以缓冲区里的数据也不固定。不同的客户端,他们的缓冲区的数据也是相互独立的,不会重叠。tcp缓冲区主要是一对一交互的时候才会出现粘包的问题,tcp通过socket建立通道,这里会生成通道id编号,多个通道会有不同的编号,他们之间相互独立。

 

场景二:客户端一次性向服务端发送一个很大的数据包,服务端接收到的数据会是什么样的呢?

场景演示:

服务端

$host = "0.0.0.0";
$port = "9501";
 
//创建对象
$serv = new Swoole\Server($host,$port);
 
//注册事件
$serv->on("Start",function($serv) use($host,$port){
    echo "启动swoole 监听的信息tcp:$host:$port\n";
});

$serv->on("Connect",function ($serv,$fd){
    echo "$fd connect\n";
});

$serv->on("Receive",function($serv,$fd,$from_id,$data){
    echo "接收到 $fd 客户端的数据\n";
    $serv->send($fd,$data);
});

$serv->on("Close",function($serv,$fd){
    echo "$fd close\n";
});
 
//开启服务
$serv->start();

客户端

$host = "127.0.0.1";
$port = "9501";
$timeout = 0.5;

//创建对象
$client = new swoole_client(SWOOLE_SOCK_TCP);
 
//连接服务器
if(!$client->connect($host,$port,$timeout)){
    die("connect failed.");
}
 
//向服务器发送数据
$client->send(str_repeat("Hello World",1024*1024*1));
 
 
//接收服务器返回的数据
if(!$data = $client->recv()){
    die("recv failed.");
}
 
//关闭连接 (主动关闭 短连接)
$client->close();
 
echo "同步客户端(短连接)\n";

结果现象:客户端一次发送一个很大的数据包,服务端接收到的数据是被分为多个数据包。

至于那个警告,后续解释。

WARNING swWorker_discard_data (ERRNO 1007): [1]received the wrong data[65536 bytes] from socket#1

这种结果现象是一个正常现象,因为tcp会自动拆包。

 

以上两种场景总结:

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

 

tcp问题解决


粘包解决:(延迟不是最佳答案)

方法一:分隔符

客户端

for ($i=0;$i<100;$i++) {
    $client->send($i."\r\n");
}

服务端

var_dump(explode("\r\n",$data));

 

swoole中是有一个参数配置,可以实现分隔符的效果。

array(
    'open_eof_split' => true,   // 打开EOF_SPLIT检测
    'package_eof' => "\r\n",    // 设置EOF
)

服务端

$host = "0.0.0.0";
$port = "9501";
 
//创建对象
$serv = new Swoole\Server($host,$port);
 
//配置
$serv->set([
    'open_eof_split' => true, //打开EOF_SPLIT检测
    'package_eof' => "\r\n", //设置EOF
]);
 
//注册事件
$serv->on("Start",function($serv) use($host,$port){
    echo "启动swoole 监听的信息tcp:$host:$port\n";
});

$serv->on("Connect",function ($serv,$fd){
    echo "$fd connect\n";
});

$serv->on("Receive",function($serv,$fd,$from_id,$data){
    echo "接收到客户端的数据".$data."\n";
    $serv->send($fd,$data);
});

$serv->on("Close",function($serv,$fd){
    echo "$fd close\n";
});
 
//开启服务
$serv->start();

客户端

$host = "127.0.0.1";
$port = "9501";
$timeout = 0.5;

//创建对象
$client = new swoole_client(SWOOLE_SOCK_TCP);
 
$client->set([
    'open_eof_split' => true, //打开EOF_SPLIT检测
    'package_eof' => "\r\n",
]);
 
//连接服务器
if(!$client->connect($host,$port,$timeout)){
    die("connect failed.");
}
 
//向服务器发送数据
for($i=0;$i<100;$i++){
    $client->send($i."\r\n");
}
 
//接收服务器返回的数据
if(!$data = $client->recv()){
    die("recv failed.");
}
 
//关闭连接 (主动关闭 短连接)
$client->close();
 
echo "同步客户端(短连接)\n";

服务端要给客户端发送数据时,客户端也需要配置。但是这种方法不好,一般都不用。因为:

EOF并不是100%在数据中不会出现这样的数据,不安全。

EOF切割需要遍历整个数据包的内容,查找EOF,因此会消耗大量CPU资源。假设每个数据包为2M,每秒10000个请求,这可能会产生20G条CPU字符匹配指令。(性能问题,资源的消耗大)

 

方法二:pack()函数(都采用的方式)(固定包头+包体协议)

那怎么理解pack()函数呢,怎么使用?

pack()函数 将数据打包成二进制字符串,返回包含数据的二进制字符串。

有多个格式字符,常用的是N(32位,占4个字节长度)和n(16位,占2个字节长度)

代码:

$str = "hello world";
var_dump($str);

$len = (string)strlen($str);
var_dump($len);

$len_pack = pack("N",$len);
var_dump($len_pack);

$re = $len_pack.$str;
var_dump($re);
var_dump(unpack("N",$re));

这样,客户端每次发送数据的时候,将数据的长度pack一下,打包成二进制数据,然后再拼接要发送的数据,发送给服务端,服务端接收到数据时,通过unpack进行解压出数据,再根据数据的长度进行实际的截取。

客户端

$host = "127.0.0.1";
$port = "9501";
$timeout = 0.5;

//创建对象
$client = new swoole_client(SWOOLE_SOCK_TCP);
 
//连接服务器
if(!$client->connect($host,$port,$timeout)){
    die("connect failed.");
}
 
//向服务器发送数据
$context = "我太帅气了";

for ($i=0;$i<100;$i++) {
    $len = pack("n",strlen($context));
    $client->send($len.$context);
}
 
//接收服务器返回的数据
if(!$data = $client->recv()){
    die("recv failed.");
}
 
//关闭连接 (主动关闭 短连接)
$client->close();
 
echo "同步客户端(短连接)\n";

服务器

$host = "0.0.0.0";
$port = "9501";
 
//创建对象
$serv = new Swoole\Server($host,$port);
 
//注册事件
$serv->on("Start",function($serv) use($host,$port){
    echo "启动swoole 监听的信息tcp:$host:$port\n";
});

$serv->on("Connect",function ($serv,$fd){
    echo "$fd connect\n";
});

$i=0;

$serv->on("Receive",function($serv,$fd,$from_id,$data) use($i){
    while($i<strlen($data)){
        $len = unpack("n",substr($data,0,2))[1];
        $res = substr($data,2,$len);
        echo $res."\n";
        $serv->send($fd,$res);
        $data = substr($data,2+$len);
        $i = $i + 2 + $len;//记录一下数据包长度
    }
    echo $i."\n";//打印长度
});

$serv->on("Close",function($serv,$fd){
    echo "$fd close\n";
});
 
//开启服务
$serv->start();

swoole中服务端通过配置,也可实现上面的结果:

open_length_check: 打开包长检测特性

package_length_type: 长度字段的类型,固定包头中用一个4字节或2字节表示包体长度。 package_length_offset:从第几个字节开始是长度,比如包头长度为120字节,第10个字节为长度值,这里填入9(从0开始计数)

package_body_offset: 从第几个字节开始计算长度,比如包头为长度为120字节,第10个字节为长度值,包体长度为1000。如果长度包含包头,这里填入0,如果不包含包头,这里填入120 package_max_length: 最大允许的包长度。因为在一个请求包完整接收前,需要将所有数据保存在内存中,所以需要做保护。避免内存占用过大。

服务端

$host = "0.0.0.0";
$port = "9501";
 
//创建对象
$serv = new Swoole\Server($host,$port);
 
//配置
$serv->set([
    'open_length_check' => true,
    'package_max_length' => 81920,
    'package_length_type' => 'N',
    'package_length_offset' => 0,
    'package_body_offset' => 4,
]);
 
//注册事件
$serv->on("Start",function($serv) use($host,$port){
    echo "启动swoole 监听的信息tcp:$host:$port\n";
});

$serv->on("Connect",function ($serv,$fd){
    echo "$fd connect\n";
});

$i=0;

$serv->on("Receive",function($serv,$fd,$from_id,$data) use(&$i){
    $i += 1;
    echo $data.$i."\n";//这里的数据还未解包,用的时候还需unpack
    $serv->send($fd,$data);
});

$serv->on("Close",function($serv,$fd){
    echo "$fd close\n";
});
 
//开启服务
$serv->start();

客户端

$host = "127.0.0.1";
$port = "9501";
$timeout = 0.5;

//创建对象
$client = new swoole_client(SWOOLE_SOCK_TCP);
 
/*$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($host,$port,$timeout)){
    die("connect failed.");
}
 
//向服务器发送数据
$context = "我太帅气了";
$len = pack("N",strlen($context));
for($i=0;$i<100;$i++){
    $client->send($len.$context);
}
 
//接收服务器返回的数据
if(!$data = $client->recv()){
    die("recv failed.");
}
var_dump($data);

//关闭连接 (主动关闭 短连接)
$client->close();
 
echo "同步客户端(短连接)\n";

若服务端发送数据给客户端,客户端也需要配置一下。

那2G视频需要发送=》package_max_length 一般设置1M,1024*1024*1,不会设置很大。

我们会选择:拆包=》根据某一个规则去拆包=》发送给客户端

这中间会因为网络原因,会导致 丢包 的问题

tcp有几种机制:

1.重连机制

2.数据校验--确认机制

 

进程与线程

专业:进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当 代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

实际:什么是进程,所谓进程其实就是操作系统中一个正在运行的程序,我们在一个终端当中,通过php,运行一个php文件,这个时候就相当于我们创建了一个进程,这个进程会在系统中驻存,申请属于它自己的内存空间系统资源并且运行相应的程序。

组成:进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程 执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

特征 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。

并发性:任何进程都可以同其他进程一起并发执行

独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;

异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进

结构特征:进程由程序、数据和进程控制块三部分组成。

多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。

对于一个进程来说,它的核心内容分为两个部分,一个是它的内存,这个内存是这进程创建之初从系统分配的,它所有创建的变量都会存储在这一片内存环境当中

一个是它的上下文环境,我们知道进程是运行在操作系统的,那么对于程序来说,它的运行依赖操作系统分配给它的资源,操作系统的一些状态。

在操作系统中可以运行多个进程的,对于一个进程来说,它可以创建自己的子进程,那么当我们在一个进程中创建出若干个子进程的时候那么可以,子进程和父进程一样,拥有自己的内存空间和上下文环境

 

为什么需要子进程?

父进程:用来管理 子进程:用来工作

正常来说父进程停止了子进程也会停止运行

如何区分父进程和子进程呢??

 

线程

线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。

一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度 和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。

线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为 多线程

特点

在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。

线程具有以下属性。

1)轻型实体 线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。 线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:

  • (1)线程状态
  • (2)当线程不运行时,被保存的现场资源。
  • (3)一组执行堆栈。
  • (4)存放每个线程的局部变量主存区。
  • (5)访问同一个进程中的主存和其它资源。 用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。

2)独立调度和分派的基本单位。 在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。

3)可并发执行。 在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。

4)共享进程资源。

在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有 的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

 

协程

协程与子例程一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。协程源自 Simula 和 Modula-2 语言,但也有其他语言支持。

协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。 一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。

我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。

协程 和线程区别:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。

 

进程和线程的关系与区别

1. 地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;因此线程可以读写同样的数据结构和变量,便于线程之间的通信。相反,进程间通信(IPC)很困难且 消耗更多资源。

2. 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源

3. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元

4. 二者均可并发执行.

5. 进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束

6. 线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志

 

简单理解

计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

一个车间里,可以有很多工人。他们协同完成一个任务。

线程就好比车间里的工人。一个进程可以包括多个线程。

车间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫”互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做”信号量”(Semaphore),用来保证多个线程不会 互相冲突。

不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

操作系统的设计,因此可以归结为三点:

(1)以多进程形式,允许多个任务同时运行;

(2)以多线程形式,允许单个任务分成不同的部分运行;

(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

评论