## workerman 進程管理
**author: xiak**
**last update: 2021-12-20 10:11:11**
----
[TOC=3,8]
----
### 進程模型
待研究子進程內部是否還可以再開子進程,應該是不行的
#### 進程相關的數據結構
```php
$worker->workerId \spl_object_hash($worker)
$worker->id {0, 1, ..., n} n: $worker->count - 1 進程計數
Worker::$_workers [$worker->workerId => $worker, ...]
Worker::$_pidMap [$worker->workerId => [pid => pid, ...], ...]
Worker::$_idMap [$worker->workerId => [0 => pid, ..., n => pid], ...] n: $worker->count - 1
Worker::$_pidsToRestart [pid => pid, ...]
$worker->_mainSocket
$worker->connections [$connection->id => $connection, ...]
$connection->id {1, 2, ..., n} n: PHP_INT_MAX
```
----
#### 架構
```
Worker::runAll():
static::initWorkers();
static::forkWorkers();
while (\count(static::$_pidMap[$worker->workerId]) < $worker->count) {
1. fork a $worker process
2. $worker->listen();
3. static::$_pidMap = array();
4. unset(static::$_workers[$key]); Remove other listener.
5. Timer::delAll();
6. run $worker->run() in process
}
```
~~~
主進程 Master
worker 實例A
子進程 0
$worker->connections: [connection1, ...]
子進程 1
$worker->connections: [connection1, ...]
...
worker 實例B
子進程 0
$worker->connections: [connection1, ...]
子進程 1
$worker->connections: [connection1, ...]
...
~~~
1. 一個進程上只有一個 worker 實例,一個 worker 實例 可以有多個相同的 worker 進程,稱為 worker 組進程
2. worker 組進程 都是相同的進程,監聽在同一協議和同一端口上
3. worker 進程 即子進程,所有的 worker 進程 都是平級和相互獨立的,都是直屬于 主進程 Master 的子進程
4. 不同的 worker 實例 間是相互獨立的,表示相互獨立互不干擾的功能
理解了這個關系,分析問題時就不會繞進去了。我們的代碼基本都是在 worker 實例 的各個回調方法上面。
----
#### worker 實例
每一個 `$worker = new Worker();` 就是一個 worker 實例,每個 worker 實例的 所有進程出生時都是一樣的(worker 實例的進程組),一樣的出生,一樣的 worker 實例,沒有本質區別。(**注意 一個進程內只有一個 worker 實例**)
----
#### 主進程、子進程、worker 實例 區分
一個類里面,既是主進程,又是子進程,還有 worker 實例,經常繞進去了,繞暈了傻傻分不清了,這里有一個方法可以幫助分清這個情況:
Workerman\\Worker::class 上:
1. 靜態屬性和靜態方法 都是進程的,可以處于 子進程 或 主進程,可根據 來判斷 static::$_masterPid === \posix_getpid()
2. 非靜態屬性和非靜態方法,直接使用 $this 對象關鍵字訪問時 都是 worker 實例,處于子進程
3. 任何時候都可以通過 static::$_masterPid === \posix_getpid() 判斷當前是 主進程還是子進程部分
----
#### 操作系統,程序,進程,socket 等之前的關系
操作系統是上帝,它管理著一切,代碼是程序的人類直觀語言描述,程序是做要做的事的一些列步驟,進程、線程 等是執行任務的使者,文件、網絡、fd,socket 等則是執行任務過程可能會使用到的資源,它們都是上帝的。
業務需求 >代碼\> 解釋/編譯 > 程序 > 進程/線程(進程數量取決于程序) > 系統調用 > 操作系統 > 執行 > 使用資源 > IO > 完成任務
link:[blog.csdn.net](https://blog.csdn.net/weixin_34025386/article/details/116487152)
link:[blog.csdn.net](https://blog.csdn.net/diavid/article/details/81205072)
> 程序、代碼、進程 三者并不是有明顯界限的客觀實體,而是一種平行的概念。這需要能夠理解計算機是如何工作的才能理解這種概念。
> 對操作系統來說,它并不關心某個進程是什么開發語言創建的,不論是什么語言只不過是上層的語法不一樣而已,最終的系統調用都是一樣的沒有區別,所以對操作系統來說,編程語言是透明的,不存在的,進程沒有這個關于是某種開發語言的屬性,各種語言起的進程都是一樣的,都是一樣的公民。
----
### workerman 進程管理設計分析
1. workerman 不想子進程能夠直接退出,它想 主進程監控子進程狀態,只要子進程退出了,就直接自動拉起一個補上,不想隨便就減少子進程數量,那是一開始定好的
2. workerman 支持平滑重啟:依次平滑重啟每個子進程(子進程收到信號后不在接受連接處理完剩下的業務后關閉),直至所有子進程重啟完畢
3. 子進程 可以自己退出,但不能自己實現重啟,因為 子進程 需要 主進程來拉起
4. 子進程 和 主進程 都是一樣的信號監聽,但可能 fork 了 后 使用之前的注冊會有問題,所以 子進程 又重新設置了一遍信號,但處理方法還是和主進程一樣的
----
#### 主進程監控原理
1. 當子進程退出時,主進程能收到通知
2. 如果收到子進程退出通知時 主進程狀態非 停止標記 static::STATUS_SHUTDOWN 說明 仍要運行,那就拉起新的 子進程(拉起數量取決于 現存子進程id 數量,要少了補齊)
如果 pid 在 static::$_pidsToRestart[$pid] 中 那么執行 static::reload()
```
if (isset(static::$_pidsToRestart[$pid])) {
unset(static::$_pidsToRestart[$pid]);
static::reload();
}
```
3. 如果 主進程狀態 為 停止標記,且 沒有子進程 pids 了,那就停止主進程
----
我們想實現,自主控制 某個子進程的平滑重啟,甚至動態增減子進程的數量,甚至必要時 徹底關閉服務,關閉所有子進程和主進程,不在啟動。
----
### 信號處理
|信號|作用|說明|
|--|--|--|
| SIGINT | 停止 Worker::stopAll() | ctrl + c 終端按下,會向主進程發送此信號 |
| SIGTERM | 停止 Worker::stopAll() | 沒有任何控制字符關聯 |
| SIGHUP | 停止 Worker::stopAll() | command: `stop`|
| SIGQUIT | **平滑停止** Worker::stopAll() | command: `stop-g`|
| SIGUSR1 | 重載 Worker::reload() | `reload`|
| SIGUSR2 | **平滑重載** | command: `reload -g` |
| SIGIO | show connections | command: `connections` |
| SIGIOT | show status| command: `status` |
> 注意:信號管理只在 linux 上可用
|信號常量名稱|信號值|
|--|--|
|SIGINT | 2 |
|SIGTERM | 15 |
|SIGHUP | 1 |
|SIGTSTP | 20 |
|SIGQUIT | 3 |
|SIGUSR2 | 12 |
|SIGUSR1 | 10 |
|SIGIOT | 6 |
|SIGIO | 29 |
----
### 命令行管理
~~~
Usage: php yourfile <command> [mode]
Commands:
start Start worker in DEBUG mode.
Use mode -d to start in DAEMON mode.
stop Stop worker.
Use mode -g to stop gracefully.
restart Restart workers.
Use mode -d to start in DAEMON mode.
Use mode -g to stop gracefully.
reload Reload codes.
Use mode -g to reload gracefully.
status Get worker status.
Use mode -d to show live status.
connections Get worker connections.
~~~
> 將主進程 pid 保存到文件中,是為了在命令管理模式下得到主進程 pid 的
> 通過命令行管理服務進程,是通過 發進程信號 給 服務(啟動文件名)的 主進程 進行的管理的
~~~
cd /root/xiak-test/pulsar-demo/test/
php server.php start
master process start_file=/root/xiak-test/pulsar-demo/test/server.php
/vendor/workerman/_root_xiak-test_pulsar-demo_test_server.php.pid
~~~
----
### 進程管理 stopAll、stop、reload
SIGINT SIGTERM SIGUSR1 static::$_gracefulStop = false(默認)
SIGHUP SIGQUIT static::$_gracefulStop = true
stop、reload 管理命令 時加 `-g` 參數表示使用 平滑停止和重載
----
#### Worker::stopAll()
首先 不論 是否 主進程 還是 子進程 static::$_status = static::STATUS_SHUTDOWN;
通常是進程收到信號時調用,也可能是 在各種啟動回調異常時或重載失敗時調用 static::stopAll(250, $e);
**主進程上:**
當子進程上執行 stopAll() 時,通常是 主進程收到 SIGHUP / SIGINT / SIGTERM 信號時調用的(通過 管理命令給主進程發信號)
1. $sign = static::$_gracefulStop ? SIGHUP : SIGINT
2. 向 現有全部 子進程發送 $sign (當子進程收到 SIGHUP / SIGINT 信號后 會執行 stopAll())
3. 如果是 非平滑停止信號 2s 后再次 向 現有全部 子進程發送 SIGKILL (確保真的殺死了)
4. 1s 后檢查 主進程的全部子進程 信號 0 ping
分析:
1. 主進程收到 的 停止信號 是 平滑 就給子進程發 平滑 的 停止信號
2. 主進程收到 的 停止信號 是 非平滑 就給子進程發 非平滑 的 停止信號
3. 非平滑停止信號 會確保 子進程真的被 kill 了(2s 后有檢查,如果你不自己動手了解,那我就替你動手了)
4. 進程狀態 標記為 關閉的,但 主進程 并不會立即退出
5. 決定 主進程 是否退出 是在 主進程的 進程監控中處理的 monitorWorkers
6. monitorWorkers: 等所有的 子進程 都退出完畢后,主進程才會真正退出 Worker::exitAndClearAll()
7. 主進程退出時 執行 Worker::onMasterStop() 回調,整個系統 exit 前的最后一個代碼執行了
----
**子進程上(worker):**
當子進程上執行 stopAll() 時,通常是 子進程收到主進程發過來的 SIGHUP / SIGINT 信號時調用的
1. 進程的 全部 worker 實例 執行 $worker->stop()
2. stop() 中:
a. 執行 worker 實例 上的回調 $worker->onWorkerStop()
b. 釋放 worker 實例的 主端口監聽:$worker->pauseAccept() 刪除 _mainSocket 上的讀事件, \fclose($this->_mainSocket)(如果有 socket 監聽的話)
c. static::$_gracefulStop = false (SIGINT) 時 關閉 worker 實例 上存在的 所有 socket 連接
3. 滿足 static::$_gracefulStop = false || ConnectionInterface::$statistics['connection_count'] <= 0 時
全部 worker 實例 銷毀 static::$_workers = array();
全局事件對象 銷毀 static::$globalEvent->destroy();
進程退出 exit($code); 至此子進程結束掉了
> 注意這里的 `stop()` 方法并**不是停止進程**,而是 “停止” worker,包含 嵌套子 worker。(worker 上除了包含 **原始的一種 worker** 以外 還有 `Worker::runAll()` 后續生成的 “嵌套子 worker” )
分析:
1. 子進程收到 SIGINT SIGTERM 后:(非平滑退出 static::$_gracefulStop:false)
a. 執行每個 worker 實例 的回調 $worker->onWorkerStop()
b. 釋放 每個 worker 實例 socket 主端口監聽,不再接受新連接
c. 關閉 每個 worker 實例 上存在的 所有 socket 連接
d. 不論進程上是否還有 連接實例 TcpConnection (不論連接是否為打開的),都直接退出進程 exit
e. 要它退出停止,它不接受新的了連接,現有的也直接關閉,不論是否還在服務中,反正就是立馬就要直接退出進程,這就是粗暴的非平滑停止
2. 子進程收到 SIGINT SIGTERM 后:(平滑退出 static::$_gracefulStop:true)
a. 執行每個 worker 實例 的回調 $worker->onWorkerStop()
b. 釋放 每個 worker 實例 socket 主端口監聽,不再接受新連接
c. 只有當進程上沒有 連接實例 TcpConnection (不論連接是否為打開的)時,才退出進程 exit
d. 此后,進程上的每個連接關閉時都會執行 Worker::stopAll()
e. 這樣 這個子進程最終在沒有 連接時,會自動 退出進程 exit
f. 要它退出停止,它不接受新的了連接,現有的也等它自己關閉
它只會在等到沒有 連接實例 TcpConnection (不論連接是否為打開的) 時,即等為所有客戶端都提供服務完畢后,才會退出進程,這就是 平滑停止啊意思啊
>[tip] 注意這里的 連接實例 TcpConnection (不論連接是否為打開的),如果一個連接確定不再使用,不會再重連接了,那么需要 釋放 TcpConnection 連接實例,不然會影響 平滑重啟機制
>[tip] 注意這里的 子進程上的 **全部 worker 實例** 其實只有**一個(一種) worker 實例**。( Worker::forkOneWorkerForLinux() 中做了處理)
----
#### Worker::reload()
進程上執行 reload() 時,通常是 主進程收到 SIGQUIT / SIGUSR1 信號時調用的(通過 管理命令給主進程發信號),和子進程收到父進程發的信號時, 或者 主進程監控 時自主調用
```php
case \SIGQUIT:
case \SIGUSR1:
static::$_gracefulStop = $signal === \SIGQUIT;
static::$_pidsToRestart = static::getAllWorkerPids();
static::reload();
```
**主進程上:**
1. 進程狀態不為 重載中 和 停止時 設置 進程狀態 為 Worker::$STATUS_RELOADING 重載中 ,調用 Worker::onMasterReload() , Worker::initId() 初始化 Worker::$_idMap;
2. 遍歷所有 worker 實例 的所有進程,判斷 $worker->reloadable === true 實例 是否可被重載(默認 true):
a. 可被重載:記錄 每個 可重載的 worker 實例的 所有可以重載的進程id $reloadable_pid_array
b. 不可被重載:給 每個 不可重載的 worker 實例的 每一個進程 發 SIGQUIT / SIGUSR1 信號 (所有可以重載的進程id 為空)
3. 計算可重載進程ids: 所有可以重載的進程id 與 static::$_pidsToRestart 取交集,其實就是 與 所有 worker 進程id 取交集
static::$_pidsToRestart = \array_intersect(static::$_pidsToRestart, $reloadable_pid_array);
4. 如果有 計算可重載進程ids
a. 向其中一個發 SIGQUIT / SIGUSR1 信號
b. 非平滑信號 2s 后 kill 這個進程
5. 如果沒有 計算可重載進程ids,只有進程狀態為非停止,就設置為 運行中
分析:
1. 可被重載時 向 可重載的 worker 實例的 一個進程 發 SIGQUIT / SIGUSR1 信號,并在主進程上記錄了 所有 要重啟的 進程id
a. 當這個 worker 實例 進程(子進程)收到信號,并成功退出后,主進程監控到了 退出進程id
b. 主進程非停止狀態,有進程退出,會拉起新進程替補
c. 當發現退出的進程id 在 static::$_pidsToRestart 中,則 執行 unset(static::$_pidsToRestart[$pid]); static::reload()
c. 這樣直到主進程第一次收到重載信號時的那一批舊進程全部完成退出更替(退出一個,啟動一個,退出一個,...)
d. 最終實現了,舊進程的依次退出啟動,從而最大限度地降低重載過程中服務進程減少對系統的影響
1. 不可被重載時 向 不可重載的 worker 實例的 所有進程 發 SIGQUIT / SIGUSR1 信號
a. 子進程收到信號后,由于 其實例 是不可重載的,所以只是 調用了 $worker->onWorkerReload() ,進程并沒有重啟
b. worker 實例 不支持重載 時,重載信號基本沒什么用了
----
**子進程上(worker):**
1. 取一個 worker 實例 (其實子進程的 Worker::$_workers 上也只有一個當前實例)
2. 調用 $worker->onWorkerReload() ,如果有異常 則調用 Worker::stopAll(250, $e);
3. $worker->reloadable === true Worker::stopAll()
----
### stop/restart 命令的區別
這兩個命令都在 一個 switch 條件分支上,都是 向主進程發送 停止信號(平滑停止 和 非平滑停止):
1. 向主進程發送停止信號
2. 不斷檢測主進程是否停止 \usleep(10000)
3. 當 非平滑停止信號時,超過 5s 主進程還未停止,那么 stop fail 停止失敗
4. 當 主進程停止成功了,兩個命令的區別就來了:
a. $command === 'stop' exit
b. $command === 'restart' 繼續向下執行,相當于 使用 `start` 命令了,效果就是 舊服務停止后,又在當前命令行下重新啟動一次(不過這個有個缺陷,-g -d 無法同時設置)
總結:restart = stop + start , 但支持的不好,-g -d 無法同時設置
----
### restart 與 reload 的區別
#### restart
重啟,重新啟動,停止整個服務后,再重新啟動,停止有 平滑停止 和 非平滑停止,服務完全停止時 會中斷服務
----
#### reload
重載(平滑重啟) 讓舊的子進程依次停止退出,退出一個舊進程,會立馬補一個新的,新舊進程一個接一個交替,相當于不會中斷服務的重啟,停止有 平滑 和 非平滑停止
----
### 什么是平滑重啟?
平滑重啟不同于普通的重啟,平滑重啟可以做到在不影響用戶的情況下重啟服務,以便重新載入PHP程序,完成業務代碼更新。
平滑重啟一般應用于業務更新或者版本發布過程中,能夠避免因為代碼發布重啟服務導致的暫時性服務不可用的影響。
注意:只有子進程運行過程中載入的文件支持reload,主進程載入的文件不支持reload。或者說Worker::runAll執行完后workerman運行過程中動態加載的文件支持reload,Worker::runAll執行前就載入的文件代碼不支持reload
### 平滑重啟原理
WorkerMan分為主進程和子進程,主進程負責監控子進程,子進程負責接收客戶端的連接和連接上發來的請求數據,做相應的處理并返回數據給客戶端。當業務代碼更新時,其實我們只要更新子進程,便可以達到更新代碼的目的。
當WorkerMan主進程收到平滑重啟信號時,主進程會向其中一個子進程發送安全退出(讓對應進程處理完畢當前請求后才退出)信號,當這個進程退出后,主進程會重新創建一個新的子進程(這個子進程載入了新的PHP代碼),然后主進程再次向另外一個舊的進程發送停止命令,這樣一個進程一個進程的重啟,直到所有舊的進程全部被置換為止。
我們看到平滑重啟實際上是讓舊的業務進程逐個退出然后并逐個創建新的進程做到的。為了在平滑重啟時不影響客用戶,這就要求進程中不要保存用戶相關的狀態信息,即業務進程最好是無狀態的,避免由于進程退出導致信息丟失。
平滑要做到兩點:
1. 現有的連接,正在服務的不能影響
2. 重啟過程中 不中斷在線服務,可以降低服務吞吐能力,但不能完全喪失服務能力。
----
### 嵌套子 worker
```php
<?php
$worker = new Worker();
$worker->onWorkerStart(function ($worker)
{
$inner_text_worker = new Worker('Text://0.0.0.0:5678');
$inner_text_worker->onMessage = function($connection, $buffer)
{
};
$inner_text_worker->listen();
});
Worker::runAll();
```
----
### 記一次 Crontab 的計劃任務失效的問題
~~~
問題: Crontab 的計劃任務都失效了?
1. application/daemon/test/start.php 中 $worker->onWorkerExit (父進程上下文) 中,用了 new workerBusiness () ,而其 __construct 中有 new Crontab()
2. 即此時 父進程 執行了一次 new Crontab() ,然后 forkWorkers 子進程,導致 fork 過來的 子進程的 Crontab 也污染了,所以子進程 在用 計劃任務就用不了。
3. 根本原因:父進程 不能使用 new Crontab(),否則會導致子進程 Crontab 無法再使用了
問題引入原因: 上周六代碼調整 將 new Crontab() 引入到 __construct 中去了(統一 默認定時器 解決內存等問題)
修復方案: $worker->onWorkerExit 父進程的 上下文 內 不在 new workerBusiness 了,改用 $worker->workerBusiness 就行了
~~~
> 這個問題只在 子進程 退出后 在被拉起時才會出現,所以 28號運行一天后,重啟的子進程的計劃任務就失效了
問題代碼:
```php
$worker->onWorkerExit = function ($worker, $status, $pid) use ($_started, $spugEnvKey) {
call_user_func([getWorkerBusinessInstance($worker, $_started['file']), 'onWorkerExit'], $worker, $status, $pid, $spugEnvKey);
};
```
修復為:
```php
$worker->onWorkerExit = function ($worker, $status, $pid) use ($_started, $spugEnvKey) {
call_user_func([$worker->workerBusiness, 'onWorkerExit'], $worker, $status, $pid, $spugEnvKey);
};
```
----
### 信號處理
給進程發送信號,會立即打斷進程的睡眠。比如 進程睡眠 50s ,但是向其發送了信號,睡眠立即中斷完成(睡眠立即結束),代碼繼續向下執行。
```php
<?php
function sig()
{
echo 'sig' . PHP_EOL;
}
pcntl_signal(SIGTERM, "sig"); // kill -15 pid
while (1) {
// pcntl_signal_dispatch(); // Calls signal handlers for pending signals
echo time() . PHP_EOL;
sleep(50); // 信號會打斷睡眠
// 注意:一次信號只能打斷一個睡眠,所以 worker 中用 sleep 有可能阻塞(所以不建議用)
// sleep(50);
}
```
[PHP: sleep - Manual](https://www.php.net/manual/zh/function.sleep.php)
> 如果函數的調用被一個信號中止,sleep() 會返回一個非零的值。在 Windows 上,該值總是 192(即 Windows API 常量 WAIT_IO_COMPLETION 的值)。其他平臺上,該返回值是剩余未 sleep 的秒數。
[PHP: stream_select - Manual](https://www.php.net/manual/zh/function.stream-select)
> On success stream_select() returns the number of stream resources contained in the modified arrays, which may be zero if the timeout expires before anything interesting happens. On error false is returned and a warning raised (this can happen if the system call is interrupted by an incoming signal). 成功時,stream_select() 返回修改后數組中包含的流資源數量,如果超時時間已過,在發生任何有趣的事情之前,該數量可能為零。如果出錯,則返回 false 并發出警告(如果系統調用被接收到的信號中斷,則可能發生這種情況)。 **(同樣也會被信號打斷)**
----
### 參考
https://www.workerman.net/doc/workerman/install/start-and-stop.html
[如何在php后端及時推送消息給客戶端-workerman社區](https://www.workerman.net/q/508)
[linux每日命令(34):ps命令和pstree命令 - 聽風。 - 博客園](https://www.cnblogs.com/huchong/p/10065246.html)
[Workerman開源框架的作者 - luckc# - 博客園](https://www.cnblogs.com/luckcs/articles/6783249.html)
[Life and Death of a Linux Process :: Tech Notes by Natan Yellin](https://natanyellin.com/posts/life-and-death-of-a-linux-process/)(進程生從何來,死往何去)
- 開始
- 公益
- 更好的使用看云
- 推薦書單
- 優秀資源整理
- 技術文章寫作規范
- SublimeText - 編碼利器
- PSR-0/PSR-4命名標準
- php的多進程實驗分析
- 高級PHP
- 進程
- 信號
- 事件
- IO模型
- 同步、異步
- socket
- Swoole
- PHP擴展
- Composer
- easyswoole
- php多線程
- 守護程序
- 文件鎖
- s-socket
- aphp
- 隊列&并發
- 隊列
- 講個故事
- 如何最大效率的問題
- 訪問式的web服務(一)
- 訪問式的web服務(二)
- 請求
- 瀏覽器訪問阻塞問題
- Swoole
- 你必須理解的計算機核心概念 - 碼農翻身
- CPU阿甘 - 碼農翻身
- 異步通知,那我要怎么通知你啊?
- 實時操作系統
- 深入實時 Linux
- Redis 實現隊列
- redis與隊列
- 定時-時鐘-阻塞
- 計算機的生命
- 多進程/多線程
- 進程通信
- 拜占庭將軍問題深入探討
- JAVA CAS原理深度分析
- 隊列的思考
- 走進并發的世界
- 鎖
- 事務筆記
- 并發問題帶來的后果
- 為什么說樂觀鎖是安全的
- 內存鎖與內存事務 - 劉小兵2014
- 加鎖還是不加鎖,這是一個問題 - 碼農翻身
- 編程世界的那把鎖 - 碼農翻身
- 如何保證萬無一失
- 傳統事務與柔性事務
- 大白話搞懂什么是同步/異步/阻塞/非阻塞
- redis實現鎖
- 淺談mysql事務
- PHP異常
- php錯誤
- 文件加載
- 路由與偽靜態
- URL模式之分析
- 字符串處理
- 正則表達式
- 數組合并與+
- 文件上傳
- 常用驗證與過濾
- 記錄
- 趣圖
- foreach需要注意的問題
- Discuz!筆記
- 程序設計思維
- 抽象與具體
- 配置
- 關于如何學習的思考
- 編程思維
- 談編程
- 如何安全的修改對象
- 臨時
- 臨時筆記
- 透過問題看本質
- 程序后門
- 邊界檢查
- session
- 安全
- 王垠
- 第三方數據接口
- 驗證碼問題
- 還是少不了虛擬機
- 程序員如何談戀愛
- 程序員為什么要一直改BUG,為什么不能一次性把代碼寫好?
- 碎碎念
- 算法
- 實用代碼
- 相對私密與絕對私密
- 學習目標
- 隨記
- 編程小知識
- foo
- 落盤
- URL編碼的思考
- 字符編碼
- Elasticsearch
- TCP-IP協議
- 碎碎念2
- Grafana
- EFK、ELK
- RPC
- 依賴注入
- 科目一
- 開發筆記
- 經緯度格式轉換
- php時區問題
- 解決本地開發時調用遠程AIP跨域問題
- 后期靜態綁定
- 談tp的跳轉提示頁面
- 無限分類問題
- 生成微縮圖
- MVC名詞
- MVC架構
- 也許模塊不是唯一的答案
- 哈希算法
- 開發后臺
- 軟件設計架構
- mysql表字段設計
- 上傳表如何設計
- 二開心得
- awesomes-tables
- 安全的代碼部署
- 微信開發筆記
- 賬戶授權相關
- 小程序獲取是否關注其公眾號
- 支付相關
- 提交訂單
- 微信支付筆記
- 支付接口筆記
- 支付中心開發
- 下單與支付
- 支付流程設計
- 訂單與支付設計
- 敏感操作驗證
- 排序設計
- 代碼的運行環境
- 搜索關鍵字的顯示處理
- 接口異步更新ip信息
- 圖片處理
- 項目搭建
- 閱讀文檔的新方式
- mysql_insert_id并發問題思考
- 行鎖注意事項
- 細節注意
- 如何處理用戶的輸入
- 不可見的字符
- 抽獎
- 時間處理
- 應用開發實戰
- python 學習記錄
- Scrapy 教程
- Playwright 教程
- stealth.min.js
- Selenium 教程
- requests 教程
- pyautogui 教程
- Flask 教程
- PyInstaller 教程
- 蜘蛛
- python 文檔相似度驗證
- thinkphp5.0數據庫與模型的研究
- workerman進程管理
- workerman網絡分析
- java學習記錄
- docker
- 筆記
- kubernetes
- Kubernetes
- PaddlePaddle
- composer
- oneinstack
- 人工智能 AI
- 京東
- pc_detailpage_wareBusiness
- doc
- 電商網站設計
- iwebshop
- 商品規格分析
- 商品屬性分析
- tpshop
- 商品規格分析
- 商品屬性分析
- 電商表設計
- 設計記錄
- 優惠券
- 生成唯一訂單號
- 購物車技術
- 分類與類型
- 微信登錄與綁定
- 京東到家庫存系統架構設計
- crmeb
- 命名規范
- Nginx https配置
- 關于人工智能
- 從人的思考方式到二叉樹
- 架構
- 今日有感
- 文章保存
- 安全背后: 瀏覽器是如何校驗證書的
- 避不開的分布式事務
- devops自動化運維、部署、測試的最后一公里 —— ApiFox 云時代的接口管理工具
- 找到自己今生要做的事
- 自動化生活
- 開源與漿果
- Apifox: API 接口自動化測試指南