25. 应用层HTTP原理(3) —— HTTP Server

2022-10-26 15:49:20 浏览数 (1)

根据本专题的上一篇文章所说提到的HTTP响应和HTTP请求的格式(HTTP请求和响应格式文章链接)我们可以书写简单的HTTP Server程序,让服务器上的返回给客户端的返回结果返回至网站中

简单版本

代码如下:

代码语言:javascript复制
package day0314;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * HTTP服务器(最简单的)
 *
 * 底层要基于TCP来实现,要按照TCP的基本格式来先进行开发
 */
public class HTTPServerV1 {
    private ServerSocket serverSocket = null;

    public HTTPServerV1(int port) throws IOException {
       serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService executorService = Executors.newCachedThreadPool();

        while (true){
            //1.获取连接
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    //2.处理连接(采用短连接)
                    process(clientSocket);
                }
            });
        }
    }

    private void process(Socket clientSocket)  {
        try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
            //下面的操作要严格按照HTTP协议来进行操作
            //1.读取请求并解析
            //  a)解析首行  三部分使用空格切分
            String firstLine = bufferedReader.readLine();
            String[] firstLineTokens = firstLine.split(" ");//切分
            String method = firstLineTokens[0];
            String url = firstLineTokens[1];
            String version = firstLineTokens[2];
            //  b)解析header  按行读取,然后按照冒号空格来分割字符
            Map<String,String> headers = new HashMap<>();
            String line = "";
            //readLine()读到的一行内容,时会自动去掉换行符的
            while ((line = bufferedReader.readLine()) != null && line.length() !=0){
                //还没读完并且读到的不是空字符串
                String[] headerTokens = line.split(": ");//用冒号空格来切分
                headers.put(headerTokens[0],headerTokens[1]);
            }
            //  c)解析body(暂时先不考虑)
            //请求解析完毕,加上一个日志看看请求的内容是否正确
            System.out.printf("%s %s %sn",method,url,version);
            for (Map.Entry<String,String> entry : headers.entrySet()){
                System.out.print(entry.getKey()   ": "   entry.getValue() "n");
            }
            System.out.println();

            //2.根据请求计算响应
            //假设不管是啥样的请求,都返回一个hello这样的html
            String resp = "";
            if(url.equals("/ok")){
                bufferedWriter.write(version " 200 OKn");
                resp = "<h1>hello</h1>";
            }else if (url.equals("/notfound")){
                bufferedWriter.write(version " 404 Not Foundn");
                resp = "<h1>not found</h1>";
            }else if (url.equals("/seeother")){
                bufferedWriter.write(version " 303 See Othern");
                bufferedWriter.write("Location: http://www.baidu.comn");
            }else {
                bufferedWriter.write(version " 200 OKn");
                resp = "<h1>else</h1>";
            }

            //3.把响应写回客户端
            bufferedWriter.write("Content-Type: text/htmln");
            bufferedWriter.write("Content-Length: " resp.getBytes().length "n");//此处的长度这样写是得到的字节的数目
            //resp.length是字符数目
            bufferedWriter.write("n");
            bufferedWriter.write(resp);
            bufferedWriter.flush();//刷新

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                clientSocket.close();//短连接,一次交互之后服务器客户端主动断开连接
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        HTTPServerV1 serverV1 = new HTTPServerV1(9090);
        serverV1.start();
    }
}

运行结果:

浏览器(客户端)中返回结果是:

服务器显示结果是:

抓包结果为:

完成再浏览器中返回a b的值

方法一:

代码语言:javascript复制
package day0314rev;


