iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 26
1
自我挑戰組

Let's Eat GO ! 實務開發雜談by Golang系列 第 26

Day26 .[心得與討論篇] struct 設計解析 - 以melody package (6)

Part I

首先跟大家介紹一下,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,有什麼要注意的地方

  1. channel 是不是已經被關閉了,往被關閉的channel塞東西可是會發生panic,所以首先做這個判斷。
if s.closed() {}
  1. 如果是普通的channel,收的那端不取走,送的那端就會卡住。而buffer channel在buffer被塞滿的時候,也會有同樣的問題,況且buffer滿了,理論上我們會採取後面來的就放棄的作法,於是搭配select和default可以完美解決問題。

塞不下的時候,會自動走到default那個case。

	select {
	case s.output <- message:
	default:
		s.melody.errorHandler(s, errors.New("session message buffer is full"))
	}

Part II

writePump() 和readPump() 兩個method應該可以說是Session最核心的部分,甚至可以說整個melody最重要的部分。

業務劃分兩個部位,分別處裡讀取和寫入,類似的概念在golang source code的http連線和DB都可以看到,讀寫分開做處理。

想想也是很合理的設計,當你的業務對象也是個主動性很高、不可控制的個體,譬如說DB server。難道能夠可以決定說,我們丟一個request到DB,DB要回傳結果後,我們才能夠繼續丟下一個request嗎?難道不可以先再丟另外一個request嗎?

所以讀取和寫入分開是很自然的事情,只要我們的處理機制能夠做好,分開也會有最大效益。

writePump

先來看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()

readPump

了解了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的心得分享。


上一篇
Day25 .[心得與討論篇] struct 設計解析 - 以melody package (5)
下一篇
Day27 .[心得與討論篇] struct 設計解析 - 以melody package (7)
系列文
Let's Eat GO ! 實務開發雜談by Golang30

尚未有邦友留言

立即登入留言