What does this game of tic-tac-toe and Twitter have in common? Both have been implemented using relatively the same technologies: Java and Netty. It was big news in 2010 when Twitter migrated their search from Ruby on Rails to Java-based Netty (read about it here1). Not only was it big in the news department, but it was also big in the results department: Twitter reported their search performance increased by 3x.
What is Netty?
Netty2 is a client server framework by JBoss that simplifies network programming. Netty is built on top of Java NIO3 but provides a much more simple API to work with. Netty can be used to build a custom server for network communications; it can be used to build anything including a lightweight HTTP server, a TCP / UDP server, a WebSocket server, or any other network server you can dream of. Because Netty is built on Java NIO, Netty’s programming model is asynchronous. This means Netty is very well suited for any number of bi-directional communication projects such as real-time group chat or anything else requiring the server to push information to a client4 rather than other methods of network communication such as long-polling5.
What is the WebSocket Protocol?
WebSocket is a protocol used for bi-directional asynchronous communications between a client (usually a web browser) and a server that supports the WebSocket protocol.
A WebSocket client connects to a server via standard HTTP and performs a handshake, which creates a persistent tunnel between the client and server. After the handshake is performed the client and the server communicate freely using a message/event-driven programming model (binding actions/methods to events). The beautiful thing about the WebSocket protocol is the number of persistent connections that WebSocket servers can handle, easily numbering in the ten-of-thousands, and the volume of messages that can be processed (depending on the way the server is implemented).
Netty in Action: A Game of Tic-Tac-Toe
Rather than build the same-old group chat application everyone else does to show off the flexibility of Netty and the WebSocket protocol, I decided to build a simple game of tic-tac-toe instead. It seems like an odd decision considering tic-tac-toe is a turn-based game rather than a real-time game, but most tic-tac-toe game demos on the net are single-player. Rather than build another single player game of tic-tac-toe, let’s build a multiplayer game!
The core concepts behind the game are:
- The tic-tac-toe client and server can support a (theoretically) infinite number of simultaneous games, and each game supports 2 players
- The player loads the client (a web page) and waits for an opponent
- An opponent loads the client
- Both players are matched together automatically
- The server responds to both players to let them know their game has started. The server notifies the client which player should go first and each player’s assigned letter
- The client only allows one player to select a cell at once
- After each turn, the other player is notified of the other player’s selection and their screen is updated automatically
- After each turn the server determines if someone has won or if the game is a draw
If you’d like to review the full code for the working Netty tic-tac-toe client and server before reading ahead, feel free to check it out:
- https://github.com/rocketpages/Netty-TicTacToe-Server
- https://github.com/rocketpages/TicTacToe-Client
Building the Netty Server
The first step to creating our tic-tac-toe server is to build the server itself. Creating a new server in Netty is dead simple. We simply need to instruct Netty which port to bind to and which pipeline factory to use.
Netty works based on inbound and outbound “handlers”; upstream handlers and downstream handlers. As a message is either received by the server or sent by the server, it is acted upon by the handlers that you specify in the pipeline factory. This is a flexible architecture and lets us work in a very modular fashion on any given message. Anyone who has done MDB, MQ, or SOAP programming (SOAP handler chain) should be familiar with the concept already.
public class TicTacToeServer {
public static void main(String[] args) throws Exception {
ChannelFactory factory =
new NioServerSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ServerBootstrap bootstrap = new ServerBootstrap(factory);
bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory());
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
bootstrap.bind(new InetSocketAddress(9000));
System.out.println("TicTacToe Server: Listening on port 9000");
}
}
The next step is creating the pipeline factory. In this case we’re using a custom pipeline factory called WebSocketServerPipelineFactory. We could also have built this as an anonymous class as our implementation is fairly simple, but I decided to break it out into it’s own high-level class.
public class WebSocketServerPipelineFactory implements ChannelPipelineFactory {
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = pipeline();
pipeline.addLast("decoder", new HttpRequestDecoder());
pipeline.addLast("aggregator", new HttpChunkAggregator(65536));
pipeline.addLast("encoder", new HttpResponseEncoder());
pipeline.addLast("handler", new TicTacToeServerHandler());
return pipeline;
}
}
Take a second to look at the above code. I’ll break out some of the key terms and concepts to understand:
Channel
A channel is a persistent connection (tunnel) from a specific client to the server.
ChannelPipeline
Each channel can be customized with it’s own pipeline. When you take a second to think about it, it becomes obvious how powerful this concept is. Encoders, decoders, aggregator, and handlers are grouped together to form a pipeline. A pipeline instructs Netty how to act on each channel.
Encoders, Decoders, and Aggregators
Netty is a low-level framework built on Java NIO. When a client first connects with the server, we need to perform a WebSocket handshake. NIO doesn’t care about HTTP however, it cares about packets. In order to process the HTTP request and response, we need to instruct Netty how to deal with the packets we’re receiving and sending. Netty makes this easy and provides multiple encoders, decoders, aggregator, and handlers. We’ll typically extend Netty to create our own custom handlers, but if we’re so inclined, we can also get really low-level and create our own encoders, etc. Keep in mind that handlers can be swapped-out at runtime. After the initial HTTP handshake is performed, we’re only going to be responding to WebSocket requests for this channel. You’ll see later how we change from an HTTP-based pipeline to a WebSocket-based pipeline for a specific channel (aka client, aka player).
TicTacToeServerHandler
This is the heart of the application, responsible for consuming and pushing all messages to and from clients. An instance of this handler is specific to a channel, but we can also declare static variables to keep track of all the games of tic-tac-toe in progress at any given time.
Let’s check out the TicTacToeServerHandler6. I’ll discuss some of the core concepts below.
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object msg = e.getMessage();
if (msg instanceof HttpRequest) {
handleHttpRequest(ctx, (HttpRequest) msg);
} else if (msg instanceof WebSocketFrame) {
handleWebSocketFrame(ctx, (WebSocketFrame) msg);
}
}
The messageReceived(…) method is the main callback method provided by Netty to process incoming messages from clients. In our case, we’ll only be processing two types of messages: HttpRequest (for the initial handshake from a client) and WebSocketFrame (for all incoming communications after the tunnel has been established).
private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
// Allow only GET methods.
if (req.getMethod() != HttpMethod.GET) {
sendHttpResponse(ctx, req, new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN));
return;
}
// Serve the WebSocket handshake request.
if (req.getUri().equals(WEBSOCKET_PATH) && Values.UPGRADE.equalsIgnoreCase(req.getHeader(CONNECTION)) && WEBSOCKET.equalsIgnoreCase(req.getHeader(Names.UPGRADE))) {
// Create the WebSocket handshake response.
HttpResponse res = new DefaultHttpResponse(HTTP_1_1, new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
res.addHeader(Names.UPGRADE, WEBSOCKET);
res.addHeader(CONNECTION, Values.UPGRADE);
// Fill in the headers and contents depending on handshake method. New handshake specification has a challenge.
if (req.containsHeader(SEC_WEBSOCKET_KEY1) && req.containsHeader(SEC_WEBSOCKET_KEY2)) {
// New handshake method with challenge
res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
res.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketLocation(req));
String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL);
if (protocol != null) {
res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
}
// Calculate the answer of the challenge.
String key1 = req.getHeader(SEC_WEBSOCKET_KEY1);
String key2 = req.getHeader(SEC_WEBSOCKET_KEY2);
int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
long c = req.getContent().readLong();
ChannelBuffer input = ChannelBuffers.buffer(16);
input.writeInt(a);
input.writeInt(b);
input.writeLong(c);
ChannelBuffer output = ChannelBuffers.wrappedBuffer(MessageDigest.getInstance("MD5").digest(input.array()));
res.setContent(output);
} else {
// Old handshake method with no challenge:
res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
res.addHeader(WEBSOCKET_LOCATION, getWebSocketLocation(req));
String protocol = req.getHeader(WEBSOCKET_PROTOCOL);
if (protocol != null) {
res.addHeader(WEBSOCKET_PROTOCOL, protocol);
}
}
// Upgrade the connection and send the handshake response.
ChannelPipeline p = ctx.getChannel().getPipeline();
p.remove("aggregator");
p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());
// Write handshake response to the channel
ctx.getChannel().write(res);
// Upgrade encoder to WebSocketFrameEncoder
p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
// Initialize the game. Assign players to a game and assign them a letter (X or O)
initGame(ctx);
return;
}
// Send an error page otherwise.
sendHttpResponse(ctx, req, new DefaultHttpResponse(
HttpVersion.HTTP_1_1, HttpResponseStatus.FORBIDDEN));
}
The code above is performed when the client initially connects to the tic-tac-toe server. The WebSocket specification defines how a handshake needs to be performed. There are two versions of the handshake implemented above: one based on the old WebSocket specification (75), and the other based on the new version of the WebSocket specification (76). The biggest difference between the two handshake methods is the old version does not require a challenge while the new version does. To slightly complicate matters, different browsers implement different versions of the WebSocket specification. Another twist is that neither 75 or 76 is the latest version of the WebSocket specification, but these are the most commonly implemented by modern browsers. A deep dive into the WebSocket specification is beyond the scope of this article, but you can find the latest draft specification here7.
This brings up an important point; the WebSocket specification is constantly evolving. Fast. I would strongly recommend exploring the option of using a higher-level framework for business programming rather than rolling your own server, because as you see above, you’ll need to keep up-to-date with the latest changes in the spec and which versions are supported by which browsers. It’s generally a good idea to leave this up to framework developers. That being said, even if you’re planning to use a high-level framework, getting an in-depth knowledge of Netty is a very good thing. Down the road if you need the kind of flexibility and power that Twitter does you’ll already know how to implement it.
Also note that we’re programmatically changing the aggregator, encoder, and decoder for this channel only. As soon as the handshake is successful we will be communicating with the client exclusively using the WebSocket protocol rather than HTTP. If we were to leave the original HTTP encoder, decoder, and aggregator alone, our server would not understand how to deal with a WebSocket packet; it would still be trying to piece together usable HTTP requests and responses.
Now let’s take a look at some tic-tac-toe specific logic. First we’ll need to create a game and assign players to it. As players connect to our server they’re paired off and assigned to a game. Our server will be able to support an infinite number of games simultaneously, although I doubt tic-tac-toe will ever become as popular as Twitter.
private void initGame(ChannelHandlerContext ctx) {
// Try to find a game waiting for a player. If one doesn't exist, create a new one.
Game game = findGame();
// Create a new instance of player and assign their channel for WebSocket communications.
Player player = new Player(ctx.getChannel());
// Add the player to the game.
Game.PlayerLetter letter = game.addPlayer(player);
// Add the game to the collection of games.
games.put(game.getId(), game);
// Send confirmation message to player with game ID and their assigned letter (X or O)
ctx.getChannel().write(new DefaultWebSocketFrame(new HandshakeMessageBean(game.getId(), letter.toString()).toJson()));
// If the game has begun we need to inform the players. Send them a "turn" message (either "waiting" or "your_turn")
if (game.getStatus() == Game.Status.IN_PROGRESS) {
game.getPlayer(PlayerLetter.X).getChannel().write(new DefaultWebSocketFrame(new TurnMessageBean(YOUR_TURN).toJson()));
game.getPlayer(PlayerLetter.O).getChannel().write(new DefaultWebSocketFrame(new TurnMessageBean(WAITING).toJson()));
}
}
private Game findGame() {
// Find an existing game and return it
for (Game g : games.values()) {
if (g.getStatus().equals(Game.Status.WAITING)) {
return g;
}
}
// Or return a new game
return new Game();
}
The code above is mainly responsible for creating and maintaining games, which are stored in the TicTacToeServerHandler as a static collection (shared across all instances of our handler). As players connect to our server we assign them to a game and let them know that they are either waiting for an opponent or that an opponent has connected and their game has begun. Exciting!
Finally, we need to accept incoming WebSocket messages. Each message represents a player’s turn (which cell they selected). After we process the turn information we need to push data out to their opponent to let them know how badly they’re getting pwned. We also need to check the status of the ongoing game; it’s fairly important to be able to tell if a game has been won or tied after the last move!
private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
Gson gson = new Gson();
IncomingMessageBean message = gson.fromJson(frame.getTextData(), IncomingMessageBean.class);
Game game = games.get(message.getGameId());
Player opponent = game.getOpponent(message.getPlayer());
Player player = game.getPlayer(PlayerLetter.valueOf(message.getPlayer()));
// Mark the cell the player selected.
game.markCell(message.getGridIdAsInt(), player.getLetter());
// Get the status for the current game.
boolean winner = game.isPlayerWinner(player.getLetter());
boolean tied = game.isTied();
// Respond to the opponent in order to update their screen.
String responseToOpponent = new OutgoingMessageBean(player.getLetter().toString(), message.getGridId(), winner, tied).toJson();
opponent.getChannel().write(new DefaultWebSocketFrame(responseToOpponent));
// Respond to the player to let them know they won.
if (winner) {
player.getChannel().write(new DefaultWebSocketFrame(new GameOverMessageBean(YOU_WIN).toJson()));
} else if (tied) {
player.getChannel().write(new DefaultWebSocketFrame(new GameOverMessageBean(TIED).toJson()));
}
}
Remember earlier that handleWebSocketFrame is one of our own methods, not a callback method provided by Netty. This method is invoked when we inspect the incoming message and determine it’s a WebSocketFrame rather than an HttpRequest. The core of the logic above deals with updating the game board and marking the current player’s last move. We also need to check for victory or draw conditions and update the player and opponent with the latest game status.
That’s it! We’ve coded an entire tic-tac-toe server from scratch and it was very painless. Netty rocks. The only classes we didn’t review are the specific POJOs for tic-tac-toe related logic. You can check out the source below:
Creating the Tic-Tac-Toe Client using jQuery
Building our tic-tac-toe server was fairly simple. Building our tic-tac-toe client is even easier thanks to the power of jQuery. If you’d like to skip straight to the source code, it can be viewed below:
https://github.com/rocketpages/TicTacToe-Client
The bulk of the logic is contained in our JavaScript file. After the initial handshake is performed, all communication with the server will be done asynchronously via the WebSocket API. Please note that not all browsers support the WebSocket protocol. This is an up-to-date list11 of which layout engines support which HTML5 features.
Below is a copy of the JavaScript for the tic-tac-toe client.
// Constants - Status Updates
var STRATEGIZING_STATUS = "Your opponent is strategizing.";
var WAITING_STATUS = "Waiting for an opponent.";
var YOUR_TURN_STATUS = "It's your turn!";
var YOU_WIN_STATUS = "You win!";
var TIED_STATUS = "The game is tied.";
var WEBSOCKET_CLOSED_STATUS = "The WebSocket Connection Has Been Closed.";
// Constants - Game
var PLAYER_O = "O";
var PLAYER_X = "X";
// Constants - Incoming message types
var MESSAGE_HANDSHAKE = "handshake";
var MESSAGE_OPPONENT_UPDATE = "response";
var MESSAGE_TURN_INDICATOR = "turn";
var MESSAGE_GAME_OVER = "game_over";
// Constants - Message turn indicator types
var MESSAGE_TURN_INDICATOR_YOUR_TURN = "YOUR_TURN";
var MESSAGE_TURN_INDICATOR_WAITING = "WAITING";
// Constants - Game over message types
var MESSAGE_GAME_OVER_YOU_WIN = "YOU_WIN";
var MESSAGE_GAME_OVER_TIED = "TIED";
// Constants - WebSocket URL
var WEBSOCKET_URL = "ws://localhost:9000/websocket";
// Variables
var player;
var opponent;
var gameId;
var yourTurn = false;
// WebSocket connection
var ws;
$(document).ready(function() {
/* Bind to the click of all divs (tic tac toe cells) on the page
We would want to qualify this if we styled the game fancier! */
$("div").click(function () {
// Only process clicks if it's your turn.
if (yourTurn == true) {
// Stop processing clicks and invoke sendMessage().
yourTurn = false;
sendMessage(this.id);
// Add the X or O to the game board and update status.
$("#" + this.id).addClass(player);
$("#" + this.id).html(player);
$('#status').text(STRATEGIZING_STATUS);
}
});
// On the intial page load we perform the handshake with the server.
ws = new WebSocket(WEBSOCKET_URL);
ws.onopen = function(event) {
$('#status').text(WAITING_STATUS);
}
// Process turn message ("push") from the server.
ws.onmessage = function(event) {
var message = jQuery.parseJSON(event.data);
// Process the handshake response when the page is opened
if (message.type === MESSAGE_HANDSHAKE) {
gameId = message.gameId;
player = message.player;
if (player === PLAYER_X) {
opponent = PLAYER_O;
} else {
opponent = PLAYER_X;
}
}
// Process your opponent's turn data.
if (message.type === MESSAGE_OPPONENT_UPDATE) {
// Show their turn info on the game board.
$("#" + message.gridId).addClass(message.opponent);
$("#" + message.gridId).html(message.opponent);
// Switch to your turn.
if (message.winner == true) {
$('#status').text(message.opponent + " is the winner!");
} else if (message.tied == true) {
$('#status').text(TIED_STATUS);
} else {
yourTurn = true;
$('#status').text(YOUR_TURN_STATUS);
}
}
/* The initial turn indicator from the server. Determines who starts
the game first. Both players wait until the server gives the OK
to start a game. */
if (message.type === MESSAGE_TURN_INDICATOR) {
if (message.turn === MESSAGE_TURN_INDICATOR_YOUR_TURN) {
yourTurn = true;
$('#status').text(YOUR_TURN_STATUS);
} else if (message.turn === MESSAGE_TURN_INDICATOR_WAITING) {
$('#status').text(STRATEGIZING_STATUS);
}
}
/* The server has determined you are the winner and sent you this message. */
if (message.type === MESSAGE_GAME_OVER) {
if (message.result === MESSAGE_GAME_OVER_YOU_WIN) {
$('#status').text(YOU_WIN_STATUS);
}
else if (message.result === MESSAGE_GAME_OVER_TIED) {
$('#status').text(TIED_STATUS);
}
}
}
ws.onclose = function(event) {
$('#status').text(WEBSOCKET_CLOSED_STATUS);
}
});
// Send your turn information to the server.
function sendMessage(id) {
var message = {gameId: gameId, player: player, gridId:id};
var encoded = $.toJSON(message);
ws.send(encoded);
}
The bulk of the logic above deals with two distinct activities; maintaining state and processing messages. You’ll notice that when the page first loads we’ll use the documentReady function to initiate the WS handshake. You’ll also notice that we use jQuery to bind different WebSocket events to code blocks.
jQuery makes it fairly trivial to communicate with a WS server. Rather than polling the server, the WebSocket API allows us to bind logic to the onmessage
event and perform the appropriate logic. This is awesome! Our tic-tac-toe game is a simple webpage that never needs refreshing. We also don’t need to waist IO constantly polling the server. As soon as our opponent makes his or her move, our screen is immediately updated and we get to take our turn.
Conclusion
That’s a very high level look at the power of Java, Netty, the WebSocket API, and jQuery. We can make a number of improvements to this game, such as tracking and displaying ongoing stats (win, loss, draw) and even allowing the same player to compete in multiple games of tic-tac-toe at once!
Using these technologies we may actually be able to make tic-tac-toe a challenge.
-
http://engineering.twitter.com/2011/04/twitter-search-is-now-3x-faster_1656.html “read about it here” ↩︎
-
http://www.jboss.org/netty “Netty” ↩︎
-
http://en.wikipedia.org/wiki/New_I/O “Java NIO” ↩︎
-
http://en.wikipedia.org/wiki/Push_technology “push information to a client” ↩︎
-
http://en.wikipedia.org/wiki/Comet_(programming) “long-polling” ↩︎
-
https://github.com/rocketpages/Netty-TicTacToe-Server/blob/master/src/main/java/com/tictactoe/server/TicTacToeServerHandler.java “TicTacToeServerHandler” ↩︎
-
https://tools.ietf.org/html/rfc6455 “The latest Websocket draft specification here” ↩︎
-
https://github.com/rocketpages/Netty-TicTacToe-Server/blob/master/src/main/java/com/tictactoe/game/Board.java “Board.java” ↩︎
-
https://github.com/rocketpages/Netty-TicTacToe-Server/blob/master/src/main/java/com/tictactoe/game/Game.java “Game.java” ↩︎
-
https://github.com/rocketpages/Netty-TicTacToe-Server/blob/master/src/main/java/com/tictactoe/game/Player.java “Player.java” ↩︎
-
http://en.wikipedia.org/wiki/Comparison_of_layout_engines_%28HTML_5%29#Related_specifications “up-to-date list” ↩︎