主頁 > 作業系統 > 使用 shell 做 tcp 協議模擬

使用 shell 做 tcp 協議模擬

2021-04-14 06:07:28 作業系統

問題背景

公司有一套訊息推送系統(簡稱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/275642.html

標籤:Linux

上一篇:PHY暫存器

下一篇:【安全等保】Linux服務器基線安全--干貨

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • CA和證書

    1、在 CentOS7 中使用 gpg 創建 RSA 非對稱密鑰對 gpg --gen-key #Centos上生成公鑰/密鑰對(存放在家目錄.gnupg/) 2、將 CentOS7 匯出的公鑰,拷貝到 CentOS8 中,在 CentOS8 中使用 CentOS7 的公鑰加密一個檔案 gpg -a ......

    uj5u.com 2020-09-10 00:09:53 more
  • Kubernetes K8S之資源控制器Job和CronJob詳解

    Kubernetes的資源控制器Job和CronJob詳解與示例 ......

    uj5u.com 2020-09-10 00:10:45 more
  • VMware下安裝CentOS

    VMware下安裝CentOS 一、軟硬體準備 1 Centos鏡像準備 1.1 CentOS鏡像下載地址 下載地址 1.2 CentOS鏡像下載程序 點擊下載地址進入如下圖的網站,選擇需要下載的版本,這里選擇的是Centos8,點擊如圖所示。 決定選擇Centos8后,選擇想要的鏡像源進行下載,此 ......

    uj5u.com 2020-09-10 00:12:10 more
  • 如何使用Grep命令查找多個字串

    如何使用Grep 命令查找多個字串 大家好,我是良許! 今天向大家介紹一個非常有用的技巧,那就是使用 grep 命令查找多個字串。 簡單介紹一下,grep 命令可以理解為是一個功能強大的命令列工具,可以用它在一個或多個輸入檔案中搜索與正則運算式相匹配的文本,然后再將每個匹配的文本用標準輸出的格式 ......

    uj5u.com 2020-09-10 00:12:28 more
  • git配置http代理

    git配置http代理 經常遇到克隆 github 慢的問題,這里記錄一下幾種配置 git 代理的方法,解決 clone github 過慢。 目錄 git配置代理 git單獨配置github代理 git配置全域代理 配置終端環境變數 git配置代理 主要使用 git config 命令 git單獨 ......

    uj5u.com 2020-09-10 00:12:33 more
  • Linux npm install 裝包時提示Error EACCES permission denied解

    npm install 裝包時提示Error EACCES permission denied解決辦法 ......

    uj5u.com 2020-09-10 00:12:53 more
  • Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包

    Centos 7下安裝nginx,使用yum install nginx,提示沒有可用的軟體包。 18 (flaskApi) [root@67 flaskDemo]# yum -y install nginx 19 已加載插件:fastestmirror, langpacks 20 Loading ......

    uj5u.com 2020-09-10 00:13:13 more
  • Linux查看服務器暴力破解ssh IP

    在公網的服務器上經常遇到別人爆破你服務器的22埠,用來挖礦或者干其他嘿嘿嘿的事情~ 這種情況下正確的做法是: 修改默認ssh的22埠 使用設定密鑰登錄或者白名單ip登錄 建議服務器密碼為復雜密碼 創建普通用戶登錄服務器(root權限過大) 建立堡壘機,實作統一管理服務器 統計爆破IP [root ......

    uj5u.com 2020-09-10 00:13:17 more
  • CentOS 7系統常見快捷鍵操作方式

    Linux系統中一些常見的快捷方式,可有效提高操作效率,在某些時刻也能避免操作失誤帶來的問題。 ......

    uj5u.com 2020-09-10 00:13:31 more
  • CentOS 7作業系統目錄結構介紹

    作業系統存在著大量的資料檔案資訊,相應檔案資訊會存在于系統相應目錄中,為了更好的管理資料資訊,會將系統進行一些目錄規劃,不同目錄存放不同的資源。 ......

    uj5u.com 2020-09-10 00:13:35 more
最新发布
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:43:21 more
  • vim的常用命令

    Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,洗掉文本等等。這也是Vim啟動后的默認模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器默認模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩沖中插入文本。大多數新用戶希望文本編輯器編輯程序中一 ......

    uj5u.com 2023-04-20 08:42:36 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:26:53 more
  • 設定Windows主機的瀏覽器為wls2的默認瀏覽器

    這里以Chrome為例。 1. 準備作業 wsl是可以使用Windows主機上安裝的exe程式,出于安全考慮,默認情況下改功能是無法使用。要使用的話,終端需要以管理員權限啟動。 我這里以Windows Terminal為例,介紹如何默認使用管理員權限打開終端,具體操作如下圖所示: 2. 操作 wsl ......

    uj5u.com 2023-04-19 09:25:49 more
  • docker學習

    ###Docker概述 真實專案部署環境可能非常復雜,傳統發布專案一個只需要一個jar包,運行環境需要單獨部署。而通過Docker可將jar包和相關環境(如jdk,redis,Hadoop...)等打包到docker鏡像里,將鏡像發布到Docker倉庫,部署時下載發布的鏡像,直接運行發布的鏡像即可。 ......

    uj5u.com 2023-04-19 09:19:04 more
  • Linux學習筆記

    IP地址和主機名 IP地址 ifconfig可以用來查詢本機的IP地址,如果不能使用,可以通過install net-tools安裝。 Centos系統下ens33表示主網卡;inet后表示IP地址;lo表示本地回環網卡; 127.0.0.1表示代指本機;0.0.0.0可以用于代指本機,同時在放行設 ......

    uj5u.com 2023-04-18 06:52:01 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:50 more
  • 解決linux系統的kdump服務無法啟動的問題

    問題:專案麒麟系統服務器的kdump服務無法啟動,沒有相關日志無法定位問題。 1、查看服務狀態是關閉的,重啟系統也無法啟動 systemctl status kdump 2、修改grub引數,修改“crashkernel”為“512M(有的機器數值太大太小都會導致報錯,建議從128M開始試,或者加個 ......

    uj5u.com 2023-04-12 09:59:01 more
  • 你是不是暴露了?

    作者:袁首京 原創文章,轉載時請保留此宣告,并給出原文連接。 如果您是計算機相關從業人員,那么應該經歷不止一次網路安全專項檢查了,你肯定是收到過資訊系統技術檢測報告,要求你加強風險監測,確保你提供的系統服務堅實可靠了。 沒檢測到問題還好,檢測到問題的話,有些處理起來還是挺麻煩的,尤其是線上正在運行的 ......

    uj5u.com 2023-04-05 16:52:56 more
  • 細節拉滿,80 張圖帶你一步一步推演 slab 記憶體池的設計與實作

    1. 前文回顧 在之前的幾篇記憶體管理系列文章中,筆者帶大家從宏觀角度完整地梳理了一遍 Linux 記憶體分配的整個鏈路,本文的主題依然是記憶體分配,這一次我們會從微觀的角度來探秘一下 Linux 內核中用于零散小記憶體塊分配的記憶體池 —— slab 分配器。 在本小節中,筆者還是按照以往的風格先帶大家簡單 ......

    uj5u.com 2023-04-05 16:44:11 more