第五章 实现HTTP Server
HTTP1.1协议大致讲完了,通过前面综合讲述我们知道,HTTP协议是运行于TCP协议之上的应用层协议,通过上一章的讲解,我们知道,这个是协议是明文的,而且我们上一章反复实验的过程中很明显的能看出来,无论是向服务器请求服务的request,还是服务器返回给客户端的response,期传递消息的格式都是一致的,都是以人类可读的格式,request结构如图5-1:注意,请求行有时候称之为请求状态行,有时候统一表示请求和响应消息会统一说成状态行。
图5-1 request消息格式
response消息结构如图5-2:
图5-2 response消息格式
想想我们现实中访问web服务用的是什么?浏览器啊,浏览器是什么?我们第一讲讲过,就是一个标准的客户端,浏览器在对web的发送请求和处理响应都有标准的实现:按照HTTP协议的标准发送一个明文的请求文本,然后服务器收到后会解析这个文本,处理浏览器传送过来的要求,然后根据这些要求启动相关的服务处理,然后返回处理结果,然后按照协议的标准封装返回消息,过去这个标准体系我们开发的时候,都会用标准的web服务器,或者带有附加业务能力处理的服务器,这一套处理流程都是藏在后面的,我知道处理结果就行,有的同学可能会基于标准的web浏览的内核实现过自己的浏览器,那么我们就要问了,既然客户端可以实现,标准和带有附加业务处理的能力的web服务器能不能自己写呢?没问题啊。
5.1 从最原始的Java Socket起步
想想我们第一章讲的一个最原始例子,针对TCP/IP协议的标准处理Java提供了普通Socket、NIO和Aio,介绍模型的时候我们讲过,HTTP协议是在TCP层之上的应用层协议,那么现在我们就可以在Java Socket的这个架构体系上上构建我们自己的任何类型的信息通信过程,实现HTTP Server更不在话下,无非就是在传输过程中,解析HTTP协议而已。
回顾第一章的简单的Java Socket例子,我们发觉一个致命的问题,我们运行这个通信过程的时候发觉服务器处理一个客户端后就结束了,这个和我们实际的业务场景出入太大了,实际应用的中的Web服务器是持续给不同的客户端提供服务,而且一台服务器可以给大量的客户端服务,回顾一下第一章Socket通信的代码:
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String args[]) {
try {
// 创建⼀个ServerSocket监听8080端⼝
ServerSocket server = new ServerSocket(8080);
// 等待请求
System.out.println("本机端口:"+String.valueOf(8080)+"服务已经启动,请通过合适的客户端链接测试");
Socket socket = server.accept();
// 接收到请求后使用socket进⾏通信,创建BufferedReader用于读取数据,
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
StringBuffer inStr = new StringBuffer();
String inHeader=null;
String line = null;
line=is.readLine();
while (line.length()>0) {
inStr.append(line+"\n<br>");
line=is.readLine();
}
inHeader= "我是服务器,收到来自: " + inStr.toString();
System.out.println(inHeader);
PrintWriter pw = new PrintWriter(socket.getOutputStream());
pw.println("Hi,客户端你好,我收到你的数据了,原样返回: " + line);
pw.flush();
// 关闭资源
pw.close();
is.close();
socket.close();
server.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
从服务程序代码看,结束是必然的,在本机8080创建个服务,等待客户机连接,然后把客户机发来的信息原样返回,到这里肯定会有个疑问,这个功能不全的服务器我们能使用浏览器访问吗?
肯定的回答你:能,用IDE工具打开这份代码,然后运行,这个时候启动浏览器访问服务器的服务,你会看到如下情形图5-3:
图5-3 服务器启动
然后你打开浏览器,在地址看输入:127.0.0.1:8080打回车,你看在浏览器看到如下图5-4:
图5-4 通过浏览器访问反馈
在看服务器上浏览器访问后的信息图5-5:
图5-5 收到浏览器访问会服务的信息
接着我们看图5-4的反馈信息,上面反馈的错误是:ERR_INVALID_HTTP_RESPONSE,而不是我们
我们访问的不存在的服务,不存在的服务的反馈是图5-6:
图5-6 无效服务或者无法访问的服务
为什么会是图5-4的界面呢,想想我们服务器返回的信息,原样返回,浏览器发的是
GET / HTTP/1.1
......
显然返回也是:
GET / HTTP/1.1
......
显然浏览器客户端懵了,这个格式的信息我看不懂啊,怎么我访问了一个回音壁?我要求你返回处理的结果啊,这个能处理吗?能啊,我们按照HTTP协议构造一个合法的reponse不就行了。
5.2 将读取的信息以浏览器可以看懂的格式返回去
为了这一次访问可以正常完成,我修改一下一下代码,当客户端收到信息干嘛?服务器按照http协议标准发回一个响应及文本,怎么封装,既然可以通过StringBuffer读数据,现在我们也可以使用这个类型来构造HTTP的response,封装的方法很简单,按照HTTP协议规定的格式写就行了,为了让返回的信息读起来的有条理点我们在每一行后面加了<br>,很多同学可能会问了,你前面加过”\n”了,难道浏览器不能处理,不能,浏览器解析HTML的换行只认<br>,老套路,看代码,注意加粗绿色部分,按照协议格式要求写一个字符串,把我们从浏览器收到的请求信息发回去:
public class Server {
public static void main(String args[]) {
try {
// 创建⼀个ServerSocket监听8080端⼝
ServerSocket server = new ServerSocket(8080);
// 等待请求
System.out.println("本机端口:" + String.valueOf(8080) + "服务已经启动,请通过合适的客户端链接测试");
Socket socket = server.accept();
// 接收到请求后使用socket进⾏通信,创建BufferedReader用于读取数据,
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
StringBuffer inStr = new StringBuffer();
String inHeader=null;
String line = null;
line=is.readLine();
while (line.length()>0) {
inStr.append(line+"\n<br>");
line=is.readLine();
}
inHeader= "我是服务器,收到来自:\n<br> " + inStr.toString();
System.out.println(inHeader);
PrintWriter pw = new PrintWriter(socket.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append("HTTP/1.1 200 OK \r\n");
sb.append("Content-Type: text/html \r\n");
sb.append("\r\n");
sb.append("<html>\n");
sb.append("<body>\n");
sb.append("<h1>" + inHeader + "</h1>\n");
sb.append("</body>\n");
sb.append("</html>\n");
// 创建PrintWriter,用于发送数据
System.out.println(sb.toString());
pw.println(sb.toString());
pw.flush();
// 关闭资源
pw.close();
is.close();
socket.close();
server.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们运行服务器,打开任意个浏览器,在地址栏输入127.0.0.1:8080,在浏览器上会惊奇的看到如图5-7的画面:
图5-7 服务器返回浏览器发给服务器的信息
仔细分析这个文本,好像和我们上一讲介绍的内容一致嘛,显然现在我知道了,浏览器发给我的我们就是HTTP的标准协议封装,我处理的消息也按照HTTP协议封装,浏览器就可以自动的分析和处理了。
跑过这份代码的都知道,第一次使用浏览器访问的时候,可以愉快的沟通,但是第二次在访问,服务器不理我们了,为什么?仔细分析代码,好像服务器启动,看到一个客户端接入马上服务,服务完成后服务进程关闭,服务器关闭,显然在这不是我们想要的,我想的是像普通web应用一样,可以持续不断的给各类客户端提供服务。我们期望我们写好的服务器运行以后,不会结束,持续的不断的在我们指定的端口监听,当有访问进来,我开服务进程对其服务。
5.3 让服务器不停机
怎么做呢?突然想到,我们是不是可以弄个循环出来的,不会结束的那种,持续的监听,服务过程。有请求进来,连接、服务、服务完关闭,见代码:
public class Cserver {
public static void main(String args[]) {
try {
// 创建⼀个ServerSocket监听8080端⼝
ServerSocket server = new ServerSocket(8080);
// 等待请求
System.out.println("本机端口:" + String.valueOf(8080) + "服务已经启动,通过客户端测试");
while (true) {
Socket socket = server.accept();
// 接收到请求后使用socket进⾏通信,创建BufferedReader用于读取数据,
BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
StringBuffer inStr = new StringBuffer();
String inHeader = null;
String line = null;
line = is.readLine();
while (line.length() > 0) {
inStr.append(line + "\n<br>");
line = is.readLine();
}
inHeader = "我是服务器,收到来自:<br> " + inStr.toString();
System.out.println(inHeader);
PrintWriter pw = new PrintWriter(socket.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append("HTTP/1.1 200 OK \r\n");
sb.append("Content-Type: text/html \r\n");
sb.append("\r\n");
sb.append("<html>\n");
sb.append("<body>\n");
sb.append("<h1>" + inHeader + "</h1>\n");
sb.append("</body>\n");
sb.append("</html>\n");
// 创建PrintWriter,用于发送数据
System.out.println(sb.toString());
pw.println(sb.toString());
pw.flush();
// 关闭资源
pw.close();
is.close();
socket.close();
}
// server.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
很爽,这个时候我们我们在启动服务器,在用浏览器反复访问,每次都会成功,而且有我们期望的返回,仔细分析代码,现在就是个接收浏览器的请求,原样返回,处理起来不需要多少时间,多台计算机同时访问,都可以立即得到响应,但是想一下,如果SOCKET的服务的处理过程过于复杂,想想我们真实业务场景,很多人同时访问服务器,调用五花八门的服务,如果我们把业务处理过程都放在这一个过程里面,想想这个处理过程的代码长度,以及持续服务一个用户占用这个进程的时间,肯定是代码长的不可想想,大量用户都在等待服务。
5.4 线程,线程,呼唤线程
想想我们Java课程,老师在讲课的时候给我们演示了如何如何写线程代码,但估计大多数的人都是一脑门子浆糊,这个东西能做什么,能解决什么问题呢?现在应用场景来了,我们可以把对客户端的服务放到一个线程类中,如何定义呢?Java 提供了一个封装好的处理线程的类,我们可以从哪里继承生成一个服务类,每次侦听的新的请求,产生一个新的socket单独服务与信的客户。ServerThread代码如下:
package cn.edu.bbc.copmpter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerThread extends Thread{
private Socket client;
public ServerThread(Socket client) {
super();
this.client = client;
}
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(client.getRemoteSocketAddress()+" 发出请求");
BufferedReader is;
try {
is = new BufferedReader(new InputStreamReader(this.client.getInputStream()));
StringBuffer inStr = new StringBuffer();
String inHeader = null;
String line = null;
line = is.readLine();
while (line.length() > 0) {
inStr.append(line + "\n<br>");
line = is.readLine();
}
inHeader = "我是服务器,收到来自:<br> " + inStr.toString();
System.out.println(inHeader);
PrintWriter pw = new PrintWriter(this.client.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append("HTTP/1.1 200 OK \r\n");
sb.append("Content-Type: text/html \r\n");
sb.append("\r\n");
sb.append("<html>\n");
sb.append("<body>\n");
sb.append("<h1>" + inHeader + "</h1>\n");
sb.append("</body>\n");
sb.append("</html>\n");
// 创建PrintWriter,用于发送数据
System.out.println(sb.toString());
pw.println(sb.toString());
pw.flush();
// 关闭资源
pw.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
服务器端代码改造为Server:
package cn.edu.bbc.copmpter;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8080);
System.out.println("本机端口:" + String.valueOf(8080) + "服务已经启动,请通过客户端链接测试");
//使用多线程处理每个请求
while(true){
Socket client = server.accept(); //阻塞式等待接收一个请求
new ServerThread(client).start();
}
}
}
这样就完美解决了,客户端只之间相互等待的问题,这样就万事大吉了?还是弱爆了,这样的代码也就是是回声机,没有任何作用,真实的服务器会是什么样?根据输入的参数不同,提供不同的服务,显然是动态的解析客户端的请求,根据请求生成的合适的响应,现在现在我们要定义的一个能动态解析HTTP请求的类,同时定义一个动态生成HTTP响应的类,同事融入写好的代码体系。
5.5 定义HTTP请求解析类和HTTP响应处理类
这个很难吗?我第四章已经完全讲解了HTTP协议,同时本章开头也给出了这两类消息个格式,注意,请求行有时候称之为请求状态行,有时候统一表示请求和响应消息会统一说成状态行
request结构如图5-1:
图5-1 request消息格式
response消息结构如图5-2:
图5-2 response消息格式
现在我们根据这个结构定义其解析类cn.edu.bbc.computer.Request:
/**
* 处理客户端套接字输入流封装成请求正文
*/
public class Request {
// 请求方式
private String method;
// 请求资源
private String url;
// 请求参数
private Map<String, List<String>> parameterMap;
private InputStream is;
private final String CRLF = "\n";
public Request(InputStream is) {
try {
// 初始化
method = "";
url = "";
parameterMap = new HashMap<>();
this.is = is;
byte[] arr = new byte[20000];
int len = this.is.read(arr);
// 开始解析请求正文
if (len > 0) {
String requestInfo = new String(arr, 0, len); // 请求正文
paraseRequest(requestInfo);
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 根据请求正文-->解析出请求方式,请求资源地址
private void paraseRequest(String requestInfo) throws UnsupportedEncodingException {
String param = ""; // 接收请求参数列表
// 1-获取请求行
String firstline = requestInfo.substring(0, requestInfo.indexOf(CRLF));
this.method = firstline.substring(0, firstline.indexOf("/")).trim();
String url = firstline.substring(firstline.indexOf("/"), firstline.indexOf("HTTP/")).trim();
// 根据不同请求方式封装请求参数和请求资源
if (method.equalsIgnoreCase("post")) {
this.url = url;
param = requestInfo.substring(requestInfo.lastIndexOf(CRLF)).trim();
} else if (method.equalsIgnoreCase("get")) {
// 判断是否有请求参数
if (url.contains("?")) {
String[] arr = url.split("\\?");
this.url = arr[0];
param = arr[1]; // 有可能有多个请求参数,还要再进一步解析
} else {
this.url = url;
}
}
// 进一步解析请求参数,并封装到集合中 格式:a=1&b=2&c="你好"
if (param != "") {
StringTokenizer token = new StringTokenizer(param, "&");
while (token.hasMoreElements()) {
String key_value = (String) token.nextElement();
String[] ks = key_value.split("=");
// 请求参数的值为空,手动赋值为null
if (ks.length == 1) {
ks = Arrays.copyOf(ks, 2);
ks[1] = null;
}
// 将请求参数键值对封装进集合
if (!parameterMap.containsKey(ks[0].trim())) {
parameterMap.put(ks[0].trim(), new ArrayList<String>());
}
// 为避免中文乱码,把请求参数的值按utf-8解码再放进集合
parameterMap.get(ks[0].trim()).add(URLDecoder.decode(ks[1].trim(), "UTF-8"));
}
}
}
/**
* 根据name获取单个参数的值
*/
public String getParameter(String name) {
String[] values = getParameterValues(name);
if (values == null)
return null;
else
return values[0];
}
// 获取多个
public String[] getParameterValues(String name) {
if (parameterMap.get(name) == null) {
return null;
} else {
List<String> values = parameterMap.get(name);
// 参数是指定转换成数组的类型
return values.toArray(new String[values.size()]);
}
}
/**
* 获取请求资源
*/
public String getUrl() {
return this.url;
}
}
cn.edu.bbc.computer.Response:
/**
* HTTP Response 根据客户端套接字的输出流封装成 请求正文
*/
public class Response {
private StringBuilder headInfo; // 响应行和头信息
private StringBuilder content; // 正文
private BufferedWriter bw;
public int code = 200; // 状态码
private int len;
private final String space = " ";
private final String CRLF = "\n";
public Response(OutputStream outputStream) {
headInfo = new StringBuilder();
content = new StringBuilder();
bw = new BufferedWriter(new OutputStreamWriter(outputStream));
len = 0;
}
// 向客户端发送响应
public void print(String html) throws IOException {
// 构建响应正文
content.append(html);
this.len = html.getBytes().length;
// 构建响应行和响应头
setHead();
// 发送
bw.append(headInfo.toString());
bw.append(content.toString());
bw.flush();
}
// 构建响应行和响应头
private void setHead() {
if (code != 200)
this.len = 0;
headInfo.append("HTTP/1.1").append(space).append(code).append(space);
switch (code) {
case 200:
headInfo.append("OK");
break;
case 404:
headInfo.append("NOT FOUND");
break;
case 505:
headInfo.append("Server ERROR");
break;
}
headInfo.append(CRLF);
headInfo.append("Date:").append(LocalDateTime.now()).append(CRLF);
headInfo.append("Content-Type:text/html;charset=UTF-8").append(CRLF);
headInfo.append("Content-Length:").append(space).append(this.len).append(CRLF).append(CRLF);
}
//
public void setCode(int code) {
this.code = code;
}
}
修改GeneralServerThread让其可以处理通用的处理过程,其代码如下:
public class GeneralServerThread extends Thread {
private Request request; //请求
private Response response; //响应
private Socket client;
//初始化request,reponse
public GeneralServerThread(Socket client) {
try {
this.client = client;
request = new Request(client.getInputStream());
response = new Response(client.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
System.out.println(client.getRemoteSocketAddress()+" 发出请求");
//浏览器会默认请求网站图标资源,我们这里忽略掉这个请求
if (request.getUrl().equals("/favicon.ico"))
return;
//1-根据请求的url获得对应的静态文件,这里仅仅是个演示,更深的套路是封装一个httphandler类任务交给他处理
if (request.getUrl().equals("/"))
{
String strHtml=response.toHtmlString(new File("src/index.html"));
response.print(strHtml);
}
else
{
String str404=response.toHtmlString(new File("src/404.html"));
response.print(str404);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
服务器代码没变,运行服务器,输入127.0.0.1:8080就能获得这个站点的静态首页,其他网址获得404页面。很有成就感是是吧。但是还有坑,我们实际的工作中,创建服务进程是一个特别耗时间的操作,服务器同时是服务多个用户,我们这种来个新用户就创建一个服务线程的方法,在系统开始缓解创建服务线程压力会很大的,怎么办?我们上一讲用到过数据库连接缓冲池,这里显然我们也能预先创建一个服务线程池,系统启动的时候就自动创建好,当服务收到连接申请,查看当前服务池是不是空闲线程,有的话直接让其服务客户端,如果没有,查看当前是不是到了连接最大限制了,没到,创建连接,服务客户端,到了,反馈服务器忙。
5.6 ServicePool服务池来了
上面描述的时候已经说出思路了,直接上代码?这个几乎是个通用模式,以后你们有需要的用到线程池的可以超这个方向套,也是一个最简单的实现方式:
package cn.edu.bbc.copmpter.pool;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
public class ServicePool<Job extends Runnable> {
// 任务列表 (线程池)
private final LinkedList<Job> jobsList = new LinkedList<>();
// 工作线程队列
private final List<MyWorker> workerList = Collections.synchronizedList(new ArrayList<MyWorker>());
// 默认工作者线程数量
private static final int DEFAULT_WORKER_NUMBERS = 5;
// 工作者编号生成序号
private AtomicLong threadNum = new AtomicLong();
// 构造方法
public ServicePool() {
initWorkerThreadByNum(DEFAULT_WORKER_NUMBERS);
}
public ServicePool(int workerNum) {
initWorkerThreadByNum(workerNum);
}
public void initWorkerThreadByNum(int workerNum) {
for (int i = 0; i < workerNum; i++) {
MyWorker worker = new MyWorker();
workerList.add(worker);
// 工作线程开始消费任务
new Thread(worker, "ThreadPool-Worker-" + threadNum.incrementAndGet()).start();
}
}
// 把任务交给线程池,之后工作线程回去消费它
public void execute(Job job) {
if (job != null) {
synchronized (jobsList) {
jobsList.addLast(job);
System.out.println("剩余待处理请求个数:" + ServicePool.this.getJobsize());
jobsList.notify(); // 随机唤醒在此jobsList锁上等待的工作者线程
}
}
}
// 通知所有的工作者线程
public void shutdown() {
for (MyWorker e : workerList) {
e.shutdown();
}
}
// 获取剩余任务个数
public int getJobsize() {
return jobsList.size();
}
/**
* 工作线程,消费任务
*/
private class MyWorker implements Runnable {
// 是否工作
private volatile boolean isRunning = true;
@Override
public void run() {
while (isRunning) {
Job job = null;
// 同步获取任务
synchronized (jobsList) {
// 如果任务列表为空就等待
while (jobsList.isEmpty()) {
try {
jobsList.wait();
} catch (InterruptedException e) {
// 感知到被中断就退出
return;
}
}
// 获取任务
job = jobsList.removeFirst();
}
// 执行任务
if (job != null) {
System.out.println("正在处理请求");
job.run();
System.out.println("处理完成,剩余待处理请求个数:" + ServicePool.this.getJobsize());
}
}
}
// 关闭线程
public void shutdown() {
isRunning = false;
}
}
}
然后服务器的代码可以修改为:
package cn.edu.bbc.copmpter.BasicServletFrame;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import cn.edu.bbc.copmpter.pool.ServicePool;
public class MainGeneralPoolServer {
private static ServicePool<GeneralServerThread> servicePool = new ServicePool<>();
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("http服务器启动成功....");
// 多线程处理每个请求
while (true) {
Socket client = server.accept(); // 阻塞式等待接收一个请求
servicePool.execute(new GeneralServerThread(client));
}
}
}
5.7 突然想起了我们的Servlet,能用这个体系实现吗?
可以,无非就是ServerThread处理代码中加入合适的路由处理,我们学JSP和Servlet的时候,我们的路由通常是在Web.xml中定义的,显然我们从request对象获取浏览器访问的地址的时候,去web.xml找器对应的服务类,然后通过反射创建对应的类处理客户端所需的业务逻辑,然后返回处理结果,首先定义路由文件的解析类,这个类的作用就是从指定的位置加载web.xml文件,看看有没有指定请求的服务类,代码如下:
package cn.edu.bbc.copmpter.BasicServletFrame;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
import java.util.List;
/**
* Servlet工厂 根据url和xml文件创建Servlet
*/
public class ServletFactory {
//Servlet上下文环境
private static ServletContext context = new ServletContext();
//web.xml文件路径
private static String xmlpath = "src/web.xml";
private ServletFactory(){}
/**
* 读取web.xml文件把servlet和url的关系进行配置存储
*/
static {
try {
//1-获得doucument
SAXReader saxReader = new SAXReader();
File f=new File(xmlpath);
System.out.println(f.getAbsolutePath());
Document document = saxReader.read(new File(xmlpath));
//2-获得根元素 <web-app>
Element rootElement = document.getRootElement();
//3-获得所有子元素
List<Element> elements = rootElement.elements();
//4-遍历处理所有子元素
for (Element e : elements) {
if ("servlet-mapping".equals(e.getName())) {
Element servlet_name = e.element("servlet-name");
Element url_pattern = e.element("url-pattern");
context.getUrl_map().put(url_pattern.getText(),servlet_name.getText());
}
else if ("servlet".equals(e.getName())) {
Element servlet_name = e.element("servlet-name");
Element servlet_class = e.element("servlet-class");
context.getServlet_map().put(servlet_name.getText(),servlet_class.getText());
}
}
} catch (DocumentException e) {
e.printStackTrace();
}
}
/**
* 获得Servlet
*/
public static synchronized Servlet getServlet(String url) throws Exception {
String servletClass = context.getServlet_map().get(context.getUrl_map().get(url));
System.out.println(servletClass);
if (servletClass != null)
return (Servlet)Class.forName(servletClass).getDeclaredConstructor().newInstance();
else
return null;
}
}
ServleContext代码:
package cn.edu.bbc.copmpter.BasicServletFrame;
import java.util.HashMap;
import java.util.Map;
/**
* Servlet的上下文环境
*/
public class ServletContext {
//Servlet别名和Servlet类名的映射关系
private Map<String,String> servlet_map;
//url和 Servlet别名的映射关系
private Map<String,String> url_map;
public ServletContext() {
servlet_map = new HashMap<>();
url_map = new HashMap<>();
}
public Map<String, String> getServlet_map() {
return servlet_map;
}
public Map<String, String> getUrl_map() {
return url_map;
}
}
通用Servlet类定义,以后所有的业务处理Servlet都从这这里继承,这里只定义两个关键函数分别处理http的get方法和post方法,当然你可以尝试着定义完整,有兴趣可以自己看Java Servlet自己的实现,和这里处理非常类似,这里是个简化版的,主要说原理。
package cn.edu.bbc.copmpter.BasicServletFrame;
/**
* Servlet抽象类
*/
public abstract class Servlet {
public void service(Request request, Response reponse) throws Exception {
this.doGet(request,reponse);
this.doPost(request,reponse);
}
public abstract void doGet(Request request, Response reponse) throws Exception;
public abstract void doPost(Request request, Response reponse) throws Exception;
}
然后具体的处理特定业务逻辑的Servlet定义,这里定义了一个登陆简单例子。
package cn.edu.bbc.copmpter.app;
import java.io.File;
import cn.edu.bbc.copmpter.BasicServletFrame.Request;
import cn.edu.bbc.copmpter.BasicServletFrame.Response;
import cn.edu.bbc.copmpter.BasicServletFrame.Servlet;
public class LoginServlet extends Servlet {
@Override
public void doGet(Request request, Response response) throws Exception {
String name = request.getParameter("name");
String password = request.getParameter("password");
System.out.println(name);
System.out.println(password);
//这列可以做什么?数据库的操作啊。啊啊啊啊,原来Servlet就这么简单
if (name!= null && password !=null && name.equals("oliver") && password.equals("olivertest")) {
String mainIndex=response.toHtmlString(new File("src/index.html"));
response.print(mainIndex);
}
else {
String failHTML=response.toHtmlString(new File("src/loginfail.html"));
response.print(failHTML);
}
}
@Override
public void doPost(Request request, Response reponse) throws Exception {
doGet(request,reponse);
}
}
ServerThread的代码改写如下,注意红色代码部分:
package cn.edu.bbc.copmpter.BasicServletFrame;
import java.io.IOException;
import java.net.Socket;
public class ServerThread extends Thread {
private Request request; //请求
private Response reponse; //响应
private Socket client;
//初始化request,reponse
public ServerThread(Socket client) {
try {
this.client = client;
request = new Request(client.getInputStream());
reponse = new Response(client.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
System.out.println(client.getRemoteSocketAddress()+" 发出请求");
//浏览器会默认请求网站图标资源,我们这里忽略掉这个请求
if (request.getUrl().equals("/favicon.ico"))
return;
//1-根据请求的url获得Servlet
System.out.println(request.getUrl());
Servlet servlet = ServletFactory.getServlet(request.getUrl());
//请求资源不存在404
if (servlet == null){
reponse.setCode(404);
reponse.print("");
}
//2-执行Servlet
if (servlet != null){
servlet.service(request,reponse);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
路由文件web.xml定义如下:
<?xml version="1.0" encoding="UTF-8" ?>
<web-app>
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>cn.edu.bbc.copmpter.app.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>XXXXXServlet</servlet-name>
<servlet-class>XXXXX</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>XXXXXServlet</servlet-name>
<url-pattern>/XXXXX</url-pattern>
</servlet-mapping>
</web-app>
服务器代码如下MainPoolServer:
package cn.edu.bbc.copmpter.BasicServletFrame;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import cn.edu.bbc.copmpter.pool.ServicePool;
public class MainPoolServer {
private static ServicePool<ServerThread> servicePool = new ServicePool<>();
@SuppressWarnings("resource")
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
System.out.println("http服务器启动成功....");
// 多线程处理每个请求
while (true) {
Socket client = server.accept(); // 阻塞式等待接收一个请求
servicePool.execute(new ServerThread(client));
}
}
}
完善吗?不,显然我们这一章实现的web服务器,仅仅只处理HTTP协议的一部分,完美的服务器要能完整处理协议的所有动作,作为中间件这门课程的讲解我们只能到这里,要是完整实现产品级的服务器,一个学期的课程32个学时是完全不够的,后期的代码完善和实现可以留作各位同学的家庭作业。
0 条 查看最新 评论
没有评论