問題背景
公司有一套訊息推送系統(簡稱GCM),由于人事變動接手了其中的客戶端部分,看了一下檔案,僅通訊協議部分有幾頁簡單的說明,代碼呢又多又亂,一時理不出一個頭緒,由于訊息是從后臺推送到端的,所以使用了 tcp 長連接通道來保證訊息的及時性,基于 http 的一堆分析工具(如 postman)完全沒有用武之地,因此決定寫個小工具來模擬 tcp 上的通訊協議,作為深入熟悉代碼之前的熱身,
問題的解決
一開始想用 c++ 來寫這個工具,但是想到 socket 一連串經典的(socket / bind / connect / send / recv…)的繁瑣呼叫我還是算了,之前用 shell 寫過幾個小工具很舒爽,但那都是借用 curl 命令來處理 http 協議,面對 tcp 協議 curl 肯定是無能為力了,因為命令執行完成后連接也就斷開了,無法模擬長連接,那是不是就不能用 shell 寫了呢?非也,
連接的建立與斷開
我突然想到 shell 本身好像可以支持將 tcp 連接打開為檔案:
exec N <> /dev/tcp/host/port
上面這段腳本就可以在句柄為 N 的檔案上打開到 host 且埠為 port 的 tcp 連接了,并且可以進行雙向讀寫,于是趕快在 msys2 中試了一下:
1 exec 3<>/dev/tcp/$gcm_host/$gcm_port 2 ret=$? 3 echo "open tcp $ret" 4 if [ $ret != 0 ]; then 5 echo "connect to gcmserver failed" 6 exit 1 7 fi 8 9 echo "connect with server"
這里腳本直接使用標準輸入(0)、輸出(1)、錯誤(2)之后的句柄 3 作為連接句柄,跑了一下,似乎什么也沒有發生:

好在 Windows 上有 procexp 工具,可以查看行程創建的所有 tcp 連接:

