现在作为一个开发人员,http server相关的内容已经是无论如何都要了解的知识了。用curl发一个请求,配置一下apache,部署一个web server对我们来说都不是很难,但要想搞清楚这些背后都发生了什么技术细节还真不是很简单的。所以新的系列将是分享我学习Http Server的过程。

NanoHttpd是Github上的一个开源项目,号称只用一个java文件就能创建一个http server,我将通过分析NanoHttpd的源码解析如何开发自己的HttpServer。Github 地址:https://github.com/NanoHttpd/nanohttpd

在开始前首先简单说明HttpServer的基本要素:

1.能接受HttpRequest并返回HttpResponse

2.满足一个Server的基本特征,能够长时间运行

关于Http协议一般HttpServer都会声明支持Http协议的哪些特性,nanohttpd作为一个轻量级的httpserver只实现了最简单、最常用的功能,不过我们依然可以从中学习很多。

首先看下NanoHttpd类的start函数

  1. public void start() throws IOException {
  2. myServerSocket = new ServerSocket();
  3. myServerSocket.bind((hostname != null) ? new InetSocketAddress(hostname, myPort) : new InetSocketAddress(myPort));
  4. myThread = new Thread(new Runnable() {
  5. @Override
  6. public void run() {
  7. do {
  8. try {
  9. final Socket finalAccept = myServerSocket.accept();
  10. registerConnection(finalAccept);
  11. finalAccept.setSoTimeout(SOCKET_READ_TIMEOUT);
  12. final InputStream inputStream = finalAccept.getInputStream();
  13. asyncRunner.exec(new Runnable() {
  14. @Override
  15. public void run() {
  16. OutputStream outputStream = null;
  17. try {
  18. outputStream = finalAccept.getOutputStream();
  19. TempFileManager tempFileManager = tempFileManagerFactory.create();
  20. HTTPSession session = new HTTPSession(tempFileManager, inputStream, outputStream, finalAccept.getInetAddress());
  21. while (!finalAccept.isClosed()) {
  22. session.execute();
  23. }
  24. } catch (Exception e) {
  25. // When the socket is closed by the client, we throw our own SocketException
  26. // to break the  "keep alive" loop above.
  27. if (!(e instanceof SocketException && "NanoHttpd Shutdown".equals(e.getMessage()))) {
  28. e.printStackTrace();
  29. }
  30. } finally {
  31. safeClose(outputStream);
  32. safeClose(inputStream);
  33. safeClose(finalAccept);
  34. unRegisterConnection(finalAccept);
  35. }
  36. }
  37. });
  38. } catch (IOException e) {
  39. }
  40. } while (!myServerSocket.isClosed());
  41. }
  42. });
  43. myThread.setDaemon(true);
  44. myThread.setName("NanoHttpd Main Listener");
  45. myThread.start();
  46. }

1.创建ServerSocket,bind制定端口

2.创建主线程,主线程负责和client建立连接

3.建立连接后会生成一个runnable对象放入asyncRunner中,asyncRunner.exec会创建一个线程来处理新生成的连接。

4.新线程首先创建了一个HttpSession,然后while(true)的执行httpSession.exec。

这里介绍下HttpSession的概念,HttpSession是java里Session概念的实现,简单来说一个Session就是一次httpClient->httpServer的连接,当连接close后session就结束了,如果没结束则session会一直存在。这点从这里的代码也能看到:如果socket不close或者exec没有抛出异常(异常有可能是client段断开连接)session会一直执行exec方法。

一个HttpSession中存储了一次网络连接中server应该保存的信息,比如:URI,METHOD,PARAMS,HEADERS,COOKIES等。

5.这里accept一个client的socket就创建一个独立线程的server模型是ThreadServer模型,特点是一个connection就会创建一个thread,是比较简单、常见的socket server实现。缺点是在同时处理大量连接时线程切换需要消耗大量的资源,如果有兴趣可以了解更加高效的NIO实现方式。

