├── README.md ├── pom.xml ├── script └── run.sh └── src ├── main ├── java │ └── com │ │ └── butterfly │ │ └── nioserver │ │ ├── App.java │ │ ├── ButterflySoftCache.java │ │ ├── ChangeRequest.java │ │ ├── HttpResponceHeaderBuilder.java │ │ ├── NioHttpServer.java │ │ ├── RequestHandler.java │ │ ├── RequestHeaderHandler.java │ │ └── util │ │ └── Utils.java └── resources │ ├── META-INF │ └── mime.types │ └── log4j.xml └── test └── java └── com └── butterfly └── nioserver └── AppTest.java /README.md: -------------------------------------------------------------------------------- 1 | a simple java nio httpserver 2 | ================================== 3 | 4 | a simple java nio based http server. understand GET and HEAD. 5 | with a SoftReference cache. I write it for fun and speed. 6 | 7 | how to run 8 | ---------- 9 | 1. mvn package 10 | 2. ./script/run.sh [port] [www-root] 11 | 12 | caution 13 | ------- 14 | no security is enforced -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.butterfly.nioserver 5 | httpserver 6 | jar 7 | 1.0-SNAPSHOT 8 | httpserver 9 | http://maven.apache.org 10 | 11 | 12 | log4j 13 | log4j 14 | 1.2.16 15 | 16 | 17 | junit 18 | junit 19 | 3.8.1 20 | test 21 | 22 | 23 | 24 | 25 | 26 | org.apache.maven.plugins 27 | maven-compiler-plugin 28 | 2.0.2 29 | 30 | 1.6 31 | 1.6 32 | 33 | 34 | 35 | 36 | org.apache.maven.plugins 37 | maven-jar-plugin 38 | 39 | 40 | 41 | true 42 | com.butterfly.nioserver.NioHttpServer 43 | 44 | 45 | 46 | 47 | 48 | maven-assembly-plugin 49 | 50 | 51 | jar-with-dependencies 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-dependency-plugin 58 | 59 | 60 | copy-dependencies 61 | package 62 | 63 | copy-dependencies 64 | 65 | 66 | target 67 | 68 | 69 | 70 | 71 | 72 | org.codehaus.mojo 73 | exec-maven-plugin 74 | 1.1 75 | 76 | 77 | 78 | exec 79 | 80 | 81 | 82 | 83 | com.butterfly.nioserver.NioHttpServer 84 | 85 | /opt/android-sdk-linux_x86/docs 86 | 8080 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | java -cp target/*: com.butterfly.nioserver.NioHttpServer $@ 3 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/App.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | /** 4 | * Hello world! 5 | * 6 | */ 7 | public class App 8 | { 9 | public static void main( String[] args ) 10 | { 11 | System.out.println( "Hello World!" ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/ButterflySoftCache.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | import java.lang.ref.ReferenceQueue; 4 | import java.lang.ref.SoftReference; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | public class ButterflySoftCache { 9 | 10 | public static class CacheEntry { 11 | public byte[] header; 12 | public byte[] body; 13 | 14 | public CacheEntry(byte[] header, byte[] body) { 15 | this.header = header; 16 | this.body = body; 17 | } 18 | 19 | } 20 | 21 | public static class MapEntry extends SoftReference { 22 | 23 | String key; 24 | 25 | public MapEntry(String key, CacheEntry referent, ReferenceQueue q) { 26 | super(referent, q); 27 | this.key = key; 28 | } 29 | 30 | } 31 | 32 | private ReferenceQueue queue = new ReferenceQueue(); 33 | 34 | /** 35 | * the back map used 36 | */ 37 | private Map map = new ConcurrentHashMap(); 38 | 39 | public CacheEntry get(String key) { 40 | CacheEntry result = null; 41 | MapEntry entry = map.get(key); 42 | if (entry != null) { 43 | result = entry.get(); 44 | if (result == null) { 45 | map.remove(entry.key); 46 | } 47 | } 48 | return result; 49 | } 50 | 51 | private void processQueue() { 52 | MapEntry entry; 53 | while ((entry = (MapEntry) queue.poll()) != null) { 54 | map.remove(entry.key); 55 | } 56 | } 57 | 58 | public void put(String key, byte[] header, byte[] body) { 59 | processQueue(); 60 | map.put(key, new MapEntry(key, new CacheEntry(header, body), queue)); 61 | } 62 | 63 | /** 64 | * debug help 65 | */ 66 | @Override 67 | public String toString() { 68 | StringBuilder sb = new StringBuilder(); 69 | int memory = 0; 70 | for (String key : map.keySet()) { 71 | CacheEntry entry = map.get(key).get(); 72 | if (entry != null) { 73 | int size = 0; 74 | if (entry.body != null) 75 | size += entry.body.length; 76 | if (entry.header != null) 77 | size += entry.header.length; 78 | 79 | sb.append(key).append("\t").append(size).append("\t").append(size / 1024).append("k\n"); 80 | memory += size; 81 | } 82 | } 83 | 84 | StringBuilder sb2 = new StringBuilder(); 85 | sb2.append("cache item count: ").append(map.size()).append("\n"); 86 | sb2.append("memory size:\t").append(memory).append("\t").append((double) memory / 1024).append("k\n"); 87 | sb2.append(sb); 88 | return sb2.toString(); 89 | 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/ChangeRequest.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | import java.nio.channels.SocketChannel; 4 | 5 | public class ChangeRequest { 6 | 7 | public static final int REGISTER = 1; 8 | public static final int CHANGEOPS = 2; 9 | 10 | public SocketChannel socket; 11 | public int type; 12 | public int ops; 13 | 14 | public ChangeRequest(SocketChannel socket, int type, int ops) { 15 | this.socket = socket; 16 | this.type = type; 17 | this.ops = ops; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/HttpResponceHeaderBuilder.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | import java.util.TreeMap; 6 | 7 | public class HttpResponceHeaderBuilder { 8 | public static final String OK_200 = "HTTP/1.1 200 OK"; 9 | public static final String NEWLINE = "\r\n"; 10 | public static final String NOT_FOUND_404 = "HTTP/1.1 404 Not Find"; 11 | public static final String SERVER_ERROR_500 = "HTTP/1.1 500 Internal Server Error"; 12 | public static final String CONTENT_TYPE = "Content-Type"; 13 | public static final String CONNECTION = "Connection"; 14 | public static final String CONTENT_LENGTH = "Content-Length"; 15 | public static final String KEEP_ALIVE = "keep-alive"; 16 | public static final String CONTENT_ENCODING = "Content-Encoding"; 17 | public static final String ACCEPT_ENCODING = "Accept-Encoding"; 18 | public static final String LAST_MODIFIED = "Last-Modified"; 19 | public static final String GZIP = "gzip"; 20 | 21 | private String status; 22 | private Map header = new TreeMap(); 23 | 24 | /** 25 | * status default to 200 26 | */ 27 | public HttpResponceHeaderBuilder() { 28 | status = OK_200; 29 | } 30 | 31 | public HttpResponceHeaderBuilder addHeader(String key, Object value) { 32 | header.put(key, value); 33 | return this; 34 | } 35 | 36 | public void clear() { 37 | status = OK_200; 38 | header.clear(); 39 | } 40 | 41 | public byte[] getHeader() { 42 | return toString().getBytes(); 43 | } 44 | 45 | public HttpResponceHeaderBuilder setStatus(String status) { 46 | this.status = status; 47 | return this; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | 53 | StringBuilder sb = new StringBuilder(120); 54 | sb.append(status).append(NEWLINE); 55 | Set keySet = header.keySet(); 56 | for (String key : keySet) { 57 | sb.append(key).append(": ").append(header.get(key)).append(NEWLINE); 58 | } 59 | sb.append(NEWLINE); // empty line; 60 | return sb.toString(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/NioHttpServer.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.net.InetAddress; 6 | import java.net.InetSocketAddress; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.SelectionKey; 9 | import java.nio.channels.Selector; 10 | import java.nio.channels.ServerSocketChannel; 11 | import java.nio.channels.SocketChannel; 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.Iterator; 15 | import java.util.LinkedList; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import org.apache.log4j.Logger; 20 | 21 | import com.butterfly.nioserver.util.Utils; 22 | 23 | public class NioHttpServer implements Runnable { 24 | 25 | private static Logger logger = Logger.getLogger(NioHttpServer.class); 26 | 27 | public static void main(String[] args) throws IOException { 28 | 29 | String root = new File(".").getAbsolutePath(); 30 | int port = 8080; 31 | if (args.length > 0) 32 | port = Integer.parseInt(args[0]); 33 | 34 | if (args.length > 1) 35 | root = args[1]; 36 | System.out.println(port+"\t"+root); 37 | NioHttpServer server = new NioHttpServer(null, port); 38 | int cpu = Runtime.getRuntime().availableProcessors(); 39 | ButterflySoftCache cache = new ButterflySoftCache(); 40 | // int i = 0; 41 | for (int i = 0; i < cpu; ++i) { 42 | RequestHandler handler = new RequestHandler(server, root, cache); 43 | server.addRequestHanlder(handler); 44 | new Thread(handler, "worker" + i).start(); 45 | } 46 | 47 | new Thread(server, "selector").start(); 48 | } 49 | 50 | private ServerSocketChannel serverChannel; 51 | private Selector selector; 52 | private ByteBuffer readBuffer = ByteBuffer.allocate(8912); 53 | private List changeRequests = new LinkedList(); 54 | private Map> pendingSent = new HashMap>(); 55 | private List requestHandlers = new ArrayList(); 56 | 57 | public NioHttpServer(InetAddress address, int port) throws IOException { 58 | selector = Selector.open(); 59 | serverChannel = ServerSocketChannel.open(); 60 | serverChannel.configureBlocking(false); 61 | serverChannel.socket().bind(new InetSocketAddress(address, port)); 62 | serverChannel.register(selector, SelectionKey.OP_ACCEPT); 63 | 64 | } 65 | 66 | private void accept(SelectionKey key) throws IOException { 67 | SocketChannel socketChannel = serverChannel.accept(); 68 | logger.info("new connection:\t" + socketChannel); 69 | socketChannel.configureBlocking(false); 70 | socketChannel.register(selector, SelectionKey.OP_READ); 71 | } 72 | 73 | public void addRequestHanlder(RequestHandler handler) { 74 | requestHandlers.add(handler); 75 | } 76 | 77 | private void read(SelectionKey key) throws IOException { 78 | SocketChannel socketChannel = (SocketChannel) key.channel(); 79 | readBuffer.clear(); 80 | int numRead; 81 | try { 82 | numRead = socketChannel.read(readBuffer); 83 | 84 | } catch (IOException e) { 85 | // the remote forcibly closed the connection 86 | key.cancel(); 87 | socketChannel.close(); 88 | logger.info("closed by exception" + socketChannel); 89 | return; 90 | } 91 | 92 | if (numRead == -1) { 93 | // remote entity shut the socket down cleanly. 94 | socketChannel.close(); 95 | key.cancel(); 96 | logger.info("closed by shutdown" + socketChannel); 97 | return; 98 | } 99 | 100 | int worker = socketChannel.hashCode() % requestHandlers.size(); 101 | if (logger.isDebugEnabled()) { 102 | logger.debug(selector.keys().size() + "\t" + worker + "\t" + socketChannel); 103 | } 104 | requestHandlers.get(worker).processData(socketChannel, readBuffer.array(), numRead); 105 | } 106 | 107 | @Override 108 | public void run() { 109 | SelectionKey key = null; 110 | while (true) { 111 | try { 112 | synchronized (changeRequests) { 113 | for (ChangeRequest request : changeRequests) { 114 | switch (request.type) { 115 | case ChangeRequest.CHANGEOPS: 116 | key = request.socket.keyFor(selector); 117 | if (key != null && key.isValid()) { 118 | key.interestOps(request.ops); 119 | } 120 | break; 121 | } 122 | } 123 | changeRequests.clear(); 124 | } 125 | 126 | selector.select(); 127 | Iterator selectedKeys = selector.selectedKeys().iterator(); 128 | while (selectedKeys.hasNext()) { 129 | key = selectedKeys.next(); 130 | selectedKeys.remove(); 131 | if (!key.isValid()) { 132 | continue; 133 | } 134 | if (key.isAcceptable()) { 135 | accept(key); 136 | } else if (key.isReadable()) { 137 | read(key); 138 | } else if (key.isWritable()) { 139 | write(key); 140 | } 141 | } 142 | } catch (Exception e) { 143 | if (key != null) { 144 | key.cancel(); 145 | Utils.closeQuietly(key.channel()); 146 | } 147 | logger.error("closed" + key.channel(), e); 148 | } 149 | } 150 | 151 | } 152 | 153 | public void send(SocketChannel socket, byte[] data) { 154 | synchronized (changeRequests) { 155 | changeRequests.add(new ChangeRequest(socket, ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE)); 156 | synchronized (pendingSent) { 157 | List queue = pendingSent.get(socket); 158 | if (queue == null) { 159 | queue = new ArrayList(); 160 | pendingSent.put(socket, queue); 161 | } 162 | queue.add(ByteBuffer.wrap(data)); 163 | } 164 | } 165 | 166 | selector.wakeup(); 167 | } 168 | 169 | private void write(SelectionKey key) throws IOException { 170 | SocketChannel socketChannel = (SocketChannel) key.channel(); 171 | synchronized (pendingSent) { 172 | List queue = pendingSent.get(socketChannel); 173 | while (!queue.isEmpty()) { 174 | ByteBuffer buf = queue.get(0); 175 | socketChannel.write(buf); 176 | // have more to send 177 | if (buf.remaining() > 0) { 178 | break; 179 | } 180 | queue.remove(0); 181 | } 182 | if (queue.isEmpty()) { 183 | key.interestOps(SelectionKey.OP_READ); 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.ACCEPT_ENCODING; 4 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.CONNECTION; 5 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.CONTENT_ENCODING; 6 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.CONTENT_LENGTH; 7 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.CONTENT_TYPE; 8 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.GZIP; 9 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.KEEP_ALIVE; 10 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.LAST_MODIFIED; 11 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.NOT_FOUND_404; 12 | import static com.butterfly.nioserver.HttpResponceHeaderBuilder.SERVER_ERROR_500; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.nio.channels.SocketChannel; 17 | import java.text.DateFormat; 18 | import java.text.SimpleDateFormat; 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | import java.util.Locale; 23 | import java.util.Map; 24 | import java.util.TimeZone; 25 | import java.util.WeakHashMap; 26 | 27 | import javax.activation.MimetypesFileTypeMap; 28 | 29 | import org.apache.log4j.Logger; 30 | 31 | import com.butterfly.nioserver.ButterflySoftCache.CacheEntry; 32 | import com.butterfly.nioserver.RequestHeaderHandler.Verb; 33 | import com.butterfly.nioserver.util.Utils; 34 | 35 | public class RequestHandler implements Runnable { 36 | 37 | private static final DateFormat formater = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 38 | static { 39 | formater.setTimeZone(TimeZone.getTimeZone("GMT")); 40 | } 41 | private static final Logger logger = Logger.getLogger(RequestHandler.class); 42 | private ButterflySoftCache cache; 43 | private File currentFile; 44 | private Date lastModified; 45 | private List pendingRequestSegment = new ArrayList(); 46 | private Map requestMap = new WeakHashMap(); 47 | private NioHttpServer server; 48 | private String serverRoot; 49 | private String acceptEncoding; 50 | 51 | /** 52 | * 53 | * @param server 54 | * {@link NioHttpServer} the server 55 | * @param wwwroot 56 | * wwwroot 57 | * @param cache 58 | * cache implementation 59 | */ 60 | public RequestHandler(NioHttpServer server, String wwwroot, ButterflySoftCache cache) { 61 | this.cache = cache; 62 | this.serverRoot = wwwroot; 63 | this.server = server; 64 | } 65 | 66 | public void processData(SocketChannel client, byte[] data, int count) { 67 | 68 | byte[] dataCopy = new byte[count]; 69 | System.arraycopy(data, 0, dataCopy, 0, count); 70 | 71 | synchronized (pendingRequestSegment) { 72 | // add data 73 | pendingRequestSegment.add(new RequestSegmentHeader(client, dataCopy)); 74 | pendingRequestSegment.notify(); 75 | } 76 | } 77 | 78 | @Override 79 | public void run() { 80 | 81 | RequestSegmentHeader requestData = null; 82 | RequestHeaderHandler header = null; 83 | CacheEntry entry = null; 84 | HttpResponceHeaderBuilder builder = new HttpResponceHeaderBuilder(); 85 | byte[] head = null; 86 | byte[] body = null; 87 | String file = null; 88 | String mime = null; 89 | boolean zip = false; 90 | 91 | // wait for data 92 | while (true) { 93 | 94 | synchronized (pendingRequestSegment) { 95 | while (pendingRequestSegment.isEmpty()) { 96 | try { 97 | pendingRequestSegment.wait(); 98 | } catch (InterruptedException e) { 99 | } 100 | } 101 | requestData = pendingRequestSegment.remove(0); 102 | } 103 | 104 | header = requestMap.get(requestData.client); 105 | if (header == null) { 106 | header = new RequestHeaderHandler(); 107 | requestMap.put(requestData.client, header); 108 | } 109 | try { 110 | if (header.appendSegment(requestData.data)) { 111 | file = serverRoot + header.getResouce(); 112 | currentFile = new File(file); 113 | mime = new MimetypesFileTypeMap().getContentType(currentFile); 114 | logger.info(currentFile+"\t"+mime); 115 | acceptEncoding = header.getHeader(ACCEPT_ENCODING); 116 | // gzip text 117 | zip = mime.contains("text") && acceptEncoding != null 118 | && (acceptEncoding.contains("gzip") || acceptEncoding.contains("gzip")); 119 | if (zip) { 120 | entry = cache.get(file + GZIP); 121 | } else { 122 | entry = cache.get(file); 123 | } 124 | 125 | // miss the cache 126 | if (entry == null) { 127 | builder.clear(); // get ready for next request; 128 | 129 | logger.info("miss the cache " + file); 130 | 131 | // always keep alive 132 | builder.addHeader(CONNECTION, KEEP_ALIVE); 133 | builder.addHeader(CONTENT_TYPE, mime); 134 | 135 | // response body byte, exception throws here 136 | body = Utils.file2ByteArray(currentFile, zip); 137 | builder.addHeader(CONTENT_LENGTH, body.length); 138 | if (zip) { 139 | // add zip header 140 | builder.addHeader(CONTENT_ENCODING, GZIP); 141 | } 142 | 143 | // last modified header 144 | lastModified = new Date(currentFile.lastModified()); 145 | builder.addHeader(LAST_MODIFIED, formater.format(lastModified)); 146 | 147 | // response header byte 148 | head = builder.getHeader(); 149 | // add to the cache 150 | if (zip) 151 | file = file + GZIP; 152 | cache.put(file, head, body); 153 | } 154 | // cache is hit 155 | else { 156 | logger.debug("cache is hit" + file); 157 | body = entry.body; 158 | head = entry.header; 159 | } 160 | // data is prepared, send out to the client 161 | server.send(requestData.client, head); 162 | if (body != null && header.getVerb() == Verb.GET) 163 | server.send(requestData.client, body); 164 | } 165 | } catch (IOException e) { 166 | builder.addHeader(CONTENT_LENGTH, 0); 167 | builder.setStatus(NOT_FOUND_404); 168 | head = builder.getHeader(); 169 | server.send(requestData.client, head); 170 | // cache 404 if case client make a mistake again 171 | cache.put(file, head, body); 172 | logger.error("404 error", e); 173 | 174 | } catch (Exception e) { 175 | // any other, it's a 505 error 176 | builder.addHeader(CONTENT_LENGTH, 0); 177 | builder.setStatus(SERVER_ERROR_500); 178 | head = builder.getHeader(); 179 | server.send(requestData.client, head); 180 | logger.error("505 error", e); 181 | } 182 | } 183 | } 184 | } 185 | 186 | class RequestSegmentHeader { 187 | SocketChannel client; 188 | byte[] data; 189 | 190 | public RequestSegmentHeader(SocketChannel client, byte[] data) { 191 | this.client = client; 192 | this.data = data; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/RequestHeaderHandler.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.CharBuffer; 5 | import java.nio.charset.CharacterCodingException; 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.CharsetDecoder; 8 | import java.util.Map; 9 | import java.util.Set; 10 | import java.util.TreeMap; 11 | 12 | public class RequestHeaderHandler { 13 | 14 | public static enum Verb { 15 | CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE 16 | } 17 | 18 | public static enum Version { 19 | HTTP10, HTTP11 20 | } 21 | 22 | private static CharsetDecoder decoder = Charset.forName("ISO-8859-1").newDecoder(); 23 | private static final byte[] END = new byte[] { 13, 10, 13, 10 }; 24 | private static final byte[] GET = new byte[] { 71, 69, 84, 32 }; 25 | private static final byte[] HEAD = new byte[] { 72, 69, 65, 68 }; 26 | 27 | /** 28 | * 29 | * @param data 30 | * to search from 31 | * @param tofind 32 | * target 33 | * @param start 34 | * start index 35 | * @return index of the first find if find, data.length if not find 36 | */ 37 | public static int findSub(byte[] data, byte[] tofind, int start) { 38 | int index = data.length; 39 | outer: for (int i = start; i < data.length; ++i) { 40 | 41 | for (int j = 0; j < tofind.length;) { 42 | if (data[i] == tofind[j]) { 43 | ++i; 44 | ++j; 45 | if (j == tofind.length) { 46 | index = i - tofind.length; 47 | break outer; 48 | } 49 | } else { 50 | i = i - j; // step back 51 | break; 52 | } 53 | } 54 | } 55 | return index; 56 | } 57 | 58 | // private static Logger logger = Logger.getLogger(HttpRequestHeader.class); 59 | 60 | /** 61 | * same as {@link #findSub(byte[], byte[], int)},except find from end to 62 | * start; 63 | * 64 | * @param data 65 | * to search from 66 | * @param tofind 67 | * target 68 | * @param start 69 | * start index 70 | * @return index of the first find if find, data.length if not find 71 | */ 72 | public static int findSubFromEnd(byte[] data, byte[] tofind, int start) { 73 | int index = data.length; 74 | outer: for (int i = data.length - tofind.length; i > 0; --i) { 75 | 76 | for (int j = 0; j < tofind.length;) { 77 | if (data[i] == tofind[j]) { 78 | ++i; 79 | ++j; 80 | if (j == tofind.length) { 81 | index = i - tofind.length; 82 | break outer; 83 | } 84 | } else { 85 | i = i - j; // step back 86 | break; 87 | } 88 | } 89 | } 90 | return index; 91 | } 92 | 93 | public static void main(String[] args) throws CharacterCodingException { 94 | 95 | CharBuffer s = decoder.decode(ByteBuffer.wrap(new byte[] { 72, 69, 65, 68 })); 96 | 97 | System.out.println(s); 98 | 99 | byte[] data = { 12, 13, 10, 13, 13, 10, 13, 10, 13, 17 }; 100 | 101 | int i = findSubFromEnd(data, END, 0); 102 | data = new byte[] { 12, 13, 10, 13, 13, 10, 13, 10, 10, 10 }; 103 | i = 0; 104 | while (i != -1) { 105 | i = findSub(data, new byte[] { 13, 10 }, i); 106 | System.out.println(i); 107 | } 108 | 109 | } 110 | 111 | // private Version version; 112 | private boolean begin = false; 113 | 114 | private CharBuffer charBuffer = ByteBuffer.allocate(2048).asCharBuffer(); 115 | 116 | private Map headerMap = new TreeMap(); 117 | 118 | private String resouce; 119 | private Verb verb; 120 | 121 | public boolean appendSegment(byte[] segment) { 122 | int beginIndex = 0; 123 | 124 | if (begin == false) { 125 | 126 | if ((beginIndex = findSub(segment, GET, 0)) != segment.length) { 127 | begin = true; 128 | headerMap.clear(); 129 | verb = Verb.GET; 130 | 131 | } else if ((beginIndex = findSub(segment, HEAD, 0)) != segment.length) { 132 | begin = true; 133 | headerMap.clear(); 134 | verb = Verb.HEAD; 135 | 136 | } else { 137 | // not begin yet, and find no begin, just return false; 138 | return false; 139 | 140 | } 141 | } 142 | 143 | int endIndex = findSubFromEnd(segment, END, 0); 144 | ByteBuffer b = ByteBuffer.wrap(segment, beginIndex, endIndex); 145 | decoder.decode(b, charBuffer, endIndex != segment.length); 146 | if (endIndex != segment.length) { 147 | extractValueAndReset(); 148 | return true; 149 | } 150 | return false; 151 | } 152 | 153 | private void extractValueAndReset() { 154 | charBuffer.flip(); 155 | String head = charBuffer.toString(); 156 | String[] lines = head.split("\r\n"); 157 | String[] split = lines[0].split(" "); 158 | 159 | resouce = split[1]; 160 | 161 | String[] temp = null; 162 | for (int i = 1; i < lines.length; ++i) { 163 | temp = lines[i].split(":"); 164 | headerMap.put(temp[0], temp[1]); 165 | } 166 | 167 | charBuffer.clear(); 168 | decoder.reset(); 169 | begin = false; 170 | } 171 | 172 | public String getHeader(String key) { 173 | return headerMap.get(key); 174 | } 175 | 176 | public Set getHeaders() { 177 | return headerMap.keySet(); 178 | } 179 | 180 | public String getResouce() { 181 | if (resouce.endsWith("/")) { 182 | resouce = resouce + "index.html"; 183 | } 184 | return resouce; 185 | } 186 | 187 | /** 188 | * 189 | * @return currently, only GET [71,69,84,32],and HEAD [72, 69, 65, 68] is 190 | * supported 191 | */ 192 | public Verb getVerb() { 193 | return verb; 194 | } 195 | 196 | public Version getVersion() { 197 | throw new RuntimeException("not implement yet"); 198 | // return version; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/main/java/com/butterfly/nioserver/util/Utils.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver.util; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.Closeable; 6 | import java.io.File; 7 | import java.io.FileInputStream; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.zip.GZIPOutputStream; 11 | 12 | public class Utils { 13 | 14 | public static void closeQuietly(Closeable is) { 15 | if (is != null) { 16 | try { 17 | is.close(); 18 | } catch (IOException e) { 19 | } 20 | 21 | } 22 | } 23 | 24 | /** 25 | * 26 | * @param file 27 | * the absolute file path 28 | * @param zip 29 | * gzip or not 30 | * @return byte array of the file 31 | * 32 | * @throws IOException 33 | */ 34 | public static byte[] file2ByteArray(File file, boolean zip) throws IOException { 35 | InputStream is = null; 36 | GZIPOutputStream gzip = null; 37 | byte[] buffer = new byte[8912]; 38 | ByteArrayOutputStream baos = new ByteArrayOutputStream(8912); 39 | try { 40 | if (zip) { 41 | gzip = new GZIPOutputStream(baos); 42 | } 43 | 44 | is = new BufferedInputStream(new FileInputStream(file)); 45 | int read = 0; 46 | while ((read = is.read(buffer)) != -1) { 47 | if (zip) { 48 | gzip.write(buffer, 0, read); 49 | } else { 50 | baos.write(buffer, 0, read); 51 | } 52 | } 53 | } catch (IOException e) { 54 | throw e; 55 | } finally { 56 | closeQuietly(is); 57 | closeQuietly(gzip); 58 | } 59 | return baos.toByteArray(); 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/mime.types: -------------------------------------------------------------------------------- 1 | text/css css 2 | -------------------------------------------------------------------------------- /src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/java/com/butterfly/nioserver/AppTest.java: -------------------------------------------------------------------------------- 1 | package com.butterfly.nioserver; 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 | --------------------------------------------------------------------------------