├── .gitignore
├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── ibdknox
│ └── socket_io_netty
│ ├── GenericIOClient.java
│ ├── HeartbeatTask.java
│ ├── INSIOClient.java
│ ├── INSIOHandler.java
│ ├── NSIOServer.java
│ ├── PollingIOClient.java
│ ├── ShutdownHook.java
│ ├── SocketIOUtils.java
│ ├── WebSocketIOClient.java
│ ├── WebSocketServerHandler.java
│ ├── WebSocketServerPipelineFactory.java
│ └── flashpolicy
│ ├── FlashPolicyServer.java
│ ├── FlashPolicyServerDecoder.java
│ ├── FlashPolicyServerHandler.java
│ └── FlashPolicyServerPipelineFactory.java
└── test
└── java
└── com
└── ibdknox
└── socket_io_netty
└── AppTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | target/*
2 | *.class
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project is currently low on my priorities list and likely won't see a lot of attention in the near future.
2 | It works with socket.io 0.6, but since socket.io 0.7 was essentially a rewrite, it will require a fair amount of work
3 | to continue moving it forward.
4 |
5 | #Socket.io-Netty
6 |
7 | This is an implementation of the socket.io server built on top of Netty. Currently, it only
8 | supports 3 of the Socket.io protocols: websocket, flashsocket, and xhr-polling.
9 |
10 | For an example of production use, check out http://www.typewire.io
11 |
12 | #Roadmap
13 |
14 | I'm currently waiting for the socket.io 0.7 release to come out before making any big changes.
15 | As the 0.7 release is looking like a big rewrite, I will take that opportunity to add in the other
16 | supported transports as well.
17 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 |
5 | com.ibdknox.socket_io_netty
6 | socket-io-netty
7 | 0.3.2
8 | jar
9 |
10 | socket.io-netty
11 | http://maven.apache.org
12 |
13 |
14 | UTF-8
15 |
16 |
17 |
18 |
19 | repository.jboss.org
20 | https://repository.jboss.org/nexus/content/repositories/releases/
21 |
22 | false
23 |
24 |
25 |
26 |
27 |
28 |
29 | junit
30 | junit
31 | 3.8.1
32 | test
33 |
34 |
35 | org.jboss.netty
36 | netty
37 | 3.2.1.Final
38 | compile
39 |
40 |
41 |
42 |
43 |
44 |
45 | org.apache.maven.plugins
46 | maven-shade-plugin
47 |
48 |
49 | package
50 |
51 | shade
52 |
53 |
54 |
55 |
56 | uber-${artifactId}-${version}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/GenericIOClient.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import org.jboss.netty.channel.Channel;
4 | import org.jboss.netty.channel.ChannelHandlerContext;
5 |
6 | public abstract class GenericIOClient implements INSIOClient {
7 |
8 | protected ChannelHandlerContext ctx;
9 | protected int beat;
10 | protected String uID;
11 | protected boolean open = false;
12 |
13 | public GenericIOClient(ChannelHandlerContext ctx, String uID) {
14 | this.ctx = ctx;
15 | this.uID = uID;
16 | this.open = true;
17 | }
18 |
19 | public void send(String message) {
20 | sendUnencoded(SocketIOUtils.encode(message));
21 | }
22 |
23 | public void heartbeat() {
24 | if(this.beat > 0) {
25 | this.beat++;
26 | }
27 | }
28 |
29 | public boolean heartbeat(int beat) {
30 | if(!this.open) return false;
31 |
32 | int lastBeat = beat - 1;
33 | if(this.beat == 0 || this.beat > beat) {
34 | this.beat = beat;
35 | } else if(this.beat < lastBeat) {
36 | //we're 2 beats behind..
37 | return false;
38 | }
39 | return true;
40 | }
41 |
42 | public ChannelHandlerContext getCTX() {
43 | return this.ctx;
44 | }
45 |
46 | public String getSessionID() {
47 | return this.uID;
48 | }
49 |
50 | public void disconnect() {
51 | Channel chan = ctx.getChannel();
52 | if(chan.isOpen()) {
53 | chan.close();
54 | }
55 | this.open = false;
56 | }
57 |
58 | public abstract void sendUnencoded(String message);
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/HeartbeatTask.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import java.util.TimerTask;
4 |
5 | public class HeartbeatTask extends TimerTask {
6 |
7 | private WebSocketServerHandler server;
8 | private int heartbeatNum = 0;
9 |
10 | public HeartbeatTask(WebSocketServerHandler server) {
11 | this.server = server;
12 | }
13 |
14 | @Override
15 | public void run() {
16 | if(server.clients.isEmpty() && server.pollingClients.isEmpty()) return;
17 |
18 | heartbeatNum++;
19 | String message = SocketIOUtils.encode("~h~" + heartbeatNum);
20 | for(INSIOClient client : server.clients.values()) {
21 | if(client.heartbeat(heartbeatNum)) {
22 | client.sendUnencoded(message);
23 | } else {
24 | server.disconnect(client);
25 | }
26 | }
27 |
28 | for(PollingIOClient client : server.pollingClients.values()) {
29 | if(client.heartbeat(heartbeatNum)) {
30 | client.sendPulse();
31 | } else {
32 | server.disconnect(client);
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/INSIOClient.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import org.jboss.netty.channel.ChannelHandlerContext;
4 |
5 | public interface INSIOClient {
6 |
7 | void send(String message);
8 | void sendUnencoded(String message);
9 | boolean heartbeat(int beat);
10 | void heartbeat();
11 | void disconnect();
12 | String getSessionID();
13 | ChannelHandlerContext getCTX();
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/INSIOHandler.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import org.jboss.netty.channel.ChannelHandlerContext;
4 | import org.jboss.netty.handler.codec.http.websocket.WebSocketFrame;
5 |
6 |
7 | public interface INSIOHandler {
8 | void OnConnect(INSIOClient ws);
9 | void OnMessage(INSIOClient ws, String message);
10 | void OnDisconnect(INSIOClient ws);
11 | void OnShutdown();
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/NSIOServer.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import java.net.InetSocketAddress;
4 | import java.util.concurrent.Executors;
5 | import com.ibdknox.socket_io_netty.flashpolicy.FlashPolicyServer;
6 |
7 | import org.jboss.netty.channel.Channel;
8 | import org.jboss.netty.bootstrap.ServerBootstrap;
9 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
10 |
11 | public class NSIOServer {
12 |
13 | private ServerBootstrap bootstrap;
14 | private Channel serverChannel;
15 | private int port;
16 | private boolean running;
17 | private INSIOHandler handler;
18 | private WebSocketServerHandler socketHandler;
19 |
20 | public NSIOServer(INSIOHandler handler, int port) {
21 | this.port = port;
22 | this.handler = handler;
23 | this.running = false;
24 | Runtime.getRuntime().addShutdownHook(new ShutdownHook(this));
25 | }
26 |
27 | public boolean isRunning() {
28 | return this.running;
29 | }
30 |
31 | public void start() {
32 | bootstrap = new ServerBootstrap(
33 | new NioServerSocketChannelFactory(
34 | Executors.newCachedThreadPool(),
35 | Executors.newCachedThreadPool()));
36 |
37 | // Set up the event pipeline factory.
38 | socketHandler = new WebSocketServerHandler(handler);
39 | bootstrap.setPipelineFactory(new WebSocketServerPipelineFactory(socketHandler));
40 | // Bind and start to accept incoming connections.
41 | this.serverChannel = bootstrap.bind(new InetSocketAddress(port));
42 | this.running = true;
43 | try {
44 | FlashPolicyServer.start();
45 | } catch (Exception e) { //TODO: this should not be exception
46 | System.out.println("You must run as sudo for flash policy server. X-Domain flash will not currently work.");
47 | }
48 | System.out.println("Server Started at port ["+ port + "]");
49 | }
50 |
51 | public void stop() {
52 | if(!this.running) return;
53 |
54 | System.out.println("Server shutting down.");
55 | this.socketHandler.prepShutDown();
56 | this.handler.OnShutdown();
57 | this.serverChannel.close();
58 | this.bootstrap.releaseExternalResources();
59 | System.out.println("**SHUTDOWN**");
60 | this.serverChannel = null;
61 | this.bootstrap = null;
62 | this.running = false;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/PollingIOClient.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import static org.jboss.netty.handler.codec.http.HttpHeaders.isKeepAlive;
4 | import static org.jboss.netty.handler.codec.http.HttpHeaders.setContentLength;
5 | import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
6 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.ACCEPTED;
7 | import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
8 |
9 | import java.nio.channels.ClosedChannelException;
10 | import java.util.LinkedList;
11 | import java.util.List;
12 |
13 | import org.jboss.netty.buffer.ChannelBuffers;
14 | import org.jboss.netty.channel.Channel;
15 | import org.jboss.netty.channel.ChannelFuture;
16 | import org.jboss.netty.channel.ChannelFutureListener;
17 | import org.jboss.netty.channel.ChannelHandlerContext;
18 | import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
19 | import org.jboss.netty.handler.codec.http.HttpRequest;
20 | import org.jboss.netty.handler.codec.http.HttpResponse;
21 | import org.jboss.netty.handler.codec.http.HttpResponseStatus;
22 | import org.jboss.netty.util.CharsetUtil;
23 |
24 |
25 | public class PollingIOClient extends GenericIOClient {
26 |
27 | private List queue;
28 | private HttpRequest req;
29 | private boolean connected;
30 |
31 | public PollingIOClient(ChannelHandlerContext ctx, String uID) {
32 | super(ctx, uID);
33 | queue = new LinkedList();
34 | }
35 |
36 | public void Reconnect(ChannelHandlerContext ctx, HttpRequest req) {
37 | this.ctx = ctx;
38 | this.req = req;
39 | this.connected = true;
40 | _payload();
41 | }
42 |
43 | private void _payload() {
44 | if(!connected || queue.isEmpty()) return;
45 | //TODO: is this necessary to synchronize?
46 | synchronized(queue) {
47 | StringBuilder sb = new StringBuilder();
48 | for(String message : queue) {
49 | sb.append(message);
50 | }
51 | _write(sb.toString());
52 | queue.clear();
53 | }
54 | }
55 |
56 | private void _write(String message) {
57 | if(!this.open) return;
58 |
59 | HttpResponse res = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.OK);
60 |
61 | res.addHeader(CONTENT_TYPE, "text/plain; charset=UTF-8");
62 | res.addHeader("Access-Control-Allow-Origin", "*");
63 | res.addHeader("Access-Control-Allow-Credentials", "true");
64 | res.addHeader("Connection", "keep-alive");
65 |
66 | res.setContent(ChannelBuffers.copiedBuffer(message, CharsetUtil.UTF_8));
67 | setContentLength(res, res.getContent().readableBytes());
68 |
69 | // Send the response and close the connection if necessary.
70 | Channel chan = ctx.getChannel();
71 | if(chan.isOpen()) {
72 | ChannelFuture f = chan.write(res);
73 | if (!isKeepAlive(req) || res.getStatus().getCode() != 200) {
74 | f.addListener(ChannelFutureListener.CLOSE);
75 | }
76 | }
77 |
78 | this.connected = false;
79 | }
80 |
81 | @Override
82 | public void sendUnencoded(String message) {
83 | this.queue.add(message);
84 | _payload();
85 | }
86 |
87 | public void sendPulse() {
88 | if(connected) {
89 | _write("");
90 | }
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/ShutdownHook.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | public class ShutdownHook extends java.lang.Thread {
4 |
5 | private NSIOServer server;
6 |
7 | public ShutdownHook(NSIOServer server) {
8 | this.server = server;
9 | }
10 |
11 | @Override
12 | public void run() {
13 | server.stop();
14 | }
15 |
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/SocketIOUtils.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import java.util.regex.Matcher;
4 | import java.util.regex.Pattern;
5 |
6 |
7 | public class SocketIOUtils {
8 |
9 | /**************************************************************************
10 | * encode/decode
11 | *************************************************************************/
12 |
13 | private static final Pattern DECODE_PATTERN = Pattern.compile(
14 | "~m~[0-9]+~m~(.*)",
15 | Pattern.MULTILINE | Pattern.DOTALL
16 | );
17 |
18 | public static String encode(String msg) {
19 | int len = msg.length();
20 | return "~m~" + len + "~m~" + msg;
21 | }
22 |
23 | public static String decode(String msg) {
24 | Matcher regex = DECODE_PATTERN.matcher(msg);
25 | if (regex.matches())
26 | return regex.group(1);
27 |
28 | return msg;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/WebSocketIOClient.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import org.jboss.netty.channel.Channel;
4 | import org.jboss.netty.channel.ChannelHandlerContext;
5 | import org.jboss.netty.handler.codec.http.websocket.DefaultWebSocketFrame;
6 |
7 |
8 | public class WebSocketIOClient extends GenericIOClient {
9 |
10 | public WebSocketIOClient(ChannelHandlerContext ctx, String uID) {
11 | super(ctx, uID);
12 | }
13 |
14 | @Override
15 | public void sendUnencoded(String message) {
16 | if(!this.open) return;
17 |
18 | Channel chan = ctx.getChannel();
19 | if(chan.isOpen()) {
20 | chan.write(new DefaultWebSocketFrame(message));
21 | } else {
22 | this.disconnect();
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/WebSocketServerHandler.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import static org.jboss.netty.handler.codec.http.HttpHeaders.*;
4 | import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;
5 | import static org.jboss.netty.handler.codec.http.HttpHeaders.Values.*;
6 | import static org.jboss.netty.handler.codec.http.HttpMethod.*;
7 | import static org.jboss.netty.handler.codec.http.HttpResponseStatus.*;
8 | import static org.jboss.netty.handler.codec.http.HttpVersion.*;
9 |
10 | import java.security.MessageDigest;
11 | import java.util.Date;
12 | import java.util.HashMap;
13 | import java.util.LinkedList;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.Timer;
17 | import java.util.UUID;
18 | import java.util.concurrent.ConcurrentHashMap;
19 |
20 | import org.jboss.netty.buffer.ChannelBuffer;
21 | import org.jboss.netty.buffer.ChannelBuffers;
22 | import org.jboss.netty.channel.ChannelFuture;
23 | import org.jboss.netty.channel.ChannelFutureListener;
24 | import org.jboss.netty.channel.ChannelHandlerContext;
25 | import org.jboss.netty.channel.ChannelPipeline;
26 | import org.jboss.netty.channel.ExceptionEvent;
27 | import org.jboss.netty.channel.MessageEvent;
28 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
29 | import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
30 | import org.jboss.netty.handler.codec.http.HttpHeaders;
31 | import org.jboss.netty.handler.codec.http.HttpRequest;
32 | import org.jboss.netty.handler.codec.http.HttpResponse;
33 | import org.jboss.netty.handler.codec.http.HttpResponseStatus;
34 | import org.jboss.netty.handler.codec.http.HttpHeaders.Names;
35 | import org.jboss.netty.handler.codec.http.HttpHeaders.Values;
36 | import org.jboss.netty.handler.codec.http.websocket.DefaultWebSocketFrame;
37 | import org.jboss.netty.handler.codec.http.websocket.WebSocketFrame;
38 | import org.jboss.netty.handler.codec.http.websocket.WebSocketFrameDecoder;
39 | import org.jboss.netty.handler.codec.http.websocket.WebSocketFrameEncoder;
40 | import org.jboss.netty.util.CharsetUtil;
41 | import org.jboss.netty.handler.codec.http.QueryStringDecoder;
42 |
43 | public class WebSocketServerHandler extends SimpleChannelUpstreamHandler {
44 |
45 | private static final long HEARTBEAT_RATE = 10000;
46 | private static final String WEBSOCKET_PATH = "/socket.io/websocket";
47 | private static final String POLLING_PATH = "/socket.io/xhr-polling";
48 | private static final String FLASHSOCKET_PATH = "/socket.io/flashsocket";
49 |
50 |
51 | private INSIOHandler handler;
52 | public ConcurrentHashMap clients;
53 | private Timer heartbeatTimer;
54 | ConcurrentHashMap pollingClients;
55 |
56 | public WebSocketServerHandler(INSIOHandler handler) {
57 | super();
58 | this.clients = new ConcurrentHashMap(20000, 0.75f, 2);
59 | this.pollingClients = new ConcurrentHashMap(20000, 0.75f, 2);
60 | this.handler = handler;
61 | this.heartbeatTimer = new Timer();
62 | heartbeatTimer.schedule(new HeartbeatTask(this), 1000, HEARTBEAT_RATE);
63 | }
64 |
65 |
66 | private String getUniqueID() {
67 | return UUID.randomUUID().toString();
68 | }
69 |
70 | private INSIOClient getClientByCTX(ChannelHandlerContext ctx) {
71 | return clients.get(ctx);
72 | }
73 |
74 | @Override
75 | public void channelDisconnected(ChannelHandlerContext ctx, org.jboss.netty.channel.ChannelStateEvent e) throws Exception {
76 | INSIOClient client = getClientByCTX(ctx);
77 | if(client != null) {
78 | this.disconnect(client);
79 | }
80 | };
81 |
82 | public void disconnect(INSIOClient client) {
83 | client.disconnect();
84 | if(this.clients.remove(client.getCTX()) == null) {
85 | this.pollingClients.remove(client.getSessionID());
86 | }
87 | handler.OnDisconnect(client);
88 | }
89 |
90 | @Override
91 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
92 | Object msg = e.getMessage();
93 | if (msg instanceof HttpRequest) {
94 | handleHttpRequest(ctx, (HttpRequest) msg);
95 | } else if (msg instanceof WebSocketFrame) {
96 | handleWebSocketFrame(ctx, (WebSocketFrame) msg);
97 | }
98 | }
99 |
100 | private void handleHttpRequest(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
101 |
102 | String reqURI = req.getUri();
103 | if(reqURI.contains(POLLING_PATH)) {
104 | String[] parts = reqURI.split("/");
105 | String ID = parts.length > 3 ? parts[3] : "";
106 | PollingIOClient client = (PollingIOClient) this.pollingClients.get(ID);
107 |
108 | if(client == null) {
109 | //new client
110 | client = connectPoller(ctx);
111 | client.Reconnect(ctx, req);
112 | return;
113 | }
114 |
115 | if(req.getMethod() == GET) {
116 | client.heartbeat();
117 | client.Reconnect(ctx, req);
118 | } else {
119 | //we got a message
120 | QueryStringDecoder decoder = new QueryStringDecoder("/?" + req.getContent().toString(CharsetUtil.UTF_8));
121 | String message = decoder.getParameters().get("data").get(0);
122 | handleMessage(client, message);
123 |
124 | //make sure the connection is closed once we send a response
125 | setKeepAlive(req, false);
126 |
127 | //send a response that allows for cross domain access
128 | HttpResponse resp = new DefaultHttpResponse(HTTP_1_1, OK);
129 | resp.addHeader("Access-Control-Allow-Origin", "*");
130 | sendHttpResponse(ctx, req, resp);
131 | }
132 | return;
133 | }
134 |
135 | // Serve the WebSocket handshake request.
136 | String location = "";
137 | if(reqURI.equals(WEBSOCKET_PATH)) {
138 | location = getWebSocketLocation(req);
139 | } else if(reqURI.equals(FLASHSOCKET_PATH)) {
140 | location = getFlashSocketLocation(req);
141 | }
142 | if (location != "" &&
143 | Values.UPGRADE.equalsIgnoreCase(req.getHeader(CONNECTION)) &&
144 | WEBSOCKET.equalsIgnoreCase(req.getHeader(Names.UPGRADE))) {
145 |
146 | // Create the WebSocket handshake response.
147 | HttpResponse res = new DefaultHttpResponse(
148 | HTTP_1_1,
149 | new HttpResponseStatus(101, "Web Socket Protocol Handshake"));
150 | res.addHeader(Names.UPGRADE, WEBSOCKET);
151 | res.addHeader(CONNECTION, Values.UPGRADE);
152 |
153 | // Fill in the headers and contents depending on handshake method.
154 | if (req.containsHeader(SEC_WEBSOCKET_KEY1) &&
155 | req.containsHeader(SEC_WEBSOCKET_KEY2)) {
156 | // New handshake method with a challenge:
157 | res.addHeader(SEC_WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
158 | res.addHeader(SEC_WEBSOCKET_LOCATION, getWebSocketLocation(req));
159 | String protocol = req.getHeader(SEC_WEBSOCKET_PROTOCOL);
160 | if (protocol != null) {
161 | res.addHeader(SEC_WEBSOCKET_PROTOCOL, protocol);
162 | }
163 |
164 | // Calculate the answer of the challenge.
165 | String key1 = req.getHeader(SEC_WEBSOCKET_KEY1);
166 | String key2 = req.getHeader(SEC_WEBSOCKET_KEY2);
167 | int a = (int) (Long.parseLong(key1.replaceAll("[^0-9]", "")) / key1.replaceAll("[^ ]", "").length());
168 | int b = (int) (Long.parseLong(key2.replaceAll("[^0-9]", "")) / key2.replaceAll("[^ ]", "").length());
169 | long c = req.getContent().readLong();
170 | ChannelBuffer input = ChannelBuffers.buffer(16);
171 | input.writeInt(a);
172 | input.writeInt(b);
173 | input.writeLong(c);
174 | ChannelBuffer output = ChannelBuffers.wrappedBuffer(
175 | MessageDigest.getInstance("MD5").digest(input.array()));
176 | res.setContent(output);
177 | } else {
178 | // Old handshake method with no challenge:
179 | res.addHeader(WEBSOCKET_ORIGIN, req.getHeader(ORIGIN));
180 | res.addHeader(WEBSOCKET_LOCATION, getWebSocketLocation(req));
181 | String protocol = req.getHeader(WEBSOCKET_PROTOCOL);
182 | if (protocol != null) {
183 | res.addHeader(WEBSOCKET_PROTOCOL, protocol);
184 | }
185 | }
186 |
187 | // Upgrade the connection and send the handshake response.
188 | ChannelPipeline p = ctx.getChannel().getPipeline();
189 | p.remove("aggregator");
190 | p.replace("decoder", "wsdecoder", new WebSocketFrameDecoder());
191 |
192 | ctx.getChannel().write(res);
193 |
194 | p.replace("encoder", "wsencoder", new WebSocketFrameEncoder());
195 |
196 | connectSocket(ctx);
197 | return;
198 | }
199 |
200 | // Send an error page otherwise.
201 | sendHttpResponse(
202 | ctx, req, new DefaultHttpResponse(HTTP_1_1, FORBIDDEN));
203 | }
204 |
205 | private PollingIOClient connectPoller(ChannelHandlerContext ctx) {
206 | String uID = getUniqueID();
207 | PollingIOClient client = new PollingIOClient(ctx, uID);
208 | pollingClients.put(uID, client);
209 | client.send(uID);
210 | handler.OnConnect(client);
211 | return client;
212 | }
213 |
214 | private void connectSocket(ChannelHandlerContext ctx) {
215 | String uID = getUniqueID();
216 | WebSocketIOClient ws = new WebSocketIOClient(ctx, uID);
217 | clients.put(ctx, ws);
218 | ws.send(uID);
219 | handler.OnConnect(ws);
220 | }
221 |
222 | private void handleWebSocketFrame(ChannelHandlerContext ctx, WebSocketFrame frame) {
223 | INSIOClient client = getClientByCTX(ctx);
224 | handleMessage(client, frame.getTextData());
225 | }
226 |
227 | private void handleMessage(INSIOClient client, String message) {
228 | String decoded = SocketIOUtils.decode(message);
229 | if(decoded.substring(0, 3).equals("~h~")) {
230 | client.heartbeat();
231 | } else {
232 | handler.OnMessage(client, decoded);
233 | }
234 | }
235 |
236 | private void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, HttpResponse res) {
237 | // Generate an error page if response status code is not OK (200).
238 | if (res.getStatus().getCode() != 200) {
239 | res.setContent(
240 | ChannelBuffers.copiedBuffer(
241 | res.getStatus().toString(), CharsetUtil.UTF_8));
242 | setContentLength(res, res.getContent().readableBytes());
243 | }
244 |
245 | // Send the response and close the connection if necessary.
246 | ChannelFuture f = ctx.getChannel().write(res);
247 | if (!isKeepAlive(req) || res.getStatus().getCode() != 200) {
248 | f.addListener(ChannelFutureListener.CLOSE);
249 | }
250 | }
251 |
252 | public void prepShutDown() {
253 | this.heartbeatTimer.cancel();
254 | }
255 |
256 | @Override
257 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
258 | throws Exception {
259 | e.getCause().printStackTrace();
260 | e.getChannel().close();
261 | }
262 |
263 | private String getWebSocketLocation(HttpRequest req) {
264 | return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + WEBSOCKET_PATH;
265 | }
266 |
267 | private String getFlashSocketLocation(HttpRequest req) {
268 | return "ws://" + req.getHeader(HttpHeaders.Names.HOST) + FLASHSOCKET_PATH;
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/WebSocketServerPipelineFactory.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import static org.jboss.netty.channel.Channels.*;
4 |
5 | import org.jboss.netty.channel.ChannelPipeline;
6 | import org.jboss.netty.channel.ChannelPipelineFactory;
7 | import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
8 | import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
9 | import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
10 |
11 | public class WebSocketServerPipelineFactory implements ChannelPipelineFactory {
12 | private WebSocketServerHandler socketHandler;
13 |
14 | public WebSocketServerPipelineFactory(WebSocketServerHandler handler) {
15 | this.socketHandler = handler;
16 | }
17 |
18 | public ChannelPipeline getPipeline() throws Exception {
19 | // Create a default pipeline implementation.
20 | ChannelPipeline pipeline = pipeline();
21 | pipeline.addLast("decoder", new HttpRequestDecoder());
22 | pipeline.addLast("aggregator", new HttpChunkAggregator(65536));
23 | pipeline.addLast("encoder", new HttpResponseEncoder());
24 | pipeline.addLast("handler", socketHandler);
25 | return pipeline;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/flashpolicy/FlashPolicyServer.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty.flashpolicy;
2 |
3 | import java.net.InetSocketAddress;
4 | import java.util.concurrent.Executors;
5 |
6 | import org.jboss.netty.channel.Channel;
7 | import org.jboss.netty.bootstrap.ServerBootstrap;
8 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
9 |
10 | public class FlashPolicyServer {
11 |
12 | public static Channel serverChannel;
13 | public static ServerBootstrap bootstrap;
14 |
15 | public static void start() {
16 | // Configure the server.
17 | bootstrap = new ServerBootstrap(
18 | new NioServerSocketChannelFactory(
19 | Executors.newCachedThreadPool(),
20 | Executors.newCachedThreadPool()));
21 |
22 | // Set up the event pipeline factory.
23 | bootstrap.setPipelineFactory(new FlashPolicyServerPipelineFactory());
24 |
25 | bootstrap.setOption("child.tcpNoDelay", true);
26 | bootstrap.setOption("child.keepAlive", true);
27 |
28 | // Bind and start to accept incoming connections.
29 | serverChannel = bootstrap.bind(new InetSocketAddress(843));
30 | }
31 |
32 | public static void stop() {
33 | serverChannel.close();
34 | bootstrap.releaseExternalResources();
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/flashpolicy/FlashPolicyServerDecoder.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty.flashpolicy;
2 | /*
3 | * Copyright 2010 Bruce Mitchener.
4 | *
5 | * Bruce Mitchener licenses this file to you under the Apache License, version 2.0
6 | * (the "License"); you may not use this file except in compliance with the
7 | * License. You may obtain a copy of the License at:
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 | * License for the specific language governing permissions and limitations
15 | * under the License.
16 | */
17 |
18 | import org.jboss.netty.buffer.ChannelBuffer;
19 | import org.jboss.netty.buffer.ChannelBuffers;
20 | import org.jboss.netty.channel.Channel;
21 | import org.jboss.netty.channel.ChannelHandlerContext;
22 | import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
23 | import org.jboss.netty.handler.codec.replay.VoidEnum;
24 | import org.jboss.netty.util.CharsetUtil;
25 |
26 | /**
27 | * @author Bruce Mitchener
28 | */
29 | public class FlashPolicyServerDecoder extends ReplayingDecoder {
30 | private final ChannelBuffer requestBuffer = ChannelBuffers.copiedBuffer("", CharsetUtil.US_ASCII);
31 |
32 | @Override
33 | protected Object decode(
34 | ChannelHandlerContext ctx, Channel channel,
35 | ChannelBuffer buffer, VoidEnum state) {
36 |
37 | ChannelBuffer data = buffer.readBytes(requestBuffer.readableBytes());
38 | if (data.equals(requestBuffer)) {
39 | return data;
40 | }
41 | channel.close();
42 | return null;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/flashpolicy/FlashPolicyServerHandler.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty.flashpolicy;
2 |
3 | /*
4 | * Copyright 2010 Bruce Mitchener.
5 | *
6 | * Bruce Mitchener licenses this file to you under the Apache License, version 2.0
7 | * (the "License"); you may not use this file except in compliance with the
8 | * License. You may obtain a copy of the License at:
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | * License for the specific language governing permissions and limitations
16 | * under the License.
17 | */
18 | import org.jboss.netty.buffer.ChannelBuffer;
19 | import org.jboss.netty.buffer.ChannelBuffers;
20 | import org.jboss.netty.channel.ChannelFuture;
21 | import org.jboss.netty.channel.ChannelFutureListener;
22 | import org.jboss.netty.channel.ChannelHandlerContext;
23 | import org.jboss.netty.channel.ChannelPipeline;
24 | import org.jboss.netty.channel.ExceptionEvent;
25 | import org.jboss.netty.channel.MessageEvent;
26 | import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
27 | import org.jboss.netty.handler.timeout.ReadTimeoutException;
28 | import org.jboss.netty.util.CharsetUtil;
29 |
30 | /**
31 | * @author Bruce Mitchener
32 | */
33 | public class FlashPolicyServerHandler extends SimpleChannelUpstreamHandler {
34 |
35 | private static final String NEWLINE = "\r\n";
36 |
37 | @Override
38 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
39 | Object msg = e.getMessage();
40 | ChannelFuture f = e.getChannel().write(this.getPolicyFileContents());
41 | f.addListener(ChannelFutureListener.CLOSE);
42 | }
43 |
44 | private ChannelBuffer getPolicyFileContents() throws Exception {
45 | return ChannelBuffers.copiedBuffer(
46 | "" + NEWLINE +
47 | "" + NEWLINE +
48 | " " + NEWLINE +
49 | " " + NEWLINE +
50 | " " + NEWLINE +
51 | "" + NEWLINE,
52 | CharsetUtil.US_ASCII);
53 | }
54 |
55 | @Override
56 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
57 | throws Exception {
58 | if (e.getCause() instanceof ReadTimeoutException) {
59 | System.out.println("Connection timed out.");
60 | e.getChannel().close();
61 | } else {
62 | e.getCause().printStackTrace();
63 | e.getChannel().close();
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/ibdknox/socket_io_netty/flashpolicy/FlashPolicyServerPipelineFactory.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty.flashpolicy;
2 |
3 | /*
4 | * Copyright 2010 Bruce Mitchener.
5 | *
6 | * Bruce Mitchener licenses this file to you under the Apache License, version 2.0
7 | * (the "License"); you may not use this file except in compliance with the
8 | * License. You may obtain a copy of the License at:
9 | *
10 | * http://www.apache.org/licenses/LICENSE-2.0
11 | *
12 | * Unless required by applicable law or agreed to in writing, software
13 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 | * License for the specific language governing permissions and limitations
16 | * under the License.
17 | */
18 |
19 | import static org.jboss.netty.channel.Channels.*;
20 |
21 | import org.jboss.netty.channel.ChannelPipeline;
22 | import org.jboss.netty.channel.ChannelPipelineFactory;
23 | import org.jboss.netty.handler.timeout.ReadTimeoutHandler;
24 | import org.jboss.netty.util.HashedWheelTimer;
25 | import org.jboss.netty.util.Timer;
26 |
27 | /**
28 | * @author Bruce Mitchener
29 | */
30 | public class FlashPolicyServerPipelineFactory implements ChannelPipelineFactory {
31 |
32 | private final Timer timer = new HashedWheelTimer();
33 |
34 | public ChannelPipeline getPipeline() throws Exception {
35 | // Create a default pipeline implementation.
36 | ChannelPipeline pipeline = pipeline();
37 | pipeline.addLast("timeout", new ReadTimeoutHandler(timer, 30));
38 | pipeline.addLast("decoder", new FlashPolicyServerDecoder());
39 | pipeline.addLast("handler", new FlashPolicyServerHandler());
40 | return pipeline;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/test/java/com/ibdknox/socket_io_netty/AppTest.java:
--------------------------------------------------------------------------------
1 | package com.ibdknox.socket_io_netty;
2 |
3 | import junit.framework.Test;
4 | import junit.framework.TestCase;
5 | import junit.framework.TestSuite;
6 |
7 | /**
8 | * Unit test for simple App.
9 | */
10 | public class AppTest
11 | extends TestCase
12 | {
13 | /**
14 | * Create the test case
15 | *
16 | * @param testName name of the test case
17 | */
18 | public AppTest( String testName )
19 | {
20 | super( testName );
21 | }
22 |
23 | /**
24 | * @return the suite of tests being tested
25 | */
26 | public static Test suite()
27 | {
28 | return new TestSuite( AppTest.class );
29 | }
30 |
31 | /**
32 | * Rigourous Test :-)
33 | */
34 | public void testApp()
35 | {
36 | assertTrue( true );
37 | }
38 |
39 | public void testDecode()
40 | {
41 | String msg = "~m~16~m~{\n user : {\n }\n}";
42 | String decode = com.ibdknox.socket_io_netty.SocketIOUtils.decode(msg);
43 | assertTrue( decode.equals("{\n user : {\n }\n}") );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------