Servlet【转】

以下转载和参考自Servlet进阶 - 廖雪峰的官方网站

  Tomcat、Jetty、GlassFish等Web服务器用来提供TCP连接处理、HTTP协议解析处理、Servlet容器等功能。HTTP协议解析处理包括识别正确和错误的HTTP请求(开始行、Header)、处理开始行和各个请求头等。Servlet是在Web服务器中运行的,它用来处理HTTP数据,比如将HTTP数据包装成Servlet对象后在Servlet中对HTTP数据进行处理,Servlet由Web服务器创建其实例运行,所以Web服务器也称为Servlet容器。使用Servlet可以使我们很方便的处理HTTP数据,可以将Servlet代码(实现对HTTP数据的处理)打成包以后提供给Web服务器使用。Servlet属于JavaEE中的组件。

1、简单Servlet实现  

  ①、下面是一个简单的Servlet的代码实现,使用Servlet需要引入其Jar包,可以通过maven来下载相关的依赖,如下所示,需要注意的是Servlet的打包类型不是jar而是war(Java Web Application Archive),所以<packaging>为war,而且Servlet的依赖库是在编译的时候使用,当编译成war包给Web服务器使用的时候,因为Web服务器中包含Servlet的Jar包,所以将<scope>指定为provided表示仅编译时使用该包,但不会打包到.war文件中。我们将生产的包名设置为hello。

  @WebServlet注解中可以对servlet进行配置,比如urlPatterns用来设置当前servlet的映射url(说明servlet能处理的路径),比如将映射url设置成"/test"的话,那么浏览器的请求应该是应用上下文(war包名)的全路径 + 映射url,即http://192.168.1.32:8080/warName/test,如果将映射url设置成"/"的话,那么浏览器的请求应该是http://192.168.1.32:8080/warName/(如果输入http://192.168.1.32:8080/warName的话自动跳转到http://192.168.1.32:8080/warName/)。一个Servlet可以映射多个url,如 @WebServlet(urlPatterns = { "/test", "/favicon.ico", "/static/*" }) 。

   一个Servlet类在服务器中只有一个实例,但对于每个HTTP请求,Web服务器会使用多线程执行请求,即Servlet容器会使用多线程来执行doGet()或doPost()方法(因为会有多个浏览器并发浏览数据,为了提升处理多个连接请求的效率,Servlet容器会使用多线程来处理客户端的请求),所以在这两个方法中使用HelloServlet类的成员的话需要注意线程安全。参数HttpServletRequest和HttpServletResponse实例是由Servlet容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题。

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(urlPatterns = "/") // WebServlet注解表示这是一个Servlet,并映射到地址/:
public class HelloServlet extends HttpServlet { //一个Servlet总是继承自HttpServlet
    @Override
    protected void doGet( //覆写doGet()或doPost()方法来处理HTTP请求,浏览器中输入URL默认为GET请求
                         HttpServletRequest req/*收到的HTTP请求*/,
                         HttpServletResponse resp/*要回复的HTTP响应*/)
                         throws ServletException, IOException {
        resp.setContentType("text/html");  // 设置响应类型:

        String name = req.getParameter("name"); //获取参数
        if (name == null) {
            name = "world";
        }

        PrintWriter pw = resp.getWriter(); // 获取实体数据输出流
        pw.write("<h1>Hello, " + name + "!</h1>"); // 写入响应实体数据

        pw.flush(); //flush强制输出
    }
}

  ②、下面还要在项目目录下的 src/main/webapp/WEB-INF/目录下创建一个web.xml描述文件,其内容如下所示,然后就可以通过mvn clean package命令来生成war文件。类似@WebServlet注解,web.xml也用来对servlet进行配置。

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
  <display-name>Archetype Created Web Application</display-name>