看起來這個連接確實建立成功了,當然你也可以用 windows 上的 netstat 命令查看:
C:\Users\yunh>netstat -no
活動連接
協議 本地地址 外部地址 狀態 PID
TCP 10.2.56.38:1993 10.100.200.2:10003 ESTABLISHED 10320
TCP 10.2.56.38:2346 175.27.0.15:80 ESTABLISHED 14808
TCP 10.2.56.38:2474 121.51.139.161:8080 ESTABLISHED 15092
TCP 10.2.56.38:3147 10.2.56.13:7680 ESTABLISHED 8816
TCP 10.2.56.38:3576 47.97.243.182:80 ESTABLISHED 11292
TCP 10.2.56.38:3602 10.0.24.13:28888 ESTABLISHED 16224
TCP 10.2.56.38:3720 113.96.233.143:443 ESTABLISHED 15252
TCP 10.2.56.38:5006 10.2.61.20:7680 ESTABLISHED 8816
TCP 10.2.56.38:5022 10.2.25.16:7680 ESTABLISHED 8816
TCP 10.2.56.38:5303 49.232.126.211:443 ESTABLISHED 11292
TCP 10.2.56.38:6182 10.0.109.249:443 ESTABLISHED 16168
TCP 10.2.56.38:6183 10.0.109.249:443 ESTABLISHED 16168
TCP 10.2.56.38:6357 52.11.109.209:443 ESTABLISHED 11292
TCP 10.2.56.38:6697 40.90.189.152:443 ESTABLISHED 5268
TCP 10.2.56.38:7065 117.18.237.29:80 CLOSE_WAIT 4724
TCP 10.2.56.38:7100 220.170.53.122:443 TIME_WAIT 0
TCP 10.2.56.38:7113 220.181.174.166:443 TIME_WAIT 0
TCP 10.2.56.38:7117 180.163.150.166:443 ESTABLISHED 11292
TCP 10.2.56.38:7135 140.143.52.226:443 TIME_WAIT 0
TCP 10.2.56.38:7141 10.0.24.13:8888 CLOSE_WAIT 16224
TCP 10.2.56.38:7143 101.201.169.146:443 TIME_WAIT 0
TCP 10.2.56.38:7144 103.15.99.107:443 TIME_WAIT 0
TCP 10.2.56.38:7148 203.119.214.115:443 TIME_WAIT 0
TCP 10.2.56.38:7149 61.151.167.89:443 TIME_WAIT 0
TCP 10.2.56.38:7150 203.119.169.141:443 TIME_WAIT 0
TCP 10.2.56.38:7151 203.119.144.59:443 TIME_WAIT 0
TCP 10.2.56.38:7159 114.55.187.58:443 ESTABLISHED 11292
TCP 10.2.56.38:7160 42.121.254.191:443 TIME_WAIT 0
TCP 10.2.56.38:7162 118.178.109.187:443 TIME_WAIT 0
TCP 10.2.56.38:7165 47.110.223.99:443 TIME_WAIT 0
TCP 10.2.56.38:7166 116.62.93.118:443 TIME_WAIT 0
TCP 10.2.56.38:7195 123.150.76.171:80 CLOSE_WAIT 10772
TCP 10.2.56.38:6974 ################## ESTABLISHED 10984
TCP 10.2.56.38:7215 192.168.0.9:80 CLOSE_WAIT 4700
TCP 10.2.56.38:7218 10.2.100.217:7680 SYN_SENT 8816
TCP 10.2.56.38:7219 192.168.56.1:7680 SYN_SENT 8816
TCP 10.2.56.38:7680 10.2.102.27:53199 ESTABLISHED 8816
TCP 10.2.56.38:9763 192.168.23.23:49156 ESTABLISHED 4600
TCP 10.2.56.38:10267 125.39.132.161:80 ESTABLISHED 10772
TCP 10.2.56.38:10816 60.205.204.27:80 ESTABLISHED 10872
TCP 127.0.0.1:443 127.0.0.1:7216 ESTABLISHED 8108
TCP 127.0.0.1:2002 127.0.0.1:2003 ESTABLISHED 11292
TCP 127.0.0.1:2003 127.0.0.1:2002 ESTABLISHED 11292
TCP 127.0.0.1:2013 127.0.0.1:2014 ESTABLISHED 9600
TCP 127.0.0.1:2014 127.0.0.1:2013 ESTABLISHED 9600
TCP 127.0.0.1:2015 127.0.0.1:2016 ESTABLISHED 12948
TCP 127.0.0.1:2016 127.0.0.1:2015 ESTABLISHED 12948
TCP 127.0.0.1:2040 127.0.0.1:2041 ESTABLISHED 13960
TCP 127.0.0.1:2041 127.0.0.1:2040 ESTABLISHED 13960
TCP 127.0.0.1:2109 127.0.0.1:2110 ESTABLISHED 15092
TCP 127.0.0.1:2110 127.0.0.1:2109 ESTABLISHED 15092
TCP 127.0.0.1:2349 127.0.0.1:50051 ESTABLISHED 6308
TCP 127.0.0.1:2566 127.0.0.1:30031 ESTABLISHED 10624
TCP 127.0.0.1:3032 127.0.0.1:3033 ESTABLISHED 20276
TCP 127.0.0.1:3033 127.0.0.1:3032 ESTABLISHED 20276
TCP 127.0.0.1:3517 127.0.0.1:3518 ESTABLISHED 18200
TCP 127.0.0.1:3518 127.0.0.1:3517 ESTABLISHED 18200
TCP 127.0.0.1:3768 127.0.0.1:3769 ESTABLISHED 14076
TCP 127.0.0.1:3769 127.0.0.1:3768 ESTABLISHED 14076
TCP 127.0.0.1:3854 127.0.0.1:3855 ESTABLISHED 17380
TCP 127.0.0.1:3855 127.0.0.1:3854 ESTABLISHED 17380
TCP 127.0.0.1:4895 127.0.0.1:4896 ESTABLISHED 15524
TCP 127.0.0.1:4896 127.0.0.1:4895 ESTABLISHED 15524
TCP 127.0.0.1:5320 127.0.0.1:5321 ESTABLISHED 16736
TCP 127.0.0.1:5321 127.0.0.1:5320 ESTABLISHED 16736
TCP 127.0.0.1:6688 127.0.0.1:10803 ESTABLISHED 10872
TCP 127.0.0.1:6688 127.0.0.1:10824 ESTABLISHED 10872
TCP 127.0.0.1:6688 127.0.0.1:10841 ESTABLISHED 10872
TCP 127.0.0.1:6688 127.0.0.1:10849 ESTABLISHED 10872
TCP 127.0.0.1:6689 127.0.0.1:10819 ESTABLISHED 10672
TCP 127.0.0.1:7187 127.0.0.1:443 TIME_WAIT 0
TCP 127.0.0.1:7216 127.0.0.1:443 ESTABLISHED 10548
TCP 127.0.0.1:8419 127.0.0.1:8420 ESTABLISHED 14716
TCP 127.0.0.1:8420 127.0.0.1:8419 ESTABLISHED 14716
TCP 127.0.0.1:10803 127.0.0.1:6688 ESTABLISHED 2256
TCP 127.0.0.1:10819 127.0.0.1:6689 ESTABLISHED 13436
TCP 127.0.0.1:10824 127.0.0.1:6688 ESTABLISHED 10672
TCP 127.0.0.1:10841 127.0.0.1:6688 ESTABLISHED 15448
TCP 127.0.0.1:10849 127.0.0.1:6688 ESTABLISHED 9772
TCP 127.0.0.1:30031 127.0.0.1:2566 ESTABLISHED 10608
TCP 127.0.0.1:50051 127.0.0.1:2349 ESTABLISHED 10608
TCP [::1]:5900 [::1]:5901 ESTABLISHED 10548
TCP [::1]:5901 [::1]:5900 ESTABLISHED 10548
TCP [::1]:7188 [::1]:8307 FIN_WAIT_2 8108
TCP [::1]:7217 [::1]:8307 ESTABLISHED 8108
TCP [::1]:8307 [::1]:7188 CLOSE_WAIT 8108
TCP [::1]:8307 [::1]:7217 ESTABLISHED 8108
這里主要是通過過濾行程 ID 來實作快速定位的,連接也可以被主動關閉,這需要使用下面的重定向語法(其實就是關閉普通檔案):
exec N < &-
其中 N 就是剛才打開的檔案句柄,可以用 > 等效替換 <,在 msys2 中就可以這樣驗證了:

