The application behaves similarly to the examples from The Java Tutorial (see http://java.sun.com/docs/books/tutorial/networking/sockets/clientServer.html). A server-side
ServerSocket listens for incoming connections. A client connects using a Socket and uses the readLine method of a BufferedReader to read data from the socket. It uses the println method of a PrintWriter to push data back to the server.Converting these kind of applications can be tricky: the server here is stateful, while App Engine is not. Also, sockets assume that a client-server connection is always online. Clients can do blocking reads, which will simply set them into a "waiting" state until new data from the server arrived. These kind of things can be a bit tough to reproduce in a webservice-based environment that expects a client/server interaction to be done within a couple of seconds or less. How can we do it? Let's take a look.
(PS: Since the source code of the chat application does not really tell me what license it is under, I am not going to reprint it here in many details. Instead, I am going to show how to port the existing client, and then simply reimplement the server side from scratch.)
The client
As mentioned before, the two methods mostly used in this example (and many others, for what it's worth), are
println and readLine. As long as we successfully substitute these two methods, we should be fine.In order to not having to reinvent the wheel, I built a little tool class called ClientSocketSubstitute. This class provides the very same methods mentioned before. It caches outgoing communication in a queue and sends it out in batches (as JSON arrays) to the server. Likewise, it receives batches from the server in those same polling requests. The first thing I do is to replace our incoming and outgoing streams with that substitute:
// BufferedReader in;
// PrintWriter out;
ClientSocketSubstitute in;
ClientSocketSubstitute out;
Since my helper class provides
println and readLine implementations, my code mostly compiles. The only area that shows problems is where the Socket would get initialized. I therefore replace the initialization code accordingly: // Make connection and initialize streams
// String serverAddress = getServerAddress();
// Socket socket = new Socket(serverAddress, 9001);
// in = new BufferedReader(new InputStreamReader(
// socket.getInputStream()));
// out = new PrintWriter(socket.getOutputStream(), true);
in = new ClientSocketSubstitute(
new URL(getServerAddress()), // Connection URL
1000, // ping frequency
5 // max. messages per ping
);
out = in;
That's it -- let's continue with the server.
The server
The client-server protocol of this example is fortunately quite simple:
- Once the socket communication is established, the server sends the text
SUBMITNAMEto the client. - The client responds with a line of text that contains the nickname of the participant in the chat. The server responds with
NAMEACCEPTED. - Any text sent from the client afterwards is a chat message. Anything sent from the server afterwards is a chat message and starts with
MESSAGE.
Like with the client, I built a server-side socket replacement that makes it easy to implement such a protocol. Outgoing connections are replaced with a ServerEndpoint class that has a
send message. Outgoing messages are buffered in the data store and sent out the next time the polling client connects. In addition, an endpoint has a bag of properties that the server can use to remember certain connection-specific attributes. Incoming messages arrive through a Listener interface that our web service can implement. The overall communication is controlled by a tool class called WebConnectionServer.Let's look at the actual implementation. We base our implementation on the servlet API and initialize an internal
WebConnectionServer instance:package com.appenginefan.sample.chat;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.*;
import com.appenginefan.toolkit.common.ServerEndpoint;
import com.appenginefan.toolkit.common.WebConnectionServer;
import com.appenginefan.toolkit.persistence.DatastorePersistence;
import com.appenginefan.toolkit.persistence.Persistence;
@SuppressWarnings("serial")
public class ChatserverServlet
extends HttpServlet
implements WebConnectionServer.Receiver {
private Persistence<byte[]> data;
private WebConnectionServer server;
/**
* Set up the local persistence and the server
* implementation.
*/
@Override
public void init() throws ServletException {
super.init();
// Persist communication data in the store
// under a particular socket name
data = new DatastorePersistence("Socket");
// Build a connection server around that server
server = WebConnectionServer.fromPeristence(data);
}
Note that the underlying storage uses the
Persistence interface that I introduced a little while ago. The concrete implementation currently uses the data store, but there is nothing preventing me from switching to something else (like memcache or a combination of those two) if I needed better performance at some point. The WebConnectionServer uses a protocol buffer based schema internally to hold on to the buffered messages.As far as the servlet itself is concerned; its function is mostly to refer all the parsing logic to the
WebConnectionServer: /**
* Plugs an incoming request into the server.
*/
public void doPost(HttpServletRequest req,
HttpServletResponse resp) throws IOException {
if (!server.dispatch(this, req, resp)) {
resp.sendError(404);
}
}
The more interesting part is our listener implementation. Let's start with a new connection happening. We have not received any data yet; the server is expected to send a
SUBMITNAME. Notice how the listener uses a property in the faux socket to determine whether or not to send out that information: @Override
public void onEmptyPayload(WebConnectionServer server,
ServerEndpoint socket, HttpServletRequest req) {
// This is the first communication, so we ask for a name once
if (socket.getProperty("namerequested", null) == null) {
socket.setProperty("namerequested", "true");
socket.send("SUBMITNAME");
}
}
Now that the
SUBMITNAME is out, we can sit back and wait for the messages to come in. The first message is going to be the user's name (which we store in another property). Anything else is incoming data that should be broadcasted to every participant we know. @Override
public void receive(
WebConnectionServer theServer,
ServerEndpoint socket,
String theMessage,
HttpServletRequest request) {
final String name = socket.getProperty("name", null);
// Are we looking for a name?
if (name == null) {
socket.setProperty("name", theMessage.trim());
socket.send("NAMEACCEPTED");
}
// Otherwise, this must be an incoming message.
// Forward it to everybody
else {
for (String handle :
data.keyScan("", "" + Character.MAX_VALUE, 100)) {
theServer.fromHandle(request, handle).send(
"MESSAGE " + name + ": " + theMessage);
}
}
}
For the broadcast, we use the
keyScan method of out Persistence interface to get to a list of all sockets known to our server. It should be noted that a broadcast like this is highly inefficient in production code and should be avoided. These kind of socket replacements are more intended for a small amount of recipients (in case of the JOgre game server, it is assumed that board games will usually have less than a dozen players per game).(PS: You can find the tool classes used in this post under Apache license at this open source project)