</web-app>

 ③、安装Tomcat:先下载安装JDK或JRE,然后Windows下的话下载对应的Tomcat版本,解压到指定目录,然后按照readme和RUNNING中的说明设置环境变量CATALINA_HOME为Tomcat所在目录,设置环境变量JAVA_HOME为JDK目录(或者JRE_HOME设置为JRE目录)。也可以下载Tomcat的安装版本,这样就不用我们自己额外配置环境变量。

   把hello.war放到Tomcat的webapps目录下,然后进入到Tomcat的bin目录,执行startup.sh(linux)或startup.bat(windows)来启动Tomcat服务器,也可以执行bin下的tomcat.exe来启动服务。这样Tomcat就开启了我们的Web服务,在浏览器输入http://192.168.1.32:8080/hello/ 就可以看到浏览器输出了“Hello, world!”。关闭Tomcat的话执行shutdown.sh或者shutdown.bat。以后新生成了.war包的话可以直接进行替换,不需要关闭Tomcat。

   一个Web服务器允许同时运行多个Web App:如果我们有另一个Servlet包的话,比如world.war,那么同样将其放到webapps目录下,然后在浏览器输入http://192.168.1.32:8080/world/的话就可以看到world包对应的网页内容。Tomcat会定时检查webapps目录下的war包文件,为其创建一个目录来存放相关的数据,可以看到webapps目录下默认有一个ROOT目录,当我们在浏览器下输入http://192.168.1.32:8080/的话使用的就是该目录下相关的数据。我们可以删除ROOT目录(需要关闭Tomcat?),设置我们生成的war包名为ROOT,这样在浏览器下只需要输入http://192.168.1.32:8080/的话浏览器显示的就是Hello World!,即不用再输入war包名了 。Servlet容器会给每个Servlet类创建唯一实例来区分各个Web App,如下所示:

  

  Servlet容器为每个Web App自动创建一个唯一的ServletContext实例,这个实例就代表了Web应用程序本身。ServletContext的getContextPath()方法可以获得上下文名称,即war包名,如"hello",ROOT包的话该值为空。ServletContext的getRealPath()可以获得对应请求资源所在服务器上的目录:

@WebServlet(urlPatterns = {"/test", "/favicon.ico"})
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req,
                          HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html");

        //比如请求的是http://localhost/hello/test的话(hello为war包名),则strContextPath为"/hello"
        ServletContext ctx = req.getServletContext();
        String strContextPath = ctx.getContextPath();

        //比如请求的是http://localhost/hello/favicon.ico(hello为war包名)
        // 则strFileName为"/favicon.ico",filePath为war包目录路径+favicon.ico,如"D:\Program\apache-tomcat-9.0.60\webapps\hello\favicon.ico"
        String strFileName = req.getRequestURI().substring(strContextPath.length());
        String filePath = ctx.getRealPath(strFileName);
        if(filePath != null){
            Path path = Paths.get(filePath);
            if (!path.toFile().isFile()) { // 文件不存在
                //...
            }
        }

    }
}

  Windows下启动Tomcat后的命令行中文输出可能会乱码,解决方法为修改Tomcat 目录下的 conf 中的 logging.properties:

java.util.logging.ConsoleHandler.encoding = GBK

2、调试Servlet  

  因为Tomcat实际上就是个Java程序,所以我们可以通过添加其Jar包到项目中,然后在项目中开启Tomcat服务,这样就可以在项目中调试我们的Servlet代码。如下所示,我们通过修改原来的pom.xml来添加Tomcat的Jar包,因为Tomcat中自动引入了Servlet,所以就不用添加Servlet依赖,还要设置项目使用的JDK版本和使用的字符编码,如果Tomcat中这些设置与我们的不同的话就会报错。可以看到Tomcat依赖的scope被设置成了provided,表示仅编译时使用,运行时不使用,如果使用默认的compile的话生成的war包就会很大。

 然后我们新增一个main方法来启动Tomcat并使用我们的HelloServlet:

import org.apache.catalina.Context;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.webresources.DirResourceSet;
import org.apache.catalina.webresources.StandardRoot;

import java.io.File;