import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class HttpServerV1 {
    private ServerSocket serverSocket = null;

    public HttpServerV1(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        while (true){
            Socket clientSocket = serverSocket.accept();
            ExecutorService executorService = Executors.newCachedThreadPool();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }

    private void process(Socket clientSocket) {
        try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))){
            //1.解析获得的请求
            //解析首行
            String firstLine = bufferedReader.readLine();
            String[] firstLineToken = firstLine.split(" ");
            String method = firstLineToken[0];
            String url = firstLineToken[1];
            String version = firstLineToken[2];

            Map<String,String> parmenters = new HashMap<>();
            int post = url.indexOf("?");
            if (post != -1){
                //如果url中有?,那么就执行里面的操作
                String parmenter = url.substring(post 1);
                KVStart(parmenter,parmenters);
            }

            //解析header
            Map<String,String> headers = new HashMap<>();
            String line = "";
            while ((line = bufferedReader.readLine()) != null && line.length() != 0){
                String[] headerToken = line.split(": ");
                headers.put(headerToken[0],headerToken[1]);
            }

            System.out.printf("%s %s %sn", method,url,version);
            for (Map.Entry<String,String> entry : headers.entrySet()){
                System.out.print(entry.getKey() ": " entry.getValue() "n");
            }
            System.out.println();

            //2.计算请求
            String resp = "";
            if (url.equals("/hello")){
                bufferedWriter.write(version " 200 OKn");
                resp = "<h1>hello</h1>";
            }else if (url.startsWith("/clar")){
                //计算a b的值
                bufferedWriter.write(version " 200 OKn");
                String aStr = parmenters.get("a");
                String bStr = parmenters.get("b");
                int a = Integer.parseInt(aStr);
                int b = Integer.parseInt(bStr);
                int sum = a b;
                resp = "<h1>" sum "</h1>";

            }else  {
                bufferedWriter.write(version   " 200 OKn");
                resp = "<h1>defualt</h1>";
            }
            //3.把请求返回给客户端

            bufferedWriter.write("Content-Type: text.htmln");
            bufferedWriter.write("Content-Length: " resp.getBytes().length "n");
            bufferedWriter.write("n");
            bufferedWriter.write(resp);
            bufferedWriter.flush();

        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void KVStart(String parmenter, Map<String, String> parmenters) {
        String[] KVToken = parmenter.split("&");
        for (String KV : KVToken){
            String[] result = KV.split("=");
            parmenters.put(result[0],result[1]);
        }
    }

    public static void main(String[] args) throws IOException {
        HttpServerV1 server = new HttpServerV1(9090);
        server.start();
    }

}

但以上代码在逻辑方面不太清晰,下面用三各类进行整理

第一个类:HttpRequest

代码语言:javascript复制
package day0314;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;

/**
 * 请求:表示一个http请求,并负责解析
 */
public class HttpRequest {
    private String method;
    private String url;
    private String version;
    private Map<String,String> headers = new HashMap<>();
    private Map<String,String> parameters = new HashMap<>();

    //请求的构造逻辑,也是用工厂模式来构造
    //此处的参数就是从socket中获取到的InputStream对象
    //就相当于反序列化
    public static HttpRequest build(InputStream inputStream) throws IOException {
        HttpRequest request = new HttpRequest();
        //此处不能把bufferedReader写道try括号中,一旦写进去就意味着bufferedReader就会被关闭,会影响到clientSocket的状态
        //等到最后正给请求处理完了再统一关闭
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        //此处的build过程就是解析请求的过程
        //1.解析首行
        String firstLine = bufferedReader.readLine();
        String[] firstLineTokens = firstLine.split(" ");
        request.method = firstLineTokens[0];
        request.url = firstLineTokens[1];
        request.version = firstLineTokens[2];

        //2.解析url中的参数
        int pos = request.url.indexOf("?");
        if (pos != -1){
            //pos表示问号的下标
            //看看有没有带问号,如果没有带问号就不用解析了
            //index.html?a=10&b=20
            String parameters = request.url.substring(pos 1);//从i 1开始取
            //切分的最终结果
            parseKV(parameters,request.parameters);
        }

        //3.解析header
        String line = "";
        //readLine()读到的一行内容,时会自动去掉换行符的
        while ((line = bufferedReader.readLine()) != null && line.length() !=0){
            //还没读完并且读到的不是空字符串
            String[] headerTokens = line.split(": ");//用冒号空格来切分
            request.headers.put(headerTokens[0],headerTokens[1]);
        }
        //4.解析body

        return request;
    }

    private static void parseKV(String input, Map<String, String> output) {
        //1.先按照&分成若干组键值对
        String[] kvTokens = input.split("&");
        //2.针对切分结果再分别进行按照=切分,得到了键和值
        for (String kv : kvTokens){
            String[] result = kv.split("=");
            output.put(result[0],result[1]);
        }
    }

    //给这个类构造getter方法,不要搞setter方法,因为请求对象内容应该是从网络上解析来的,用户不应该修改

