首先跟大家介紹一下,Session在處理channel上的小技巧。
多個goroutine進行業務邏輯的處理,和資料的交換,基本上最好的方案是透過channel。
因為channel是有序的,而且是為了goroutine而設計出來的結構和使用方式。
所以多了解ㄧ些使用上的重點,對於我們架構和設計上也非常有幫助,可說技多不壓身。
如下面的writeMessage(),簡簡單單幾行,卻很好的處理幾項關鍵。
func (s *Session) writeMessage(message *envelope) {
if s.closed() {
s.melody.errorHandler(s, errors.New("tried to write to closed a session"))
return
}
select {
case s.output <- message:
default:
s.melody.errorHandler(s, errors.New("session message buffer is full"))
}
}
整個method其實圍繞著把訊息送進channel裡面為重點。
s.output <- message
把東西丟送進channel,有什麼要注意的地方
if s.closed() {}
塞不下的時候,會自動走到default那個case。
select {
case s.output <- message:
default:
s.melody.errorHandler(s, errors.New("session message buffer is full"))
}
writePump() 和readPump() 兩個method應該可以說是Session最核心的部分,甚至可以說整個melody最重要的部分。
業務劃分兩個部位,分別處裡讀取和寫入,類似的概念在golang source code的http連線和DB都可以看到,讀寫分開做處理。
想想也是很合理的設計,當你的業務對象也是個主動性很高、不可控制的個體,譬如說DB server。難道能夠可以決定說,我們丟一個request到DB,DB要回傳結果後,我們才能夠繼續丟下一個request嗎?難道不可以先再丟另外一個request嗎?
所以讀取和寫入分開是很自然的事情,只要我們的處理機制能夠做好,分開也會有最大效益。
先來看writePump
func (s *Session) writePump() {
ticker := time.NewTicker(s.melody.Config.PingPeriod)
defer ticker.Stop()
loop:
for {
select {
case msg, ok := <-s.output:
if !ok {
break loop
}
err := s.writeRaw(msg)
if err != nil {
s.melody.errorHandler(s, err)
break loop
}
if msg.t == websocket.CloseMessage {
break loop
}
if msg.t == websocket.TextMessage {
s.melody.messageSentHandler(s, msg.msg)
}
if msg.t == websocket.BinaryMessage {
s.melody.messageSentHandlerBinary(s, msg.msg)
}
case <-ticker.C:
s.ping()
}
}
}
tag + for迴圈+ select 三者結合是一個很經典的方式,也是當初讓筆者困惑的寫法,但後來了解原因就覺得裡所當然。
loop:
for {
select {
}
}
select 也可以視為一個迴圈,但它跟for的用法不同,它會一直等待裡面的case,其中有任何一個條件被滿足,則會進到那個case做處理,然後就離開select。
因為工作會ㄧ直重複進來,所以外面加一層for迴圈,當每一輪的select完成任務後,又可以重新架設新的一輪select。
於是select+for會是兩層迴圈的存在,在select裡面寫break,只會打破select這層迴圈,沒有碰到for迴圈,所以單單寫個break是永遠離不開這塊程式碼。
因此最外面加個tag,break + tag,就可以離開tag底下的範圍,繼續執行後面的程式碼,當然視情況你也可以換成return,結束這個method也是另外一種方法。
再來就是ticker的部分,ticker只要固定時間到,就會自動觸發一個channel傳入的動作,因此拿來作需要定時的業務處理,或者是不是timeout的檢測,都非常好用。
ticker := time.NewTicker(s.melody.Config.PingPeriod)
defer ticker.Stop()
了解了writePump,其實readPump()裡面並沒有額外的驚艷之處,但對於gorutine的使用上其實有個小地方,筆者覺得值得參考。
以連線的角度,尤其是socket的方式來講,連線會ㄧ直需要連著不放,而不是像API一個來回就結束。
而進到melody這個package,不論裡面的業務怎麼劃分,開幾個goroutine處理,本身『你能夠進來』這個事實也暗示著你正在某個goroutine上面執行,不論是main routine或者後面開出來的。
對於這隻正在執行的routine上,就可以卡for迴圈,(通常作為收受業務的ㄧ方),直到你的業務判斷達到條件讓它離開為止。
而類似的writePump的業務處理,才用開額外的goroutine去執行。
這個部份筆者在DB連線的source code,也是看到如此這般,當連線都滿了的時候,就進到select迴圈,等待別的goroutine送來的通知,認真覺得是很有價值的設計參考。
for {
t, message, err := s.conn.ReadMessage()
}
最後一篇,將以掛載handler和event trigger的說明,結束melody的心得分享。