不使用第三方工具, 纯java搭建web服务

作者: pq217 | 来源:发表于2022-05-05 09:25 被阅读0次

前言

不知道有没有人和我一样,开始学java就是springboot的天下了,springboot内嵌了tomcat,写一个controller加上RequestMapping 注解,启动项目就可以对前端提供服务了,而对底层网络编程题就像一个黑匣子触摸不到

今天就尝试再不使用spring,tomcat的情况下,纯java搭建一个web服务

socket

既然是处理前端请求,就绕不开网络编程socket,那就先使用ServerSocket创建一个基础的网络服务:绑定端口-接受连接-打印接受数据-返回success

public class SampleServer {
    public static void main(String[] args) throws IOException {
        // 开启一个socket服务,绑定端口号8888
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("===server start===");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            byte[] bytes = new byte[1024];
            System.out.println("准备read。。");
            //接收客户端的数据,阻塞方法,没有数据可读时就阻塞
            int read = clientSocket.getInputStream().read(bytes);
            System.out.println("read完毕。。");
            if (read != -1) {
                System.out.println("接收到客户端的数据:" + new String(bytes, 0, read));
            }
            System.out.println("返回success");
            clientSocket.getOutputStream().write("success".getBytes());
            clientSocket.getOutputStream().flush();
            clientSocket.close();
        }
    }
}

再写一个客户端发送“hello”到服务端

public class SocketClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost", 8888);
        byte[] bytes = new byte[1024];
        socket.getOutputStream().write("get".getBytes());
        socket.getOutputStream().flush();
        System.out.println("发送请求:get");
        int read = socket.getInputStream().read(bytes);
        System.out.println(new String(bytes, 0, read));
        socket.close();
    }
}

服务端打印结果

===server start===
准备read。。
read完毕。。
接收到客户端的数据:get
返回success

一个简单的网络服务就搭建好了

http

接下来我们使用浏览器发送一个请求过来测试一下:

chrome

浏览器直接报错,在看服务端输出

准备read。。
read完毕。。
接收到客户端的数据:
GET / HTTP/1.1
Host: localhost:8888
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9


返回success

没有任何异常,看一下收到的信息,好家伙,这么多!仔细一看,这不就是http协议的格式吗:也就是说我们只是输入一个网址,浏览器把网址按http协议包装成规定的格式发给了后端接口,回顾一下http协议的格式如下:

http协议格式

那服务端如果要获取用户请求的路径和请求方式(GET PUT等)等,首先就是按照http协议规定格式摘取出我们想要的信息,再回头看浏览器的错误:ERR_INVALID_HTTP_RESPONSE,即无效的http响应,也就是我们的返回数据没有遵循http协议

在使用springboot时我们完全不用考虑http协议,只是简单的接受数据返回数据即可,因为这些事已经被tomcat处理好了,所以说客户端浏览器负责封装和解析http协议格式数据,服务端tomcat负责封装和解析http协议格式数据,如下图

tomcat

tomcat

所以当前我们要自己实现一个tomcat来作为请求数据和业务代码的对接中间件,负责接收数据是解析http协议格式的请求数据,返回是封装符合http协议响应格式的返回数据

新建两个类HttpRequest和HttpResponse,前者负责按http协议规定读取请求信息,解析成对象,后者负责把返回的数据封装成http协议规定响应格式

HttpRequest:

public class HttpRequest {
    /**
     * 请求路径
     */
    private String pathInfo;

    /**
     * 请求方式
     */
    private String method;

    /**
     * 请求方式
     */
    private Map<String, String> header;

