├── .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 | --------------------------------------------------------------------------------