当获得client的socket后自然要开始处理client发送的httprequest。

Http Request Header的parse:

  1. // Read the first 8192 bytes.
  2. // The full header should fit in here.
  3. // Apache's default header limit is 8KB.
  4. // Do NOT assume that a single read will get the entire header at once!
  5. byte[] buf = new byte[BUFSIZE];
  6. splitbyte = 0;
  7. rlen = 0;
  8. {
  9. int read = -1;
  10. try {
  11. read = inputStream.read(buf, 0, BUFSIZE);
  12. } catch (Exception e) {
  13. safeClose(inputStream);
  14. safeClose(outputStream);
  15. throw new SocketException("NanoHttpd Shutdown");
  16. }
  17. if (read == -1) {
  18. // socket was been closed
  19. safeClose(inputStream);
  20. safeClose(outputStream);
  21. throw new SocketException("NanoHttpd Shutdown");
  22. }
  23. while (read > 0) {
  24. rlen += read;
  25. splitbyte = findHeaderEnd(buf, rlen);
  26. if (splitbyte > 0)
  27. break;
  28. read = inputStream.read(buf, rlen, BUFSIZE - rlen);
  29. }
  30. }

1.读取socket数据流的前8192个字节,因为http协议中头部最长为8192

2.通过findHeaderEnd函数找到header数据的截止位置,并把位置保存到splitbyte内。

  1. if (splitbyte < rlen) {
  2. inputStream.unread(buf, splitbyte, rlen - splitbyte);
  3. }
  4. parms = new HashMap<String, String>();
  5. if(null == headers) {
  6. headers = new HashMap<String, String>();
  7. }
  8. // Create a BufferedReader for parsing the header.
  9. BufferedReader hin = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(buf, 0, rlen)));
  10. // Decode the header into parms and header java properties
  11. Map<String, String> pre = new HashMap<String, String>();
  12. decodeHeader(hin, pre, parms, headers);

1.使用unread函数将之前读出来的body pushback回去,这里使用了pushbackstream,用法比较巧妙,因为一旦读到了header的尾部就需要进入下面的逻辑来判断是否需要再读下去了,而不应该一直读,读到没有数据为止

2.decodeHeader,将byte的header转换为java对象

  1. private int findHeaderEnd(final byte[] buf, int rlen) {
  2. int splitbyte = 0;
  3. while (splitbyte + 3 < rlen) {
  4. if (buf[splitbyte] == '\r' && buf[splitbyte + 1] == '\n' && buf[splitbyte + 2] == '\r' && buf[splitbyte + 3] == '\n') {
  5. return splitbyte + 4;
  6. }
  7. splitbyte++;
  8. }
  9. return 0;
  10. }

1.http协议规定header和body之间使用两个回车换行分割

  1. private void decodeHeader(BufferedReader in, Map<String, String> pre, Map<String, String> parms, Map<String, String> headers)
  2. throws ResponseException {
  3. try {
  4. // Read the request line
  5. String inLine = in.readLine();
  6. if (inLine == null) {
  7. return;
  8. }
  9. StringTokenizer st = new StringTokenizer(inLine);
  10. if (!st.hasMoreTokens()) {
  11. throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Syntax error. Usage: GET /example/file.html");
  12. }
  13. pre.put("method", st.nextToken());
  14. if (!st.hasMoreTokens()) {
  15. throw new ResponseException(Response.Status.BAD_REQUEST, "BAD REQUEST: Missing URI. Usage: GET /example/file.html");
  16. }
  17. String uri = st.nextToken();
  18. // Decode parameters from the URI
  19. int qmi = uri.indexOf('?');
  20. if (qmi >= 0) {
  21. decodeParms(uri.substring(qmi + 1), parms);
  22. uri = decodePercent(uri.substring(0, qmi));
  23. } else {
  24. uri = decodePercent(uri);
  25. }
  26. // If there's another token, it's protocol version,
  27. // followed by HTTP headers. Ignore version but parse headers.
  28. // NOTE: this now forces header names lowercase since they are
  29. // case insensitive and vary by client.
  30. if (st.hasMoreTokens()) {
  31. String line = in.readLine();
  32. while (line != null && line.trim().length() > 0) {
  33. int p = line.indexOf(':');
  34. if (p >= 0)
  35. headers.put(line.substring(0, p).trim().toLowerCase(Locale.US), line.substring(p + 1).trim());
  36. line = in.readLine();
  37. }
  38. }
  39. pre.put("uri", uri);
  40. } catch (IOException ioe) {
  41. throw new ResponseException(Response.Status.INTERNAL_ERROR, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage(), ioe);
  42. }
  43. }

