- WebServer项目:
- 1、V5项目
(0)《readme.txt》:
(1)《ClientHandler.java》:
(2)《WebServerApplication.java》:
(3)《HttpServerRequest.java》: - 2、V6项目
(0)《readme.txt》:
(1)《ClientHandler.java》:
(2)《WebServerApplication.java》:
(3)《HttpServerRequest.java》:
(4)《classTable.java》:
(5)《index.html》: - 3、V7项目
(0)《readme.txt》:
(1)《ClientHandler.java》
(2)《WebServerApplication.java》:
(3)《HttpServerRequest.java》:
(4)《classTable.java》:
(5)《index.html》:
(6)《404.html》:
1、V5项目
流程图:
(0)《readme.txt》:
重构--------【没有代码的改变 只有代码的来回移动】
进行功能个拆分,将ClientHandler中第一个环节解析请求的细节拆分出去,使得
ClientHandler仅关心处理一次HTTP交互的流程控制.
实现:
1:新建一个包:com.webserver.http 【:右键单击com.webserver.core包,new一个package,只将最后的core改为“http”】
2:在http包下新建类:HttpServletRequest 请求对象
使用这个类的每一个实例表示客户端发送过来的一个HTTP请求
3:在HttpServletRequest的构造方法中完成解析请求的工作
4:ClientHandler第一步解析请求只需要实例化一个HttpServletRequest即可.
具体重构过程:
(1)
①:新建一个包:com.webserver.http 【:右键单击com.webserver.core包,new一个package,只将最后的core改为“http”】
②:http包中定义一个构造方法
③:把【private String readLine() throws IOException {】方法从ClientHandler包中剪切到http包的类中方法外
④:http包中最上边定义private Socket socket;
⑤:在http包中的构造方法中添加this.socket = socket; 并传参(即写参数:Socket socket)
(2)
①:ClientHandler包中从【public void run()中】从“//1.1解析请求行”→到catch上边的全部,
剪切到http包中的构造方法里 即this.socket = socket;的下面
②:http包中:“//1.1解析请求行下面的String line = readLine();” →抛异常
③:http包中:将请求行的3行相关信息代码【String method;...】剪切到本包的构造器的上面定义为成员变量→每一行都定义private
→再将1.2的消息头相关信息一行代码【Map<String,String> headers = new HashMap<>();】剪切到刚才的下面作为属性 也是定义private
(3)
http包中:在构造器的下面定义三个方法→→
解析请求行【private void parseRequestLine(){}】、
消息头【private void parseHeaders(){}】、
消息正文private void parseContent(){}】
①:然后在构造器中将【1.1解析请求行的所有内容:String line = readLine();到System.out.println("protocol:"+protocol);】剪切到上面解析请求行的方法中 → alt+enter进行抛异常
②:将【1.2解析消息头的所有内容:while(true)到System.out.println("headers:"+headers);】剪切到上面解析消息头的方法中 在readLine处按alt+enter抛异常
③:然后在line的前面加String、data的前边加String[]
④:此时构造器中只有一句代码,-----在代码下面自己添加三句代码:
【//1.1解析请求行parseRequestLine();】【//1.2解析消息头parseHeaders();】【//1.3解析消息正文parseContent();】
⑤:http包中:readLine()方法下即return builder.toString().trim();}】:alt+insert调用getter----选择【getMethod、getUri、getProtocol】三个点击ok
⑥:再从上边的三个方法下自己再写一个方法:【public String getHeader(String name){return headers.get(name);}】
(4)
①:ClientHandler包中:第一步解析请求只需要实例化一个HttpServletRequest即可:
【HttpServerRequest request = new HttpServerRequest(socket);】
(1)《ClientHandler.java》:
package com.webserver.core;
import com.webserver.http.HttpServerRequest;
import javax.xml.ws.handler.Handler;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
/**
* 该线程任务负责与指定的客户端完成HTTP交互
* 每次Http交互都采取一问一答的规则,因此交互由三步来完成:
* 1:解析请求
* 2:处理请求(Request)
* 3:发送响应(Response)
*/
public class ClientHandler implements Runnable{
private Socket socket;//socket为成员变量 类中方法外 无需手动初始化,会自动赋予对应类型的默认值
public ClientHandler(Socket socket){//创建构造器(必须与类同名/没有返回值类型连void都没有)
/*
//成员变量与局部变量同名时,若想访问成员变量则this不能省略
//this.socket:访问上边的成员变量 等号后的socket为局部变量 位置:在方法里/局部代码块里
*/
this.socket = socket;
}
public void run(){
try {
//实例化HttpServerRequest解析请求的过程
HttpServerRequest request = new HttpServerRequest(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
(2)《WebServerApplication.java》:
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* WebServer主类
* WebServer是一个web容器,模拟Tomcat的基础功能。
* Web容器的两个主要任务:
* 1:管理部署在容器中的所有网络应用(WebApp),每个网络应用就是我们俗称的一个“网站”。
* 它通常包含页面,处理业务的代码,其他资源等等。
* 2:负责与客户端(通常是浏览器)完成TCP连接,并基于HTTP协议进行交互,使得客户端可以
* 通过网络远程调用容器中的某个网络应用。
*/
public class WebServerApplication {
/*
定义成员变量serverSocket:
//1)位置:类中、方法外
//2)无需手动初始化,会自动赋予对应类型的默认值
//3)作用域:在整个类中生效,类消失,变量才会消失
*/
private ServerSocket serverSocket;
public WebServerApplication(){//创建构造器(与类同名/没有返回值类型连void都没有)
try {
System.out.println("正在启动服务器...");//打桩输出
serverSocket = new ServerSocket(8088);//设置端口,启动服务端。按alt+insert+t-调用try-catch-处理异常
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){//写一个start方法
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();//等待接收连接。 按alt+insert+t-调用try-catch-处理异常
System.out.println("一个客户端连接了!");
//启动一个线程负责与该客户端交互
ClientHandler handler = new ClientHandler(socket);//ClientHandler:客户端处理程序
Thread t = new Thread(handler);//创建一个新的线程
t.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
/*
//1.定义局部变量server,引用类型WebServerApplication:
//1)位置:在方法里/局部代码块里
//2)必须手动初始化
//3)作用域:在方法/局部代码块中,对应的代码执行完局部变量就被释放
*/
WebServerApplication server = new WebServerApplication();//实例化
server.start();//调用start方法,跑起来
}
}
(3)《HttpServerRequest.java》:
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协议规定的客户端发送过来的一个请求内容。
* 每个请求由三部分构成:
* 请求行 消息头 消息正文
*/
public class HttpServerRequest {
/*
请求方式(SP)抽象路径(SP)协议版本(CRLF) 注:SP是空格 CRLF是回车加换行
GET/index.html HTTP/1.1
//请求行的相关信息:
*/
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议
//消息头相关信息
Map<String,String> headers = new HashMap<>();
private Socket socket;
/**定义一个构造方法:实例化请求对象的过程也是解析的过程*/
public HttpServerRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行:
parseRequestLine();
//1.2解析消息头:
parseHeaders();
//1.3解析消息正文:
parseContent();
}
/** 解析请求行 */
private void parseRequestLine() throws IOException {
//第一次调用readLine:
//1.1解析请求行
String line = readLine();
System.out.println(line);
//将请求行内容拆分出来并分别赋值给三个变量:
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//测试路径:http://localhost:8088/myweb/index.html
System.out.println("method:"+method);//method:GET
System.out.println("uri:"+uri);///myweb/uri:index.html
System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
}
/** 解析消息头 */
private void parseHeaders() throws IOException {
//第二次调用readLine:
//1.2解析消息头
while(true) {
String line = readLine();//读一行
/*
3种判断是否为空串的方式:
①if(line.isEmpty()){
②if(line.length()==0){ //若读取到的这一行长度为0
③if("".equals(line)){ //若读取到的这一行内容为空
*/
if(line.isEmpty()){//若读取的字符串为 *空串* ,说明单独读取了回车+换行
break;
}
System.out.println("消息头:" + line);//消息头:Host: localhost:8088
//将消息头的名字和值以key,value形式存入headers这个Map中:
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
}
/** 解析消息正文 */
private void parseContent(){
}
private String readLine() throws IOException {//单独定义readLine方法,为了读一行的方法(readLine())在这复用
InputStream in = socket.getInputStream(); //复用的代码不能处理异常(try-catch),要抛出异常!!
StringBuilder builder = new StringBuilder();
/*
局部变量 d 使用之前必须初始化(即赋值)!
【char pre='a',cur='a';】:在这里初始化了
*/
int d;
char pre='a',cur='a';//pre上一次读取的字符 cur本次读取到的字符
while((d = in.read())!=-1){
cur = (char)d;//将本地读取的字节转换为字符(char)赋值给cur
if(pre == 13 & cur == 10){//是否连续读取到了回车+换行
break;
}
builder.append(cur);//将本次读取到的字符cur拼接到StringBuilder中
//把cur再赋给pre:
pre = cur;//在进行下一个字符读取前将本地读取的字符记录为上次读取的字符
}
/*
return 值:-------用在有返回值方法中
//1)结束方法的执行 2)返回结果给调用方
将【String line = builder.toString().trim();】换为下面的代码,
因为需要返回一个值。
*/
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);
}
}
2、V6项目
(0)《readme.txt》:
此版本完成响应客户端的工作
这里先将ClientHandler中处理一次交互的第三步:响应客户端 实现出来。
目标:将一个固定的html页面通过发送一个标准的HTTP响应回复给浏览器使其呈现出来。
需要的知识点:
1:HTML基础语法,html是超文本标记语言,用于构成一个"网页"的语言。
2:HTTP的响应格式。
实现:
一:先创建第一个页面index.html
1:在src/main/resource下新建目录static
这个目录用于存放当前服务端下所有的网络应用中的静态资源。注:每个网路应用相当于一个"网站"的所有内容
而每个网络应用以一个独立的目录保存在static下,该目录的名字就是这个网络应用的名字。
2:在static目录下新建目录:myweb,作为我们的第一个"网站"
3:在myweb目录下新建第一个页面:index.html
二:实现将index.html页面响应给浏览器
在ClientHandler第三步发送响应处,按照HTTP协议规定的响应格式,将该页面包含在正文部分将其
发送给浏览器即可。
用这俩网址做测试:
http://localhost:8088/myweb/index.html
http://localhost:8088/myweb/classTable.html
具体过程:
(1)
①:ClientHandler包中:run()里添加【3、发送响应....到catch之间的代码---再添加finally】
(1)《ClientHandler.java》:
package com.webserver.core;
import com.webserver.http.HttpServerRequest;
import javax.xml.ws.handler.Handler;
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:处理请求(Request)
* 3:发送响应(Response)
*/
public class ClientHandler implements Runnable{
private Socket socket;//socket为成员变量 类中方法外 无需手动初始化,会自动赋予对应类型的默认值
public ClientHandler(Socket socket){//创建构造器(必须与类同名/没有返回值类型连void都没有)
/*
//成员变量与局部变量同名时,若想访问成员变量则this不能省略
//this.socket:访问上边的成员变量 等号后的socket为局部变量 位置:在方法里/局部代码块里
*/
this.socket = socket;
}
public void run(){
try {
//1、解析请求:(实例化HttpServerRequest解析请求的过程)
HttpServerRequest request = new HttpServerRequest(socket);
//2、处理请求
//例如:浏览器地址栏输入的路径为:http://localhost:8088/myweb/index.html
//那么解析请求后,得到的抽象路径部分uri为:/myweb/index.html
String path = request.getUri();
System.out.println("请求路径:"+path);//请求路径:/myweb/index.html
//3、发送响应
//临时测试:将resource目录中static/myweb/index.html响应给浏览器
/*
实际开发中,我们常用的相对路径都是类的加载路径。
对应的写法:
//类名.类. 类加载器. 获取资源("./")
类名.class.getClassLoader().getResource("./")
这里的"./"当前目录指的就是类加载路径的开始目录。它的实际位置
JVM理解的就是当前类的包名指定中最上级包的上一层(classes)。
例如:下面的代码中,当前类ClientHandler指定的包:
package com.webserver.core;
那么包的最上级就是com,因此类加载路径的开始目录就是com的上级目录
实际就是项目的target/classes这个目录了。
maven项目编译(在 idea最上边边框Build→build Project)后会将
src/main/java目录和src/main/resource目录最终合并到target/classes中。
*/
File file = new File(
//调用.toURI()方法,若路径错误,运行时则会在控制台抛出异常(:URISyntaxException)
/*
例如:浏览器地址栏输入的路径为:http://localhost:8088/myweb/index.html
那么解析请求后得到的抽象路径部分uri:/myweb/index.html 即为path
*/
ClientHandler.class.getClassLoader().getResource("./static"+path).toURI()
);
/*
响应的大致内容:
HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......
*/
//3.1、发送状态行
String line = "HTTP/1.1 200 OK";
println(line);
//3.2、发送响应头
line = "Content-Type: text/html";
println(line);
line = "Content-Length: "+file.length();//获取file文件的长度
println(line);
//单独发送回车+换行表示响应头部分发送完毕
println("");
//3.3、发送响应正文(二进制数据)
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024*10];//一次只能发10k,一般最大不会超过10240k
int len;//读取到的内容len
FileInputStream fis = new FileInputStream(file);
while((len = fis.read(buf))!=-1){//若(读取到的内容复制给len)没有读取到文件末尾
out.write(buf,0,len);//则将buf数组的内容从“0”开始到文件全部写出
}
System.out.println("响应发送完毕!");//
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}finally {
//一次HTTP交互后断开连接(HTTP协议要求):
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
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);//发送换行符
}
}
(2)《WebServerApplication.java》:
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* WebServer主类
* WebServer是一个web容器,模拟Tomcat的基础功能。
* Web容器的两个主要任务:
* 1:管理部署在容器中的所有网络应用(WebApp),每个网络应用就是我们俗称的一个“网站”。
* 它通常包含页面,处理业务的代码,其他资源等等。
* 2:负责与客户端(通常是浏览器)完成TCP连接,并基于HTTP协议进行交互,使得客户端可以
* 通过网络远程调用容器中的某个网络应用。
*/
public class WebServerApplication {
/*
定义成员变量serverSocket:
//1)位置:类中、方法外
//2)无需手动初始化,会自动赋予对应类型的默认值
//3)作用域:在整个类中生效,类消失,变量才会消失
*/
private ServerSocket serverSocket;
public WebServerApplication(){//创建构造器(与类同名/没有返回值类型连void都没有)
try {
System.out.println("正在启动服务器...");//打桩输出
serverSocket = new ServerSocket(8088);//设置端口,启动服务端。按alt+insert+t-调用try-catch-处理异常
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){//写一个start方法
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();//等待接收连接。 按alt+insert+t-调用try-catch-处理异常
System.out.println("一个客户端连接了!");
//启动一个线程负责与该客户端交互
ClientHandler handler = new ClientHandler(socket);//ClientHandler:客户端处理程序
Thread t = new Thread(handler);//创建一个新的线程
t.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
/*
//1.定义局部变量server,引用类型WebServerApplication:
//1)位置:在方法里/局部代码块里
//2)必须手动初始化
//3)作用域:在方法/局部代码块中,对应的代码执行完局部变量就被释放
*/
WebServerApplication server = new WebServerApplication();//实例化
server.start();//调用start方法,跑起来
}
}
(3)《HttpServerRequest.java》:
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协议规定的客户端发送过来的一个请求内容。
* 每个请求由三部分构成:
* 请求行 消息头 消息正文
*/
public class HttpServerRequest {
/*
请求方式(SP)抽象路径(SP)协议版本(CRLF) 注:SP是空格 CRLF是回车加换行
GET/index.html HTTP/1.1
//请求行的相关信息:
*/
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议
//消息头相关信息
Map<String,String> headers = new HashMap<>();
private Socket socket;
/**定义一个构造方法:实例化请求对象的过程也是解析的过程*/
public HttpServerRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行:
parseRequestLine();
//1.2解析消息头:
parseHeaders();
//1.3解析消息正文:
parseContent();
}
/** 解析请求行 */
private void parseRequestLine() throws IOException {
//第一次调用readLine:
//1.1解析请求行
String line = readLine();
System.out.println(line);
//将请求行内容拆分出来并分别赋值给三个变量:
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//测试路径:http://localhost:8088/myweb/index.html
System.out.println("method:"+method);//method:GET
System.out.println("uri:"+uri);///myweb/uri:index.html
System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
}
/** 解析消息头 */
private void parseHeaders() throws IOException {
//第二次调用readLine:
//1.2解析消息头
while(true) {
String line = readLine();//读一行
/*
3种判断是否为空串的方式:
①if(line.isEmpty()){
②if(line.length()==0){ //若读取到的这一行长度为0
③if("".equals(line)){ //若读取到的这一行内容为空
*/
if(line.isEmpty()){//若读取的字符串为 *空串* ,说明单独读取了回车+换行
break;
}
System.out.println("消息头:" + line);//消息头:Host: localhost:8088
//将消息头的名字和值以key,value形式存入headers这个Map中:
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
}
/** 解析消息正文 */
private void parseContent(){
}
private String readLine() throws IOException {//单独定义readLine方法,为了读一行的方法(readLine())在这复用
InputStream in = socket.getInputStream(); //复用的代码不能处理异常(try-catch),要抛出异常!!
StringBuilder builder = new StringBuilder();
/*
局部变量 d 使用之前必须初始化(即赋值)!
【char pre='a',cur='a';】:在这里初始化了
*/
int d;
char pre='a',cur='a';//pre上一次读取的字符 cur本次读取到的字符
while((d = in.read())!=-1){
cur = (char)d;//将本地读取的字节转换为字符(char)赋值给cur
if(pre == 13 & cur == 10){//是否连续读取到了回车+换行
break;
}
builder.append(cur);//将本次读取到的字符cur拼接到StringBuilder中
//把cur再赋给pre:
pre = cur;//在进行下一个字符读取前将本地读取的字符记录为上次读取的字符
}
/*
return 值:-------用在有返回值方法中
//1)结束方法的执行 2)返回结果给调用方
将【String line = builder.toString().trim();】换为下面的代码,
因为需要返回一个值。
*/
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);
}
}
(4)《classTable.java》:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>课程表</title>
</head>
<body>
<center>
<h5><font size="8">课程表</font></b></h5>
<table border="2">
<tr>
<td> </td>
<td>星期一</td>
<td>星期二</td>
<td>星期三</td>
<td>星期四</td>
<td>星期五</td>
</tr>
<tr>
<td rowspan="4">上午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td colspan="6" align="center">午休</td>
</tr>
<tr>
<td rowspan="4">下午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
</table>
</center>
</body>
</html>
(5)《index.html》:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>你好啊</title>
</head>
<body>
<!--
<h>标签:分为<h1>-<h6>表示各级标题。标题独占一行。
<center>标签:HTML5之后不再建议使用的标签。作用是将它包含的所有元素在浏览器
上居中显示
<input>标签:输入组件,用于获取用户在页面上的输入。输入组件可以有多种样子
具体可以通过type属性执行。例如:
type="text":文本框
type="password":密码框
type="radio":单选框
type="checkbox":复选框
type="button":无事件按钮
type="submit":提交查询按钮
<a>标签:超链接,标签中间指定超链接的文本信息,href属性用于指定点击后跳转
的位置
<br>标签:换行
<table>标签:表格。属性border用于指定边框。
<table>标签中包含<tr>标签用于表示行
<tr>标签中包含<td>标签用于表示列
<td>标签中常见属性:
align:对其方式。left左对齐,right右对齐,center剧中对其
colspan:跨列合并列,合并是从左向右合并列
rowspan:跨行合并列,合并是从上向下合并列
<td> </td>:" ":空格的意思
-->
<center>
<h1><b><font color="blue">百度</font></b></h1>
<input type="test" size="32">
<input type="button" value="点我一下" onclick="alert('点你妹啊')" >
<br>
<a href="http://www.taobao.com/">淘宝</a>
</br>
<table border="1">
<tr>
<td rowspan="2"><font color="green">你</font></td>
<td align="center"><font color="orange">是</font></td>
<td colspan="2" align="center">谁</td>
</tr>
<tr>
<td>第二列</td>
<td>第三列</td>
<td>第四列</td>
</tr>
</table>
</center>
</body>
</html>
3、V7项目:
(0)《readme.txt》:
完成404的响应
上一个版本中我们已经实现了根据浏览器中用户在地址栏上输入的URL中的抽象路径去
static目录下寻找对应资源进行响应的工作。
但是会存在路径输入有误,导致定位不对(要么定位的是一个目录,要么该文件不存在)
此时再发送响应的响应正文时使用文件输入流读取就会出现异常提示该资源不存在。
这是一个典型的404情况,因此我们在ClientHandler处理请求的环节,在实例化File
对象根据抽象路径定位webapps下的资源后,要添加一个分支,若该资源存在则将其响应
回去,如果不存在则要响应404状态代码和404页面提示用户。
实现:
1:在static下新建一个子目录root 【!注意:此处在resources目录下右键new点击directory:取名为static/root】
该目录用于保存当前服务端所有网络应用共用的资源,比如404页面,因为无论请求哪个
网络应用中的资源都可能发生不存在的情况。
2:在root目录下新建页面:404.html
该页面居中显示一行字即可:404,资源不存在!
3:在ClientHandler处理请求的环节,当实例化File对象后添加一个分支,如果该File
对象存在且表示的是一个文件则将其响应给浏览器
否则发送的响应做如下变化:
1:状态行中的状态代码改为404,状态描述改为NotFound
2:响应头Content-Length发送的是404页面的长度
3:响应正文为404页面内容
完成后,在浏览器地址栏输入一个不存在的资源地址(http://localhost:8088/myweb/index456.html),检查服务端是否正确响应404页面
测试网址:
http://localhost:8088/myweb
http://localhost:8088/myweb/index.html
http://localhost:8088/myweb/classTable.html
(1)《ClientHandler.java》:
package com.webserver.core;
import com.webserver.http.HttpServerRequest;
import javax.xml.ws.handler.Handler;
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:处理请求(Request)
* 3:发送响应(Response)
*/
public class ClientHandler implements Runnable{
private Socket socket;//socket为成员变量 类中方法外 无需手动初始化,会自动赋予对应类型的默认值
public ClientHandler(Socket socket){//创建构造器(必须与类同名/没有返回值类型连void都没有)
/*
//成员变量与局部变量同名时,若想访问成员变量则this不能省略
//this.socket:访问上边的成员变量 等号后的socket为局部变量 位置:在方法里/局部代码块里
*/
this.socket = socket;
}
public void run(){
try {
//1、解析请求:(实例化HttpServerRequest解析请求的过程)
HttpServerRequest request = new HttpServerRequest(socket);
//2、处理请求
//例如:浏览器地址栏输入的路径为:http://localhost:8088/myweb/index.html
//那么解析请求后,得到的抽象路径部分uri为:/myweb/index.html
String path = request.getUri();
System.out.println("请求路径:"+path);//请求路径:/myweb/index.html
//3、发送响应
//临时测试:将resource目录中static/myweb/index.html响应给浏览器
/*
实际开发中,我们常用的相对路径都是类的加载路径。
对应的写法:
//类名.类. 类加载器. 获取资源("./")
类名.class.getClassLoader().getResource("./")
这里的"./"当前目录指的就是类加载路径的开始目录。它的实际位置
JVM理解的就是当前类的包名指定中最上级包的上一层(classes)。
例如:下面的代码中,当前类ClientHandler指定的包:
package com.webserver.core;
那么包的最上级就是com,因此类加载路径的开始目录就是com的上级目录
实际就是项目的target/classes这个目录了。
maven项目编译(在 idea最上边边框Build→build Project)后会将
src/main/java目录和src/main/resource目录最终合并到target/classes中。
*/
/*
File(File parent,String sub):阅读文档,理解该构造方法
提示:static目录是确定存在的目录
我们是要找这个目录下的内容是否存在
*/
File staticDir = new File(
//调用.toURI()方法,若路径错误,运行时则会在控制台抛出异常(:URISyntaxException)
/*
例如:浏览器地址栏输入的路径为:http://localhost:8088/myweb/index.html
那么解析请求后得到的抽象路径部分uri:/myweb/index.html 即为path
*/
ClientHandler.class.getClassLoader().getResource("./static").toURI()
);
//去static目录下根据用于请求的抽象路径定位下面的文件
File file = new File(staticDir,path);
String line;//状态行
if(file.isFile()){ //是一个实际存在的文件,往下走↓
line = "HTTP/1.1 200 OK(CRLF)";
}else{ //1:文件不存在 2:是一个目录
line = "HTTP/1.1 404 NotFound";
file = new File(staticDir,"/root/404.html");
}
/*
响应的大致内容:
HTTP/1.1 200 OK(CRLF)
Content-Type: text/html(CRLF)
Content-Length: 2546(CRLF)(CRLF)
1011101010101010101......
*/
//3.1、发送状态行
// String line = "HTTP/1.1 200 OK";
println(line);
//3.2、发送响应头
line = "Content-Type: text/html";
println(line);
line = "Content-Length: "+file.length();//获取file文件的长度
println(line);
//单独发送回车+换行表示响应头部分发送完毕
println("");
//3.3、发送响应正文(二进制数据)
OutputStream out = socket.getOutputStream();
byte[] buf = new byte[1024*10];//一次只能发10k,一般最大不会超过10240k
int len;//读取到的内容len
FileInputStream fis = new FileInputStream(file);
while((len = fis.read(buf))!=-1){//若(读取到的内容复制给len)没有读取到文件末尾
out.write(buf,0,len);//则将buf数组的内容从“0”开始到文件全部写出
}
System.out.println("响应发送完毕!");//
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
}finally {
//一次HTTP交互后断开连接(HTTP协议要求):
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
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);//发送换行符
}
}
(2)《WebServerApplication.java》:
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* WebServer主类
* WebServer是一个web容器,模拟Tomcat的基础功能。
* Web容器的两个主要任务:
* 1:管理部署在容器中的所有网络应用(WebApp),每个网络应用就是我们俗称的一个“网站”。
* 它通常包含页面,处理业务的代码,其他资源等等。
* 2:负责与客户端(通常是浏览器)完成TCP连接,并基于HTTP协议进行交互,使得客户端可以
* 通过网络远程调用容器中的某个网络应用。
*/
public class WebServerApplication {
/*
定义成员变量serverSocket:
//1)位置:类中、方法外
//2)无需手动初始化,会自动赋予对应类型的默认值
//3)作用域:在整个类中生效,类消失,变量才会消失
*/
private ServerSocket serverSocket;
public WebServerApplication(){//创建构造器(与类同名/没有返回值类型连void都没有)
try {
System.out.println("正在启动服务器...");//打桩输出
serverSocket = new ServerSocket(8088);//设置端口,启动服务端。按alt+insert+t-调用try-catch-处理异常
System.out.println("服务端启动完毕!");
} catch (IOException e) {
e.printStackTrace();
}
}
public void start(){//写一个start方法
try {
System.out.println("等待客户端连接...");
Socket socket = serverSocket.accept();//等待接收连接。 按alt+insert+t-调用try-catch-处理异常
System.out.println("一个客户端连接了!");
//启动一个线程负责与该客户端交互
ClientHandler handler = new ClientHandler(socket);//ClientHandler:客户端处理程序
Thread t = new Thread(handler);//创建一个新的线程
t.start();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
/*
//1.定义局部变量server,引用类型WebServerApplication:
//1)位置:在方法里/局部代码块里
//2)必须手动初始化
//3)作用域:在方法/局部代码块中,对应的代码执行完局部变量就被释放
*/
WebServerApplication server = new WebServerApplication();//实例化
server.start();//调用start方法,跑起来
}
}
(3)《HttpServerRequest.java》:
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协议规定的客户端发送过来的一个请求内容。
* 每个请求由三部分构成:
* 请求行 消息头 消息正文
*/
public class HttpServerRequest {
/*
请求方式(SP)抽象路径(SP)协议版本(CRLF) 注:SP是空格 CRLF是回车加换行
GET/index.html HTTP/1.1
//请求行的相关信息:
*/
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议
//消息头相关信息
Map<String,String> headers = new HashMap<>();
private Socket socket;
/**定义一个构造方法:实例化请求对象的过程也是解析的过程*/
public HttpServerRequest(Socket socket) throws IOException {
this.socket = socket;
//1.1解析请求行:
parseRequestLine();
//1.2解析消息头:
parseHeaders();
//1.3解析消息正文:
parseContent();
}
/** 解析请求行 */
private void parseRequestLine() throws IOException {
//第一次调用readLine:
//1.1解析请求行
String line = readLine();
System.out.println(line);
//将请求行内容拆分出来并分别赋值给三个变量:
String[] data = line.split("\\s");
method = data[0];
uri = data[1];
protocol = data[2];
//测试路径:http://localhost:8088/myweb/index.html
System.out.println("method:"+method);//method:GET
System.out.println("uri:"+uri);///myweb/uri:index.html
System.out.println("protocol:"+protocol);//protocol:HTTP/1.1
}
/** 解析消息头 */
private void parseHeaders() throws IOException {
//第二次调用readLine:
//1.2解析消息头
while(true) {
String line = readLine();//读一行
/*
3种判断是否为空串的方式:
①if(line.isEmpty()){
②if(line.length()==0){ //若读取到的这一行长度为0
③if("".equals(line)){ //若读取到的这一行内容为空
*/
if(line.isEmpty()){//若读取的字符串为 *空串* ,说明单独读取了回车+换行
break;
}
System.out.println("消息头:" + line);//消息头:Host: localhost:8088
//将消息头的名字和值以key,value形式存入headers这个Map中:
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("headers:"+headers);
}
/** 解析消息正文 */
private void parseContent(){
}
private String readLine() throws IOException {//单独定义readLine方法,为了读一行的方法(readLine())在这复用
InputStream in = socket.getInputStream(); //复用的代码不能处理异常(try-catch),要抛出异常!!
StringBuilder builder = new StringBuilder();
/*
局部变量 d 使用之前必须初始化(即赋值)!
【char pre='a',cur='a';】:在这里初始化了
*/
int d;
char pre='a',cur='a';//pre上一次读取的字符 cur本次读取到的字符
while((d = in.read())!=-1){
cur = (char)d;//将本地读取的字节转换为字符(char)赋值给cur
if(pre == 13 & cur == 10){//是否连续读取到了回车+换行
break;
}
builder.append(cur);//将本次读取到的字符cur拼接到StringBuilder中
//把cur再赋给pre:
pre = cur;//在进行下一个字符读取前将本地读取的字符记录为上次读取的字符
}
/*
return 值:-------用在有返回值方法中
//1)结束方法的执行 2)返回结果给调用方
将【String line = builder.toString().trim();】换为下面的代码,
因为需要返回一个值。
*/
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);
}
}
(4)《classTable.java》:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>课程表</title>
</head>
<body>
<center>
<h5><font size="8">课程表</font></b></h5>
<table border="2">
<tr>
<td> </td>
<td>星期一</td>
<td>星期二</td>
<td>星期三</td>
<td>星期四</td>
<td>星期五</td>
</tr>
<tr>
<td rowspan="4">上午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td colspan="6" align="center">午休</td>
</tr>
<tr>
<td rowspan="4">下午</td>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
<tr>
<td>语文</td>
<td>数学</td>
<td>英语</td>
<td>体育</td>
<td>生物</td>
</tr>
</table>
</center>
</body>
</html>
(5)《index.html》:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>你好啊</title>
</head>
<body>
<!--
<h>标签:分为<h1>-<h6>表示各级标题。标题独占一行。
<center>标签:HTML5之后不再建议使用的标签。作用是将它包含的所有元素在浏览器
上居中显示
<input>标签:输入组件,用于获取用户在页面上的输入。输入组件可以有多种样子
具体可以通过type属性执行。例如:
type="text":文本框
type="password":密码框
type="radio":单选框
type="checkbox":复选框
type="button":无事件按钮
type="submit":提交查询按钮
<a>标签:超链接,标签中间指定超链接的文本信息,href属性用于指定点击后跳转
的位置
<br>标签:换行
<table>标签:表格。属性border用于指定边框。
<table>标签中包含<tr>标签用于表示行
<tr>标签中包含<td>标签用于表示列
<td>标签中常见属性:
align:对其方式。left左对齐,right右对齐,center剧中对其
colspan:跨列合并列,合并是从左向右合并列
rowspan:跨行合并列,合并是从上向下合并列
<td> </td>:" ":空格的意思
-->
<center>
<h1><b><font color="blue">百度</font></b></h1>
<input type="test" size="32">
<input type="button" value="点我一下" onclick="alert('点你妹啊')" >
<br>
<a href="http://www.taobao.com/">淘宝</a>
</br>
<table border="1">
<tr>
<td rowspan="2"><font color="green">你</font></td>
<td align="center"><font color="orange">是</font></td>
<td colspan="2" align="center">谁</td>
</tr>
<tr>
<td>第二列</td>
<td>第三列</td>
<td>第四列</td>
</tr>
</table>
</center>
</body>
</html>
(6)《404.html》:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<center><h5><font color="red">404,资源不存在!</font></h5></center>
</body>
</html>