写在前面的话:
- 版权声明:本文为博主原创文章,转载请注明出处!
- 博主是一个小菜鸟,并且非常玻璃心!如果文中有什么问题,请友好地指出来,博主查证后会进行更正,啾咪~~
- 每篇文章都是博主现阶段的理解,如果理解的更深入的话,博主会不定时更新文章。
- 本文初次更新时间:2020.12.27,最后更新时间:2021.01.01
正文开始
1. 解决空请求问题
HTTP协议有说明这种情况,允许客户端连接服务端后发送空请求
,实际并没有发送标准的请求内容。
对于我们现阶段代码而言,ClientHandler
做的第一步就是解析请求,而请求对象HttpRequest
实例化时一定会开始解析工作顺序解析请求行、消息头、消息正文,若客户端发送的是空请求,则在解析请求行获取其中三部分信息method
、url
、protocol
时由于拆分不出三项,会导致下标越界
报错。
解决办法:
当解析请求行是遇到空请求时,直接向
ClientHandler
抛出空请求异常
(一个自定义异常),当ClientHandler
接收到该异常后忽略本次处理客户端的后续操作。
大概流程如下:
- 在
com.webserver.http
包中定义一个类EmptyRequestException
,使用这个类的实例表示一个空请求异常
。- 在
HttpRequest
解析请求行的方法parseRequestLine
中当读取请求行后,添加判断,若这行字符串是一个空字符串,则这个请求是一个空请求,那么就实例化一个空请求异常并将其抛出。- 在
HttpRequest
的构造方法中继续声明将空请求异常抛出。- 在
ClientHandler
中,添加一个catch
,单独捕获空请求异常
,这样当实例化HttpRequest
解析请求时若其构造方法抛出该异常则跳过后续处理请求的所有步骤,直接与客户端断开连接即可。
1.1 空请求异常类
1.1.1 创建 EmptyRequestException 类
在com.webserver.http
包中定义一个类EmptyRequestException
,使用这个类的实例表示一个空请求异常
:
EmptyRequestException
继承Exception类
:
package com.webserver.http;
/**
* 空请求异常
* 当客户端发送空请求时,HttpRequest的构造方法会抛出该异常
* @author returnzc
*
*/
public class EmptyRequestException extends Exception {
}
1.1.2 生成默认的序列化版本号
序列号版本号:
对象流可以读写java中的对象,序列号版本号影响着反序列化是否成功,要求
implements Serializable
的都要加。
生成默认的序列化版本号:
生成的序列号版本号如下:
//序列号版本号
private static final long serialVersionUID = 1L;
1.1.3 生成构造方法
在空白区域【右键】->【Source】->【Generate Constructors from Superclass】:
自定义异常没什么业务逻辑,只要把名字写清楚就可以了。如下:
package com.webserver.http;
/**
* 空请求异常
* 当客户端发送空请求时,HttpRequest的构造方法会抛出该异常
* @author returnzc
*
*/
public class EmptyRequestException extends Exception {
//序列号版本号
private static final long serialVersionUID = 1L;
public EmptyRequestException() {
super();
// TODO Auto-generated constructor stub
}
public EmptyRequestException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
// TODO Auto-generated constructor stub
}
public EmptyRequestException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public EmptyRequestException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
public EmptyRequestException(Throwable cause) {
super(cause);
// TODO Auto-generated constructor stub
}
}
1.2 抛出空请求异常
首先需要在HttpRequest类
中的parseRequestLine()方法
添加对空请求的判断,如果是空请求,则处理这个空请求异常;但是这个异常不在该方法处理,所以将该空请求异常抛出。这里抛出异常就相当于抛给调用方,实际上是在HttpRequest
的构造方法
中调用的parseRequestLine()方法
,同样如果我们在解析请求行的时候里面抛出来了告诉你这是个空请求,那就要接着对外抛出去,因为我们要告诉ClientHandler
这是个空请求,所以接着往外抛。即:
- parseRequestLine() 判断并抛出空请求异常
- HttpRequest 构造方法抛出空请求异常
- ClientHandler 捕获空请求异常
1.2.1 parseRequestLine() 判断并抛出空请求异常
//若请求行内容是一个空串,则是空请求
if ("".equals(line)) {
throw new EmptyRequestException();
}
具体如下:
/**
* 解析请求行
* @throws EmptyRequestException
*/
private void parseRequestLine() throws EmptyRequestException {
System.out.println("开始解析请求行...");
try {
String line = readLine();
System.out.println("请求行:" + line);
//若请求行内容是一个空串,则是空请求
if ("".equals(line)) {
throw new EmptyRequestException();
}
//将请求行进行拆分,将每部分内容对应的设置到属性上
String[] data = line.split("\\s");
this.method = data[0];
this.url = data[1];
this.protocol = data[2];
System.out.println("method: " + method);
System.out.println("url: " + url);
System.out.println("protocol: " + protocol);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("请求行解析完毕!");
}
1.2.2 HttpRequest 构造方法抛出空请求异常
public HttpRequest(Socket socket) throws EmptyRequestException {
try {
this.socket = socket;
this.in = socket.getInputStream();
/*
* 解析请求的过程:
* 1. 解析请求行
* 2. 解析消息头
* 3. 解析消息正文
*/
parseRequestLine();
parseHeaders();
parseContent();
} catch (IOException e) {
e.printStackTrace();
}
}
1.2.3 ClientHandler 捕获空请求异常
ClientHandler捕获着最大的异常,涵盖着空请求异常,空请求我们在这里抛出来只是为了达到流程,所以这种异常实际上是不需要处理的,跟其他的异常不一样,其他的异常需要关注并且输出,因为这是个bug,但是空请求是我们有意抛给外界的,是让你注意有这个情况的话,后续逻辑就不走了,单独捕获一个。
该异常的捕获一定要放在捕获Exception
前面:
catch (EmptyRequestException e) {
System.out.println("空请求");
}
改后代码:
package com.webserver.core;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import com.webserver.http.EmptyRequestException;
import com.webserver.http.HttpRequest;
import com.webserver.http.HttpResponse;
/**
* 处理客户端请求
* @author returnzc
*
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
//解析请求
HttpRequest request = new HttpRequest(socket);
//创建响应
HttpResponse response = new HttpResponse(socket);
//处理请求
String url = request.getUrl();
File file = new File("webapps" + url);
if (file.exists()) {
System.out.println("该资源已找到!");
//将该资源响应给客户端
response.setEntity(file);
response.flush();
} else {
System.out.println("该资源不存在!");
}
} catch (EmptyRequestException e) {
System.out.println("空请求");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//响应完毕后与客户端断开连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2. 支持 tomcat 中所有的介质类型(XML文档)
前面我们说到,tomcat支持1000多种介质类型,前面我们只支持了几种类型,这里我们修改成支持tomcat中所有的介质类型。
Tomcat
安装目录中的conf
目录里有一个web.xml
文件,该文件中整理了所有Content-Type
所支持的值,我们将他们解析出来用于初始化HttpContext
中mimeTypeMapping
。
过程如下:
- 在当前项目目录下新建一个目录
conf
- 将
tomcat
安装目录中conf/web.xml
拷贝到我们的conf
目录中- 修改
HttpContext
初始化mimeTypeMapping
的方法:initMimeTypeMapping()
。
也就是说,将原有的直接向mimeTypeMapping
这个Map中put元素的代码全部删除,改为通过解析conf/web.xml
中所有的mime来完成初始化。
2.1 拷贝 conf 目录
步骤:
- 在当前项目目录下新建一个目录
conf
: - 将
tomcat
安装目录中conf/web.xml
拷贝到我们的conf
目录中
目前项目目录结构如下:
2.2 maven 配置
Maven组件坐标可以在Eclipse中搜索, 也可以利用在线工具搜索,例如阿里云jar组件"坐标"搜索【地址】。
打开 maven 的配置文件( windows 机器一般在 maven 安装目录的 conf/settings.xml
),在<mirrors></mirrors>
标签中添加 mirror 子节点:
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
然后在 pom.xml
文件<denpendencies></denpendencies>
节点中加入要引用的文件信息:
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
贴一下这时我的 pom.xml
文件:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.returnzc</groupId>
<artifactId>MyWebServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
</project>
在保存pom.xml
文件时,会自动下载jar组件
:
这时我们便可以import
之后会用到的:
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
2.3 修改 initMimeTypeMapping() 方法
修改初始化Mime
类型映射过程:
- 解析
conf/web.xml
。 - 获取该文件中根标签下所有名为
<mime-mapping>
的子标签,注意根标签下不止有<mime-mapping>
名字的子标签,还有其他的,所以获取所有子标签时必须指定名字。 - 遍历每一个
<mime-mapping>
标签:
(1) 将其子标签<extension>
中间的文本作为key
。
(2) 将其子标签<mime-type>
中间的文本作为value
。
(3) 存入到mimeTypeMapping
这个Map
中完成初始化。
解析后该mimeTypeMapping
中应当有1000多个元素。
代码如下:
/**
* 初始化 Mime 类型映射
*/
private static void initMimeTypeMapping() {
try {
SAXReader reader = new SAXReader();
Document doc = reader.read(new File("conf/web.xml"));
Element root = doc.getRootElement();
List<Element> mimeList = root.elements("mime-mapping");
for (Element mimeEle : mimeList) {
String key = mimeEle.elementText("extension");
String value = mimeEle.elementText("mime-type");
mimeTypeMapping.put(key, value);
}
} catch (DocumentException e) {
e.printStackTrace();
}
}
3. 添加响应404页面的功能
当用户请求的地址无效时,服务端要向客户端响应404
错误。
当前HttpResponse
发送状态行时,状态代码和对应描述是固定的信息为200 OK
,因此我们要将状态代码也改变为可以进行设置的。但出于方便(大部分请求应当都是正常的),我们可以有默认值为200 OK
,这样大部分响应就无需都设置状态代码了。
当用户请求路径有误时,我们需要做以下操作:
- 响应中的
状态代码
应当改为404
,状态描述
变为NOT FOUND
,并且响应的正文内容
为一个404页面
,404页面应当是一个公共页面
,无论哪个网络应用都可能会出现用户请求的路径不对,导致要响应该页面,对此我们将此页面单独存放在一个公共目录中。 - 由于不同的状态代码有对应的状态描述(HTTP协议有建议值),我们可以在
HttpContext
类中再定义一个Map
,key
保存状态代码,value
保存建议的状态描述。 - 当我们调用
HttpResponse
设置状态代码的方法来设置状态代码时,可以同时根据状态代码找到对应的状态描述并自动进行设置。(由于我们提供了单独设置状态描述的set方法,所以若不想使用建议值,也可再设置完状态代码后再单独设置状态描述,但通常不需要这样做) 。
大致流程如下:
- 在
HttpResponse
中添加两个属性:int statusCode
、String statusReason
,默认值分别为200
、"OK"
。- 修改发送状态行的方法:
sendStatusLine
,将原有的固定发送改为根据这两个属性发送。- 在
HttpContext
中定义一个Map属性
:statusCodeReasonMapping
。- 再定义一个静态方法,可根据状态代码获取对应状态描述。
- 再定义一个静态方法用于初始化这个Map,并在
HttpContext
的静态块中执行来完成初始化操作。- 修改
HttpResponse
设置状态代码的方法,添加额外逻辑,根据设置的状态代码从HttpContext
中获取对应的状态描述,并自动设置到对应属性上。- 在
webapps
下新建一个目录root
,用这个目录存放公共资源
。- 在
root
下新建404.html
,修改ClientHandler
,当用户请求的资源不存在时,我们在分支中对response
进行设置。- 设置
response
的状态代码为404
。- 设置
response
响应实体为404页面
。
接下来可能不是严格按照上面流程的顺序,不过每一步都是会修改到的。
3.1 修改 HttpContext 类
定义一个Map
属性statusCodeReasonMapping
,key
保存状态代码,value
保存建议的状态描述:
//状态代码与描述的映射
private static Map<Integer, String> statusCodeReasonMapping = new HashMap<Integer, String>();
定义一个静态方法,可根据状态代码获取对应状态描述:
/**
* 根据给定的状态代码获取其对应的状态描述
* @param statusCode
* @return
*/
public static String getStatusReason(int statusCode) {
return statusCodeReasonMapping.get(statusCode);
}
定义一个静态方法用于初始化这个Map,并在HttpContext
的静态块中执行来完成初始化操作:
/**
* 初始化静态资源
*/
static {
//初始化mime类型
initMimeTypeMapping();
//初始化状态代码与描述的映射
initStatusCodeReasonMapping();
}
/**
* 初始化状态代码与描述的映射
*/
private static void initStatusCodeReasonMapping() {
statusCodeReasonMapping.put(200, "OK");
statusCodeReasonMapping.put(201, "Created");
statusCodeReasonMapping.put(202, "Accepted");
statusCodeReasonMapping.put(204, "No Content");
statusCodeReasonMapping.put(301, "Moved Permanently");
statusCodeReasonMapping.put(302, "Moved Temporarily");
statusCodeReasonMapping.put(304, "Not Modified");
statusCodeReasonMapping.put(400, "Bad Request");
statusCodeReasonMapping.put(401, "Unauthorized");
statusCodeReasonMapping.put(403, "Forbidden");
statusCodeReasonMapping.put(404, "Not Found");
statusCodeReasonMapping.put(500, "Internal Server Error");
statusCodeReasonMapping.put(501, "Not Implemented");
statusCodeReasonMapping.put(502, "Bad Gateway");
statusCodeReasonMapping.put(503, "Service Unavailable");
}
可以通过在main函数中执行下面语句进行测试:
String reason = getStatusReason(404);
System.out.println(reason);
HttpContext 最终代码:
package com.webserver.http;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
* HTTP协议相关信息定义
* @author returnzc
*
*/
public class HttpContext {
//介质类型映射
private static Map<String, String> mimeTypeMapping = new HashMap<String, String>();
//状态代码与描述的映射
private static Map<Integer, String> statusCodeReasonMapping = new HashMap<Integer, String>();
/**
* 初始化静态资源
*/
static {
//初始化mime类型
initMimeTypeMapping();
//初始化状态代码与描述的映射
initStatusCodeReasonMapping();
}
/**
* 初始化状态代码与描述的映射
*/
private static void initStatusCodeReasonMapping() {
statusCodeReasonMapping.put(200, "OK");
statusCodeReasonMapping.put(201, "Created");
statusCodeReasonMapping.put(202, "Accepted");
statusCodeReasonMapping.put(204, "No Content");
statusCodeReasonMapping.put(301, "Moved Permanently");
statusCodeReasonMapping.put(302, "Moved Temporarily");
statusCodeReasonMapping.put(304, "Not Modified");
statusCodeReasonMapping.put(400, "Bad Request");
statusCodeReasonMapping.put(401, "Unauthorized");
statusCodeReasonMapping.put(403, "Forbidden");
statusCodeReasonMapping.put(404, "Not Found");
statusCodeReasonMapping.put(500, "Internal Server Error");
statusCodeReasonMapping.put(501, "Not Implemented");
statusCodeReasonMapping.put(502, "Bad Gateway");
statusCodeReasonMapping.put(503, "Service Unavailable");
}
/**
* 初始化Mime类型映射
*/
private static void initMimeTypeMapping() {
try {
SAXReader reader = new SAXReader();
Document doc = reader.read(new File("conf/web.xml"));
Element root = doc.getRootElement();
List<Element> mimeList = root.elements("mime-mapping");
for (Element mimeEle : mimeList) {
String key = mimeEle.elementText("extension");
String value = mimeEle.elementText("mime-type");
mimeTypeMapping.put(key, value);
}
} catch (DocumentException e) {
e.printStackTrace();
}
}
/**
* 根据资源后缀获取对应的mime类型
* @param ext
* @return
*/
public static String getMimeType(String ext) {
return mimeTypeMapping.get(ext);
}
/**
* 根据给定的状态代码获取其对应的状态描述
* @param statusCode
* @return
*/
public static String getStatusReason(int statusCode) {
return statusCodeReasonMapping.get(statusCode);
}
public static void main(String[] args) {
String fileName = "header.css";
//根据文件名的后缀,获取对应的mime类型
String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
System.out.println(getMimeType(ext));
String type = getMimeType("png");
System.out.println(type);
String reason = getStatusReason(404);
System.out.println(reason);
}
}
测试结果:
text/css
image/png
Not Found
3.2 修改 HttpResponse 类
在HttpResponse
中添加两个属性:int statusCode
、String statusReason
,默认值分别为200
、"OK"
,当然,我们还需要给这两个属性生成set
和get
方法:
/*
* 状态行相关信息
*/
private int statusCode = 200; //状态代码
private String statusReason = "OK"; //状态描述
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;
}
修改发送状态行的方法:sendStatusLine
,将原有的固定发送改为根据这两个属性发送:
/**
* 发送状态行
*/
public void sendStatusLine() {
String line = "HTTP/1.1" + " " + statusCode + " " + statusReason;
println(line);
}
修改HttpResponse
设置状态代码的方法,添加额外逻辑,根据设置的状态代码从HttpContext
中获取对应的状态描述,并自动设置到对应属性上:
/**
* 设置状态代码,设置的同时会自动设置该状态代码默认对应的状态描述
* @param statusCode
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
this.statusReason = HttpContext.getStatusReason(statusCode);
}
HttpResponse 最终代码:
package com.webserver.http;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* 响应对象
* @author returnzc
*
*/
public class HttpResponse {
//和连接相关的信息
private Socket socket;
private OutputStream out;
//响应的实体文件
private File entity;
/*
* 状态行相关信息
*/
private int statusCode = 200; //状态代码
private String statusReason = "OK"; //状态描述
/*
* 响应头相关信息
*/
private Map<String, String> headers = new HashMap<String, String>();
public HttpResponse(Socket socket) {
try {
this.socket = socket;
this.out = socket.getOutputStream();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 将当前响应对象内容按照HTTP响应格式发送给客户端
*/
public void flush() {
/*
* 1. 发送状态行
* 2. 发送响应头
* 3. 发送响应正文
*/
sendStatusLine();
sendHeaders();
sendContent();
}
/**
* 发送状态行
*/
public void sendStatusLine() {
String line = "HTTP/1.1" + " " + statusCode + " " + statusReason;
println(line);
}
/**
* 发送响应头
*/
public void sendHeaders() {
//遍历headers,将每个响应头发送给客户端
Set<Entry<String, String>> entrySet = headers.entrySet();
for (Entry<String, String> header : entrySet) {
String line = header.getKey() + ": " + header.getValue();
println(line);
}
//单独发送CRLF表示响应头发送完毕
println("");
}
/**
* 发送响应正文
*/
public void sendContent() {
try (
FileInputStream fis = new FileInputStream(entity);
) {
int len = -1;
byte[] data = new byte[1024*1024];
while ((len = fis.read(data)) != -1) {
out.write(data, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 向客户端发送一行字符串(发送后会自动发送CRLF结尾)
* @param line
*/
private void println(String line) {
try {
out.write(line.getBytes("ISO8859-1"));
out.write(13); //CR
out.write(10); //LF
} catch (IOException e) {
e.printStackTrace();
}
}
public File getEntity() {
return entity;
}
/**
* 设置响应正文的实体文件
* 在设置该文件的同时,自动设置对应该正文内容的两个响应头:
* Content-Type、Content-Length
* @param entity
*/
public void setEntity(File entity) {
this.entity = entity;
putHeaderByEntity();
}
/**
* 根据响应正文的实体文件设置对应的响应头:
* Content-Type和Content-Length
*/
private void putHeaderByEntity() {
String fileName = entity.getName();
//根据文件名的后缀,获取对应的mime类型
String ext = fileName.substring(fileName.lastIndexOf(".") + 1);
String type = HttpContext.getMimeType(ext);
headers.put("Content-Type", type);
headers.put("Content-Length", entity.length() + "");
}
public int getStatusCode() {
return statusCode;
}
/**
* 设置状态代码,设置的同时会自动设置该状态代码默认对应的状态描述
* @param statusCode
*/
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
this.statusReason = HttpContext.getStatusReason(statusCode);
}
public String getStatusReason() {
return statusReason;
}
public void setStatusReason(String statusReason) {
this.statusReason = statusReason;
}
}
3.3 添加并设置 404 页面
在webapps
下新建一个目录root
,用这个目录存放公共资源
:
在root
下新建404.html
:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>404</title>
</head>
<body>
<h1 align="center">404 资源未找到</h1>
</body>
</html>
页面如下:
修改ClientHandler
,当用户请求的资源不存在时,我们在分支中对response
进行设置,设置response
的状态代码为404
,设置response
响应实体为404页面
:
response.setStatusCode(404);
response.setEntity(new File("webapps/root/404.html"));
然后响应客户端:
response.flush();
ClientHandler 最终代码:
package com.webserver.core;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import com.webserver.http.EmptyRequestException;
import com.webserver.http.HttpRequest;
import com.webserver.http.HttpResponse;
/**
* 处理客户端请求
* @author returnzc
*
*/
public class ClientHandler implements Runnable {
private Socket socket;
public ClientHandler(Socket socket) {
this.socket = socket;
}
public void run() {
try {
//解析请求
HttpRequest request = new HttpRequest(socket);
//创建响应
HttpResponse response = new HttpResponse(socket);
//处理请求
String url = request.getUrl();
File file = new File("webapps" + url);
if (file.exists()) {
System.out.println("该资源已找到!");
//将该资源响应给客户端
response.setEntity(file);
} else {
System.out.println("该资源不存在!");
response.setStatusCode(404);
response.setEntity(new File("webapps/root/404.html"));
}
//响应客户端
response.flush();
} catch (EmptyRequestException e) {
System.out.println("空请求");
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
//响应完毕后与客户端断开连接
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
让我们打开服务端测试一下。我们输入两个地址:
- http://localhost:9999/myweb/index.html
- http://localhost:9999/myweb/haha.html
结果证明,存在的页面index.html
能够正常显示,而不存在的页面haha.html
会显示404
:
index.html
haha.html
大概总结一下,目前是这样子滴:
参考
《Java核心技术》(原书第10版)