public class Main {
    public static void main(String[] args) throws Exception {
        // 启动Tomcat:
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(Integer.getInteger("port", 8080));
        tomcat.getConnector();

        // 创建webapp:
        Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
        WebResourceRoot resources = new StandardRoot(ctx);
        DirResourceSet ds = new DirResourceSet(resources,
                                  "/WEB-INF/classes",
                                                new File("target/classes").getAbsolutePath(),
                                     "/");
        resources.addPreResources(ds);
        ctx.setResources(resources);
        tomcat.start();
        tomcat.getServer().await();
    }
}

   当我们运行程序的时候需要注意,因为引入的Tomcat的依赖为provided,所以直接运行会报错找不到Tomcat相关类。所以我们应该让程序运行的时候将Tomcat相关依赖包添加到classpath,可以通过以下实现:在 工具栏-Run-Edit Configurations-Application-Main-Modify options-Use classpath of module,然后选择我们的项目名,勾选Include dependencies with "Provided" scope。这样的设置对于生成的war包大小无影响。

  经我测试,上面自定义Tomcat的代码实际上是只启动了Root webapp,因为输入http://localhost:8080/也能访问数据。SpringBoot也支持在main()方法中一行代码直接启动Tomcat,并且还能方便地更换成Jetty等其他服务器,它的启动方式和我们介绍的是基本一样的。

3、HttpServletRequest和HttpServletResponse

 

  HttpServletRequest的getRequestURL()获得的是整个请求的URL,如 "http://http://localhost:8080/"、"http://localhost:8080/favicon.ico",getRequestURI()获得的是包含包名的请求路径,如果包名为ROOT的话请求也就不包含包名,如"/"、"/test"、"hello/test"、"/favicon.ico"。

  下面为向浏览器发送一个错误响应:

    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html");  // 设置响应类型:

        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); //400错误响应
        PrintWriter pw = resp.getWriter();
        pw.write("<html><body><h1>");
        pw.write("error");
        pw.write("</h1></body></html>");
        pw.flush();
    }

4、重定向

  当在浏览器输入http://localhost:8080/warName/hi,使用以下代码的话会向浏览器回复了一个重定向,重定向的映射地址设置为"/warName/hello",浏览器会收到如下的302响应,然后浏览器会去http://localhost:8080/warName/hello请求资源。

@WebServlet(urlPatterns = "/hi")
public class HiServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String redirectToUrl = "/warName/hello";
        resp.sendRedirect(redirectToUrl); //重定向回应
    }
}



@WebServlet(urlPatterns = "/hello")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        resp.setContentType("text/html");

        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, world!!!</h1>");
        pw.flush();
    }
}
HTTP/1.1 302 Found
Location: /hello

   重定向有两种:一种是302响应,称为临时重定向,一种是301响应,称为永久重定向。两者的区别是,如果服务器发送301永久重定向响应,浏览器会缓存/hi到/hello这个重定向的关联,下次请求/hi的时候,浏览器就直接发送/hello请求了。以下为实现301永久重定向 :

resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301
resp.setHeader("Location", "/hello");

  当我们使用"/"作为映射路径的话,在浏览器输入http://localhost:8080/hello/或者http://localhost:8080/hello都能访问到资源,但第二种方式实际上是被Tomcat重定向到http://localhost:8080/hello/的。如果使用"test"作为映射路径的话,在浏览器只能输入http://localhost:8080/hello/test,输入http://localhost:8080/hello/test/的话访问出错。

  上面重定向的地址实际上是同一个Servlet下的不同映射地址,如果要重定向不同网址的话,使用下面的做法:

@WebServlet(urlPatterns = "/test")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req,
                          HttpServletResponse resp)
            throws ServletException, IOException {
        resp.setContentType("text/html");
        String site = new String("http://192.168.70.21:8080/hello/test2"); // 要重定向的新位置
        resp.setStatus(resp.SC_MOVED_TEMPORARILY); //302
        resp.setHeader("Location", site);
    }
}

5、转发

   如果想要实现一个war包下的不同映射地址之间的转换,最好使用转发而不是重定向,因为重定向多出了两个步骤:服务给浏览器发送302,然后浏览器再重新请求。如下所示ForwardServlet会将请求转发给路径为"/test"的Servlet,即HelloServlet:

@WebServlet(urlPatterns = "/forward")
public class ForwardServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException{

        req.getRequestDispatcher("/test").forward(req, resp);
    }
}
@WebServlet(urlPatterns = "/test")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        resp.setContentType("text/html");
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, world!!!</h1>");
        pw.flush();
    }
}

   转发是服务器行为,重定向是客户端行为,所以转发后浏览器地址栏不变,而重定向变成转发后的资源。

