September 7, 2013

1407 letters 3 mins read

Go言語:goroutineとchannelを使ってWebSocketでブラウザと通信するXMPPチャットWebクライアントを実装する

Go言語でGoogle Hangout(旧Google Talk)などで利用できるXMPPチャットクライアントのWebアプリを実装した。
github.com/inatus/xmpp-web-client-go

使用した技術要素は以下のとおり。

  • Webブラウザとの通信はWebSocketを利用
  • チャットサーバとの通信はXMPPを利用
  • 受信データはGo言語のgoroutine、channelを用いて並行処理を行う

WebSocketには準標準パッケージのcode.google.com/p/go.net/websocketを利用する。

XMPPはcode.google.com/p/rsc/xmppを使おうと思ったが、Roster(コンタクトリスト)がうまく取得できなかったため、github.com/agl/xmppを利用することにした。

方針

本プログラムでは以下のように、異なるプロトコルの通信を扱う必要がある。

Webブラウザから入力されたメッセージは、WebSocket経由で受信し(①)、XMPPサーバへ送信する(②)。
チャット先の相手がメッセージを送信した際には、XMPPサーバからメッセージを受信し(③)、WebSocket経由でWebブラウザに送信する(④)。
このようなリモートからの通信(①、③)を受ける際には並行処理が必要になる。

今回は、以下のようにWebSocket、XMPPからの通信を受けるgoroutineを別々に立て、メインの処理を走らせるgoroutineに集めて処理を行う方法をとった。

Go言語では、goroutine、channelを用いることで、こういった並行処理を驚くほど簡単に実現できる。

実装

まずコネクションを1つの型にまとめる。
goroutineとして実行する関数は、この型のメソッドとして定義する。(後述)

 type session struct { talk *xmpp.Conn ws *websocket.Conn }

その上で、それぞれのコネクションからの受信データを格納するchannelを定義し、これを引数にしてgoroutineを起動する。
メインのgoroutineはそのままselect-case構文によりchannelからのデータを待ち受ける。
case文にそれぞれのchannelを指定してやれば複数のchannelでデータを待ち受けることが可能。
なお、okにはchannelが開いていればtrue、閉じていればfalseが返る。

ses := session{ 
    talk: talk, // XMPPのコネクション 
    ws: ws, // WebSocketのコネクション 
} 
stanzaChan := make(chan xmpp.Stanza) // xmpp.Stanza型のchannelを定義 
go ses.receiveMessage(stanzaChan) // goroutineの起動 
webSocketChan := make(chan string) // string型のchannelを定義 
go ses.receiveWebSocket(webSocketChan) // goroutineの起動 
for { 
    select { 
        case receivedStanza, ok := <-stanzaChan: // XMPP用channelからのデータを待ち受ける 
            if !ok { 
                // エラー処理 
            } 
            // 処理 
        case receivedMessage, ok := <-webSocketChan: // WebSocket用channelからのデータを待ち受ける 
            if !ok { 
                // エラー処理 
            } 
            // 処理 
    } 
}

処理

XMPP、WebSocketからの通信を待ち受けるgoroutineで行うことは、受け取ったデータを引数のchannelに渡すことのみである。

func (ses *session) receiveMessage(stanzaChan chan<- xmpp.Stanza) { 
    defer close(stanzaChan) 
    for { receivedStanza, err := ses.talk.Next() // XMPPからのデータを待ち受ける 
        if err != nil { 
        // エラー処理 
        } 
    stanzaChan <- receivedStanza // channelにデータを渡す 
    } 
} 

func (ses *session) receiveWebSocket(webSocketChan chan<- string) { 
    defer close(webSocketChan) 
    for { 
        var receivedMessage string 
        if err := websocket.Message.Receive(ses.ws, &receivedMessage); err != nil { 
            // WebSocketからのデータを待ち受ける 
            // エラー処理 
        } 
    webSocketChan <- receivedMessage // channelにデータを渡す 
    } 
}

たったこれだけで上記の図に示すような並行処理を組むことができる。