使用Golang開發微信公眾平台-接收簡訊

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

一旦接入驗證成功,成為正式開發人員,你可能會迫不及待地想通過手機發送一條"Hello, Wechat”到你的公眾號伺服器。不過上一篇的那個程式還無法處理手機提交的簡訊,本篇將介紹如何用Golang編寫公眾號程式來接收手機端發送的 簡訊以及回複響應訊息。

根據公眾平台開發文檔中描述:“當普通使用者向公眾帳號發訊息時,伺服器將POST訊息的XML資料包到開發人員填寫的URL上”。我們 用一個展示一下這個訊息流程程:

伺服器通過一個HTTP Post請求將終端使用者發送的訊息轉寄給公眾號伺服器,訊息內容被封裝在HTTP Post Request的Body中。資料包以XML格式儲存,文本類訊息XML格式範例如下(引自公眾平台開發文檔):

資料包中各個欄位的含義都顯而易見,我們重點關注的時Content這個欄位填寫的內容,也就是終端使用者發送的訊息內容。為了得到這個欄位值,我 們需要解析伺服器發來的HTTP Post包的Body。

在“接入驗證”一文中我們提到過,伺服器發起的請求都帶有驗證欄位,可被公眾號服務用於驗證HTTP Request是否來自於伺服器,避免惡意請求。這些用於驗證來源的資訊,不僅僅在接入驗證階段會發給公眾號伺服器,在後續伺服器與公眾號伺服器 的訊息互動過程中,HTTP Request中也都會攜帶這些資訊(注意:沒有echostr參數了)。

下面我們來看接收簡訊的Golang程式。

一、接收簡訊

公眾號所用的HTTP Server可以沿用“接入驗證”一文中的那個main中的Server,我們需要修改的是procRequest函數。

在procRequest函數中,我們保留validateUrl,用於校正請求是否來自於伺服器。

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }
        log.Println("Wechat Service: validateUrl Ok!")
       
        … …//在此解析HTTP Request Body
}

通過驗證後,我們開始解析HTTP Request的Body,Body中的資料是XML格式的,我們可以通過Golang標準庫encoding/xml包中提供的函數對Body進行解 析。encoding/xml根據xml欄位名與struct欄位名或struct tag(struct中每個欄位後面反單引號引用的內容,比如xml: "xml")的對應關係將xml資料中的欄位值解析到struct的欄位中,因此我們需要根據這個xml包的組成定義出對應該格式的struct,這個 struct定義如下:

type TextRequestBody struct {
        XMLName      xml.Name `xml:"xml"`
        ToUserName   string
        FromUserName string
        CreateTime   time.Duration
        MsgType      string
        Content      string
        MsgId        int
}

其中FromUserName是發送方帳號,這是一個OpenID,每個使用者針對某個關注的公眾號都有唯一OpenID。舉個例 子:"tonybai"這個使用者,關注了"GoNuts"和"GoDev"兩個公眾號,則"tonybai"發給GoNuts的訊息中的 OpenID是“tonybai-gonuts”,而tonybai發給GoDev的訊息中的OpenID則是“tonybai-godev”。

MsgId是一個64位整型,可用於訊息排重。對於一個HTTP Post,伺服器在五秒內如果收不到響應會斷掉串連,並且針對該訊息重新發起請求,總共重試三次。嚴謹的公眾號服務端實現是應該實現訊息排重功能的。

通過encoding/xml包中的Unmarshal函數,我們將上面的xml資料轉換為一個TextRequestBody執行個體,具體代碼如 下:

//recvtextmsg_unencrypt.go
func parseTextRequestBody(r *http.Request) *TextRequestBody {
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
                log.Fatal(err)
                return nil
        }
        fmt.Println(string(body))
        requestBody := &TextRequestBody{}
        xml.Unmarshal(body, requestBody)
        return requestBody
}

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }

        if r.Method == "POST" {
                textRequestBody := parseTextRequestBody(r)
                if textRequestBody != nil {
                        fmt.Printf("Wechat Service: Recv text msg [%s] from user [%s]!",
                                textRequestBody.Content,
                                textRequestBody.FromUserName)
                }
        }
}

構建並執行該程式:

$>sudo ./recvtextmsg_unencrypt
2014/12/19 08:03:27 Wechat Service: Start!

通過手機或公眾開發平台提供的頁面調試工具發送"Hello, Wechat",我們可以看到如下輸出:

2014/12/19 08:05:51 Wechat Service: validateUrl Ok!
Wechat Service: Recv text msg [Hello, Wechat] from user [oBQcwuAbKpiSAbbvd_DEZg7q27QI]!

