上一篇博客《自己动手实现简单web容器一》,留下了一些问题,今天我们来解决并行访问的问题,即多个用户同时访问,我们的web服务器能给出响应,而不至于阻塞住。现代计算机越来越好,CPU核心数也越来越多,为了更高效的利用CPU,Java提供多线程的编程方式,下面我们就用多线程的方式解决这个问题。
1、写一个HandleRequestThread类,实现Runnable接口,重写run方法,便实现了一个线程类。代码如下:
public class HandleRequestThread implements Runnable {
private static final StringBuilder responseContent = new StringBuilder();
static {
responseContent.append("HTTP/1.1 200 OK");
responseContent.append("Content-Type: text/html; charset=utf-8");
responseContent.append("Content-Length:%s");
responseContent.append("Date:%s");
responseContent.append("Server:This is a simulation web container");
}
private Socket accept;
public HandleRequestThread(Socket accept) {
this.accept = accept;
}
@Override
public void run() {
try {
// 获取请求中的流
BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream()));
// 只读取第一行数据
String request = br.readLine();
String result = "";
if (!"".equals(request) && null != request) {
String urlAndPrams = request.split(" ")[1];
if (urlAndPrams.indexOf("/login") != -1) {
// 获取用户传过来的用户名密码
String[] params = urlAndPrams.split("\\?")[1].split("&");
String username = params[0].split("=").length < 2 ? null : params[0].split("=")[1];
String password = params[1].split("=").length < 2 ? null : params[1].split("=")[1];
if ("admin".equals(username) && "admin".equals(password)) {
result = "login success";
} else {
result = "username or password error";
}
} else {
// 调用前1面定义的获取登录页方法,获取已经写好的登录页
result = getLoginPage();
}
} else {
// 调用前面定义的获取登录页方法,获取已经写好的登录页
result = getLoginPage();
}
// 拿到该请求对应Socket的输出流,准备向浏览器写数据了
PrintWriter printWriter = new PrintWriter(accept.getOutputStream());
// 把文件长度、日期填回Response字符串中
printWriter.println(String.format(responseContent.toString(), result.length(), new Date()));
// 必须有个换行,换行之后才能向浏览器端写要传回的数据(HTTP协议)
printWriter.println();
// 写页面数据
printWriter.println(result);
printWriter.flush();
printWriter.close();
accept.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private String getLoginPage() {
// 用于存储从文件读出的文件
StringBuilder sb = new StringBuilder();
try {
// 声明一个BufferedReader准备读文件
BufferedReader htmlReader = new BufferedReader(new InputStreamReader(
new FileInputStream(new File(System.getProperty("user.dir") + "/webapp/login.html"))));
String html = null;
// 按行读取,直到最后一行结束
while ((html = htmlReader.readLine()) != null) {
sb.append(html);
}
// 关闭
htmlReader.close();
} catch (Exception e) {
e.printStackTrace();
}
// 以字符串形式返回读到的HTML页面
return sb.toString();
}
}
2、启动web 服务
public class WebServer {
public static void main(String[] args) {
try {
// 监听在8080端口
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket accept = serverSocket.accept();
// 接收到客户端请求,新起一个线程来处理该请求
new Thread(new HandleRequestThread(accept)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
以上代码已经可以实现多用户并行访问了,视乎完美解决了问题,假设现在有1000个用户同时访问,那么意味着要创建1000个线程,那么CPU和内存资源肯定吃紧,那么有什么办法优化呢?答案是线程池,预先创建一批线程在池子里,请求来了就处理,线程数不够,请求排队(当然线程池的实现是很复杂的,不是本文讨论的重点)。
废话不多说,上代码。
public class WebServer {
public static void main(String[] args) {
try {
// 创建一个定长为10的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
// 监听在8080端口
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket accept = serverSocket.accept();
// 接收到客户端请求,新起一个线程来处理该请求
pool.execute(new HandleRequestThread(accept));
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
用线程池,不仅可以节省线程创建和销毁的开销,还可以稳定线程的数量,不至于撑爆CPU和内存,这里不得不提一下Executors,Executors提供四种线程池创建的静态方法:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor,这个不是本文讨论的重点,我们只说我们用到的定长线程池,分析一下jdk源码
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
ThreadPoolExecutor是JDK默认线程池的实现,这里corePoolSize和maximumPoolSize相等,keepAliveTime表示不保存空闲线程,这个参数可以 根据请求的密集程度来调整,TimeUnit线程存活时间单位,BlockingQueue线程池满,选用的队列,这里我们是不限长队列,LinkedBlockingQueue是FIFO队列。具体线程池的用法将在后面的文章中详细讨论。
后记
至此,我们用伪异步的方式,对我们的web服务器进行了改造,实现了请求的并行,但是这个方式并非最高效,那么还有什么高效的方式呢?
快乐源于分享。
此博客乃作者原创, 转载请注明出处