6、Session和Cookie

  如下所示,当浏览器首次请求http://localhost:8080/test的时候我们创建了两个Cookie:user和pwd。如下所示,当服务端首次调用req.getSession()的时候会创建名为JSESSIONID的Cookie并将其发送给浏览器,当浏览器再次请求的时候(非首次请求),会在请求头里将JSESSIONID和其它自定义的cookie发送给服务器,这个JSESSIONID就相当于是该用户与服务端会话的ID。HttpServletRequest的getSession()方法返回的是HttpSession对象,在Servlet中总是通过HttpSession访问当前Session,第一次调用req.getSession()时,Servlet容器就会自动创建一个名为JSESSIONID的Cookie。

@WebServlet(urlPatterns = "/test")
public class TestServlet  extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String user = (String) req.getSession().getAttribute("user"); //获得名为user的Cookie值
        String pwd = (String) req.getSession().getAttribute("pwd"); //获得名为pwd的Cookie值
        if (user == null) { //cookie不存在,创建
            req.getSession().setAttribute("user", "leon"); //创建名为user的Cookie
            req.getSession().setAttribute("pwd", "123456"); //创建名为pwd的Cookie
        } else { //cookie存在,将其删除
            req.getSession().removeAttribute("user");
            req.getSession().removeAttribute("pwd");
        }
    }
}

    上面是直接将用户名(用户ID)作为cookie值来进行身份认证,我们可以将用户名或用户ID加密后来作为cookie值(关于加密的具体可以参考https://www.cnblogs.com/milanleon/p/8929685.html中MD5、SHA-1部分),服务器将此cookie值与对应的用户保存起来(且服务器也需要进行有效期管理)以供下次浏览页面时的身份认证。cookie认证的具体可以参考http随笔这篇文章中用户身份认证-基于表单认证部分。

  下面为在Servlet下对Session的支持,下面的四个Servlet在同一个war包hello中,当浏览http://localhost:8080/hello/signin会进入如下的登录页面,输入正确的密码后点击Sign In按钮的话会进入到SignInServlet的POST请求处理,在其中验证输入的密码,正确的话保存用户名到Session,并跳转到http://localhost:8080/hello/。下次用户浏览http://localhost:8080/hello/index页面的时候,浏览器自动带了Session保存的用户名,所以会显示一个登出的链接,其链接地址为http://localhost:8080/hello/signout,在这个登出的页面中将Session保存的用户名删除:

@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {

    // GET请求
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Sign In</h1>");
        pw.write("<form action=\"/signin\" method=\"post\">"); //创建表单,提交动作为向"/hello/signin"发送POST请求
        pw.write("<p>Username: <input name=\"username\"></p>"); //输入栏username
        pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>"); //密码输入栏password
        pw.write("<p><button type=\"submit\">Sign In</button> <a href=\"/\">Cancel</a></p>"); //提交按钮、到"/hello"的链接按钮
        pw.write("</form>");
        pw.flush();
    }

    // POST请求
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String name = req.getParameter("username");
        String password = req.getParameter("password");
        String expectedPassword = "123456";
        if (expectedPassword != null && expectedPassword.equals(password)) { // 登录成功
            req.getSession().setAttribute("user", name); //将用户名放入当前HttpSession
            resp.sendRedirect("/"); //重定向到"/hello"
        } else { //登录失败
            resp.sendError(HttpServletResponse.SC_FORBIDDEN); //返回403错误
        }
    }
}

 

@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        resp.setContentType("text/html");
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Hello, world!!!</h1>");
        pw.flush();
    }
}

 

@WebServlet(urlPatterns = "/index")
public class IndexServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        resp.setHeader("X-Powered-By", "JavaEE Servlet");
        String user = (String) req.getSession().getAttribute("user"); //从HttpSession获取当前用户名
        if (user != null) { //已登录
            PrintWriter pw = resp.getWriter();
            pw.write("<p><a href=\"/signout\">Sign Out</a></p>"); //显示登出链接
            pw.flush();
        }else{ //未登录
            resp.sendRedirect("/signin"); //跳转到"/signin"
        }
    }
}

 

