目录
前言
从文件上传、下载入门Java web
文件上传Demo
简单的BS架构服务端Demo
***:一个ServerSocket就是后台服务?
依赖Tomcat实现后台服务
***:一个servlet就是后台服务?
***:Tomcat做了什么?
***:(实战)实现简单的Servlet容器
带着问题学java系列博文之java基础篇。从问题出发,学习java知识。
JavaWeb,主要指以Java语言为基础,利用Servlet、JSP等技术开发动态页面,方便用户通过浏览器与服务器后台交互。Java Web应用程序可运行在一个轻量级的Servlet容器中,比如Tomcat。那服务端到底是如何实现,浏览器又是如何和服务端交互,为什么java web要Tomcat,Tomcat又做了什么呢?让我们从文件上传、下载开始,逐步了解java网络编程,入门Java web。
public class TcpServer {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
String fileName = System.currentTimeMillis() + ".txt";
File file = new File("src\\com\\zst\\javabasedemo\\net\\upload");
if (!file.exists()) {
file.mkdirs();
}
byte[] bytes = new byte[1024];
int len = 0;
InputStream inFromClient = socket.getInputStream();
FileOutputStream fos = new FileOutputStream(file + File.separator + fileName);
while ((len = inFromClient.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
DataOutputStream outToClient = new DataOutputStream(socket.getOutputStream());
outToClient.writeUTF("上传成功");
fos.close();
socket.close();
} catch (IOException e){
e.printStackTrace();
}
}
}).start();
}
}
}
public class TcpClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("localhost",8080);
FileInputStream fis = new FileInputStream("src\\com\\zst\\javabasedemo\\file\\test.txt");
OutputStream outToServer = socket.getOutputStream();
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1){
outToServer.write(bytes,0,len);
}
//添加这个表示关闭输入,文件上传结束,防止阻塞
socket.shutdownOutput();
DataInputStream in = new DataInputStream(socket.getInputStream());
System.out.println(in.readUTF());
fis.close();
socket.close();
}
}
如上demo,使用BIO搭建:ServerSocket作为服务端,客户端使用Socket与服务端实现通讯。当客户端与服务端连接建立后,客户端向服务端上传文件,服务端接收后保存在本地upload文件夹下。
ServerSocket和Socket是java.net包下的用于网络编程的两个类,服务端初始化ServerSocket后,进行端口绑定,然后就由ServerSocket进行监听,阻塞等待客户端socket连接事件。一旦监听到客户端连接,就创建一个socket实例用于与客户端通讯。注意此时,客户端有一个socket,服务端也有一个socket,它们不是同一个。ServerSocket和Socket通讯是在传输层,基于TCP/IP协议。
***:注意最后要写入文件结束标志,否则服务端while循环永远读不到-1,也就无法退出循环,导致线程一直运行,不关闭io,释放资源。
有了文件上传,那文件下载其实也很简单,就是将整个过程反过来就行了。文件上传、下载无非都是一个文件读取、传输、写入的过程,其中文件读取和写入就是传统的io输入、输出流操作,网络数据传输就交给了Socket。客户端和服务端都各自持有一个Socket实例,要想向对方发送内容,就使用Socket.getOutputStream得到输出流,写入内容即可;要想获取对方发送的内容,就使用Socket.getInputStream得到输入流,读取内容即可。
可以看到,我们目前还是在客户端和服务端之间通讯,也就是常说的CS架构。而我们实际使用更多的是浏览器发送一个请求,服务端响应,也就是BS架构。而java语言最大的特点也是跨平台,跨语言;对网络编程也有良好的支持。所以经常使用Java来开发服务端,使用HTML/JavaScript等开发前端,用浏览器访问,做BS架构的应用。
CS架构,必须我们实现客户端,通过socket通讯;其实BS架构也是一样的,浏览器与后端服务之间通讯也是通过socket,http协议底层采用tcp/ip协议。一个请求的完整过程是:由浏览器建立一个socket连接,向服务端写入url和参数等内容;服务端等待连接建立后拿到socket,通过socket读取浏览器发送过来的内容,处理完毕,向浏览器写入响应内容,并且为了适配浏览器的规范,还要加入特殊的头信息。
public class BSServerDemo {
private static ExecutorService service = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
//循环执行,持续监听客户端连接事件
while (true){
Socket socket = serverSocket.accept();
//监听到一个客户端连接(浏览器发起一次请求),开启一个子线程处理请求
service.execute(new RequestTask(socket));
}
}
private static class RequestTask implements Runnable{
private Socket socket;
public RequestTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream inputStream = socket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
//读取浏览器发送过来的第一行内容
String line = reader.readLine();
//解析,拿到请求方式、请求路径和参数
String[] info = line.split(" ");
System.out.println("请求方式是:"+info[0]);
String url = info[1].substring(1);
String path = url;
String params = "";
if (url.contains("?")){
params = url.substring(url.indexOf("?"));
path = url.substring(0,url.indexOf("?"));
System.out.println("请求参数是:"+params);
}
System.out.println("请求路径是:"+path);
//获取socket输入流,回应浏览器
OutputStream outToBrowser = socket.getOutputStream();
//由于是向浏览器响应,需要特殊格式,写入特殊格式头,否则浏览器不会解读回应的内容
outToBrowser.write("HTTP/1.1 200 OK\r\n".getBytes());
outToBrowser.write("Content-type:text/html;charset=utf-8\r\n".getBytes());
outToBrowser.write("\r\n".getBytes());
if (path.equals("hello")){
//响应hello请求,注意浏览器会将请求url进行url编码(UTF-8)后再发送过来
String[] split = params.split("=");
//中文参数需要经过url解码后转换为具体的字符串
String name = URLDecoder.decode(split[1], "UTF-8");
outToBrowser.write(("你好"+name+",欢迎回来").getBytes());
reader.close();
socket.close();
} else if (path.contains(".html")){
//创建文件输入流,读取文件
FileInputStream fis = new FileInputStream(path);
//读取浏览器想获取的对应路径文件,并写入socket输入流,响应浏览器
byte[] bytes = new byte[1024];
int len = 0;
while ((len = fis.read(bytes)) != -1){
outToBrowser.write(bytes,0,len);
}
fis.close();
reader.close();
socket.close();
} else {
outToBrowser.write("没有对应的资源".getBytes());
reader.close();
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
启动后台服务之后,在浏览器中输入:
如上图,此时后台服务就响应了一串字串,并且拿到了传递的参数。
如上图,浏览器发起请求,后端服务拿到具体路径后,读取本地文件(从《Java基础篇--IO》我们知道此时相对路径加上运行路径就是绝对路径),响应给浏览器(其实就是浏览器从服务器下载hello.html文件)。
上面的服务端流程梳理如下:
1.启动ServerSocket,绑定8080端口,持续监听浏览器的连接请求;
2.一旦监听到浏览器的连接,就开启一个子线程执行这个请求的具体业务;
3.通过socket的inputstream得到浏览器发送过来的url和具体参数等信息;
4.处理完毕,通过socket的outputstream向浏览器发送响应数据,为了遵循浏览器规范,特意先写入响应头。
从这里也验证了,浏览器发起请求其实就是通过socket与服务端建立连接,然后通过socket进行网络通讯。
整个写下来,发现上面的服务端demo其实也不是很复杂,就是一个BIO+多线程编程而已,原来后台服务也不神秘呀。难道后台服务就是一个ServerSocket的事?而且讲到现在我们连Tomcat都还没接触到!
很显然,后台服务这样实现肯定是不行的,原因主要有几点:
1.首先BIO是阻塞IO,服务端监听客户端连接的过程中主线程阻塞无法做其它的事情;
2.每一个客户端连接来了就会起一个子线程去执行具体业务,Demo中是用了一个固定大小为10的线程池来作为执行池,当请求非常多的时候就会存在请求排队,响应延迟现象;
3.我们需要在Task中去根据获取到的url来执行相应的业务,每增加一个请求路径,就需要修改Task代码,增加一个else if判断,这与开闭原则不符;
4.随着请求路径增多,Task的else if判断越来越多,整个task代码将会越来越多,可以预见最后一个Task动不动就有好几万行,阅读和维护起来将有多痛苦。
显然上面的通过一个ServerSocket,自己编码实现后台服务的方案存在很多缺点,而且每次都需要自己编写同样的一部分代码(ServerSocket启动,监听连接事件,请求参数获取,头信息写入等);所以大佬们为java web封装了一套规范,将公有代码进行封装,极大的简便了编码,让我们码农只需要关注后台业务逻辑开发即可——这也就是接下来要讲的Servlet和Servlet容器方案。
上面的文件上传和简单的BS服务端Demo,都是我们自己编写一个java程序,通过main方法入口,启动应用;服务端都是依赖ServerSocket实现的,通讯依赖Socket。这和我们常见的后台服务完全不一样,我们通常都是一个Servlet,加一个web.xml(如果用注解的话,连web.xml都不需要),然后部署到Tomcat,一个后台服务就完成了。那一个Servlet就是后台服务吗?我们从具体代码一探究竟。
还是和上面的ServerSocket服务一样,一个是拿到参数,然后返回字串;一个是拿到具体地址,然后返回html页面。这次改用Servlet实现,依赖Tomcat容器运行。代码如下:
public class HelloServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String method = request.getMethod();
System.out.println("请求方式是:"+method);
String name = request.getParameter("name");
System.out.println("请求参数是:"+name);
String uri = request.getRequestURI();
System.out.println("请求uri是:"+uri);
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("你好"+name+",欢迎回来");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {}
}
public class HtmlServlet implements Servlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setContentType("text/html;charset=utf-8");
String uri = request.getRequestURI();
System.out.println("uri:"+uri);
String path = request.getParameter("path");
//创建文件输入流,读取文件
FileReader fr = new FileReader(path);
//读取浏览器想获取的对应路径文件,并写入socket输入流,响应浏览器
char[] data = new char[258];
int len = 0;
while ((len = fr.read(data)) != -1){
response.getWriter().write(data,0,len);
}
fr.close();
}
@Override
public String getServletInfo() {
return null;
}
@Override
public void destroy() {}
}
hello
servlet.HelloServlet
5
hello
/hello
html
servlet.HtmlServlet
1
html
/html
运行结果如下图:
如上代码,只是实现了两个类,两个类都实现了一个接口“Servlet”;然后不管是读取请求参数还是写回响应数据,都是使用两个已经提供好的对象ServletRequest和ServletResponse。再加上一份web.xml配置文件,之后就是由Idea根据配置部署在Tomcat中运行即可。
有没有发现使用Servlet实现后台服务好简单,不需要启动ServerSocket监听,也不需要开启子线程处理浏览器请求,不管是获取请求参数还是写回响应数据,都有对象方法支持,甚至我们连Main方法入口都没有。其实上述这些东西都是由Tomcat提前做好了,这也正是Servlet容器需要做的主要工作,Servlet程序需要运行就必须依赖Servlet容器。
对比上面的两个后台服务,可以梳理出Tomcat等Servlet容器主要做了以下几件事情:
1.启动ServerSocket监听客户端连接请求;
2.根据web.xml配置,实现请求路径和servlet实例之间的映射,并初始化创建servlet实例;
3.封装socket.inputstream成ServletRequest对象,并实现对应方法;
4.封装socket.outputStream成ServletResponse对象,并实现对应方法;
5.处理请求线程池管理、请求排队、拒绝策略等;
6.根据路径映射找到servlet实例处理具体请求。
按照上面梳理的主要事项,我们一步一步实现后,其实就是一个Servlet容器。当揭开Tomcat的神秘面纱后,发现其实Tomcat之类的Servlet容器也是可以慢慢理解其核心的,甚至我们自己来封装实现,说不定也能写出比Tomcat更好的Servlet容器呢!
容器入口:
/**
* 简单实现Tomcat
* 1.读取配置,初始化参数、线程池,servlet实例(自定义的Servlet)与路径映射等
* 2.启动ServerSocket,监听连接事件
* 3.封装request和response对象
* 4.找到具体的servlet实例,执行对应方法
*/
public class MyTomcat {
private static int port = 8080;
public static final ConcurrentHashMap servletMapping = new ConcurrentHashMap();
private static ExecutorService pool;
public static void main(String[] args) {
try {
//1.初始化参数、线程池等
init();
//2.启动ServerSocket,监听连接事件
ServerSocket server = new ServerSocket(port);
while (true){
//2.1 持续监听客户端连接
Socket socket = server.accept();
//2.2 提交请求task,交给线程池处理
pool.execute(new SocketTask(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void init(){
//初始化线程池
pool = Executors.newFixedThreadPool(100);
//读取web.xml,初始化Servlet实例和路径映射
InputStream resourceAsStream = MyTomcat.class.getClassLoader().getResourceAsStream("web.xml");
SAXReader saxReader = new SAXReader();
try {
Document document = saxReader.read(resourceAsStream);
Element rootElement = document.getRootElement();
List elements = rootElement.elements();
for (int i = 0, length=elements.size(); i < length; i++) {
Element element = elements.get(i);
List es = element.elements();
for (int j = 0, lgth=es.size(); j < lgth; j++) {
Element element2 = es.get(j);
String ename1 = element2.getName().toString();
if ("servlet-name".equals(ename1) && "servlet".equals(element.getName().toString())) {
String servletName = element2.getStringValue();
Element ele2 = element.element("servlet-class");
String classname = ele2.getStringValue();
List elements2 = rootElement.elements("servlet-mapping");
for (int k = 0, lk=elements2.size(); k < lk; k++) {
Element element4 = elements2.get(k);
List es3 = element4.elements();
for (int op = 0, opp=es3.size(); op < opp; op++) {
if ("servlet-name".equals(es3.get(op).getName().toString())
&& servletName.equals(es3.get(op).getStringValue())) {
Element element7 = element4.element("url-pattern");
String urlPattern = element7.getStringValue();
servletMapping.put(urlPattern, (MyServlet) Class.forName(classname).newInstance());
System.out.println("==> 加载 "+ classname + ":" +urlPattern);
}
}
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != resourceAsStream) {
try {
resourceAsStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
请求处理类:
/**
* 请求处理类
* 1.封装request和response对象
* 2.根据请求url查找容器,找到则调用servlet实例方法处理请求;未找到则响应错误提示
*/
public class SocketTask implements Runnable {
private Socket socket;
public SocketTask(Socket socket) {
this.socket = socket;
}
public void run() {
try {
// 封装自定义request对象
MyServletRequest request = new MyServletRequest(socket.getInputStream());
// 封装自定义response对象
MyServletResponse response = new MyServletResponse(socket.getOutputStream());
String url = request.getUrl();
// 从映射mapping中获取servlet实例,处理具体请求
MyServlet servlet = (MyServlet) MyTomcat.servletMapping.get(url);
if (null != servlet) {
// 容器中找到了对应的servlet实例,调用实例方法处理请求
servlet.service(request, response);
} else {
// 容器中不存在处理该请求的Servlet
OutputStream outputStream = response.getOutputStream();
outputStream.write((MyServletResponse.RESPONSE_HEADER + "Welcome! error: Cannot find the servlet!").getBytes());
outputStream.flush();
outputStream.close();
}
if (!"/favicon.ico".equals(url)) {
// 简单记录我们自己的访问日志
//logRecord();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != socket) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
自定义的Request类:
/**
* 自定义的request对象
* 1.拿到处理类传递过来的inputStream,读取浏览器发送过来的信息
* 2.解析发送过来的信息,拿到请求方式、URL、参数等
* 3.对参数进行URL解码,防止中文乱码
*/
public class MyServletRequest {
// 请求方式
private String method;
// 请求URL
private String url;
// 携带参数
private Map paramMap = new HashMap();
public MyServletRequest(InputStream inputStream) {
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
try {
String line = bufferedReader.readLine();
if (null != line && line.length() > 0) {
String[] split = line.split(" ");
if (split.length == 3) {
this.method = split[0];
String allUrl = split[1];
if (allUrl.contains("?")) {
this.url = allUrl.substring(0, allUrl.indexOf("?"));
String params = allUrl.substring(allUrl.indexOf("?") + 1);
String[] paramArray = params.split("&");
for (String param : paramArray) {
String[] paramValue = param.split("=");
if (paramValue.length == 2) {
paramMap.put(paramValue[0], paramValue[1]);
}
}
} else {
this.url = allUrl;
}
if (allUrl.endsWith("ico")) {
return;
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public String getMethod() {
return method;
}
public String getUrl() {
return url;
}
public String getParameter(String name){
try {
String nameEncode = paramMap.get(name);
//防止中文乱码,进行URL解码
String nameStr = URLDecoder.decode(nameEncode, "utf-8");
return nameStr;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
}
自定义的response类:
/**
* 自定义的response对象
* 1.拿到处理类传递过来的outputStream,封装为BufferWriter,便于写入内容;
* 2.写入特殊的头信息,适配浏览器规范;
*/
public class MyServletResponse {
private OutputStream outputStream;
private BufferedWriter bufferedWriter;
// 添加Response响应头
public static final String RESPONSE_HEADER=
"HTTP/1.1 200 \r\n"
+ "Content-Type: text/html;charset=utf-8\r\n"
+ "\r\n";
public MyServletResponse(OutputStream outputStream) {
this.outputStream = outputStream;
this.bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
//为了符合浏览器规范,写入特殊头信息
try {
bufferedWriter.write(RESPONSE_HEADER);
} catch (IOException e) {
e.printStackTrace();
}
}
public OutputStream getOutputStream() {
return outputStream;
}
public BufferedWriter getWriter(){
return bufferedWriter;
}
}
自定义Servlet(满足自定义容器规范):
/**
* 自定义的Servlet
* 与MyTomcat对应,自定义的Servlet容器仅识别自定义的Servlet
*/
public abstract class MyServlet {
public void service(MyServletRequest request, MyServletResponse response) {
if ("GET".equalsIgnoreCase(request.getMethod())) {
doGet(request, response);
} else {
doPost(request, response);
}
}
public abstract void doGet(MyServletRequest request, MyServletResponse response);
public abstract void doPost(MyServletRequest request, MyServletResponse response);
}
至此,一个自定义的支持MyServlet的容器就完成了。
接下来我们要开发BS架构的后台服务,只需要实现具体的类(继承MyServlet),然后在web.xml中做好mapping配置。
public class HelloServlet extends MyServlet {
public void doGet(MyServletRequest request, MyServletResponse response) {
doPost(request,response);
}
public void doPost(MyServletRequest request, MyServletResponse response) {
String url = request.getUrl();
System.out.println("url:"+url);
String name = request.getParameter("name");
try {
response.getWriter().write("你好"+name+",欢迎回来!");
response.getWriter().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class HtmlServlet extends MyServlet {
public void doGet(MyServletRequest request, MyServletResponse response) {
doPost(request,response);
}
public void doPost(MyServletRequest request, MyServletResponse response) {
try {
String uri = request.getUrl();
System.out.println("uri:"+uri);
String path = request.getParameter("path");
//创建文件输入流,读取文件
FileReader fr = new FileReader(path);
//读取浏览器想获取的对应路径文件,并写入socket输入流,响应浏览器
char[] data = new char[258];
int len = 0;
while ((len = fr.read(data)) != -1){
response.getWriter().write(data,0,len);
}
fr.close();
response.getWriter().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
html
com.zst.servlet.HtmlServlet
html
/html
hello
com.zst.servlet.HelloServlet
hello
/hello
运行MyTomcat,浏览器测试如下图:
以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!