1.Http协议第一行是Method URI HTTP_VERSION

2.后面每行都是KEY:VALUE格式的header

3.uri需要经过URIDecode处理后才能使用

4.uri中如果包含?则表示有param,httprequest的param一般表现为:/index.jsp?username=xiaoming&id=2

下面是处理cookie,不过这里cookie的实现较为简单,所以跳过。之后是serve方法,serve方法提供了用户自己实现httpserver具体逻辑的很好接口。在NanoHttpd中的serve方法实现了一个默认的简单处理功能。

  1. /**
  2. * Override this to customize the server.
  3. * <p/>
  4. * <p/>
  5. * (By default, this delegates to serveFile() and allows directory listing.)
  6. *
  7. * @param session The HTTP session
  8. * @return HTTP response, see class Response for details
  9. */
  10. public Response serve(IHTTPSession session) {
  11. Map<String, String> files = new HashMap<String, String>();
  12. Method method = session.getMethod();
  13. if (Method.PUT.equals(method) || Method.POST.equals(method)) {
  14. try {
  15. session.parseBody(files);
  16. } catch (IOException ioe) {
  17. return new Response(Response.Status.INTERNAL_ERROR, MIME_PLAINTEXT, "SERVER INTERNAL ERROR: IOException: " + ioe.getMessage());
  18. } catch (ResponseException re) {
  19. return new Response(re.getStatus(), MIME_PLAINTEXT, re.getMessage());
  20. }
  21. }
  22. Map<String, String> parms = session.getParms();
  23. parms.put(QUERY_STRING_PARAMETER, session.getQueryParameterString());
  24. return serve(session.getUri(), method, session.getHeaders(), parms, files);
  25. }

这个默认的方法处理了PUT和POST方法,如果不是就返回默认的返回值。

parseBody方法中使用了tmpFile的方法保存httpRequest的content信息,然后处理,具体逻辑就不细说了,不是一个典型的实现。

最后看一下发response的逻辑:

  1. /**
  2. * Sends given response to the socket.
  3. */
  4. protected void send(OutputStream outputStream) {
  5. String mime = mimeType;
  6. SimpleDateFormat gmtFrmt = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
  7. gmtFrmt.setTimeZone(TimeZone.getTimeZone("GMT"));
  8. try {
  9. if (status == null) {
  10. throw new Error("sendResponse(): Status can't be null.");
  11. }
  12. PrintWriter pw = new PrintWriter(outputStream);
  13. pw.print("HTTP/1.1 " + status.getDescription() + " \r\n");
  14. if (mime != null) {
  15. pw.print("Content-Type: " + mime + "\r\n");
  16. }
  17. if (header == null || header.get("Date") == null) {
  18. pw.print("Date: " + gmtFrmt.format(new Date()) + "\r\n");
  19. }
  20. if (header != null) {
  21. for (String key : header.keySet()) {
  22. String value = header.get(key);
  23. pw.print(key + ": " + value + "\r\n");
  24. }
  25. }
  26. sendConnectionHeaderIfNotAlreadyPresent(pw, header);
  27. if (requestMethod != Method.HEAD && chunkedTransfer) {
  28. sendAsChunked(outputStream, pw);
  29. } else {
  30. int pending = data != null ? data.available() : 0;
  31. sendContentLengthHeaderIfNotAlreadyPresent(pw, header, pending);
  32. pw.print("\r\n");
  33. pw.flush();
  34. sendAsFixedLength(outputStream, pending);
  35. }
  36. outputStream.flush();
  37. safeClose(data);
  38. } catch (IOException ioe) {
  39. // Couldn't write? No can do.
  40. }
  41. }

