1 of 29

WebSocket

CS 240: Adv Software Construction

2 of 29

HTTP

3 of 29

HTTP highlights

Client initiates, server responds

Methods, paths, headers

Extensive caching

4 of 29

HTTP�Server�

HTTP�Client

GET /scores HTTP/2

HTTP/2 200 OK��{ score: [1,2,3]}

5 of 29

Great for getting resources

Not great for peer to peer messaging

6 of 29

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!” }

7 of 29

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” }

8 of 29

Long Polling

HTTP�Server�

HTTP�Client

POST /chat HTTP/2

{ msg: “Anything” }

HTTP/2 200 OK{ msg: “Something” }

Much later

9 of 29

HTTP bidirectional shortcomings

Inefficient (polling, reconnections)

HTTP overhead (method, paths, headers)

Application specific strategies

Client must always initiate every exchange

10 of 29

WebSocket

11 of 29

WebSocket

HTTP upgrade

Bidirectional (full duplex) communication

More efficient communication

Ping/Pong for detecting dropped connections

12 of 29

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

13 of 29

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?

. . .

14 of 29

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);

}

}

15 of 29

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()); }

}

16 of 29

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);

}

}

17 of 29

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);

});

}

}

18 of 29

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");

}

}

19 of 29

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;

20 of 29

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());

}

}

}

21 of 29

Petshop WebSocket Example

22 of 29

Chess WebSocket

23 of 29

Chess WebSocket

Server

Player1

CONNECT

MAKE_MOVE

Player2

CONNECT

LOAD_GAME

LOAD_GAME

NOTIFY join

LOAD_GAME

LOAD_GAME

24 of 29

UserGameCommand (from Client)

CONNECT: Connect to the game as a player or observer

MAKE_MOVE: Player move

LEAVE: Abandon game

RESIGN: Admit defeat

25 of 29

26 of 29

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()));

}

}

27 of 29

ServerMessage (from server)

LOAD_GAME: The current game

NOTIFICATION: Opaque textual message

ERROR: Opaque error message

28 of 29

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()));

}

}

29 of 29

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());

}

}

}