@WebServlet(urlPatterns = "/signout")
public class SignOutServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        req.getSession().removeAttribute("user"); //从HttpSession移除用户名,下次发送的Session数据不再包含它
        resp.sendRedirect("/signin"); //跳转到"/signin"
    }
}

  上面是通过HttpServletRequest来添加Cookie的,而且默认在关闭浏览器后Cookie会失效。我们也可以通过HttpServletResponse来创建Cookie,可以自定义Cookie的有效时间等,如下所示。如果想要删除浏览器上的某个cookie的话,可以添加同名的cookie到HttpServletResponse,并且通过setMaxAge设置其有效期为0。

@WebServlet(urlPatterns = "/test")
public class TestServlet  extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

        String strCookieValue = "";
        String strResponseBody = "";

        Cookie[] cookies = req.getCookies();  // 获取请求附带的所有Cookie
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("lang")) { //存在名为lang的cookie
                    strCookieValue = cookie.getValue(); // 获得lang Cookie的值:
                    strResponseBody = "<h1>has cookie lang</h1>";
                    break;
                }
            }
        }

        if (strCookieValue.isEmpty()) { //不存在名为lang的cookie
            strResponseBody = "<h1>No cookie</h1>";

            Cookie cookie = new Cookie("lang", "en"); //创建名为lang的cookie,值为"en"
            cookie.setPath("/"); // 该Cookie生效的路径范围,如果是setPath("/user/"),那么浏览器只有在请求以/user/开头的路径时才会附加此Cookie
            cookie.setMaxAge(10); // 该Cookie有效期: 86400秒=1天,不调用该方法进行设置的话关闭浏览器Cookie即失效
            cookie.setSecure(true); //如果是HTTPS的话才需要该行
            resp.addCookie(cookie); // 将该Cookie添加到响应
        }
    }
}

   为了防止客户端的cookie信息被窃取使用,可以设置cookie的以下属性:

    ①、设置HttpOnly属性,这样就不允许JS脚本读取cookie信息,防止了跨站脚本攻击(XSS)。

    ②、设置secure属性,这样能够可以防止通过抓包工具来获得cookie值,因为设置了该属性后cookie只会在https中发送给服务端,https协议会加密http包中数据。

    ③、设置samesite属性为strict或lax,防止跨站请求伪造(CSRF)。浏览器在请求A网站时,只要浏览器存储了适用于A网站的Cookie,就会带上它发起请求。在网站B中,如果有一个按钮是向网站A发起请求,那么仍然会带上你在A网站的Cookie将请求发出去。比如你打开了银行网站并进行了登录,然后你去浏览一个论坛,当你在这个论坛发帖的时候,其背后代码逻辑会向银行发送转账或购买物品请求,因为这个请求携带了你登录银行网站的cookie信息,所以银行会以为转账或购物就是你发的请求,这就是跨站请求伪造CSRF。通过给cookie设置amesite属性,这样能够禁止跨站请求携带Cookie,从而确保了能够携带Cookie的请求一定是用户在浏览器自己的网站期间发出来的。

  服务器将所有用户的Session都存储在内存中,如果遇到内存不足的情况,就会把部分不活动的Session序列化到磁盘上,所以放入的Cookie对象要小,比如是一个字符串或简单的结构体对象。

  

  上面所说的Session问题,也可以单独整一个Session服务器用来专门存储Session相关信息,所有的服务从Session服务器里获得Session信息,如下图所示。关于反向代理的更多内容,可以参考 与http协作的web服务器 里的反向代理和负载均衡部分。

   cookie和web storage:localStorage用来存储数据到本地,可以存储大容量数据,除非是通过js删除,或者清除浏览器缓存,否则localStorage是永远不会过期。cookie用于浏览器和服务端间的信息传递(cookie数据会随请求发送到服务端),cookie数据大小有限制,cookie默认关闭浏览器清空,也可以设置其有效期。 sessionStorage用于本地存储一个会话中的数据,这些数据只有在同一个会话中的页面才能访问,并且当会话结束后,数据也随之销毁。所以sessionStorage仅仅是会话级别的存储,而不是一种持久化的本地存储。

