├── LICENSE.md ├── README.md ├── message.go ├── response.go ├── sender.go └── sender_test.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Alex Lockwood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gcm 2 | === 3 | 4 | ## NOTE: I no longer maintain this library. Feel free to fork! :) 5 | 6 | The Android SDK provides a nice convenience library ([com.google.android.gcm.server](https://github.com/google/gcm/tree/master/client-libraries/java/rest-client/src/com/google/android/gcm/server)) that greatly simplifies the interaction between Java-based application servers and Google's GCM servers. However, Google has not provided much support for application servers implemented in languages other than Java, specifically those written in the Go programming language. The `gcm` package helps to fill in this gap, providing a simple interface for sending GCM messages and automatically retrying requests in case of service unavailability. 7 | 8 | Documentation: http://godoc.org/github.com/alexjlockwood/gcm 9 | 10 | Getting Started 11 | --------------- 12 | 13 | To install gcm, use `go get`: 14 | 15 | ```bash 16 | go get github.com/alexjlockwood/gcm 17 | ``` 18 | 19 | Import gcm with the following: 20 | 21 | ```go 22 | import "github.com/alexjlockwood/gcm" 23 | ``` 24 | 25 | Sample Usage 26 | ------------ 27 | 28 | Here is a quick sample illustrating how to send a message to the GCM server: 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "fmt" 35 | "net/http" 36 | 37 | "github.com/alexjlockwood/gcm" 38 | ) 39 | 40 | func main() { 41 | // Create the message to be sent. 42 | data := map[string]interface{}{"score": "5x1", "time": "15:10"} 43 | regIDs := []string{"4", "8", "15", "16", "23", "42"} 44 | msg := gcm.NewMessage(data, regIDs...) 45 | 46 | // Create a Sender to send the message. 47 | sender := &gcm.Sender{ApiKey: "sample_api_key"} 48 | 49 | // Send the message and receive the response after at most two retries. 50 | response, err := sender.Send(msg, 2) 51 | if err != nil { 52 | fmt.Println("Failed to send message:", err) 53 | return 54 | } 55 | 56 | /* ... */ 57 | } 58 | ``` 59 | 60 | Note for Google AppEngine users 61 | ------------------------------- 62 | 63 | If your application server runs on Google AppEngine, you must import the `appengine/urlfetch` package and create the `Sender` as follows: 64 | 65 | ```go 66 | package sample 67 | 68 | import ( 69 | "appengine" 70 | "appengine/urlfetch" 71 | 72 | "github.com/alexjlockwood/gcm" 73 | ) 74 | 75 | func handler(w http.ResponseWriter, r *http.Request) { 76 | c := appengine.NewContext(r) 77 | client := urlfetch.Client(c) 78 | sender := &gcm.Sender{ApiKey: "sample_api_key", Http: client} 79 | 80 | /* ... */ 81 | } 82 | ``` 83 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package gcm 2 | 3 | // Message is used by the application server to send a message to 4 | // the GCM server. See the documentation for GCM Architectural 5 | // Overview for more information: 6 | // http://developer.android.com/google/gcm/gcm.html#send-msg 7 | type Message struct { 8 | RegistrationIDs []string `json:"registration_ids"` 9 | CollapseKey string `json:"collapse_key,omitempty"` 10 | Data map[string]interface{} `json:"data,omitempty"` 11 | DelayWhileIdle bool `json:"delay_while_idle,omitempty"` 12 | TimeToLive int `json:"time_to_live,omitempty"` 13 | RestrictedPackageName string `json:"restricted_package_name,omitempty"` 14 | DryRun bool `json:"dry_run,omitempty"` 15 | } 16 | 17 | // NewMessage returns a new Message with the specified payload 18 | // and registration IDs. 19 | func NewMessage(data map[string]interface{}, regIDs ...string) *Message { 20 | return &Message{RegistrationIDs: regIDs, Data: data} 21 | } 22 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package gcm 2 | 3 | // Response represents the GCM server's response to the application 4 | // server's sent message. See the documentation for GCM Architectural 5 | // Overview for more information: 6 | // http://developer.android.com/google/gcm/gcm.html#send-msg 7 | type Response struct { 8 | MulticastID int64 `json:"multicast_id"` 9 | Success int `json:"success"` 10 | Failure int `json:"failure"` 11 | CanonicalIDs int `json:"canonical_ids"` 12 | Results []Result `json:"results"` 13 | } 14 | 15 | // Result represents the status of a processed message. 16 | type Result struct { 17 | MessageID string `json:"message_id"` 18 | RegistrationID string `json:"registration_id"` 19 | Error string `json:"error"` 20 | } 21 | -------------------------------------------------------------------------------- /sender.go: -------------------------------------------------------------------------------- 1 | // Google Cloud Messaging for application servers implemented using the 2 | // Go programming language. 3 | package gcm 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/ioutil" 11 | "math/rand" 12 | "net/http" 13 | "time" 14 | ) 15 | 16 | const ( 17 | // GcmSendEndpoint is the endpoint for sending messages to the GCM server. 18 | GcmSendEndpoint = "https://android.googleapis.com/gcm/send" 19 | // Initial delay before first retry, without jitter. 20 | backoffInitialDelay = 1000 21 | // Maximum delay before a retry. 22 | maxBackoffDelay = 1024000 23 | ) 24 | 25 | // Declared as a mutable variable for testing purposes. 26 | var gcmSendEndpoint = GcmSendEndpoint 27 | 28 | // Sender abstracts the interaction between the application server and the 29 | // GCM server. The developer must obtain an API key from the Google APIs 30 | // Console page and pass it to the Sender so that it can perform authorized 31 | // requests on the application server's behalf. To send a message to one or 32 | // more devices use the Sender's Send or SendNoRetry methods. 33 | // 34 | // If the Http field is nil, a zeroed http.Client will be allocated and used 35 | // to send messages. If your application server runs on Google AppEngine, 36 | // you must use the "appengine/urlfetch" package to create the *http.Client 37 | // as follows: 38 | // 39 | // func handler(w http.ResponseWriter, r *http.Request) { 40 | // c := appengine.NewContext(r) 41 | // client := urlfetch.Client(c) 42 | // sender := &gcm.Sender{ApiKey: key, Http: client} 43 | // 44 | // /* ... */ 45 | // } 46 | type Sender struct { 47 | ApiKey string 48 | Http *http.Client 49 | } 50 | 51 | // SendNoRetry sends a message to the GCM server without retrying in case of 52 | // service unavailability. A non-nil error is returned if a non-recoverable 53 | // error occurs (i.e. if the response status is not "200 OK"). 54 | func (s *Sender) SendNoRetry(msg *Message) (*Response, error) { 55 | if err := checkSender(s); err != nil { 56 | return nil, err 57 | } else if err := checkMessage(msg); err != nil { 58 | return nil, err 59 | } 60 | 61 | data, err := json.Marshal(msg) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | req, err := http.NewRequest("POST", gcmSendEndpoint, bytes.NewBuffer(data)) 67 | if err != nil { 68 | return nil, err 69 | } 70 | req.Header.Add("Authorization", fmt.Sprintf("key=%s", s.ApiKey)) 71 | req.Header.Add("Content-Type", "application/json") 72 | 73 | resp, err := s.Http.Do(req) 74 | if err != nil { 75 | return nil, err 76 | } 77 | defer resp.Body.Close() 78 | 79 | if resp.StatusCode != http.StatusOK { 80 | return nil, fmt.Errorf("%d error: %s", resp.StatusCode, resp.Status) 81 | } 82 | 83 | body, err := ioutil.ReadAll(resp.Body) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | response := new(Response) 89 | err = json.Unmarshal(body, response) 90 | return response, err 91 | } 92 | 93 | // Send sends a message to the GCM server, retrying in case of service 94 | // unavailability. A non-nil error is returned if a non-recoverable 95 | // error occurs (i.e. if the response status is not "200 OK"). 96 | // 97 | // Note that messages are retried using exponential backoff, and as a 98 | // result, this method may block for several seconds. 99 | func (s *Sender) Send(msg *Message, retries int) (*Response, error) { 100 | if err := checkSender(s); err != nil { 101 | return nil, err 102 | } else if err := checkMessage(msg); err != nil { 103 | return nil, err 104 | } else if retries < 0 { 105 | return nil, errors.New("'retries' must not be negative.") 106 | } 107 | 108 | // Send the message for the first time. 109 | resp, err := s.SendNoRetry(msg) 110 | if err != nil { 111 | return nil, err 112 | } else if resp.Failure == 0 || retries == 0 { 113 | return resp, nil 114 | } 115 | 116 | // One or more messages failed to send. 117 | regIDs := msg.RegistrationIDs 118 | allResults := make(map[string]Result, len(regIDs)) 119 | backoff := backoffInitialDelay 120 | for i := 0; updateStatus(msg, resp, allResults) > 0 && i < retries; i++ { 121 | sleepTime := backoff/2 + rand.Intn(backoff) 122 | time.Sleep(time.Duration(sleepTime) * time.Millisecond) 123 | backoff = min(2*backoff, maxBackoffDelay) 124 | if resp, err = s.SendNoRetry(msg); err != nil { 125 | msg.RegistrationIDs = regIDs 126 | return nil, err 127 | } 128 | } 129 | 130 | // Bring the message back to its original state. 131 | msg.RegistrationIDs = regIDs 132 | 133 | // Create a Response containing the overall results. 134 | finalResults := make([]Result, len(regIDs)) 135 | var success, failure, canonicalIDs int 136 | for i := 0; i < len(regIDs); i++ { 137 | result, _ := allResults[regIDs[i]] 138 | finalResults[i] = result 139 | if result.MessageID != "" { 140 | if result.RegistrationID != "" { 141 | canonicalIDs++ 142 | } 143 | success++ 144 | } else { 145 | failure++ 146 | } 147 | } 148 | 149 | return &Response{ 150 | // Return the most recent multicast id. 151 | MulticastID: resp.MulticastID, 152 | Success: success, 153 | Failure: failure, 154 | CanonicalIDs: canonicalIDs, 155 | Results: finalResults, 156 | }, nil 157 | } 158 | 159 | // updateStatus updates the status of the messages sent to devices and 160 | // returns the number of recoverable errors that could be retried. 161 | func updateStatus(msg *Message, resp *Response, allResults map[string]Result) int { 162 | unsentRegIDs := make([]string, 0, resp.Failure) 163 | for i := 0; i < len(resp.Results); i++ { 164 | regID := msg.RegistrationIDs[i] 165 | allResults[regID] = resp.Results[i] 166 | if resp.Results[i].Error == "Unavailable" { 167 | unsentRegIDs = append(unsentRegIDs, regID) 168 | } 169 | } 170 | msg.RegistrationIDs = unsentRegIDs 171 | return len(unsentRegIDs) 172 | } 173 | 174 | // min returns the smaller of two integers. For exciting religious wars 175 | // about why this wasn't included in the "math" package, see this thread: 176 | // https://groups.google.com/d/topic/golang-nuts/dbyqx_LGUxM/discussion 177 | func min(a, b int) int { 178 | if a < b { 179 | return a 180 | } 181 | return b 182 | } 183 | 184 | // checkSender returns an error if the sender is not well-formed and 185 | // initializes a zeroed http.Client if one has not been provided. 186 | func checkSender(sender *Sender) error { 187 | if sender.ApiKey == "" { 188 | return errors.New("the sender's API key must not be empty") 189 | } 190 | if sender.Http == nil { 191 | sender.Http = new(http.Client) 192 | } 193 | return nil 194 | } 195 | 196 | // checkMessage returns an error if the message is not well-formed. 197 | func checkMessage(msg *Message) error { 198 | if msg == nil { 199 | return errors.New("the message must not be nil") 200 | } else if msg.RegistrationIDs == nil { 201 | return errors.New("the message's RegistrationIDs field must not be nil") 202 | } else if len(msg.RegistrationIDs) == 0 { 203 | return errors.New("the message must specify at least one registration ID") 204 | } else if len(msg.RegistrationIDs) > 1000 { 205 | return errors.New("the message may specify at most 1000 registration IDs") 206 | } else if msg.TimeToLive < 0 || 2419200 < msg.TimeToLive { 207 | return errors.New("the message's TimeToLive field must be an integer " + 208 | "between 0 and 2419200 (4 weeks)") 209 | } 210 | return nil 211 | } 212 | -------------------------------------------------------------------------------- /sender_test.go: -------------------------------------------------------------------------------- 1 | package gcm 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | ) 10 | 11 | type testResponse struct { 12 | StatusCode int 13 | Response *Response 14 | } 15 | 16 | func startTestServer(t *testing.T, responses ...*testResponse) *httptest.Server { 17 | i := 0 18 | handler := func(w http.ResponseWriter, r *http.Request) { 19 | if i >= len(responses) { 20 | t.Fatalf("server received %d requests, expected %d", i+1, len(responses)) 21 | } 22 | resp := responses[i] 23 | status := resp.StatusCode 24 | if status == 0 || status == http.StatusOK { 25 | w.Header().Set("Content-Type", "application/json") 26 | respBytes, _ := json.Marshal(resp.Response) 27 | fmt.Fprint(w, string(respBytes)) 28 | } else { 29 | w.WriteHeader(status) 30 | } 31 | i++ 32 | } 33 | server := httptest.NewServer(http.HandlerFunc(handler)) 34 | gcmSendEndpoint = server.URL 35 | return server 36 | } 37 | 38 | func TestSendNoRetryInvalidApiKey(t *testing.T) { 39 | server := startTestServer(t) 40 | defer server.Close() 41 | sender := &Sender{ApiKey: ""} 42 | if _, err := sender.SendNoRetry(&Message{RegistrationIDs: []string{"1"}}); err == nil { 43 | t.Fatal("test should fail when sender's ApiKey is \"\"") 44 | } 45 | } 46 | 47 | func TestSendInvalidApiKey(t *testing.T) { 48 | server := startTestServer(t) 49 | defer server.Close() 50 | sender := &Sender{ApiKey: ""} 51 | if _, err := sender.Send(&Message{RegistrationIDs: []string{"1"}}, 0); err == nil { 52 | t.Fatal("test should fail when sender's ApiKey is \"\"") 53 | } 54 | } 55 | 56 | func TestSendNoRetryInvalidMessage(t *testing.T) { 57 | server := startTestServer(t) 58 | defer server.Close() 59 | sender := &Sender{ApiKey: "test"} 60 | if _, err := sender.SendNoRetry(nil); err == nil { 61 | t.Fatal("test should fail when message is nil") 62 | } 63 | if _, err := sender.SendNoRetry(&Message{}); err == nil { 64 | t.Fatal("test should fail when message RegistrationIDs field is nil") 65 | } 66 | if _, err := sender.SendNoRetry(&Message{RegistrationIDs: []string{}}); err == nil { 67 | t.Fatal("test should fail when message RegistrationIDs field is an empty slice") 68 | } 69 | if _, err := sender.SendNoRetry(&Message{RegistrationIDs: make([]string, 1001)}); err == nil { 70 | t.Fatal("test should fail when more than 1000 RegistrationIDs are specified") 71 | } 72 | if _, err := sender.SendNoRetry(&Message{RegistrationIDs: []string{"1"}, TimeToLive: -1}); err == nil { 73 | t.Fatal("test should fail when message TimeToLive field is negative") 74 | } 75 | if _, err := sender.SendNoRetry(&Message{RegistrationIDs: []string{"1"}, TimeToLive: 2419201}); err == nil { 76 | t.Fatal("test should fail when message TimeToLive field is greater than 2419200") 77 | } 78 | } 79 | 80 | func TestSendInvalidMessage(t *testing.T) { 81 | server := startTestServer(t) 82 | defer server.Close() 83 | sender := &Sender{ApiKey: "test"} 84 | if _, err := sender.Send(nil, 0); err == nil { 85 | t.Fatal("test should fail when message is nil") 86 | } 87 | if _, err := sender.Send(&Message{}, 0); err == nil { 88 | t.Fatal("test should fail when message RegistrationIDs field is nil") 89 | } 90 | if _, err := sender.Send(&Message{RegistrationIDs: []string{}}, 0); err == nil { 91 | t.Fatal("test should fail when message RegistrationIDs field is an empty slice") 92 | } 93 | if _, err := sender.Send(&Message{RegistrationIDs: make([]string, 1001)}, 0); err == nil { 94 | t.Fatal("test should fail when more than 1000 RegistrationIDs are specified") 95 | } 96 | if _, err := sender.Send(&Message{RegistrationIDs: []string{"1"}, TimeToLive: -1}, 0); err == nil { 97 | t.Fatal("test should fail when message TimeToLive field is negative") 98 | } 99 | if _, err := sender.Send(&Message{RegistrationIDs: []string{"1"}, TimeToLive: 2419201}, 0); err == nil { 100 | t.Fatal("test should fail when message TimeToLive field is greater than 2419200") 101 | } 102 | } 103 | 104 | func TestSendNoRetrySuccess(t *testing.T) { 105 | server := startTestServer(t, &testResponse{Response: &Response{}}) 106 | defer server.Close() 107 | sender := &Sender{ApiKey: "test"} 108 | msg := NewMessage(map[string]interface{}{"key": "value"}, "1") 109 | if _, err := sender.SendNoRetry(msg); err != nil { 110 | t.Fatalf("test failed with error: %s", err) 111 | } 112 | } 113 | 114 | func TestSendNoRetryNonrecoverableFailure(t *testing.T) { 115 | server := startTestServer(t, &testResponse{StatusCode: http.StatusBadRequest}) 116 | defer server.Close() 117 | sender := &Sender{ApiKey: "test"} 118 | msg := NewMessage(map[string]interface{}{"key": "value"}, "1") 119 | if _, err := sender.SendNoRetry(msg); err == nil { 120 | t.Fatal("test expected non-recoverable error") 121 | } 122 | } 123 | 124 | func TestSendOneRetrySuccess(t *testing.T) { 125 | server := startTestServer(t, 126 | &testResponse{Response: &Response{Failure: 1, Results: []Result{{Error: "Unavailable"}}}}, 127 | &testResponse{Response: &Response{Success: 1, Results: []Result{{MessageID: "id"}}}}, 128 | ) 129 | defer server.Close() 130 | sender := &Sender{ApiKey: "test"} 131 | msg := NewMessage(map[string]interface{}{"key": "value"}, "1") 132 | if _, err := sender.Send(msg, 1); err != nil { 133 | t.Fatal("send should succeed after one retry") 134 | } 135 | } 136 | 137 | func TestSendOneRetryFailure(t *testing.T) { 138 | server := startTestServer(t, 139 | &testResponse{Response: &Response{Failure: 1, Results: []Result{{Error: "Unavailable"}}}}, 140 | &testResponse{Response: &Response{Failure: 1, Results: []Result{{Error: "Unavailable"}}}}, 141 | ) 142 | defer server.Close() 143 | sender := &Sender{ApiKey: "test"} 144 | msg := NewMessage(map[string]interface{}{"key": "value"}, "1") 145 | resp, err := sender.Send(msg, 1) 146 | if err != nil || resp.Failure != 1 { 147 | t.Fatal("send should return response with one failure") 148 | } 149 | } 150 | 151 | func TestSendOneRetryNonrecoverableFailure(t *testing.T) { 152 | server := startTestServer(t, 153 | &testResponse{Response: &Response{Failure: 1, Results: []Result{{Error: "Unavailable"}}}}, 154 | &testResponse{StatusCode: http.StatusBadRequest}, 155 | ) 156 | defer server.Close() 157 | sender := &Sender{ApiKey: "test"} 158 | msg := NewMessage(map[string]interface{}{"key": "value"}, "1") 159 | if _, err := sender.Send(msg, 1); err == nil { 160 | t.Fatal("send should fail after one retry") 161 | } 162 | } 163 | --------------------------------------------------------------------------------