最后仍然是通過 procexp 工具或 netstat 命令來查看執行結果,另外使用 echo $? 獲取 exec 執行結果為 0 似乎并不能確認連接已經建立,因為我對一個錯誤的 host + port 使用 exec 仍然能得到 0,
機器上下線
連接建立好以后,需要向后臺上報本機的一些基本資訊,這個協議稱為機器上線:
1 function send_request_100 () 2 { 3 local msg=$(cat protocol/100.gcm) 4 # do replace 5 msg=$(echo "$msg" | jq --arg guid "$devid" --arg hwid "$hardid" -c '{ version, msgtype, guid: $guid, devinfo: { hwid: $hwid, devid: $guid, os: .devinfo.os, os_version: .devinfo.os_version, sysbit: .devinfo.sysbit, languageid: .devinfo.languageid } }') 6 echo $msg >&3 7 local ret=$? 8 if [ $ret -ne 0 ]; then 9 echo "connection break, send failed" 10 exit 3 11 fi 12 } 13 14 # online myself 15 send_request_100
機器上線的程序被封裝成了一個函式:send_request_100,這里 100 是機器上線的訊息號,其實訊息發送就是一句代碼的事兒(line 6),這個函式的主要作業是組裝 100 協議的內容(line 3-5),訊息都是 json 格式的串,為了降低代碼與協議的耦合度,這里把每個協議都放在單獨的檔案中,例如上面的 “100.gcm” 檔案存放的就是機器上線的訊息模板:
{
"version": "3.1",
"msgtype": "100",
"guid": "",
"devinfo": {
"hwid": "",
"devid": "",
"os": "Windows",
"os_version": "7",
"sysbit": "64",
"languageid": "2052"
}
}
從檔案中讀取到本地變數后,需要做一些填充作業(guid / hwid / devid… 欄位),這里使用了 jq 命令的 --arg 選項來傳遞外部引數并基于它們重新捏合 json 串,這些引數(devid / hardid)又是在腳本啟動前就從注冊表中讀取并傳入的,機器上線后才可以進行產品的上下線,而且相應的,當客戶端停止時,也要告訴后臺機器下線:
1 function send_request_101 2 { 3 local msg=$(cat protocol/101.gcm) 4 # do replace 5 msg=$(echo "$msg" | jq --arg guid "$devid" -c '{ version, msgtype, guid: $guid }') 6 echo $msg >&3 7 local ret=$? 8 echo "send 101 msg to gcm $ret" >> log.txt 9 if [ $ret -ne 0 ]; then 10 echo "connection break, send failed" 11 exit 3 12 fi 13 14 # no response for 101 message 15 echo "offline success! devid=$devid" 16 } 17 18 # -1st offline myself 19 send_request_101
這個程序封裝在 send_request_101 函式中,這里 101 是機器下線的訊息號,同樣的,這個訊息也有模板檔案:
{
"version": "3.1",
"msgtype": "101",
"guid": ""
}
相對簡單一點,protocol 子目錄包含了所有訊息協議的模板:
$ ls -lhrt total 7.0K -rw-r--r-- 1 yunh 1049089 312 5月 28 2019 102.gcm -rw-r--r-- 1 yunh 1049089 102 5月 28 2019 101.gcm -rw-r--r-- 1 yunh 1049089 350 5月 28 2019 100.gcm -rw-r--r-- 1 yunh 1049089 141 5月 28 2019 412.gcm -rw-r--r-- 1 yunh 1049089 166 5月 28 2019 108.gcm -rw-r--r-- 1 yunh 1049089 193 5月 28 2019 103.gcm -rw-r--r-- 1 yunh 1049089 478 7月 26 2019 custom.gcm
機器下線的其它處理流程和上線差不太多,這里就不贅述了,后面不會對訊息內容做詳細介紹了,主要是涉及到協議保密的問題,
產品上下線
機器在開機上線,產品在啟動時上線,這樣當后臺有推送內容時,相應的訊息就可以推送過來(不會對沒上線的產品推送):
1 # $1: app name 2 # $2: app version 3 # $3: user id 4 # $4: device id 5 function send_request_102 () 6 { 7 local guid=$(echo "$4$1$3" | sha1sum | awk '{ print $1 }') 8 local msg=$(cat protocol/102.gcm) 9 # do replace 10 msg=$(echo "$msg" | jq --arg appname "$1" --arg appver "$2" --arg userid "$3" --arg guid "$guid" -c '{ version, msgtype, guid: $guid, appclientid: $appname, appuserid: $userid, clientinfo: { appversion: $appver, platform: .clientinfo.platform, bits: .clientinfo.bits } }') 11 echo $msg >&3 12 local ret=$? 13 echo "send 102 msg to gcm $ret" >> log.txt 14 if [ $ret -ne 0 ]; then 15 echo "connection break, send failed" 16 exit 3 17 fi 18 } 19 20 # online GCMPopBox/GUX/GSUP 21 send_request_102 "GCMPopBox" "2.0.0.0" "$hardid" "$devid" 22 send_request_102 "GUX" "$version" "$devid" "$devid" 23 send_request_102 "GSUP" "$version" "$devid" "$devid"
這個程序封裝在 send_request_102 函式中,這里 102 是產品上線的訊息號,這個函式接收四個引數,分別是產品標識、產品版本、用戶標識和機器標識,在機器上線后,會固定上線三個產品:GCMPopBox、GUX 和 GSUP,都是客戶端自帶的幾個服務產品,當產品關閉時,要向后臺發送產品下線訊息:
1 # $1: app name 2 # $2: user id 3 # $3: device id 4 function send_request_103 5 { 6 local guid=$(echo "$3$1$2" | sha1sum | awk '{print $1}') 7 local msg=$(cat protocol/103.gcm) 8 # do replace 9 msg=$(echo "$msg" | jq --arg appname "$1" --arg userid "$2" --arg guid "$guid" -c '{ version, msgtype, guid: $guid, appclientid: $appname, appuserid: $userid }') 10 echo $msg >&3 11 local ret=$? 12 echo "send 103 msg to gcm $ret" >> log.txt 13 if [ $ret -ne 0 ]; then 14 echo "connection break, send failed" 15 exit 3 16 fi 17 18 # no response for 103 message 19 echo "$1 offline success! userid=$2" 20 } 21 22 # -2nd offline GCMPopBox/GUX/GSUP 23 send_request_103 "GCMPopBox" "$hardid" "$devid" 24 send_request_103 "GUX" "$devid" "$devid" 25 send_request_103 "GSUP" "$devid" "$devid"
這個程序封裝在 send_request_103 函式中,其中 103 是產品下線的訊息號,和產品上線訊息相比,不用再提供產品版本了,其它方面大同小異,在機器下線前,需要對之前上線的幾個客戶端服務產品一一下線(GCMPopBox / GUX / GSUP),除了固定的產品,用戶也可以在命令列指定某個產品去上線,這個工具跑起來后長這個樣子:

紅框中的部分其實是一個回圈,用戶可以不停的輸入要上下線的產品進行操作,這部分的代碼相應的也位于一段 while 回圈中:
1 # online/offline products 2 while : 3 do 4 echo "-------------------------------------------" 5 echo -n "product name to operate (exit|quit to quit): " 6 read product 7 if [ "$product" == "exit" -o "$product" == "quit" ]; then 8 break; 9 fi 10 11 echo -n "operation (online|offline): " 12 read resp 13 online=0 14 case "$resp" in 15 ""|"o"|"O"|"on"|"ON"|"online"|"ONLINE") 16 online=1 17 ;; 18 *) 19 ;; 20 esac 21 22 if [ $online == 1 ]; then 23 echo -n "version: " 24 read version 25 fi 26 27 echo -n "user id: " 28 read userid 29 30 if [ $online == 1 ]; then 31 send_request_102 "$product" "$version" "$userid" "$devid" 32 else 33 send_request_103 "$product" "$userid" "$devid" 34 fi 35 36 sleep 1 37 done
下面做個簡單講解:
- line 4-9:如果用戶輸入 quit 或 exit,退出回圈從而退出整個腳本,否則收集到要操作的產品標識;
- line 11-20:提示用戶輸入進行何種操作,上線 or 下線;
- line 22-25:如果是上線操作,則需要用戶輸入產品版本;
- line 27-28:提示用戶輸入用戶 ID;
- line 30-34:根據用戶輸入的操作型別,呼叫前面封裝好的函式來完成產品上下線,
接收推送訊息
產品上線成功后,就可以接收來自后臺的“問候”了,這塊和前面那種一問一答模式不一樣,需要異步處理連接上到達的資料,我的第一反應就是開個執行緒來處理,但是 shell 里并沒有執行緒這種東西,只有子行程可以用,問題是開子行程后原句柄 (3) 還能代表以前的連接嗎?在 linux 上這一點不容置疑,但是 windows 上可沒有 fork 這東東啊,怎么保證新啟動的子行程復制父行程的用戶空間呢?帶著疑問,我嘗試了下面的代碼:
1 echo "connect with server" 2 on_recv & 3 cpid=$!
將接收訊息相關代碼封裝在 on_recv 函式中,就可以直接用 ‘&’ 啟動一個單獨的行程去跑這個函式啦!作為測驗,一開始我只在 on_recv 中處理了幾個簡單的應答訊息(100->201,102->301……):
1 function on_recv 2 { 3 # can not break read ! 4 #trap "echo recv exit signal from parent" INT 5 while : 6 do 7 #read msg <&3 8 #msg=$(tail -f <&3) 9 #read -t 1 msg <&3 10 local msg="" 11 read -d '}' msg <&3 12 if [ -z "$msg" ]; then 13 echo -e "\nconnection break, receive failed" 14 exit 3 15 fi 16 17 msg="$msg}" 18 local type=$(echo "$msg" | jq -r '.msgtype') 19 case "$type" in 20 "201") 21 local guid=$(echo "$msg" | jq -r '.guid') 22 echo "online success! devid=$guid" 23 ;; 24 "301") 25 local appname=$(echo "$msg" | jq -r '.appclientid') 26 local userid=$(echo "$msg" | jq -r '.dstuserid') 27 echo "$appname online success! userid=$userid" 28 ;; 29 *) 30 echo "unknown response type: $type" 31 #exit 32 ;; 33 esac 34 done 35 }
收到這幾個應答訊息時,會將其中關鍵的欄位列印在螢屏上,應答訊息同請求訊息一樣,也是純 json 格式,因此這里使用 jq 來做決議 (line 17-33),
不過難點倒不在這兒,真正讓我費了半天勁兒的地方是在讀取,可能有人說了,讀取有什么難的,直接 read 不就行了嗎?我一開始就是這樣做的 (line 7),然而 read 會一直卡在那里讀資料,即使已經有訊息讀到了也不回傳,簡單分析一下:看起來 read 在等一個結束標志,一般而言就是換行 '\n',這也是為什么你可以一直在 console 界面輸入內容、直到回車結束的原因,然而后臺應答訊息并沒有換行符作為訊息結束,于是我嘗試了另外一個方案,使用 tail -f 讀取連接中的內容 (line 8),然而沒有任何改進,
因此這里我又試了第三個方案 (line 9),為 read 增加了一個超時時間 (1s),這樣當時間足夠長了以后也能回傳之前讀到的訊息,缺點是 read 會每隔一秒中斷一次;然而我忽略了一個更嚴重的問題的,那就是當產品積壓了很多訊息沒有推送后,當它上線的一刻后臺會同時給它推送多個訊息,這樣一來,這個帶超時的 read 常常會將多個訊息粘在一起回傳,導致后面的決議出錯,
于是我試了現在這個方案 (line 11),告訴 read 一直讀、直到遇到 json 結尾符 '}',當然這也不是完全保險的,因為 json 中有可能存在嵌套的子結構、導致內部含有 ‘}’,但好在現有的協議中應答訊息都比較簡單,基本上一對花括號之內不會再有花括號了,所以可以這樣搞,這個腳本跑起來的效果,其實前面那張圖已經展示過了,這次重新劃一下重點:

可以看到,新的子行程可以很好的收到機器和產品上線的應答訊息(下線沒有應答訊息),看起來就像它與父行程共享了這個連接一樣,驗證了 msys2 這個功能沒問題后,下面就開始我們的重頭戲了 —— 接收后臺推送的訊息:
1 "105") 2 local guid=$(echo "$msg" | jq -r '.guid') 3 local appclientid=$(echo "$msg" | jq -r '.appclientid') 4 local msgid=$(echo "$msg" | jq -r '.msgid') 5 local msgbody=$(echo "$msg" | jq -r '.msgbody') 6 local appuserid=$(echo "$msg" | jq -r '.appuserid') 7 local dstuserid=$(echo "$msg" | jq -r '.dstuserid') 8 echo "" 9 echo "*******************************************" 10 echo "receive customer message " 11 echo "product: $appclientid" 12 echo "userid : $appuserid" 13 echo "msgid : $msgid" 14 local body_utf8=$(echo "$msgbody" | base64 -d) 15 local body=$(echo "$body_utf8" | iconv -f utf-8 -t gb2312) 16 echo "content: $body" 17 echo "*******************************************" 18 send_request_108 "$guid" "$appclientid" "$appuserid" "$msgid" 19 ;;
這里直接上 case 陳述句,105 是自定義訊息,這個應用自己“偷摸”處理掉就好啦,不用給用戶展示,這邊出于演示目的直接將訊息內容列印在螢屏上(有一些 base64 解碼及 utf8 編碼轉換的作業:line 14-15),收完訊息后,給后臺回復一個 108 訊息表示成功接收,send_request_108 與其它 send 函式大同小異,這里不展開說明了,真正復雜的部分是接收彈窗訊息:
1 "401") 2 local guid=$(echo "$msg" | jq -r '.guid') 3 local appclientid=$(echo "$msg" | jq -r '.appclientid') 4 local msgid=$(echo "$msg" | jq -r '.msgid') 5 local msgbody=$(echo "$msg" | jq -r '.msgbody') 6 local appuserid=$(echo "$msg" | jq -r '.appuserid') 7 local dstuserid=$(echo "$msg" | jq -r '.dstuserid') 8 echo "" 9 echo "*******************************************" 10 echo "receive popup message " 11 echo "product: $appclientid" 12 echo "userid : $appuserid" 13 echo "msgid : $msgid" 14 echo "" 15 local body=$(echo "$msgbody" | base64 -d) 16 local title_utf8=$(echo "$body" | jq -r '.title') 17 local title=$(echo "$title_utf8" | iconv -f utf-8 -t gb2312) 18 local content_utf8=$(echo "$body" | jq -r '.content') 19 local content=$(echo "$content_utf8" | iconv -f utf-8 -t gb2312) 20 local ctxurl=$(echo "$body" | jq -r '."content-url"') # - is not recognized 21 local image=$(echo "$body" | jq -r '.image') 22 local imgurl=$(echo "$body" | jq -r '."image-url"') # - is not recognized 23 echo "title : $title" 24 echo "content: $content" 25 echo "ctxurl : $ctxurl" 26 echo "image : $image" 27 echo "imgurl : $imgurl" 28 echo "*******************************************" 29 30 # prepare 108 message 31 send_request_108 "$guid" "$appclientid" "$appuserid" "$msgid" 32 sleep 1 33 # prepare 402 message 34 send_request_402 "$msg" "$hardid" 35 ;;
401 就是彈窗訊息,本來是要給用戶在螢屏右下角彈個小窗顯示的,這里為了簡化問題,也直接列印在螢屏上,收到 401 訊息后要先給后臺回復一個 108 表示成功接收,再回復一個 402 來表示彈窗最終結果,例如用戶點擊、關閉、查看詳情…等等,這里直接回傳用戶關閉作為模擬,下面是產品上線后,收到推送訊息的效果:

