Java实现能获取静态资源的简易版服务器(类Tomcat)
我们平时使用Tomcat
做为服务器进行Web
开发,通常会输入http://localhost:8888/index.html
这种URL
,这种请求的Request Headers
如下:
GET /index.html HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36
Sec-Fetch-User: ?1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
从Request Headers
可以得到请求想要访问的静态资源是index.html
(我们自己实现服务器,浏览器发起请求,服务器后台得到就是Request Headers
这种数据 )。
现在我们知道如何获取请求想要的资源了(从Request Headers
获取),这就好办了。
当用户在浏览器中输入URL
发起请求,其实就是与服务器先建立网络连接,再获取服务器上的资源。
这就和Java网络编程有关系了,现在说一说服务器主要的任务:
- 服务器首先要打开
ServerSocket
,当有客户端(浏览器)进行连接,并且发起请求后,获得该请求的Request Headers
(你可以把浏览器当作我们之前实现的多人聊天室里面的客户端,因为浏览器向服务器发起请求,数据也是通过流Stream
或者类似的组件实现的)。 - 服务器获得用户请求的
Request Headers
后,然后从Request Headers
中分析出资源名称。 - 知道用户想要的资源后,服务器将资源与一些必要信息打包发送给客户端(协议、状态码、资源等),当资源不存在时,服务器返回
404.html
资源。
Bootstrap
类,启动服务器的模块。
import connector.Connector;
public final class Bootstrap {
public static void main(String[] args) {
Connector connector = new Connector();
connector.start();
}
}
Connector
类,创建ServerSocket
(用于客户端建立连接)、创建Request
(从Request Headers
中获取客户端请求的资源)、创建Response
(将协议、状态码、资源等数据打包写入流中)、创建StaticProcessor
(借助Response
将资源与一些必要信息发送给服务器),代码注释应该非常清楚。
package connector;
import processor.StaticProcessor;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class Connector implements Runnable {
private static final int DEFAULT_PORT = 8888;
private ServerSocket server;
private int port;
public Connector(){
this(DEFAULT_PORT);
}
public Connector(int port) {
this.port = port;
}
public void start(){
new Thread(this).start();
}
@Override
public void run() {
try {
// 创建ServerSocket,绑定、监听端口
server = new ServerSocket(port);
System.out.println("启动服务器,监听端口:" + port);
while(true){
// 等待客户端连接
Socket socket = server.accept();
// 获取输入流
InputStream input = socket.getInputStream();
// 获取输出流
OutputStream output = socket.getOutputStream();
// 创建请求request,并且传入输入流(有客户端请求的信息)
Request request = new Request(input);
// request通过输入流的信息,分析出客户端想要的资源
request.parse();
// 创建响应response,并且传入输出流(方便将获取的资源发送给客户端)
Response response = new Response(output);
// response需要request的uri(客户端请求的资源)
response.setRequest(request);
// 创建处理者processor
StaticProcessor processor = new StaticProcessor();
// processor通过response把数据发送给客户端
processor.process(response);
//关闭socket
close(socket);
}
} catch (IOException e) {
// 浏览器可以识别状态码,当状态码表示请求不成功时(如404),似乎会断开socket,所以这里不进行处理
} finally{
close(server);
}
}
private void close(Closeable closeable){
if(closeable != null){
try {
closeable.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Request
类,从客户端与服务器建立连接的socket
的输入流InputStream
中读取信息,并且分析出客户端想要的资源。
package connector;
import java.io.IOException;
import java.io.InputStream;
public class Request {
private static final int BUFFER_SIZE = 1024;
private InputStream input;
private String uri;
public Request(InputStream input) {
this.input = input;
}
public String getUri() {
return uri;
}
public void parse(){
int length = 0;
byte[] buffer = new byte[BUFFER_SIZE];
try {
// 读取流里面的数据,并且记录长度
length = input.read(buffer);
} catch (IOException e) {
e.printStackTrace();
}
// 将数据转化成StringBuilder
StringBuilder request = new StringBuilder();
for (int i = 0; i < length; i++) {
request.append((char) buffer[i]);
}
// 分析出客户端想要的资源
uri = parseUri(request.toString());
}
/**
*从 “GET /index.html HTTP/1.1
* ......
* ”中获取index.html
* 通过空格来分离出来
* */
public String parseUri(String request){
int index1 , index2;
// 第一个空格的位置
index1 = request.indexOf(' ');
if(index1 != -1){
// 第二个空格的位置
index2 = request.indexOf(' ', index1+1);
if(index2 != -1){
// 分离出资源名称
return request.substring(index1 + 2 , index2);
}
}
// 没有办法解析出uri
return "";
}
}
Response
类,将协议、状态码、资源等数据打包写入流中。
package connector;
import java.io.*;
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 {
try{
// 通过request的uri,获取资源的路径
String filePath = getClass()
.getClassLoader()
.getResource(request.getUri()).getFile();
// 创建资源文件
File file = new File(filePath.substring(1 , filePath.length()));
// 将资源写入流里面,HttpStatus.SC_OK是状态码
write(file , HttpStatus.SC_OK);
} catch (Exception e) {
// 当出现错误时,简单处理 ,发送404.html给客户端
String errorFilePath = getClass().getClassLoader().getResource("404.html").getFile();
// 将资源写入流里面,HttpStatus.SC_NOT_FOUND是状态码
write(new File(errorFilePath.substring(1 , errorFilePath.length())) ,
HttpStatus.SC_NOT_FOUND);
}
}
private void write(File resource , HttpStatus status) throws IOException {
try(FileInputStream fis = new FileInputStream(resource)){
// 先将协议、状态码等必要信息写入流中,ConnectorUtils是工具类
output.write(ConnectorUtils.renderStatus(status).getBytes());
byte[] buffer = new byte[BUFFER_SIZE];
int length = 0;
// 把资源文件写入流中
while((length = fis.read(buffer , 0 , BUFFER_SIZE)) != -1){
output.write(buffer , 0 ,length);
}
}
}
}
HttpStatus
类,状态码枚举类。
package connector;
public enum HttpStatus {
SC_OK(200 , "OK"),
SC_NOT_FOUND(404 , "File Not Found");
private int statusCode;
private String reason;
HttpStatus(int statusCode, String reason) {
this.statusCode = statusCode;
this.reason = reason;
}
public int getStatusCode() {
return statusCode;
}
public String getReason() {
return reason;
}
}
ConnectorUtils
类(工具类),将协议、状态码等信息组装成浏览器可以识别的信息。
package connector;
public class ConnectorUtils {
public static final String PROTOCOL = "HTTP/1.1";
public static final String CARRIAGE = "\r";
public static final String NEWLINE = "\n";
public static final String SPACE = " ";
public static String renderStatus(HttpStatus status){
StringBuilder sb = new StringBuilder(PROTOCOL)
.append(SPACE)
.append(status.getStatusCode())
.append(SPACE)
.append(status.getReason())
.append(CARRIAGE).append(NEWLINE)
.append(CARRIAGE).append(NEWLINE);
return sb.toString();
}
}
StaticProcessor
类,做为静态资源处理者的身份,借助Response
进行处理。
package processor;
import connector.Response;
import java.io.IOException;
public class StaticProcessor {
public void process(Response response){
try {
response.sendStaticResource();
} catch (IOException e) {
// 不处理浏览器断开连接等错误
}
}
}
这里我们便完成了一个Java实现的能获取静态资源的简易版服务器。
项目结构
项目源码
测试
请求成功,服务器有资源。
请求失败,服务器没有资源。
大家可以动手试一试,不用ServerSocket
来实现,用NIO模型或者AIO模型的组件来实现。
如果有说错的地方,请大家不吝赐教(记得留言哦~~~~)。