通向Golang的捷徑【19. 創建一個完整應用】

19.1 介紹

本章將開發一個完整的應用 goto, 它是一個可上線的 web 應用, 來自於 Andrew Gerrand 的講座, 這裏將分三個階段, 每個階段都會追加一些功能, 以便展現 Go 語言的更多特性, 它比第 15 章給出的 web 應用更加複雜.

• 第 1 版: 會使用一個 map 和一個結構, 以及 sync 包的 Mutex 和一個結構類型的創建工廠.
• 第 2 版: 可將 gob 格式的數據, 寫入文件中.
• 第 3 版: 將使用併發協程和併發通道, 重新編寫之前的應用
• 第 4 版: 將給出 json 格式的應用
• 第 5 版: 將使用 rpc 協議, 實現一個分佈式應用

19.2 應用項目的介紹

在瀏覽器中, 可輸入一些複雜的 url 地址, 而 web 服務功能可將這些複雜地址, 替換成簡單的 url 地址, 我們的項目類似於一個 web 服務, 它包含了兩個功能:

• 將超長 url 地址轉換成簡單地址, 比如:
http://maps.google.com/maps?f=q&source=s_q&hl=en&geocode=&q=tokyo&sll=37.0625,-95.677068
&sspn=68.684234,65.566406&ie=UTF8&hq=&hnear=Tokyo,+Japan&t=h&z=9
可轉換成http://goto/UrcGq,並存儲 url 地址中包含的數據

• 如果用戶請求了上述的簡單地址,web 服務可將簡單地址, 重定向到複雜的原始 url 地址, 也就是在瀏覽
器中輸入 B 地址,web 服務可重定向到 A 地址.

19.3 數據結構

以下將給出項目的第 1 版本,19.3 和 19.4 節將討論這一版本, 可在隨書代碼中找到 goto_v1.

當項目上線時, 將會收到大量簡單 url 地址的請求, 而其中一些請求, 是由複雜 url 地址轉換而來, 因此需要將簡單地址和複雜地址都保存起來, 因爲這兩類地址都是字符串, 並且相互關聯, 可將簡單地址視爲 key, 而複製地址可視爲 value, 所以能將它們保存在一個 map 中, 而幾乎所有的編程語言都給出了相似的類型, 比如哈希表, 字典等. 在 Go 語言中, 可使用內建的 map 類型, 即map[string]string,[] 中是 key 類型, 隨後是value 類型, 第 8 章已詳細介紹了 map, 在實際應用中, 需要爲 map 指定一個特殊的類型名, 比如type URLStore map[string]string, 它可將 url 長地址轉換爲 url 短地址.

之後可創建上述類型的變量 m,m := make(URLStore). 假定在 m 中, 需要將 http://google.com/轉換成http://goto/a,可使用以下語句:
在這裏插入圖片描述
同時可將http://goto/前綴也配置成一個 key, 這可節省一些空間, 因爲前綴都是一樣的, 無須重複保存, 所以使用 a 可獲取 url 長地址,url := m[」a」].

注意, 使用:= 賦值, 則無須將 url 定義爲 string 類型, 因爲編譯器可基於賦值的右側值, 推斷出 url 的類型.

提供線程的安全性

URLStore 變量可視爲一個存儲區, 當獲得一些傳輸數據, 大量 Redirect 類型的請求, 這時只能提供讀取操作,使用 url 短地址作爲 key, 獲取 url 長地址, 但 Add 類型的請求不同, 它可在 URLStore 變量中, 增加一組鍵值對, 如果同一時刻下, 出現大量的 Add 更新請求, 將產生問題,add 請求的處理能被同類型的其他請求所中斷,因此 url 長地址 (value) 可能沒有足夠時間來寫入, 由於讀取操作和修改操作同時進行, 讀取結果也可能出現錯誤,map 類型也無法保證在下一次更新之前, 當前的更新操作可以完成, 所以 map 變量將被多個併發請求所訪問, 但它無法爲線程提供安全性, 因此必須保證 URLStore(map) 類型, 能被線程安全訪問, 而最簡單和最經典的方式則是加鎖, 在 Go 語言的 sync 標準包中, 提供了 Mutex 類型, 參見 9.3 節.

