今天我们计划使用Netty完成一个Web服务器。

Tomcat是基于J2EE规范的Web容器。包括了Servlet、Filter、Linster,这些在J2EE中都是抽象实现,具体的实现交给开发者来完成。本文以Servlet为例来详细展开。

本部分计划分上下来完成,上部分主要完成基于传统的IO的Tomcat服务器。

1.定义Servlet抽象类

在servlet中,最常用的方法就是doGet和doPost方法。我们定义了GPServlet这样的一个抽象类,包括了这两个方法,和service方法。

/*** GPServlet抽象类** @author 荀彧 2020/12/26*/
public abstract class GPServlet {
public void service(GPRequest request, GPResponse response) throws Exception{
// 如果请求方法为GET if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request,response);
}else {
doPost(request,response);
}
}
public abstract void doGet(GPRequest request,GPResponse response) throws Exception;
public abstract void doPost(GPRequest request,GPResponse response) throws Exception;
}

从代码可以看到,doGet和doPost中有两个参数,GPRequest和GPResponse对象,这两个对象主要是对Socket的封装。

2. 实现GPRequest

其中主要包括了获取用户的请求方式,以及请求的url

/*** 是对input的封装。** @author 荀彧 2020/12/26*/
public class GPRequest {
private String method;
private String url;
public GPRequest(InputStream in) {
String content = "";
byte[] buff = new byte[1024];
int len = 0;
try {
if ((len = in.read(buff)) > 0) {
content = new String(buff, 0, len);
}
String line = content.split("\\n")[0];
String[] arr = line.split("\\s");
this.method = arr[0];
this.url = arr[1].split("\\s")[0];
} catch (IOException e) {
e.printStackTrace();
}
}
/*** 获取请求方式。** @return*/
public String getMethod() {
return this.method;
}
public String getUrl() {
return this.url;
}
}

3.实现GPResponse

/*** 是对output的封装** @author 荀彧 2020/12/26*/
public class GPResponse {
private OutputStream outputStream;
public GPResponse(OutputStream outputStream) {
this.outputStream = outputStream;
}
/*** 输出 ,需要遵守HTTP协议。* 按照HTTP规范输出字符串。* @param outMsg*/
public void write(String outMsg) throws Exception{
StringBuilder sb = new StringBuilder();
sb.append("HTTP/1.1 200 OK\n")
.append("Content-Type: text/html;\n")
.append("\r\n")
.append(outMsg);
outputStream.write(sb.toString().getBytes());
}
}

GPResponse主要实现了对用户的响应,主要为设置响应内容,和相应的状态码。

4.创建用户的业务代码

我们基于GPServlet来完成我们的两个业务逻辑。FirstSevlet 和SecondServlet。

FirstServlet如下:

/*** 第一个Servlet** @author 荀彧 2020/12/26*/
public class FirstServlet extends GPServlet {
public void doGet(GPRequest request, GPResponse response) throws Exception {
doPost(request,response);
}
public void doPost(GPRequest request, GPResponse response) throws Exception {
response.write("this is the first Servlet");
}
}

SecondServlet如下:

/*** 第二个Servlet** @author 荀彧 2020/12/26*/
public class SecondServlet extends GPServlet {
public void doGet(GPRequest request, GPResponse response) throws Exception {
doPost(request,response);
}
public void doPost(GPRequest request, GPResponse response) throws Exception {
response.write("this is the second Servlet");
}
}

我们继承了GPServlet这个抽象类,并重写了doGet和doPost方法。

5.完成web.properties配置

servlet.one.url=/firstServlet

servlet.one.className=com.learn.netty.tomcat.servlet.FirstServlet

servlet.second.url=/secondServlet

servlet.second.className=com.learn.netty.tomcat.servlet.SecondServlet

配置的url主要用于映射。className用于反射来寻找到相对应的servlet。

6.创建GPTomcat启动类

/*** TomCat的启动类** @author 荀彧 2020/12/26*/
public class GPTomcat {
/*** 默认端口号。*/
private int port = 8080;
private ServerSocket server;
private Map servletMap = new HashMap();
/*** 读取配置文件*/
private Properties webProperties = new Properties();
/*** 初始化函数*/
private void init() {
String WEB_INF = this.getClass().getResource("/").getPath();
try {
FileInputStream fis = new FileInputStream(WEB_INF + "web.properties");
webProperties.load(fis);
for (Object k: webProperties.keySet()) {
String key = k.toString();
if (key.endsWith(".url")) {
String servletName = key.replaceAll("\\.url$","");
String url = webProperties.getProperty(key);
String className = webProperties.getProperty(servletName + ".className");
GPServlet obj = (GPServlet) Class.forName(className).newInstance();
servletMap.put(url,obj);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/*** 服务就绪,准备启动*/
public void start() {
init();
try {
server = new ServerSocket(this.port);
System.out.println("TomCat已经启动。。。。端口号为:" + this.port);
// 等待用户请求 while (true) {
Socket client = server.accept();
// 处理http请求 process(client);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/*** 处理的业务逻辑。* @param client* @throws Exception*/
private void process(Socket client) throws Exception {
InputStream inputStream = client.getInputStream();
OutputStream outputStream = client.getOutputStream();
// 转换为GPRequest GPRequest request = new GPRequest(inputStream);
GPResponse response = new GPResponse(outputStream);
// 从协议中获取url。 String url = request.getUrl();
if (servletMap.containsKey(url)) {
servletMap.get(url).service(request,response);
}else {
response.write("404-Not-Found");
}
outputStream.flush();
outputStream.close();
inputStream.close();
client.close();
}
public static void main(String[] args) {
new GPTomcat().start();
}
}

Web容器的实现,主要分为三个阶段。初始化阶段。对应于代码中的init部分。主要是解析web.properties

服务就绪阶段。对应于代码中的start()方法。

接受请求阶段。对应于代码中的process()方法。

每次客户端请求到来的时候,会从Map中寻找到对应的Servlet对象,并实例化GPRequest 和GPResponse对象。最后给用户反馈。

测试

我们启动之后。

此时我们用浏览器发送一个Get请求。得到了正确的响应。

小结

我们利用传统IO,完成了一个简易的web服务器,主要也了解到了进行网络通信的过程。下一节我们会进行基于Netty的改进。

传统IO的问题:线程资源受限:线程是操作系统中非常宝贵的资源,同一时刻有大量的线程处于阻塞状态是非常严重的资源浪费,操作系统耗不起

线程切换效率低下:单机 CPU 核数固定,线程爆炸之后操作系统频繁进行线程切换,应用性能急剧下降。

除了以上两个问题,IO 编程中,我们看到数据读写是以字节流为单位。

代码:

uestc_wb/netty-tomcatgitee.com

切换分支:simple-web