发送response的步骤如下:

1.设置mimeType和Time等内容。

2.创建一个PrintWriter,按照HTTP协议依次开始写入内容

3.第一行是HTTP的返回码

4.然后是content-Type

5.然后是Date时间

6.之后是其他的HTTP Header

7.设置Keep-Alive的Header,Keep-Alive是Http1.1的新特性,作用是让客户端和服务器端之间保持一个长链接。

8.如果客户端指定了ChunkedEncoding则分块发送response,Chunked Encoding是Http1.1的又一新特性。一般在response的body比较大的时候使用,server端会首先发送response的HEADER,然后分块发送response的body,每个分块都由chunk length\r\n和chunk data\r\n组成,最后由一个0\r\n结束。

  1. private void sendAsChunked(OutputStream outputStream, PrintWriter pw) throws IOException {
  2. pw.print("Transfer-Encoding: chunked\r\n");
  3. pw.print("\r\n");
  4. pw.flush();
  5. int BUFFER_SIZE = 16 * 1024;
  6. byte[] CRLF = "\r\n".getBytes();
  7. byte[] buff = new byte[BUFFER_SIZE];
  8. int read;
  9. while ((read = data.read(buff)) > 0) {
  10. outputStream.write(String.format("%x\r\n", read).getBytes());
  11. outputStream.write(buff, 0, read);
  12. outputStream.write(CRLF);
  13. }
  14. outputStream.write(String.format("0\r\n\r\n").getBytes());
  15. }

9.如果没指定ChunkedEncoding则需要指定Content-Length来让客户端指定response的body的size,然后再一直写body直到写完为止。

  1. private void sendAsFixedLength(OutputStream outputStream, int pending) throws IOException {
  2. if (requestMethod != Method.HEAD && data != null) {
  3. int BUFFER_SIZE = 16 * 1024;
  4. byte[] buff = new byte[BUFFER_SIZE];
  5. while (pending > 0) {
  6. int read = data.read(buff, 0, ((pending > BUFFER_SIZE) ? BUFFER_SIZE : pending));
  7. if (read <= 0) {
  8. break;
  9. }
  10. outputStream.write(buff, 0, read);
  11. pending -= read;
  12. }
  13. }
  14. }

最后总结下实现HttpServer最重要的几个部分:

1.能够accept tcp连接并从socket中读取request数据

2.把request的比特流转换成request对象中的对象数据

3.根据http协议的规范处理http request

4.产生http response再写回到socket中传给client。