7、再谈映射URL和ROOT包

  前面说过映射url的作用,比如对于下面的IndexServlet,我们输入http://localhost:8080/warName/index的话才能访问到IndexServlet页面,而对于HelloServlet,输入http://localhost:8080/warName的话就能访问到HelloServlet页面(实际上会跳转到http://localhost:8080/warName/)。映射到 "/" 的Servlet相当于映射到了"/*",我们输入不存在的地址如http://localhost:8080/warName/none的话相当于输入了http://localhost:8080/warName,当然如果该地址存在的话则会进入对应页面,如http://localhost:8080/warName/index。

@WebServlet(urlPatterns = "/index")
public class IndexServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
       //......
    }
}
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {

       //......
    }
}

  如果将war包名设置为ROOT的话,那么就不用再输入war包名了,如上面的的两个Servlet,访问其页面的话只需要输入http://localhost:8080/index 和http://localhost:8080。

  Tomcat默认使用端口号8080(https为8443),可以打开其安装目录下的server.xml来进行修改:找到“ <Connector port="8080" ....../>”将其修改为80重启Tomcat,这样就不用再输入端口号了。可以修改C:\Windows\System32\下的hosts文件,比如添加一行“127.0.0.1 www.test.com”,这样就可以在浏览器中用域名替代IP了。如果我们使用http://127.0.0.1:8080/test无法访问Tomcat服务的话,可以换成http://localhost:8080/test试试。

  另外需要注意的是,将映射设置成指定路径的话,输入地址的时候最后可以不加'/',比如http://localhost:8080/warName/index。如果将映射设置成"/"的话,比如上面的HelloServlet,那么访问其页面应该是http://localhost:8080/warName/,因为如果输入http://localhost:8080/warName的话服务也会重定向到http://localhost:8080/warName/,但是如果使用ROOT的话那么直接输入http://localhost:8080就能访问HelloServlet,服务器不会再重定向到http://localhost:8080/。

  如果我们没有添加指定路径的Servlet处理类的话,即请求了不存在的Servlet,那么这个路径的请求就会自动转到"/"路径的Servlet类来处理。比如在浏览器输入http://localhost:8080的话实际上会发送两个URL请求,一个是http://localhost:8080,另一个是http://localhost:8080/favicon.ico,如果我们只编写了"/"路径的Servlet处理类(如下所示的HelloServlet)而没有编写"/favicon.ico"路径的Servlet处理类的话,那么在浏览器输入http://localhost:8080的话HelloServlet的doGet()方法会被调用两次。如果我们编写了"/favicon.ico"路径的Servlet处理类的话(如下所示的FileServlet),那么HelloServlet的doGet()被调用一次,FileServlet的doGet()被调用一次。favicon.ico是浏览器上显示的网站图标,可以读取一个图标返回给浏览器来显示,。

@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req
                         HttpServletResponse resp) throws ServletException, IOException {
        ......
    }
}


@WebServlet(urlPatterns = "/favicon.ico")
public class FileServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req,
                         HttpServletResponse resp) throws ServletException, IOException {

        Path path = Paths.get("favicon.ico");
        if (!path.toFile().isFile()) { // 文件不存在
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //读取文件并发送给浏览器
        OutputStream output = resp.getOutputStream();
        try (InputStream input = new BufferedInputStream(new FileInputStream("favicon.ico"))) {
            input.transferTo(output);
        }
        output.flush();
    }
}

8、处理用户上传的数据

  如下的UploadServlet可以处理用户上传的数据——将数据回显给用户。可以使用curl命令对其进行测试,如下使用curl命令上传了数据"test-data":

@WebServlet(urlPatterns = "/upload/file")
public class UploadServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 读取Request Body:
        InputStream input = req.getInputStream();
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        for (;;) {
            int len = input.read(buffer);
            if (len == -1) {
                break;
            }
            output.write(buffer, 0, len);
        }
        // TODO: 写入文件:
        // 显示上传结果:
        String uploadedText = output.toString(StandardCharsets.UTF_8);
        PrintWriter pw = resp.getWriter();
        pw.write("<h1>Uploaded:</h1>");
        pw.write("<pre><code>");
        pw.write(uploadedText);
        pw.write("</code></pre>");
        pw.flush();
    }
}

