Apple Push Notifications with Go language
February 25, 2011 12 Comments
I started to familiarize myself to the Go language, and decided to do the usual try out, i.e. sending Apple Push Notifications. It’s my personal usability benchmark for new programming environments. So far in the series
Step 1. Prerequisites
Get and build Go. Example here was done on Ubuntu 10.04 LTS x64 with Go installed based on instructions here at Go getting started guide.
- Read introduction to Apple Push here and get application and private key sandbox certificates as .pem files.
- And of course you need to have 32 byte push token from your iOS application.
Step 2. The Code.
The code here is complete, copy it to file apn.go or get it from Github.
Make sure you change the certificate files (cert.pem and key-noenc.pem) to point to your own certificate files. Also, replace the push token with your own push token, it’s written as hexadecimal string in this example for clarity.
package main import ( "crypto/tls" "fmt" "net" "json" "os" "time" "bytes" "encoding/hex" "encoding/binary" ) func main() { // load certificates and setup config cert, err := tls.LoadX509KeyPair("cert.pem", "key-noenc.pem") if err != nil { fmt.Printf("error: %s\n", err.String()) os.Exit(1) } conf := &tls.Config { Certificates: []tls.Certificate{cert}, } // connect to the APNS and wrap socket to tls client conn, err := net.Dial("tcp", "", "gateway.sandbox.push.apple.com:2195") if err != nil { fmt.Printf("tcp error: %s\n", err.String()) os.Exit(1) } tlsconn := tls.Client(conn, conf) // Force handshake to verify successful authorization. // Handshake is handled otherwise automatically on first // Read/Write attempt err = tlsconn.Handshake() if err != nil { fmt.Printf("tls error: %s\n", err.String()) os.Exit(1) } // informational debugging stuff state := tlsconn.ConnectionState() fmt.Printf("conn state %v\n", state) // prepare binary payload from JSON structure payload := make(map[string]interface{}) payload["aps"] = map[string]string{"alert": "Hello Push"} bpayload, err := json.Marshal(payload) // decode hexadecimal push device token to binary byte array btoken, _ := hex.DecodeString("6b4628de9317c80edd1c791640b58fdfc46d21d0d2d1351687239c44d8e30ab1") // build the actual pdu buffer := bytes.NewBuffer([]byte{}) // command binary.Write(buffer, binary.BigEndian, uint8(1)) // transaction id, optional binary.Write(buffer, binary.BigEndian, uint32(1)) // expiration time, 1 hour binary.Write(buffer, binary.BigEndian, uint32(time.Seconds() + 60*60)) // push device token binary.Write(buffer, binary.BigEndian, uint16(len(btoken))) binary.Write(buffer, binary.BigEndian, btoken) // push payload binary.Write(buffer, binary.BigEndian, uint16(len(bpayload))) binary.Write(buffer, binary.BigEndian, bpayload) pdu := buffer.Bytes() // write pdu _, err = tlsconn.Write(pdu) if err != nil { fmt.Printf("write error: %s\n", err.String()) os.Exit(1) } // wait for 5 seconds error pdu from the socket tlsconn.SetReadTimeout(5*1E9) readb := [6]byte{} n, err := tlsconn.Read(readb[:]) if n > 0 { fmt.Printf("received: %s\n", hex.EncodeToString(readb[:n])) } tlsconn.Close() }
Step 3. Compile and Run
Simple
$ 6g apn.go $ 6l apn.6 $ ./6.out conn state {true 47} $
If everything went fine, the program exits within few seconds and you’ll see your push notification appear on your iPhone.
How would you compare Go with Node.js / JavaScript and Erlang? In this use case and in general.
I would do this with Go for real life app, but much more important than the language are available libraries and stable tools.
Looks good, especially for your first go program. A few comments, you might already know these but the sample is so short it’s hard to tell..
– A more canonical way than printing the error and calling os.Exit() would be to just call panic. However, in this case I think you did the right thing, the output would be messier if you’d used panic. I would probably wrap the error reporting into a helper function and call that instead:
func checkError(err os.Error, format string, v …interface{}) {
if err != nil {
fmt.Printf(format, v)
os.Exit(1)
}
}
and then call checkError(err, “LoadX509KeyPair: %v\n”, err)
– You don’t need to explicitly call err.String(), the fmt library uses reflection and can dig the information out for you if you use the generic %v formatting parameter. All os.Errors satisfy the interface { String() string }.
– The way you build the data structure passed to json.Marshal works (and is pretty short) but would probably get tedious for a more complex input. Usually you just define structs directly and let the json library use reflection to dig the data out:
type jsonNotification struct {
Aps map[string] string “aps” // tag required to force lover casing of the json output
}
and then your marshaling code would become:
notification := jsonNotification{
Aps: map[string] string {“alert”: “Hello Notification”},
}
payload, err := json.Marshal(notification)
– You really should be checking the error from binary.Write.. In this case you know it probably wont fail, but it’s pretty bad form. That said checking every error is quite tedious and the calls to binary.Write are repetitive so when I recently did similar code I wrote this little helper to make the code more readable:
func bwrite(w io.Writer, values …interface{}) os.Error {
for _, v := range values {
err := binary.Write(w, binary.BigEndian, v)
if err != nil {
return err
}
}
return nil
}
you can then replace the mid section with this:
err := bwrite(buffer, uint8(1). uint32(1), uint32(time.Seconds() + 60*60), uint16(len(btoken)), btoken, uint16(len(bpayload)), bpayload)
checkError(err, “binary write %v\n”, err)
or split if up into several writes if you want to.
I can see who has been influencing Petteri to tinker around with Go.. :-)
Thanks for feedback! I didn’t realize the binary Write could fail, unless you mean Out of memory condition?
binary.Write will fail if the underlying stream fails. In this case you are writing to a bytes.Buffer so it wont fail except with OOM. However, memory depletion doesn’t really count as there is usually not much you can do if you detect it.
I’m just a stickler with checking error values, you never know when something in the code changes subtly and the person doing the change misses a hidden assumption like the one above. It’s more a question of canonical form than an actual error in this particular code.
Duh, just noticed the existing binary.Write already supports passing multiple values. The benefit of the shorthand is mostly to hide the encoding which probably isn’t useful. So you could just say this:
err := binary.Write(buffer, binary.BigEndian, uint8(1). uint32(1), uint32(time.Seconds() + 60*60), uint16(len(btoken)), btoken, uint16(len(bpayload)), bpayload)
Nice feedback!
Kai, I actually pointed Teemu towards Go. I’m a specialist in finding new languages and platforms and then having neither time nor skills to really test them.
Yeah, for first program this is impressive. It looks clean, compact and easy to read.
Great write up.
HI, thanks for the post. I’m using this as a starting point for writing a push notification service and it’s been very useful so far. I have a question about the .pem files.
How do you generate them? I have the certificate from Apple as well as the private key. What is meant to be in each of the .pem files and how would I generate them from the keychain?
Thanks
Ryan
Hi, check this blog post for detailed instructions: http://blog.serverdensity.com/2010/06/05/how-to-renew-your-apple-push-notification-push-ssl-certificate/
Pingback: Apple Push Notifications with Haskell « Brave New Method
Pingback: Simple test server for Apple push notification and feedback integration | Brave New Method