Websockets - What are those and how to use them in Flutter?
Atharva Patwardhan
Posted on May 14, 2023
If you're someone who's been developing mobile apps for a while, you've probably used REST APIs for fetching/storing data on a remote server somewhere. These APIs use something that we in the tech biz call HTTP protocols, so the calls that we make to these APIs are often called as HTTP requests.
Why are we talking about HTTP requests in an article about Websockets?
"Before one learns something, one must learn WHY it exists in the first place." - Unknown
Why do we need websockets?
As it turns out, HTTP requests are uni-directional or one-way requests. That means, once an HTTP request is sent, the sender needs to wait for a response from the receiver. This sounds perfectly fine, but now imagine using this in a chat application. Two devices chatting with each other would have to constantly keep making HTTP requests to keep track of each other's messages. This is not just tedious but also a costly operation to perform.
Does this mean HTTP cannot be used for messaging apps? No. Take Gmail, or any other email service for instance. The sender of the email simply needs to send an email, without the need of an immediate reply from the receiver. HTTP is more than sufficient enough to be applied in this case.
Coming back to the chat application case, it is quite clear that HTTP isn't the best option to go through. We would need to use a protocol that can pass information both ways, in realtime.
Enter, the websocket. WebSocket is a communications protocol for a persistent, bi-directional, full duplex TCP connection from a client to a server.
How a Websocket works
A WebSocket connection is initiated by sending a WebSocket handshake request from a browser's HTTP connection to a server to upgrade the connection. From that point, the connection is binary and does not conform to the HTTP protocol. A server application is aware of all WebSocket connections and can communicate with each one individually.
As WebSocket remains open, either the server or the client can send messages at any time until one of them closes the session. The communication can be initiated at either end, which makes event-driven programming possible.
Common applications of websockets
You'd be surprised to learn how many things we use on a daily basis harness the power of websockets. Some of them are:
- Realtime Chatting apps (duh) eg. WhatsApp, Messenger, Telegram, etc
- Multiplayer games eg. Valorant, CS:GO, Apex Legends, PUBG, etc
- Collaborative apps eg. Figma, Google Docs
- Audio/Video Chats eg. Skype, Google Meet
- Realtime location apps eg. Google Maps, Apple Maps
You get the point. Without websockets, most of us wouldn't be living the same convenient lives we are living right now.
Fun Fact
If you're someone who plays online multiplayer games, you must've faced Ping issues at least once. This "Ping", is a term used in the ping-pong method to ensure that the websocket is connected to the server. The approach goes something like this:
The client sends a "ping" type of message with some dummy data to the server
The server receives this "ping" and simply returns the same dummy data back to the client with a "pong" type of message
The client receives this "pong" and thus acknowledges that the connection is active
The total roundabout time taken to receive the response from the server is denoted to us as the "Ping". Another word used to describe this process is "Latency" and the lesser the number, the better it results in performance.
Implementation in Flutter
Alright. So we have a pretty good idea of what a websocket is used for. Let's move on to implementing a basic socket in Flutter.
1. Install the package
For implementation of websockets in our Flutter app, we'll be using the package web_socket_channel.
2. Basic implementation
import 'package:web_socket_channel/io.dart';
class WebsocketService {
static final WebsocketService _webSocketService =
WebsocketService._internal();
factory WebsocketService() {
return _webSocketService;
}
WebsocketService._internal();
IOWebSocketChannel? channel;
// DUMMY URL; DO NOT TRY TO IMPLEMENT
String websocketUrl = 'ws://test.example.com?token=eySjks7sadjklwaskfjtAw8sS';
void init() {
// INITIATE A CONNECTION THROUGH AN IOWebsocketChannel channel
channel = IOWebSocketChannel.connect(websocketUrl);
if (channel != null) {
// IF CHANNEL IS INITIALIZED AND WEBSOCKET IS CONNECTED
// LISTEN TO WEBSOCKET EVENTS
channel!.stream.listen(_eventListener).onDone(_reconnect);
}
}
void transmit(dynamic data) {
// THIS METHOD CAN BE CALLED ANYWHERE THROUGHOUT THE APP
// AND CAN BE USED TO SEND DATA FROM THE CLIENT TO THE SERVER
// VIA THE WEBSOCKET
if (channel != null) {
channel!.sink.add(data);
}
}
void _eventListener(dynamic event) {
if (event == 'message') {
// PERFORM OPERATIONS ON THE EVENT PAYLOAD
}
}
void _reconnect() {
// IF CONTROL HAS TRANSFERRED TO THIS FUNCTION, IT MEANS
// THAT THE WEBSOCKET HAS DISCONNECTED.
if (channel != null) {
// CLOSE THE PREVIOUS WEBSOCKET CHANNEL AND ATTEMPT A RECONNECTION
channel!.sink.close();
init();
}
}
}
Let's go through this code step-by-step.
static final WebsocketService _webSocketService = WebsocketService._internal();
factory WebsocketService() {
return _webSocketService;
}
WebsocketService._internal();
IOWebSocketChannel? channel;
// DUMMY URL; DO NOT TRY TO IMPLEMENT
String websocketUrl = 'ws://test.example.com?token=eySjks7sadjklwaskfjtAw8sS';
First, we create a websocket service using the singleton pattern (i.e. only one instance of the service can be created throughout the app). Read more about Singletons here.
We'll be using the IOWebsocketChannel class to initialize our websocket channel through which our data will be passed.
The websocketUrl stores the endpoint of our websocket, quite similar to the url endpoint we use in HTTP requests. For this example, I've used a dummy url.
TLDR: Don't use the dummy url, it won't work
void init() {
// INITIATE A CONNECTION THROUGH AN IOWebsocketChannel channel
channel = IOWebSocketChannel.connect(websocketUrl);
if (channel != null) {
// IF CHANNEL IS INITIALIZED AND WEBSOCKET IS CONNECTED
// LISTEN TO WEBSOCKET EVENTS
channel!.stream.listen(_eventListener).onDone(_reconnect);
}
}
Then, inside the init() method, we'll initialize our websocket channel by passing our websocket url endpoint to the connect() method.
After the websocket is initialized, we can start listening to the events emitted inside the websocket. Since all events coming from the websocket server are emitted inside a stream, we'll be using channel.stream.listen() method to listen to these events.
void _eventListener(dynamic event) {
if (event == 'message') {
// PERFORM OPERATIONS ON THE EVENT PAYLOAD
}
}
We create a method _eventListener() and pass this to the channel.stream.listen() method. By doing so, all events can be caught inside the _eventListener(). These events are generally received in the form of JSON objects, so it's quite easy to parse the data and perform operations on it like one would using HTTP GET requests.
void transmit(dynamic data) {
// THIS METHOD CAN BE CALLED ANYWHERE THROUGHOUT THE APP
// AND CAN BE USED TO SEND DATA FROM THE CLIENT TO THE SERVER
// VIA THE WEBSOCKET
if (channel != null) {
channel!.sink.add(data);
}
}
Cool, we'll be able to receive data from the websocket, but what about sending data to the server via the websocket? For this, we created another method transmit(). Similar to how data is received via a stream, data can be sent via a sink, and so, adding data to the sink is all that this method does. This method is better kept public instead of private, since we might need to send data from anywhere inside the app.
void _reconnect() {
// IF CONTROL HAS TRANSFERRED TO THIS FUNCTION, IT MEANS
// THAT THE WEBSOCKET HAS DISCONNECTED.
if (channel != null) {
// CLOSE THE PREVIOUS WEBSOCKET CHANNEL AND ATTEMPT A RECONNECTION
channel!.sink.close();
init();
}
}
Now, if the websocket disconnects suddenly due to some network problems, we attempt a reconnection by using the _reconnect() method. We close the previous connection using channel.sink.close() and call the init() method to re-initialize our websocket connection.
Lastly, we need to call the init() method somewhere for the service to be initialized in the first place. Generally speaking, most websockets require a kind of token or a key to be passed inside the url endpoint. In case of your app, you'll just have to decide which is the best place to initialize your websocket. It has to be some point in the app where you have the token/key that's required in the url. Once you're there, just add this one line and you are good to go.
WebsocketService().init();
And, that's it! This is pretty much all you need to get a websocket connection up and running inside your app.
Kudos! You now possess the knowledge of the websocket. Please comment below your suggestions/feedback and share this article if you found it useful 🥹
Until next time.
Adios.
Posted on May 14, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.