Tomcat之手写Servlet服务器
冲冲冲本博客详细的代码已上传至github,所以本篇博客就不将代码写太详细了我们知道,Servlet不是代码,也不是框架,而是一组规范,是一组所谓的Tomcat服务器实现的规范/接口。如果想要写出一个Tomcat,则必须要对Servlet这组规范有非常清晰的了解,我们才能实现它如果只是写一个简单的Web服务器的话,我们很清楚,只需要创建三个类,一个是Response, 另一个是Req...
冲冲冲
本博客详细的代码已上传至github,所以本篇博客就不将代码写太详细了
我们知道,Servlet不是代码,也不是框架,而是一组规范,是一组所谓的Tomcat服务器实现的规范/接口。如果想要写出一个Tomcat,则必须要对Servlet这组规范有非常清晰的了解,我们才能实现它
-
如果只是写一个简单的Web服务器的话,我们很清楚,只需要创建三个类,一个是
Response
, 另一个是Request
,分别负责响应和请求;最后一个类则是HttpServer
负责服务器的运行,接收和解析Request,发送Response -
因为是servlet服务器,所以
Response
和Request
一定要实现ServletResponse
和ServletRequest
,以及自定义的Servlet类要实现Servlet
接口
只能返回静态文件的Http服务器
要点
只能返回静态文件说明了两点:
- 我们需要找到解析出请求的静态文件的路径
- 将请求的静态文件放到输出流中,作为response的一部分输出出去
对于服务器来说,整个流程是这样的:
- HttpServer等待客户端的Request
- HttpServer拿到请求之后解析该Http请求
- 解析之后拿到请求路径
- 通过请求路径匹配自己的资源路径,找到静态文件
- 将静态文件作为response的一部分输出出去
我们需要注意的是,因为Http请求,所以在输出的时候一定要满足Http协议的格式
HttpServer实现
该类无疑有一个main方法,是一个启动类,它所要完成的任务就是等待request,然后解析,处理,再将response返回。下面是等待的过程:
private void await(){
// 获得服务端的socket
ServerSocket serverSocket = null;
String host = "127.0.0.1";
int port = 8080;
try {
// 对该socket进行初始化
serverSocket = new ServerSocket(port, 1, InetAddress.getByName(host));
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
// 等待响应
while (!shutdown){
// 拿到服务端接收的socket
try(Socket socket = serverSocket.accept()) {
// 拿到客户端传给服务端的输入流,并构建输出流
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
// 把客户端的输入流解析为request
Request request = new Request(inputStream);
request.parse();
// 服务端作出响应并把输出流response到客户端
Response response = new Response(outputStream);
response.setRequest(request);
response.sendStaticResource();
String uri = request.getUri();
if (uri != null){
shutdown = uri.equals(SHUTDOWN_COMMAND);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Request类实现
因为是Http请求,Request类最重要的职责就是解析Http请求的URI(PS:不了解HTTP协议请自行百度)。因为我们这只是一个简单的服务器,请求的URI即是我们自己的资源路径
// 将request的字节流变为String
void parse() {
// 读出socket中的字符
StringBuilder request = new StringBuilder(2048);
// read的次数
int i;
// 每次read的缓冲区大小
byte[] buffer = new byte[2048];
try {
i = inputStream.read(buffer);
} catch (IOException e) {
e.printStackTrace();
// i = -1 说明没有读入
i = -1;
}
// 把缓冲buffer中的输入流读入request
for (int j = 0; j < i; j++) {
// 此处必须要转换为char,不然是一堆输入流
request.append((char)buffer[j]);
}
System.out.println(request.toString());
uri = parseUri(request.toString());
}
/**
* 解析HTTP请求中的URI
* @param requestString 请求
* @return URI
*/
private String parseUri(String requestString) {
// POST /uri HTTP/1.1
int index1, index2;
index1 = requestString.indexOf(' ');
// 如果出现该字符串
if (index1 != -1){
index2 = requestString.indexOf(' ', index1 + 1);
if (index1 < index2){
return requestString.substring(index1 + 1, index2);
}
}
return null;
}
Response类的实现
response主要的功能就是拿到request解析好的路径,找到静态资源,并将资源转为二进制到输出流返回给客户端
void sendStaticResource() throws IOException {
byte[] bytes = new byte[2048];
FileInputStream fileInputStream = null;
try {
// 通过父路径(自己设置的) + 子路径(URI)拿到文件
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
// 把文件作为输入流
fileInputStream = new FileInputStream(file);
// 把从文件中读入的字节放到bytes中,缓冲区为BUFFER_SIZE,字节个数为ch
int ch = fileInputStream.read(bytes, 0, BUFFER_SIZE);
while (ch != -1) {
// 把bytes中读到的字节写入输出流中
outputStream.write(bytes, 0, ch);
ch = fileInputStream.read(bytes, 0, BUFFER_SIZE);
}
} else {
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"\r\n" +
"<h1>File Not Found</h1>";
outputStream.write(errorMessage.getBytes());
}
} catch (Exception e){
System.out.println(e.toString());
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
但是上面的代码其实是有错的,如果服务器找到静态资源后,只把静态资源的二进制文件返回给客户端,客户端是无法接收到的,并且会爆出405,原因就是我们返回的文件没有遵循Http协议,具体如何解决,请自行思考,然后参考我在上文中提到的github的代码
可以解析Servlet类的Servlet服务器
要点
可以解析Servlet类和上一个返回静态资源其实没有什么大的不同,主要有三点:
- 因为是servlet服务器,所以
Response
和Request
一定要实现ServletResponse
和ServletRequest
,以及自定义的Servlet类要实现Servlet
接口 - 我们通过Http请求头拿到Servlet类的路径和类名,进而通过反射去拿到Servlet类的实例,之后调用其service方法,完成解析
- 因为资源可分为servlet和静态资源,所以我们分两个类来解析
StaticResourceProcessor
和ServletProcessor
ServletProcessor类的实现
此时的重点就落在了如何拿到Servlet类的实例,
class ServletProcessor {
void process(Request request, Response response) {
String uri = request.getUri();
// 截取URI的最后一个路径作为servletName
String servletName = uri.substring(uri.lastIndexOf('/') + 1);
System.out.println(servletName);
URLClassLoader urlClassLoader = null;
try {
URL[] urls = new URL[1];
URLStreamHandler streamHandler = null;
File classPath = new File(HttpServer.WEB_CONTROLLER);
// 拿到servlet的路径
String repository = String.valueOf(new URL("file", null, classPath.getCanonicalPath() + File.separator));
System.out.println(repository);
urls[0] = new URL(null, repository, streamHandler);
// 通过文件路径去加载urls,进而加载servlet类
urlClassLoader = new URLClassLoader(urls);
} catch (IOException e){
e.printStackTrace();
}
Class clazz = null;
try {
// 获取包名
String packName = PrimitiveServlet.class.getPackage().toString();
System.out.println(packName);
// 此处加载的是.class字节码文件,不是Java文件,同时,不仅仅是文件名,还有 它的包名
clazz = urlClassLoader != null ? urlClassLoader.loadClass(packName.substring(packName.indexOf(" ") + 1) + "." + servletName) : null;
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Servlet servlet;
try {
if (clazz != null) {
servlet = (Servlet) clazz.newInstance();
// 防止servlet调用request和response的其他方法,故产生了外观类
ResponseFacade responseFacade = new ResponseFacade(response);
RequestFacade requestFacade = new RequestFacade(request);
servlet.service(requestFacade, responseFacade);
}
} catch (Exception e) {
e.printStackTrace();
} catch (Throwable e) {
System.out.println(e.toString());
}
}
}
其他类
我们一定要注意,Servlet类,request和response一定要实现servlet接口,同时,因为servlet终归是向输出流中写东西的,所以response需要重写ServletResponse的getWriter方法
@Override
public PrintWriter getWriter() throws IOException {
// 把自动刷新设置为true,对println自动刷新到输出流中
return new PrintWriter(outputStream, true);
}
至于我们自定义的Servlet类,它会先拿到response的输出流,然后向输出流写入数据,再返回给response。则可以这样写:
public class PrimitiveServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("from service");
PrintWriter out = servletResponse.getWriter();
out.println("Hello, Servlet");
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {
}
}
我们可以看到,这个servlet类返回的输出流并没有遵循Http协议,即没有返回响应头,那么会什么接收它的客户端还可以正常解析呢?
因为Servlet规范已经定义好了呀,当我们实现Servlet的时候,就可以不用自己去实现 HTTP 协议报文了
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)