• 2018-06-13

    炮打TCP – 关于一而再再而三的粘包拆包问题的大字报

    Views: 4850 | 15 Comments

    TCP 所谓的粘包和拆包问题,是技术圈里最奇葩的问题之一!

    一而再,再而三,就跟傻逼的中国球迷支持中国足球队一样,前赴后继。有时候同一个人多次在犯同一个错误,有时候是前脚一个犯错了后脚又来一个还犯同样的错。即使是最优秀的程序员,也会在这个问题上面栽跟头,思维甚至很难转过弯,很久才能意识到自己的错误。而低水平的程序员就更不用说了,很多人到死都没有理解这个错误并解决掉,只是逃掉了而已。

    我们固然可以认为原因是某些人学艺不精,但那么多的人,其中包括无数的优秀程序员在 TCP 粘包和拆包问题在犯错误,难道我们不能说,这其实是 TCP 自身的原因吗?

    在我看来,这个问题的出现,原因就在于 TCP 协议是有原罪的 -- 也就是 TCP 协议所谓的“流式”协议。所以,我要炮轰 TCP!

    经过几十年的验证,除了几数几个网络协议会用到 TCP 所谓的流式特性之外,没有任何应用协议使用流式特性。我们必须承认,所有的应用层协议都是基于报文的协议,而不是流式协议。而某些名字中带有”流(Stream)"字样的协议,如 RTP,流媒体等,其本质是无数小体积的报文按顺序拼接而成,根本就和 TCP 的流式没有任何关系!

    那么我们就可以确定,数据的本质是报文,流数据是某类报文数据的一种伪称。事实,TCP 的流就是基于 IP 报文的。

    因为“流”是一个伪抽象的概念,所以流式协议是违反人的天性和事物的内在逻辑的。万物的本质是报文。这因为如此,”流”所引出的粘包拆包问题,就必然会一而再,再而三,大量地出现。

    炮轰之后,我们要怎么解决问题呢?

    由于 TCP 协议已经成为事实上的基础,所以淘汰掉 TCP 是不可想象的。我们要做的是,找到正确的编程代码,解决粘包拆包问题。经过无数人的探索,以及无数人一次又一次重复的愚蠢错误的反证,我发现了解决 TCP 粘包和拆包问题只有一条路径,没有第二条!我断言,所有和我的解决方案不同的代码,都是错误的。

    彻底解决 TCP 粘包和拆包问题的代码架构如下:

    char tmp[];
    Buffer buffer;
    // 网络循环:必须在一个循环中读取网络,因为网络数据是源源不断的。
    while(1){
        // 从TCP流中读取不定长度的一段流数据,不能保证读到的数据是你期望的长度
        tcp.read(tmp);
        // 将这段流数据和之前收到的流数据拼接到一起
        buffer.append(tmp);
        // 解析循环:必须在一个循环中解析报文,应对所谓的粘包
        while(1){
            // 尝试解析报文
            msg = parse(buffer);
            if(!msg){
                // 报文还没有准备好,糟糕,我们遇到拆包了!跳出解析循环,继续读网络。
                break;
            }
            // 将解析过的报文对应的流数据清除
            buffer.remove(msg.length);
            // 业务处理
            process(msg);
        }
    }
    

    这段代码是终极地解决 TCP 粘包和拆包问题的代码!

    这段代码之所以正确,是因为它包含了两个循环:网络循环和解析循环。

    网络循环用于从 TCP socket 中读取流式数据,每一次读取到的数据的长度是不可预期,也就是,读取到的数据长短不一,无法保证,这就是所谓“流式”引出的问题。

    而解析循环的功能是从拼接后流数据中,尝试解析出多个报文。注意,是多个报文,不是一个。因为所谓的粘包问题存在,所以可能是多个,而不是一个。如果解析不成功,那说明是遇到了拆包问题,我们继续读网络数据。

    你只需要死记硬背上面的正确代码即可。不死记硬背也一样,最终你还是要得出和我相同的结论写出和我一样的代码。那么,何不现在就死记硬背呢?

    最后,附上经典的错误代码:

    tcp.read(tmp, HEADER_LEN);
    header = parse_header(tmp);
    tcp.read(tmp, header.body_len);
    body = parse(tmp);
    

    这样的代码当然是错误的,这么简单代码怎么可能是对的?如果对了,TCP 还是 TCP 吗?

    如果你认为本文有用,请关注这个 GitHub 项目:https://github.com/ideawu/FUCK_TCP 让更多人一起炮打 TCP!

    该项目还提供了模拟粘包和拆包的代码,你如果不信邪,可以写一个自己的 client 试试。

    Posted by ideawu at 2018-06-13 15:05:11
  • 2018-05-31

    PHP 获取周一,上个月的正确做法

    Views: 3296 | 2 Comments

    如果你理所当然地认为在 PHP 中获取周一,上周一,上个月,下个月这些时间能简单地用 strtotime() 函数来获取的话,那你就大错特错了!

    $now = strtotime('2018-01-31');
    echo date('Y-m-d', strtotime('+1 month', $now)) . "\n";
    

    输出的结果是:

    2018-03-03
    

    并不是期望的返回2月份的某一天,而返回了错误的3月份。为什么呢?这和 strtotime() 的实现原理有关:

    • 将月份加1,就变成了 2018-02-31
    • 因为2月没有31号,所以修正,从 2018-02-28 再往前加3天

    类似的,星期相关的代码也有问题。

    $now = strtotime('2018-05-20'); // 这一天是周日
    echo date('Y-m-d', strtotime('this monday', $now)) . "\n";
    echo date('Y-m-d', strtotime('+0 monday', $now)) . "\n";
    

    输出的结果是:

    2018-05-21
    2018-05-21
    

    +0 monday 和 +1 monday 的结果是一样的,并不能将这种格式理解为本周一和下周一(见评论),特别容易出错的是,你可能会错误地认为 +1 monday 是下周一,但事实不是。要得到下周N,或者下下周N,需要先求出本周一。本周一的求法如下:

    $now = strtotime('2018-05-20');
    $time = $now - 86400 * (date('N', $now) - 1);
    echo date('Y-m-d', $time) . "\n"; // 2018-05-14
    

    然后再用本周一加减N个7天,得出前后的周一,之后再根据周一求周N。还有,"+n monday" 和 "-n monday" 的结果和期望不同,其处理逻辑不一致。结合评论,+n xxx 是从当天(含)开始找,-n xxx 是从前一天开始找, +-0 按 +1 处理。

    一句话总结:PHP strtotime() 求上周,下周,上个月,下个月等,不能简单处理,需要特殊处理,都需要先求出本周一或者本周一号,再手动处理,而且星期的处理应该在求出本周一之后用时间戳整数运算。

    Posted by ideawu at 2018-05-31 19:09:33
  • 2018-05-21

    给Nginx设置默认主机

    Views: 3562 | No Comments

    给Nginx设置默认主机,让所有没有明确设置的Host的访问,都落到特定的配置下。

    1. 生成证书

    sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl.key -out /etc/nginx/ssl.crt
    

    2. 配置Nginx

    server {
    	listen       80 default_server;
    	listen       443 default_server;
    	server_name  _;
    
    	ssl_certificate     /etc/nginx/ssl.crt;
    	ssl_certificate_key /etc/nginx/ssl.key;
    
    	return 200;
    }
    
    Posted by ideawu at 2018-05-21 17:40:22
  • 2018-05-10

    快速排序算法(QuickSort)的代码实现

    Views: 4738 | No Comments

    快速排序算法,也即快排,是递归和分而治之这两种计算机基本思想的应用,再加上其实现逻辑复杂度较好,性能较快,所以快速排序算法非常经典。

    快速排序算法经常作为面试算法题。快速排序算法本身并不复杂,其本身的逻辑非常简单,要掌握其思想不是难事,甚至基于其实现代码的形而上学的表面形状背下来也很轻松。但是,如果仅掌握了快速排序的思想以及代码表面形状,就认为自己懂了快速排序,就是没有真正地理解。

    快速排序算法作为面试题,一是考查理论结合实践的能力,要求面试者除了知道快速排序算法的实现逻辑和代码形状,还要知道快速排序为什么快,怎么快。二是考查编码细节的能力,考虑的是人经验之外的智商和思维水平。

    Continue reading »

    Posted by ideawu at 2018-05-10 19:46:06
  • 2018-03-03

    Mac 下最好用的看图软件 Tovi 免费下载了!

    Views: 8708 | 1 Comment

    众所周知,Mac 系统自带的图片浏览软件主要 Preview 预览和在 Finder 里按空格键,前者无法播放 GIF 动画,而后者的浏览方式非常不自然,例如不支持鼠标滚轮缩放操作。

    我在 2013 年开发出了 Mac 系统使用的看图软件 Tovi - Total Image Viewer for Mac,自发布以来,以后被安装超过 10000 次,大部分是收费用户,其中有过一次限时免费。

    目前,Tovi 2.0 版本已经发布了!新的 Tovi 2.0 重写了图片显示引擎,改善了操作体验,使得更加流畅。同时,并进一步完善了手势操作。

    为了让更多的朋友能体验到 Tovi 的好用,特发布了免费试用版。免费版除了显示试用信息之外,功能不受限,欢迎大家下载试用!


    Tovi 免费下载链接:http://tovi.ideawu.com/

    Posted by ideawu at 2018-03-03 17:24:29
  • 2018-02-07

    NSView NSImage NSData转换

    Views: 8331 | No Comments
    NSBitmapImageRep *bitmap =  [view bitmapImageRepForCachingDisplayInRect:[view visibleRect]];
    [view cacheDisplayInRect:[view visibleRect] toBitmapImageRep:bitmap];
    
    NSImage *image = [[NSImage alloc] initWithSize:NSMakeSize(width, height)];
    [image addRepresentation:bitmap];
    
    NSBitmapImageRep *bitmap = [NSBitmapImageRep imageRepWithData:data];
    
    NSBitmapImageRep *bitmap = [[[NSBitmapImageRep alloc] initWithCGImage:CGImage];
    
    Posted by ideawu at 2018-02-07 16:10:47
|<<<123456789>>>| 1/121 Pages, 726 Results.