$ curl http://localhost:8080/upload/file -v -d 'test-data' \
  -H 'Signature-Method: SHA-1' \
  -H 'Signature: 7115e9890f5b5cc6914bdfa3b7c011db1cdafedb' \
  -H 'Content-Type: application/octet-stream'

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /upload/file HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Signature-Method: SHA-1
> Signature: 7115e9890f5b5cc6914bdfa3b7c011db1cdafedb
> Content-Type: application/octet-stream
> Content-Length: 9
> 
* upload completely sent off: 9 out of 9 bytes
< HTTP/1.1 200 
< Transfer-Encoding: chunked
< Date: Thu, 30 Jan 2020 13:56:39 GMT
< 
* Connection #0 to host localhost left intact
<h1>Uploaded:</h1><pre><code></code></pre>
* Closing connection 0

9、总结

   使用Web服务器可以免去手写TCP连接处理、HTTP协议解析等服务器功能代码。Web服务器中使用Servlet来处理HTTP请求,所以我们可以继承Servlet来编写处理HTTP请求的代码:重写doGet()来处理GET请求,重写doPost()处理POST请求。我们写好的Servlet可以打成包后交给Web服务器使用。

   Servlet的@WebServlet注解可以用来设置映射地址,如@WebServlet(urlPatterns = "/test")对应"http://192.168.1.32:8080/warName/test",@WebServlet(urlPatterns = "/")对应"http://192.168.1.32:8080/warName/",如果将包名设置为ROOT的话,那么@WebServlet(urlPatterns = "/test")对应"http://192.168.1.32:8080/test",@WebServlet(urlPatterns = "/")对应"http://192.168.1.32:8080/"。

   使用HttpServletRequest或者HttpServletResponse都可以添加Cookie,但通过HttpServletRequest添加的Cookie默认在关闭浏览器后Cookie会失效,通过HttpServletResponse添加的Cookie可以设置其有效期。

   同一个war包下的Servlet转换使用转发,不同war包下的Servlet转换使用重定向。

10、Tomcat、Apache、JBoss

    Tomcat是一种轻量级Web服务器,其运行时占用的系统资源小,且支持负载平衡与邮件服务等,适合并发访问用户不是很多的场合。

    Apache也是Web服务器,其对Html静态网页处理上比Tomcat要好,但不支持ASP、PHP、JSP等动态网页。人们通常将Apache和Tomcat集成到一起:如果客户端请求的是静态页面,则只需要Apache服务器响应请求;如果客户端请求动态页面,则是Tomcat服务器响应请求;因为jsp是服务器端解释代码的,这样整合就可以减少Tomcat的服务开销。也可以使用JBoss来统一对静态、动态内容进行支持。

   JBoss是应用服务器,其不仅包括Web服务,还可以处理JMS、EJB。JBoss内嵌了Tomcat作为其Servlet容器引擎来支持servlet,JSP,并对Tomcat加以调优,解决了Tomcat在活动连接支持、静态内容、大文件和HTTPS等方面的短点。

11、长连接性能问题   

   基于Servlet的线程池模型不能高效地支持成百上千的长连接(如WebSocket),Java提供了NIO能充分利用Linux系统的epoll机制高效支持大量的长连接,但是直接使用NIO的接口非常繁琐,通常我们会选择基于NIO的Netty服务。基于Netty开发的话,我们还可以进一步选择封装了Netty的Spring WebFlux或者Vert.x来进行开发。

  WebFlux是Spring 5推出的Reactor框架,其是一个异步非阻塞式的 Web 框架,底层基于Netty实现,使用它可以使系统显著提高吞吐量和伸缩性。和Spring MVC一样,Spring WebFlux可以使用Tomcat, Jetty, Undertow Servlet 容器(WebFlux 默认情况下使用 Netty 作为服务器),WebFlux也可以使用Spring MVC 注解,方便我们在两个 Web 框架中自由转换。WebFlux不支持 MySql。  

  Vert.x是基于Netty和NIO2编写的,它的实现原理与Node.JS的原理非常类似。关于Vert.x的使用可以参考 设计推送系统 - 廖雪峰的官方网站 。

   使用BIO的Tomcat并发能力比vert.x并不会很差,事实上,springboot集成的tomcat, 缺省的运行模式就是NIO。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值