上述接收"Hello, Wechat"簡訊的Http抓包分析文本如下(Copy from wireshark output):

POST /?signature=9b8233c4ef635eaf5b9545dc196da6661ee039b0&timestamp=1418976343&nonce=1368270896 HTTP/1.0\r\n
User-Agent: Mozilla/4.0\r\n
Accept: */*\r\n
Host: wechat.tonybai.com\r\n
Pragma: no-cache\r\n
Content-Length: 286\r\n
Content-Type: text/xml\r\n

公眾號伺服器給伺服器返回的HTTP Post Response為:

HTTP/1.0 200 OK\r\n
Date: Fri, 19 Dec 2014 08:05:51 GMT\r\n
Content-Length: 0\r\n
Content-Type: text/plain; charset=utf-8\r\n

二、響應簡訊

上面的例子中,終端使用者發送"Hello, Wechat",雖然公眾號伺服器成功接收到了這段內容,但終端使用者並沒有得到響應,這顯然不那麼友好!這裡我們來給終端使用者補發一個簡訊的響 應:Hello,使用者OpenID。

這類響應訊息可以通過HTTP Post Request的Response包攜帶,將資料放入Response包的Body中,當然也可以單獨向公眾平台發起請求(後話)。公眾平台開發 文檔中關於被動的簡訊響應的定義如下:

這與前面的接收訊息結構極其類似,欄位含義也不說自明。Golang encoding/xml中的Marshal(和MarshalIndent)函數提供了將struct編碼為XML資料流的功能,它是 Unmarshal的逆過程,Golang實現回複 文本響應訊息的代碼如下:

type TextResponseBody struct {
        XMLName      xml.Name `xml:"xml"`
        ToUserName   string
        FromUserName string
        CreateTime   time.Duration
        MsgType      string
        Content      string
}

func makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {
        textResponseBody := &TextResponseBody{}
        textResponseBody.FromUserName = fromUserName
        textResponseBody.ToUserName = toUserName
        textResponseBody.MsgType = "text"
        textResponseBody.Content = content
        textResponseBody.CreateTime = time.Duration(time.Now().Unix())
        return xml.MarshalIndent(textResponseBody, " ", "  ")
}

func procRequest(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
        if !validateUrl(w, r) {
                log.Println("Wechat Service: this http request is not from Wechat platform!")
                return
        }

        if r.Method == "POST" {
                textRequestBody := parseTextRequestBody(r)
                if textRequestBody != nil {
                        fmt.Printf("Wechat Service: Recv text msg [%s] from user [%s]!",
                                textRequestBody.Content,
                                textRequestBody.FromUserName)
                        responseTextBody, err := makeTextResponseBody(textRequestBody.ToUserName,
                                textRequestBody.FromUserName,
                                "Hello, "+textRequestBody.FromUserName)
                        if err != nil {
                                log.Println("Wechat Service: makeTextResponseBody error: ", err)
                                return
                        }
                        fmt.Fprintf(w, string(responseTextBody))
                }
        }
}

編譯執行上面程式後,通過手機或網頁調試工具發送一條"Hello, Wechat"到公眾號,公眾號會響應如下資訊:“Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI",手機端會正確接收該響應。

上述響應的抓包分析如下。公眾號伺服器給伺服器返回的HTTP Post Response為:

HTTP/1.0 200 OK\r\n
Date: Fri, 19 Dec 2014 09:03:55 GMT\r\n
Content-Length: 220\r\n
Content-Type: text/plain; charset=utf-8\r\n
\r\n
oBQcwuAbKpiSAbbvd_DEZg7q27QIgh_xxxxxxxx1418979835textHello, oBQcwuAbKpiSAbbvd_DEZg7q27QI

三、關於Content-Type設定

雖然Content-Type為:text/plain; charset=utf-8的 響應資訊可以被平台正確解析,但通過抓取平台給公眾號伺服器發送的HTTP Post Request來看,在發送xml資料時伺服器用的Content-Type為Content-Type: text/xml。我們的響應資訊Body也是xml資料包,我們能否為響應資訊重新設定Content-Type為 text/xml呢?我們可以通過如下代碼設定:

w.Header().Set("Content-Type", "text/xml")
fmt.Fprintf(w, string(responseTextBody))

不過奇怪的是我通過AWS EC2上抓包得到的Content-Type始終是“text/plain; charset=utf-8”。但利用ngrok映射到本地連接埠後抓包看到的卻是正確的"text/xml",在AWS本地用 curl -d xxx.xxx.xxx.xxx測試公眾號服務程式而抓到的包也是正確的。通過代碼沒看出什麼端倪,因為邏輯上顯式設定Header的Content- Type後,Go標準庫不會在sniff內容的格式了。

通過ngrok映射本地80連接埠後,得到的HTTP Post Response抓包分析文字:

HTTP/1.1 200 OK\r\n
Content-Type: text/xml\r\n
Date: Sat, 20 Dec 2014 04:29:16 GMT\r\n
Content-Length: 220\r\n

xml資料包這裡忽略。

四、CDATA的使用

從抓包可以看到,我們回複的響應中的XML資料包是不帶CDATA,即便這樣用戶端接收也沒有問題。但這並未嚴遵循協議範例。

XML下CDATA含義是:在標記CDATA下,所有的標記、實體引用都被忽略,而被XML處理常式一視同仁地當做字元資料看待,CDATA的形 式如下:

常值內容

我們嘗試加上為每個文本類型的欄位值上直接添加CDATA標記。

func value2CDATA(v string) string {
        return "" + v + ""
}

func makeTextResponseBody(fromUserName, toUserName, content string) ([]byte, error) {
        textResponseBody := &TextResponseBody{}
        textResponseBody.FromUserName = value2CDATA(fromUserName)
        textResponseBody.ToUserName = value2CDATA(toUserName)
        textResponseBody.MsgType = value2CDATA("text")
        textResponseBody.Content = value2CDATA(content)
        textResponseBody.CreateTime = time.Duration(time.Now().Unix())
        return xml.MarshalIndent(textResponseBody, " ", "  ")
}

這樣修改後,我們試著發一條訊息給公眾號平台,不過結果並不正確。手機無法收到響應資訊,並顯示“該公眾號暫時無法提供服務,請稍後再 試”。通過Println輸出Body可以看到:

<![CDATA[oBQcwuAbKpiSAbbvd_DEZg7q27QI]]><![CDATA[gh_1fd4719f81fe]]>1419051400<![CDATA[text]]><![CDATA[Hello, oBQcwuAbKpiSAbbvd_DEZg7q27QI]]>

可以看到左右角括弧分別被轉義為<和>了,這顯然不是我們想要的結果。那如何加入CDATA標記呢。Golang並 不直接顯式支援產生CDATA欄位的xml流,我們只能間接實現。前面提到過struct定義時的struct tag,golang xml包規定:"a field with tag ",innerxml" is written verbatim, not subject to the usual marshalling procedure"。 大致的意思是如果一個欄位的struct tag是",innerxml",則Marshal時欄位值原封不動,不提交給通常的marshalling程式。我們就利用innerxml來實現 CDATA標記。

type TextResponseBody struct {
        XMLName      xml.Name `xml:"xml"`
        ToUserName   CDATAText
        FromUserName CDATAText
        CreateTime   time.Duration
        MsgType      CDATAText
        Content      CDATAText
}

type CDATAText struct {
        Text string `xml:",innerxml"`
}

func value2CDATA(v string) CDATAText {
        return CDATAText{"" + v + ""}
}

編譯器後測試,這回CDATA標記正確了,用戶端也收到的響應資訊。

五、用ngrok在本地調試公眾平台介面

在“接入驗證”一文中,我們建議申請諸如AWS EC2來應對公眾平台介面開發,但其方便程度畢竟不如本地。網上一開源工具ngrok可以協助我們實現本地調試公眾平台介面。

使用ngrok的步驟如下:

1、下載ngrok
 ngrok也是使用golang實現的,因此主流平台都支援。ngrok下載後就是一個可執行檔二進位檔案,可直接執行(放在PATH路徑 下)。

2、註冊ngrok
到ngrok.com上註冊一個帳號,註冊成功後,就能看到ngrok.com為你分配的auth token,把這個auth token放到~/.ngrok中:

auth_token:YOUR_AUTH_TOKEN

3、執行ngrok

$ngrok 80

ngrok                                                                                                                                                           (Ctrl+C to quit)

Tunnel Status                 online
Version                       1.7/1.6
Forwarding                    http://xxxxxxxx.ngrok.com -> 127.0.0.1:80
Forwarding                    https://xxxxxxxx.ngrok.com -> 127.0.0.1:80
Web Interface                 127.0.0.1:4040
# Conn                        1
Avg Conn Time                 1.90ms

其中"xxxxxxxx.ngrok.com"就是ngrok為你分配的子網域名稱。

在你的開發人員中心將這個地址配置到URL欄位中,提交驗證,驗證訊息就會順著ngrok建立的隧道流到你的local機器的80連接埠上。

另外本地調試抓包,要用loopback網口,比如:
$sudo tcpdump -w http.cap -i lo0 tcp port 80

本篇文章涉及的代碼在這裡可以找到。

2014 – 2015, bigwhite. 著作權.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.