├── .gitignore ├── README.md ├── pom.xml ├── script └── run.sh └── src ├── main ├── java │ └── me │ │ └── shenfeng │ │ └── http │ │ ├── ButterflySoftCache.java │ │ ├── ChangeRequest.java │ │ ├── NioHttpServer.java │ │ ├── RequestHandler.java │ │ ├── RequestHeaderDecoder.java │ │ ├── ResponceHeaderBuilder.java │ │ └── Util.java └── resources │ ├── index.tpl │ ├── log4j.xml │ └── mime.types └── test └── java └── me └── shenfeng └── http ├── RequestHeaderDecoderTest.java └── UtilTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | target/ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple JAVA HTTP server 2 | ================================== 3 | 4 | A simple java HTTP server. Understand GET and HEAD. It will do 5 | directory list as well. 6 | 7 | I first write it as an exercise for leaning JAVA NIO in late 2010, 8 | `python -m SimpleHTTPSever [port]` is very handy, but a little slow for 9 | me, so I use this as a replacement for daily use. 10 | 11 | ### how to run 12 | 1. install maven 13 | 2. `mvn package` 14 | 3. `./script/run.sh [port] [www-root]` 15 | 16 | ### TODO 17 | 1. More MIME mapping 18 | 2. Delivering large file 19 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | me.shenfeng.http 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 | 4.8.1 20 | test 21 | 22 | 23 | com.samskivert 24 | jmustache 25 | 1.3 26 | 27 | 28 | 29 | 30 | 31 | org.apache.maven.plugins 32 | maven-jar-plugin 33 | 2.3.1 34 | 35 | 36 | 37 | true 38 | me.shenfeng.http.NioHttpServer 39 | 40 | 41 | 42 | 43 | 44 | maven-assembly-plugin 45 | 46 | 47 | jar-with-dependencies 48 | 49 | 50 | 51 | 52 | org.apache.maven.plugins 53 | maven-dependency-plugin 54 | 55 | 56 | copy-dependencies 57 | package 58 | 59 | copy-dependencies 60 | 61 | 62 | target 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /script/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mvn package && java -cp target/*: me.shenfeng.http.NioHttpServer $@ 3 | -------------------------------------------------------------------------------- /src/main/java/me/shenfeng/http/ButterflySoftCache.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 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/me/shenfeng/http/ChangeRequest.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 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/me/shenfeng/http/NioHttpServer.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 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 | 22 | public class NioHttpServer implements Runnable { 23 | 24 | private static Logger logger = Logger.getLogger(NioHttpServer.class); 25 | 26 | public static void main(String[] args) throws IOException { 27 | 28 | String root = new File(".").getAbsolutePath(); 29 | int port = 8080; 30 | if (args.length > 0) 31 | port = Integer.parseInt(args[0]); 32 | 33 | if (args.length > 1) 34 | root = args[1]; 35 | logger.info("listenning at *." + port + "; root: " + root); 36 | NioHttpServer server = new NioHttpServer(null, port); 37 | int cpu = Runtime.getRuntime().availableProcessors(); 38 | ButterflySoftCache cache = new ButterflySoftCache(); 39 | // int i = 0; 40 | for (int i = 0; i < cpu; ++i) { 41 | RequestHandler handler = new RequestHandler(server, root, cache); 42 | server.addRequestHanlder(handler); 43 | new Thread(handler, "worker" + i).start(); 44 | } 45 | 46 | new Thread(server, "selector").start(); 47 | } 48 | 49 | private ServerSocketChannel serverChannel; 50 | private Selector selector; 51 | private ByteBuffer readBuffer = ByteBuffer.allocate(8912); 52 | private List changeRequests = new LinkedList(); 53 | private Map> pendingSent = new HashMap>(); 54 | private List requestHandlers = new ArrayList(); 55 | 56 | public NioHttpServer(InetAddress address, int port) throws IOException { 57 | selector = Selector.open(); 58 | serverChannel = ServerSocketChannel.open(); 59 | serverChannel.configureBlocking(false); 60 | serverChannel.socket().bind(new InetSocketAddress(address, port)); 61 | serverChannel.register(selector, SelectionKey.OP_ACCEPT); 62 | 63 | } 64 | 65 | private void accept(SelectionKey key) throws IOException { 66 | SocketChannel socketChannel = serverChannel.accept(); 67 | // logger.info("new connection:\t" + socketChannel); 68 | socketChannel.configureBlocking(false); 69 | socketChannel.register(selector, SelectionKey.OP_READ); 70 | } 71 | 72 | public void addRequestHanlder(RequestHandler handler) { 73 | requestHandlers.add(handler); 74 | } 75 | 76 | private void read(SelectionKey key) throws IOException { 77 | SocketChannel socketChannel = (SocketChannel) key.channel(); 78 | readBuffer.clear(); 79 | int numRead; 80 | try { 81 | numRead = socketChannel.read(readBuffer); 82 | 83 | } catch (IOException e) { 84 | // the remote forcibly closed the connection 85 | key.cancel(); 86 | socketChannel.close(); 87 | // logger.info("closed by exception" + socketChannel); 88 | return; 89 | } 90 | 91 | if (numRead == -1) { 92 | // remote entity shut the socket down cleanly. 93 | socketChannel.close(); 94 | key.cancel(); 95 | // logger.info("closed by shutdown" + socketChannel); 96 | return; 97 | } 98 | 99 | int worker = socketChannel.hashCode() % requestHandlers.size(); 100 | if (logger.isDebugEnabled()) { 101 | logger.debug(selector.keys().size() + "\t" + worker + "\t" 102 | + socketChannel); 103 | } 104 | requestHandlers.get(worker).processData(socketChannel, 105 | readBuffer.array(), numRead); 106 | } 107 | 108 | @Override 109 | public void run() { 110 | SelectionKey key = null; 111 | while (true) { 112 | try { 113 | synchronized (changeRequests) { 114 | for (ChangeRequest request : changeRequests) { 115 | switch (request.type) { 116 | case ChangeRequest.CHANGEOPS: 117 | key = request.socket.keyFor(selector); 118 | if (key != null && key.isValid()) { 119 | key.interestOps(request.ops); 120 | } 121 | break; 122 | } 123 | } 124 | changeRequests.clear(); 125 | } 126 | 127 | selector.select(); 128 | Iterator selectedKeys = selector.selectedKeys() 129 | .iterator(); 130 | while (selectedKeys.hasNext()) { 131 | key = selectedKeys.next(); 132 | selectedKeys.remove(); 133 | if (!key.isValid()) { 134 | continue; 135 | } 136 | if (key.isAcceptable()) { 137 | accept(key); 138 | } else if (key.isReadable()) { 139 | read(key); 140 | } else if (key.isWritable()) { 141 | write(key); 142 | } 143 | } 144 | } catch (Exception e) { 145 | if (key != null) { 146 | key.cancel(); 147 | Util.closeQuietly(key.channel()); 148 | } 149 | logger.error("closed" + key.channel(), e); 150 | } 151 | } 152 | 153 | } 154 | 155 | public void send(SocketChannel socket, byte[] data) { 156 | synchronized (changeRequests) { 157 | changeRequests.add(new ChangeRequest(socket, 158 | ChangeRequest.CHANGEOPS, SelectionKey.OP_WRITE)); 159 | synchronized (pendingSent) { 160 | List queue = pendingSent.get(socket); 161 | if (queue == null) { 162 | queue = new ArrayList(); 163 | pendingSent.put(socket, queue); 164 | } 165 | queue.add(ByteBuffer.wrap(data)); 166 | } 167 | } 168 | 169 | selector.wakeup(); 170 | } 171 | 172 | private void write(SelectionKey key) throws IOException { 173 | SocketChannel socketChannel = (SocketChannel) key.channel(); 174 | synchronized (pendingSent) { 175 | List queue = pendingSent.get(socketChannel); 176 | while (!queue.isEmpty()) { 177 | ByteBuffer buf = queue.get(0); 178 | socketChannel.write(buf); 179 | // have more to send 180 | if (buf.remaining() > 0) { 181 | break; 182 | } 183 | queue.remove(0); 184 | } 185 | if (queue.isEmpty()) { 186 | key.interestOps(SelectionKey.OP_READ); 187 | } 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/main/java/me/shenfeng/http/RequestHandler.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 2 | 3 | import static me.shenfeng.http.ResponceHeaderBuilder.ACCEPT_ENCODING; 4 | import static me.shenfeng.http.ResponceHeaderBuilder.CONNECTION; 5 | import static me.shenfeng.http.ResponceHeaderBuilder.CONTENT_ENCODING; 6 | import static me.shenfeng.http.ResponceHeaderBuilder.CONTENT_LENGTH; 7 | import static me.shenfeng.http.ResponceHeaderBuilder.CONTENT_TYPE; 8 | import static me.shenfeng.http.ResponceHeaderBuilder.GZIP; 9 | import static me.shenfeng.http.ResponceHeaderBuilder.KEEP_ALIVE; 10 | import static me.shenfeng.http.ResponceHeaderBuilder.LAST_MODIFIED; 11 | import static me.shenfeng.http.ResponceHeaderBuilder.NOT_FOUND_404; 12 | import static me.shenfeng.http.ResponceHeaderBuilder.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 me.shenfeng.http.RequestHeaderDecoder.Verb; 28 | 29 | import org.apache.log4j.Logger; 30 | 31 | 32 | public class RequestHandler implements Runnable { 33 | 34 | private static final DateFormat formater = new SimpleDateFormat( 35 | "EEE, dd MMM yyyy HH:mm:ss z", Locale.US); 36 | static { 37 | formater.setTimeZone(TimeZone.getTimeZone("GMT")); 38 | } 39 | private static final Logger logger = Logger.getLogger(RequestHandler.class); 40 | private ButterflySoftCache cache; 41 | private File currentFile; 42 | private Date lastModified; 43 | private List pendingRequestSegment = new ArrayList(); 44 | private Map requestMap = new WeakHashMap(); 45 | private NioHttpServer server; 46 | private String serverRoot; 47 | private String acceptEncoding; 48 | 49 | /** 50 | * 51 | * @param server 52 | * {@link NioHttpServer} the server 53 | * @param wwwroot 54 | * wwwroot 55 | * @param cache 56 | * cache implementation 57 | */ 58 | public RequestHandler(NioHttpServer server, String wwwroot, 59 | ButterflySoftCache cache) { 60 | this.cache = cache; 61 | this.serverRoot = wwwroot; 62 | this.server = server; 63 | } 64 | 65 | public void processData(SocketChannel client, byte[] data, int count) { 66 | 67 | byte[] dataCopy = new byte[count]; 68 | System.arraycopy(data, 0, dataCopy, 0, count); 69 | 70 | synchronized (pendingRequestSegment) { 71 | // add data 72 | pendingRequestSegment 73 | .add(new RequestSegmentHeader(client, dataCopy)); 74 | pendingRequestSegment.notify(); 75 | } 76 | } 77 | 78 | @Override 79 | public void run() { 80 | 81 | RequestSegmentHeader requestData = null; 82 | RequestHeaderDecoder header = null; 83 | ResponceHeaderBuilder builder = new ResponceHeaderBuilder(); 84 | byte[] head = null; 85 | byte[] body = null; 86 | String file = null; 87 | String mime = null; 88 | boolean zip = false; 89 | 90 | // wait for data 91 | while (true) { 92 | 93 | synchronized (pendingRequestSegment) { 94 | while (pendingRequestSegment.isEmpty()) { 95 | try { 96 | pendingRequestSegment.wait(); 97 | } catch (InterruptedException e) { 98 | } 99 | } 100 | requestData = pendingRequestSegment.remove(0); 101 | } 102 | 103 | header = requestMap.get(requestData.client); 104 | if (header == null) { 105 | header = new RequestHeaderDecoder(); 106 | requestMap.put(requestData.client, header); 107 | } 108 | try { 109 | if (header.appendSegment(requestData.data)) { 110 | file = serverRoot + header.getResouce(); 111 | currentFile = new File(file); 112 | mime = Util.getContentType(currentFile); 113 | // logger.info(currentFile + "\t" + mime); 114 | acceptEncoding = header.getHeader(ACCEPT_ENCODING); 115 | // gzip text 116 | zip = mime.contains("text") 117 | && acceptEncoding != null 118 | && (acceptEncoding.contains("gzip") || acceptEncoding 119 | .contains("gzip")); 120 | builder.clear(); // get ready for next request; 121 | 122 | // always keep alive 123 | builder.addHeader(CONNECTION, KEEP_ALIVE); 124 | builder.addHeader(CONTENT_TYPE, mime); 125 | 126 | // response body byte, exception throws here 127 | body = Util.file2ByteArray(currentFile, zip); 128 | builder.addHeader(CONTENT_LENGTH, body.length); 129 | if (zip) { 130 | // add zip header 131 | builder.addHeader(CONTENT_ENCODING, GZIP); 132 | } 133 | 134 | // last modified header 135 | lastModified = new Date(currentFile.lastModified()); 136 | builder.addHeader(LAST_MODIFIED, 137 | formater.format(lastModified)); 138 | 139 | // response header byte 140 | head = builder.getHeader(); 141 | 142 | // data is prepared, send out to the client 143 | server.send(requestData.client, head); 144 | if (body != null && header.getVerb() == Verb.GET) 145 | server.send(requestData.client, body); 146 | 147 | logger.info(header.getResouce() + " 200"); 148 | 149 | } 150 | } catch (IOException e) { 151 | builder.addHeader(CONTENT_LENGTH, 0); 152 | builder.setStatus(NOT_FOUND_404); 153 | head = builder.getHeader(); 154 | server.send(requestData.client, head); 155 | // cache 404 if case client make a mistake again 156 | cache.put(file, head, body); 157 | logger.error(header.getResouce() + " 404"); 158 | 159 | } catch (Exception e) { 160 | // any other, it's a 505 error 161 | builder.addHeader(CONTENT_LENGTH, 0); 162 | builder.setStatus(SERVER_ERROR_500); 163 | head = builder.getHeader(); 164 | server.send(requestData.client, head); 165 | logger.error("505 error", e); 166 | } 167 | } 168 | } 169 | } 170 | 171 | class RequestSegmentHeader { 172 | SocketChannel client; 173 | byte[] data; 174 | 175 | public RequestSegmentHeader(SocketChannel client, byte[] data) { 176 | this.client = client; 177 | this.data = data; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/me/shenfeng/http/RequestHeaderDecoder.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 2 | 3 | import java.nio.ByteBuffer; 4 | import java.nio.CharBuffer; 5 | import java.nio.charset.Charset; 6 | import java.nio.charset.CharsetDecoder; 7 | import java.util.Map; 8 | import java.util.Set; 9 | import java.util.TreeMap; 10 | 11 | public class RequestHeaderDecoder { 12 | 13 | public static enum Verb { 14 | CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT, TRACE 15 | } 16 | 17 | public static enum Version { 18 | HTTP10, HTTP11 19 | } 20 | 21 | private static CharsetDecoder decoder = Charset.forName("ISO-8859-1") 22 | .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 | // private Version version; 28 | private boolean begin = false; 29 | 30 | private CharBuffer charBuffer = ByteBuffer.allocate(2048).asCharBuffer(); 31 | 32 | private Map headerMap = new TreeMap(); 33 | 34 | private String resouce; 35 | private Verb verb; 36 | 37 | public boolean appendSegment(byte[] segment) { 38 | int beginIndex = 0; 39 | 40 | if (begin == false) { 41 | 42 | if ((beginIndex = Util.subArray(segment, GET, 0)) != segment.length) { 43 | begin = true; 44 | headerMap.clear(); 45 | verb = Verb.GET; 46 | 47 | } else if ((beginIndex = Util.subArray(segment, HEAD, 0)) != segment.length) { 48 | begin = true; 49 | headerMap.clear(); 50 | verb = Verb.HEAD; 51 | 52 | } else { 53 | // not begin yet, and find no begin, just return false; 54 | return false; 55 | 56 | } 57 | } 58 | 59 | int endIndex = Util.subArrayFromEnd(segment, END, 0); 60 | ByteBuffer b = ByteBuffer.wrap(segment, beginIndex, endIndex); 61 | decoder.decode(b, charBuffer, endIndex != segment.length); 62 | if (endIndex != segment.length) { 63 | extractValueAndReset(); 64 | return true; 65 | } 66 | return false; 67 | } 68 | 69 | private void extractValueAndReset() { 70 | charBuffer.flip(); 71 | String head = charBuffer.toString(); 72 | String[] lines = head.split("\r\n"); 73 | String[] split = lines[0].split(" "); 74 | 75 | resouce = split[1]; 76 | 77 | for (int i = 1; i < lines.length; ++i) { 78 | String[] temp = lines[i].split(":"); 79 | headerMap.put(temp[0].trim(), temp[1].trim()); 80 | } 81 | 82 | charBuffer.clear(); 83 | decoder.reset(); 84 | begin = false; 85 | } 86 | 87 | public String getHeader(String key) { 88 | return headerMap.get(key); 89 | } 90 | 91 | public Set getHeaders() { 92 | return headerMap.keySet(); 93 | } 94 | 95 | public String getResouce() { 96 | return resouce; 97 | } 98 | 99 | /** 100 | * 101 | * @return currently, only GET [71,69,84,32],and HEAD [72, 69, 65, 68] is 102 | * supported 103 | */ 104 | public Verb getVerb() { 105 | return verb; 106 | } 107 | 108 | public Version getVersion() { 109 | throw new RuntimeException("not implement yet"); 110 | // return version; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/me/shenfeng/http/ResponceHeaderBuilder.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | import java.util.TreeMap; 6 | 7 | public class ResponceHeaderBuilder { 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 ResponceHeaderBuilder() { 28 | status = OK_200; 29 | } 30 | 31 | public ResponceHeaderBuilder 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 ResponceHeaderBuilder 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/me/shenfeng/http/Util.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.BufferedReader; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.Closeable; 7 | import java.io.File; 8 | import java.io.FileInputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.InputStreamReader; 12 | import java.text.DateFormat; 13 | import java.text.SimpleDateFormat; 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.Date; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.zip.GZIPOutputStream; 21 | 22 | import com.samskivert.mustache.Mustache; 23 | 24 | class FileItem implements Comparable{ 25 | public final String href; 26 | public final String name; 27 | public final String size; 28 | public final String mtime; 29 | 30 | public FileItem(String href, String name, String size, String mtime) { 31 | this.href = href; 32 | this.name = name; 33 | this.size = size; 34 | this.mtime = mtime; 35 | } 36 | @Override 37 | public int compareTo(FileItem o) { 38 | return name.compareTo(o.name); 39 | } 40 | } 41 | 42 | public class Util { 43 | 44 | private static String defaultType = "application/octet-stream"; 45 | private static String mapFile = "mime.types"; 46 | private static String indexTmpl = "index.tpl"; 47 | private static DateFormat df = new SimpleDateFormat("yyyy-HH-dd HH:mm:ss"); 48 | 49 | public static String getExtension(File file) { 50 | String name = file.getName(); 51 | int index = name.lastIndexOf('.'); 52 | if (index != -1) 53 | return name.substring(index + 1).toLowerCase(); 54 | else 55 | return ""; 56 | 57 | } 58 | 59 | public static Object listDir(final File folder) { 60 | File[] files = folder.listFiles(); 61 | final List fileItems = new ArrayList(); 62 | for (File file : files) { 63 | String href = file.isDirectory() ? file.getName() + "/" : file 64 | .getName(); 65 | String mtime = df.format(new Date(file.lastModified())); 66 | fileItems.add(new FileItem(href, file.getName(), 67 | file.length() + "", mtime)); 68 | } 69 | Collections.sort(fileItems); 70 | return new Object() { 71 | Object files = fileItems; 72 | Object dir = folder.getName(); 73 | }; 74 | } 75 | 76 | public static byte[] directoryList(File dir, boolean zip) { 77 | StringBuilder sb = new StringBuilder(300); 78 | 79 | InputStream ins = Util.class.getClassLoader().getResourceAsStream( 80 | indexTmpl); 81 | BufferedReader br = new BufferedReader(new InputStreamReader(ins)); 82 | String line = null; 83 | try { 84 | while ((line = br.readLine()) != null) { 85 | sb.append(line); 86 | sb.append("\n"); 87 | } 88 | } catch (IOException e) { 89 | } 90 | 91 | String html = Mustache.compiler().compile(sb.toString()) 92 | .execute(listDir(dir)); 93 | 94 | if (zip) { 95 | try { 96 | ByteArrayOutputStream baos = new ByteArrayOutputStream(8912); 97 | GZIPOutputStream gzip = new GZIPOutputStream(baos); 98 | gzip.write(html.getBytes()); 99 | closeQuietly(gzip); 100 | return baos.toByteArray(); 101 | } catch (IOException e) { 102 | } 103 | } else { 104 | return html.getBytes(); 105 | } 106 | return new byte[] {}; 107 | } 108 | 109 | public static String getContentType(File file) { 110 | 111 | if (file.isDirectory()) 112 | return "text/html"; 113 | 114 | InputStream ins = Util.class.getClassLoader().getResourceAsStream( 115 | mapFile); 116 | 117 | String exten = getExtension(file); 118 | Map map = new HashMap(); 119 | 120 | try { 121 | BufferedReader bis = new BufferedReader(new InputStreamReader(ins)); 122 | String line = null; 123 | while ((line = bis.readLine()) != null) { 124 | String[] tmp = line.split("\\s+"); 125 | map.put(tmp[0], tmp[1]); 126 | } 127 | } catch (IOException e) { 128 | } 129 | 130 | if (map.get(exten) == null) 131 | return defaultType; 132 | else 133 | return map.get(exten); 134 | 135 | } 136 | 137 | public static void closeQuietly(Closeable is) { 138 | if (is != null) { 139 | try { 140 | is.close(); 141 | } catch (IOException e) { 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * 148 | * @param file 149 | * the absolute file path 150 | * @param zip 151 | * gzip or not 152 | * @return byte array of the file 153 | * 154 | * @throws IOException 155 | */ 156 | public static byte[] file2ByteArray(File file, boolean zip) 157 | throws IOException { 158 | if (file.isFile()) { 159 | InputStream is = null; 160 | GZIPOutputStream gzip = null; 161 | byte[] buffer = new byte[8912]; 162 | ByteArrayOutputStream baos = new ByteArrayOutputStream(8912); 163 | try { 164 | if (zip) { 165 | gzip = new GZIPOutputStream(baos); 166 | } 167 | is = new BufferedInputStream(new FileInputStream(file)); 168 | int read = 0; 169 | while ((read = is.read(buffer)) != -1) { 170 | if (zip) { 171 | gzip.write(buffer, 0, read); 172 | } else { 173 | baos.write(buffer, 0, read); 174 | } 175 | } 176 | } catch (IOException e) { 177 | throw e; 178 | } finally { 179 | closeQuietly(is); 180 | closeQuietly(gzip); 181 | } 182 | return baos.toByteArray(); 183 | } else if (file.isDirectory()) { 184 | return directoryList(file, zip); 185 | } else { 186 | return new byte[] {}; 187 | } 188 | } 189 | 190 | /** 191 | * same as {@link Util#subArray(byte[], byte[], int)},except find from end 192 | * to start; 193 | * 194 | * @param data 195 | * to search from 196 | * @param tofind 197 | * target 198 | * @param start 199 | * start index 200 | * @return index of the first find if find, data.length if not find 201 | */ 202 | public static int subArrayFromEnd(byte[] data, byte[] tofind, int start) { 203 | int index = data.length; 204 | outer: for (int i = data.length - tofind.length; i > 0; --i) { 205 | 206 | for (int j = 0; j < tofind.length;) { 207 | if (data[i] == tofind[j]) { 208 | ++i; 209 | ++j; 210 | if (j == tofind.length) { 211 | index = i - tofind.length; 212 | break outer; 213 | } 214 | } else { 215 | i = i - j; // step back 216 | break; 217 | } 218 | } 219 | } 220 | return index; 221 | } 222 | 223 | /** 224 | * 225 | * @param data 226 | * to search from 227 | * @param tofind 228 | * target 229 | * @param start 230 | * start index 231 | * @return index of the first find if find, data.length if not find 232 | */ 233 | public static int subArray(byte[] data, byte[] tofind, int start) { 234 | int index = data.length; 235 | outer: for (int i = start; i < data.length; ++i) { 236 | 237 | for (int j = 0; j < tofind.length;) { 238 | if (data[i] == tofind[j]) { 239 | ++i; 240 | ++j; 241 | if (j == tofind.length) { 242 | index = i - tofind.length; 243 | break outer; 244 | } 245 | } else { 246 | i = i - j; // step back 247 | break; 248 | } 249 | } 250 | } 251 | return index; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/main/resources/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Direcoty List 5 | 24 | 25 | 26 |

