如何学习源码,对于我来说,拿到一个框架/开源项目,首先思考的是,为什么有人写这个东西,它的出现是为了解决什么样的现实问题。知道了框架/开源项目的前世今生,就更容易融入框架作者的思路里来。
Tomcat对于从事java开发/学习的同学来说实在是太亲切了,每个人都用过,那么,为什么会有tomcat这个东西呢?他的出现解决了程序员们哪些现实问题?当我们运行一个web项目时,我们将项目打成war包,并将war包放到tomcat的webApp文件夹下,启动tomcat,剩下的我们不需要再考虑,交给Tomcat来管理即可。这一篇文章,我们假设没有Tomcat,项目的运行和发布都自己来实现,我们会怎么做?我们绘制一张思路图:
- 它首先应该能够接受请求,
- 根据请求的内容,它可以找到对应的web项目的处理者(service)
- 处理后的结果,它可以返还给请求者
以上三点是我们目前来看最最基本的,也是最最迫切需要的功能。那么我们开始吧。
新建一个JAVA项目,并新建一个DemoTomcat类,在main方法里创建serverSocket用于监听某个端口:
/**
* @author syy
* @Title: DemoTomcat
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 14:39
*/
public class DemoTomcat {
public static void main(String args[]) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("开始监听8080端口");
while (true) {
Socket socket = server.accept();
String content = null;
byte[] buff = new byte[1024];
int len;
if ((len = socket.getInputStream().read(buff)) > 0) {
content = new String(buff, 0, len);
System.out.println(content);
}
}
}
}
运行该main方法(main是类的入口),控制台打印出:开始监听8080端口 ,说明监听8080的serverSocket开始工作。
在浏览器中输入地址: http://localhost:8080/myapp?id=23 并访问,查看控制台打印的内容:
GET /myapp?id=23 HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
请求已经收到了,并且有丰富的参数,但我们从浏览器看到的是访问一直在转圈圈,什么原因呢?因为我们没有返回响应,所以浏览器一直在等待(转圈圈)。
下面我们为请求增加响应:
public static void main(String args[]) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("开始监听8080端口");
while (true) {
Socket socket = server.accept();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
//响应 hello World
String response = "Hello World";
OutputStream outputStream = socket.getOutputStream();
outputStream.write(response.getBytes());
outputStream.flush();
outputStream.close();
}
}
如上,刷新网页,在谷歌浏览器中发现报错了:ERR_INVALID_HTTP_RESPONSE 怎么回事?代码很简单,步骤也很明白,什么原因呢?别担心,这是因为浏览器访问的请求和响应有一定的格式要求。回顾下上面打印的请求内容可以看出来,请求包含:请求方式(GET/POST),Http协议(HTTP/1.1)HttpHost,Connection,Cache-Control,Upgrade-Insecure-Requests,User-Agent等信息,那么,我们也为响应指定浏览器必须包含的信息(协议、响应的格式):
public static final String responseHeader="HTTP/1.1 200 \r\n"
+ "Content-Type: text/html\r\n"
+ "\r\n";
public static void main(String args[]) throws IOException {
......
//响应
String response = responseHeader+"Hello World";
OutputStream outputStream = socket.getOutputStream();
outputStream.write(response.getBytes());
outputStream.flush();
outputStream.close();
}
}
现在结果正常了。到此,我们已经具备了基本的请求和响应的功能。有了基本的请求响应链路,我们开始思考,我们的请求如何传参呢?如何根据Url将请求分配给对应的处理者?http请求的方式有post和get,我们如何区分呢?,带着问题,我们对思路图进行完善:
上面我们知道,请求/响应包含很多信息,包括方法,协议,内容,类型等,我们将请求和响应封装成对象,方便使用。
新建两个类文件,分别为Request和Response:
/**
* @author syy
* @Title: Request
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 17:00
*/
public class Request {
private String method;
private String url;
public Request(InputStream inputStream) throws IOException {
//请求的第一行类似于这样 GET /myapp?id=23 HTTP/1.1,从这里可以分辨出是post还是get,也可以得到请求的完整地址
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String[] methodAndUrl = bufferedReader.readLine().split(" ");
this.method= methodAndUrl[0];
this.url=methodAndUrl[1];
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
/**
* @author syy
* @Title: Response
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 17:13
*/
public class Response {
public OutputStream outputStream;
public static final String responseHeader="HTTP/1.1 200 \r\n"
+ "Content-Type: text/html\r\n"
+ "\r\n";
public Response(OutputStream outputStream) throws IOException {
this.outputStream= outputStream;
}
}
修改DemoTomcat的代码:
/**
* @author syy
* @Title: DemoTomcat
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 14:39
*/
public class DemoTomcat {
public static void main(String args[]) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("开始监听8080端口");
while (true) {
Socket socket = server.accept();
Request request=new Request(socket.getInputStream());
Response response=new Response(socket.getOutputStream());
}
}
}
新建第一个处理请求的severlet,新建类文件命名为FirstSeverlet:
import java.io.OutputStream;
/**
* @author syy
* @Title: FirstServlet
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 17:24
*/
public class FirstServlet {
public void service(Request request, Response response) {
//判断是调用doget 还是 dopost
if ("get".equalsIgnoreCase(request.getMethod())) {
this.doGet(request, response);
} else {
this.doPost(request, response);
}
}
public void doGet(Request request, Response response) {
try {
OutputStream outputStream = response.outputStream;
String res = Response.responseHeader + "Get Request ->FirstServlet!!";
outputStream.write(res.getBytes());
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void doPost(Request request, Response response) {
try {
OutputStream outputStream = response.outputStream;
String res = Response.responseHeader + "Post Request ->FirstServlet!!";
outputStream.write(res.getBytes());
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
为了丰富servlet,我们拷贝一份FirstServlet,命名为SecondServlet,并修改doPost和doGet:
import java.io.OutputStream;
/**
* @author syy
* @Title: SecondServlet
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 17:24
*/
public class SecondServlet {
public void service(Request request, Response response) {
//判断是调用doget 还是 dopost
if ("get".equalsIgnoreCase(request.getMethod())) {
this.doGet(request, response);
} else {
this.doPost(request, response);
}
}
public void doGet(Request request, Response response) {
try {
OutputStream outputStream = response.outputStream;
String res = Response.responseHeader + " Get Request ->SecondServlet!";
outputStream.write(res.getBytes());
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
public void doPost(Request request, Response response) {
try {
OutputStream outputStream = response.outputStream;
String res = Response.responseHeader + "Post Request ->SecondServlet!!";
outputStream.write(res.getBytes());
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
为了调用servlet方便,我们新建一个ProcessRequest类,用于判断具体调用哪个servlet:
import java.io.OutputStream;
import java.net.Socket;
/**
* @author syy
* @Title: ProcessRequest
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 17:36
*/
public class ProcessRequest extends Thread {
private Socket socket;
private Request request;
private Response response;
public ProcessRequest(Socket socket, Request request, Response response) {
this.socket = socket;
this.request = request;
this.response = response;
}
@Override
public void run() {
try {
if (request.getUrl().contains("first")) {
FirstServlet firstSevelet = new FirstServlet();
if ("GET".equalsIgnoreCase(request.getMethod())) {
firstSevelet.doGet(request, response);
} else {
firstSevelet.doPost(request, response);
}
} else if (request.getUrl().contains("second")) {
SecondServlet secondSevelet = new SecondServlet();
if ("GET".equalsIgnoreCase(request.getMethod())) {
secondSevelet.doGet(request, response);
} else {
secondSevelet.doPost(request, response);
}
} else {
String res = Response.responseHeader + "servlet not found";
OutputStream outputStream = socket.getOutputStream();
outputStream.write(res.getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (socket != null) {
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
修改DemoTomcat的代码:
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @author syy
* @Title: DemoTomcat
* @ProjectName DemoTomcat
* @Description: TODO
* @date 2019/4/11 14:39
*/
public class DemoTomcat {
public static void main(String args[]) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("开始监听8080端口");
while (true) {
Socket socket = server.accept();
Request request=new Request(socket.getInputStream());
Response response=new Response(socket.getOutputStream());
Thread thread = new ProcessRequest(socket,request,response);
thread.start();
}
}
}
运行,在浏览器中输入http://localhost:8080/first 结果如下:
运行,在浏览器中输入结果如下:http://localhost:8080/second 结果如下:
运行,在浏览器中输入http://localhost:8080/third结果如下:
可见,请求已经被我们根据url分发给不同的servlet处理了。
上面代码的ProcessRequest里写死了servlet的调用关系,并不利于servlet的扩展,我们应该有一个配置文件,配置文件里配置了有哪些servlet,每个servlet处理哪些对应的请求,随着业务的增长或者webapp的增多,需要增加处理的servlet的时候,我们只需要修改配置文件即可
我们新建一个servlet.yml文件来当作servlet的配置文件,url中是first则交给FirstServlet处理,second则交给SecondServlet处理:
加载yml文件的代码也很简单,篇幅限制,我已将代码打包上传,需要的同学可以下载运行。点击这里下载。