大家好,我今天打算换一个新的出场方式。所以,我打算从下面倒计时后开始重新打招呼,你们就假装开头这句话我没写配合一下,谢谢。
你们准备好了吗,我要重新从头开始了...
5
4
3
2
1
.
.
.
大家好!
事情是这样的,昨天晚上我先发了一篇关于API Token的文章,然后又引入了PHP Session,虽然这两篇文章阅读量创了历史新低(我仿佛看到了永强新欣慰的脸庞泛着笑容和淫光),但是还是依旧有些个问题就像屎一样甩在了我脸上,其中第一个问题角度还是比较刁钻的,你们感受下:
看到这三个令人绝望的回答,我穿过网线就已经听到了有人似乎在说:“ 老李,你变了... ”,然而我要告诉你并没有,你李哥办事你们心里不清楚么?
第一个问题
这个问题实际上是在考验session id的生成策略,抽象一下就是【某个空间中生成全局唯一的id】。这个其实没啥好说的,得去简单翻一下PHP源码中关于生成session id这里的部分了,我手里常年备着一份PHP 7.2.8的源码,但我基本没这么看过只是有需要的时候翻翻,比如现在。你可以在ext / session / session.c 文件里连蒙带搜加grep找到相关代码,你们感受下(如果我找错了,记得来打我脸,我专门出一期修正):
/* 这个叫做 php_session_create_id 的函数生成了session id 但是生成的核心函数是调用的 bin_to_readable 函数 */PHPAPI zend_string *php_session_create_id(PS_CREATE_SID_ARGS) /* {{{ */{ // 声明一个 char 数组,数组长度就是后面两个常量相加 unsigned char rbuf[PS_MAX_SID_LENGTH + PS_EXTRA_RAND_BYTES]; // zend_string 是zend封装好的字符串struct,类似于redis里d的 sds // 这里是声明一个指向 zend_string 的指针 zend_string *outid;
/* Read additional PS_EXTRA_RAND_BYTES just in case CSPRNG is not safe enough */ // 这里看起来就是如果生成失败的情况. if (php_random_bytes_throw(rbuf, PS(sid_length) + PS_EXTRA_RAND_BYTES) == FAILURE) { return NULL; } // zend_string_alloc 应该是zend封装好的为string分配内存的函数 // 功能类似于 malloc 函数... outid = zend_string_alloc(PS(sid_length), 0); /* ZSTR_LEN可以获取zend_string的长度 ZSTR_VAL可以获取zend_string的值 但谁能告诉我这个PS宏是做什么用的... ... */ ZSTR_LEN(outid) = bin_to_readable(rbuf, PS(sid_length), ZSTR_VAL(outid), (char)PS(sid_bits_per_character));
return outid;}
static char hexconvtab[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,-";/* returns a pointer to the byte after the last valid character in out *//* * 注意这个函数里的玩意略风骚。至于你们能不能顶住,我反正顶不住 */static size_t bin_to_readable(unsigned char *in, size_t inlen, char *out, char nbits) /* {{{ */{ unsigned char *p, *q; unsigned short w; size_t len = inlen; int mask; int have; p = (unsigned char *)in; q = (unsigned char *)in + inlen; w = 0; have = 0; mask = (1 << nbits) - 1; while (inlen--) { if (have < nbits) { if (p < q) { // 。。。。。。。。 // 你们有兴趣好好研究一下这行,反正我特么不看了,艹 w |= *p++ << have; have += 8; } else { /* consumed everything? */ if (have == 0) break; /* No? We need a final round */ have = nbits; } } /* consume nbits */ // 关键行在这里...out是字符串数组指针 // 这里就是将hexconvtab数组里的字符一个一个 // 随机出来赋值给out指针 *out++ = hexconvtab[w & mask]; w >>= nbits; have -= nbits; } // 这个,没啥好说的,就是给字符数组最后加上一个\0,变成字符串 *out = '\0'; return len;}
说句实话,跟我想象中猜测推理的还是不太一样的,按照我之前理解,PHP的session id生成应该至少有时间戳在其中的,然而真的并没有...这段充斥着位移运算和位运算的代码,真的是...给我整吐了,恕我直言我没仔细研究。不过既然核心依然是伪随机出一个偏移量,然后取出偏移量位置上字符,那么重复还是有一定概率,只是这个概率一定是非常非常非常低,我感觉我在说废话...
然后是上面那坨代码,如果以前哪位分析过,可以简单给投稿介绍下。我感觉这个C函数可以拿走实现自己的低碰撞率随机序列了。
第二个问题
这个问题其实还是有点儿意思的,而且我估计注意到的人不多。我给下demo代码,你们复制粘贴走感受下:
// 首先在a.php里<?phpsession_start();$_SESSION['name'] = 'wahaha';sleep( 30 );
// 在b.php里 <?phpsession_start();echo $_SESSION['name'];
复现方法就是:先访问a.php,然后再访问b.php,这会儿b.php就会被阻塞住一直等到a.php的sleep(30)完事儿后才会运行...这就是传说中session阻塞问题。这个,咱就不去扒源码了,首先请找到session文件所在的目录找到session文件,然后用lsof命令简单分析你们感受下,如下图:
上图中,我一共执行了两次lsof命令。
第一次执行的时候,PID为29645的fpm进程率先打开session,注意第一排的第二个红圈里FD那一列,值为6uW,6表示为当前文件描述符,u表示该文件已经被某进程打开并且正在被读或者被写,W表示全文件写锁。
第二次执行的时候,PID为29645的fpm进程还在sleep中,而此时又来了一个新的fpm进程,也就是PID为29640的fpm进程,但是由于PID为29645的进程持有当前session文件的文件锁,所以29640就只能等...
解决方案是什么?session_write_close()函数了解一下...
然后写到这里我突然想到另外一个问题,如果说我们把session扔到redis或者memcache后,这个锁机制还会生效么?... ...这个我没试过,真没试过,不骗你们,真的没有...
第三个问题
这个如果你用文件方式存session,是真的没有办法的,全凭信仰。如果一定要精确,只有说你把session存储到mem或者redis中的时候,利用人家的key ttl属性才能实现精准控制。。。实际上,redis key ttl如果是惰性策略,到期后也不会真的被删除的... ...