编写静态-动态http服务器
用例图:
实现需求:
实现静态-动态服务器的目的是为了更深的理解HTTP协议和Servlet 底层原生的原理。实现描述:
用户通过浏览器提交一个http 请求到指定的服务器的启动端口,在HttpServer 中,Java通过Socket 封装好得到http的请求流和响应流,通过HttpPrase方法将Http请求流封装成HttpRequest 对象,然后读取指定目录中的html,css,js 文件,转换为文件流,通过响应流封装好的HttpResponse 对象将数据返回给浏览器,实现一个类似nginx 的静态服务器。
通过修改实现类,实现可以配置映射路径的动态服务器,类似于:
- 访问/ 即可访问/index.html
项目目录:
- -java
- -ServerApplication
- -handler
- -impl
- -DynamicHandler
- -StaticHandler
- -Handler
- -HttpPrase
- -http
- -server
- -HttpServer
- -HttpWorkerThread
1、编写HttpServer类
作用:
- -绑定端口号
- -创建Socket 对象,接收请求响应流
- -创建新线程,专门用于处理请求和响应
/**
* @author HaiPeng Wang
* @date 2021/8/5 11:22
* @Description:
*/
public class HttpServer {
/*端口号*/
private int port;
public HttpServer(int port) {
this.port = port;
}
public void start(){
try {
ServerSocket serverSocket = new ServerSocket();
/*绑定端口号*/
serverSocket.bind(new InetSocketAddress(port));
System.out.println("Server start in "+ port);
/*通过死循环不断的接收浏览器发来的请求,并专门给他创建一个线程去处理他的请求和响应*/
while (true){
/*建立新连接*/
Socket cilentSocket = serverSocket.accept();
cilentSocket.setSoTimeout(1000);
System.out.println("A new connection : "+cilentSocket.getInetAddress());
/*为每一个连接创建一个线程*/
new HttpWorkerThread(cilentSocket).start();
}
}catch (IOException e) {
e.printStackTrace();
}
}
}
2、编写HttpWorkerThread类
作用:
- -调用HttpPrase.prase()
- -创建HttpResponse 对象
- -调用Handler.handle 方法处理请求和响应
/**
* @author HaiPeng Wang
* @date 2021/8/5 11:26
* @Description:
*/
public class HttpWorkerThread extends Thread{
public Socket cilentSocket;
public HttpWorkerThread(Socket cilentSocket) {
this.cilentSocket = cilentSocket;
}
@Override
public void run() {
/*根据Http协议解析Http请求*/
try{
System.out.println("线程:" + this.getId());
/*通过Socket 获取InputStream创建转换器对象*/
HttpParse httpParse = new HttpParse(cilentSocket.getInputStream());
/*调用转换器方法,将InputStream 中的请求报文信息封装成HttpRequest*/
HttpRequest httpRequest = httpParse.prase();
/*通过Socket 获取OutputStream 封装成响应对象*/
HttpResponse httpResponse = new HttpResponse(cilentSocket.getOutputStream());
/*创建处理器,类似于Servlet 中的Service 方法*/
Handler handler = new StaticHandler();
// Handler dynamicHandler = new DynamicHandler();
handler.handle(httpRequest,httpResponse);
}catch (IOException e) {
e.printStackTrace();
}
}
}
3、编写HttpRequest类
作用:
- -存储InputStream 中的信息
/**
* @author HaiPeng Wang
* @date 2021/8/5 11:34
* @Description:
*/
public class HttpRequest {
/*Http 输入流*/
private InputStream inputStream;
/*Http 请求方法*/
private String method;
/*Http 请求行*/
private String line;
/*Http 请求头,用HashMap 存储*/
private Map<String,String> headers = new HashMap<>();
/*Http 请求体*/
private String body;
/*Http 请求路径 比如说:localhost:1024/test/url 则url = “/test/url”*/
private String url;
public HttpRequest(InputStream inputStream) {
this.inputStream = inputStream;
}
public InputStream getInputStream() {
return inputStream;
}
public String getMethod() {
return method;
}
public String getLine() {
return line;
}
public Map<String, String> getHeaders() {
return headers;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getHeader(String name){
return headers.get(name);
}
public void addHeader(String name,String value){
headers.put(name, value);
}
public void setLine(String result) {
this.line = result;
}
public void setMethod(String method) {
this.method = method;
}
public void setUrl(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
}
4、编写HttpPrase类
作用:
- -封装InputStream 返回HttpRequest 对象
- -封装请求头
- -封装请求行
- -封装请求体
Http 协议:
- 是一种超文本传输协议,基于TCP/IP协议来传输超文本到本地浏览器
- 是一种无连接协议:每次连接只处理一个请求,服务器处理完请求,处理后做出响应,浏览器接收到响应则端开连接,这样可以节省传输时间
- 是一种无状态协议:对事务处理没有记忆功能
Http请求报文:
public class HttpParse {
/*Http 输入流*/
private InputStream is;
/*持有HttpRequest 对象*/
private HttpRequest httpRequest;
/*将输入流根据/r/n 转换成字符串数组*/
private String[] requestStr;
/*必须通过输入流去创建*/
public HttpParse(InputStream is) {
this.is = is;
httpRequest = new HttpRequest(is);
}
/**
* 解析请求行
* @return 请求行字符串
*/
public HttpRequest prase() throws IOException {
HttpRequest httpRequest = parseToString()
.requestLine()
.requestHeader()
.requestBody();
return httpRequest;
}
/**
* 解析请求行
* @return
* @throws IOException
*/
public HttpParse requestLine() throws IOException {
String line = requestStr[0];
String[] str1 = line.split(" ");
httpRequest.setLine(line); //设置完整响应行
httpRequest.setMethod(str1[0]); //设置响应方法
httpRequest.setUrl(str1[1]); //设置url
return this;
}
/**
* 解析请求头
* @return 请求头Map
*/
public HttpParse requestHeader() throws IOException {
int i = 1;
while (i < requestStr.length && requestStr[i].equals("") ){
String[] strings = requestStr[i].split(":");
if (strings[0].toLowerCase().equals("host")){
strings[1] = strings[1] + ":" +strings[2];
}
httpRequest.addHeader(strings[0],strings[1]); //添加每一个响应头到请求头中
i++;
}
return this;
}
/**
* 解析请求体
* @return 请求体字符串
*/
public HttpRequest requestBody(){
int i = requestStr.length-1;
String body = requestStr[i];
httpRequest.setBody(body);
return this.httpRequest;
}
/**
* 读取请求数据流并根据/r/n转换成字符串数组
* @return
* @throws IOException
*/
public HttpParse parseToString() throws IOException {
this.httpRequest = new HttpRequest(is);
String result = new String();
StringBuffer stringBuffer = new StringBuffer();
try {
byte[] buf = new byte[128];
int size = 0;
while (( size = is.read(buf,0,buf.length)) != -1) {
for (int i = 0; i < buf.length ; i ++){
stringBuffer.append((char) buf[i]);
}
}
}catch (SocketTimeoutException e){
/**
* 如果浏览器没有接收到响应数据,那么是不会主动去端开连接的
* 这样就导致InputStream 是一个无限的流
* 但是这样在读取的时候就会造成read 方法的阻塞
* 这时利用tSocket.setSoTimeout(1000); 设置read 等待的时间,超出时间则扔出异常
* 这时就可以主动的结束read 阻塞跳出来然后进行继续的步骤
* 否则浏览器就会一直卡着转圈圈
*/
}
this.requestStr = stringBuffer.toString().split("\r\n");
return this;
}
}
5、编写HttpResponse类
/**
* @author HaiPeng Wang
* @date 2021/8/5 15:12
* @Description:
*/
public class HttpResponse {
/*枚举类 存储StatusCode 和 StatusDescription*/
private HttpStatus httpStatus;
/*响应头信息*/
private Map<String,String> headers = new HashMap<>();
/*ContentType 信息,用于存储文件后缀名和*/
private static Map<String,String> type = new HashMap<>();
static{
/**
* 初始化type对象,存储后缀名和contentType 的对应关系
*/
type.put("css","text/css;charset=utf-8");
type.put("js","text/js;charset=utf-8");
type.put("html","text/html;charset=utf-8");
type.put("json","text/json;charset=utf-8");
type.put("png","application/x-png");
}
{
/**
* 初始化headers 相当于设置默认值
*/
headers.put("Content-Type","text/html;charset=utf-8 ");
// headers.put("Content-Length","4096");
/**
* 报错net::ERR_CONTENT_LENGTH_MISMATCH
* 我以为设计成个死的长度就行,结果发现这样也不行
* 不行就不行吧,查看response.length 这个头字段的含义发现一般的浏览器会自动的去计算长度
* content length 的长度是消息实体的传输长度,一个是传输长度和实体长度是不同的
* 消息实体这里就表示响应体,单位是字节
* 那我这里就不去计算来让他自己去计算吧
*
* 如果conten length 的长度大于实际长度则会被阻塞然后报错
* 如果小于则会被截断
* 所以他必须是精确的,有的浏览器都有计算的功能,但是有的浏览器老一些的其实是没有计算功能的
*/
headers.put("Connection","close");
}
private OutputStream os;
public HttpResponse(OutputStream os) {
this.os = os;
}
public void setHttpStatus(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public OutputStream getOs() {
return os;
}
public void addHeader(String name,String value){
headers.put(name,value);
}
public String getHeader(String name){
return headers.get(name);
}
public void setContentType(String value){
headers.put("Content-Type",value);
}
public String getContentType(){
return this.headers.get("Content-Type");
}
public void setContentLength(Integer value){
headers.put("Content-Length",value.toString());
}
public String getContentLength(){
return this.headers.get("Content-Length");
}
public Map<String, String> getType() {
return type;
}
/**
* 根据HttpResponse 对象生成响应字符串
* @return
*/
public String loadResponse(){
String lankLine = "\r\n";
String blank = " ";
String result = "";
String colon = ":";
result += "HTTP/1.1" + blank + this.httpStatus.getCode() + blank + this.httpStatus.getMsg()+ lankLine;
Set<String> keySet = this.headers.keySet();
for (String key : keySet){
result += key + colon + this.headers.get(key) + lankLine ;
}
result += lankLine;
return result;
}
/**
* 将字符串写回前端
* @param str
* @throws IOException
*/
public void write(String str) throws IOException {
os.write(str.getBytes(StandardCharsets.UTF_8));
os.flush();
os.close();
}
/**
* 用字符串+字节流的形式写回前端
* @param str
* @param bytes
* @throws IOException
*/
public void write(String str,byte[] bytes) throws IOException {
os.write(str.getBytes(StandardCharsets.UTF_8));
os.write(bytes);
os.flush();
os.close();
}
/**
* 将对象转换成json 写回前端
* @param t
* @param <T>
* @throws IOException
*/
public<T> void writeBody(T t) throws IOException {
this.httpStatus = HttpStatus.SC_OK;
this.setContentType("application/json");
String result = loadResponse();
String json = "{lalalala}";
result += json;
write(result);
}
}
Http 响应报文和请求报文的本质就是约定好的一种形式的字符串
6、编写HttpStatus枚举类
作用:
- -表示响应码和描述
/**
* @author HaiPeng Wang
* @date 2021/8/5 15:13
* @Description:
*/
public enum HttpStatus {
NOT_FOUNT(404,"NotFound"),
SC_OK(200,"OK"),
BAD_REQUEST(200,"BadRequest"),
SERVER_ERROR(400,"ServerError");
private int code;
private String msg;
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
HttpStatus(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
7、编写Handler接口
作用:
- -这里使用了灵活的策略模式,只需要更改Handler 的实现类,即可实现静态和动态的转换
/**
* @author HaiPeng Wang
* @date 2021/8/5 15:10
* @Description:请求处理接口
*/
public interface Handler {
public void handle(HttpRequest request, HttpResponse httpResponse) throws IOException;
}
8、编写StaticHandler实现类
作用:
- -读取指定路径中的文件,并将文件传输给浏览器
- -根据不同的文件后缀名设置不同的content-type
- -编写error 响应
/**
* @author HaiPeng Wang
* @date 2021/8/5 15:11
* @Description:静态服务器实现类
*/
public class StaticHandler implements Handler {
/**
* 解析文件,将文件返回前端
* @param request
* @param response
*/
@Override
public void handle(HttpRequest request, HttpResponse response) throws IOException {
sendFile(request,response);
}
public void error(HttpRequest request,HttpResponse response) throws IOException {
response.setHttpStatus(HttpStatus.NOT_FOUNT);
String result = response.loadResponse();
response.write(result);
}
public void sendFile(HttpRequest request,HttpResponse response) throws IOException {
String staicPath = "W:\\桌面文件\\2021暑期云泽\\会议记录\\20210804-Java-Tomcat\\20210804-Java-Tomcat\\html";
/*替换Url中的分隔符*/
String url = request.getUrl().replace("/",File.separator);
String path = staicPath + url;
String[] strs = url.split("\\.");
String contentType = response.getType().get(strs[1]);
response.setContentType(contentType);
File file = new File(path);
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
}catch (Exception e){
error(request,response);
}
byte[] bytes = inputStream.readAllBytes();
response.setHttpStatus(HttpStatus.SC_OK);
response.write(response.loadResponse(),bytes);
}
}
9、编写DynamicHandler实现类
作用:
- -实现根据映射完成动态访问
- -小细节,每一个Server 的mapping映射信息都应该是由每一个server 对象所共享的,这样这些映射和server 对象有相同的生命周期
/**
* @author HaiPeng Wang
* @date 2021/8/8 19:50
* @Description:动态实现类
*/
public class DynamicHandler implements Handler {
private Map<String,String> maps = new HashMap<>();
{
maps.put("/cgxz","/cgxz/index.html");
maps.put("/css/base.css","/css/base.css");
maps.put("/css/header.css","/css/header.css");
maps.put("/css/footer.css","/css/footer.css");
maps.put("/css/index.css","/css/index.css");
maps.put("/images/banner.png","/images/banner.png");
maps.put("/images/img1.png","/images/img1.png");
maps.put("/js/index.js","/js/index.js");
}
@Override
public void handle(HttpRequest request, HttpResponse response) throws IOException {
String staicPath = "W:\\桌面文件\\2021暑期云泽\\会议记录\\20210804-Java-Tomcat\\20210804-Java-Tomcat\\html";
String url = null;
try {
url = maps.get(request.getUrl()).replace("/", File.separator);
}catch (NullPointerException e){
error(request,response);
}
if (url == "" || url == null){
error(request,response);
}
String path = staicPath + url;
String[] strs = url.split("\\.");
String contentType = response.getType().get(strs[1]);
response.setContentType(contentType);
File file = new File(path);
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream(file);
}catch (Exception e){
error(request,response);
}
byte[] bytes = inputStream.readAllBytes();
response.setHttpStatus(HttpStatus.SC_OK);
response.write(response.loadResponse(),bytes);
}
public void addMapping(String realPath,String mapping){
maps.put(mapping,realPath);
}
public void error(HttpRequest request,HttpResponse response) throws IOException {
response.setHttpStatus(HttpStatus.NOT_FOUNT);
String result = response.loadResponse();
response.write(result);
}
}
10、编写启动类
public class ServerApplication {
public static void main(String[] args) {
/*args 是命令行参数的字符串数组,这里可以根据命令行参数获取对应的配置*/
if (args.length == 0){
/*给出添加port 的形式*/
System.out.println("Usage:Java -jar static-server.jar <port>");
}
int port = Integer.parseInt(args[0]);
HttpServer server = new HttpServer(port);
server.start();
}
}
11、idea配置命令行参数的方法
1、
2、在1024处进行配置