    public HttpRequest(Reader inReader) {
        try {
            BufferedReader reader = new BufferedReader(inReader);
            // 第一行:请求行
            String firstLine = reader.readLine();
            String[] firstLineItems = firstLine.split(" ");
            // 请求方式
            method = firstLineItems[0];
            // 请求路径
            pathInfo = firstLineItems[1];
            // 读取接下来的行:请求头
            String headerLine;
            header = new HashMap<>();
            while ((headerLine=reader.readLine())!=null) {
                if(headerLine.length()==0){
                    break;
                }
                String[] headerLineItems = headerLine.split(": ");
                header.put(headerLineItems[0], headerLineItems[1]);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("错误");
        }
    }

    public String getPathInfo() {
        return pathInfo;
    }

    public String getMethod() {
        return method;
    }

    public String getHeader(String name) {
        return header.get(name);
    }
}

HttpResponse:

public class HttpResponse {

    public HttpResponse(String data) {
        this.data = data;
    }

    private String data;

    /**
     * 返回数据
     * @param
     */
    public byte[] getBytes() {
        // 最终返回的数据: 响应行+响应头+空行+响应正文
        return ("HTTP/1.1 200\r\n" +
                "Content-Type: application/json\r\n" +
                "\r\n" + data).getBytes();
    }
}

有了这两个类,我们就可以很轻松实现一个web服务

public class WebServer {
    public static void main(String[] args) throws IOException {
        // 开启一个socket服务,绑定端口号8888
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("===server start===");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            try {
                // 解析请求信息为HttpRequest对象
                HttpRequest request = new HttpRequest(new InputStreamReader(clientSocket.getInputStream(), "utf-8"));
                // 返回数据
                String data = "{\n" +
                        "  \"code\": 200,\n" +
                        "  \"message\": \"success\"\n" +
                        "  \"path\": \""+request.getPathInfo()+"\"\n" +
                        "}";
                // 响应
                HttpResponse response = new HttpResponse(data);
                // 返回数据
                try {
                    clientSocket.getOutputStream().write(response.getBytes());
                    clientSocket.getOutputStream().flush();
                } finally {
                    clientSocket.getOutputStream().close();
                }
            } finally {
                clientSocket.close();
            }
        }
    }
}

此时再用浏览器请求

chrome

这样我们就做好一个简单的web服务

servlet

web服务虽然搭建好了,但却是只能提供一个服务:接受请求把请求的path返回

而这显然不是我们要的结果,我们希望比如/user就进入UserController(用户服务),/order就进入OrderController(订单服务)这样的结果

为了实现这个样的功能, 我们给所有服务做一个抽象,简单点就一个方法:service代表服务开始执行入口,把之前封装的HttpRequest作为参数传入进去(HttpRequest也该传,比如做文件下载导出流的功能,这里简化就不传了)

public interface Servlet {
    String service(HttpRequest request);
}

然后做一个path到服务的映射map,放到WebServer中,构造时传入,然后接受请求时按map执行不同Servlet的service方法,修改后的WebServer如下

public class WebServer {
    /**
     * 存储path到服务的映射
     */
    private Map<String, Servlet> servletMap;

    /**
     * 初始化
     *
     * @param servletMap
     */
    public WebServer(Map<String, Servlet> servletMap) {
        this.servletMap = servletMap;
    }

