Web Sockets are a great way to stream data from a server to a client. Http4s is a fantastic http server for Scala.
The trick to serving Web Sockets with Http4s is that you need to get hold of WebSocketBuilder2
before you can make the HttpRoutes
. You get the WebSocketBuilder by using withHttpWebSocketApp
on the ServerBuilder
your using.
Then you can serve or receive data as FS2 Streams of Stream[IO, WebSocketFrame]
with all the power and flexibility they offer.
For example you to serve websocket data on /ws
you could have a routes
function like this.
def routes(ws: WebSocketBuilder2[IO]): HttpRoutes[IO] =
HttpRoutes.of[IO] {
case GET -> Root / "ws" =>
val send: Stream[IO, WebSocketFrame] =
Stream.awakeEvery[IO](1.second)
.evalMap(_ => IO(WebSocketFrame.Text("ok")))
val receive: Pipe[IO, WebSocketFrame, Unit] =
in => in.evalMap(frameIn => IO(println("in " + frameIn.length)))
ws.build(send, receive)
}
Then in a JavaScript client, you can subscribe to the Web Socket like this.
const socket = new WebSocket("ws://localhost:8080/ws");
socket.onmessage = function (event) {
console.log(`[message] Data received from server: ${event.data}`);
};
The complete example
import com.comcast.ip4s._
import cats.effect.{ExitCode, IO, IOApp, Resource}
import cats.syntax.all._
import fs2.{Pipe, Stream}
import org.http4s.HttpRoutes
import org.http4s.dsl.io._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.Server
import org.http4s.server.websocket.WebSocketBuilder2
import org.http4s.websocket.WebSocketFrame
import java.time.Instant
import scala.concurrent.duration._
object Main extends IOApp {
def routes(ws: WebSocketBuilder2[IO]): HttpRoutes[IO] =
HttpRoutes.of[IO] {
case GET -> Root / "ws" =>
val send: Stream[IO, WebSocketFrame] =
Stream.awakeEvery[IO](1.second)
.evalMap(_ => IO(WebSocketFrame.Text("ok")))
val receive: Pipe[IO, WebSocketFrame, Unit] =
in => in.evalMap(frameIn => IO(println("in " + frameIn.length)))
ws.build(send, receive)
}
val serverResource: Resource[IO, Server] =
EmberServerBuilder
.default[IO]
.withHost(ipv4"0.0.0.0")
.withPort(port"8080")
.withHttpWebSocketApp(ws => routes(ws).orNotFound)
.build
def run(args: List[String]): IO[ExitCode] = {
Stream
.resource(
serverResource >> Resource.never
)
.compile
.drain
.as(ExitCode.Success)
}
}