【转】如何开发自己的HttpServer-NanoHttpd源码解读的更多相关文章

  1. swoft| 源码解读系列二: 启动阶段, swoft 都干了些啥?

    date: 2018-8-01 14:22:17title: swoft| 源码解读系列二: 启动阶段, swoft 都干了些啥?description: 阅读 sowft 框架源码, 了解 sowf ...

  2. 50个Android开发人员必备UI效果源码[转载]

    50个Android开发人员必备UI效果源码[转载] http://blog.csdn.net/qq1059458376/article/details/8145497 Android 仿微信之主页面 ...

  3. 《iOS开发指南》正式出版-源码-样章-目录,欢迎大家提出宝贵意见

    智捷iOS课堂-关东升老师最新作品:<iOS开发指南-从0基础到AppStore上线>正式出版了 iOS架构设计.iOS性能优化.iOS测试驱动.iOS调试.iOS团队协作版本控制.... ...

  4. 微信小程序版博客——开发汇总总结(附源码)

    花了点时间陆陆续续,拼拼凑凑将我的小程序版博客搭建完了,这里做个简单的分享和总结. 整体效果 对于博客来说功能页面不是很多,且有些限制于后端服务(基于ghost博客提供的服务),相关样式可以参考截图或 ...

  5. Lumen开发:lumen源码解读之初始化(4)——服务提供(ServiceProviders)与路由(Routes)

    版权声明:本文为博主原创文章,未经博主允许不得转载. 前面讲了singleton和Middleware,现在来继续讲ServiceProviders和Routes,还是看起始文件bootstrap/a ...

  6. swoft| 源码解读系列一: 好难! swoft demo 都跑不起来怎么破? docker 了解一下呗~

    title: swoft| 源码解读系列一: 好难! swoft demo 都跑不起来怎么破? docker 了解一下呗~description: 阅读 sowft 框架源码, swoft 第一步, ...

  7. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  8. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  9. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  10. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

随机推荐

  1. 撩课-Java每天5道面试题第22天

    141.Spring AOP是什么? AOP:面向切面编程 AOP技术利用一种称为“横切”的技术, 解剖封装的对象内部, 并将那些影响了多个类的公共行为 封装到一个可重用模块, 这样就能减少系统的重复 ...

  2. msql查询指定日期

    今天 select * from 表名 where to_days(时间字段名) = to_days(now()); 昨天 SELECT * FROM 表名 WHERE TO_DAYS( NOW( ) ...

  3. MySQL数据源驱动报错

    报错信息:MySQL数据源驱动报错: 1.mysql8.0以上版本需要连接数据库的JDBC驱动也是8.0版本以上 com.mysql.cj.jdbc.Driver 2.MySQL高版本需要指明是否需要 ...

  4. Linux 目录结构说明

    根目录是整个系统最重要的一个目录,因为不但所有的目录都是由根目录衍生出来的,同时根目录也与开机/还原/系统修 复等动作有关. 由于系统开机时需要特定的开机软件.核心文件.开机所需程序.函数库等等文件数 ...

  5. spss C# 二次开发 学习笔记(六)——Spss统计结果的输出

    Spss的二次开发可以很简单,实例化一个对象,然后启用服务,接着提交命令,最后停止服务. 其中重点为提交命令,针对各种统计功能需求,以及被统计分析的数据内容等,命令的内容可以很复杂,但也可以简单的为一 ...

  6. protobuf版本冲突

    在编译chromium代码的过程中发现,官方推荐使用的版本是ubuntu16.04,但是这个版本的ubuntu比较老旧,一些库都比较老了,但是google自己用的部分却是挺新的,protobuf就是一 ...

  7. Canvas学习:globalCompositeOperation详解

    在默认情况之下,如果在Canvas之中将某个物体(源)绘制在另一个物体(目标)之上,那么浏览器就会简单地把源特体的图像叠放在目标物体图像上面. 简单点讲,在Canvas中,把图像源和目标图像,通过Ca ...

  8. Java快速入门-03-小知识汇总篇(全)

    Java快速入门-03-小知识汇总篇(全) 前两篇介绍了JAVA入门的一系小知识,本篇介绍一些比较偏的,说不定什么时候会用到,有用记得 Mark 一下 快键键 常用快捷键(熟记) 快捷键 快捷键作用 ...

  9. Java基础之创建实例化对象的方式

    Java中创建(实例化)对象的五种方式  1.用new语句直接创建对象,这是最常见的创建对象的方法. 2.通过工厂方法返回对象,如:String str = String.valueOf(23); 3 ...

  10. 购买 In-app Billing 商品

    购买 In-app Billing 商品 一旦你的应用连接上了 Google Play,你就可以初始化内购商品的购买请求了.Google Play 提供了结算接口,可以让用户进入使用他们的支付方式,所 ...