WebSocket
CS 240: Adv Software Construction
HTTP
HTTP highlights
Client initiates, server responds
Methods, paths, headers
Extensive caching
HTTP�Server�
HTTP�Client
GET /scores HTTP/2
HTTP/2 200 OK��{ score: [1,2,3]}
Great for getting resources
Not great for peer to peer messaging
Client must ask server to speak
HTTP�Server�
HTTP�Client
POST /chat HTTP/2
�{ msg: “What’s up!” }
HTTP/2 200 OK��{ msg:
“About time you ask!” }
Short Polling
HTTP�Server�
HTTP�Client
POST /chat HTTP/2
{ msg: “Anything?” }
HTTP/2 200 OK�{ msg: “Nope” }
POST /chat HTTP/2
{ msg: “Anything?” }
HTTP/2 200 OK�{ msg: “Nope” }
Long Polling
HTTP�Server�
HTTP�Client
POST /chat HTTP/2
{ msg: “Anything” }
HTTP/2 200 OK�{ msg: “Something” }
Much later
HTTP bidirectional shortcomings
Inefficient (polling, reconnections)
HTTP overhead (method, paths, headers)
Application specific strategies
Client must always initiate every exchange
WebSocket
WebSocket
HTTP upgrade
Bidirectional (full duplex) communication
More efficient communication
Ping/Pong for detecting dropped connections
HTTP�Server�
webSocket�peer�
HTTP�Client���webSocket�peer
GET /chat HTTP/2�Upgrade: websocket�Connection: Upgrade
HTTP/2 200 OK�Upgrade: websocket
Connection: Upgrade
ping
pong
ping
pong
ws�peer
ws�peer
hey, how’s it going?
only if it’s good!
close
close
want to hear a joke?
. . .
Got any more jokes?
. . .
HTTP server
Install io.javalin, SLF4J.simple
import io.javalin.Javalin;
public class SimpleHttpEchoServer {
public static void main(String[] args) {
Javalin.create()
.get("/echo/{msg}", ctx ->
ctx.result("HTTP response: " + ctx.pathParam("msg")))
.start(8080);
}
}
HTTP client
private final HttpClient httpClient = HttpClient.newHttpClient();
private void get(String msg) throws URISyntaxException, IOException, InterruptedException {
String urlString = "http://localhost:8080/echo/" + msg;
HttpRequest request = HttpRequest.newBuilder()
.uri(new URI(urlString))
.timeout(java.time.Duration.ofMillis(5000))
.GET()
.build();
HttpResponse<String> httpResponse = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
if (httpResponse.statusCode() == 200) { System.out.println(httpResponse.body()); } else {
System.out.println("Error: received status code " + httpResponse.statusCode()); }
}
Simple WebSocket server
Install io.javalin, SLF4J.simple
import io.javalin.Javalin;
public class SimpleWsEchoServer {
public static void main(String[] args) {
Javalin.create()
.get("/echo/{msg}", ctx -> ctx.result("HTTP response: " +
ctx.pathParam("msg")))
.ws("/ws", ws -> {
ws.onConnect(ctx -> {
ctx.enableAutomaticPings();
System.out.println("Websocket connected");
});
ws.onMessage(ctx -> ctx.send("WebSocket response:" + ctx.message()));
ws.onClose(_ -> System.out.println("Websocket closed"));
})
.start(8080);
}
}
WebSocket Server
import io.javalin.Javalin;
public class WsEchoServer {
public static void main(String[] args) {
Javalin javalinServer = Javalin.create();
createHandlers(javalinServer);
javalinServer.start(8080);
}
private static void createHandlers(Javalin javalinServer) {
javalinServer.get("/echo/{msg}", new HttpEchoRequestHandler());
WsRequestHandler wsHandler = new WsRequestHandler();
javalinServer.ws("/ws", ws -> {
ws.onConnect(wsHandler);
ws.onClose(wsHandler);
ws.onMessage(wsHandler);
});
}
}
WebSocket Request Handler
import io.javalin.websocket.WsCloseContext;
import io.javalin.websocket.WsCloseHandler;
import io.javalin.websocket.WsConnectContext;
import io.javalin.websocket.WsConnectHandler;
import io.javalin.websocket.WsMessageContext;
import io.javalin.websocket.WsMessageHandler;
import org.jetbrains.annotations.NotNull;
public class WsRequestHandler implements WsConnectHandler, WsMessageHandler, WsCloseHandler {
@Override
public void handleConnect(WsConnectContext ctx) {
ctx.enableAutomaticPings();
System.out.println("Websocket connected");
}
@Override
public void handleMessage(WsMessageContext ctx) {
ctx.send("WebSocket response:" + ctx.message());
}
@Override
public void handleClose(WsCloseContext ctx) {
System.out.println("Websocket closed");
}
}
WebSocket client
Install glassfish.tyrus.bundles.standalone.client 2.1.4
public class WSEchoClient extends Endpoint {
private Session session;
public WSEchoClient() throws Exception {
URI uri = new URI("ws://localhost:8080/ws");
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
this.session = container.connectToServer(this, uri);
this.session.addMessageHandler(new MessageHandler.Whole<String>() {
public void onMessage(String message) {
System.out.println(message);
}
});
}
public void send(String msg) throws Exception {this.session.getBasicRemote().sendText(msg);}
public void onOpen(Session session, EndpointConfig endpointConfig) {}
}
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.Endpoint;
import jakarta.websocket.EndpointConfig;
import jakarta.websocket.MessageHandler;
import jakarta.websocket.Session;
import jakarta.websocket.WebSocketContainer;
import java.net.URI;
import java.util.Scanner;
Use WebSocket client
public class Main {
public static void main(String[] args) throws Exception {
var ws = new WSEchoClient();
Scanner scanner = new Scanner(System.in);
while (true){
ws.send(scanner.nextLine());
}
}
}
Petshop WebSocket Example
Chess WebSocket
Chess WebSocket
Server
Player1
CONNECT
MAKE_MOVE
Player2
CONNECT
LOAD_GAME
LOAD_GAME
NOTIFY join
LOAD_GAME
LOAD_GAME
UserGameCommand (from Client)
CONNECT: Connect to the game as a player or observer
MAKE_MOVE: Player move
LEAVE: Abandon game
RESIGN: Admit defeat
Chess server (websocket request handler)
public void handleMessage(@NotNull WsMessageContext wsMessageContext) throws Exception {
int gameId = -1;
Session session = wsMessageContext.session;
try {
UserGameCommand command = Serializer.fromJson(
wsMessageContext.message(), UserGameCommand.class);
gameId = command.getGameID();
String username = getUsername(command.getAuthString());
saveSession(gameId, session);
switch (command.getCommandType()) {
case CONNECT -> connect(session, username, (ConnectCommand) command);
case MAKE_MOVE -> makeMove(session, username, (MakeMoveCommand) command);
case LEAVE -> leaveGame(session, username, (LeaveGameCommand) command);
case RESIGN -> resign(session, username, (ResignCommand) command);
}
} catch (UnauthorizedException ex) {
sendMessage(session, gameId, new ErrorMessage("Error: unauthorized"));
} catch (Exception ex) {
ex.printStackTrace();
sendMessage(session, gameId, new ErrorMessage("Error: " + ex.getMessage()));
}
}
ServerMessage (from server)
LOAD_GAME: The current game
NOTIFICATION: Opaque textual message
ERROR: Opaque error message
WebsocketCommunicator
(called WebSocketFacade in Pet Shop)
private void handleMessage(String messageString) {
try {
ServerMessage message = Serializer.fromJson(messageString, ServerMessage.class);
listener.notify(message);
} catch(Exception ex) {
listener.notify(new ErrorMessage(ex.getMessage()));
}
}
Chess Client
public class Client implements ServerMessageObserver {
…
@Override
public void notify(ServerMessage message) {
switch (message.getServerMessageType()) {
case NOTIFICATION -> displayNotification(((NotificationMessage) message).getMessage());
case ERROR -> displayError(((ErrorMessage) message).getErrorMessage());
case LOAD_GAME -> loadGame(((LoadGameMessage) message).getGame());
}
}
…
}