以下转载和参考自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()获得的是不包括协议、主机名和端口号的请求路径,如"/"、"/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、post中的参数
对于post方式的请求,使用HttpServletRequest::getParameter()可能无法获得请求的参数,因为post请求的参数是放到请求体里的,或者说post请求是不带参数的,只是附带post数据。在post请求中获取附带数据如下所示:
@PostMapping("/test")
public void test(HttpServletRequest req) throws IOException {
ServletInputStream inputStream = req.getInputStream();
String bodyData = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
}
10、总结
使用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转换使用重定向。
11、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等方面的短点。
12、长连接性能问题
基于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。