不使用第三方工具, 纯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