一、前言
主流系统架构无非分两种:C/S(客户端/服务器)、B/S(浏览器/服务器)
1、C/S架构
优点:安全
缺点:升级维护成本高,一旦要升级,所有终端要逐个升级
2、B/S架构
优点:升级维护、部署简单,只需维护服务端即可
缺点:安全性有所欠缺
当下B/S无疑是系统开发中主流模式,在B/S架构流行的前提下,就JavaEE规范下出现了各种各样的Web框架,如Struts2、SpringMVC,而这些框架,无不基于Servlet规范,而Servlet容器中几乎没有不基于HTTP协议的实现(当然本文讨论的不是HTTP协议),在开发中我们处处使用框架,忽略底层细节,在部署时,打包扔进Tomcat、Jetty等web容器,简单方便。那么这些web容器的底层实现又是如何的呢?
今天本文就展示如何一步步实现一个简单web服务器。
二、需求
1、实现一个web容器,并模拟登陆操作
2、可并发
3、线程安全
三、技术点
1、Socket编程
2、Java IO
3、线程池
4、ThreadLocal
5、多线程
6、NIO、BIO
四、单线程模式实现
1、写一个HTML页面,里面有登陆需要的用户名、密码等输入元素,值得一提的是,本例使用的是GET方式提交请求,已经足够说明问题了,必须提到的是GET和POST获取参数的方式是有区别的。
<html>
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
<form action="login" method="get">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" ></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="text" name="password" ></td>
</tr>
<tr>
<td><input type="reset" value="重置"></td>
<td><input type="submit" value="登录"></td>
</tr>
</table>
</form>
</body>
</html>
2、有了HTML页面,那么如何把这个页面读到程序里呢,这个时候就要用到IO了,请看下面的方法:
// 为了方便main方法调用,并声明为static方法
private static 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();
}
3、那么服务端怎么与浏览器端交互,那么需要Socket,请看下面例子:
(1)、先定义要返回的响应头
// HTTP请求的返回头
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");
}
(2)、启动web服务
public static void main(String[] args) {
try {
// 监听在8080端口
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
// 接收请求
Socket accept = serverSocket.accept();
// 获取请求中的流
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();
}
}
(3)、说明
上一步代码,只读一行数据 拿到用户传过来的参数,其实是可以拿到HTTP请求的所有信息的,如下:
GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
Cookie: _ga=GA1.1.1755609982.1449039124; CNZZDATA1258600948=1953676425-1469624561-%7C1469624561; CNZZDATA1258740907=123042277-1469628131-%7C1469628131; userName=admin
五、执行效果
1、启动程序,浏览器器敲下http://localhost:8080/,回车,便能看到如下界面
2、输入用户名:admin,密码:admin,回车,提示登录成功
3、输入错误用户名或密码
六、后记
至此,一个单线程的web服务完成,其中存在如下问题
1、只能处理单个用户请求,多用户无法同时使用
2、如何实现并行处理用户请求
3、并行时线程如何安全
4、阻塞式的IO效率低,如何改进
请期待下一篇《自己动手实现简单web容器二》
快乐源于分享。
此博客乃作者原创, 转载请注明出处