package org.httpkit.server; import clojure.lang.IFn; import clojure.lang.Keyword; import org.httpkit.DynamicBytes; import org.httpkit.HeaderMap; import org.httpkit.HttpVersion; import sun.misc.Unsafe; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; import java.util.Map; import static org.httpkit.HttpUtils.*; import static org.httpkit.server.ClojureRing.*; import static org.httpkit.server.WSDecoder.*; @SuppressWarnings({"unchecked"}) public class AsyncChannel { static final Unsafe unsafe; static final long closedRanOffset; static final long closeHandlerOffset; static final long receiveHandlerOffset; static final long headerSentOffset; private final SelectionKey key; private final HttpServer server; private HttpRequest request; // package private, for http 1.0 keep-alive volatile int closedRan = 0; // 0 => false, 1 => run // streaming private volatile int isHeaderSent = 0; private volatile IFn receiveHandler = null; volatile IFn closeHandler = null; static { try { // Unsafe instead of AtomicReference to save few bytes of RAM per connection Field field = Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); unsafe = (Unsafe) field.get(null); closedRanOffset = unsafe.objectFieldOffset( AsyncChannel.class.getDeclaredField("closedRan")); closeHandlerOffset = unsafe.objectFieldOffset( AsyncChannel.class.getDeclaredField("closeHandler")); receiveHandlerOffset = unsafe.objectFieldOffset( AsyncChannel.class.getDeclaredField("receiveHandler")); headerSentOffset = unsafe.objectFieldOffset( AsyncChannel.class.getDeclaredField("isHeaderSent")); } catch (Exception e) { throw new RuntimeException(e); } } // messages sent from a WebSocket client should be handled orderly by server // Changed from a Single Thread(IO event thread), no volatile needed LinkingRunnable serialTask; public AsyncChannel(SelectionKey key, HttpServer server) { this.key = key; this.server = server; } public void reset(HttpRequest request) { this.request = request; serialTask = null; unsafe.putOrderedInt(this, closedRanOffset, 0); // lazySet to false unsafe.putOrderedInt(this, headerSentOffset, 0); unsafe.putOrderedObject(this, closeHandlerOffset, null); // lazySet to null unsafe.putOrderedObject(this, receiveHandlerOffset, null); // lazySet to null } private static final byte[] finalChunkBytes = "0\r\n\r\n".getBytes(); private static final byte[] newLineBytes = "\r\n".getBytes(); private static ByteBuffer chunkSize(int size) { String s = Integer.toHexString(size) + "\r\n"; return ByteBuffer.wrap(s.getBytes()); } // Write first HTTP header and [first chunk data]? to client private void firstWrite(Object data, boolean close) throws IOException { ByteBuffer buffers[]; int status = 200; Object body = data; HeaderMap headers; if (data instanceof Map) { Map<Keyword, Object> resp = (Map<Keyword, Object>) data; headers = HeaderMap.camelCase((Map) resp.get(HEADERS)); status = getStatus(resp); body = resp.get(BODY); } else { headers = new HeaderMap(); } if (headers.isEmpty()) { // default 200 and text/html headers.put("Content-Type", "text/html; charset=utf-8"); } if (request.isKeepAlive && request.version == HttpVersion.HTTP_1_0) { headers.put("Connection", "Keep-Alive"); } if (close) { // normal response, Content-Length. Every http client understand it buffers = HttpEncode(status, headers, body); } else { if (request.version == HttpVersion.HTTP_1_1) { headers.put("Transfer-Encoding", "chunked"); // first chunk } ByteBuffer[] bb = HttpEncode(status, headers, body); if (body == null) { buffers = bb; } else { buffers = new ByteBuffer[]{ bb[0], // header chunkSize(bb[1].remaining()), // chunk size bb[1], // chunk data ByteBuffer.wrap(newLineBytes) // terminating CRLF sequence }; } } if (close) { onClose(0); } server.tryWrite(key, !close, buffers); } // for streaming, send a chunk of data to client private void writeChunk(Object body, boolean close) throws IOException { if (body instanceof Map) { // only get body if a map body = ((Map<Keyword, Object>) body).get(BODY); } if (body != null) { // null is ignored ByteBuffer t = bodyBuffer(body); if (t.hasRemaining()) { ByteBuffer[] buffers = new ByteBuffer[]{ chunkSize(t.remaining()), t, // actual data ByteBuffer.wrap(newLineBytes) // terminating CRLF sequence }; server.tryWrite(key, !close, buffers); } } if (close) { serverClose(0); } } public void setReceiveHandler(IFn fn) { if (!unsafe.compareAndSwapObject(this, receiveHandlerOffset, null, fn)) { throw new IllegalStateException("receive handler exist: " + receiveHandler); } } public void messageReceived(final Object mesg) { IFn f = receiveHandler; if (f != null) { f.invoke(mesg); // byte[] or String } } public void sendHandshake(Map<String, Object> headers) { HeaderMap map = HeaderMap.camelCase(headers); server.tryWrite(key, HttpEncode(101, map, null)); } public void setCloseHandler(IFn fn) { if (!unsafe.compareAndSwapObject(this, closeHandlerOffset, null, fn)) { // only once throw new IllegalStateException("close handler exist: " + closeHandler); } if (closedRan == 1) { // no handler, but already closed fn.invoke(K_UNKNOWN); } } public void onClose(int status) { if (unsafe.compareAndSwapInt(this, closedRanOffset, 0, 1)) { IFn f = closeHandler; if (f != null) { f.invoke(readable(status)); } } } // also sent CloseFrame a final Chunk public boolean serverClose(int status) { if (!unsafe.compareAndSwapInt(this, closedRanOffset, 0, 1)) { return false; // already closed } if (isWebSocket()) { server.tryWrite(key, WsEncode(OPCODE_CLOSE, ByteBuffer.allocate(2) .putShort((short) status).array())); } else { server.tryWrite(key, false, ByteBuffer.wrap(finalChunkBytes)); } IFn f = closeHandler; if (f != null) { f.invoke(readable(0)); // server close is 0 } return true; } public boolean send(Object data, boolean close) throws IOException { if (closedRan == 1) { return false; } if (isWebSocket()) { if (data instanceof Map) { // only get the :body if map Object tmp = ((Map<Keyword, Object>) data).get(BODY); if (tmp != null) { // save contains(BODY) && get(BODY) data = tmp; } } if (data instanceof String) { // null is not allowed server.tryWrite(key, WsEncode(OPCODE_TEXT, ((String) data).getBytes(UTF_8))); } else if (data instanceof byte[]) { server.tryWrite(key, WsEncode(OPCODE_BINARY, (byte[]) data)); } else if (data instanceof InputStream) { DynamicBytes bytes = readAll((InputStream) data); server.tryWrite(key, WsEncode(OPCODE_BINARY, bytes.get(), bytes.length())); } else if (data != null) { // ignore null String mesg = "send! called with data: " + data.toString() + "(" + data.getClass() + "), but only string, byte[], InputStream expected"; throw new IllegalArgumentException(mesg); } if (close) { serverClose(1000); } } else { if (isHeaderSent == 1) { // HTTP Streaming writeChunk(data, close); } else { isHeaderSent = 1; firstWrite(data, close); } } return true; } public String toString() { Socket s = ((SocketChannel) key.channel()).socket(); return s.getLocalSocketAddress() + "<->" + s.getRemoteSocketAddress(); } public boolean isWebSocket() { return key.attachment() instanceof WsAtta; } public boolean isClosed() { return closedRan == 1; } static Keyword K_BY_SERVER = Keyword.intern("server-close"); static Keyword K_CLIENT_CLOSED = Keyword.intern("client-close"); // http://datatracker.ietf.org/doc/rfc6455/?include_text=1 // 7.4.1. Defined Status Codes static Keyword K_WS_1000 = Keyword.intern("normal"); static Keyword K_WS_1001 = Keyword.intern("going-away"); static Keyword K_WS_1002 = Keyword.intern("protocol-error"); static Keyword K_WS_1003 = Keyword.intern("unsupported"); static Keyword K_UNKNOWN = Keyword.intern("unknown"); private static Keyword readable(int status) { switch (status) { case 0: return K_BY_SERVER; case -1: return K_CLIENT_CLOSED; case 1000: return K_WS_1000; case 1001: return K_WS_1001; case 1002: return K_WS_1002; case 1003: return K_WS_1003; default: return K_UNKNOWN; } } }