xue微xue微深入地聊一聊PHP session

  • 2019 年 11 月 25 日
  • 筆記

大家好,我今天打算换一个新的出场方式。所以,我打算从下面倒计时后开始重新打招呼,你们就假装开头这句话我没写配合一下,谢谢。

你们准备好了吗,我要重新从头开始了…

5

4

3

2

1

.

.

.

大家好!

事情是这样的,昨天晚上我先发了一篇关于API Token的文章,然后又引入了PHP Session,虽然这两篇文章阅读量创了历史新低(我仿佛看到了永强新欣慰的脸庞泛着笑容和淫光),但是还是依旧有些个问题就像屎一样甩在了我脸上,其中第一个问题角度还是比较刁钻的,你们感受下:

  • 老李,双11那么一大坨人访问PHP商城,PHP session id会不会重复啊? 答:你确定你们用户量过一千了吗?
  • 老李,为毛我多个控制器访问同一个session成员,其他页面会被卡住,你遇到过咩? 答:没遇到过,就特么你事儿多…告诉用户让TA们等等就行了,又不是不能用
  • 用什么方法可以精确控制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;  }  // 这个,没啥好说的,就是给字符数组最后加上一个,变成字符串  *out = '';  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如果是惰性策略,到期后也不会真的被删除的… …