也就是將 URLStore 類型設定爲結構類型, 其中將包含兩個數據域, 一是 map, 二是來自於 sync 包的 RWMutex:
在這裏插入圖片描述
一個 RWMutex 類型可包含兩個鎖, 一個用於讀取, 另一個用於寫入, 多個客戶端可同時獲取讀取鎖, 但只有一個客戶端可獲取到寫入鎖 (其實可忽略讀取鎖), 這使得上述的更新操作可實現高效串行, 並實現連續處理.

在 Get 函數中, 實現了 Redirect 類型的讀取請求, 而在 Set 函數中, 實現 Add 類型的寫入請求, 如下:
在這裏插入圖片描述
Get 函數可使用 url 短地址 (key), 可從 map 中獲取到 url 長地址 (value), 同時它從屬於 URLStore 類型, 進行讀取之前, 可使用語句 s.mu.RLock(), 獲取讀取鎖, 因此更新操作將無法中斷讀取操作, 當讀取完畢後, 可使用語句 s.mu.RUnlock(), 釋放讀取鎖, 同時掛起的更新操作可恢復運行, 如果 key 未包含在 map 中, 將返回字符串類型的默認值 (空串), 注意, 這裏的點操作符與 OO 語言很相似, 所以 s.mu.RLock() 意味着, 調用 s 數據域 mu 的方法 RLock().

Set 函數包含了兩個形參, 一個是 key, 另一個是 url 地址, 進行更新操作之前, 將獲取寫入鎖 (Lock), 因此其他更新將無法中斷當前更新, 該函數還可返回一個布爾值, 用於描述更新是否成功:
在這裏插入圖片描述
使用語句_, present := s.urls[key], 可測試 map 中是否包含了 key, 如果包含,present 將爲 true, 否則將爲 false, 以上即爲 comma, ok 格式, 如果 key 包含在 map 中,Set 函數將返回 false, 這時 map 並未更新, 因爲函數提前返回了 (因爲不允許重複使用同一個 url 地址), 如果 key 未包含在 map 中, 將被添加到 map 中,Set 函數將返回 true, 左側的 _ 是一個佔位符, 且表示該數值不會使用, 注意一下更新完成後的 Unlock() 函數.

使用 defer

以上給出的代碼相當簡單, 但容易忘記使用 Unlock(), 而在更復雜的代碼中, 更容易忘記 Unlock(), 或是將其放置在錯誤的地方, 這將引發難以跟蹤的錯誤, 基於以上原因,Go 語言提供了一個特殊的關鍵字 defer(參見 6.4節), 以便在鎖定操作之後, 自動進行解鎖, 也就是在函數退出之前, 自動調用 Unlock(), 如下:
在這裏插入圖片描述
在 Set 函數中也可使用 defer, 因此無須再考慮解鎖的問題:
在這裏插入圖片描述

創建 URLStore 變量的工廠函數

在 URLStore 結構中, 包含了一個 map 數據域, 但在使用前, 必須進行初始化, 因此可定義一個包含 New 前綴的函數, 以創建 URLStore 結構的實例, 同時該函數還能返回已創建的 URLStore 實例 (在大多數情況下, 該實例會是一個指針),
在這裏插入圖片描述
在返回的 URLStore 實例中, 將提供已初始化的 map 數據域, 但鎖定標誌無須初始化, 這也是 Go 語言中創建結構的標準方法, 同時可使用取地址操作符 &, 返回一個結構指針, 即 *URLStore, 使用語句var s = NewURLStore(), 可創建一個 URLStore 變量 s.

URLStore 變量的用法

