Socket——实现一个简单的静态网页服务器
整体结构就是使用ServerSocket监听一个地址,当有接受到请求之后,解析请求中的资源路径。服务器资源路径存放在项目下的一个目录中,服务器会到这个目录中根据请求的路径去寻找相应的资源。如果找到了则返回该文件内容,否则提示找不到文件。
功能主要分为三块,一块是监听IP和端口号;一块是接受HTTP请求报文,并解析报文;最后是处理和返回响应。
HttpServer.java
package com.oolong.webserver; 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 final int port = 8080; private boolean isShutdown = false; // 表示服务器是否关闭 public void waiting() { ServerSocket serverSocket = null; try { serverSocket = new ServerSocket(port, 1, InetAddress.getByName("127.0.0.1")); } catch (IOException ex) { ex.printStackTrace(); System.exit(1); } // 循环等待请求 while(!isShutdown) { Socket socket = null; InputStream input = null; OutputStream output = null; try { // 从服务器等待队列中获取一个连接 socket = serverSocket.accept(); input = socket.getInputStream(); output = socket.getOutputStream(); // 从输入中解析请求字符串,生成一个请求对象 HttpRequest request = new HttpRequest(input); request.parse(); // 创建一个响应对象返回内容 HttpResponse response = new HttpResponse(output); response.setRequest(request); response.sendStaticResource(); // 关闭socket socket.close(); } catch (Exception ex) { ex.printStackTrace(); } } } public static void main(String[] args) { HttpServer server = new HttpServer(); server.waiting(); } }
这里利用ServerSocket接受请求的Socket:
socket = serverSocket.accept();
然后从请求的Socket中获取到输入流和输出流:
input = socket.getInputStream();
output = socket.getOutputStream();
将输入流交给一个HttpRequest对象处理,进行解析请求:
HttpRequest request = new HttpRequest(input); request.parse();
将解析后的请求对象和输出流交给HttpResponse对象,用来返回响应:
HttpResponse response = new HttpResponse(output); response.setRequest(request); response.sendStaticResource();
下面分别看看请求对象和响应对象的实现。
HttpRequest.java
package com.oolong.webserver; import java.io.IOException; import java.io.InputStream; public class HttpRequest { private InputStream input; private String uri; public HttpRequest(InputStream input) { this.input = input; } public void parse() { // 从socket中读取字符流 StringBuffer requestStr = new StringBuffer(2048); int i; byte[] buffer = new byte[2048]; try { i = input.read(buffer); } catch (IOException ex) { ex.printStackTrace(); i = -1; } for (int j = 0; j < i; j++) { requestStr.append((char) buffer[j]); } System.out.println(requestStr.toString()); uri = parseUri(requestStr.toString()); System.out.println(uri); } private String parseUri(String requestStr) { int index1, index2; index1 = requestStr.indexOf(' '); if (index1 != -1) { index2 = requestStr.indexOf(' ', index1 + 1); if (index2 > index1) { return requestStr.substring(index1 + 1, index2); } } return null; } public String getUri() { return uri; } }
可以看到这个请求对象中主要就是parse()这个解析请求字符的方法,以及parseUri()这个解析URI的方法。
首先看parse(),它创建了一个缓冲区,然后从输入流中读取请求字符串。然后调用parseUri()解析请求路径。
HttpResponse.java
这个是处理响应的类,看起来稍显复杂。其实只是根据请求解析的URI到资源目录中去寻找对应的文件,然后将文件写入Socket的输出流中。
如果没有找到,则输出一段错误信息即可。
package com.oolong.webserver; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; public class HttpResponse { private static final int BUFFER_SIZE = 1024; private HttpRequest request; private OutputStream output; public HttpResponse(OutputStream output) { this.output = output; } public void setRequest(HttpRequest request) { this.request = request; } public void sendStaticResource() throws IOException { byte[] bytes = new byte[BUFFER_SIZE]; FileInputStream fis = null; String filePath = request.getUri() == null ? "" : request.getUri().trim(); // 处理根路径 if (filePath.equals("/")) { filePath = "/index.html"; } try { String page = null; File file = new File(HttpServer.WEB_ROOT, filePath); Long fileLength = file.length(); byte[] fileContent = new byte[fileLength.intValue()]; if (file.exists()) { fis = new FileInputStream(file); fis.read(fileContent); fis.close(); page = new String(fileContent); page = warpMessage("200", page); output.write(page.getBytes()); } else { String errorMessage = warpMessage("404", "404 File Not Found!"); output.write(errorMessage.getBytes()); } } catch(Exception ex) { ex.printStackTrace(); } } private String warpMessage(String statusCode, String message) { return "HTTP/1.1 " + statusCode + "\r\n" + "Content-Type: text/html\r\n" + "Content-Length: " + message.length() + "\r\n" + "\r\n" + message; } public static void main(String[] args) { System.out.println(HttpServer.WEB_ROOT); } }
测试
在项目的根目录下的webroot目录中创建一个简单的index.html页面。
<!DOCTYPE html> <html> <head> <title></title> </head> <body> Hello World! </body> </html>
运行HttpServer。然后在浏览器中输入http://127.0.0.1:8080/ 或者http://127.0.0.1:8080/index.html
可以看到:
如果输入其他地址,如:http://127.0.0.1:8080/home.html
注意
你可能在调试的时候会发现,在浏览器发起一次请求的时候,
socket = serverSocket.accept();
可能会执行两次,这是因为浏览器会自动发起一次对icon的请求。这个是浏览器的特性,与代码无关,不是bug。