独立完成登录模块
步骤:
1:创建登录相关的页面
1.1:login.html页面,表单要求两个输入框分别为用户名和密码,表单action=“/loginUser”
1.2:login_info_error页面,当用户登录信息输入有误(空着不填等)时提示该页面,页面居中显示一行字:登录信息输入有误,请重新登录
1.3:login_success.html 登录成功提示页面
1.4:login_fail.html 登录失败提示页面2:在
UserController
中定义用于处理登录业务的方法login
方法定义参考reg
方法
3:在DispatcherServlet的判断注册业务下面添加一个else if分支,判断请求路径path的值是否为"/loginUser" 从而调用登录方法处理登录业务
package com.birdboot.controller;
import com.birdboot.entity.User;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;
import java.io.*;
/**
* 处理与用户相关的业务
*/
public class UserController {
private static File userDir;//表示存放所有用户信息的目录:users
static {
userDir = new File("./users");
if(!userDir.exists()){
userDir.mkdirs();
}
}
//处理"/regUser"这个请求
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");
System.out.println(username+","+password+","+nickname+","+ageStr);
//必要的验证
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;
}
int age = Integer.parseInt(ageStr);//将年龄转换为int值
//2
User user = new User(username,password,nickname,age);
/*
File的构造器
File(File parent,String child)
创建一个File对象表示child这个子项,而它是在parent这个File对象所表示的目录中
*/
File file = new File(userDir,username+".obj");
if(file.exists()){//注册前发现以该用户名命名的obj文件已经存在,说明是重复用户
response.sendRedirect("/have_user.html");
return;
}
try (
FileOutputStream fos = new FileOutputStream(file);
ObjectOutputStream oos = new ObjectOutputStream(fos);
){
oos.writeObject(user);//保存用户信息完毕
//3给用户回馈注册成功页面
//要求浏览器重新访问下述地址对应的页面
response.sendRedirect("/reg_success.html");
} catch (IOException e) {
e.printStackTrace();
}
}
public void login(HttpServletRequest request,HttpServletResponse response){
System.out.println("开始处理登录!!!!");
//1获取登录信息
String username = request.getParameter("username");
String password = request.getParameter("password");
//必要验证
if(username==null||username.isEmpty()||password==null||password.isEmpty()){
response.sendRedirect("/login_info_error.html");
return;
}
//根据该登录用户的名字去定位users下该用户的注册信息
File file = new File(userDir,username+".obj");
//判断该文件是否存在,不存在则说明该用户没有注册过
if(file.exists()){
//将该用户曾经的注册信息读取出来用于比较密码
try (
FileInputStream fis = new FileInputStream(file);
ObjectInputStream ois = new ObjectInputStream(fis);
){
User user = (User)ois.readObject();//读取注册信息
//比较本次登录的密码是否与注册时该用户输入的密码一致
if(user.getPassword().equals(password)){
//密码一致则登录成功
response.sendRedirect("/login_success.html");
return;
}
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
//如果程序可以执行到这里,则说明要么是用户名没输入对,要么是密码没有输入对.都属于登录失败
response.sendRedirect("/login_fail.html");
}
}
package com.birdboot.core;
import com.birdboot.http.EmptyRequestException;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;
import java.io.*;
import java.net.Socket;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 该线程任务负责与指定的客户端进行HTTP交互
* HTTP协议要求浏览器与服务端采取"一问一答"的模式。对此,这里的处理流程分为三步:
* 1:解析请求
* 2:处理请求
* 3:发送响应
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
//1 解析请求
HttpServletRequest request = new HttpServletRequest(socket);
HttpServletResponse response = new HttpServletResponse(socket);
//2 处理请求
//V8改造:将处理请求的操作移动到DispatcherServlet的service方法中并调用
DispatcherServlet servlet = new DispatcherServlet();
servlet.service(request,response);
//3 发送响应
response.response();
} catch (IOException e) {
e.printStackTrace();
} catch (EmptyRequestException e) {
} finally {
//HTTP协议要求浏览器与服务端交互完毕后要断开连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
package com.birdboot.core;
import com.birdboot.controller.UserController;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;
import java.io.File;
import java.net.URISyntaxException;
/**
* V8新增内容:
* 该类是SpringMVC框架与Tomcat整合时的一个关键类
* Tomcat处理业务原生的都是调用继承了HttpServlet的类来完成,此时需要进行很多配置
* 以及使用时要作很多重复性劳动。
* SpringMVC框架提供的该类也是继承了HttpServlet的,使用它来接收处理请求的工作。
*/
public class DispatcherServlet {
private static File baseDir;//类加载路径
private static File staticDir;//类加载路径下的static目录
static{
try {
//定位当前项目的类加载路径
baseDir = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//定位类加载路径下的static目录
staticDir = new File(baseDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* service方法实际上是当我们继承了HttpServlet后必须重写的方法
* 该方法要求接收两个参数:请求对象与响应对象。
* Tomcat在处理请求时就是调用某个Servlet的service方法并将请求与响应对象传入
* 来让其完成处理工作的。
*/
public void service(HttpServletRequest request, HttpServletResponse response){
//获取请求的抽象路径
//不能在使用uri判断请求了,因为uri可能含参数,内容不固定。
String path = request.getRequestURI();
System.out.println(path);
//判断该请求是否为请求一个业务
if("/regUser".equals(path)){
UserController controller = new UserController();
controller.reg(request, response);
}else if("/loginUser".equals(path)){
UserController controller = new UserController();
controller.login(request, response);
}else {
File file = new File(staticDir, path);
if (file.isFile()) {
//由于响应对象中状态代码和描述默认值为200,OK因此正确情况下不用再设置
response.setContentFile(file);
//设置响应头
response.addHeader("Server", "BirdServer");
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "404.html");
response.setContentFile(file);
response.addHeader("Server", "BirdServer");
}
}
}
}
package com.birdboot.http;
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* V7新增内容:
* 响应对象
* 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
* HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
*/
public class HttpServletResponse {
private static MimetypesFileTypeMap mftm = new MimetypesFileTypeMap();
private Socket socket;
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应头相关信息 key:响应头的名字 value:响应头的值
private Map<String,String> headers = new HashMap<>();
//响应正文相关信息
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 {
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String name = e.getKey();
String value = e.getValue();
println(name+": "+value);
}
//单独发送回车+换行,表示响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
if(contentFile!=null) {
FileInputStream fis = new FileInputStream(contentFile);
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024 * 10];//10kb
int d;//记录每次实际读取的数据量
while ((d = fis.read(buf)) != -1) {
out.write(buf, 0, d);
}
}
}
/**
* V7:将ClientHandler中发送响应的工作全部移动到这里,println方法也是。
* 向客户端发送一行字符串
* @param line
*/
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;
}
/**
* 设置响应正文对应的实体文件,该方法中会自动根据该文件添加对应的两个响应头:
* Content-Type和Content-Length
* @param contentFile
*/
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
addHeader("Content-Type",mftm.getContentType(contentFile));
addHeader("Content-Length",contentFile.length()+"");
}
/**
* 添加一个响应头
* @param name
* @param value
*/
public void addHeader(String name,String value){
headers.put(name,value);
}
/**
* 要求浏览器重定向到指定位置
* @param location
*/
public void sendRedirect(String location){
//1设置状态代码302
statusCode = 302;
statusReason = "Moved Temporarily";
//2添加响应头Location
addHeader("Location",location);
}
}
package com.birdboot.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* V4:新增内容
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* HTTP协议要求请求的格式由三部分构成:请求行,消息头,消息正文
*/
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<>();//保存每一组参数
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
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);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
parseURI();//进一步解析uri
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){
queryString = data[1];
String[] paraArr = queryString.split("&");
/*
再遍历paraArr进一步拆分每一组参数的参数名和参数值
每一组参数按照"="进行拆分
*/
//para:username=fancq
for(String para : paraArr){
/*
username=fancq 用户在浏览器该输入框输入信息
username= 用户在浏览器该输入框没有输入信息
*/
String[] arr = para.split("=",2);
parameters.put(arr[0],arr[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],data[1]);
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent(){}
/**
* 通过socket获取的输入流读取客户端发送过来的一行字符串
* @return
*/
private String readLine() throws IOException {//通常被重用的代码不自己处理异常
//对一个socket实例调用多次getInputStream()返回的始终是同一条输入流。而输出流也是如此
InputStream in = socket.getInputStream();
int d;
char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
StringBuilder builder = new StringBuilder();//保存读取后的所有字符
while((d = in.read())!=-1){
cur = (char)d;//本次读取的字符
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;
}
/**
* 根据给定的消息头的名字获取对应消息头的值
* @param name
* @return
*/
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
支持
POST
请求
当页面form表单中包含用户隐私信息或有附件上传时,应当使用POST形式提交。
POST
会将表单数据包含在请求的消息正文中。
如果表单中没有附件,则正文中包含的表单数据就是一个字符串,而格式就是原GET
形式提交时抽象路径中"?"右侧的内容。实现:
1:完成HttpServletRequest
中的解析消息正文的方法,当页面(reg.html或login.html)中form的提交方式改为POST
时,表单数据被包含在正文里,并且请求的消息头中会出现Content-Type
和Content-Length
用于告知服务端正文内容。因此我们可以根据它们来解析正文。
2:将解析参数的操作从parseURI
中单独提取出来定义在parseParameter()
方法中重用。parseURI
和解析正文方法parseContent
都可以调用parseParameter()
来重用拆分操作。
package com.birdboot.core;
import com.birdboot.controller.UserController;
import com.birdboot.http.HttpServletRequest;
import com.birdboot.http.HttpServletResponse;
import java.io.File;
import java.net.URISyntaxException;
/**
* V8新增内容:
* 该类是SpringMVC框架与Tomcat整合时的一个关键类
* Tomcat处理业务原生的都是调用继承了HttpServlet的类来完成,此时需要进行很多配置
* 以及使用时要作很多重复性劳动。
* SpringMVC框架提供的该类也是继承了HttpServlet的,使用它来接收处理请求的工作。
*/
public class DispatcherServlet {
private static File baseDir;//类加载路径
private static File staticDir;//类加载路径下的static目录
static{
try {
//定位当前项目的类加载路径
baseDir = new File(
DispatcherServlet.class.getClassLoader().getResource(".").toURI()
);
//定位类加载路径下的static目录
staticDir = new File(baseDir, "static");
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* service方法实际上是当我们继承了HttpServlet后必须重写的方法
* 该方法要求接收两个参数:请求对象与响应对象。
* Tomcat在处理请求时就是调用某个Servlet的service方法并将请求与响应对象传入
* 来让其完成处理工作的。
*/
public void service(HttpServletRequest request, HttpServletResponse response){
//获取请求的抽象路径
//不能在使用uri判断请求了,因为uri可能含参数,内容不固定。
String path = request.getRequestURI();
System.out.println(path);
//判断该请求是否为请求一个业务
if("/regUser".equals(path)){
UserController controller = new UserController();
controller.reg(request, response);
}else if("/loginUser".equals(path)){
UserController controller = new UserController();
controller.login(request, response);
}else {
File file = new File(staticDir, path);
if (file.isFile()) {
//由于响应对象中状态代码和描述默认值为200,OK因此正确情况下不用再设置
response.setContentFile(file);
//设置响应头
response.addHeader("Server", "BirdServer");
} else {
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir, "404.html");
response.setContentFile(file);
response.addHeader("Server", "BirdServer");
}
}
}
}
package com.birdboot.http;
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* V7新增内容:
* 响应对象
* 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
* HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
*/
public class HttpServletResponse {
private static MimetypesFileTypeMap mftm = new MimetypesFileTypeMap();
private Socket socket;
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应头相关信息 key:响应头的名字 value:响应头的值
private Map<String,String> headers = new HashMap<>();
//响应正文相关信息
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 {
/*
遍历headers将所有待发送的响应头发送给浏览器
headers
key value
Content-Type text/html
Content-Length 42123
Server BirdServer
... ...
*/
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String name = e.getKey();
String value = e.getValue();
println(name+": "+value);
}
//单独发送回车+换行,表示响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
if(contentFile!=null) {
FileInputStream fis = new FileInputStream(contentFile);
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024 * 10];//10kb
int d;//记录每次实际读取的数据量
while ((d = fis.read(buf)) != -1) {
out.write(buf, 0, d);
}
}
}
/**
* V7:将ClientHandler中发送响应的工作全部移动到这里,println方法也是。
* 向客户端发送一行字符串
* @param line
*/
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;
}
/**
* 设置响应正文对应的实体文件,该方法中会自动根据该文件添加对应的两个响应头:
* Content-Type和Content-Length
* @param contentFile
*/
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
addHeader("Content-Type",mftm.getContentType(contentFile));
addHeader("Content-Length",contentFile.length()+"");
}
/**
* 添加一个响应头
* @param name
* @param value
*/
public void addHeader(String name,String value){
headers.put(name,value);
}
/**
* 要求浏览器重定向到指定位置
* @param location
*/
public void sendRedirect(String location){
//1设置状态代码302
statusCode = 302;
statusReason = "Moved Temporarily";
//2添加响应头Location
addHeader("Location",location);
}
}
package com.birdboot.http;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* V4:新增内容
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* HTTP协议要求请求的格式由三部分构成:请求行,消息头,消息正文
*/
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<>();//保存每一组参数
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
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);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
parseURI();//进一步解析uri
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){
queryString = data[1];
parseParameters(queryString);
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
/**
* 解析参数
* 参数的格式应当是:name=value&name=value&...
* 如果是GET请求,参数来自抽象路径的"?"右侧
* 如果是POST请求,参数来自消息正文
* 但是格式是一致的。
* @param line
*/
private void parseParameters(String line){
String[] paraArr = line.split("&");
for(String para : paraArr){
String[] arr = para.split("=",2);
parameters.put(arr[0],arr[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],data[1]);
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent() throws IOException {
//确定本次请求是否包含正文(消息头中是否含有Content-Length)
if(headers.containsKey("Content-Length")){
int contentLength = Integer.parseInt(getHeader("Content-Length"));
byte[] contentData = new byte[contentLength];
InputStream in = socket.getInputStream();
in.read(contentData);//读取消息正文到字节数组中
String contentType = getHeader("Content-Type");
//分支:判断正文类型,并进行对应的解析
if("application/x-www-form-urlencoded".equals(contentType)){
//正文是form表单提交的数据(原get请求提交是在抽象路径"?"右侧内容)
String line = new String(contentData, StandardCharsets.ISO_8859_1);
System.out.println("=============正文:"+line);
parseParameters(line);
}
//后续可扩展支持其他正文类型的解析
// else if("xxx/xxx".equals(contentType)){
//
// }
}
}
/**
* 通过socket获取的输入流读取客户端发送过来的一行字符串
* @return
*/
private String readLine() throws IOException {//通常被重用的代码不自己处理异常
//对一个socket实例调用多次getInputStream()返回的始终是同一条输入流。而输出流也是如此
InputStream in = socket.getInputStream();
int d;
char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
StringBuilder builder = new StringBuilder();//保存读取后的所有字符
while((d = in.read())!=-1){
cur = (char)d;//本次读取的字符
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;
}
/**
* 根据给定的消息头的名字获取对应消息头的值
* @param name
* @return
*/
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
解决传递中文问题:
现象:
- 当浏览器无论是以
GET
形式提交还是POST形式提交表单,如果表单中含有中文信息时,所有的中文内容都会被转为:- “username=%E8%8C%83%E4%BC%A0%E5%A5%87&password=123456&nickname=%E4%BC%A0%E5%A5%87&age=22”,
原因:
- 以GET请求为例,表单信息会被拼接到抽象路径的"?"右侧。抽象路径是包含在浏览器发送的请求的请求行中:
- GET /regUser?username=%E8%8C%83%E4%BC%A0%E5%A5%87&… HTTP/1.1
HTTP协议要求浏览器发送的请求的请求行和消息头必须是文本且字符集只能是
ISO8859-1
实际上可传输的字符只有英文,数字,符号。
因为==ISO8859-1
不支持中文==,因此不能直接将中文包含在抽象路径中
GET /regUser?username=张三&… HTTP/1.1 不合法!!!解决办法:
- 核心思想:用
ISO8859-1
支持的字符来表达不支持的字符。- 可以使用的字符:英文,数字,符号
例:
首先:先将中文字符"范"按照支持的字符(通常是UTF-8)转换为2进制"范"-----UTF8---->11101000 10001100 10000011
我们可以用字符"0"和字符"1"表示2进制
传递时可以将:/regUser?username=范&… 不合法换做:
/regUser?username=111010001000110010000011&… 合法服务端接收到后,再将这一串1和0组成的内容当做2进制看待,再按照
UTF-8
还原就可以看到"范"问题得到解决,但是新的问题产生了:数据太长
解决办法:将2进制用16进制表示
二进制 十进制 十六进制 0000 0 0 0001 1 1 0010 2 2 0011 3 3 0100 4 4 0101 5 5 0110 6 6 0111 7 7 1000 8 8 1001 9 9 1010 10 a 1011 11 b 1100 12 c 1101 13 d 1110 14 e 1111 15 f 原本:
/regUser?username=11101000 10001100 10000011&…
用16进制表示:
/regUser?username=E88C83&…
长度可以缩短4倍长度问题解决了,新的问题又出现了
/regUser?username=E88C83&…
服务端接收到username后,该用户的名字是"范"还是此人就叫 E88C83
为了避免混淆,URL地址格式进行了规定:
如果使用英文+数字组合表示的是16进制,则需要在每两位16进制前添加一个"%"
因此:
/regUser?username=E88C83&… 此人就叫E88C83
/regUser?username=%E8%8C%83&… 这里是16进制表示了3个字节,可转换为"范"服务端如果想正确得到中文则需要将上述动作反推。
%E8%8C%83–>11101000 10001100 10000011—>范
该操作JAVA有现成的API:URLDecoder
package com.birdboot.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;
/**
* V4:新增内容
* 请求对象
* 该类的每一个实例用于表示浏览器发送过来的一个HTTP请求
* HTTP协议要求请求的格式由三部分构成:请求行,消息头,消息正文
*/
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<>();//保存每一组参数
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
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);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
parseURI();//进一步解析uri
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//进一步解析uri
private void parseURI(){
String[] data = uri.split("\\?");
requestURI = data[0];
if(data.length>1){
queryString = data[1];
parseParameters(queryString);
}
System.out.println("requestURI:"+requestURI);
System.out.println("queryString:"+queryString);
System.out.println("parameters:"+parameters);
}
/**
* 解析参数
* 参数的格式应当是:name=value&name=value&...
* 如果是GET请求,参数来自抽象路径的"?"右侧
* 如果是POST请求,参数来自消息正文
* 但是格式是一致的。
* @param line
*/
private void parseParameters(String line){
//将line转码
try {
line = URLDecoder.decode(line,"UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
String[] paraArr = line.split("&");
for(String para : paraArr){
String[] arr = para.split("=",2);
parameters.put(arr[0],arr[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],data[1]);
}
System.out.println("headers:"+headers);
}
//解析消息正文
private void parseContent() throws IOException {
//确定本次请求是否包含正文(消息头中是否含有Content-Length)
if(headers.containsKey("Content-Length")){
int contentLength = Integer.parseInt(getHeader("Content-Length"));
byte[] contentData = new byte[contentLength];
InputStream in = socket.getInputStream();
in.read(contentData);//读取消息正文到字节数组中
String contentType = getHeader("Content-Type");
//分支:判断正文类型,并进行对应的解析
if("application/x-www-form-urlencoded".equals(contentType)){
//正文是form表单提交的数据(原get请求提交是在抽象路径"?"右侧内容)
String line = new String(contentData, StandardCharsets.ISO_8859_1);
System.out.println("=============正文:"+line);
parseParameters(line);
}
//后续可扩展支持其他正文类型的解析
// else if("xxx/xxx".equals(contentType)){
// }
}
}
/**
* 通过socket获取的输入流读取客户端发送过来的一行字符串
* @return
*/
private String readLine() throws IOException {//通常被重用的代码不自己处理异常
//对一个socket实例调用多次getInputStream()返回的始终是同一条输入流。而输出流也是如此
InputStream in = socket.getInputStream();
int d;
char pre='a',cur='a';//pre表示上次读取的字符,cur表示本次读取的字符
StringBuilder builder = new StringBuilder();//保存读取后的所有字符
while((d = in.read())!=-1){
cur = (char)d;//本次读取的字符
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;
}
/**
* 根据给定的消息头的名字获取对应消息头的值
* @param name
* @return
*/
public String getHeader(String name) {
return headers.get(name);
}
public String getRequestURI() {
return requestURI;
}
public String getQueryString() {
return queryString;
}
public String getParameter(String name) {
return parameters.get(name);
}
}
package com.birdboot.http;
import javax.activation.MimetypesFileTypeMap;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* V7新增内容:
* 响应对象
* 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
* HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
*/
public class HttpServletResponse {
private static MimetypesFileTypeMap mftm = new MimetypesFileTypeMap();
private Socket socket;
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应头相关信息 key:响应头的名字 value:响应头的值
private Map<String,String> headers = new HashMap<>();
//响应正文相关信息
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 {
/*
遍历headers将所有待发送的响应头发送给浏览器
headers
key value
Content-Type text/html
Content-Length 42123
Server BirdServer
... ...
*/
Set<Map.Entry<String,String>> entrySet = headers.entrySet();
for(Map.Entry<String,String> e : entrySet){
String name = e.getKey();
String value = e.getValue();
println(name+": "+value);
}
//单独发送回车+换行,表示响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
if(contentFile!=null) {
FileInputStream fis = new FileInputStream(contentFile);
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024 * 10];//10kb
int d;//记录每次实际读取的数据量
while ((d = fis.read(buf)) != -1) {
out.write(buf, 0, d);
}
}
}
/**
* V7:将ClientHandler中发送响应的工作全部移动到这里,println方法也是。
* 向客户端发送一行字符串
* @param line
*/
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;
}
/**
* 设置响应正文对应的实体文件,该方法中会自动根据该文件添加对应的两个响应头:
* Content-Type和Content-Length
* @param contentFile
*/
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
addHeader("Content-Type",mftm.getContentType(contentFile));
addHeader("Content-Length",contentFile.length()+"");
}
/**
* 添加一个响应头
* @param name
* @param value
*/
public void addHeader(String name,String value){
headers.put(name,value);
}
/**
* 要求浏览器重定向到指定位置
* @param location
*/
public void sendRedirect(String location){
//1设置状态代码302
statusCode = 302;
statusReason = "Moved Temporarily";
//2添加响应头Location
addHeader("Location",location);
}
}
反射机制使用的第一步:获取待操作的类的类对象, 类对象:Class类的实例
类名.class
Class cls = String.class;//获取String的类对象
Class cls = int.class;//获取int的类对象(基本类型只有这一种方式获取类对象)
Class.forName(String className)
//根据类的完全限定(包名.类名)名加载并获取该类的类对象
Class cls = Class.forName("java.lang.String");
ClassLoader类加载器方式
Class
,它的每一个实例用于表示一个类的信息Package
,它的每一个实例用于表示一个包的信息Method
,它的每一个实例用于表示一个方法Constructor
,它的每一个实例用于表示一个构造器Filed
,它的每一个实例用于表示一个属性