MessageRPC

項目地址 :  https://github.com/kelin-xycs/MessageRPCgit

MessageRPC

一個 用 C# 實現的 使用 Message 的 RPCgithub

MessageRPC ,使用 Message 進行 RPC 通訊 。 每個調用(Call) 就是一次 發送消息(Send), 返回結果也是返回一條消息 。服務器

MessageRPC 的 RPC 協議格式 能夠算是 Http 的 一個 簡化版 。 包括了 Head 和 Body(Content) , Head 包含多個 Header , 目前定義的 Header 有 3 個: Parameters Error Content-Length , Parameters 用來傳遞參數,值的格式是 「id=001&name=小明&」 這樣,和 Http 查詢字符串 格式同樣 。 Error 用來 傳遞 錯誤信息 。 若是有 Error Header , 則 rpc 會拋出 RPCServerException 。網絡

Content-Length 用來表示 Body 的長度,能夠用 body 來傳遞 大二進制數據 。 好比 圖片 文件 等 。性能

Header 之間經過 \r\n 分隔, Head 和 Body 之間也經過 \r\n 分隔 , 這和 Http 是同樣的 。測試

這 3 個 Header 都是可選, 但最少要有 1 個,否則 服務器端 解析 時會報錯並關閉鏈接 。 Header 的值能夠爲空,如 「Parameters: 」 。編碼

Header 值 會通過 UrlEncode 編碼,具體的說是 Parameters 的參數字符串裏的 參數名 和 參數值 會 通過 UrlEncode 編碼 ,還有 Error 的 值也會通過 UrlEncode 編碼 。 Content-Length 的值是 數字(long),因此不須要 UrlEncode 。spa

在 通訊機制 上,採用了 鏈接池(SocketPool)的機制 , 每一個 Socket 的 存活期 是 2 分鐘,超過 2 分鐘 未被使用的 Socket 將被回收(關閉), 鏈接池大小沒有上限 。 這是爲了知足 實時響應性 和 吞吐量 。 就是說,若是 鏈接池 中的 Socket 不夠用, 會建立新的 Socket 。.net

由於採用了 鏈接池 , 因此在 數據通訊 上必須嚴格的準確,具體是指 每次發送的 Body(Content) 長度必須等於 Content-Length 的長度, 小於了必須 經過 SendVacancy() 發送 空字符 \0 來補齊 , 大於了必須截斷 。 也由於此, 在 協議通訊 中若是發生錯誤(異常),則會 關閉鏈接, 不然 上一次的 Content 可能被當成這一次的 Head, 或者, 這一次的 Head 被當成上一次 的 Content, 而且這種錯誤只要發生一次,以後極可能就一直錯誤下去,因此最好的作法就是把鏈接關閉 。線程

通常狀況,發生上述的 數據傳輸 的 異常,一般是由於 服務器 網絡 問題 或者 受到攻擊 。

開發中解決的一些問題 :

一般服務器不會主動關閉鏈接,若是意外關閉了鏈接(好比 Message RPC Host 進程重啓或關閉),那麼 客戶端(SocketPool) 不知道服務器端已關閉鏈接, 仍然會返回 鏈接池(SocketPool)中的 Socket 使用,但此時 Socket 已經失效,會拋出 「遠程主機強行關閉了一個已有的鏈接」 異常 。 爲了解決這個問題, 在 客戶端 向 服務器 Send() 的過程當中,在 Send Head 的時候,若是 Socket 拋出異常, 會讓 SocketPool 新建一個 Socket 從新 Send Head , 此時若是 服務器 已經恢復正常監聽,則 能夠正常 通訊 , 若是 服務器 仍然未恢復正常監聽,則 Socket 會拋出異常,此時即按正常流程繼續執行。

這樣作的緣由是,以目前瞭解的資料來看,客戶端 若是要知道 服務器 有沒有關閉鏈接 , 好像須要發送一個 測試消息 。 正常的狀況下每次 Send 以前都發送一個測試消息的話對 性能 比較浪費 。 因此就採用了 上述 的 重試 的 方式 。

我想之前用 Ado.net 的時候,有時候會拋出 「基礎鏈接已關閉 ……」 這樣的 異常,是否是跟上述相似的狀況。啊哈哈哈

另外一個問題是 服務器端 在 客戶端訪問以後一段時間客戶端沒有訪問的話,服務器端的 CPU 佔用率 會 上升到 30%左右,也有可能 70 以上 。 這個問題的緣由是,客戶端訪問以後一段時間客戶端沒有訪問的話 , 客戶端 鏈接池(Socket Pool)會將 Socket 回收掉(Shutdown() Close()), 客戶端 回收掉 Socket 以後, 服務器 會 Receive() 到 一個 長度爲 0 的數據 , 按照 微軟 docs 的說法 , 當對方主機 「優雅的」 關閉了鏈接後,己方會收到一個 長度爲 0 的數據 。 但在我之前寫的一個 Socket 程序裏,我記得 客戶端(WebClient) 一段時間以後會關閉鏈接,此時 服務器端 確實收到了 一個 長度爲 0 的數據 , 但當再次 Receive() 的時候,就會拋出異常 「你的本機上的軟件關閉了一個已有的鏈接」 (客戶端 和 服務器 都在我本機) , 但如今的狀況是,再次 Receive() 仍然會收到一個 一個 長度爲 0 的數據 , 並不會拋出異常 , 而個人程序邏輯是經過 異常 來判斷 客戶端 是否關閉鏈接 , 若是沒有異常則一直循環 Receive() 下去,因而就形成了無限循環 Receive() , 每次接收到 一個 長度爲 0 的數據 , 這樣至關於 「空轉」 , 就形成了 CPU 佔用率升高 。

那爲何 CPU 佔用率有時候是 30% , 有時候是 70% 呢 , 這個跟 服務器端 「空轉」 的 線程數有關, 若是 客戶端 和 服務器端 只創建了一個鏈接,那麼 服務器端也只有 1 個線程在監聽, 也就只有 1 個線程 「空轉」 , 在多核處理器上, 1 個線程空轉最多隻會 佔用 1 個核的資源,並不會佔用 100% 的 CPU 。 因此 30% 是隻有 1 個線程空轉時的狀況 , 70% 是 1 個以上的線程(Socket)空轉的狀況 。

對於這個問題,如今的解決辦法是若是 Receive 的數據長度爲 0 (Socket.Receive()方法的返回值), 則拋出異常,這樣就能夠關閉鏈接了。也避免了空轉 。

爲何 印象中 WebClient 將鏈接關閉後 服務器端的 Socket 第一次 Receive() 獲得 長度爲 0 的數據 而 第二次就拋出異常呢 ? 也許 WebClient 在 關閉鏈接 時 先調用了 Socket.DisConnect() 方法, 這是說 也許,由於沒有看過 WebClient 的代碼, 而個人程序中是直接調用 Shutdown() Close() 來關閉 Socket 的 。 沒有調用 DisConnect() 方法 , 不知會不會跟 DisConnect() 方法有關係 。 但 服務器端 按上述作法(Receive 的數據長度爲 0 則 拋出異常)來作應該也是合理, 由於 服務器端 沒法控制 客戶端 是以什麼方法來關閉鏈接 。

相關文章
相關標籤/搜索