這里演示了兩個訊息,分別是彈窗訊息與自定義訊息,可以看到都能正常的決議與顯示,后臺也可以正常的統計到這兩個訊息的推送情況:

最后,當用戶退出操作回圈后,需要及時回收子行程:
1 exec 3>&- 2 kill -INT $cpid 3 wait
這里通過 kill 產生 INT 訊息來通知子行程退出接識訓圈,接著通過 wait 等待子行程完全退出,之前也嘗試在子行程中捕獲 (trap) INT 信號并優雅的退出,但是發現在 windows 環境下加了這個捕獲反而導致 read 不能被中斷了,so 放棄之,現在這種方式可能是直接把子行程給殺死了,雖然“暴力”一點,但是起碼可以正常作業,
后記
通過構建這個小工具,我甚至發現了協議檔案中書寫錯誤或不詳的地方,不過最讓我感到好奇的還是 —— windows 上是怎么實作兩個行程共享一個連接句柄的? 為了解答這個問題,祭出 procexp 大殺器:

可以看到連接只在父行程(20612)中展示,子行程(16844)中并沒有對應的連接,那它是怎么在連接上讀資料的呢?左看右看沒有看出什么頭緒,想用 msys2 的 lsof 命令查看下行程句柄,但是翻遍了安裝目錄也沒有找到這個命令,看來 msys2 也不是移植了所有的命令,不過好在 lsof 也是通過 proc 檔案子系統實作的,那能不能查看行程的 proc 目錄呢?答案是可以的:
$ ls -l /proc total 0 dr-xr-xr-x 3 yunh 1049089 0 11月 24 14:47 16796/ dr-xr-xr-x 3 yunh 1049089 0 11月 24 13:35 17992/ dr-xr-xr-x 3 yunh 1049089 0 11月 24 14:47 18468/ dr-xr-xr-x 3 yunh 1049089 0 11月 25 15:36 20828/ dr-xr-xr-x 3 yunh 1049089 0 11月 24 13:35 7464/ -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 cpuinfo lrwxrwxrwx 1 yunh 1049089 0 11月 25 15:36 cygdrive -> // -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 devices -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 filesystems -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 loadavg -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 meminfo -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 misc lrwxrwxrwx 1 yunh 1049089 0 11月 25 15:36 mounts -> self/mounts dr-xr-xr-x 2 yunh 1049089 0 11月 25 15:36 net/ -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 partitions dr-xr-xr-x 8 yunh 1049089 0 11月 25 15:36 registry/ dr-xr-xr-x 8 yunh 1049089 0 11月 25 15:36 registry32/ dr-xr-xr-x 8 yunh 1049089 0 11月 25 15:36 registry64/ lrwxrwxrwx 1 yunh 1049089 0 11月 25 15:36 self -> 20828/ -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 stat -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 swaps drwxrwx--- 1 Administrators 18 0 11月 25 15:36 sys/ dr-xr-xr-x 2 yunh 1049089 0 11月 25 15:36 sysvipc/ -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 uptime -r--r--r-- 1 yunh 1049089 0 11月 25 15:36 version
這是我在另一個終端中列印的內容,不過找了一下,沒有找到上面兩個行程 ID 對應的目錄,那在腳本里直接列印呢?
ls -lhrt /proc/self/
其中 self 就是指自己啦,將這句代碼分別放置在父行程連接建立后的位置與子行程 on_recv 函式開頭中,得到下面的輸出:
connect with server total 0 lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 0 -> /dev/null lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 1 -> /dev/cons0 lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 2 -> /tools/gsupgo/error.txt lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 3 -> socket:[1] lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 4 -> /proc/8532/fd total 0 lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 0 -> /dev/cons0 lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 1 -> /dev/cons0 lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 2 -> /tools/gsupgo/error.txt lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 3 -> socket:[1] lrwxrwxrwx 1 yunh Domain Users 0 Nov 25 15:31 4 -> /proc/15580/fd
上面一段是父行程的輸出,句柄 3 對應的確實是 tcp 連接;下面一段是子行程的輸出,看起來與父行程無異,最有意思的是兩個行程的 4 號檔案句柄,顯示出了它們各自的 pid,顯然和它們在 windows 上的行程 ID 是不一樣的,這也可能是之前我在 /proc 目錄下找不到它們的原因吧,但是再次查看 /proc 目錄,仍然沒有上面兩個 pid,可見這個 pid 可能只是局限于本行程組的 (?),并不是全域共享,因而也沒有什么利用價值,
探索到這就走到死胡同了,有了解 msys2 在 windows 上實作的大神請不吝賜教,
最后這個小工具沒有資源可供下載 —— 涉及到公司內部協議安全的問題,不過寫了這么多,相信拿來改改寫一個自己的也不是什么難事了吧~
參考
[1]. Linux shell腳本中發起tcp、udp連接
[2]. netstat--查看服務器[有效]連接數--統計埠并發數--access.log分析
[3]. jq add or update a value with multiple --arg
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/275644.html
標籤:其他
