v11
- 实现HttpServletResponse响应正确的MIME类型,即:Content-Type的值
这里我们使用java.nio.file.Files这个类来完成这个功能。
这样一来,服务端就可以正确响应浏览器请求的任何资源了,使得浏览器可以正确显示内容
实现:
1:将原DispatcherServlet中设置响应头Content-Type和Content-Length的工作移动到HttpServletResponse的设置响应正文方法setContentFile中.
原因:一个响应中只要包含正文就应当包含说明正文信息的两个头Content-Type和Content-Length.因此我们完全可以在设置正文的时候自动设置这两个头.这样做的好处是将来设置完正文(调用setContentFile)后无需再单独设置这两个头了.
2:使用MimetypesFileTypeMap的方法getContentType按照正文文件分析MIME类型并设置到 响应头Content-Type上。需要在resources中导入文件mime.types。
HttpServletResponse
package com.webserver.http;
import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 响应对象
* 该类的每一个实例用于表示一个HTTP的响应
* 一个响应由三部分构成:
* 状态行,响应头,响应正文
*
*/
public class HttpServletResponse {
private static MimetypesFileTypeMap mft = new MimetypesFileTypeMap();
private Socket socket;
//状态行相关信息
private int statusCode = 200; //状态代码
private String statusReason = "OK"; //状态描述
//响应头相关信息
private Map<String,String> headers = new HashMap<>(); //key:响应头名字 value:对应的值
//响应正文相关信息
private File contentFile; //响应正文对应的实体文件
public HttpServletResponse(Socket socket){
this.socket = socket;
}
/**
* 用于将当前响应对象的内容以标准的HTTP响应格式发送给客户端(浏览器)
*/
public void response() throws IOException {
//3.1发送状态行
sendStatusLine();
//3.2发送响应头
sendHeaders();
//3.3发送响应正文
sendContent();
}
private void sendStatusLine() throws IOException {
println("HTTP/1.1" + " " + statusCode + " " + statusReason);
}
private void sendHeaders() throws IOException {
/*
header
key value
Content-Type text/html Entry
Content-Length 245 Entry
Server WebServer Entry
XXX XXXX Entry
*/
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String key = e.getKey();
String value = e.getValue();
println(key + ": " + value);
}
println("");
}
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
FileInputStream fis = new FileInputStream(contentFile);
int len;
byte[] buf = new byte[1024*10];
while((len = fis.read(buf))!=-1){
out.write(buf,0,len);
}
}
private void println(String line) throws IOException {
OutputStream out = socket.getOutputStream();
byte[] data = line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);//发送回车符
out.write(10);//发送换行符
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
public File getContentFile() {
return contentFile;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
addHeader("Content-Type",mft.getContentType(contentFile));
addHeader("Content-Length",contentFile.length()+"");
}
/**
* 添加一个响应头
* @param name 响应头的名字
* @param value 响应头对应的值
*/
public void addHeader(String name,String value){
headers.put(name,value);
}
}
DispatcherServlet
package com.webserver.core;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.net.URISyntaxException;
/**
* DispatcherServlet实际是由SpringMVC框架提供的一个类,用于和Tomcat整合并负责
* 接手处理请求的工作。
*
* Servlet是JAVA EE里的一个接口,译作:运行在服务端的小程序
* Servlet中有一个重要的抽象方法:
* public void service(HttpServletRequest request,HttpServletResponse response)
* 该方法用于处理某个服务
*
* SpringMVC框架提供的DispatcherServlet就实现了该接口并重写了service方法,那么与Tomcat整合后,Tomcat在处理
* 请求的环节就可以调用DispatcherServlet的service方法将请求对象与响应对象传递进去由SpringMVC框架完成处理请求
* 的操作。
*/
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File root;
private static File staticDir;
static {
try {
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
staticDir = new File(root,"static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet(){}
public static DispatcherServlet getInstance(){
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response){
String path = request.getUri();
File file = new File(staticDir,path);
if(file.isFile()){//判断请求的文件真实存在且确定是一个文件(不是目录)
response.setContentFile(file);
}else{//404情况
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"404.html");
response.setContentFile(file);
response.addHeader("Content-Type","text/html");
response.addHeader("Content-Length",file.length()+"");
}
response.addHeader("Server","BirdServer");
}
}
v11结束
V12部分
- 页面为GET请求方式,会将客户输入的信息放在路径上,设置方法进一步解析uri,得到相应数据。
实现:
-
1:定义三个新的属性:String requestURI,String queryString,Map parameters 分别保存抽象路径中的请求部分,参数部分和每一组参数
2:定义parseUri方法,进一步解析抽象路径中包含的参数内容,将uri按?拆分,左边的为请求部分,右边的为参数部分,将参数部分按照&拆分放入数组paraArray中,再将paraArray按照=拆分,放入数组paras中,遍历数组paras将其存入parameters Map集合中。
3:在解析请求的方法中解析出三部分后调用parseUri进一步解析uri。 -
设置方法parameters集合中根据key值查找value的方法。
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* 一个请求由三部分构成的:
* 1:请求行
* 2:消息头
* 3:消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行的相关信息
private String method; //请求方式
private String uri; //抽象路径
private String protocol; //协议版本
private String requestURI; //uri中请求部分("?"左侧内容)
private String queryString; //uri中参数部分("?"右侧内容)
private Map<String,String> parameters = new HashMap<>();//存每一组参数
//消息头的相关信息
private Map<String, String> headers = new HashMap<>(); //key:消息头名字 value:对应的值
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1.1:读取请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if(line.isEmpty()){//如果请求行没有读取到内容,则为空请求
throw new EmptyRequestException();
}
System.out.println("请求行:" + line);
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
parseURI();//进一步解析uri
}
//进一步解析uri(将请求行中的抽象路径部分进一步解析)
private void parseURI(){
/*
uri有两种情况:
1:不含有参数的
例如: /index.html
直接将uri的值赋值给requestURI即可.
2:含有参数的
例如:/regUser?username=fancq&password=&nickname=chuanqi&age=22
将uri中"?"左侧的请求部分赋值给requestURI
将uri中"?"右侧的参数部分赋值给queryString
将参数部分首先按照"&"拆分出每一组参数,再将每一组参数按照"="拆分为参数名与参数值
并将参数名作为key,参数值作为value存入到parameters中。
如果表单某个输入框没有输入信息,那么存入parameters时对应的值应当保存为空字符串
*/
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){//如果拆分出第二项,则说明本次uri包含参数
queryString = data[1];//将参数部分赋值给queryString
// queryString:username=fancq&password=&nickname=chuanqi&age=22
//先将queryString按照"&"拆分出每一组参数
String[] paraArray = queryString.split("&");
// paraArray:["username=fancq", "password=", "nickname=chuanqi", "age=22"]
for(String para : paraArray){
// String[] paras = para.split("=");
// parameters.put(paras[0], paras.length>1?paras[1]:"");
String[] paras = para.split("=",2);
// paras:["username", "fancq"]或["password", ""]
parameters.put(paras[0], paras[1]);
}
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
//解析消息头
private void parseHeaders() throws IOException {
while (true) {
String line = readLine();
if (line.isEmpty()) {
break;
}
System.out.println("消息头:" + line);
String[] data = line.split(":\\s");
headers.put(data[0].toLowerCase(), data[1]);
}
System.out.println("所有消息头:" + headers);
}
//解析消息正文
private void parseContent(){
}
/**
* 读取客户端发送过来的一行字符串
*
* @return
*/
private String readLine() throws IOException {//被重用的代码对应的方法通常不自己处理异常
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();//保存拼接的一行内容
char cur = 'a', pre = 'a';//cur记录本次读取的字符,pre记录上次读取的字符
int d;//每次读取的字节
while ((d = in.read()) != -1) {
cur = (char) d;//将本次读取到的字节转换为char记录在cur上。
if (pre == 13 && cur == 10) {//如果上次读取的为回车符,本次读取的是换行符
break;//停止读取(一行结束了)
}
builder.append(cur);//将本次读取的字符拼接到已经读取的字符串中
pre = cur;//在下次读取前将本次读取的字符记作"上次读取的字符"
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
public String getHeader(String name) {
/*
因为headers中消息头名字以全小写形式保存,
所以要将获取的消息头名字也转换为全小写再提取
这样的好处就是不要求外界获取消息头时指定的名字必须区分大小写了
*/
name = name.toLowerCase();
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
v12结束
V13部分
实现处理业务
步骤:
1:新建包com.webserver.controller
2:在controller包中新建类UserController并定义reg方法(与SpringBoot项目一致)
3:在DispatcherServlet中修改逻辑
3.1:废弃原有的通过request.getUri()方式获取请求路径(因为包含参数,不适用判断具体请求内容)
3.2:改用request.getRequestURI()作为请求路径判断用户请求
3.3:添加分支,首先判断请求路径是否为请求注册,如果是则实例化UserController并调用reg方法
如果不是请求注册则走原有的分支判断是否请求static目录下的文件或404
实现重定向
当浏览器提交一个请求时,比如注册,那么请求会带着表单信息请求注册功能。而注册功能处理
完毕后直接设置一个页面给浏览器,这个过程是内部跳转。
即:浏览器上的地址栏中地址显示的是提交表单的请求,而实际看到的是注册结果的提示页面。
这有一个问题,如果此时浏览器刷新,会重复上一次的请求,即:再次提交表单请求注册业务。
为了解决这个问题,我们可以使用重定向。
重定向是当我们处理完请求后,不直接响应一个页面,而是给浏览器回复一个路径,让其再次根据该路径发起请求。这样一来,无论用户如何刷新,请求的都是该路径。避免表单的重复提交。
实现:
在HttpServletResponse中定义一个方法:sendRedirect()
该方法中设置状态代码为302,并在响应头中包含Location指定需要浏览器重新发起请求的路径,将原来Controller中内部跳转页面的操作全部改为重定向。
**注意:**因为过程是内部跳转,所以不会重新发出请求,不会重新发送响应正文,所以设置发送响应正文方法为:
private void sendContent() throws IOException {
OutputStream out = socket.getOutputStream();
if(contentFile!=null) {
FileInputStream fis = new FileInputStream(contentFile);
int len;
byte[] buf = new byte[1024 * 10];
while ((len = fis.read(buf)) != -1) {
out.write(buf, 0, len);
}
}
}
独立练习部分:
按照注册的流程实现用户登录功能
1:在static下准备登录所需页面(与SpringBoot一致)
2:在UserController中定义方法login
3:实现登录逻辑(与SpringBoot项目一致)
4:在DispatcherServlet中添加分支,判断请求路径是否为登录,
如果是则调用UserController的login方法
UserController
package com.webserver.controller;
import com.webserver.entity.User;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class Usercontroller {
private static File userDir;
static {
userDir = new File("./users");
if (!userDir.exists()){
userDir.mkdirs();
}
}
public void reg(HttpServletRequest request, HttpServletResponse response){
System.out.println("开始处理用户注册!!!!!!");
String username = request.getParameter("username");
String password = request.getParameter("password");
String nickname = request.getParameter("nickname");
String ageStr = request.getParameter("age");
if(username==null||username.isEmpty()||password==null||password.isEmpty()||
nickname==null||nickname.isEmpty()||ageStr==null||ageStr.isEmpty()||
!ageStr.matches("[0-9]+")){
//要求浏览器查看错误提示页面
response.sendRedirect("/reg_info_error.html");
return;
}
System.out.println(username+","+password+","+nickname+","+ageStr);
int age = Integer.parseInt(ageStr);
File file = new File(userDir,username+".obj");
if (file.isFile()){
response.sendRedirect("/have_user.html");
return;
}
User user = new User(username,password,nickname,age);
try (
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
){
oos.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
}
response.sendRedirect("/reg_success.html");
}
}
DispatcherServlet
package com.webserver.core;
import com.webserver.controller.Usercontroller;
import com.webserver.http.HttpServletRequest;
import com.webserver.http.HttpServletResponse;
import java.io.File;
import java.net.URISyntaxException;
public class DispatcherServlet {
private static DispatcherServlet instance = new DispatcherServlet();
private static File root;
private static File staticDir;
static {
try {
root = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
staticDir = new File(root,"static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private DispatcherServlet(){}
public static DispatcherServlet getInstance(){
return instance;
}
public void service(HttpServletRequest request, HttpServletResponse response) {
//判断用户的请求路径不应当含有参数部分。所以uri不适用。
String path = request.getRequestURI();
//判断是否为请求业务
if ("/regUser".equals(path)) {
Usercontroller controller = new Usercontroller();
controller.reg(request, response);
} else {
File file = new File(staticDir, path);
if (file.isFile()) {//判断请求的文件真实存在且确定是一个文件(不是目录)
response.setContentFile(file);
} else {//404情况
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "404.html");
response.setContentFile(file);
response.addHeader("Content-Type", "text/html");
response.addHeader("Content-Length", "" + file.length());
}
response.addHeader("Server", "BirdServer");
}
}
}
HttpServletResponse中sendRedirect方法
public void sendRedirect(String location){
this.statusCode = 302;
this.statusReason = "Moved Temporarily";
addHeader("Location",location);
}
}
流程图查看
13结束
V14部分
-
当我们在浏览器注册页面输入框中输入了中文,并以GET形式提交表单,那么浏览器会将表单信息拼接 到URL的抽象路径中。
http://localhost:8088/regUser?username=王德发&password=123456&...
请求的请求行:
请求行:GET /regUser?username=王德发&password=123456&... HTTP/1.1
HTTP协议规定请求与响应的前两部分都是文字,但是字符集必须为【ISO8859-1】.
而这是一个欧洲的字符集,里面不包含中文。因此如果以上述请求行样子传递中文则违背了HTTP协议 对于请求的要求,因此是不被允许的。 结论:中文不能被直接传递。 -
实现:
在HttpServletRequest的parseURI方法中,在拆分出参数部分queryString后,使用 JAVA的API提供的类:java.net.URLDecoder的decode方法转码
//为参数部分转码
try {
queryString = URLDecoder.decode(queryString,"UTF-8");
} catch (UnsupportedEncodingException e) {
}
v14结束
V15部分
支持POST请求
当页面form表单中包含用户隐私信息或有附件上传时,应当使用POST形式提交。
POST会将表单数据包含在请求的消息正文中。
如果表单中没有附件,则正文中包含的表单数据就是一个字符串,而格式就是原GET形式
提交时抽象路径中"?"右侧的内容。
实现:
1:完成HttpServletRequest中的解析消息正文的方法
当页面(reg.html或login.html)中form的提交方式改为POST时,表单数据被包含在正文里,并且
请求的消息头中会出现Content-Type和Content-Length用于告知服务端正文内容。
因此我们可以根据它们来解析正文。
2:将解析参数的操作从parseURI中单独提取出来定义在parseParameter()方法中重用。
parseURI和解析正文方法parseContent都可以调用parseParameter()来重用拆分操作。
parseContent()方法
//解析消息正文
private void parseContent() throws IOException {
/*
首先:判断请求中是否含有消息头Content-Length
因为这个头是浏览器告知本次请求中消息正文的长度。
如果不含有这个头说明本次请求没有正文。
*/
String contentLength = getHeader("Content-Length");
if (contentLength != null) {
int length = Integer.parseInt(contentLength);
byte[] data = new byte[length];
InputStream in = socket.getInputStream();
in.read(data);
//根据消息头Content-Type判定正文类型并对应转换
String contentType = getHeader("Content-Type");
//判断是否为不含有附件的普通表单内容
if ("application/x-www-form-urlencoded".equals(contentType)) {
String line = new String(data, StandardCharsets.ISO_8859_1);
System.out.println("正文内容"+line);
parseParameter(line);
}
}
}
HttpServletRequest
package com.webserver.http;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* 一个请求由三部分构成的:
* 1:请求行
* 2:消息头
* 3:消息正文
*/
public class HttpServletRequest {
private Socket socket;
//请求行的相关信息
private String method; //请求方式
private String uri; //抽象路径
private String protocol; //协议版本
private String requestURI; //uri中请求部分("?"左侧内容)
private String queryString; //uri中参数部分("?"右侧内容)
private Map<String, String> parameters = new HashMap<>();//存每一组参数
//消息头的相关信息
private Map<String, String> headers = new HashMap<>(); //key:消息头名字 value:对应的值
public HttpServletRequest(Socket socket) throws IOException, EmptyRequestException {
this.socket = socket;
//1.1:读取请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException, EmptyRequestException {
String line = readLine();
if (line.isEmpty()) {//如果请求行没有读取到内容,则为空请求
throw new EmptyRequestException();
}
System.out.println("请求行:" + line);
String[] data = line.split("\s");
method = data[0];
uri = data[1];
protocol = data[2];
parseURI();//进一步解析uri
}
//进一步解析uri(将请求行中的抽象路径部分进一步解析)
private void parseURI() {
String[] data = uri.split("\?");
requestURI = data[0];
if (data.length > 1) {
queryString = data[1];
parseParameter(queryString);
}
}
//解析参数
private void parseParameter(String line) {
//为参数部分转码
try {
line = URLDecoder.decode(line, "UTF-8");
} catch (UnsupportedEncodingException e) {
}
String[] paraArray = line.split("&");
for (String para : paraArray) {
String[] paras = para.split("=", 2);
parameters.put(paras[0], paras[1]);
}
}
//解析消息头
private void parseHeaders() throws IOException {
while (true) {
String line = readLine();
if (line.isEmpty()) {
break;
}
System.out.println("消息头:" + line);
String[] data = line.split(":\s");
headers.put(data[0].toLowerCase(), data[1]);
}
System.out.println("所有消息头:" + headers);
}
//解析消息正文
private void parseContent() throws IOException {
/*
首先:判断请求中是否含有消息头Content-Length
因为这个头是浏览器告知本次请求中消息正文的长度。
如果不含有这个头说明本次请求没有正文。
*/
String contentLength = getHeader("Content-Length");
if (contentLength != null) {
int length = Integer.parseInt(contentLength);
byte[] data = new byte[length];
InputStream in = socket.getInputStream();
in.read(data);//将消息正文所有字节读入数组
//根据消息头Content-Type判定正文类型并对应转换
String contentType = getHeader("Content-Type");
//判断是否为不含有附件的普通表单内容
if ("application/x-www-form-urlencoded".equals(contentType)) {
//正文就是一行字符串,内容是原GET形式提交后抽象路径中"?"后面内容
String line = new String(data, StandardCharsets.ISO_8859_1);
System.out.println("正文内容:" + line);
parseParameter(line);
}
// 后期可添加其他判断,比如含有附件的正文解析
// else if(){
// }
}
}
/**
* 读取客户端发送过来的一行字符串
*
* @return
*/
private String readLine() throws IOException {//被重用的代码对应的方法通常不自己处理异常
InputStream in = socket.getInputStream();
StringBuilder builder = new StringBuilder();//保存拼接的一行内容
char cur = 'a', pre = 'a';//cur记录本次读取的字符,pre记录上次读取的字符
int d;//每次读取的字节
while ((d = in.read()) != -1) {
cur = (char) d;//将本次读取到的字节转换为char记录在cur上。
if (pre == 13 && cur == 10) {//如果上次读取的为回车符,本次读取的是换行符
break;//停止读取(一行结束了)
}
builder.append(cur);//将本次读取的字符拼接到已经读取的字符串中
pre = cur;//在下次读取前将本次读取的字符记作"上次读取的字符"
}
return builder.toString().trim();
}
public String getMethod() {
return method;
}
public String getUri() {
return uri;
}
public String getProtocol() {
return protocol;
}
public String getHeader(String name) {
/*
因为headers中消息头名字以全小写形式保存,
所以要将获取的消息头名字也转换为全小写再提取
这样的好处就是不要求外界获取消息头时指定的名字必须区分大小写了
*/
name = name.toLowerCase();
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
v15结束