爲了在 map 中添加長短 url 地址, 需調用 s 的 Set 方法, 該方法可返回一個布爾值, 因此它將封閉在一個 if 條件中,
在這裏插入圖片描述
爲了基於 url 短地址, 獲取 url 長地址, 可調用 s 的 Get 方法, 並可將 url 長地址返回給 url 變量,
在這裏插入圖片描述
在 Go 語言的實際編程中, 在 if 條件中, 通常會給出一條初始化語句, 同時還需要對 map 包含的鍵值對進行計數, 所以需要一個 Count(計數) 方法:
在這裏插入圖片描述
在這裏插入圖片描述
如果需要基於 url 長地址, 而獲取 url 短地址, 則需要另一個函數 genKey(n int) string {…}, 其中的 n 即爲
s.Count() 的當前值.

以下將創建一個 Put 方法, 它將包含一個 url 長地址, 並在函數中, 使用 genKey 生成一個 key, 之後可使用Set 方法, 保存已生成的 key 和 url 長地址, 再返回 key,
在這裏插入圖片描述
在 for 循環中,Set 方法可運行成功, 這意味着生成的 key 並未包含在 map 中, 之後可定義一個存儲區和存儲函數, 以實現 map 的保存 (參見 store.go 文件), 當然目前並不需要這類功能, 只需定義一個 web 服務器, 已提供 Add 和 Redirect 服務.

19.4 用戶接口: 前端 web 服務器

以下代碼可參見goto_v1\main.go 文件, 所有 Go 項目都會包含一個主函數 main(), 這與 C,C++ 和 Java 語言相同, 使用語句 http.ListenAndServe(」:8080」, nil), 可在 8080 端口上, 啓動一個本地 web 服務器. 在第 15章中已經詳細介紹了 http 包, 它可爲 web 服務器提供所需的功能, 這時 web 服務器將在一個死循環中, 監聽來自外部的請求, 但是在 web 服務器中, 必須定義外部請求的響應, 也就是調用 http 處理器 (HandleFunc 函數),
在這裏插入圖片描述
因此基於/add 路徑的所有請求, 將調用 Add 函數, 在當前項目中, 將給出兩個 http 處理器:
• Redirect, 可實現 url 短地址請求的重定向
• Add, 可實現新 url 地址的保存
在這裏插入圖片描述
以下給出了最小化的主函數 main(),
在這裏插入圖片描述
基於/add 路徑的請求, 將交給 Add 處理器, 而其他請求將交給 Redirect 處理器, 處理器函數可從外部請求(*http.Request 類型的變量) 中獲取信息, 並會創建一個 http.ResponseWriter 類型的變量 w, 同時會將外部請求, 寫入該變量.

在 Add 函數中, 可實現以下操作:
• 讀取 url 長地址, 也就是從 r.FormValue(」url」) 包含的 http 請求中, 讀取一個 html 格式的 url 地址
• 使用 Put 方法, 保存已讀取的 url 長地址
• 將基於 url 長地址所轉換的 url 短地址, 發送給用戶

每個請求都將實現長短 url 地址的轉換, 如下:
在這裏插入圖片描述
使用 Fprintf(來自 fmt 包) 函數, 可打印出與 url 長地址對應的 key(map), 而 key 也將回傳給客戶端, 注意
Fprintf 還可實現對 ResponseWriter 類型變量的寫入, 事實上 Fprintf 可基於 io.Writer(將在 Write 方法中調用該函數), 實現對結構變量的寫入, 而 io.Writer() 將調用一個接口, 因此基於接口的使用,Fprintf 可變成一個通用函數, 可處理不同類型的數據, 在 Go 語言中廣泛使用了接口, 它可使代碼更加通用, 參見第 11 章.

