重构功能拆分,将ClientHandler中第一个环节解析请求的细节拆分出去,使得
ClientHandler仅关心处理一次HTTP交互的流程控制。
实现:
1:新建一个包:com.webserver.http
2:在http包下新建类:HttpServletRequest 请求对象
使用这个类的每一个实例表示客户端发送过来的一个HTTP请求
3:在HttpServletRequest的构造方法中完成解析请求的工作
4:ClientHandler第一步解析请求只需要实例化一个HttpServletRequest即可.
package com.webserver.core;
public class ClientHandler implements Runnable{
private Socket socket;
public ClientHandler(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try{
//1.解析请求,实例化请求对象的过程就是解析的过程
HttpServletRequest request=new HttpServletRequest(socket);
//2.处理请求
String path = request.getUri();
System.out.println("请求的抽象路径:"+path);
//3.发送响应
} catch (IOException e) {
e.printStackTrace();
}
}
}
package com.webserver.http;
public class HttpServletRequest {
private Socket socket;
private String method;//请求方式
private String uri;//抽象路径
private String protocol;//协议版本
private Map<String,String> headers=new HashMap<>();
public HttpServletRequest(Socket socket) throws IOException {
this.socket = socket;
//1.解析请求行
parseRequestLine();
//2.解析消息头
parseRequestHeaders();
//3.解析消息正文
parseContent();
}
//解析请求行
private void parseRequestLine() throws IOException {
String line=readLine();
// System.out.println("请求行:"+line);
//请求相关信息
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 parseRequestHeaders() throws IOException {
//获取消息头
while (true){
String line=readLine();
if(line.isEmpty()){//如果单独读取回车+换行,readLine方法会返回空串
break;
}
System.out.println("原消息头:"+line);
String[] data = line.split(":\\s");
headers.put(data[0],data[1]);
}
System.out.println("-----------------------");
headers.forEach((k,v)-> System.out.println("集合中的消息头:"+k+":"+v));
}
//解析消息正文
private void parseContent(){
}
//定义一次读取一行的方法
private String readLine() throws IOException {
InputStream is = socket.getInputStream();
int d;
StringBuilder builder=new StringBuilder();
char cur=' ';
char pre=' ';
while ((d=is.read())!=-1){//这样可以不止读取文本信息
cur=(char)d;
if(cur==10&&pre==13){
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 getHeaders(String name) {
return headers.get(name);
}
}
此版本完成响应客户端的工作
这里先将ClientHandler中处理一次交互的第三步:响应客户端 实现出来。
目标:将一个固定的html页面通过发送一个标准的HTTP响应回复给浏览器使其呈现出来。
需要的知识点:
1:HTML基础语法,html是超文本标记语言,用于构成一个"网页"的语言。
2:HTTP的响应格式。
实现:
一:先创建第一个页面index.html
1:在src/main/resource下新建目录static
这个目录用于存放当前服务端下所有的静态资源。
2:在static目录下新建目录新建第一个页面:index.html
二:实现将index.html页面响应给浏览器
在ClientHandler第三步发送响应处,按照HTTP协议规定的响应格式,将该页面包含在正文部分将其发送给浏览器即可。
三:第二步测试成功后,我们就可以根据请求中浏览器传递过来的抽象路径去static目录下定位浏览器实际
请求的页面,然后用上述方式将该页面响应给浏览器,达到浏览器自主请求其需要的资源。
四:一问一答实现后,在ClientHandler异常处理机制最后添加finally并最终与客户端断开链接。这也是
HTTP协议的要求,因此在这里调用socket.close()
五:可以在WebServerApplication中的start方法里将接受客户端链接的动作放在死循环里重复进行了。
无论浏览器请求多少次,我们都可以遵从一问一答的原则响应用户请求的所有页面了。
动态返回static目录下的html页面给浏览器
public class ClientHandler implements Runnable{
private static File dir;
private static File staticDir;
static {
try {
dir=new File(ClientHandler.class.getClassLoader().getResource(".").toURI());
//定位target/classes/static目录
staticDir=new File(dir,"static");
System.out.println("static是否存在:"+staticDir.exists());
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private Socket socket;
public ClientHandler(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try{
//1.解析请求,实例化请求对象的过程就是解析的过程
HttpServletRequest request=new HttpServletRequest(socket);
//2.处理请求
String path = request.getUri();
System.out.println("请求的抽象路径:"+path);
//3.发送响应
File file=new File(staticDir,path);
OutputStream out = socket.getOutputStream();
String line="HTTP/1.1 200 OK";
byte [] data=line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
line="Content-Type: text/html";
data=line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
line="Content-Length: "+file.length();
data=line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
out.write(13);
out.write(10);
FileInputStream fis=new FileInputStream(file);
int len=0;
byte[] buf=new byte[1024*10];
while ((len=fis.read(buf))!=-1){
out.write(buf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
完成404的响应
上一个版本中我们已经实现了根据浏览器中用户在地址栏上输入的URL中的抽象路径去
static目录下寻找对应资源进行响应的工作。
但是会存在路径输入有误,导致定位不对(要么定位的是一个目录,要么该文件不存在)
此时再发送响应的响应正文时使用文件输入流读取就会出现异常提示该资源不存在。
这是一个典型的404情况,因此我们在ClientHandler处理请求的环节,在实例化File
对象根据抽象路径定位webapps下的资源后,要添加一个分支,若该资源存在则将其响应
回去,如果不存在则要响应404状态代码和404页面提示用户。
实现:
1:在static下新建一个子目录root
该目录用于保存当前服务端所有网络应用共用的资源,比如404页面,因为无论请求哪个
网络应用中的资源都可能发生不存在的情况。
2:在root目录下新建页面:404.html
该页面居中显示一行字即可:404,资源不存在!
3:在ClientHandler处理请求的环节,当实例化File对象后添加一个分支,如果该File
对象存在且表示的是一个文件则将其响应给浏览器
否则发送的响应做如下变化:
1:状态行中的状态代码改为404,状态描述改为NotFound
2:响应头Content-Length发送的是404页面的长度
3:响应正文为404页面内容
完成后,在浏览器地址栏输入一个不存在的资源地址,检查服务端是否正确响应404页面
http://localhost:8088/root
http://localhost:8088/myweb/
http://localhost:8088/myweb/123.html
流程控制代码
public class ClientHandler implements Runnable{
private static File dir;
private static File staticDir;
static {
try {
dir=new File(ClientHandler.class.getClassLoader().getResource(".").toURI());
//定位target/classes/static目录
staticDir=new File(dir,"static");
// System.out.println("static是否存在:"+staticDir.exists());
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private Socket socket;
public ClientHandler(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try{
//1.解析请求,实例化请求对象的过程就是解析的过程
HttpServletRequest request=new HttpServletRequest(socket);
//2.处理请求
//请求的抽象路径
String path = request.getUri();
//3.发送响应
File file=new File(staticDir,path);//按照浏览器访问的路径寻找资源文件
String line;
//如果访问的资源不存在
if(!file.isFile()){
file=new File(staticDir,"/root/404.html");
line="HTTP/1.1 404 NotFound";
}else {
line = "HTTP/1.1 200 OK";
}
OutputStream out = socket.getOutputStream();
byte [] data=line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
line="Content-Type: text/html";
data=line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
line="Content-Length: "+file.length();
data=line.getBytes(StandardCharsets.ISO_8859_1);
out.write(data);
out.write(13);
out.write(10);
out.write(13);
out.write(10);
FileInputStream fis=new FileInputStream(file);
int len=0;
byte[] buf=new byte[1024*10];
while ((len=fis.read(buf))!=-1){
out.write(buf,0,len);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
错误页面
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>错误title>
head>
<body>
<center>
<h1>404,资源不存在!h1>
center>
body>
html>
服务端的永久启动
package com.webserver.core;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* WebServer web容器
* 用于实现Tomcat基本功能
*/
public class WebServerApplication {
private ServerSocket serverSocket;
public WebServerApplication() {
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) {
WebServerApplication server=new WebServerApplication();
server.start();
}
}