    public String getMethod() {
        return method;
    }

    public String getUrl() {
        return url;
    }

    public String getVersion() {
        return version;
    }

    public String getHeaders(String key) {
        return headers.get(key);
    }

    public String getParameters(String key) {
        return parameters.get(key);
    }

    @Override
    public String toString() {
        return "HttpRequest{"  
                "method='"   method   '''  
                ", url='"   url   '''  
                ", version='"   version   '''  
                ", headers="   headers  
                ", parameters="   parameters  
                '}';
    }
}

第二个类:HttpResponse

代码语言:javascript复制
package day0314;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * 响应:表示一个http响应,负责构造
 */
public class HttpResponse {
    private String version = "HTTP/1.1";
    private int status; //状态码
    private String message; //状态码描述信息
    private Map<String,String> headers = new HashMap<>();
    private StringBuilder body = new StringBuilder();
    //当代码需要把响应写回客户端的时候,就往这个OutputStream里面写就好了
    private OutputStream outputStream = null;

    //工厂方法
    public static HttpResponse build(OutputStream outputStream){
        HttpResponse response = new HttpResponse();
        response.outputStream = outputStream;
        return response;
    }

    public void setVersion(String version) {
        this.version = version;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void setHeaders(String key, String value) {
        headers.put(key,value);
    }

    public void writeBody(String content) {
        body.append(content);
    }

   //以上的设置属性操作都是在内存中搞得,还需要一个专门的方法,把这些属性按照HTTP协议,都写到Socket中
    public void flush() throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(version " " status  " " message "n");
        headers.put("Content-Length",body.toString().getBytes().length "");
        for (Map.Entry<String,String> entry : headers.entrySet()){
            bufferedWriter.write(entry.getKey() ": " entry.getValue() "n");
        }
        bufferedWriter.write("n");
        bufferedWriter.write(body.toString());
        bufferedWriter.flush();
    }
}

第三个类:整理

代码语言:javascript复制
package day0314;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 主逻辑:HttpRequest和 HttpResponse已经梳理的逻辑,在这里完成调用
 */
public class HttpServerV2 {
    private ServerSocket serverSocket = null;

    public HttpServerV2(int port) throws IOException {
        serverSocket = new ServerSocket(port);
    }

    public void start() throws IOException {
        System.out.println("服务器启动");
        ExecutorService executorService = Executors.newCachedThreadPool();
        while (true){
            Socket clientSocket = serverSocket.accept();
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    process(clientSocket);
                }
            });
        }
    }

    public void process(Socket clientSocket){
        try {
            //1.读取并解析请求
            HttpRequest request = HttpRequest.build(clientSocket.getInputStream());
            System.out.println("request:" request);
            HttpResponse response = HttpResponse.build(clientSocket.getOutputStream());
            response.setHeaders("Content-Type","text.html");

            //2.根据请求计算响应
            if (request.getUrl().startsWith("/hello")){
                response.setStatus(200);
                response.setMessage("OK");
                response.writeBody("<h1>hello</h1>");
            }else if (request.getUrl().startsWith("/calc")){
                //根据这个逻辑进行计算
                //先获取到a b两个值
                String aStr = request.getParameters("a");
                String bStr = request.getParameters("b");
                int a = Integer.parseInt(aStr);
                int b = Integer.parseInt(bStr);
                int result = a b;
                response.setStatus(200);
                response.setMessage("OK");
                response.writeBody("<h1> result = " result "</h1>");
            } else if (request.getUrl().startsWith("/cookie")){
                response.setStatus(200);
                response.setMessage("OK");
                response.setHeaders("Set-Cookie","I am cookie");
                response.writeBody("<h1>set cookie</h1>");
            }else{
                response.setStatus(200);
                response.setMessage("OK");
                response.writeBody("<h1>default</h1>");
            }

            //3.吧响应写回客户端
            response.flush();

        } catch (IOException | NullPointerException e) {
            e.printStackTrace();
        }finally {
            try {
                //这个操作可以同时关闭getInputStream和getOutputStream
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws IOException {
        HttpServerV2 serverV2 = new HttpServerV2(9090);
        serverV2.start();

    }
}

这样也可以完成上述任务,完成结果如下:

0 人点赞