重构
在之前一讲里,我们已经构建了一个简单的Web服务器,可以很好的处理静态的网页,而我们的目标是希望能够写一个tomcat,简单的来说是可以处理那些java写的servlet程序,其实tomcat也可以被称为servlet容器。在我们为程序添加这部分功能之前,我们需要进行一次重构。
我想作为程序员的你,对于重构这个词肯定应该不会陌生。在一个项目开发的不同阶段,由于性能、功能乃至其他原因,我们都需要对我们的程序进行不断的调整,在重构之前,我们先对我们目前程序的逻辑做一个梳理。我们的程序主要功能可以分为如下三大块:
1 接收客户端的请求
创建一个ServerSocket对象,持续接收客户端发来的Socket连接,我们可以将这部分称为连接器,算作整个程序的“前端”部分;
2 处理客户端的请求
通过前面得到的Socket对象,取得其中的InputStream对象,然后可以读取HTTP的协议内容,目前我们是需要读取客户端请求的文件名称,我们可以将这部分称为服务器的请求部分。
3 响应客户端的请求
根据得到的Socket对象,取得其中的OutputStream对象,根据前面客户端处理的请求的结果,目前就是客户端请求的文件路径,调用OutputStream对象的write方法写回给客户端,我们可以将这部分称为服务器的响应部分。
在我们之前的程序中,对应客户端请求,我们写了一个方法为parseUrl方法,用于解析HTTP协议中文件路径,对应客户端的响应部分,我们写了一个方法为processStaticResource方法,用于将指定的文件内容以二进制数组的形式发回给浏览器。从程序设计的思想上来看,我们采用的是最为直接的过程式思想,整个程序的运行“核心”是方法(函数),而另一种面向对象的编程思想我想你也应该不陌生,而关于这两种思想的优劣就不再我们的讨论范围。单就我们目前的程序的复杂程度上来说,我们使用面向对象的思想来重构,也许你觉得未必就有多好,甚至还会觉得更麻烦(确实,我第一次这么做的时候也有这样的感受),但请相信我,当我们再往后两天的话,你一定会有新的感受。
好,现在我们就开始重构我们的代码。
首先根据前面的分析,我们可以根据三块功能,分别对应为三个对象:连接器对象、请求对象和响应对象,这里本着简单的原则,因为我们目前还并为给连接器有什么单独的处理,所以暂时就先放在main方法中,保持原有的逻辑,而将请求对象和响应对象单独分离开来。
请求对象,是直接的反馈用户请求的内容,比如需要显示的文件的名称等,具体的代码如下:
package com.tomcat;
import java.io.IOException;
import java.io.InputStream;
public class Request {
private String filePath;
private InputStream inputStream;
public Request(InputStream inputStream){
this.inputStream = inputStream;
}
public void parse(){
StringBuilder stringBuilder = new StringBuilder();
byte[] bytes = new byte[Constants.BufferedSize];
int length = 0;
try {
length = inputStream.read(bytes);
} catch (IOException e) {
length = -1;
}
for(int i=0;i<length;i++){
stringBuilder.append((char)bytes[i]);
}
parseUri(stringBuilder.toString());
}
private void parseUri(String requestHeader){
filePath = null;
int index1 = 0;
index1 = requestHeader.indexOf(' ');
if(index1>-1){
int index2 = requestHeader.indexOf(' ', index1+1);
if(index2>-1){
filePath = requestHeader.substring(index1+1, index2);
}
}
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public InputStream getInputStream() {
return inputStream;
}
public void setInputStream(InputStream inputStream) {
this.inputStream = inputStream;
}
}
Request类有两个变量:
InputStream:从连接器的Socket对象中获得的数据输入流对象;
FilePath:客户端请求的文件路径;
Request类有一个私有的方法:
parseUri:用于从客户端的HTTP协议内容中提取客户端请求文件的路径,与前面一讲的方法其实是一致的。
Request类有一个重要的公用的方法:
parse:用于从InputStream中读取客户端的HTTP协议内容,并转换为字符串,然后调用内部私有parseUri方法获得最终客户端请求文件的路径。
另外,Request类还对外公开了两个变量的set、get方法,这里就不再多说。
关于方法的私有还是公开的,这个还涉及了很多面向对象的知识,可以多阅读相关的书籍并多实践,这里如果你觉得我这样的设计有任何不妥也欢迎随时指正。
好,我们下面接着来说响应对象,主要是根据之前的请求对象响应用户的请求,比如把用户请求的文件以文件流的方式发回给客户端,具体的代码如下:
package com.tomcat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
public class Response {
private OutputStream outputStream;
private Request request;
public Response(OutputStream outputStream,Request request){
this.outputStream = outputStream;
this.request = request;
}
public void sendStaticResource() throws IOException{
if(request.getFilePath()!=null){
File file = new File(Constants.WEB_ROOT, request.getFilePath());
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(file);
byte[] bytesForFile = new byte[Constants.BufferedSize];
int ch = fileInputStream.read(bytesForFile, 0, Constants.BufferedSize);
while(ch!=-1){
outputStream.write(bytesForFile,0,ch);
ch = fileInputStream.read(bytesForFile, 0, Constants.BufferedSize);
}
} catch (FileNotFoundException e) {
System.out.println(e.getMessage());
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>";
outputStream.write(errorMessage.getBytes());
} finally{
try {
if(fileInputStream!=null){
fileInputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public OutputStream getOutputStream() {
return outputStream;
}
public void setOutputStream(OutputStream outputStream) {
this.outputStream = outputStream;
}
public Request getRequest() {
return request;
}
public void setRequest(Request request) {
this.request = request;
}
}
Response类有两个属性:
OutputStream:从连接器的socket对象中取得的数据输出流,使用这个对象的write方法向客户端写入文件;
Request:目前我们发送静态文件的时候,只需要一个文件路径即可,但今后我们还需要接收用户的参数等内容,这些也将会作为Request对象的属性,因此这里我们就直接把Request对象作为Response对象的属性,这样今后就可以取得任何需要的内容。
Response类有一个公开的方法:
sendStaticResource:这个方法和之前一天我们写的send方法很相似,区别在于现在我们不需要考虑参数的传递,可以直接使用Response对象的属性取得数据输出流和文件路径,完成整个响应的过程。
好,接下来我们就可以通过调用我们前面写的两个类,与我们的主方法组合起来,具体的代码如下:
package com.tomcat;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
public class Main {
public static void main(String[] args){
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket(Constants.port, 0, InetAddress.getByName("127.0.0.1"));
while(true){
Socket socket = serverSocket.accept();
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
Request request = new Request(inputStream);
request.parse();
Response response = new Response(outputStream,request);
response.sendStaticResource();
socket.close();
}
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
现在你可以明显的感觉的到,我们的主方法代码清爽了不少,在接收到客户端的请求后,通过Sokcet对象,可以取得InputStream对象和OutputStream对象,然后就是分别构建Request对象和Response对象,最后调用各自的公共方法,其他的业务处理都与之前相同。
好了,又到了享受胜利果实的时候了,启动你的程序,打开网页,看看自己的成果?哎,看起来好像和之前也没什么区别吗?恩,别着急,下一讲我们会做点有挑战性的工作。
特别提醒一下,对于面向对象的设计思想必须要多多揣摩,多写代码,往往我们都是看看别人的都头头是道,但到自己的时候却头脑空空的。