Java阶段二Day03
文章目录
- Java阶段二Day03
- V5
- BirdBootApplication
- ClientHandler
- HttpServletRequest
- V6
- BirdBootApplication
- ClientHandler
- HttpServletRequest
- V7
- BirdBootApplication
- ClientHandler
- HttpServletRequest
- HttpServletResponse
- V8
- BirdBootApplication
- ClientHandler
- HttpServletRequest
- HttpServletResponse
- DispatcherServlet
- SpringBoot核心逻辑
- WebServer流程
V5
此版本完成响应客户端的工作
这里先将ClientHandler中处理一次交互的第三步:响应客户端 实现出来。
目标:将一个固定的html页面通过发送一个标准的HTTP响应回复给浏览器使其呈现出来。
需要的知识点:
HTTP的响应格式。实现:
一:先创建第一个页面index.html
1:在src/main/resource下新建目录static
这个目录用于存放当前服务端下所有的静态资源。2:在static目录下新建目录新建第一个页面:index.html
二:实现将index.html页面响应给浏览器
在ClientHandler第三步发送响应处,按照HTTP协议规定的响应格式,将该页面包含在正文部分将其发送给浏览器即可。三:第二步测试成功后,我们就可以根据请求中浏览器传递过来的抽象路径去static目录下定位浏览器实际
请求的页面,然后用上述方式将该页面响应给浏览器,达到浏览器自主请求其需要的资源。四:可以在BirdBootApplication中的start方法里将接受客户端链接的动作放在死循环里重复进行了。
无论浏览器请求多少次,我们都可以遵从一问一答的原则响应用户请求的所有页面了。
BirdBootApplication
package com.birdboot.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 主启动类
*/
public class BirdBootApplication {
private ServerSocket serverSocket;
public BirdBootApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程处理该客户端交互
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BirdBootApplication application = new BirdBootApplication();
application.start();
}
}
ClientHandler
package com.birdboot.core;
import com.birdboot.http.HttpServletRequest;
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);
//获取请求的抽象路径
String path = request.getUri();
System.out.println(path);
//2 处理请求
//3 发送响应
/*
V5的第一个测试目标:将static目录下的index.html页面发送给浏览器
相对路径中非常常用的一个:类加载路径
是我们项目中任何一个类(该类的class文件)所在包的顶级包的上一层目录
就是类加载路径。
如果使用File去定位该目录,格式是固定的
File dir = new File(
当前类名.class.getClassLoader().getResource(".").toURI()
);
以在当前类ClientHandler中使用为例:
ClientHandler所在的包:com.birdboot.core。该包的顶层包是com,
此时类加载路径就是指com所在的目录。
在当前项目中代码编译后,src/main/java中所有的java类都会编译后
放到该项目的target/classes目录中。而src/main/resources下的静态
资源也会放到target/classes目录中。
而com目录就是放到了target/classes中,因此该目录就是类加载路径。
从该目录能看出,只要定位到他,就可以顺着该目录找到所有java类以及
所有的静态资源
*/
//定位当前项目的类加载路径
File baseDir = new File(
ClientHandler.class.getClassLoader().getResource(".").toURI()
);
//定位类加载路径下的static目录
File staticDir = new File(baseDir, "static");
//定位static下的index.html
// File file = new File(staticDir, "index.html");
/*
V5的第二个测试目标:根据浏览器输入的抽象路径去定位static下的资源
并将其发送给浏览器
此测试需要将上面66行代码注释
改造后测试:
浏览器输入路径:
http://localhost:8088/index.html
http://localhost:8088/classtable.html
*/
File file = new File(staticDir, path);
/*
一个响应的大致格式:
HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......(index.html页面内容)
*/
//3.1发送状态行
println("HTTP/1.1 200 OK");
//3.2发送响应头
println("Content-Type: text/html");
println("Content-Length: "+file.length());
//单独发送回车+换行,表示响应头发送完毕
println("");
//3.3发送响应正文(index.html页面内容)
FileInputStream fis = new FileInputStream(file);
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024*10];//10kb
int d;//记录每次实际读取的数据量
while( (d = fis.read(buf)) !=-1){
out.write(buf,0,d);
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* 向客户端发送一行字符串
* @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 static void main(String[] args) throws URISyntaxException {
File baseDir = new File(
ClientHandler.class.getClassLoader()
.getResource(".").toURI()
);
//定位类加载路径下的static目录
File staticDir = new File(baseDir, "static");
//定位static下的index.html
File file = new File(staticDir, "index.html");
System.out.println("页面是否存在:" + file.exists());
}
}
HttpServletRequest
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;//协议版本
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行
parseRequestLine();
// String line = readLine();
// System.out.println("请求行:"+line);
//
// //将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
// String[] data = line.split("\\s");
// method = data[0];
// uri = data[1];
// protocol = data[2];
//
// System.out.println("method:"+method);
// System.out.println("uri:"+uri);
// System.out.println("protocol:"+protocol);
//1.2解析消息头
parseHeaders();
// while(true) {
// line = readLine();
// if(line.isEmpty()){//如果读取到了空行
// break;
// }
// System.out.println("消息头:" + line);
// data = line.split(":\\s");
// headers.put(data[0],data[1]);
// }
// System.out.println("headers:"+headers);
//1.3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException {
String line = readLine();
System.out.println("请求行:"+line);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//解析消息头
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);
}
}
V6
完成404的响应
上一个版本中我们已经实现了根据浏览器中用户在地址栏上输入的URL中的抽象路径去
static目录下寻找对应资源进行响应的工作。但是会存在路径输入有误,导致定位不对(要么定位的是一个目录,要么该文件不存在)
此时再发送响应的响应正文时使用文件输入流读取就会出现异常提示该资源不存在。这是一个典型的404情况,因此我们在ClientHandler处理请求的环节,在实例化File
对象根据抽象路径定位static下的资源后,要添加一个分支,若该资源存在则将其响应
回去,如果不存在则要响应404状态代码和404页面提示用户。实现:
1:在static下新建页面:404.html
该页面居中显示一行字即可:404,资源不存在!
2:在ClientHandler处理请求的环节,当实例化File对象后添加一个分支,如果该File
对象存在且表示的是一个文件则将其响应给浏览器
否则发送的响应做如下变化:
- 状态行中的状态代码改为404,状态描述改为NotFound
- 响应头Content-Length发送的是404页面的长度
- 响应正文为404页面内容
完成后,在浏览器地址栏输入一个不存在的资源地址,检查服务端是否正确响应404页面
404响应
HTTP/1.1 404 NotFound(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF) <—- 404页面长度
1011101010101010101… <—- 404页面内容http://localhost:8088/
http://localhost:8088/123.html
BirdBootApplication
package com.birdboot.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 主启动类
*/
public class BirdBootApplication {
private ServerSocket serverSocket;
public BirdBootApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程处理该客户端交互
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BirdBootApplication application = new BirdBootApplication();
application.start();
}
}
ClientHandler
package com.birdboot.core;
import com.birdboot.http.HttpServletRequest;
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);
//2 处理请求
//获取请求的抽象路径
String path = request.getUri();
System.out.println(path);
//定位当前项目的类加载路径
File baseDir = new File(
ClientHandler.class.getClassLoader().getResource(".").toURI()
);
//定位类加载路径下的static目录
File staticDir = new File(baseDir, "static");
/*
File定位static下用户请求的资源可能不存在或者定位的是一个目录
这都属于失效的情况
例如:
http://localhost:8088/123.html static下没有123.html
http://localhost:8088/ 定位的就是static目录本身
上述两种情况都应当响应404
*/
File file = new File(staticDir, path);
int statusCode;//状态代码
String statusReason;//状态描述
if(file.isFile()){
statusCode=200;
statusReason="OK";
}else{
statusCode=404;
statusReason="NotFound";
file = new File(staticDir,"404.html");
}
//3 发送响应
//3.1发送状态行
println("HTTP/1.1"+" "+statusCode+" "+statusReason);
//3.2发送响应头
println("Content-Type: text/html");
println("Content-Length: "+file.length());
//单独发送回车+换行,表示响应头发送完毕
println("");
//3.3发送响应正文(index.html页面内容)
FileInputStream fis = new FileInputStream(file);
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024*10];//10kb
int d;//记录每次实际读取的数据量
while( (d = fis.read(buf)) !=-1){
out.write(buf,0,d);
}
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
/**
* 向客户端发送一行字符串
* @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 static void main(String[] args) throws URISyntaxException {
File baseDir = new File(
ClientHandler.class.getClassLoader()
.getResource(".").toURI()
);
//定位类加载路径下的static目录
File staticDir = new File(baseDir, "static");
//定位static下的index.html
File file = new File(staticDir, "index.html");
System.out.println("页面是否存在:" + file.exists());
}
}
HttpServletRequest
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;//协议版本
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException {
String line = readLine();
System.out.println("请求行:"+line);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//解析消息头
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);
}
}
V7
重构代码
将ClientHandler中发送响应的工作拆分出去实现:
- 在com.webserver.http包中新建类:HttpServletResponse 响应对象
- 在响应对象中定义对应的属性来保存响应内容并定义response方法来发送响应.
- 修改ClientHandler,使用响应对象完整响应的发送
BirdBootApplication
package com.birdboot.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 主启动类
*/
public class BirdBootApplication {
private ServerSocket serverSocket;
public BirdBootApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程处理该客户端交互
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BirdBootApplication application = new BirdBootApplication();
application.start();
}
}
ClientHandler
package com.birdboot.core;
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 处理请求
//获取请求的抽象路径
String path = request.getUri();
System.out.println(path);
//定位当前项目的类加载路径
File baseDir = new File(
ClientHandler.class.getClassLoader().getResource(".").toURI()
);
//定位类加载路径下的static目录
File staticDir = new File(baseDir, "static");
File file = new File(staticDir, path);
/*
V7改造
1:将原来在这里定义的两个局部变量statusCode和statusReason删除
2:将处理结果设置到response对应的属性上
3:将发送响应的操作移动到HttpServletResponse的response方法中
并在第三步发送响应时改为调用response方法发送响应
*/
if(file.isFile()){
//由于响应对象中状态代码和描述默认值为200,OK因此正确情况下不用再设置
response.setContentFile(file);
}else{
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"404.html");
response.setContentFile(file);
}
//3 发送响应
response.response();
} catch (IOException e) {
e.printStackTrace();
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
}
HttpServletRequest
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;//协议版本
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException {
String line = readLine();
System.out.println("请求行:"+line);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//解析消息头
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);
}
}
HttpServletResponse
package com.birdboot.http;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* V7新增内容:
* 响应对象
* 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
* HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
*/
public class HttpServletResponse {
private Socket socket;
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应头相关信息
//响应正文相关信息
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 {
println("Content-Type: text/html");
println("Content-Length: "+contentFile.length());
//单独发送回车+换行,表示响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
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;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
}
}
V8
重构代码
将ClientHandler中处理请求的操作拆分出去实现:
在com.webserver.core包下新建类:DispatcherServlet
并定义service方法,用来处理请求
将ClientHandler处理请求的操作移动到service方法中去
ClientHandler通过调用DispatcherServlet的service完成处理请求环节.
将DispatcherServlet定义为单例的(使用单例模式)
BirdBootApplication
package com.birdboot.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 主启动类
*/
public class BirdBootApplication {
private ServerSocket serverSocket;
public BirdBootApplication(){
try {
System.out.println("正在启动服务端...");
serverSocket = new ServerSocket(8088);
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){
try {
while(true) {
System.out.println("等待客户端链接...");
Socket socket = serverSocket.accept();
System.out.println("一个客户端链接了!");
//启动一个线程处理该客户端交互
ClientHandler handler = new ClientHandler(socket);
Thread t = new Thread(handler);
t.start();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
BirdBootApplication application = new BirdBootApplication();
application.start();
}
}
ClientHandler
package com.birdboot.core;
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();
} finally {
//V8新增内容:交互后与浏览器断开连接
//HTTP协议要求浏览器与服务端交互完毕后要断开连接
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
HttpServletRequest
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;//协议版本
//消息头相关信息 key:消息头名字 value:消息头对应的值
private Map<String,String> headers = new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行
parseRequestLine();
//1.2解析消息头
parseHeaders();
//1.3解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException {
String line = readLine();
System.out.println("请求行:"+line);
//将请求行按照空格("\s"在正则表达式中表示一个空白字符,包含空格)拆分为三部分
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
System.out.println("method:"+method);
System.out.println("uri:"+uri);
System.out.println("protocol:"+protocol);
}
//解析消息头
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);
}
}
HttpServletResponse
package com.birdboot.http;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* V7新增内容:
* 响应对象
* 该类的每一个实例用于表示服务端给客户端发送的一个HTTP的响应
* HTTP协议要求一个响应由三部分构成:状态行,响应头,响应正文
*/
public class HttpServletResponse {
private Socket socket;
//状态行相关信息
private int statusCode = 200;//状态代码
private String statusReason = "OK";//状态描述
//响应头相关信息
//响应正文相关信息
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 {
println("Content-Type: text/html");
println("Content-Length: "+contentFile.length());
//单独发送回车+换行,表示响应头发送完毕
println("");
}
//发送响应正文
private void sendContent() throws IOException {
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;
}
public void setContentFile(File contentFile) {
this.contentFile = contentFile;
}
}
DispatcherServlet
package com.birdboot.core;
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){
//获取请求的抽象路径
String path = request.getUri();
System.out.println(path);
File file = new File(staticDir, path);
if(file.isFile()){
//由于响应对象中状态代码和描述默认值为200,OK因此正确情况下不用再设置
response.setContentFile(file);
}else{
response.setStatusCode(404);
response.setStatusReason("NotFound");
file = new File(staticDir,"404.html");
response.setContentFile(file);
}
}
}