• Android 视频投射之NanoHTTPD


    Android 视频投射之NanoHTTPD

    号称用一个java文件实现Http服务器 有必要对其源码及例子进行分析

       1 public abstract class NanoHTTPD {
       2 
       3     //异步执行请求
       4     public interface AsyncRunner {
       5 
       6         void closeAll();
       7 
       8         void closed(ClientHandler clientHandler);
       9 
      10         void exec(ClientHandler code);
      11     }
      12 
      13     //每一个新的连接创建一个线程
      14     public class ClientHandler implements Runnable {
      15 
      16         private final InputStream inputStream;
      17 
      18         private final Socket acceptSocket;
      19 
      20         private ClientHandler(InputStream inputStream, Socket acceptSocket) {
      21             this.inputStream = inputStream;
      22             this.acceptSocket = acceptSocket;
      23         }
      24 
      25         public void close() {
      26             safeClose(this.inputStream);
      27             safeClose(this.acceptSocket);
      28         }
      29 
      30         @Override
      31         public void run() {
      32             OutputStream outputStream = null;
      33             try {
      34                 outputStream = this.acceptSocket.getOutputStream();
      35                 TempFileManager tempFileManager = NanoHTTPD.this.tempFileManagerFactory.create();
      36                 HTTPSession session = new HTTPSession(tempFileManager, this.inputStream, outputStream, this.acceptSocket.getInetAddress());
      37                 while (!this.acceptSocket.isClosed()) {
      38                     session.execute();
      39                 }
      40             } catch (Exception e) {
      41           
      42                 if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage())) && !(e instanceof SocketTimeoutException)) {
      43                     NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
      44                 }
      45             } finally {
      46                 safeClose(outputStream);
      47                 safeClose(this.inputStream);
      48                 safeClose(this.acceptSocket);
      49                 NanoHTTPD.this.asyncRunner.closed(this);
      50             }
      51         }
      52     }
      53 
      54     public static class Cookie {
      55 
      56         public static String getHTTPTime(int days) {
      57             Calendar calendar = Calendar.getInstance();
      58             SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
      59             dateFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
      60             calendar.add(Calendar.DAY_OF_MONTH, days);
      61             return dateFormat.format(calendar.getTime());
      62         }
      63 
      64         private final String n, v, e;
      65 
      66         public Cookie(String name, String value) {
      67             this(name, value, 30);
      68         }
      69 
      70         public Cookie(String name, String value, int numDays) {
      71             this.n = name;
      72             this.v = value;
      73             this.e = getHTTPTime(numDays);
      74         }
      75 
      76         public Cookie(String name, String value, String expires) {
      77             this.n = name;
      78             this.v = value;
      79             this.e = expires;
      80         }
      81 
      82         public String getHTTPHeader() {
      83             String fmt = "%s=%s; expires=%s";
      84             return String.format(fmt, this.n, this.v, this.e);
      85         }
      86     }
      87 
      88     //提供对cookies的支持
      89     public class CookieHandler implements Iterable<String> {
      90 
      91         private final HashMap<String, String> cookies = new HashMap<String, String>();
      92 
      93         private final ArrayList<Cookie> queue = new ArrayList<Cookie>();
      94 
      95         public CookieHandler(Map<String, String> httpHeaders) {
      96             String raw = httpHeaders.get("cookie");
      97             if (raw != null) {
      98                 String[] tokens = raw.split(";");
      99                 for (String token : tokens) {
     100                     String[] data = token.trim().split("=");
     101                     if (data.length == 2) {
     102                         this.cookies.put(data[0], data[1]);
     103                     }
     104                 }
     105             }
     106         }
     107 
     108         //设置cookies存放时间为一个月
     109         public void delete(String name) {
     110             set(name, "-delete-", -30);
     111         }
     112 
     113         @Override
     114         public Iterator<String> iterator() {
     115             return this.cookies.keySet().iterator();
     116         }
     117 
     118         //从HTTP Headers读取Cookie
     119         public String read(String name) {
     120             return this.cookies.get(name);
     121         }
     122 
     123         public void set(Cookie cookie) {
     124             this.queue.add(cookie);
     125         }
     126 
     127         //设置Cookie
     128         public void set(String name, String value, int expires) {
     129             this.queue.add(new Cookie(name, value, Cookie.getHTTPTime(expires)));
     130         }
     131 
     132         public void unloadQueue(Response response) {
     133             for (Cookie cookie : this.queue) {
     134                 response.addHeader("Set-Cookie", cookie.getHTTPHeader());
     135             }
     136         }
     137     }
     138 
     139     //每次进来的请求启动一个新的线程
     140     public static class DefaultAsyncRunner implements AsyncRunner {
     141 
     142         private long requestCount;
     143 
     144         private final List<ClientHandler> running = Collections.synchronizedList(new ArrayList<NanoHTTPD.ClientHandler>());
     145 
     146         //返回目前所有正在运行的客户端
     147         public List<ClientHandler> getRunning() {
     148             return running;
     149         }
     150 
     151         @Override
     152         public void closeAll() {
     153             for (ClientHandler clientHandler : new ArrayList<ClientHandler>(this.running)) {
     154                 clientHandler.close();
     155             }
     156         }
     157 
     158         @Override
     159         public void closed(ClientHandler clientHandler) {
     160             this.running.remove(clientHandler);
     161         }
     162 
     163         @Override
     164         public void exec(ClientHandler clientHandler) {
     165             ++this.requestCount;
     166             Thread t = new Thread(clientHandler);
     167             t.setDaemon(true);
     168             t.setName("NanoHttpd Request Processor (#" + this.requestCount + ")");
     169             this.running.add(clientHandler);
     170             t.start();
     171         }
     172     }
     173 
     174     //默认创建临时文件的策略
     175     public static class DefaultTempFile implements TempFile {
     176 
     177         private final File file;
     178 
     179         private final OutputStream fstream;
     180 
     181         public DefaultTempFile(String tempdir) throws IOException {
     182             this.file = File.createTempFile("NanoHTTPD-", "", new File(tempdir));
     183             this.fstream = new FileOutputStream(this.file);
     184         }
     185 
     186         @Override
     187         public void delete() throws Exception {
     188             safeClose(this.fstream);
     189             if (!this.file.delete()) {
     190                 throw new Exception("could not delete temporary file");
     191             }
     192         }
     193 
     194         @Override
     195         public String getName() {
     196             return this.file.getAbsolutePath();
     197         }
     198 
     199         @Override
     200         public OutputStream open() throws Exception {
     201             return this.fstream;
     202         }
     203     }
     204 
     205     //创建和清除临时文件的策略
     206     public static class DefaultTempFileManager implements TempFileManager {
     207 
     208         private final String tmpdir;
     209 
     210         private final List<TempFile> tempFiles;
     211 
     212         public DefaultTempFileManager() {
     213             this.tmpdir = System.getProperty("java.io.tmpdir");
     214             this.tempFiles = new ArrayList<TempFile>();
     215         }
     216 
     217         @Override
     218         public void clear() {
     219             for (TempFile file : this.tempFiles) {
     220                 try {
     221                     file.delete();
     222                 } catch (Exception ignored) {
     223                     NanoHTTPD.LOG.log(Level.WARNING, "could not delete file ", ignored);
     224                 }
     225             }
     226             this.tempFiles.clear();
     227         }
     228 
     229         @Override
     230         public TempFile createTempFile(String filename_hint) throws Exception {
     231             DefaultTempFile tempFile = new DefaultTempFile(this.tmpdir);
     232             this.tempFiles.add(tempFile);
     233             return tempFile;
     234         }
     235     }
     236 
     237     private class DefaultTempFileManagerFactory implements TempFileManagerFactory {
     238 
     239         @Override
     240         public TempFileManager create() {
     241             return new DefaultTempFileManager();
     242         }
     243     }
     244 
     245     private static final String CONTENT_DISPOSITION_REGEX = "([ |	]*Content-Disposition[ |	]*:)(.*)";
     246 
     247     private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern.compile(CONTENT_DISPOSITION_REGEX, Pattern.CASE_INSENSITIVE);
     248 
     249     private static final String CONTENT_TYPE_REGEX = "([ |	]*content-type[ |	]*:)(.*)";
     250 
     251     private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile(CONTENT_TYPE_REGEX, Pattern.CASE_INSENSITIVE);
     252 
     253     private static final String CONTENT_DISPOSITION_ATTRIBUTE_REGEX = "[ |	]*([a-zA-Z]*)[ |	]*=[ |	]*['|"]([^"^']*)['|"]";
     254 
     255     private static final Pattern CONTENT_DISPOSITION_ATTRIBUTE_PATTERN = Pattern.compile(CONTENT_DISPOSITION_ATTRIBUTE_REGEX);
     256 
     257     protected class HTTPSession implements IHTTPSession {
     258 
     259         private static final int REQUEST_BUFFER_LEN = 512;
     260 
     261         private static final int MEMORY_STORE_LIMIT = 1024;
     262 
     263         public static final int BUFSIZE = 8192;
     264 
     265         private final TempFileManager tempFileManager;
     266 
     267         private final OutputStream outputStream;
     268 
     269         private final BufferedInputStream inputStream;
     270 
     271         private int splitbyte;
     272 
     273         private int rlen;
     274 
     275         private String uri;
     276 
     277         private Method method;
     278 
     279         private Map<String, String> parms;
     280 
     281         private Map<String, String> headers;
     282 
     283         private CookieHandler cookies;
     284 
     285         private String queryParameterString;
     286 
     287         private String remoteIp;
     288 
     289         private String protocolVersion;
     290 
     291         public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream) {
     292             this.tempFileManager = tempFileManager;
     293             this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
     294             this.outputStream = outputStream;
     295         }
     296 
     297         public HTTPSession(TempFileManager tempFileManager, InputStream inputStream, OutputStream outputStream, InetAddress inetAddress) {
     298             this.tempFileManager = tempFileManager;
     299             this.inputStream = new BufferedInputStream(inputStream, HTTPSession.BUFSIZE);
     300             this.outputStream = outputStream;
     301             this.remoteIp = inetAddress.isLoopbackAddress() || inetAddress.isAnyLocalAddress() ? "127.0.0.1" : inetAddress.getHostAddress().toString();
     302             this.headers = new HashMap<String, String>();
     303         }
     304 
     305         //解析发送头
     306         private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers) throws ResponseException {
     307             try {
     308                 // Read the request line
     309                 String inLine = in.readLine();
     310                 if (inLine == null) {
     311                     return;
     312                 }
     313 
     314                 StringTokenizer st = new StringTokenizer(inLine);
     315                 if (!st.hasMoreTokens()) {
     316                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
     317                 }
     318 
     319                 pre.put("method", st.nextToken());
     320 
     321                 if (!st.hasMoreTokens()) {
     322                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
     323                 }
     324 
     325                 String uri = st.nextToken();
     326 
     327                 //从URI中解析参数
     328                 int qmi = uri.indexOf('?');
     329                 if (qmi >= 0) {
     330                     decodeParms(uri.substring(qmi + 1), parms);
     331                     uri = decodePercent(uri.substring(0, qmi));
     332                 } else {
     333                     uri = decodePercent(uri);
     334                 }
     335 
     336                 
     337                 if (st.hasMoreTokens()) {
     338                     protocolVersion = st.nextToken();
     339                 } else {
     340                     protocolVersion = "HTTP/1.1";
     341                     NanoHTTPD.LOG.log(Level.FINE, "no protocol version specified, strange. Assuming HTTP/1.1.");
     342                 }
     343                 String line = in.readLine();
     344                 while (line != null && line.trim().length() > 0) {
     345                     int p = line.indexOf(':');
     346                     if (p >= 0) {
     347                         headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
     348                     }
     349                     line = in.readLine();
     350                 }
     351 
     352                 pre.put("uri", uri);
     353             } catch (IOException ioe) {
     354                 throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
     355             }
     356         }
     357 
     358         /**
     359          * Decodes the Multipart Body data and put it into Key/Value pairs.
     360          */
     361         private void decodeMultipartFormData(String boundary, ByteBuffer fbuf, Map<String, String> parms, Map<String, String> files) throws ResponseException {
     362             try {
     363                 int[] boundary_idxs = getBoundaryPositions(fbuf, boundary.getBytes());
     364                 if (boundary_idxs.length < 2) {
     365                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but contains less than two boundary strings.");
     366                 }
     367 
     368                 final int MAX_HEADER_SIZE = 1024;
     369                 byte[] part_header_buff = new byte[MAX_HEADER_SIZE];
     370                 for (int bi = 0; bi < boundary_idxs.length - 1; bi++) {
     371                     fbuf.position(boundary_idxs[bi]);
     372                     int len = (fbuf.remaining() < MAX_HEADER_SIZE) ? fbuf.remaining() : MAX_HEADER_SIZE;
     373                     fbuf.get(part_header_buff, 0, len);
     374                     ByteArrayInputStream bais = new ByteArrayInputStream(part_header_buff, 0, len);
     375                     BufferedReader in = new BufferedReader(new InputStreamReader(bais, Charset.forName("US-ASCII")));
     376 
     377                     // First line is boundary string
     378                     String mpline = in.readLine();
     379                     if (!mpline.contains(boundary)) {
     380                         throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Content type is multipart/form-data but chunk does not start with boundary.");
     381                     }
     382 
     383                     String part_name = null, file_name = null, content_type = null;
     384                     // Parse the reset of the header lines
     385                     mpline = in.readLine();
     386                     while (mpline != null && mpline.trim().length() > 0) {
     387                         Matcher matcher = CONTENT_DISPOSITION_PATTERN.matcher(mpline);
     388                         if (matcher.matches()) {
     389                             String attributeString = matcher.group(2);
     390                             matcher = CONTENT_DISPOSITION_ATTRIBUTE_PATTERN.matcher(attributeString);
     391                             while (matcher.find()) {
     392                                 String key = matcher.group(1);
     393                                 if (key.equalsIgnoreCase("name")) {
     394                                     part_name = matcher.group(2);
     395                                 } else if (key.equalsIgnoreCase("filename")) {
     396                                     file_name = matcher.group(2);
     397                                 }
     398                             }
     399                         }
     400                         matcher = CONTENT_TYPE_PATTERN.matcher(mpline);
     401                         if (matcher.matches()) {
     402                             content_type = matcher.group(2).trim();
     403                         }
     404                         mpline = in.readLine();
     405                     }
     406 
     407                     // Read the part data
     408                     int part_header_len = len - (int) in.skip(MAX_HEADER_SIZE);
     409                     if (part_header_len >= len - 4) {
     410                         throw new ResponseException(Response.Status.INTERNAL_ERROR, "Multipart header size exceeds MAX_HEADER_SIZE.");
     411                     }
     412                     int part_data_start = boundary_idxs[bi] + part_header_len;
     413                     int part_data_end = boundary_idxs[bi + 1] - 4;
     414 
     415                     fbuf.position(part_data_start);
     416                     if (content_type == null) {
     417                         // Read the part into a string
     418                         byte[] data_bytes = new byte[part_data_end - part_data_start];
     419                         fbuf.get(data_bytes);
     420                         parms.put(part_name, new String(data_bytes));
     421                     } else {
     422                         // Read it into a file
     423                         String path = saveTmpFile(fbuf, part_data_start, part_data_end - part_data_start, file_name);
     424                         if (!files.containsKey(part_name)) {
     425                             files.put(part_name, path);
     426                         } else {
     427                             int count = 2;
     428                             while (files.containsKey(part_name + count)) {
     429                                 count++;
     430                             }
     431                             files.put(part_name + count, path);
     432                         }
     433                         parms.put(part_name, file_name);
     434                     }
     435                 }
     436             } catch (ResponseException re) {
     437                 throw re;
     438             } catch (Exception e) {
     439                 throw new ResponseException(Response.Status.INTERNAL_ERROR, e.toString());
     440             }
     441         }
     442 
     443         /**
     444          * Decodes parameters in percent-encoded URI-format ( e.g.
     445          * "name=Jack%20Daniels&pass=Single%20Malt" ) and adds them to given
     446          * Map. NOTE: this doesn't support multiple identical keys due to the
     447          * simplicity of Map.
     448          */
     449         private void decodeParms(String parms, Map<String, String> p) {
     450             if (parms == null) {
     451                 this.queryParameterString = "";
     452                 return;
     453             }
     454 
     455             this.queryParameterString = parms;
     456             StringTokenizer st = new StringTokenizer(parms, "&");
     457             while (st.hasMoreTokens()) {
     458                 String e = st.nextToken();
     459                 int sep = e.indexOf('=');
     460                 if (sep >= 0) {
     461                     p.put(decodePercent(e.substring(0, sep)).trim(), decodePercent(e.substring(sep + 1)));
     462                 } else {
     463                     p.put(decodePercent(e).trim(), "");
     464                 }
     465             }
     466         }
     467 
     468         @Override
     469         public void execute() throws IOException {
     470             Response r = null;
     471             try {
     472                 // Read the first 8192 bytes.
     473                 // The full header should fit in here.
     474                 // Apache's default header limit is 8KB.
     475                 // Do NOT assume that a single read will get the entire header
     476                 // at once!
     477                 byte[] buf = new byte[HTTPSession.BUFSIZE];
     478                 this.splitbyte = 0;
     479                 this.rlen = 0;
     480 
     481                 int read = -1;
     482                 this.inputStream.mark(HTTPSession.BUFSIZE);
     483                 try {
     484                     read = this.inputStream.read(buf, 0, HTTPSession.BUFSIZE);
     485                 } catch (Exception e) {
     486                     safeClose(this.inputStream);
     487                     safeClose(this.outputStream);
     488                     throw new SocketException("NanoHttpd Shutdown");
     489                 }
     490                 if (read == -1) {
     491                     // socket was been closed
     492                     safeClose(this.inputStream);
     493                     safeClose(this.outputStream);
     494                     throw new SocketException("NanoHttpd Shutdown");
     495                 }
     496                 while (read > 0) {
     497                     this.rlen += read;
     498                     this.splitbyte = findHeaderEnd(buf, this.rlen);
     499                     if (this.splitbyte > 0) {
     500                         break;
     501                     }
     502                     read = this.inputStream.read(buf, this.rlen, HTTPSession.BUFSIZE - this.rlen);
     503                 }
     504 
     505                 if (this.splitbyte < this.rlen) {
     506                     this.inputStream.reset();
     507                     this.inputStream.skip(this.splitbyte);
     508                 }
     509 
     510                 this.parms = new HashMap<String, String>();
     511                 if (null == this.headers) {
     512                     this.headers = new HashMap<String, String>();
     513                 } else {
     514                     this.headers.clear();
     515                 }
     516 
     517                 if (null != this.remoteIp) {
     518                     this.headers.put("remote-addr", this.remoteIp);
     519                     this.headers.put("http-client-ip", this.remoteIp);
     520                 }
     521 
     522                 // Create a BufferedReader for parsing the header.
     523                 BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, this.rlen)));
     524 
     525                 // Decode the header into parms and header java properties
     526                 Map<String, String> pre = new HashMap<String, String>();
     527                 decodeHeader(hin, pre, this.parms, this.headers);
     528 
     529                 this.method = Method.lookup(pre.get("method"));
     530                 if (this.method == null) {
     531                     throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error.");
     532                 }
     533 
     534                 this.uri = pre.get("uri");
     535 
     536                 this.cookies = new CookieHandler(this.headers);
     537 
     538                 String connection = this.headers.get("connection");
     539                 boolean keepAlive = protocolVersion.equals("HTTP/1.1") && (connection == null || !connection.matches("(?i).*close.*"));
     540 
     541                 // Ok, now do the serve()
     542 
     543                 // TODO: long body_size = getBodySize();
     544                 // TODO: long pos_before_serve = this.inputStream.totalRead()
     545                 // (requires implementaion for totalRead())
     546                 r = serve(this);
     547                 // TODO: this.inputStream.skip(body_size -
     548                 // (this.inputStream.totalRead() - pos_before_serve))
     549 
     550                 if (r == null) {
     551                     throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: Serve() returned a null response.");
     552                 } else {
     553                     String acceptEncoding = this.headers.get("accept-encoding");
     554                     this.cookies.unloadQueue(r);
     555                     r.setRequestMethod(this.method);
     556                     r.setGzipEncoding(useGzipWhenAccepted(r) && acceptEncoding != null && acceptEncoding.contains("gzip"));
     557                     r.setKeepAlive(keepAlive);
     558                     r.send(this.outputStream);
     559                 }
     560                 if (!keepAlive || "close".equalsIgnoreCase(r.getHeader("connection"))) {
     561                     throw new SocketException("NanoHttpd Shutdown");
     562                 }
     563             } catch (SocketException e) {
     564                 // throw it out to close socket object (finalAccept)
     565                 throw e;
     566             } catch (SocketTimeoutException ste) {
     567                 // treat socket timeouts the same way we treat socket exceptions
     568                 // i.e. close the stream & finalAccept object by throwing the
     569                 // exception up the call stack.
     570                 throw ste;
     571             } catch (IOException ioe) {
     572                 Response resp = newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
     573                 resp.send(this.outputStream);
     574                 safeClose(this.outputStream);
     575             } catch (ResponseException re) {
     576                 Response resp = newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
     577                 resp.send(this.outputStream);
     578                 safeClose(this.outputStream);
     579             } finally {
     580                 safeClose(r);
     581                 this.tempFileManager.clear();
     582             }
     583         }
     584 
     585         /**
     586          * Find byte index separating header from body. It must be the last byte
     587          * of the first two sequential new lines.
     588          */
     589         private int findHeaderEnd(final byte[] buf, int rlen) {
     590             int splitbyte = 0;
     591             while (splitbyte + 3 < rlen) {
     592                 if (buf[splitbyte] == '
    ' && buf[splitbyte + 1] == '
    ' && buf[splitbyte + 2] == '
    ' && buf[splitbyte + 3] == '
    ') {
     593                     return splitbyte + 4;
     594                 }
     595                 splitbyte++;
     596             }
     597             return 0;
     598         }
     599 
     600         /**
     601          * Find the byte positions where multipart boundaries start. This reads
     602          * a large block at a time and uses a temporary buffer to optimize
     603          * (memory mapped) file access.
     604          */
     605         private int[] getBoundaryPositions(ByteBuffer b, byte[] boundary) {
     606             int[] res = new int[0];
     607             if (b.remaining() < boundary.length) {
     608                 return res;
     609             }
     610 
     611             int search_window_pos = 0;
     612             byte[] search_window = new byte[4 * 1024 + boundary.length];
     613 
     614             int first_fill = (b.remaining() < search_window.length) ? b.remaining() : search_window.length;
     615             b.get(search_window, 0, first_fill);
     616             int new_bytes = first_fill - boundary.length;
     617 
     618             do {
     619                 // Search the search_window
     620                 for (int j = 0; j < new_bytes; j++) {
     621                     for (int i = 0; i < boundary.length; i++) {
     622                         if (search_window[j + i] != boundary[i])
     623                             break;
     624                         if (i == boundary.length - 1) {
     625                             // Match found, add it to results
     626                             int[] new_res = new int[res.length + 1];
     627                             System.arraycopy(res, 0, new_res, 0, res.length);
     628                             new_res[res.length] = search_window_pos + j;
     629                             res = new_res;
     630                         }
     631                     }
     632                 }
     633                 search_window_pos += new_bytes;
     634 
     635                 // Copy the end of the buffer to the start
     636                 System.arraycopy(search_window, search_window.length - boundary.length, search_window, 0, boundary.length);
     637 
     638                 // Refill search_window
     639                 new_bytes = search_window.length - boundary.length;
     640                 new_bytes = (b.remaining() < new_bytes) ? b.remaining() : new_bytes;
     641                 b.get(search_window, boundary.length, new_bytes);
     642             } while (new_bytes > 0);
     643             return res;
     644         }
     645 
     646         @Override
     647         public CookieHandler getCookies() {
     648             return this.cookies;
     649         }
     650 
     651         @Override
     652         public final Map<String, String> getHeaders() {
     653             return this.headers;
     654         }
     655 
     656         @Override
     657         public final InputStream getInputStream() {
     658             return this.inputStream;
     659         }
     660 
     661         @Override
     662         public final Method getMethod() {
     663             return this.method;
     664         }
     665 
     666         @Override
     667         public final Map<String, String> getParms() {
     668             return this.parms;
     669         }
     670 
     671         @Override
     672         public String getQueryParameterString() {
     673             return this.queryParameterString;
     674         }
     675 
     676         private RandomAccessFile getTmpBucket() {
     677             try {
     678                 TempFile tempFile = this.tempFileManager.createTempFile(null);
     679                 return new RandomAccessFile(tempFile.getName(), "rw");
     680             } catch (Exception e) {
     681                 throw new Error(e); // we won't recover, so throw an error
     682             }
     683         }
     684 
     685         @Override
     686         public final String getUri() {
     687             return this.uri;
     688         }
     689 
     690         /**
     691          * Deduce body length in bytes. Either from "content-length" header or
     692          * read bytes.
     693          */
     694         public long getBodySize() {
     695             if (this.headers.containsKey("content-length")) {
     696                 return Integer.parseInt(this.headers.get("content-length"));
     697             } else if (this.splitbyte < this.rlen) {
     698                 return this.rlen - this.splitbyte;
     699             }
     700             return 0;
     701         }
     702 
     703         @Override
     704         public void parseBody(Map<String, String> files) throws IOException, ResponseException {
     705             RandomAccessFile randomAccessFile = null;
     706             try {
     707                 long size = getBodySize();
     708                 ByteArrayOutputStream baos = null;
     709                 DataOutput request_data_output = null;
     710 
     711                 // Store the request in memory or a file, depending on size
     712                 if (size < MEMORY_STORE_LIMIT) {
     713                     baos = new ByteArrayOutputStream();
     714                     request_data_output = new DataOutputStream(baos);
     715                 } else {
     716                     randomAccessFile = getTmpBucket();
     717                     request_data_output = randomAccessFile;
     718                 }
     719 
     720                 // Read all the body and write it to request_data_output
     721                 byte[] buf = new byte[REQUEST_BUFFER_LEN];
     722                 while (this.rlen >= 0 && size > 0) {
     723                     this.rlen = this.inputStream.read(buf, 0, (int) Math.min(size, REQUEST_BUFFER_LEN));
     724                     size -= this.rlen;
     725                     if (this.rlen > 0) {
     726                         request_data_output.write(buf, 0, this.rlen);
     727                     }
     728                 }
     729 
     730                 ByteBuffer fbuf = null;
     731                 if (baos != null) {
     732                     fbuf = ByteBuffer.wrap(baos.toByteArray(), 0, baos.size());
     733                 } else {
     734                     fbuf = randomAccessFile.getChannel().map(FileChannel.MapMode.READ_ONLY, 0, randomAccessFile.length());
     735                     randomAccessFile.seek(0);
     736                 }
     737 
     738                 // If the method is POST, there may be parameters
     739                 // in data section, too, read it:
     740                 if (Method.POST.equals(this.method)) {
     741                     String contentType = "";
     742                     String contentTypeHeader = this.headers.get("content-type");
     743 
     744                     StringTokenizer st = null;
     745                     if (contentTypeHeader != null) {
     746                         st = new StringTokenizer(contentTypeHeader, ",; ");
     747                         if (st.hasMoreTokens()) {
     748                             contentType = st.nextToken();
     749                         }
     750                     }
     751 
     752                     if ("multipart/form-data".equalsIgnoreCase(contentType)) {
     753                         // Handle multipart/form-data
     754                         if (!st.hasMoreTokens()) {
     755                             throw new ResponseException(Response.Status.BAD_REQUEST,
     756                                     "BAD REQUEST: Content type is multipart/form-data but boundary missing. Usage: GET /example/file.html");
     757                         }
     758 
     759                         String boundaryStartString = "boundary=";
     760                         int boundaryContentStart = contentTypeHeader.indexOf(boundaryStartString) + boundaryStartString.length();
     761                         String boundary = contentTypeHeader.substring(boundaryContentStart, contentTypeHeader.length());
     762                         if (boundary.startsWith(""") && boundary.endsWith(""")) {
     763                             boundary = boundary.substring(1, boundary.length() - 1);
     764                         }
     765 
     766                         decodeMultipartFormData(boundary, fbuf, this.parms, files);
     767                     } else {
     768                         byte[] postBytes = new byte[fbuf.remaining()];
     769                         fbuf.get(postBytes);
     770                         String postLine = new String(postBytes).trim();
     771                         // Handle application/x-www-form-urlencoded
     772                         if ("application/x-www-form-urlencoded".equalsIgnoreCase(contentType)) {
     773                             decodeParms(postLine, this.parms);
     774                         } else if (postLine.length() != 0) {
     775                             // Special case for raw POST data => create a
     776                             // special files entry "postData" with raw content
     777                             // data
     778                             files.put("postData", postLine);
     779                         }
     780                     }
     781                 } else if (Method.PUT.equals(this.method)) {
     782                     files.put("content", saveTmpFile(fbuf, 0, fbuf.limit(), null));
     783                 }
     784             } finally {
     785                 safeClose(randomAccessFile);
     786             }
     787         }
     788 
     789         /**
     790          * Retrieves the content of a sent file and saves it to a temporary
     791          * file. The full path to the saved file is returned.
     792          */
     793         private String saveTmpFile(ByteBuffer b, int offset, int len, String filename_hint) {
     794             String path = "";
     795             if (len > 0) {
     796                 FileOutputStream fileOutputStream = null;
     797                 try {
     798                     TempFile tempFile = this.tempFileManager.createTempFile(filename_hint);
     799                     ByteBuffer src = b.duplicate();
     800                     fileOutputStream = new FileOutputStream(tempFile.getName());
     801                     FileChannel dest = fileOutputStream.getChannel();
     802                     src.position(offset).limit(offset + len);
     803                     dest.write(src.slice());
     804                     path = tempFile.getName();
     805                 } catch (Exception e) { // Catch exception if any
     806                     throw new Error(e); // we won't recover, so throw an error
     807                 } finally {
     808                     safeClose(fileOutputStream);
     809                 }
     810             }
     811             return path;
     812         }
     813     }
     814 
     815     /**
     816      * Handles one session, i.e. parses the HTTP request and returns the
     817      * response.
     818      */
     819     public interface IHTTPSession {
     820 
     821         void execute() throws IOException;
     822 
     823         CookieHandler getCookies();
     824 
     825         Map<String, String> getHeaders();
     826 
     827         InputStream getInputStream();
     828 
     829         Method getMethod();
     830 
     831         Map<String, String> getParms();
     832 
     833         String getQueryParameterString();
     834 
     835         /**
     836          * @return the path part of the URL.
     837          */
     838         String getUri();
     839 
     840         /**
     841          * Adds the files in the request body to the files map.
     842          * 
     843          * @param files
     844          *            map to modify
     845          */
     846         void parseBody(Map<String, String> files) throws IOException, ResponseException;
     847     }
     848 
     849     /**
     850      * HTTP Request methods, with the ability to decode a <code>String</code>
     851      * back to its enum value.
     852      */
     853     public enum Method {
     854         GET,
     855         PUT,
     856         POST,
     857         DELETE,
     858         HEAD,
     859         OPTIONS,
     860         TRACE,
     861         CONNECT,
     862         PATCH;
     863 
     864         static Method lookup(String method) {
     865             for (Method m : Method.values()) {
     866                 if (m.toString().equalsIgnoreCase(method)) {
     867                     return m;
     868                 }
     869             }
     870             return null;
     871         }
     872     }
     873 
     874     /**
     875      * HTTP response. Return one of these from serve().
     876      */
     877     public static class Response implements Closeable {
     878 
     879         public interface IStatus {
     880 
     881             String getDescription();
     882 
     883             int getRequestStatus();
     884         }
     885 
     886         /**
     887          * Some HTTP response status codes
     888          */
     889         public enum Status implements IStatus {
     890             SWITCH_PROTOCOL(101, "Switching Protocols"),
     891             OK(200, "OK"),
     892             CREATED(201, "Created"),
     893             ACCEPTED(202, "Accepted"),
     894             NO_CONTENT(204, "No Content"),
     895             PARTIAL_CONTENT(206, "Partial Content"),
     896             REDIRECT(301, "Moved Permanently"),
     897             NOT_MODIFIED(304, "Not Modified"),
     898             BAD_REQUEST(400, "Bad Request"),
     899             UNAUTHORIZED(401, "Unauthorized"),
     900             FORBIDDEN(403, "Forbidden"),
     901             NOT_FOUND(404, "Not Found"),
     902             METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
     903             NOT_ACCEPTABLE(406, "Not Acceptable"),
     904             REQUEST_TIMEOUT(408, "Request Timeout"),
     905             CONFLICT(409, "Conflict"),
     906             RANGE_NOT_SATISFIABLE(416, "Requested Range Not Satisfiable"),
     907             INTERNAL_ERROR(500, "Internal Server Error"),
     908             NOT_IMPLEMENTED(501, "Not Implemented"),
     909             UNSUPPORTED_HTTP_VERSION(505, "HTTP Version Not Supported");
     910 
     911             private final int requestStatus;
     912 
     913             private final String description;
     914 
     915             Status(int requestStatus, String description) {
     916                 this.requestStatus = requestStatus;
     917                 this.description = description;
     918             }
     919 
     920             @Override
     921             public String getDescription() {
     922                 return "" + this.requestStatus + " " + this.description;
     923             }
     924 
     925             @Override
     926             public int getRequestStatus() {
     927                 return this.requestStatus;
     928             }
     929 
     930         }
     931 
     932         /**
     933          * Output stream that will automatically send every write to the wrapped
     934          * OutputStream according to chunked transfer:
     935          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
     936          */
     937         private static class ChunkedOutputStream extends FilterOutputStream {
     938 
     939             public ChunkedOutputStream(OutputStream out) {
     940                 super(out);
     941             }
     942 
     943             @Override
     944             public void write(int b) throws IOException {
     945                 byte[] data = {
     946                     (byte) b
     947                 };
     948                 write(data, 0, 1);
     949             }
     950 
     951             @Override
     952             public void write(byte[] b) throws IOException {
     953                 write(b, 0, b.length);
     954             }
     955 
     956             @Override
     957             public void write(byte[] b, int off, int len) throws IOException {
     958                 if (len == 0)
     959                     return;
     960                 out.write(String.format("%x
    ", len).getBytes());
     961                 out.write(b, off, len);
     962                 out.write("
    ".getBytes());
     963             }
     964 
     965             public void finish() throws IOException {
     966                 out.write("0
    
    ".getBytes());
     967             }
     968 
     969         }
     970 
     971         /**
     972          * HTTP status code after processing, e.g. "200 OK", Status.OK
     973          */
     974         private IStatus status;
     975 
     976         /**
     977          * MIME type of content, e.g. "text/html"
     978          */
     979         private String mimeType;
     980 
     981         /**
     982          * Data of the response, may be null.
     983          */
     984         private InputStream data;
     985 
     986         private long contentLength;
     987 
     988         /**
     989          * Headers for the HTTP response. Use addHeader() to add lines.
     990          */
     991         private final Map<String, String> header = new HashMap<String, String>();
     992 
     993         /**
     994          * The request method that spawned this response.
     995          */
     996         private Method requestMethod;
     997 
     998         /**
     999          * Use chunkedTransfer
    1000          */
    1001         private boolean chunkedTransfer;
    1002 
    1003         private boolean encodeAsGzip;
    1004 
    1005         private boolean keepAlive;
    1006 
    1007         /**
    1008          * Creates a fixed length response if totalBytes>=0, otherwise chunked.
    1009          */
    1010         protected Response(IStatus status, String mimeType, InputStream data, long totalBytes) {
    1011             this.status = status;
    1012             this.mimeType = mimeType;
    1013             if (data == null) {
    1014                 this.data = new ByteArrayInputStream(new byte[0]);
    1015                 this.contentLength = 0L;
    1016             } else {
    1017                 this.data = data;
    1018                 this.contentLength = totalBytes;
    1019             }
    1020             this.chunkedTransfer = this.contentLength < 0;
    1021             keepAlive = true;
    1022         }
    1023 
    1024         @Override
    1025         public void close() throws IOException {
    1026             if (this.data != null) {
    1027                 this.data.close();
    1028             }
    1029         }
    1030 
    1031         /**
    1032          * Adds given line to the header.
    1033          */
    1034         public void addHeader(String name, String value) {
    1035             this.header.put(name, value);
    1036         }
    1037 
    1038         public InputStream getData() {
    1039             return this.data;
    1040         }
    1041 
    1042         public String getHeader(String name) {
    1043             for (String headerName : header.keySet()) {
    1044                 if (headerName.equalsIgnoreCase(name)) {
    1045                     return header.get(headerName);
    1046                 }
    1047             }
    1048             return null;
    1049         }
    1050 
    1051         public String getMimeType() {
    1052             return this.mimeType;
    1053         }
    1054 
    1055         public Method getRequestMethod() {
    1056             return this.requestMethod;
    1057         }
    1058 
    1059         public IStatus getStatus() {
    1060             return this.status;
    1061         }
    1062 
    1063         public void setGzipEncoding(boolean encodeAsGzip) {
    1064             this.encodeAsGzip = encodeAsGzip;
    1065         }
    1066 
    1067         public void setKeepAlive(boolean useKeepAlive) {
    1068             this.keepAlive = useKeepAlive;
    1069         }
    1070 
    1071         private static boolean headerAlreadySent(Map<String, String> header, String name) {
    1072             boolean alreadySent = false;
    1073             for (String headerName : header.keySet()) {
    1074                 alreadySent |= headerName.equalsIgnoreCase(name);
    1075             }
    1076             return alreadySent;
    1077         }
    1078 
    1079         /**
    1080          * Sends given response to the socket.
    1081          */
    1082         protected void send(OutputStream outputStream) {
    1083             String mime = this.mimeType;
    1084             SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
    1085             gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
    1086 
    1087             try {
    1088                 if (this.status == null) {
    1089                     throw new Error("sendResponse(): Status can't be null.");
    1090                 }
    1091                 PrintWriter pw = new PrintWriter(new BufferedWriter(new OutputStreamWriter(outputStream, "UTF-8")), false);
    1092                 pw.print("HTTP/1.1 " + this.status.getDescription() + " 
    ");
    1093 
    1094                 if (mime != null) {
    1095                     pw.print("Content-Type: " + mime + "
    ");
    1096                 }
    1097 
    1098                 if (this.header == null || this.header.get("Date") == null) {
    1099                     pw.print("Date: " + gmtFrmt.format(new Date()) + "
    ");
    1100                 }
    1101 
    1102                 if (this.header != null) {
    1103                     for (String key : this.header.keySet()) {
    1104                         String value = this.header.get(key);
    1105                         pw.print(key + ": " + value + "
    ");
    1106                     }
    1107                 }
    1108 
    1109                 if (!headerAlreadySent(header, "connection")) {
    1110                     pw.print("Connection: " + (this.keepAlive ? "keep-alive" : "close") + "
    ");
    1111                 }
    1112 
    1113                 if (headerAlreadySent(this.header, "content-length")) {
    1114                     encodeAsGzip = false;
    1115                 }
    1116 
    1117                 if (encodeAsGzip) {
    1118                     pw.print("Content-Encoding: gzip
    ");
    1119                     setChunkedTransfer(true);
    1120                 }
    1121 
    1122                 long pending = this.data != null ? this.contentLength : 0;
    1123                 if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
    1124                     pw.print("Transfer-Encoding: chunked
    ");
    1125                 } else if (!encodeAsGzip) {
    1126                     pending = sendContentLengthHeaderIfNotAlreadyPresent(pw, this.header, pending);
    1127                 }
    1128                 pw.print("
    ");
    1129                 pw.flush();
    1130                 sendBodyWithCorrectTransferAndEncoding(outputStream, pending);
    1131                 outputStream.flush();
    1132                 safeClose(this.data);
    1133             } catch (IOException ioe) {
    1134                 NanoHTTPD.LOG.log(Level.SEVERE, "Could not send response to the client", ioe);
    1135             }
    1136         }
    1137 
    1138         private void sendBodyWithCorrectTransferAndEncoding(OutputStream outputStream, long pending) throws IOException {
    1139             if (this.requestMethod != Method.HEAD && this.chunkedTransfer) {
    1140                 ChunkedOutputStream chunkedOutputStream = new ChunkedOutputStream(outputStream);
    1141                 sendBodyWithCorrectEncoding(chunkedOutputStream, -1);
    1142                 chunkedOutputStream.finish();
    1143             } else {
    1144                 sendBodyWithCorrectEncoding(outputStream, pending);
    1145             }
    1146         }
    1147 
    1148         private void sendBodyWithCorrectEncoding(OutputStream outputStream, long pending) throws IOException {
    1149             if (encodeAsGzip) {
    1150                 GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream);
    1151                 sendBody(gzipOutputStream, -1);
    1152                 gzipOutputStream.finish();
    1153             } else {
    1154                 sendBody(outputStream, pending);
    1155             }
    1156         }
    1157 
    1158         /**
    1159          * Sends the body to the specified OutputStream. The pending parameter
    1160          * limits the maximum amounts of bytes sent unless it is -1, in which
    1161          * case everything is sent.
    1162          * 
    1163          * @param outputStream
    1164          *            the OutputStream to send data to
    1165          * @param pending
    1166          *            -1 to send everything, otherwise sets a max limit to the
    1167          *            number of bytes sent
    1168          * @throws IOException
    1169          *             if something goes wrong while sending the data.
    1170          */
    1171         private void sendBody(OutputStream outputStream, long pending) throws IOException {
    1172             long BUFFER_SIZE = 16 * 1024;
    1173             byte[] buff = new byte[(int) BUFFER_SIZE];
    1174             boolean sendEverything = pending == -1;
    1175             while (pending > 0 || sendEverything) {
    1176                 long bytesToRead = sendEverything ? BUFFER_SIZE : Math.min(pending, BUFFER_SIZE);
    1177                 int read = this.data.read(buff, 0, (int) bytesToRead);
    1178                 if (read <= 0) {
    1179                     break;
    1180                 }
    1181                 outputStream.write(buff, 0, read);
    1182                 if (!sendEverything) {
    1183                     pending -= read;
    1184                 }
    1185             }
    1186         }
    1187 
    1188         protected static long sendContentLengthHeaderIfNotAlreadyPresent(PrintWriter pw, Map<String, String> header, long size) {
    1189             for (String headerName : header.keySet()) {
    1190                 if (headerName.equalsIgnoreCase("content-length")) {
    1191                     try {
    1192                         return Long.parseLong(header.get(headerName));
    1193                     } catch (NumberFormatException ex) {
    1194                         return size;
    1195                     }
    1196                 }
    1197             }
    1198 
    1199             pw.print("Content-Length: " + size + "
    ");
    1200             return size;
    1201         }
    1202 
    1203         public void setChunkedTransfer(boolean chunkedTransfer) {
    1204             this.chunkedTransfer = chunkedTransfer;
    1205         }
    1206 
    1207         public void setData(InputStream data) {
    1208             this.data = data;
    1209         }
    1210 
    1211         public void setMimeType(String mimeType) {
    1212             this.mimeType = mimeType;
    1213         }
    1214 
    1215         public void setRequestMethod(Method requestMethod) {
    1216             this.requestMethod = requestMethod;
    1217         }
    1218 
    1219         public void setStatus(IStatus status) {
    1220             this.status = status;
    1221         }
    1222     }
    1223 
    1224     public static final class ResponseException extends Exception {
    1225 
    1226         private static final long serialVersionUID = 6569838532917408380L;
    1227 
    1228         private final Response.Status status;
    1229 
    1230         public ResponseException(Response.Status status, String message) {
    1231             super(message);
    1232             this.status = status;
    1233         }
    1234 
    1235         public ResponseException(Response.Status status, String message, Exception e) {
    1236             super(message, e);
    1237             this.status = status;
    1238         }
    1239 
    1240         public Response.Status getStatus() {
    1241             return this.status;
    1242         }
    1243     }
    1244 
    1245     /**
    1246      * The runnable that will be used for the main listening thread.
    1247      */
    1248     public class ServerRunnable implements Runnable {
    1249 
    1250         private final int timeout;
    1251 
    1252         private IOException bindException;
    1253 
    1254         private boolean hasBinded = false;
    1255 
    1256         private ServerRunnable(int timeout) {
    1257             this.timeout = timeout;
    1258         }
    1259 
    1260         @Override
    1261         public void run() {
    1262             try {
    1263                 myServerSocket.bind(hostname != null ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
    1264                 hasBinded = true;
    1265             } catch (IOException e) {
    1266                 this.bindException = e;
    1267                 return;
    1268             }
    1269             do {
    1270                 try {
    1271                     final Socket finalAccept = NanoHTTPD.this.myServerSocket.accept();
    1272                     if (this.timeout > 0) {
    1273                         finalAccept.setSoTimeout(this.timeout);
    1274                     }
    1275                     final InputStream inputStream = finalAccept.getInputStream();
    1276                     NanoHTTPD.this.asyncRunner.exec(createClientHandler(finalAccept, inputStream));
    1277                 } catch (IOException e) {
    1278                     NanoHTTPD.LOG.log(Level.FINE, "Communication with the client broken", e);
    1279                 }
    1280             } while (!NanoHTTPD.this.myServerSocket.isClosed());
    1281         }
    1282     }
    1283 
    1284     /**
    1285      * A temp file.
    1286      * <p/>
    1287      * <p>
    1288      * Temp files are responsible for managing the actual temporary storage and
    1289      * cleaning themselves up when no longer needed.
    1290      * </p>
    1291      */
    1292     public interface TempFile {
    1293 
    1294         void delete() throws Exception;
    1295 
    1296         String getName();
    1297 
    1298         OutputStream open() throws Exception;
    1299     }
    1300 
    1301     /**
    1302      * Temp file manager.
    1303      * <p/>
    1304      * <p>
    1305      * Temp file managers are created 1-to-1 with incoming requests, to create
    1306      * and cleanup temporary files created as a result of handling the request.
    1307      * </p>
    1308      */
    1309     public interface TempFileManager {
    1310 
    1311         void clear();
    1312 
    1313         TempFile createTempFile(String filename_hint) throws Exception;
    1314     }
    1315 
    1316     /**
    1317      * Factory to create temp file managers.
    1318      */
    1319     public interface TempFileManagerFactory {
    1320 
    1321         TempFileManager create();
    1322     }
    1323 
    1324     /**
    1325      * Maximum time to wait on Socket.getInputStream().read() (in milliseconds)
    1326      * This is required as the Keep-Alive HTTP connections would otherwise block
    1327      * the socket reading thread forever (or as long the browser is open).
    1328      */
    1329     public static final int SOCKET_READ_TIMEOUT = 5000;
    1330 
    1331     /**
    1332      * Common MIME type for dynamic content: plain text
    1333      */
    1334     public static final String MIME_PLAINTEXT = "text/plain";
    1335 
    1336     /**
    1337      * Common MIME type for dynamic content: html
    1338      */
    1339     public static final String MIME_HTML = "text/html";
    1340 
    1341     /**
    1342      * Pseudo-Parameter to use to store the actual query string in the
    1343      * parameters map for later re-processing.
    1344      */
    1345     private static final String QUERY_STRING_PARAMETER = "NanoHttpd.QUERY_STRING";
    1346 
    1347     /**
    1348      * logger to log to.
    1349      */
    1350     private static final Logger LOG = Logger.getLogger(NanoHTTPD.class.getName());
    1351 
    1352     /**
    1353      * Hashtable mapping (String)FILENAME_EXTENSION -> (String)MIME_TYPE
    1354      */
    1355     protected static Map<String, String> MIME_TYPES;
    1356 
    1357     public static Map<String, String> mimeTypes() {
    1358         if (MIME_TYPES == null) {
    1359             MIME_TYPES = new HashMap<String, String>();
    1360             loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/default-mimetypes.properties");
    1361             loadMimeTypes(MIME_TYPES, "META-INF/nanohttpd/mimetypes.properties");
    1362             if (MIME_TYPES.isEmpty()) {
    1363                 LOG.log(Level.WARNING, "no mime types found in the classpath! please provide mimetypes.properties");
    1364             }
    1365         }
    1366         return MIME_TYPES;
    1367     }
    1368 
    1369     private static void loadMimeTypes(Map<String, String> result, String resourceName) {
    1370         try {
    1371             Enumeration<URL> resources = NanoHTTPD.class.getClassLoader().getResources(resourceName);
    1372             while (resources.hasMoreElements()) {
    1373                 URL url = (URL) resources.nextElement();
    1374                 Properties properties = new Properties();
    1375                 InputStream stream = null;
    1376                 try {
    1377                     stream = url.openStream();
    1378                     properties.load(url.openStream());
    1379                 } catch (IOException e) {
    1380                     LOG.log(Level.SEVERE, "could not load mimetypes from " + url, e);
    1381                 } finally {
    1382                     safeClose(stream);
    1383                 }
    1384                 result.putAll((Map) properties);
    1385             }
    1386         } catch (IOException e) {
    1387             LOG.log(Level.INFO, "no mime types available at " + resourceName);
    1388         }
    1389     };
    1390 
    1391     /**
    1392      * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and an
    1393      * array of loaded KeyManagers. These objects must properly
    1394      * loaded/initialized by the caller.
    1395      */
    1396     public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManager[] keyManagers) throws IOException {
    1397         SSLServerSocketFactory res = null;
    1398         try {
    1399             TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    1400             trustManagerFactory.init(loadedKeyStore);
    1401             SSLContext ctx = SSLContext.getInstance("TLS");
    1402             ctx.init(keyManagers, trustManagerFactory.getTrustManagers(), null);
    1403             res = ctx.getServerSocketFactory();
    1404         } catch (Exception e) {
    1405             throw new IOException(e.getMessage());
    1406         }
    1407         return res;
    1408     }
    1409 
    1410     /**
    1411      * Creates an SSLSocketFactory for HTTPS. Pass a loaded KeyStore and a
    1412      * loaded KeyManagerFactory. These objects must properly loaded/initialized
    1413      * by the caller.
    1414      */
    1415     public static SSLServerSocketFactory makeSSLSocketFactory(KeyStore loadedKeyStore, KeyManagerFactory loadedKeyFactory) throws IOException {
    1416         try {
    1417             return makeSSLSocketFactory(loadedKeyStore, loadedKeyFactory.getKeyManagers());
    1418         } catch (Exception e) {
    1419             throw new IOException(e.getMessage());
    1420         }
    1421     }
    1422 
    1423     /**
    1424      * Creates an SSLSocketFactory for HTTPS. Pass a KeyStore resource with your
    1425      * certificate and passphrase
    1426      */
    1427     public static SSLServerSocketFactory makeSSLSocketFactory(String keyAndTrustStoreClasspathPath, char[] passphrase) throws IOException {
    1428         try {
    1429             KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
    1430             InputStream keystoreStream = NanoHTTPD.class.getResourceAsStream(keyAndTrustStoreClasspathPath);
    1431             keystore.load(keystoreStream, passphrase);
    1432             KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    1433             keyManagerFactory.init(keystore, passphrase);
    1434             return makeSSLSocketFactory(keystore, keyManagerFactory);
    1435         } catch (Exception e) {
    1436             throw new IOException(e.getMessage());
    1437         }
    1438     }
    1439 
    1440     /**
    1441      * Get MIME type from file name extension, if possible
    1442      * 
    1443      * @param uri
    1444      *            the string representing a file
    1445      * @return the connected mime/type
    1446      */
    1447     public static String getMimeTypeForFile(String uri) {
    1448         int dot = uri.lastIndexOf('.');
    1449         String mime = null;
    1450         if (dot >= 0) {
    1451             mime = mimeTypes().get(uri.substring(dot + 1).toLowerCase());
    1452         }
    1453         return mime == null ? "application/octet-stream" : mime;
    1454     }
    1455 
    1456     private static final void safeClose(Object closeable) {
    1457         try {
    1458             if (closeable != null) {
    1459                 if (closeable instanceof Closeable) {
    1460                     ((Closeable) closeable).close();
    1461                 } else if (closeable instanceof Socket) {
    1462                     ((Socket) closeable).close();
    1463                 } else if (closeable instanceof ServerSocket) {
    1464                     ((ServerSocket) closeable).close();
    1465                 } else {
    1466                     throw new IllegalArgumentException("Unknown object to close");
    1467                 }
    1468             }
    1469         } catch (IOException e) {
    1470             NanoHTTPD.LOG.log(Level.SEVERE, "Could not close", e);
    1471         }
    1472     }
    1473 
    1474     private final String hostname;
    1475 
    1476     private final int myPort;
    1477 
    1478     private volatile ServerSocket myServerSocket;
    1479 
    1480     private SSLServerSocketFactory sslServerSocketFactory;
    1481 
    1482     private String[] sslProtocols;
    1483 
    1484     private Thread myThread;
    1485 
    1486     /**
    1487      * Pluggable strategy for asynchronously executing requests.
    1488      */
    1489     protected AsyncRunner asyncRunner;
    1490 
    1491     /**
    1492      * Pluggable strategy for creating and cleaning up temporary files.
    1493      */
    1494     private TempFileManagerFactory tempFileManagerFactory;
    1495 
    1496     /**
    1497      * Constructs an HTTP server on given port.
    1498      */
    1499     public NanoHTTPD(int port) {
    1500         this(null, port);
    1501     }
    1502 
    1503     // -------------------------------------------------------------------------------
    1504     // //
    1505     //
    1506     // Threading Strategy.
    1507     //
    1508     // -------------------------------------------------------------------------------
    1509     // //
    1510 
    1511     /**
    1512      * Constructs an HTTP server on given hostname and port.
    1513      */
    1514     public NanoHTTPD(String hostname, int port) {
    1515         this.hostname = hostname;
    1516         this.myPort = port;
    1517         setTempFileManagerFactory(new DefaultTempFileManagerFactory());
    1518         setAsyncRunner(new DefaultAsyncRunner());
    1519     }
    1520 
    1521     /**
    1522      * Forcibly closes all connections that are open.
    1523      */
    1524     public synchronized void closeAllConnections() {
    1525         stop();
    1526     }
    1527 
    1528     /**
    1529      * create a instance of the client handler, subclasses can return a subclass
    1530      * of the ClientHandler.
    1531      * 
    1532      * @param finalAccept
    1533      *            the socket the cleint is connected to
    1534      * @param inputStream
    1535      *            the input stream
    1536      * @return the client handler
    1537      */
    1538     protected ClientHandler createClientHandler(final Socket finalAccept, final InputStream inputStream) {
    1539         return new ClientHandler(inputStream, finalAccept);
    1540     }
    1541 
    1542     /**
    1543      * Instantiate the server runnable, can be overwritten by subclasses to
    1544      * provide a subclass of the ServerRunnable.
    1545      * 
    1546      * @param timeout
    1547      *            the socet timeout to use.
    1548      * @return the server runnable.
    1549      */
    1550     protected ServerRunnable createServerRunnable(final int timeout) {
    1551         return new ServerRunnable(timeout);
    1552     }
    1553 
    1554     /**
    1555      * Decode parameters from a URL, handing the case where a single parameter
    1556      * name might have been supplied several times, by return lists of values.
    1557      * In general these lists will contain a single element.
    1558      * 
    1559      * @param parms
    1560      *            original <b>NanoHTTPD</b> parameters values, as passed to the
    1561      *            <code>serve()</code> method.
    1562      * @return a map of <code>String</code> (parameter name) to
    1563      *         <code>List&lt;String&gt;</code> (a list of the values supplied).
    1564      */
    1565     protected static Map<String, List<String>> decodeParameters(Map<String, String> parms) {
    1566         return decodeParameters(parms.get(NanoHTTPD.QUERY_STRING_PARAMETER));
    1567     }
    1568 
    1569     // -------------------------------------------------------------------------------
    1570     // //
    1571 
    1572     /**
    1573      * Decode parameters from a URL, handing the case where a single parameter
    1574      * name might have been supplied several times, by return lists of values.
    1575      * In general these lists will contain a single element.
    1576      * 
    1577      * @param queryString
    1578      *            a query string pulled from the URL.
    1579      * @return a map of <code>String</code> (parameter name) to
    1580      *         <code>List&lt;String&gt;</code> (a list of the values supplied).
    1581      */
    1582     protected static Map<String, List<String>> decodeParameters(String queryString) {
    1583         Map<String, List<String>> parms = new HashMap<String, List<String>>();
    1584         if (queryString != null) {
    1585             StringTokenizer st = new StringTokenizer(queryString, "&");
    1586             while (st.hasMoreTokens()) {
    1587                 String e = st.nextToken();
    1588                 int sep = e.indexOf('=');
    1589                 String propertyName = sep >= 0 ? decodePercent(e.substring(0, sep)).trim() : decodePercent(e).trim();
    1590                 if (!parms.containsKey(propertyName)) {
    1591                     parms.put(propertyName, new ArrayList<String>());
    1592                 }
    1593                 String propertyValue = sep >= 0 ? decodePercent(e.substring(sep + 1)) : null;
    1594                 if (propertyValue != null) {
    1595                     parms.get(propertyName).add(propertyValue);
    1596                 }
    1597             }
    1598         }
    1599         return parms;
    1600     }
    1601 
    1602     /**
    1603      * Decode percent encoded <code>String</code> values.
    1604      * 
    1605      * @param str
    1606      *            the percent encoded <code>String</code>
    1607      * @return expanded form of the input, for example "foo%20bar" becomes
    1608      *         "foo bar"
    1609      */
    1610     protected static String decodePercent(String str) {
    1611         String decoded = null;
    1612         try {
    1613             decoded = URLDecoder.decode(str, "UTF8");
    1614         } catch (UnsupportedEncodingException ignored) {
    1615             NanoHTTPD.LOG.log(Level.WARNING, "Encoding not supported, ignored", ignored);
    1616         }
    1617         return decoded;
    1618     }
    1619 
    1620     /**
    1621      * @return true if the gzip compression should be used if the client
    1622      *         accespts it. Default this option is on for text content and off
    1623      *         for everything. Override this for custom semantics.
    1624      */
    1625     protected boolean useGzipWhenAccepted(Response r) {
    1626         return r.getMimeType() != null && r.getMimeType().toLowerCase().contains("text/");
    1627     }
    1628 
    1629     public final int getListeningPort() {
    1630         return this.myServerSocket == null ? -1 : this.myServerSocket.getLocalPort();
    1631     }
    1632 
    1633     public final boolean isAlive() {
    1634         return wasStarted() && !this.myServerSocket.isClosed() && this.myThread.isAlive();
    1635     }
    1636 
    1637     /**
    1638      * Call before start() to serve over HTTPS instead of HTTP
    1639      */
    1640     public void makeSecure(SSLServerSocketFactory sslServerSocketFactory, String[] sslProtocols) {
    1641         this.sslServerSocketFactory = sslServerSocketFactory;
    1642         this.sslProtocols = sslProtocols;
    1643     }
    1644 
    1645     /**
    1646      * Create a response with unknown length (using HTTP 1.1 chunking).
    1647      */
    1648     public static Response newChunkedResponse(IStatus status, String mimeType, InputStream data) {
    1649         return new Response(status, mimeType, data, -1);
    1650     }
    1651 
    1652     /**
    1653      * Create a response with known length.
    1654      */
    1655     public static Response newFixedLengthResponse(IStatus status, String mimeType, InputStream data, long totalBytes) {
    1656         return new Response(status, mimeType, data, totalBytes);
    1657     }
    1658 
    1659     /**
    1660      * Create a text response with known length.
    1661      */
    1662     public static Response newFixedLengthResponse(IStatus status, String mimeType, String txt) {
    1663         if (txt == null) {
    1664             return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(new byte[0]), 0);
    1665         } else {
    1666             byte[] bytes;
    1667             try {
    1668                 bytes = txt.getBytes("UTF-8");
    1669             } catch (UnsupportedEncodingException e) {
    1670                 NanoHTTPD.LOG.log(Level.SEVERE, "encoding problem, responding nothing", e);
    1671                 bytes = new byte[0];
    1672             }
    1673             return newFixedLengthResponse(status, mimeType, new ByteArrayInputStream(bytes), bytes.length);
    1674         }
    1675     }
    1676 
    1677     /**
    1678      * Create a text response with known length.
    1679      */
    1680     public static Response newFixedLengthResponse(String msg) {
    1681         return newFixedLengthResponse(Status.OK, NanoHTTPD.MIME_HTML, msg);
    1682     }
    1683 
    1684     /**
    1685      * Override this to customize the server.
    1686      * <p/>
    1687      * <p/>
    1688      * (By default, this returns a 404 "Not Found" plain text error response.)
    1689      * 
    1690      * @param session
    1691      *            The HTTP session
    1692      * @return HTTP response, see class Response for details
    1693      */
    1694     public Response serve(IHTTPSession session) {
    1695         Map<String, String> files = new HashMap<String, String>();
    1696         Method method = session.getMethod();
    1697         if (Method.PUT.equals(method) || Method.POST.equals(method)) {
    1698             try {
    1699                 session.parseBody(files);
    1700             } catch (IOException ioe) {
    1701                 return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
    1702             } catch (ResponseException re) {
    1703                 return newFixedLengthResponse(re.getStatus(), NanoHTTPD.MIME_PLAINTEXT, re.getMessage());
    1704             }
    1705         }
    1706 
    1707         Map<String, String> parms = session.getParms();
    1708         parms.put(NanoHTTPD.QUERY_STRING_PARAMETER, session.getQueryParameterString());
    1709         return serve(session.getUri(), method, session.getHeaders(), parms, files);
    1710     }
    1711 
    1712     /**
    1713      * Override this to customize the server.
    1714      * <p/>
    1715      * <p/>
    1716      * (By default, this returns a 404 "Not Found" plain text error response.)
    1717      * 
    1718      * @param uri
    1719      *            Percent-decoded URI without parameters, for example
    1720      *            "/index.cgi"
    1721      * @param method
    1722      *            "GET", "POST" etc.
    1723      * @param parms
    1724      *            Parsed, percent decoded parameters from URI and, in case of
    1725      *            POST, data.
    1726      * @param headers
    1727      *            Header entries, percent decoded
    1728      * @return HTTP response, see class Response for details
    1729      */
    1730     @Deprecated
    1731     public Response serve(String uri, Method method, Map<String, String> headers, Map<String, String> parms, Map<String, String> files) {
    1732         return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found");
    1733     }
    1734 
    1735     /**
    1736      * Pluggable strategy for asynchronously executing requests.
    1737      * 
    1738      * @param asyncRunner
    1739      *            new strategy for handling threads.
    1740      */
    1741     public void setAsyncRunner(AsyncRunner asyncRunner) {
    1742         this.asyncRunner = asyncRunner;
    1743     }
    1744 
    1745     /**
    1746      * Pluggable strategy for creating and cleaning up temporary files.
    1747      * 
    1748      * @param tempFileManagerFactory
    1749      *            new strategy for handling temp files.
    1750      */
    1751     public void setTempFileManagerFactory(TempFileManagerFactory tempFileManagerFactory) {
    1752         this.tempFileManagerFactory = tempFileManagerFactory;
    1753     }
    1754 
    1755     /**
    1756      * Start the server.
    1757      * 
    1758      * @throws IOException
    1759      *             if the socket is in use.
    1760      */
    1761     public void start() throws IOException {
    1762         start(NanoHTTPD.SOCKET_READ_TIMEOUT);
    1763     }
    1764 
    1765     /**
    1766      * Start the server.
    1767      * 
    1768      * @param timeout
    1769      *            timeout to use for socket connections.
    1770      * @param daemon
    1771      *            start the thread daemon or not.
    1772      * @throws IOException
    1773      *             if the socket is in use.
    1774      */
    1775     public void start(final int timeout, boolean daemon) throws IOException {
    1776         if (this.sslServerSocketFactory != null) {
    1777             SSLServerSocket ss = (SSLServerSocket) this.sslServerSocketFactory.createServerSocket();
    1778             if (this.sslProtocols != null) {
    1779                 ss.setEnabledProtocols(this.sslProtocols);
    1780             } else {
    1781                 ss.setEnabledProtocols(ss.getSupportedProtocols());
    1782             }
    1783             ss.setUseClientMode(false);
    1784             ss.setWantClientAuth(false);
    1785             ss.setNeedClientAuth(false);
    1786             ss.setSoTimeout(timeout);
    1787             this.myServerSocket = ss;
    1788         } else {
    1789             this.myServerSocket = new ServerSocket();
    1790         }
    1791         this.myServerSocket.setReuseAddress(true);
    1792 
    1793         ServerRunnable serverRunnable = createServerRunnable(timeout);
    1794         this.myThread = new Thread(serverRunnable);
    1795         this.myThread.setDaemon(daemon);
    1796         this.myThread.setName("NanoHttpd Main Listener");
    1797         this.myThread.start();
    1798         while (!serverRunnable.hasBinded && serverRunnable.bindException == null) {
    1799             try {
    1800                 Thread.sleep(10L);
    1801             } catch (Throwable e) {
    1802                 // on android this may not be allowed, that's why we
    1803                 // catch throwable the wait should be very short because we are
    1804                 // just waiting for the bind of the socket
    1805             }
    1806         }
    1807         if (serverRunnable.bindException != null) {
    1808             throw serverRunnable.bindException;
    1809         }
    1810     }
    1811 
    1812     /**
    1813      * Starts the server (in setDaemon(true) mode).
    1814      */
    1815     public void start(final int timeout) throws IOException {
    1816         start(timeout, true);
    1817     }
    1818 
    1819     /**
    1820      * Stop the server.
    1821      */
    1822     public void stop() {
    1823         try {
    1824             safeClose(this.myServerSocket);
    1825             this.asyncRunner.closeAll();
    1826             if (this.myThread != null) {
    1827                 this.myThread.join();
    1828             }
    1829         } catch (Exception e) {
    1830             NanoHTTPD.LOG.log(Level.SEVERE, "Could not stop all connections", e);
    1831         }
    1832     }
    1833 
    1834     public final boolean wasStarted() {
    1835         return this.myServerSocket != null && this.myThread != null;
    1836     }
    1837 }

     ServerRunner

     1 public class ServerRunner {
     2 
     3     /**
     4      * logger to log to.
     5      */
     6     private static final Logger LOG = Logger.getLogger(ServerRunner.class.getName());
     7 
     8     public static void executeInstance(NanoHTTPD server) {
     9         try {
    10             server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
    11         } catch (IOException ioe) {
    12             System.err.println("Couldn't start server:
    " + ioe);
    13             System.exit(-1);
    14         }
    15 
    16         System.out.println("Server started, Hit Enter to stop.
    ");
    17 
    18         try {
    19             System.in.read();
    20         } catch (Throwable ignored) {
    21         }
    22 
    23         server.stop();
    24         System.out.println("Server stopped.
    ");
    25     }
    26 
    27     public static <T extends NanoHTTPD> void run(Class<T> serverClass) {
    28         try {
    29             executeInstance(serverClass.newInstance());
    30         } catch (Exception e) {
    31             ServerRunner.LOG.log(Level.SEVERE, "Cound nor create server", e);
    32         }
    33     }
    34 }

    第一个服务器程序

     1 public class HelloServer extends NanoHTTPD {
     2 
     3     /**
     4      * logger to log to.
     5      */
     6     private static final Logger LOG = Logger.getLogger(HelloServer.class.getName());
     7 
     8     public static void main(String[] args) {
     9         ServerRunner.run(HelloServer.class);
    10     }
    11 
    12     public HelloServer() {
    13         super(8080);
    14     }
    15 
    16     @Override
    17     public Response serve(IHTTPSession session) {
    18         Method method = session.getMethod();
    19         String uri = session.getUri();
    20         HelloServer.LOG.info(method + " '" + uri + "' ");
    21 
    22         String msg = "<html><body><h1>Hello server</h1>
    ";
    23         Map<String, String> parms = session.getParms();
    24         if (parms.get("username") == null) {
    25             msg += "<form action='?' method='get'>
    " + "  <p>Your name: <input type='text' name='username'></p>
    " + "</form>
    ";
    26         } else {
    27             msg += "<p>Hello, " + parms.get("username") + "!</p>";
    28         }
    29 
    30         msg += "</body></html>
    ";
    31 
    32         return newFixedLengthResponse(msg);
    33     }
    34 }

    另外一个服务器程序

    DebugServer

     1 public class DebugServer extends NanoHTTPD {
     2 
     3     public static void main(String[] args) {
     4         ServerRunner.run(DebugServer.class);
     5     }
     6 
     7     public DebugServer() {
     8         super(8080);
     9     }
    10 
    11     private void listItem(StringBuilder sb, Map.Entry<String, ? extends Object> entry) {
    12         sb.append("<li><code><b>").append(entry.getKey()).append("</b> = ").append(entry.getValue()).append("</code></li>");
    13     }
    14 
    15     @Override
    16     public Response serve(IHTTPSession session) {
    17         Map<String, List<String>> decodedQueryParameters = decodeParameters(session.getQueryParameterString());
    18 
    19         StringBuilder sb = new StringBuilder();
    20         sb.append("<html>");
    21         sb.append("<head><title>Debug Server</title></head>");
    22         sb.append("<body>");
    23         sb.append("<h1>Debug Server</h1>");
    24 
    25         sb.append("<p><blockquote><b>URI</b> = ").append(String.valueOf(session.getUri())).append("<br />");
    26 
    27         sb.append("<b>Method</b> = ").append(String.valueOf(session.getMethod())).append("</blockquote></p>");
    28 
    29         sb.append("<h3>Headers</h3><p><blockquote>").append(toString(session.getHeaders())).append("</blockquote></p>");
    30 
    31         sb.append("<h3>Parms</h3><p><blockquote>").append(toString(session.getParms())).append("</blockquote></p>");
    32 
    33         sb.append("<h3>Parms (multi values?)</h3><p><blockquote>").append(toString(decodedQueryParameters)).append("</blockquote></p>");
    34 
    35         try {
    36             Map<String, String> files = new HashMap<String, String>();
    37             session.parseBody(files);
    38             sb.append("<h3>Files</h3><p><blockquote>").append(toString(files)).append("</blockquote></p>");
    39         } catch (Exception e) {
    40             e.printStackTrace();
    41         }
    42 
    43         sb.append("</body>");
    44         sb.append("</html>");
    45         return newFixedLengthResponse(sb.toString());
    46     }
    47 
    48     private String toString(Map<String, ? extends Object> map) {
    49         if (map.size() == 0) {
    50             return "";
    51         }
    52         return unsortedList(map);
    53     }
    54 
    55     private String unsortedList(Map<String, ? extends Object> map) {
    56         StringBuilder sb = new StringBuilder();
    57         sb.append("<ul>");
    58         for (Map.Entry<String, ? extends Object> entry : map.entrySet()) {
    59             listItem(sb, entry);
    60         }
    61         sb.append("</ul>");
    62         return sb.toString();
    63     }
    64 }

    项目代码见

    https://github.com/huanyi0723/NanoHTTPDCode/

  • 相关阅读:
    Ubuntu环境下IPython的搭建和使用
    智能移动导游解决方案简介
    企业文化、团队文化与知识共享
    CoinPunk项目介绍
    Insight API开源项目介绍
    比特币Bitcoin源代码安装编译
    Javascript单元测试Unit Testing之QUnit
    Node.js的UnitTest单元测试
    Node.js调试
    Alfresco 4 项目介绍
  • 原文地址:https://www.cnblogs.com/huanyi0723/p/4860402.html
Copyright © 2020-2023  润新知