对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
应用进程被阻塞,直到数据从内核缓冲区复制到应用进程缓冲区中才返回。
在阻塞的过程中,其它应用进程还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其它应用进程还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率会比较高。
data = socket.read()
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率比较低。
selector线程轮询多个socket状态,只有socket有读写事件时,才会通知用户线程进行 I/O。
只需一个线程就能管理多个socket,并且在系统内核中检查状态,大大节约系统资源,适用于连接数多单消息体不大的情况。
linux有 select,poll,epoll
用户需要 I/O 时,系统为该请求对应的socket注册一个信号函数,然后立即返回;
内核数据就绪时,系统发送信号到用户线程,通知用户线程取数据。
用户线程发起一个 asynchronous read 操作到内核,内核立即返回是否请求成功;数据准备完成并转移到用户线程中后,通知用户线程,直接使用数据即可。
- 同步 I/O:将数据从内核缓冲区复制到应用进程缓冲区的阶段(第二阶段),应用进程会阻塞。
- 异步 I/O:第二阶段应用进程不会阻塞。
同步 I/O 包括阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O ,它们的主要区别在第一个阶段。
非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。
selector 监听 channel 的事件,使用 buffer 进行读写
Channel:类似于Stream,但是是双向的,可读可写。实现类:FileChannel、DatagreamChannel、SocketChannel、ServerSocketChannel
Buffer:内部通过一个连续的字节数组存储数据。Channel 在文件、网络上的读写都必须经过 buffer。实现类:ByteBuffer、IntBuffer、CharBuffer等
Selector:用于检测在多个注册的 Channel 上是否有 I/O 事件发生。
NIO 与传统 I/O 的区别:
- 传统 I/O 面向流,NIO面向缓存区。
- 传统 I/O 阻塞式,NIO非阻塞。
Server端
public class TCPServer {
public static void main(String[] args) throws IOException {
// 监听指定端口
ServerSocket ss = new ServerSocket(6666);
System.out.println("server is running...");
for (;;) {
// 每当有新的客户端连接进来后,就返回一个Socket实例
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
// 每当收到新连接后,就创建一个新线程进行处理
Thread t = new Handler(sock);
t.start();
}
}
}
class Handler extends Thread {
Socket sock;
public Handler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
// Socket流
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}
private void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
writer.write("hello\n");
// 调用flush()强制把缓冲区数据发送出去
writer.flush();
for (;;) {
String s = reader.readLine();
if (s.equals("bye")) {
writer.write("bye\n");
writer.flush();
break;
}
writer.write("ok: " + s + "\n");
writer.flush();
}
}
}
Client端
public class TCPClient {
public static void main(String[] args) throws IOException {
// 连接指定服务器和端口
Socket sock = new Socket("localhost", 6666);
try (InputStream input = sock.getInputStream()) {
try (OutputStream output = sock.getOutputStream()) {
handle(input, output);
}
}
sock.close();
System.out.println("disconnected.");
}
private static void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server] " + reader.readLine());
for (;;) {
System.out.print(">>> "); // 打印提示
String s = scanner.nextLine(); // 读取一行输入
writer.write(s);
writer.newLine();
writer.flush();
String resp = reader.readLine();
System.out.println("<<< " + resp);
if (resp.equals("bye")) {
break;
}
}
}
}
主要原因就是tcp数据传递模式是流模式,在保持长连接的时候可以进行多次的收和发。
“粘包”可发生在发送端也可发生在接收端:
1.由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
2.接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
出现”粘包”的关键在于接收方不确定将要传输的数据包的大小,因此我们可以对数据包进行封包和拆包的操作。
封包:封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的,并且它存储了包体的长度,根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
我们可以自己定义一个协议,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
Server端
public class UDPServer {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
for (;;) { // 无限循环
// 数据缓冲区:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet); // 收取一个UDP数据包
// 收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和长度
// 将其按UTF-8编码转换为String:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
System.out.println(packet.getAddress() + " :" + s);
// 发送数据:
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
packet.setData(data);
ds.send(packet);
}
}
}
Client端:
public class UDPClient {
public static void main(String[] args) throws IOException {
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口
// 发送:
byte[] data = "Hello szy".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
System.out.println("send :" + new String(data));
// 接收:
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
System.out.println("return :" + resp);
ds.disconnect();
}
}
WebSocket是一种在单个TCP连接上进行全双工通信的协议
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据
在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输
发送邮件前,我们首先要确定作为MTA的邮件服务器地址和端口号。邮件服务器地址通常是smtp.example.com
,端口号由邮件服务商确定使用25、465还是587。以下是一些常用邮件服务商的SMTP信息:
- QQ邮箱:SMTP服务器是smtp.qq.com,端口是465/587;
- 163邮箱:SMTP服务器是smtp.163.com,端口是465;
- Gmail邮箱:SMTP服务器是smtp.gmail.com,端口是465/587。
创建一个Maven工程,并把JavaMail相关的两个依赖加入进来:
<dependency>
<groupId>javax.mail</groupId>
<artifactId>javax.mail-api</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>javax.mail</artifactId>
<version>1.6.2</version>
</dependency>
通过JavaMail API连接到SMTP服务器上:
// 服务器地址:
String smtp = "smtp.qq.com";
// 登录用户名:
String username = "[email protected]";
// 登录口令:
String password = "xuhxspjtabwxbfbh";
// 连接到SMTP服务器端口:
Properties props = new Properties();
props.put("mail.smtp.host", smtp); // SMTP主机名
props.put("mail.smtp.port", "587"); // 主机端口号
props.put("mail.smtp.auth", "true"); // 是否需要用户认证
props.put("mail.smtp.starttls.enable", "true"); // 启用TLS加密
// 获取Session实例:
Session session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
// 设置debug模式便于调试:
session.setDebug(true);
发送邮件
发送邮件时,我们需要构造一个Message
对象,然后调用Transport.send(Message)
即可完成发送:
MimeMessage message = new MimeMessage(session);
// 设置发送方地址:
message.setFrom(new InternetAddress("[email protected]"));
// 设置接收方地址:
message.setRecipient(Message.RecipientType.TO, new InternetAddress("[email protected]"));
// 设置邮件主题:
message.setSubject("Hello", "UTF-8");
// 设置邮件正文:
message.setText("Hi Xiaoming...", "UTF-8");
// 发送html邮件
message.setText(body, "UTF-8", "html");
// 发送:
Transport.send(message);
发送附件
要在电子邮件中携带附件,我们就不能直接调用message.setText()
方法,而是要构造一个Multipart
对象:
Multipart multipart = new MimeMultipart();
// 内嵌图片
// 添加text:
BodyPart textpart = new MimeBodyPart();
textpart.setContent("<h1>Hello!This is SZY!</h1><p><img src=\"cid:img01\"></p>", "text/html;charset=utf-8");
multipart.addBodyPart(textpart);
// 添加image:
BodyPart imagepart = new MimeBodyPart();
imagepart.setFileName("表情包.png");
imagepart.setDataHandler(new DataHandler(new ByteArrayDataSource("D:\\img.jpg", "image/jpeg")));
// 与HTML的<img src="cid:img01">关联:
imagepart.setHeader("Content-ID", "<img01>");
multipart.addBodyPart(imagepart);
// 添加附件:
BodyPart enclosepart = new MimeBodyPart();
enclosepart.setFileName("java.md");
enclosepart.setDataHandler(new DataHandler(new ByteArrayDataSource("D:\\Java.md", "application/octet-stream")));
multipart.addBodyPart(enclosepart);
// 设置邮件内容为multipart:
message.setContent(multipart);
一个Multipart
对象可以添加若干个BodyPart
,其中第一个BodyPart
是文本,即邮件正文,后面的BodyPart是附件。
BodyPart
依靠setContent()
决定添加的内容:
-
用
setContent("...", "text/plain;charset=utf-8")
添加纯文本 -
用
setContent("...", "text/html;charset=utf-8")
添加HTML文本 -
添加附件,需要设置文件名(不一定和真实文件名一致),并且添加一个
DataHandler()
,传入文件的MIME类型。二进制文件可以用application/octet-stream
,Word文档是application/msword
,图片是image/jpeg
或image/png
通过Maven来引入Servlet API:
<!--Java Web Application Archive-->
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
实现一个最简单的Servlet:
// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
// 设置响应类型:
res.setContentType("text/html");
// 获取输出流:
PrintWriter pw = res.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}
我们还需要在工程目录下创建一个web.xml
描述文件,放到src/main/webapp/WEB-INF
目录下(固定目录结构,不要修改路径,注意大小写)。文件内容可以固定如下:
<!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>
整个工程结构如下:
web-servlet-hello
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ └── servlet
│ └── HelloServlet.java
├── resources
└── webapp
└── WEB-INF
└── web.xml
HttpServletRequest
通过HttpServletRequest
提供的接口方法可以拿到HTTP请求的几乎全部信息,常用的方法有:
- getMethod():返回请求方法,例如,
"GET"
,"POST"
; - getRequestURI():返回请求路径,但不包括请求参数,例如,
"/hello"
; - getQueryString():返回请求参数,例如,
"name=Bob&a=1&b=2"
; - getParameter(name):返回请求参数,GET请求从URL读取参数,POST请求从Body中读取参数;
- getContentType():获取请求Body的类型,例如,
"application/x-www-form-urlencoded"
; - getContextPath():获取当前Webapp挂载的路径,对于ROOT来说,总是返回空字符串
""
; - getCookies():返回请求携带的所有Cookie;
- getHeader(name):获取指定的Header,对Header名称不区分大小写;
- getHeaderNames():返回所有Header名称;
- getInputStream():如果该请求带有HTTP Body,该方法将打开一个输入流用于读取Body;
- getReader():和getInputStream()类似,但打开的是Reader;
- getRemoteAddr():返回客户端的IP地址;
- getScheme():返回协议类型,例如,
"http"
,"https"
;
此外,HttpServletRequest
还有两个方法:setAttribute()
和getAttribute()
,可以给当前HttpServletRequest
对象附加多个Key-Value,相当于把HttpServletRequest
当作一个Map<String, Object>
使用。
HttpServletResponse
HttpServletResponse
封装了一个HTTP响应。由于HTTP响应必须先发送Header,再发送Body,所以,操作HttpServletResponse
对象时,必须先调用设置Header的方法,最后调用发送Body的方法。
常用的设置Header的方法有:
- setStatus(sc):设置响应代码,默认是
200
; - setContentType(type):设置Body的类型,例如,
"text/html"
; - setCharacterEncoding(charset):设置字符编码,例如,
"UTF-8"
; - setHeader(name, value):设置一个Header的值;
- addCookie(cookie):给响应添加一个Cookie;
- addHeader(name, value):给响应添加一个Header,因为HTTP协议允许有多个相同的Header;
写入响应时,需要通过getOutputStream()
获取写入流,或者通过getWriter()
获取字符流,二者只能获取其中一个。
重定向
HttpServletResponse
提供了快捷的redirect()
方法实现302重定向:
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
// 构造重定向的路径:
String name = req.getParameter("name");
String redirectToUrl = "/hello" + (name == null ? "" : "?name=" + name);
// 发送重定向响应:
res.sendRedirect(redirectToUrl);
}
如果要实现301永久重定向,可以这么写:
resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301
resp.setHeader("Location", "/hello");
Forward
Forward是指内部转发。当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理
ForwardServlet
在收到请求后,它并不自己发送响应,而是把请求和响应都转发给路径为/hello
的Servlet,即下面的代码:
req.getRequestDispatcher("/hello").forward(req, res);
转发和重定向的区别在于,转发是在Web服务器内部完成的,对浏览器来说,它只发出了一个HTTP请求
由于 HTTP 协议是无状态的,完成操作关闭浏览器后,客户端和服务端的连接就断开了,所以我们必须要有一种机制来保证客户端和服务端之间会话的连续性,常见的,就是使用 Cookie + Session(会话) 的方式。
具体来说,当客户端请求服务端的时候,服务端会为此次请求开辟一块内存空间(Session 对象),服务端可以在此存储客户端在该会话期间的一些操作记录(比如用户信息就可以存在 Session 中),同时会生成一个 sessionID ,并通过响应头的 Set-Cookie:JSESSIONID=XXXXXXX
命令,将 seesionID 存储进客户端的 Cookie 中。
当一个用户登录成功后,我们就可以把这个用户的名字放入一个HttpSession
对象,以便后续访问其他页面的时候,能直接从HttpSession
取出用户名:
@WebServlet(urlPatterns = "/signin")
public class SignInServlet extends HttpServlet {
// 模拟一个数据库:
private Map<String, String> users = Map.of("bob", "bob123", "alice", "alice123", "tom", "tomcat");
// 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\">");
pw.write("<p>Username: <input name=\"username\"></p>");
pw.write("<p>Password: <input name=\"password\" type=\"password\"></p>");
pw.write("<p><button type=\"submit\">Sign In</button> <a href=\"/\">Cancel</a></p>");
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 = users.get(name.toLowerCase());
if (expectedPassword != null && expectedPassword.equals(password)) {
// 登录成功:
req.getSession().setAttribute("user", name);
resp.sendRedirect("/");
} else {
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
}
}
}
在IndexServlet
中,可以从HttpSession
取出用户名:
@WebServlet(urlPatterns = "/")
public class IndexServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 从HttpSession获取当前用户名:
String user = (String) req.getSession().getAttribute("user");
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
resp.setHeader("X-Powered-By", "JavaEE Servlet");
PrintWriter pw = resp.getWriter();
pw.write("<h1>Welcome, " + (user != null ? user : "Guest") + "</h1>");
if (user == null) {
// 未登录,显示登录链接:
pw.write("<p><a href=\"/signin\">Sign In</a></p>");
} else {
// 已登录,显示登出链接:
pw.write("<p><a href=\"/signout\">Sign Out</a></p>");
}
pw.flush();
}
}
如果用户已登录,可以通过访问/signout
登出。登出逻辑就是从HttpSession
中移除用户相关信息:
@WebServlet(urlPatterns = "/signout")
public class SignOutServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 从HttpSession移除用户名:
req.getSession().removeAttribute("user");
resp.sendRedirect("/");
}
}
❓ 为什么对于大型Web应用程序来说,通常需要避免使用Session机制
Session 无法在分布式存储中发挥有效的作用:客户端发送一个请求给服务器,经过负载均衡后该请求会被分发到集群中多个服务器中的其中一个,由于不同的服务器可能含有不同的 Web 服务器,而 Web 服务器之间并不能发现其他 Web 服务器中保存的 Session 信息,这样,它就会再次重新生成一个 JSESSIONID,导致之前的状态丢失。
分布式Session的解决办法
- Session Replication:Session在各个服务器上保持同步。这种方法网络和存储开销非常大
- Session Sticky:负载均衡实现会话粘滞,每次将某主机的请求转发到同一个服务器上。这种方法会使负载均衡器变为有状态,增加开销
- Session 数据集中存储:借助外部存储将 Session 数据进行集中存储。这种方法过度依赖外部存储
- ThreadLocal:线程本地内存。
实际上,Servlet提供的HttpSession
本质上就是通过一个名为JSESSIONID
的Cookie来跟踪用户会话的。除了这个名称外,其他名称的Cookie我们可以任意使用。
设置Cookie,例如,记录用户选择的语言:
@WebServlet(urlPatterns = "/pref")
public class LanguageServlet extends HttpServlet {
private static final Set<String> LANGUAGES = Set.of("en", "zh");
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String lang = req.getParameter("lang");
if (LANGUAGES.contains(lang)) {
// 创建一个新的Cookie:
Cookie cookie = new Cookie("lang", lang);
// 该Cookie生效的路径范围:
cookie.setPath("/");
// 该Cookie有效期:
cookie.setMaxAge(8640000); // 8640000秒=100天
// 将该Cookie添加到响应:
resp.addCookie(cookie);
}
resp.sendRedirect("/");
}
}
如果访问的是https网页,还需要调用setSecure(true)
,否则浏览器不会发送该Cookie
读取Cookie,例如,在IndexServlet
中,读取名为lang
的Cookie以获取用户设置的语言:
private String parseLanguageFromCookie(HttpServletRequest req) {
// 获取请求附带的所有Cookie:
Cookie[] cookies = req.getCookies();
// 如果获取到Cookie:
if (cookies != null) {
// 循环每个Cookie:
for (Cookie cookie : cookies) {
// 如果Cookie名称为lang:
if (cookie.getName().equals("lang")) {
// 返回Cookie的值:
return cookie.getValue();
}
}
}
// 返回默认值:
return "en";
}
❓ 为什么不直接把数据全部存在 Cookie 中
- Cookie 长度的限制:首先,最基本的,Cookie 是有长度限制的,这限制了它能存储的数据的长度
- 性能影响:Cookie 确实和 Session 一样可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些 Cookie,那如果 Cookie 中存储的数据比较多的话,这无疑增加了客户端与服务端之间的数据传输量,增加了服务器的压力。
- 安全性:Session 数据其实是属于服务端的数据,而 Cookie 属于客户端,把本应在 Session 中存储的数据放到客户端 Cookie,使得服务端数据延伸到了外部网络及客户端,显然是存在安全性上的问题的。当然我们可以对这些数据做加密,不过从技术来讲物理上不接触才是最安全的。
❓ 禁用Cookie怎么办
一般的做法就是把这个 sessionID 放到 URL 参数中。
JSP是Java Server Pages的缩写,它的文件必须放到/src/main/webapp
下,文件名必须以.jsp
结尾,整个文件与HTML并无太大区别,但需要插入变量,或者动态输出的地方,使用特殊指令<% ... %>
:
- 包含在
<%--
和--%>
之间的是JSP的注释,它们会被完全忽略; - 包含在
<%
和%>
之间的是Java代码,可以编写任意Java代码; - 如果使用
<%= xxx %>
则可以快捷输出一个变量的值。
JSP页面内置了几个变量:
- out:表示HttpServletResponse的PrintWriter;
- session:表示当前HttpSession对象;
- request:表示HttpServletRequest对象。
JSP在执行前首先被编译成一个Servlet。在Tomcat的临时目录下,可以找到一个hello_jsp.java
的源文件,这个文件就是Tomcat把JSP自动转换成的Servlet源码
<body>
<%-- JSP Comment --%>
<h1>Hello World!</h1>
<p>
<%
out.println("Your IP address is ");
%>
<span style="color:red">
<%= request.getRemoteAddr() %>
</span>
</p>
</body>
JSP页面本身可以通过page
指令引入Java类:
<%@ page import="java.io.*" %>
<%@ page import="java.util.*" %>
这样后续的Java代码才能引用简单类名而不是完整类名。
使用include
指令可以引入另一个JSP文件:
<html>
<body>
<%@ include file="header.jsp"%>
<h1>Index Page</h1>
<%@ include file="footer.jsp"%>
</body>
**Model:**假设我们已经编写了几个JavaBean:
public class User {
public long id;
public String name;
public School school;
}
public class School {
public String name;
public String address;
}
**Controller:**在UserServlet
中,我们可以从数据库读取User
、School
等信息,然后,把读取到的JavaBean先放到HttpServletRequest中,再通过forward()
传给user.jsp
处理:
@WebServlet(urlPatterns = "/user")
public class UserServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 假装从数据库读取:
School school = new School("No.1 Middle School", "101 South Street");
User user = new User(123, "Bob", school);
// 放入Request中:
req.setAttribute("user", user);
// forward给user.jsp:
req.getRequestDispatcher("/WEB-INF/user.jsp").forward(req, resp);
}
}
**View:**在user.jsp
中,我们只负责展示相关JavaBean的信息,不需要编写访问数据库等复杂逻辑:
<%@ page import="com.itranswarp.learnjava.bean.*"%>
<%
User user = (User) request.getAttribute("user");
%>
<html>
<head>
<title>Hello World - JSP</title>
</head>
<body>
<h1>Hello <%= user.name %>!</h1>
<p>School Name:
<span style="color:red">
<%= user.school.name %>
</span>
</p>
<p>School Address:
<span style="color:red">
<%= user.school.address %>
</span>
</p>
</body>
</html>
参数
// 通过model
@RequestParam()
// 通过/{id}
@PathVariable("id")
返回值
// 什么都不加,返回视图,进入视图解析器拼接前后缀形成页面
return "login";
modelAndView.setViewName("login");
// 重定向
return "redirect:/login";
response.sendRedirect("/login");
// 返回各种值
@ResponseBody
// 返回模板参数
request.setAttribute("msg","输入有误");
model.addAttribute("msg","输入有误");
modelMap.addAttribute("msg","输入有误");
modelAndView.addObject("msg","输入有误");
Session
// 获取session
session = request.getSession()
// 获取属性
session.getAttribute("loginUser");
// 设置session
session.setAttribute("loginUser", username);
// 设置session失效
session.invalidate();
==定义==
-
Apache Apache HTTP Server(简称Apache)是Apache软件基金会的一个开放源码的网页,它是一个模块化的服务器,可以运行在几乎所有广泛使用的计算机平台上。其属于应用服务器。 Apache支持模块多,性能稳定,Apache本身是静态解析,适合静态HTML、图片等,但可以通过扩展脚本、模块等支持动态页面等。 缺点:配置相对复杂,自身不支持动态页面。 优点:相对于Tomcat服务器来说处理静态文件是它的优势,速度快。Apache是静态解析,适合静态HTML、图片等。 (Apche可以支持PHPcgiperl,但是要使用Java的话,你需要Tomcat在Apache后台支撑,将Java请求由Apache转发给Tomcat处理。)
-
Tomcat: Tomcat 是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目。Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器。 Tomcat是应用(Java)服务器,它只是一个Servlet(JSP也翻译成Servlet)容器,可以认为是Apache的扩展,但是可以独立于Apache运行。 缺点:可以说Tomcat 只能用做java服务器 优点:动态解析容器,处理动态请求,是编译JSP/Servlet的容器。
三个端口: 8005:关闭tomcat通信接口 8009:与其他http服务器通信接口,用于http服务器集合 8080:建立http连接 用,如浏览器访问
-
Nginx Nginx是俄罗斯人编写的十分轻量级的HTTP服务器,Nginx,它的发音为“engine X”,是一个高性能的HTTP和反向代理服务器,同时也是一个IMAP/POP3/SMTP 代理服务器。其特点是占有内存少,并发能力强,易于开发,部署方便。Nginx 支持多语言通用服务器。 缺点:Nginx 只适合静态和反向代理。 优点:负载均衡、反向代理、处理静态文件优势。Nginx 处理静态请求的速度高于Apache。 Nginx有动态分离机制,静态请求直接就可以通过Nginx处理,动态请求才转发请求到后台交由Tomcat进行处理。 Tomcat结合Apache、Nginx实现高性能的web服务器
Tomcat虽然是一个servlet和jsp容器,但是它也是一个轻量级的web服务器。它既可以处理动态内容,也可以处理静态内容。不过,tomcat的最大优势在于处理动态请求,处理静态内容的能力不如apache和nginx,并且经过测试发现,tomcat在高并发的场景下,其接受的最大并发连接数是有限制的,连接数过多会导致tomcat处于"僵死"状态,因此,在这种情况下,我们可以利用nginx的高并发,低消耗的特点与tomcat一起使用。因此,tomcat与nginx、apache结合使用共有如下几点原因: 1、tomcat处理html的能力不如Apache和nginx,tomcat处理静态内容的速度不如apache和nginx。 2、tomcat接受的最大并发数有限,连接数过多,会导致tomcat处于"僵尸"状态,对后续的连接失去响应,需要结合nginx一起使用。
通常情况下,tomcat与nginx、Apache结合使用,nginx、apache既可以提供web服务,也可以转发动态请求至tomcat服务器上。但在一个高性能的站点上,通常nginx、apache只提供代理的功能,也就是转发请求至tomcat服务器上,而对于静态内容的响应,则由前端负载均衡器来转发至专门的静态服务器上进行处理。其架构类似于如下图:
在这种架构中,当haproxy或nginx作为前端代理时,如果是静态内容,如html、css等内容,则直接交给静态服务器处理;如果请求的图片等内容,则直接交给图片服务器处理;如果请求的是动态内容,则交给tomcat服务器处理,不过在tomcat服务器上,同时运行着nginx服务器,此时的nginx作为静态服务器,它不处理静态请求,它的作用主要是接受请求,并将请求转发给tomcat服务器的,除此之外,nginx没有任何作用。
==区别== 1)Nginx和tomcat的区别 nginx常用做静态内容服务和代理服务器,直接外来请求转发给后面的应用服务器(tomcat,Django等),tomcat更多用来做一个应用容器,让java web app泡在里面的东西。严格意义上来讲,Apache和nginx应该叫做HTTP Server,而tomcat是一个Application Server是一个Servlet/JSO应用的容器。 客户端通过HTTP Server访问服务器上存储的资源(HTML文件,图片文件等),HTTP Server是中只是把服务器上的文件如实通过HTTP协议传输给客户端。 应用服务器往往是运行在HTTP Server的背后,执行应用,将动态的内容转化为静态的内容之后,通过HTTP Server分发到客户端 注意:nginx只是把请求做了分发,不做处理!!!
2)nginx和Apache的区别 Apache是同步多进程模型,一个连接对应一个进程,而nginx是异步的,多个连接(万级别)可以对应一个进程。 nginx轻量级,抗并发,处理静态文件好 Apache超稳定,对PHP支持比较简单,nginx需要配合其他后端用,处理动态请求有优势,建议使用前端nginx抗并发,后端apache集群,配合起来会更好 nignx的正向代理和反向代理
3、优缺点比较
- nginx相对于apache的优点 轻量级,同样起web 服务,比apache占用更少的内存及资源 抗并发,nginx 处理请求是异步非阻塞的,而apache 则是阻塞型的,在高并发下nginx 能保持低资源低消耗高性能高度模块化的设计,编写模块相对简单提供负载均衡 社区活跃,各种高性能模块出品迅速
- apache 相对于nginx 的优点 apache的 rewrite(重写) 比nginx 的强大 ; 支持动态页面; 支持的模块多,基本涵盖所有应用; 性能稳定,而nginx相对bug较多。
- 两者优缺点比较 Nginx 配置简洁, Apache 复杂 ; Nginx 静态处理性能比 Apache 高 3倍以上 ; Apache 对 PHP 支持比较简单,Nginx 需要配合其他后端用;Apache 的组件比 Nginx 多 ; apache是同步多进程模型,一个连接对应一个进程;nginx是异步的,多个连接(万级别)可以对应一个进程; nginx处理静态文件好,耗费内存少; 动态请求由apache去做,nginx只适合静态和反向; Nginx适合做前端服务器,负载性能很好; Nginx本身就是一个反向代理服务器 ,且支持负载均衡
==总结== **Nginx优点:**负载均衡、反向代理、处理静态文件优势。nginx处理静态请求的速度高于apache; **Apache优点:**相对于Tomcat服务器来说处理静态文件是它的优势,速度快。Apache是静态解析,适合静态HTML、图片等。 **Tomcat:**动态解析容器,处理动态请求,是编译JSPServlet的容器,Nginx有动态分离机制,静态请求直接就可以通过Nginx处理,动态请求才转发请求到后台交由Tomcat进行处理。
Apache在处理动态有优势,Nginx并发性比较好,CPU内存占用低,如果rewrite频繁,那还是Apache较适合。