同時 Fprintf 還可顯示一個 html 表單, 並能將其寫入到 w 中, 因此如果請求中未包含 url 地址, Add 函數還需顯示出 html 表單:
在這裏插入圖片描述
在上述功能中, 需要將常量字符串 AddForm 發送給客戶端, 其實 AddForm 就是一個 html 表單, 其中包含了一個 url 數據域, 以及一個 submit 按鈕, 並能在客戶端的瀏覽器中顯示, 如果在瀏覽器中, 點擊 submit, 可發送一個基於/add 路徑的請求, 這時 Add 處理器將再次被調用, 同時 url 數據域的 text 子域 (包含地址) 將傳遞給 web 服務器 (’’ 可封閉一個字符串流, 而其他字符串通常會封閉在」」 中).

Redirect 函數可獲取 http 請求中包含的 key(而請求路徑中包含的 url 短地址, 可移除首字符, 因此在 Go 語言中會使用 [1:], 比如請求路徑爲/abc, 那麼 key 則爲 abc), 那麼使用 Get 函數, 可獲取 url 長地址, 同時會將重定向的 http 地址, 返回給用戶, 如果 url 無法找到, 則會發送一個 404(url 無法找到) 錯誤.
在這裏插入圖片描述
http.NotFound 和 http.Redirect 可用於發送通用的 http 響應.

編譯和運行

由於在隨書源碼包中, 給出了已編譯的可執行文件, 因此可以跳過編譯, 直接測試可執行文件, 而上述的三個Go 文件將包含在一個 Makefile 中, 之後可使用 Makefle, 實現可執行文件的編譯和鏈接.

在 Linux 和 OS X 系統中, 可在一個控制檯窗口中輸入 make, 或是在 LiteIDE 中創建一個項目, 生成可執行文件, 在 Windows 系統中, 可啓動 MINGW 環境, 並使用 MINGW Shell 生成可執行文件, 因此可選擇控制檯窗口, 輸入 make 並回車, 可得以下消息:
在這裏插入圖片描述
完成編譯和鏈接後, 在 Linux/OS X 系統中, 將得到一個 goto 程序, 在 Windows 系統中, 將得到一個 goto.exe程序.

使用以下方式, 可運行 web 服務器,
• 在 Linux/OS X 系統中, 可輸入命令./goto
• 在 Windows 系統中, 可從 GoIDE 中, 啓動 goto.exe(如果防火牆阻礙了程序的運行, 請關閉防火牆)

程序測試

打開瀏覽器, 並請求 url 地址http://localhost:8080/add, 這調用 web 服務器的 Add 處理器函數, 由於在 html表單中未提供 url 變量, 因此 web 服務器將發送另一個 html 表單給瀏覽器, 以詢問所需的信息.
在這裏插入圖片描述
在表單的文本框中, 可輸入一個 url 長地址 (需轉換成 url 短地址), 比如 http://golang.org/pkg/bufio/#Writer,並點擊 Add 按鈕, 之後 web 服務器將回傳一個 url 短地址給瀏覽器, 比如http://localhost:8080/2.
在這裏插入圖片描述
將顯示的 url 短地址, 複製到瀏覽器的地址欄中, 並請求該地址, 這時 web 服務器的 Redirect 處理器將被調用, 並回傳 url 長地址的頁面信息.
在這裏插入圖片描述

19.5 god 的連續存儲

以下將給出本項目的第 2 個版本, 參見隨書源碼包的 goto_v2 目錄.

當 goto 程序退出, 內存中保存的長短 url 地址的轉換, 將被丟棄, 爲了防止 map 中存儲數據的丟失, 必須將這些數據, 保存在一個磁盤文件中, 當完成 URLStore 變量的修改後, 可將對應數據, 寫入文件, 同時在 goto 程序啓動後, 可將文件中保存的數據, 重新寫入到 map 中, 這些操作都需要使用 encoding/gob 包, 它可實現結構與數組 (更準確地說, 應該是 slice) 之間的串行和並行的數據交換, 參見 12.11 節.

