-
Notifications
You must be signed in to change notification settings - Fork 0
Server Side Processing
The more solid the connection get, the more complex the protocol get. Because one of the aims of the jQuery Socket is easy to establish a two-way connection, there is and will be no specified protocol. In other words, the server implementation totally hinges on you, and the jQuery Socket provides various options to meet your own protocol.
All you need is a event-driven server.
The HTTP method GET is used to establish a connection.
The socket URI looks like the following:
chat?id=ed84f5a4-0772-43c6-ba95-e293c287be7c&transport=sse&heartbeat=false&lastEventId=25&_=1333818006226
The following request parameters are included in the above URI unless the url handler in the client side is substituted.
-
id: An identifier of the socket. By using this, you can create a map of identifiers and connections for future use. In spite of reconnection, the id is not changed. -
heartbeat: A heartbeat interval. The value which is in a number format means that the client manages the connection using a heartbeat timer, and the value offalseindicates no heartbeat. -
transport: A required transport for the connection. It can have the following values unless you add or remove transport:ws,sse,streamxdr,streamiframe,streamxhr,longpollajax,longpollxdrandlongpolljsonp. -
lastEventId: An id of the client received the last event. If it does not exist, the value will be empty string instead. To make up for events which could not be sent mostly during reconnection, the server can determine which events should be sent again by comparing an event id to this value. -
_: A time stamp value to prevent the client from caching a GET request.
Generally, GET request handler involves the following:
- Making a request asynchronous.
- Meeting a prerequisite of a required transport.
- Adding a established connection to open connections.
- Adding a listener which removes the connection from open connections when the request completes.
The time to add the connection object is the start of the connection life cycle, and the time to remove the connection object is the end of the connection life cycle. You can fire the open and close event at those times like the socket object in the client side.
@WebServlet(urlPatterns = "/chat", asyncSupported = true)
public class ChatServlet extends HttpServlet {
// Open connections
private Map<String, AsyncContext> connections = new ConcurrentHashMap<String, AsyncContext>();
// Handles a GET request
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Finds the 'id' and 'transport'
final String id = request.getParameter("id");
String transport = request.getParameter("transport");
// Makes a request asynchronous
AsyncContext asyncContext = request.startAsync();
// Meets a prerequisite of a required transport
// ...
// Adds a listener which removes the closed connection
asyncContext.addListener(new AsyncListener() {
public void onStartAsync(AsyncEvent event) throws IOException {}
public void onComplete(AsyncEvent event) throws IOException {
cleanup(event);
}
public void onTimeout(AsyncEvent event) throws IOException {
cleanup(event);
}
public void onError(AsyncEvent event) throws IOException {
cleanup(event);
}
private void cleanup(AsyncEvent event) {
connections.remove(id);
}
});
// Adds a open connection to the connection map
connections.put(id, asyncContext);
}
// ...
}You don't need to implement all transports. Consider your client and server application's runtime environment, choose proper transports and implement them. For details about transports, see Supported Transports and Browsers.
Handles ws.
You have to start a WebSocket handshake according to the WebSocket specification. In most cases, however, the server or framework will provide a way to deal with a WebSocket request. Except that, there is nothing you have to do.
Handles sse, streamxdr, streamiframe and streamxhr.
For the server, the Server-Sent Events is a kind of the streaming.
- The response should be encoded in
utf-8format. -sse. -
Access-Control-Allow-Originheader should be either*or the value of theOriginrequest header. -streamxdr. - The content-type header should be set to
text/event-streamif the transport issseand should be set totext/plainif the transport isstreamiframeto prevent iframe tag from parsing response as HTML. - The padding is required, which makes the transport object in the client side aware of change of the response. The padding should be greater than one kilobyte (4K for Android browser 2 and 3), be composed of white space characters and end with
\r,\nor\r\n. -streamxdr,streamiframe,streamxhrin WebKit and Android browser 2 and 3 andssein Webkit and Firefox. The socket object fires theopenevent when noticing padding.
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// ...
response.setCharacterEncoding("utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setContentType("text/" + ("sse".equals(transport) ? "event-stream" : "plain"));
PrintWriter writer = response.getWriter();
for (int i = 0; i < 4096; i++) {
writer.print(' ');
}
writer.print("\n");
writer.flush();
// ...
}Handles longpollajax, longpollxdr and longpolljsonp.
In fact, the long polling implementation is somewhat tiresome, because the request's life cycle and the connection's life cycle do not correspond unlike other transports, and if the server is going to send data continuously, an additional measure is needed for the client so as not to lose data.
-
Access-Control-Allow-Originheader should be either*or the value of theOriginrequest header. -longpollxdr. - The content-type header should be set to
text/javascriptif the transport islongpolljsonp, and for the others,text/plainis fine. - The start of a connection is the normal completion of the first long polling request. The server should complete immediately the request whose the request parameter
countis1. The purpose of this is to tell the client that the server is alive. The socket object fires theopenevent when the first request completes normally. - The end of a connection is determined by whether a response text is empty. When a request is complete, a not empty one means the server sent data, and an empty one means the server did nothing, that is to say the end of a connection. For this reason, the socket object fires the
closeevent if the response is empty and reconnect if the response is not empty.
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// ...
final boolean first = "1".equals(request.getParameter("count"));
response.setCharacterEncoding("utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setContentType("text/" + ("longpolljsonp".equals(transport) ? "javascript" : "plain"));
asyncContext.addListener(new AsyncListener() {
// ...
private void cleanup(AsyncEvent event) {
if (!first && !event.getAsyncContext().getResponse().isCommitted()) {
connections.remove(id);
}
}
});
if (first) {
asyncContext.complete();
}
// ...
}Once the server succeeds in establishing a connection, the open event is fired in the client side.
$.socket("url", {transports: ["transport"]}).open(function() {
console.log("connected");
});The final string to be sent to the client is a JSON string representing an event object, and the inbound handler in the client side makes such a raw string into an event object. The event object should contain at least the type property. This event concept has nothing to do with WebSocket's event or Server-Sent Events' event.
{"type":"message","data":"data","id":1,"reply":false}
-
type: An event type. -
id: An event id. -
data: Data. -
reply: Whether to request a reply.
Sending data to multiple clients is a typical producer-consumer scenario, and the producer-consumer queue which is a thread safe queue with first-in-first-out semantics is fairly suitable to do that. The producer is a thread which puts data to be processed into the queue and the consumer is a thread which take those data from the queue and handles them. In this situation, the server serves as the producer and the consumer is needed.
@WebServlet(urlPatterns = "/chat", asyncSupported = true)
public class ChatServlet extends HttpServlet {
// The producer-consumer queue
private BlockingQueue<Event> queue = new LinkedBlockingQueue<Event>();
// The consumer
private Thread broadcaster = new Thread(new Runnable() {
public void run() {
while (true) {
try {
// Waits until an event becomes available
Event event = queue.take();
for (Entry<String, AsyncContext> entry : connections.entrySet()) {
try {
send(entry.getValue(), event);
} catch (IOException ex) {
connections.remove(entry.getKey());
}
}
} catch (InterruptedException e) {
break;
}
}
}
});
// Called when the servlet is being placed into service
public void init() throws ServletException {
broadcaster.setDaemon(true);
broadcaster.start();
}
// Makes the final data and prints it according to each connection's transport
private void send(AsyncContext asyncContext, Event event) throws IOException {
String data = new Gson().toJson(event);
PrintWriter writer = asyncContext.getResponse().getWriter();
// ....
}
// Receives client sent event
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// ...
if (event.type.equals("message")) {
// Inserts an event to the queue
queue.offer(new Event("message").setData(event.getData()));
}
// ...
}
// ...
}A single WebSocket message corresponds to a single data naturally. No need to process, just send it.
The response text should be formatted in the event stream format. This is a requirement of the sse transport, but the rest of the streaming transports also accept that format for convenience. Break data up by \r, \n, or \r\n, append data:`` to the beginning of each line and \n to the end of each line and print them. Finally, print \n to mark the end of a single data. In case of sse, only data event is supported. Android browser 2 and 3 need 4K padding at the top of each event.
private void send(AsyncContext asyncContext, Event event) throws IOException {
// ...
String userAgent = ((HttpServletRequest) asyncContext.getRequest()).getHeader("user-agent");
if (userAgent != null && (userAgent.indexOf("Android 2.") != -1 || userAgent.indexOf("Android 3.") != -1)) {
for (int i = 0; i < 4096; i++) {
writer.print(" ");
}
}
for (String datum : data.split("\r\n|\r|\n")) {
writer.print("data: ");
writer.print(datum);
writer.print("\n");
}
writer.print("\n");
writer.flush();
}For the longpollajax and longpollxdr transport, the response text corresponds to a single data. In case of the longpolljsonp transport, the response text is a JavaScript code snippet executing a corresponding callback in the client side with data. The callback function name is passed as the request parameter callback and the data should be escaped to a JavaScript string literal. All the long polling transports has to finish the request after processing.
private void send(AsyncContext asyncContext, Event event) throws IOException {
// ...
if (asyncContext.getRequest().isAsyncStarted()) {
if ("longpolljsonp".equals(asyncContext.getRequest().getParameter("transport"))) {
writer.print(asyncContext.getRequest().getParameter("callback"));
writer.print("(");
writer.print(new Gson().toJson(data));
writer.print(")");
} else {
writer.print(data);
}
writer.flush();
asyncContext.complete();
}
}There are cases when the server tries to send data but it's not possible for the reason of reconnection. The server had better prepare buffer or cache, just in case. Store data when that happens and send stored data when the next request comes in. This is not required but recommended to prevent the client from loosing data.
If the server sends a message event to say hi.
$.socket("url", {transports: ["transport"]}).message(function(data) {
console.log(data);
});If the server is going to only push, then skip this section.
The HTTP method POST is used to receive event. Maybe the client want to send RESTful request instead of POST request, but the RESTful concept is far away from a concept to send and receive data in real time. In other words, it's not easy to match the jQuery Socket with RESTful application.
The outbound handler in the client side makes an event object into a JSON string representing it, and such a plain string is sent to the server. This event concept has nothing to do with WebSocket's event or Server-Sent Events' event.
{"id":"1","socket":"341043d0-5564-4cdd-bb25-64dbe40b7a71","type":"message","data":"data","reply":false}
-
id: An event id. -
socket: A socket id. -
type: An event type. -
data: Data. -
reply: Whether to request a reply.
The POST request message body is like the following.
data={"id":"1","socket":"0c9d84f2-b8e3-4d65-bf41-7bb32f597b74","type":"message","data":"data","reply":false}
- The character encoding should be set to
UTF-8explicitly. Although the socket object encodes data inUTF-8, some browsers behave strangely. - The server has to access and parse the request's message body directly, because to generalize how to extract data, the socket sets content type to
text/plainnotapplication/x-www-form-urlencoded. Nevertheless, XDomainRequest doesn't set content type header explicitly in a cross-origin connection. The problem results from the fourth restriction of the XDomainRequest. - The data which the client sends is a substring of the body which begins after the
data=and extends to the end of the body. For your information, once in a great while, some browsers send a request without body for some reason.
@WebServlet(urlPatterns = "/chat", asyncSupported = true)
public class ChatServlet extends HttpServlet {
// Handles a POST request
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
// request.getParameter("data") returns null
String data = request.getReader().readLine();
if (data != null) {
data = data.substring("data=".length());
Event event = new Gson().fromJson(data, Event.class);
// Broadcasts
if (event.type.equals("message")) {
queue.offer(new Event("message").setData(event.getData()));
}
}
}
// ...
}With the echo server, the sent event should be echoed back.
$.socket("url", {transports: ["transport"]}).send("data").message(function(data) {
console.log(data);
});