「How Tomcat works」这本书的第一个例子是读取文件并输出,如果读取的文件符合 HTTP 协议格式,那么就可以输出到浏览器并展示。
按照这个思路就可以实现一个简单的 HTTP 服务器,服务器的页面以 HTTP 格式保存在磁盘上,虽然这个 demo 并没有什么实际的用途,但是对于理解 HTTP 协议及 HTTP 服务器的实现有很大帮助,本文记录下实现,希望能有所启发。
如何实现 因为将协议数据全部保存到文件中,因此代码中并不对 HTTP 协议做任何处理。
Request Request 只需要从请求的输入流中解析出 uri 即可,用于定位要访问的页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import java.io.IOException;import java.io.InputStream;public class Request { private InputStream input; private String uri; public Request (InputStream input) { this .input = input; } public void parse () { StringBuffer request = new StringBuffer(2048 ); int i; byte [] buffer = new byte [2048 ]; try { i = input.read(buffer); } catch (IOException e) { e.printStackTrace(); i = -1 ; } for (int j = 0 ; j < i; j++) { request.append((char ) buffer[j]); } System.out.print(request.toString()); uri = parseUri(request.toString()); } private String parseUri (String requestString) { int index1, index2; index1 = requestString.indexOf(' ' ); if (index1 != -1 ) { index2 = requestString.indexOf(' ' , index1 + 1 ); if (index2 > index1) return requestString.substring(index1 + 1 , index2); } return null ; } public String getUri () { return uri; } }
Response Response 负责将对应的内容输出到输出流,为了实现简单它还负责了读取文件的职责。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 import java.io.File;import java.io.FileInputStream;import java.io.IOException;import java.io.OutputStream;public class Response { private static final int BUFFER_SIZE = 1024 ; Request request; OutputStream output; public Response (OutputStream output) { this .output = output; } public void setRequest (Request request) { this .request = request; } public void sendStaticResource () throws IOException { byte [] bytes = new byte [BUFFER_SIZE]; FileInputStream fis = null ; try { File file = new File(HttpServer.WEB_ROOT, request.getUri()); if (file.exists()) { fis = new FileInputStream(file); int ch = fis.read(bytes, 0 , BUFFER_SIZE); while (ch != -1 ) { output.write(bytes, 0 , ch); ch = fis.read(bytes, 0 , BUFFER_SIZE); } } else { String errorMessage = "HTTP/1.1 404 File Not Found\r\n" + "Content-Type: text/html\r\n" + "Content-Length: 23\r\n" + "\r\n" + "<h1>File Not Found</h1>" ; output.write(errorMessage.getBytes()); } } catch (Exception e) { System.out.println(e.toString()); } finally { if (fis != null ){ fis.close(); } } } }
HttpServer 核心的 HttpServer 负责启动 ServerSocket 并绑定到指定端口上,监听到请求后创建 Socket,获取 InputStream 构建 Request 对象,完成 Request 对象解析和 Response 对象构建,之后读取文件并输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 import java.io.File;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.net.InetAddress;import java.net.ServerSocket;import java.net.Socket;public class HttpServer { public static final String WEB_ROOT = System.getProperty("user.dir" ) + File.separator + "webroot" ; private static final String SHUTDOWN_COMMAND = "/SHUTDOWN" ; private static final String DEFAULT_HOST = "127.0.0.1" ; private boolean shutdown = false ; public static void main (String[] args) { String host = DEFAULT_HOST; if (args.length > 0 && args[0 ].equals("-s" )) { host = NetUtils.getFirstLocalIp(); } System.out.println("host: " + host); HttpServer server = new HttpServer(); server.await(host); } public void await (String host) { ServerSocket serverSocket = null ; int port = 8080 ; try { serverSocket = new ServerSocket(port, 1 , InetAddress.getByName(host)); } catch (IOException e) { e.printStackTrace(); System.exit(1 ); } while (!shutdown) { Socket socket = null ; InputStream input = null ; OutputStream output = null ; try { socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); Request request = new Request(input); request.parse(); Response response = new Response(output); response.setRequest(request); response.sendStaticResource(); socket.close(); shutdown = request.getUri().equals(SHUTDOWN_COMMAND); } catch (Exception e) { e.printStackTrace(); continue ; } } } }
NetUtils 为了获取到网卡 IP 地址,需要用到一个 NetUtils 工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.net.InetAddress;import java.net.NetworkInterface;import java.net.SocketException;import java.util.ArrayList;import java.util.Enumeration;import java.util.List;public class NetUtils { public static String getFirstLocalIp () { List<String> allNoLoopbackAddresses = getAllLocalIp(); if (allNoLoopbackAddresses.isEmpty()) { throw new IllegalStateException("Sorry, seems you don't have a network card :( " ); } return allNoLoopbackAddresses.get(allNoLoopbackAddresses.size() - 1 ); } public static List<String> getAllLocalIp () { List<String> noLoopbackAddresses = new ArrayList<>(); List<InetAddress> allInetAddresses = getAllLocalAddress(); for (InetAddress address : allInetAddresses) { if (!address.isLoopbackAddress() && !address.isLinkLocalAddress()) { noLoopbackAddresses.add(address.getHostAddress()); } } return noLoopbackAddresses; } public static List<InetAddress> getAllLocalAddress () { try { Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); List<InetAddress> addresses = new ArrayList<>(); while (networkInterfaces.hasMoreElements()) { NetworkInterface networkInterface = networkInterfaces.nextElement(); Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses(); while (inetAddresses.hasMoreElements()) { InetAddress inetAddress = inetAddresses.nextElement(); addresses.add(inetAddress); } } return addresses; } catch (SocketException e) { throw new RuntimeException(e.getMessage(), e); } } }
如何运行 将上述代码放到一个目录下,执行 javac HttpServer.java
编译,不出意外的会生成所有的字节码,所有文件如下
1 2 HttpServer.class NetUtils.class Request.class Response.class webroot HttpServer.java NetUtils.java Request.java Response.java
其中 webroot 是一个目录,因为程序中定义了资源文件的存放路径为
1 System.getProperty("user.dir" ) + File.separator + "webroot"
如果不设置 user.dir
系统属性,那么默认是在当前目录下。
接下来在 webroot 目录下创建 HTTP 协议文件,如下是我定义的一个 json 格式的返回数据文件 tip.json
1 2 3 4 5 6 7 8 HTTP/1.1 200 SUCCESS Content-Type: text/json;charset=utf-8 Content-Length: 48 { "code" : 0 , "msg" : "记得写周报" }
执行 java HttpServer
命令启动服务
1 2 ➜ 1 java HttpServer host: 127.0 .0 .1
然后在浏览器中输入 http://127.0.0.1:8080/tip.json
,你会看到一个 json 格式的输出
如果想对外提供服务,你可以输入 java HttpServe -s
启动服务,然后就会看到服务绑定到了一个对外的 IP
1 2 ➜ 1 java HttpServer -s host: 192.168 .1 .4
HTTP 协议格式 这里说的是 HTTP 1.1 的协议格式,它使用的是文本格式
如下是一个 HTTP 响应的格式
它主要包含 3 部分数据:
状态行。包含协议版本号、code、状态 响应报头。这里有响应内容的格式,响应内容的长度 响应报文主体。这是真正的业务内容 每一行数据使用 \r\n
作为结尾。