gob 包的 NewEncoder 和 NewDecoder 函數, 可實現數據的讀寫, 而 Encode 和 Decode 方法則提供了 Encoder和 Decoder 對象, 可完成結構與文件之間的讀寫, 即 Encoder 實現了 Writer(寫入器) 接口, Decoder 實現了Reader(讀取器) 接口, 所以需要在 URLStore 結構中, 增加一個新的數據域 (*os.File 類型), 它可獲取一個文件句柄, 以實現文件的讀寫.
在這裏插入圖片描述
之後在創建 URLStore 實例時, 可傳入一個文件名 store.gob,
在這裏插入圖片描述
因此還需要對 NewURLStore 函數進行修改,
在這裏插入圖片描述
函數包含了一個文件名形參, 在函數中, 可打開該文件, 並關聯到 URLStore 變量的 file 數據域, 如果 OpenFile調用出錯 (比如磁盤文件被刪除), 將返回一個錯誤 err.

如果 err 並非 nil 值, 則表明出現了一個錯誤, 這時需終止程序, 併發送一條警告消息, 在一般情況下, 都需要對函數返回的錯誤碼進行檢查, 但可以使用之前給出的錯誤檢查模式, 同時上述函數可打開一個文件.

在開啓文件時, 給出了可寫模式, 同時也給出了附加 (append) 模式, 當程序生成了一對新的 URL(長短) 地址時, 就可通過 gob 包的功能, 將 URL 地址寫入文件, 因此需要定義一個新結構, 用於 URL 地址的存儲.
在這裏插入圖片描述
另外還需要提供一個 save 方法, 已將 URL 地址保存到文件, 這時可使用 gob 的編碼功能, 進行保存:
在這裏插入圖片描述
在 goto 程序啓動時, 還需要從文件中, 將保存的 URL 地址, 讀取到 URLStore 變量的 map 中, 因此還需要提供一個載入方法 load:
在這裏插入圖片描述
load 方法可跳轉到文件開頭, 並使用 Decode 函數, 讀取每對 URL 地址, 之後使用 Set 方法, 將 URL 地址保存到 map 中, 從上可見 load 方法中遍佈了多個錯誤處理, 如果未出現錯誤, load 方法將在一個死循環中, 遍歷整個文件,
在這裏插入圖片描述
如果所有的 URL 地址都讀取完畢, 並出現文件末尾時, 將產生一個 io.EOF 錯誤, 如果錯誤並未 io.EOF 錯誤, 則會停止文件讀取, 並返回該錯誤, 同時 load 方法必須加入到 NewURLStore 方法中,
在這裏插入圖片描述
Put 函數可在一對新的 URL 地址寫入 map 時, 並將這對 URL 地址也保存到文件中,
在這裏插入圖片描述
在這裏插入圖片描述
編譯和測試本項目的第 2 個版本, 或是直接使用已編譯的可執行文件, 都能驗證, 即使 web 服務器終止, 也能獲取到 url 短地址 (在控制檯窗口中, 可使用 Ctrl+C, 終止一個進程), 如果數據文件 store.gob 不存在, 當 goto第一次啓動時, 將給出一個錯誤消息,
在這裏插入圖片描述
這時停止 goto 進程, 並再次啓動時, 將發現一個空的 store.gob, 因爲在第一次啓動時, 創建了該文件, 在 goto第 2 次啓動時, 也可能會出現以下錯誤,
在這裏插入圖片描述
這是因爲 gob 是一個基於數據流的協議, 並不支持重新啓動, 在本項目的第 4 個版本中, 可使用 json 存儲格式, 來替代 gob, 以避免上述問題的發生.

19.6 使用併發協程

以下將實現本項目的第 3 個版本, 可在隨書源碼包中找到 goto_3 目錄, 以下的源碼都能在該目錄下找到.

