Gomobile to adapt Centrifugo Go WebSocket client for iOS and Android app development
Centrifugo can work with mobile devices — thanks to WebSocket protocol and its wide adoption — there are implementations for almost all languages and of course Java, Objective-C and Swift are not exceptions. I also mentioned that native iOS and Android libraries for Centrifugo have been contributed by community members. While I really appreciate this there are some problems — when Centrifugo users ask a question or open an issue related to mobile clients I can’t help them at all. So every time I have to ask client contributors for help. Asking for help is not a very comfortable thing, client authors have their own interests and supporting Centrifugo clients can’t be their main priority. Another moment is that current mobile client implementations lack some features.
At the same time we have full-featured Go client for Centrifugo on top of Gorilla Websocket library. This is a write-up about work I’ve done adapting Go Websocket client for Centrifugo to be used with gomobile project and its gobind command to generate language bindings for Android and iOS application development.
The final result has its drawbacks of course. Fully native approach is better in several aspects (no overhead at all, more idiomatic code). Also gomobile is still considered experimental project with all the consequences.
But as a bright side — now I have single code base that supports all Centrifugo protocol features and can be transformed both to Java and Objective-C/Swift language bindings. And which I can support myself as it’s written in Go. This is not a complete replacement to existing clients (at least at moment) but an alternative to reason about.
There were lots of googling and collecting the crumbs over internet about gomobile during this work. Documentation is nice but short. So I hope this post will simplify a life for future gomobile explorers. Let’s look on some problems I came across and how generated bindings can be used for Android and iOS app development.
This is still a work in progress and I am looking for any feedback that can help to improve library I’ve done — Go code, example project for iOS and example project for Android. Help me to fix any parts of this work in a more efficient, idiomatic or maybe just more beautiful way.
For those who are just interested in final result and examples here is repo on Github.
What we build here
Before we start investigating gomobile specifics let me describe in short what is Centrifugo Websocket client so reader has more clean understanding about client’s goal so I can show concrete code examples throughout this post below.
Centrifugo is a real-time messaging server supporting Websocket and SockJS connections. It’s idiomatic purpose is being a one-way transport of real-time messages (messages about some event delivered instantly without application page reload) from application backend to clients. Centrifugo handles lots of persistent client connections and has an API so your backend can stream messages to connected clients. You can think about it as a standalone Socket.io like server. Like in Socket.io there is also a concept of channels (rooms) built in. In idiomatic case to achieve two-way communication with Centrifugo all messages that must be delivered from client to server should be published to your backend first using convenient technique (like AJAX POST request for web apps for example).
Centrifugo WebSocket client is a library that abstracts some Centrifugo protocol specific things like connecting to correct endpoint over WebSocket, sending authentication message, subscribing on channels, providing possibility to register important callbacks (new message in channel, connect or disconnect events), recovering missed messages after short network disconnects, reconnecting etc. All these specific things must be adapted to be used with gomobile.
The beginning
As I said I am far from mobile development (now a bit closer) so most things here I had to learn from scratch. Just making bindings from Go code is not enough — I still had to write some proof of concept examples in Swift and Java to be sure that bindings work as expected. I had to setup mobile development platforms — both XCode and Android Studio, create example projects that use generated language bindings, understand how to import and use library from Swift and Java code. As gomobile comes with some limitations I had to introduce some library API design decisions to deal with those restrictions.
I am not planning to write about installation a lot here— this is not very straightforward but described in docs.
But to sum up this is what you need to install (I am working on MacOS and my targets were both Swift/Objective-C and Java bindings):
- XCode (from App Store)
- Android Studio https://developer.android.com/studio/index.html
- Android NDK https://developer.android.com/ndk/downloads/index.html
- Java (
brew cask install java
)
And then:
gomobile init
– does all the rest. If you get an error xcrun — show-sdk-path: exit status 1
then running:
xcode-select --switch /Applications/Xcode.app
– can fix it for you.
There are also some tutorials in the internet I’ve found that can be helpful as starting point:
- https://github.com/golang/go/wiki/Mobile
- https://logpacker.com/blog/gomobile-library-development-for-ios-and-android
- https://www.sitepoint.com/ios-and-android-programming-with-go/
Sure you need to spend some time to install everything — this is the most tedious part — but I believe you to cope with this.
Now let’s look at programming challenges I came across when adapting Go client code to be used with gomobile. There are nothing too complex but I wish someone wrote about this process. And now I do this myself.
Named types
Gomobile does not support named types yet. Centrifugo Go client used time.Duration
for connection timeouts as part of public API. Solution is not difficult — gomobile supports int
type so I ended up with TimeoutMilliseconds
variable for public API which is transformed to time.Duration
inside Go code when needed. Not very idiomatic but should be pretty comfortable to use.
Another named type is json.RawMessage
that is used in Centrifugo client for message JSON payload. Every message published from application backend to Centrifugo API and then coming to client over WebSocket connection has this JSON payload part. This payload is specific to concrete application and Centrifugo leaves it untouched while message travelling from application to client.
In this case I had to export adapted Message
type where string
type used instead of json.RawMessage
. This requires conversion for every message coming to client but I think it’s not a big deal for a client side (it’s hard to imagine a situation when client needs to handle thousands of new messages from channels per second). I.e. our normal Go code Message
struct looked like this:
type Message struct {
UID string
Channel string
Data json.RawMessage
}
And adapted exported mobile variant now looks like:
type Message struct {
UID string
Channel string
Data string
}
Note: I omitted some fields in real message implementation here for simplicity.
Using nil
Several of my initializer functions accept pointers to struct that contains various event handlers. This can be event handlers for connect, disconnect and some other Centrifugo protocol specific events.
For example let’s consider Subscribe()
method of client that creates subscription to channel. It accepts string
channel as first argument and a pointer to SubEventHandler
struct as second argument. So it’s possible to hamdle messages published to that channel and handle unsubscribe
events (note: actually there are more events, let’s describe only 2 here for simplicity).
type SubEventHandler struct {
OnMessage func(m *Message)
OnUnsubscribe func()
}func (c *Client) Subscribe(ch string, events *SubEventHandler) {
...
}
If I don’t want to handle any events of subscription in Go I could just write:
sub, _ := client.Subscribe("channel", nil)
Go interprets nil
as nil pointer to SubEventHandler
and then that nil
can be used as identificator of default behaviour — skip handling events.
When I was trying to use generated bindings I tried to pass nil
from Swift language to subscribe method and got EXC_BAD_ACCESS
error. Looks like we have to pass concrete type in this case. So I made an initializer function for empty SubEventHandler
:
func NewSubEventHandler() *SubEventHandler {
return &SubEventHandler{}
}
Now this function can be used from Swift/Java to indicate empty SubEventHandler
object. But actually this is not a code I end up with, to allow Java and Swift/Objective-C register callbacks I needed to refactor SubEventHandler
a bit.
Callbacks
One of great parts of Go is that it’s possible to write code without callbacks. For example look at how Redigo library allows to handle messages coming from Redis PUB/SUB connection:
pubSubConn.Subscribe("example")
for {
switch v := pubSubConn.Receive().(type) {
case redis.Message:
fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
case redis.Subscription:
fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
case error:
return v
}
}
Though it’s possible and can be implemented in similar manner in case of Centrifugo mobile client we use function callbacks. As I showed in previous section we pass event handler functions as part of SubEventHandler
struct in second argument to Subscribe()
method. This has couple advantages in my opinion:
- reconnect built in into Centrifugo client, so when disconnect happens there is no need to restart routine manually in user-level code to start handling events again.
- it’s simpler to transfer from Go to Swift/Objective-C and Java — everyone understands quite well that in order to handle new message coming from channel you have to register callback function for this event.
So the task here is to register callback function from Swift/Java application level code. I spent some time to figure it out but finally found that solution was actually described in gomobile docs.
So my refactored SubEventHandler
looks like:
type MessageHandler interface {
OnMessage(*Sub, *Message) error
}type UnsubscribeHandler interface {
OnUnsubscribe(*Sub) error
}type SubEventHandler struct {
onMessage MessageHandler
onUnsubscribe UnsubscribeHandler
}func NewSubEventHandler() *SubEventHandler {
return &SubEventHandler{}
}func (h *SubEventHandler) OnMessage(mh MessageHandler) {
h.onMessage = mh
}func (h *SubEventHandler) OnUnsubscribe(uh UnsubscribeHandler) {
h.onUnsubscribe = uh
}
Actually there are ways to do the same simpler if using plain Go. But this code will be transformed into proper Swift protocol and Java interface classes. Let’s look how this code can be used from Go code:
type MyAppMessageHandler struct{}func(h *MyAppMessageHandler) OnMessage(m *Message) {
fmt.Printf("Message received: %#v", m)
}subEvents := NewSubEventHandler()
subEvents.OnMessage(&MyAppMessageHandler{})sub, _ := client.Subscribe("channel", subEvents)
The tricky part for me was how to use generated code to achieve the same in Swift and Java. We will look at this very soon below.
Map and slices in return value
Gomobile does not support slices and maps as return values. In case of Centrifugo client we have two methods when we had such return types:
History()
method to return a slice ofMessage
:[]Message
— last messages sent into channel.Presence()
method to get map with presence information for channel — i.e. clients connected to Centrifugo and subscribed to channel at this moment
Let’s look at History()
method in pure Go:
func (c *Client) History(channel string) ([]Message, error) {
...
}
To deal with this limitation I had to introduce new exported type: HistoryData
, see the code:
type HistoryData struct {
messages []Message
}func (d *HistoryData) NumMessages() int {
return len(d.messages)
}func (d *HistoryData) MessageAt(i int) *Message {
return &d.messages[i]
}func (c *Client) History(channel string) (*HistoryData, error) {
...
}
Now we return HistoryData
to Swift/Java code and this object has everything developer needs to iterate and extract single messages from it.
The same technique can be used for map as return value — just return some new wrapper type with helpful methods operating with types gomobile supports.
Staying effective as Go library
As I said above I had to put adapters to Centrifugo Message
type so it can be used from mobile environments — in this adapter code I transform json.RawMessage
to string
type. But I don’t want this conversion when using centrifuge-mobile
library from pure Go application. What I prefer is that user could just pass message.Data
to json.Unmarshal
function and get decoded application specific data.
So I decided to use Go mechanism of custom build tags to build in slightly different way for different targets.
See these two files in repo: one and two.
There is a build tag on top of each file so we can build a slightly different version of library for mobile platforms ensuring mobile
tag when building bindings for iOS:
gomobile bind -target=ios -tags="mobile" github.com/centrifugal/centrifuge-mobile
And Android:
gomobile bind -target=android -tags="mobile" github.com/centrifugal/centrifuge-mobile
And finally I want to show code examples that use generated client bindings. Examples will have some Centrifugo specific things — but I hope you will extract patterns that will help you when writing your own library with gomobile. Both examples will connect to Centrifugo and simply update text label on screen when messages from channel received settingData
from message as text value. Examples in library repo have more functionality including handling connect/disconnect events and even publishing new message into channel from client. To keep examples here as short as possible I decided to concentrate on bare minimum.
Basic usage for Android development
When bindings for Android built using gomobile bind
command it’s possible to import generated .aar
file (in case of our client centrifuge.aar
) to Android Studio project in two steps:
- File → New → New Module → Import .JAR/.AAR package → write path to
centrifuge.aar
and clickFinish
button - File → Project Structure → app → Dependencies → Add Module Dependency → add
centrifuge
to dependency list
And let’s look at Java code that uses Centrifugo Websocket client:
I also had to add some extra permission to AndroidManifest.xml
file:
<uses-permission android:name="android.permission.INTERNET" />
– so app had access to network.
To test app you can build APK: Build → Build APK, then just send it to your Android device (via Dropbox, email, USB) and install.
Basic usage for iOS development
For iOS you can just drag generated by gomobile bind
command Centrifuge.framework
bundle into project tree panel and you are ready to import from it.
Here is some Swift code:
The discouraging thing while deploying code generated by Gomobile on real device is that you need to disable bitcode in project build settings:
This means that Go does not generate LLVM bitcode for you, at moment this is not very critical but if Apple decides that bitcode should be a requirement — I think gomobile will have problems with targeting iOS.
If you prefer Objective-C then here is a snippet for you:
Final words
Adapting client was an interesting process and I am pretty satisfied with result. Will see how it goes. This is still a work in progress — API of library is not stable and I can update something after getting feedback — will try to maintain this post actual.
That’s all, thanks for reading!:)