|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +title: π WebSockets |
| 4 | +--- |
| 5 | + |
| 6 | +# WebSockets π |
| 7 | + |
| 8 | +Dart Frog recently introduced [`package:dart_frog_web_socket`](https://pub.dev/packages/dart_frog_web_socket) to make working with WebSockets easier. |
| 9 | + |
| 10 | +:::info |
| 11 | +We added WebSocket support as a separate package to keep the Dart Frog package lightweight, containing only core functionality. In the future, we may add additional packages in order to extend the Dart Frog ecosystem without bloating the core. |
| 12 | +::: |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +To get started, add the `dart_frog_web_socket` package as a dependency to your existing Dart Frog project: |
| 17 | + |
| 18 | +```sh |
| 19 | +dart pub add dart_frog_web_socket |
| 20 | +``` |
| 21 | + |
| 22 | +## Creating a WebSocket Handler |
| 23 | + |
| 24 | +We can use the `webSocketHandler` from `package:dart_frog_web_socket` to manage WebSocket connections. |
| 25 | + |
| 26 | +You can either create a new route handler or integrate with an existing handler. For simplicity, we'll first take a look at adding a new route handler specifically for WebSocket connections. |
| 27 | + |
| 28 | +Start by creating a new route, `routes/ws.dart`: |
| 29 | + |
| 30 | +```dart |
| 31 | +import 'package:dart_frog/dart_frog.dart'; |
| 32 | +
|
| 33 | +Response onRequest(RequestContext context) { |
| 34 | + return Response(); |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +Next, instead of handling the request directly, we can create a WebSocket handler: |
| 39 | + |
| 40 | +```dart |
| 41 | +import 'package:dart_frog/dart_frog.dart'; |
| 42 | +import 'package:dart_frog_web_socket/dart_frog_web_socket.dart'; |
| 43 | +
|
| 44 | +Future<Response> onRequest(RequestContext context) async { |
| 45 | + final handler = webSocketHandler((channel, protocol) { |
| 46 | + // TODO: react to new connections. |
| 47 | + }); |
| 48 | + return handler(context); |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +:::note |
| 53 | +We need to refactor the request handler to be `async` and return a `Future<Response>` when using the `webSocketHandler`. |
| 54 | +::: |
| 55 | + |
| 56 | +The `webSocketHandler` will handle upgrading `HTTP` requests to WebSocket connections and provides an `onConnection` callback which exposes the `WebSocketChannel` as well as an optional subprotocol. |
| 57 | + |
| 58 | +## Receiving Messages from the Client |
| 59 | + |
| 60 | +Next, we can subscribe to the stream of messages exposed by the `WebSocketChannel`: |
| 61 | + |
| 62 | +```dart |
| 63 | +import 'package:dart_frog/dart_frog.dart'; |
| 64 | +import 'package:dart_frog_web_socket/dart_frog_web_socket.dart'; |
| 65 | +
|
| 66 | +Future<Response> onRequest(RequestContext context) async { |
| 67 | + final handler = webSocketHandler((channel, protocol) { |
| 68 | + channel.stream.listen((message) { |
| 69 | + // Handle incoming client messages. |
| 70 | + print(message); |
| 71 | + }); |
| 72 | + }); |
| 73 | + return handler(context); |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +For simplicity, we are just printing all messages sent by connected clients. |
| 78 | + |
| 79 | +Before moving on, let's verify that a client is able to establish a connection and send a message to our server. To do this, we'll create a simple client in Dart. |
| 80 | + |
| 81 | +## Sending Messages from the Client |
| 82 | + |
| 83 | +Create a new directory called `example` at the project root and create a `pubspec.yaml`: |
| 84 | + |
| 85 | +```yaml |
| 86 | +name: example |
| 87 | +publish_to: none |
| 88 | + |
| 89 | +environment: |
| 90 | + sdk: '>=2.18.0 <3.0.0' |
| 91 | + |
| 92 | +dependencies: |
| 93 | + web_socket_channel: ^2.0.0 |
| 94 | +``` |
| 95 | +
|
| 96 | +Next, install the dependencies: |
| 97 | +
|
| 98 | +```sh |
| 99 | +dart pub get |
| 100 | +``` |
| 101 | + |
| 102 | +Now, create a `main.dart` with the following contents: |
| 103 | + |
| 104 | +```dart |
| 105 | +import 'package:web_socket_channel/web_socket_channel.dart'; |
| 106 | +
|
| 107 | +void main() { |
| 108 | + // Connect to the remote WebSocket endpoint. |
| 109 | + final uri = Uri.parse('ws://localhost:8080/ws'); |
| 110 | + final channel = WebSocketChannel.connect(uri); |
| 111 | +
|
| 112 | + // Send a message to the server. |
| 113 | + channel.sink.add('hello'); |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +We're using [`package:web_socket_channel`](https://pub.dev/packages/web_socket_channel) to connect to our Dart Frog `/ws` endpoint. We can then send messages to the server by calling `add` on the `WebSocketChannel` sink. |
| 118 | + |
| 119 | +We can run our Dart Frog dev server now: |
| 120 | + |
| 121 | +```sh |
| 122 | +dart_frog dev |
| 123 | +``` |
| 124 | + |
| 125 | +:::note |
| 126 | +Be sure to run `dart_frog dev` from the project root. |
| 127 | +::: |
| 128 | + |
| 129 | +Once the server is running in a new terminal, we can run our example client to test the connection: |
| 130 | + |
| 131 | +```sh |
| 132 | +dart example/main.dart |
| 133 | +``` |
| 134 | + |
| 135 | +We should see the message we sent in our server logs: |
| 136 | + |
| 137 | +``` |
| 138 | +β Running on http://localhost:8080 (1.3s) |
| 139 | +The Dart VM service is listening on http://127.0.0.1:8181/YKEF_nbwOpM=/ |
| 140 | +The Dart DevTools debugger and profiler is available at: http://127.0.0.1:8181/YKEF_nbwOpM=/devtools/#/?uri=ws%3A%2F%2F127.0.0.1%3A8181%2FYKEF_nbwOpM%3D%2Fws |
| 141 | +[hotreload] Hot reload is enabled. |
| 142 | +hello |
| 143 | +``` |
| 144 | + |
| 145 | +## Sending Messages from the Server |
| 146 | + |
| 147 | +Now, let's send a message back to the client from the server. We can do that by adding a message to the channel sink on the server just like we previously did on the client. |
| 148 | + |
| 149 | +```dart |
| 150 | +import 'package:dart_frog/dart_frog.dart'; |
| 151 | +import 'package:dart_frog_web_socket/dart_frog_web_socket.dart'; |
| 152 | +
|
| 153 | +Future<Response> onRequest(RequestContext context) async { |
| 154 | + final handler = webSocketHandler((channel, protocol) { |
| 155 | + channel.stream.listen((message) { |
| 156 | + // Handle incoming client messages. |
| 157 | + print(message); |
| 158 | + }); |
| 159 | +
|
| 160 | + // Send a message back to the client. |
| 161 | + channel.sink.add('hi'); |
| 162 | + }); |
| 163 | + return handler(context); |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +## Receiving Messages from the Server |
| 168 | + |
| 169 | +Lastly, we need to update the client code to subscribe to messages from the server. |
| 170 | + |
| 171 | +```dart |
| 172 | +import 'package:web_socket_channel/web_socket_channel.dart'; |
| 173 | +
|
| 174 | +void main() { |
| 175 | + // Connect to the remote WebSocket endpoint. |
| 176 | + final uri = Uri.parse('ws://localhost:8080/ws'); |
| 177 | + final channel = WebSocketChannel.connect(uri); |
| 178 | +
|
| 179 | + // Send a message to the server. |
| 180 | + channel.sink.add('hello'); |
| 181 | +
|
| 182 | + // Subscribe to messages from the server. |
| 183 | + channel.stream.listen((message) { |
| 184 | + print(message); |
| 185 | + }); |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Once we save all the changes and let the server hot reload, we can run the client code again: |
| 190 | + |
| 191 | +```sh |
| 192 | +dart example/main.dart |
| 193 | +``` |
| 194 | + |
| 195 | +And we should see the following output: |
| 196 | + |
| 197 | +**Server** |
| 198 | + |
| 199 | +```sh |
| 200 | +[hotreload] Hot reload is enabled. |
| 201 | +hello |
| 202 | +``` |
| 203 | + |
| 204 | +**Client** |
| 205 | + |
| 206 | +```sh |
| 207 | +hi |
| 208 | +``` |
| 209 | + |
| 210 | +## Simplifying Things Further |
| 211 | + |
| 212 | +To make things even simpler, we can refactor the route handler implementation in cases where the route is only managing WebSocket connections: |
| 213 | + |
| 214 | +```dart |
| 215 | +import 'package:dart_frog/dart_frog.dart'; |
| 216 | +import 'package:dart_frog_web_socket/dart_frog_web_socket.dart'; |
| 217 | +
|
| 218 | +Handler get onRequest { |
| 219 | + return webSocketHandler((channel, protocol) { |
| 220 | + channel.stream.listen((message) { |
| 221 | + // Handle incoming client messages. |
| 222 | + print(message); |
| 223 | + }); |
| 224 | +
|
| 225 | + // Send a message back to the client. |
| 226 | + channel.sink.add('hi'); |
| 227 | + }); |
| 228 | +} |
| 229 | +``` |
| 230 | + |
| 231 | +## Conclusion |
| 232 | + |
| 233 | +That's it! π |
| 234 | + |
| 235 | +We've successfully established a WebSocket connection between the Dart Frog server and a client. We've also demonstrated how messages can be streamed between a server and one or more clients. |
0 commit comments