當大量客戶端同步添加 URL(長短) 地址 (即請求 add 頁面) 時, 本項目的第 2 個版本將出現性能問題, 基於加鎖機制,map 可在併發訪問中, 實現安全更新, 而將每對新地址寫入磁盤文件的操作將成爲處理瓶頸, 因爲磁盤的同步寫入, 則依賴於 OS 的特性, 這會引發一些性能損失, 即使寫入操作之間不會發生衝突, 每個客戶端也必須等待數據寫入磁盤文件, 以使 Put 函數可返回, 因此這是一個任務繁重的 IO 負載系統, 客戶端發出 Add請求後, 必須處於長時間的等待狀態.

爲了解決上述問題, 必須分離 Put 操作和 Save 操作, 以利用 Go 語言的併發機制, 爲了替代磁盤文件的直接寫入, 可將數據寫入到 channel 中, 因爲併發通道可包含緩衝, 所以發送函數無須等待.

在 Save 函數中, 可將併發通道中讀取數據, 寫入到磁盤文件中, 這類操作可放置一個獨立的線程中 (即併發協程 saveLoop),main 程序和 saveLoop 可併發執行, 同時不會出現相互阻塞, 因此在 URLStore 結構中, 將使用channel 類型來替換 file 類型, 如下:
在這裏插入圖片描述
channel 類型與 map 類型很相似, 也必須使用 make 進行創建, 所以在 NewURLStore 方法中, 必須創建一個緩衝長度爲 1000 的併發通道, 即 save := make(chan record,saveQueueLength), 同時還需要創建一個函數, 以將每對 URL 地址保存到磁盤文件, 同時 Put 函數中, 只需將每對 URL 地址, 發送給併發通道 save,
在這裏插入圖片描述
在併發通道 save 的另一端, 必須提供一個接收器, 因此將定義一個新方法 saveLoop(它可運行在一個獨立的併發協程中), 它可從併發通道 save 中讀取數據, 並將數據寫入文件, 在 NewURLStore 方法中, 將啓動saveLoop, 並給出了關鍵字 go, 這表明它將在一個併發協程中運行, 同時可移除文件開啓代碼, 以下將給出已修改的 NewURLStore 方法,
在這裏插入圖片描述
以下是 saveLoop 方法的代碼,
在這裏插入圖片描述
上述方法將在一個死循環中, 從併發通道 save 中讀取數據, 並將讀取數據寫入到文件中.

在第 14 章中, 已經深入學習了併發協程和併發通道, 但在本節中, 將提供一個更好的程序管理的示例, 這裏所使用的加密 (Encoder), 是爲了節約內存和進程的資源.

增加 goto 程序靈活性的另一種方式, 是放棄文件名, 監聽器地址和主機名的硬編碼, 使其變成一個常量, 也就是將其定義成 flag, 如果需要在程序中改變這些數值時, 可在命令行中輸入, 當未給出對應的輸入時,flag 還可提供一個默認值, 只是在編碼過程中, 需要導入 flag 包, 即import 」flag」, 參見 12.4 節.

因此可在 flag 中, 創建一些全局變量, 如下:
在這裏插入圖片描述
爲了實現命令行參數的解析, 必須在 main 函數中, 加入 flag.Parse(), 當命令行參數完成解析後, 可進行 URLStore 類型的初始化, 經過解析後, 可知 dataFile 的數值 (以下代碼中使用了 *dataFile, 由於 flag 是一個指針, 因此它可反向獲取到指針指向的數值, 參見 4.9 節).
在這裏插入圖片描述在這裏插入圖片描述
在 Add 處理器中, 可使用 *hostname 替換localhost:8080,
在這裏插入圖片描述
編譯和測試本項目的第 3 個版本, 或是直接使用已生成的可執行文件.

19.7 json 格式

以下將給出本項目的第 4 個版本, 在隨書源碼包的 goto_v4 目錄中, 可找到以下代碼.

