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如果是惰性策略,到期後也不會真的被刪除的... ...

Tags: