文章目录
一个简单的HttpServer实现
通常我们写后台,浏览器请求后台接口,后台服务响应请求。对于其中的响应部分,我们只需要负责响应逻辑,即业务逻辑。、
而对于浏览器的请求是如何正确的访问到对应的后台接口,这一部分,很少有涉猎。
How Tomcat works
这本书向我揭秘了其中的神奇之处。
中文译本为《深入剖析Tomcat》
演示
访问存在的静态文件
首先在WEB_ROOT
目录下创建一个Hello.html
,内容如下图所以,必须加上响应信息。否则浏览器无法解析信息。
Postman
请求服务
服务打印请求信息
访问不存在的资源
这里有个注意点:使用最新版本的postman
测试。每次都是访问一次后台。是正常的。
如果使用浏览器测试,我使用google
测试,每次访问结束,关闭Socket
以后,会再次访问后台,也就是后台的控制台打印信息,不是停留在等待连接
,而是停留在第二次的连接中的处理请求
处,阻塞在输入流的read
方法中。
虽然设置了Connection: close
,还是会这样。原因不明。所以,我改用了Postman
测试。
Socket
响应浏览器请求的所有的神奇之处,都源自与Socket
。
学Java
基础的时候,学到后面,会接触到一点点的网络编程,会学到Socket
,然后会利用Socket
实现一个简单的聊天程序。大家应该都是这么过来的,我第一次接触Socket
就是在这个时候。
浏览器请求后台,后台响应浏览器的请求。把这个模型简化,就是建立一个连接,传输数据。而这个连接就是Socket
连接。
所以,我们想搭建一个简单的httpServer
服务器,首先需要在双方之间建立一个连接。浏览器端不需要我们处理,浏览器会自行的创建Socket
与我们的后台连接。
我们需要做的部分,仅仅只有一个后台的Socket
连接。
ServerSocket
java
中Socket
分两种,客户端的Socket
就是Socket
,服务端的Socket
则是ServerSocket
。hhtpServer
需要的Socket
是ServerSocket
.
MyHttpServer
package com.chapter_1;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 一个简易的http服务器
*/
public class MyHttpServer {
// 关机指令
// 浏览器访问该地址,即可关闭服务器
private static final String SHUTDOWN_COMMAND = "/ShutdownByYiazzz";
private boolean shutdown = false;
// WEB_ROOT 根目录
// System.getProperty("user.dir") --- 获取JVM属性
// user.dir --- 当前工作目录,即当前项目的根目录
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "WEB_ROOT";
public static void main(String[] args) {
System.out.println(" ---- 服务器启动 ---");
MyHttpServer myHttpServer = new MyHttpServer();
myHttpServer.await();
System.out.println(" ---- 服务器关闭 ---");
}
// 处理 连接
// 逻辑:
// 1.建立serverSocket
// 2.循环等待连接
public void await() {
ServerSocket serverSocket = null;
int port = 8080;
// 第二个参数,代表等待队列长度
// 服务器同时接收到许多客户端的连接请求,会将请求放在队列中,这个参数就是设置队列的长度
// 队列,满了以后,将不再接收新的连接。accept 方法,会从队列中一个一个的移除已经连接的请求。
// 操作系统的默认值是50。这里设置的参数,将覆盖操作系统的默认值。
// 但是,当设置的参数超过操作系统的最大值或者没设置,再或者小于等于0,则还是用操作系统的默认值
try {
serverSocket = new ServerSocket(port, 10, InetAddress.getByName("127.0.0.1"));
} catch (IOException e) {
e.printStackTrace();
}
// 循环等待连接
while (!shutdown) {
try {
Socket socket = null;
InputStream input = null;
OutputStream output = null;
// 阻塞方法,等待连接成功,才会返回
System.out.println("等待连接");
socket = serverSocket.accept();
System.out.println("--- 建立连接 ---"+socket.toString());
input = socket.getInputStream();
output = socket.getOutputStream();
System.out.println("处理请求");
// 封装 request,response 对象
// 实际上,就是为了处理请求,因为对请求的处理逻辑,在它们两个内部
MyRequest myRequest = new MyRequest(input);
// 处理请求数据
myRequest.parse();
MyResponse myResponse = new MyResponse(output, myRequest);
// 响应请求
myResponse.sendStaticResource();
// 关闭 socket
// 通过此socket创建的 input output 流都会自动被关闭
// 关闭之前,会强制刷新下缓冲区,所以信息会被写到浏览器。
System.out.println("----关闭连接----"+socket.toString()+"\n");
socket.close();
// 判断是否停机
shutdown = myRequest.getUri().equals(SHUTDOWN_COMMAND);
} catch (Exception e) {
e.printStackTrace();
System.out.println("---");
continue;
}
}
}
}
关机指令
代码中,我们可以看到,有个关机指令。访问该地址,就会关闭服务器,其实就是设置ShutDown
的值,如果是我们设置的指令,myRequest.getUri().equals(SHUTDOWN_COMMAND);
判断就会为真,就会跳出while
循环,程序就会结束,所以服务器就会终止。
WEB_ROOT 根目录
如果是直接使用SpringBoot
,打成jar
包,内嵌Tomcat
,这种情况的使用者,可能会对这个WEB_ROOT
根目录的概念比较陌生。
这个目录下面是放静态文件。是对浏览器暴露的,浏览器可以访问该目录下的文件。后面的代码,会将这个目录的作用展示出来,目前先不谈。
我们需要在代码结构目录中,创建一个WEB_ROOT
根目录。静态文件放在这个目录下面,其中WEB_ROOT
目录,放在哪里,需要配合代码中的路径:
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "WEB_ROOT";
代码中写明了,WEB_ROOT
的上级目录为System.getProperty("user.dir")
的值,即项目的根路径,所以WEB_ROOT
放在项目根目录下。
await 方法
方法内部创建了一个ServerSocket
对象,指定监听的port
和IP
。然后就是while
循环,循环的处理。浏览器的连接请求。
serverSocket.accept()方法
这个方法是一个阻塞方法,具体的细节,在注释中已经写好了。它会依次拿取队列中的Socket
对象。每一个Socket
对象,代表着一个连接。
流
因为每一个Socket
代表一个已经建立好的连接。所以可以从这个连接中,获取输入流和输出流。
从输入流中,获取浏览器发来的请求信息,包括请求头,请求行,请求体。
在输出流中,向浏览器回写响应信息,同样也要包括,响应头,响应行,响应体。干巴巴的直接回写信息,浏览器是无法正确解析信息的。
HTTP/1.1 200 yiazzz
Content-type: text/html; charset=utf-8
<html>
<head>
<title>HTTP Response Example</title>
</head>
<body>
Welcome to Http Server Demo
</body>
</html>
前面写好响应头,然后空一行,写上响应体。这里熟悉http
的同学应该很熟悉。不熟悉的去补下相关的知识,具体补 http协议的格式 。
这点和我们直接使用容器,不一样,因为Tomcat
容器中,帮我们写好了响应头,响应行,响应体。而我们自己搭建HttpServer
服务器,是需要我们自己写的。
MyRequest
在MyRequest
中处理请求。
package com.chapter_1;
import java.io.IOException;
import java.io.InputStream;
public class MyRequest {
// 接收连接进来的socket的inputStream
private InputStream input;
// 保存 URI,注意区别URL URI
private String uri;
public MyRequest(InputStream input) {
this.input = input;
}
/**
* 解析 请求,将请求信息从 input 输入流中拿出卡,转成字符串
* 获取请求行。
*/
public void parse() {
// 读取整个请求数据
// 缓冲区大小为 2k ,意味着,我们实现的request,请求数据 最长是2K数据
StringBuffer request = new StringBuffer(2048);
int length = -1;
byte[] buffer = new byte[2048];
try {
length = input.read(buffer);
System.out.println("完成读取请求");
for (int i = 0; i < length; i++) {
// 将请求信息,转成字符串
request.append((char) buffer[i]);
}
System.out.println("请求信息如下:");
System.out.println(request.toString());
this.uri = parseUri(request.toString());
System.out.println(this.uri);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 从整个请求信息中解析出 URI
*
* @param requestString 请求行
* @return
*/
private String parseUri(String requestString) {
int firstBlank = requestString.indexOf(' ');
if (firstBlank < 0) {
return null;
}
int secondBlank = requestString.indexOf(' ', firstBlank + 1);
// 如果 secondBlank <= firstBlank,说明 secondBlank 为 -1
if (secondBlank <= firstBlank) {
return null;
}
return requestString.substring(firstBlank+1, secondBlank);
}
public String getUri() {
return this.uri;
}
}
可以看到,是我们人为的创建了一个Request
对象,然后在内部处理了请求信息。
也和使用容器不一样,使用容器是我们人为的获取request
对象。
目前,我们自己实现的request
对象,功能很弱,只是提取了请求的信息,从请求中,截取出访问的URI
。
MyResponse
在MyResponse
中响应请求。
package com.chapter_1;
import java.io.*;
public class MyResponse {
private static final String FILE_NOT_FOUND = "404.html";
// 用于返回数据
private OutputStream output;
// 用于获取 请求的URI
private MyRequest myRequest;
public MyResponse(OutputStream output, MyRequest myRequest) {
this.output = output;
this.myRequest = myRequest;
}
/**
* 回写 静态资源
* <p>
* 逻辑:
* 读取资源,返回
*/
public void sendStaticResource() throws IOException {
byte[] buffer = new byte[2048];
FileInputStream fileInputStream = null;
// 创建文件对象
File file = new File(MyHttpServer.WEB_ROOT, myRequest.getUri());
try {
// 判断文件是否存在
if (file.exists()) {
fileInputStream = new FileInputStream(file);
int length = fileInputStream.read(buffer, 0, 2048);
while (length != -1) {
output.write(buffer,0,length);
length = fileInputStream.read(buffer, 0, 2048);
}
} else {
// 返回 404.html
// file = new File(MyHttpServer.WEB_ROOT, FILE_NOT_FOUND);
//
// fileInputStream = new FileInputStream(file);
// int length = fileInputStream.read(buffer, 0, 2048);
//
// while (length != -1) {
// output.write(buffer,0,length);
// length = fileInputStream.read(buffer, 0, 2048);
// }
// 这里 404 直接硬编码了 响应头 和 响应行
String errorMessage = "HTTP/1.1 404 File Not Found\r\n" +"Content-type: text/html\r\n" +"Content-Length: 23\r\n"+"\r\n"+"<h1>File not found</h1>" ;
output.write(errorMessage.getBytes());
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// output 和 input 都是外部传进来的,不需要内部关闭
// 在 MyHttpServlet 中 关闭。
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
}
代码中展示了,前面提到的WEB_ROOT
目录的作用了。请求的URI
,配上WEB_ROOT
形成一个完整的文件路径,然后读取该文件,回写到浏览器。
如果该文件不存在,则返回404
。
总结
只需要这三个类,即可搭建一个简单的HttpServer
服务器。
MyHttpServer
,启动一个循环,监听具体的端口和地址。
MyRequest
,处理请求,主要是读取输入流中的请求信息。
MyResponse
,响应请求,主要是配合WEB_ROOT
,读取对应的静态文件,回写到浏览器。
目前,该服务器,仅仅可以返回静态资源,后面会对其进行增强。