如果你是一個敏銳的測試者, 可能會注意到 goto 第 2 次啓動時所出現問題, 如果 goto 第 2 次啓動時, 給
出了一個 url 短地址, 則可正常執行, 而在第 3 次啓動時, 將出現一個錯誤: Error loading URLStore: ex-
tra data in buffer, 這是因爲 gob 包採用了一種基於數據流的協議, 所以並不支持重新啓動, 爲了解決這個問題, 應選擇 json 格式的存儲協議 (參見 12.9 節), 它可將數據保存在一個文本中, 同時該格式也能與其他編程語言所共享, 由於存儲操作被分離到兩個方法 (load 和 saveLoop) 中, 所以變更存儲協議也很簡單.

首先需要創建一個空文件 store.json, 並在 main 函數中, 修改 dataFile 變量的聲明:
在這裏插入圖片描述
在 store.go 文件中, 需導入 json 包, 之後在 saveLoop 方法中, 修改一行代碼, 如下:
在這裏插入圖片描述
同樣在 load 方法, 也需要修改一行代碼,
在這裏插入圖片描述
其他語句無須修改, 之後可編譯和測試可執行文件 goto, 之前的錯誤不會再出現.

19.8 多機通訊

以下將給出本項目的第 5 個版本, 在隨書源碼包的 goto_5 目錄中, 可找到以下代碼, 在當前版本中, 仍會使用gob 存儲.

goto 程序可在單個進程中運行 (該進程將在一臺主機中運行), 該進程可生成多個併發協程, 以服務 (處理) 多個併發的 web 請求, 同時 URL 短地址可獲取到重定向服務 (Redirects, 可使用 Get), 之前的添加服務 (Add,可使用 Put) 將記錄該 URL 短地址, 因此我們可創建任意數量的客戶端 slave(可發送 Get 請求), 同時也可發送 Put 請求給 web 服務器 master, 如下圖:
在這裏插入圖片描述
在網絡中, 只會運行一個服務器應用, 但能運行多個客戶端應用, 因此服務器和客戶端之間, 可實現多機通訊,rpc 包提供一種機制, 可通過網絡連接, 實現函數的調用, 因此可使 URLStore 類型給出一個 rpc 服務 (參見 15.9 節), 那麼在客戶端進程中, 處理 Get 請求時, 也可獲取到 URL 長地址 (可調用服務器的函數, 並得到返回結果), 當一個新的 URL 長地址需要轉換成短地址時, 可通過 rpc 連接, 將 URL 長短地址的轉換任務, 委託給服務器, 因爲只有服務器纔可寫入數據文件.

URLStore 類型中, 原有的 Get 和 Put 方法的原型如下:
在這裏插入圖片描述
在這裏插入圖片描述
同時還需要修改 Get 方法,
在這裏插入圖片描述
由於 key 和 url 變量都是指針, 因此必須給出前綴 *, 比如*key, u 是一個數值, 如果將其分配給一個指針變量, 應寫爲*url = u. 同時也需要修改 Put 方法,
在這裏插入圖片描述
由於 Put 需調用 Set 方法, 因此 key 和 url 包含的數值應當匹配, 否則將返回一個錯誤碼, 而不是一個布爾值.
在這裏插入圖片描述
在這裏插入圖片描述
同時還需要修改 http 處理器, 以適應 URLStore 類型的變化, 比如 Redirect 處理器應返回一個錯誤消息 (字符串類型).
在這裏插入圖片描述
Add 處理器也需要修改,
在這裏插入圖片描述
在這裏插入圖片描述
爲了讓應用程序更加靈活, 還可增加一個命令行參數 (使能 rpc 功能),
在這裏插入圖片描述
爲了實現 rpc 的正常工作, 還需要在 rpc 包中註冊 URLStore 類型, 並配置一個基於 http 連接的 rpc 處理器, 即 HandleHTTP, 如下:
在這裏插入圖片描述

19.9 ProxyStore

在當前項目中, 加入了 rpc 服務, 因此需創建另一種類型 ProxyStore, 用於描述來自於客戶端的 rpc 請求,
在這裏插入圖片描述
在這裏插入圖片描述