    /**
     * 运行tomcat
     *
     * @throws IOException
     */
    public void run() throws IOException {
        // 开启一个socket服务,绑定端口号8888
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("===server start listen 8888===");
        while (true) {
            Socket clientSocket = serverSocket.accept();
            try {
                // 解析请求信息为HttpRequest对象
                HttpRequest request = new HttpRequest(new InputStreamReader(clientSocket.getInputStream(), "utf-8"));
                // 根据path获取servlet
                Servlet servlet = servletMap.get(request.getPathInfo());
                if (servlet == null) {
                    continue;
                }
                // 执行业务
                String data = servlet.service(request);
                // 响应
                HttpResponse response = new HttpResponse(data);
                // 返回数据
                clientSocket.getOutputStream().write(response.getBytes());
                clientSocket.getOutputStream().flush();
                clientSocket.getOutputStream().close();
                clientSocket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

到此,一个简易内嵌版的tomcat做完了,项目结构如下

项目结构

接下来我们在apps包中分别写两个servlet实现UserController和OrderController测试一下

public class OrderController implements Servlet {
    public String service(HttpRequest request) {
        return "{\"message\": \"订单服务完成\"}";
    }
}
public class UserController implements Servlet {
    public String service(HttpRequest request) {
        return "{\"message\": \"用户服务完成\"}";
    }
}

写个启动类

public class MainApplication {
    public static void main(String[] args) throws IOException {
        new WebServer(new HashMap<String, Servlet>(){{
            put("/user", new UserController());
            put("/order", new OrderController());
        }}).run();
    }
}

效果如下

订单服务和用户服务

实际上tomcat使用servlet并不是tomcat定义的而是JavaEE规范定义的
到此,我们的自己的web服务搭建完了,而且还顺便封装出一个简版tomcat-embed,接下来再有新的服务,只需在apps包下创建新的servlet并注册映射即可,完全不用考虑什么http协议了

注册映射有些麻烦,新增一个servlet就得改一下代码,在真实tomcat中是改xml配置,我们可以继续优化,比如自定义一个注解比如@RequestMapping("/user"),然后扫描所有类然后把带注解的类注册到映射器中,这就是springboot帮我们干的事了

BIO&NIO

上面的代码跑通了,但性能上存在问题,上面的代码同一时间只能处理一个请求,如果某个请求一直不结束,那其他请求只能干等了

可以优化一下每个请求新建一个线程去处理(或者使用线程池),可以解决以上问题,这就是BIO模型

改造代码很简单,accept之后新开一个线程去处理请求,而主线程回到accpet上等待

public class BioWebServer {
    /**
     * 存储path到服务的映射
     */
    private Map<String, Servlet> servletMap;
    /**
     * 初始化
     *
     * @param servletMap
     */
    public BioWebServer(Map<String, Servlet> servletMap) {
        this.servletMap = servletMap;
    }
    /**
     * 运行tomcat
     *
     * @throws IOException
     */
    public void run() throws IOException {
        // 开启一个socket服务,绑定端口号8888
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("===server start listen 8888===");
        // 创建一个线程池
        ExecutorService pool = Executors.newFixedThreadPool(100);
        while (true) {
            Socket clientSocket = serverSocket.accept();
            // 线程池处理请求
            pool.execute(()->{
                try {
                    // 解析请求信息为HttpRequest对象
                    HttpRequest request = new HttpRequest(new InputStreamReader(clientSocket.getInputStream(), "utf-8"));
                    // 根据path获取servlet
                    Servlet servlet = servletMap.get(request.getPathInfo());
                    if (servlet == null) {
                        return;
                    }
                    // 执行业务
                    String data = servlet.service(request);
                    // 响应
                    HttpResponse response = new HttpResponse(data);
                    // 返回数据
                    try {
                        clientSocket.getOutputStream().write(response.getBytes());
                        clientSocket.getOutputStream().flush();
                    } finally {
                        clientSocket.getOutputStream().close();
                    }
                    clientSocket.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

但BIO模型有个缺点,主要在inputStream的read上,这个read是阻塞操作(这也是为什么这个模式叫BIO,即Blocking IO,除此之外accpet操作也是阻塞的),阻塞等待什么呐,等待客户端数据传过来,并等待内核把数据准备好,而这个等待过程线程阻塞,造成了资源的浪费

  • 如果使用线程池,本来就那么几个人干活,还有几个人傻等着,效率能高吗
  • 如果不使用线程池,线程过多又会给服务器造成压力,比如c10k问题

而如果我们把代码改成NIO模型,就可以解决这个问题了,这个下篇再说~

相关文章

网友评论

    本文标题:不使用第三方工具, 纯java搭建web服务

    本文链接:https://www.haomeiwen.com/subject/ptenyrtx.html