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