ProxyStore 緩存

如果客戶端將所有任務都委託給服務器, 這並不是一種有效的方式, 因此客戶端自身可處理 Get 請求, 爲了實現該功能, 必須定義一個 URLStore 類型的副本, 也就是在 ProxyStore 結構中包含 URLStore 類型:
在這裏插入圖片描述
之後還必須修改 NewProxyStore,
在這裏插入圖片描述
同時還需要修改 NewURLStore, 以便在文件名不存在的情況下, 不會對文件進行讀寫,
在這裏插入圖片描述
客戶端的 Get 方法也需要擴展, 首先需要在緩存中, 查找 key 是否存在, 如果存在,Get 方法將返回緩衝包含的數據, 如果 key 不存在, 將生成一個 rpc 調用, 從服務器中獲取 url 地址.
在這裏插入圖片描述
同理,Put 方法在向服務器發送 Put 的 rpc 請求時, 也需要更新本地緩存,
在這裏插入圖片描述
客戶端可使用 ProxyStore, 而服務器只能使用 URLStore, 這使得客戶端與服務器的操作很相似, 它們包含了相同的 Get 和 Put 方法, 所以可指定一個接口 Store, 實現相似操作的通用性,
在這裏插入圖片描述
之後可定義 Store 類型的全局變量 store,
在這裏插入圖片描述
最後可在客戶端或服務器進程啓動時, 在對應的 main 函數中, 使用上述接口, 以便使用接口, 實現 Get 和 Put操作, 同時還可添加一個新的命令參數 masterAddr, 以標識服務器地址 (無默認值).
在這裏插入圖片描述
如果命令行給出了服務器地址, 那麼就可啓動一個客戶端進程, 並創建一個新的 ProxyStore, 否則只能啓動一個服務器進程, 並創建一個新的 URLStore,
在這裏插入圖片描述
這可在 web 前端中使用 ProxyStore, 因爲它已經包含了 URLStore, 而 web 前端的操作如前所述, 並且無須使用 Store 接口, 只有服務器進程需要將數據寫入文件. 當啓動了一個服務器和多個客戶端之後, 可基於多個客戶端, 實現服務器的壓力測試, 同時需要先編譯本項目的第 5 個版本, 或是直接使用已編譯的可執行文件. 在命令行中, 首先啓動服務器,
在這裏插入圖片描述
在Windows系統中, 可輸入goto, 其中包含了兩個參數, 服務器將監聽 8080 端口, 並使能 rpc 功能. 之後可使用以下命令, 啓動一個客戶端,
在這裏插入圖片描述
其中包含了服務器地址, 該地址可在 8080 端口上, 接收客戶端的請求.

使用以下的 Unix Shell 腳本 demo.sh, 可自動實現服務器和客戶端的啓動.
在這裏插入圖片描述
爲了實現 Windows 系統中的測試, 可啓動一個 MINGW shell, 開啓服務器進程, 再啓動一個新的 MINGW shell, 再開啓客戶端進程.

19.10 總結與提高

在 goto 應用的構建過程中, 運用了 Go 語言包含的所有重要特性, 雖然應用程序給出了所需的功能, 但是還能使用以下辦法, 實現一些改進:

• 優雅: 用戶接口可更加優雅, 即使用 template 包.
• 可靠性: 服務器/客戶端的 rpc 連接能夠更加可靠, 如果連接中斷, 客戶端可嘗試重新連接, 這類操作可交
給一個 dialer(連接) 併發協程.
• 資源: 隨着 URL 地址記錄的增加, 內存用量將變成一個問題, 可使客戶端和服務器基於 key, 共享同一個URL 地址記錄的集合.
• 刪除: 支持 URL 短地址的刪除, 但這一操作會導致服務器與客戶端之間的交互更加複雜.

在這裏插入圖片描述