{{dir}}/

27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{#files}} 37 | 38 | 39 | 40 | 41 | 42 | {{/files}} 43 | 44 |
NameSizeLast Modified
{{name}}{{size}}{{mtime}}
45 | 46 | 47 | -------------------------------------------------------------------------------- /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/main/resources/mime.types: -------------------------------------------------------------------------------- 1 | css text/css 2 | scss text/plain 3 | html text/html 4 | htm text/html 5 | js application/x-javascript 6 | git image/gif 7 | png image/png 8 | jpg image/jpeg 9 | jpeg image/jpeg 10 | clj text/plain 11 | conf text/plain 12 | xml text/xml -------------------------------------------------------------------------------- /src/test/java/me/shenfeng/http/RequestHeaderDecoderTest.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 2 | 3 | import me.shenfeng.http.RequestHeaderDecoder.Verb; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | public class RequestHeaderDecoderTest { 10 | 11 | private static String HOST = "www.google.com"; 12 | private static String ACCEPT = "image/png,image/*;q=0.8,*/*;q=0.5"; 13 | private static String END = "\r\n\r\n"; 14 | private String GET = "GET /\r\n" + "Host: " + HOST + "\r\n" + "Accept: " 15 | + ACCEPT + END; 16 | 17 | private RequestHeaderDecoder decoder; 18 | 19 | @Before 20 | public void setup() { 21 | decoder = new RequestHeaderDecoder(); 22 | } 23 | 24 | @Test 25 | public void testDecodeGet() { 26 | byte[] bytes = GET.getBytes(); 27 | boolean b = decoder.appendSegment(bytes); 28 | Assert.assertEquals(b, true); 29 | Assert.assertEquals("/", decoder.getResouce()); 30 | Assert.assertEquals(HOST, decoder.getHeader("Host")); 31 | Assert.assertEquals(ACCEPT, decoder.getHeader("Accept")); 32 | Assert.assertEquals(Verb.GET, decoder.getVerb()); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/me/shenfeng/http/UtilTest.java: -------------------------------------------------------------------------------- 1 | package me.shenfeng.http; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | 5 | import java.io.File; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | public class UtilTest { 11 | 12 | private File f1; 13 | private File f2; 14 | 15 | @Before 16 | public void setUp() { 17 | f1 = new File("/path/to/what.css"); 18 | f2 = new File("/path/to/what"); 19 | } 20 | 21 | @Test 22 | public void testGetExtension() { 23 | assertEquals("css", Util.getExtension(f1)); 24 | assertEquals("", Util.getExtension(f2)); 25 | } 26 | 27 | @Test 28 | public void testGetContentType() { 29 | assertEquals("text/css", Util.getContentType(f1)); 30 | assertEquals("application/octet-stream", Util.getContentType(f2)); 31 | assertEquals("text/html", Util.getContentType(new File("/"))); 32 | } 33 | 34 | @Test 35 | public void testSubArray() { 36 | byte[] s = { 72, 69, 65, 68 }; 37 | byte[] f = { 71, 69, 84, 32 }; 38 | 39 | byte[] data1 = { 71, 69, 84, 72, 69, 65, 68 }; 40 | assertEquals(Util.subArray(data1, s, 0), 3); 41 | assertEquals(Util.subArray(data1, f, 0), data1.length); 42 | 43 | assertEquals(Util.subArrayFromEnd(data1, s, 0), 3); 44 | assertEquals(Util.subArrayFromEnd(data1, f, 0), data1.length); 45 | } 46 | } 47 | --------------------------------------------------------------------------------