2021.05.29 手写服务器问题搞定了……后面继续搞事情

问题搞定了

昨天的问题,已经找到问题了,是因为请求参数里有空字符串的情况,就是这些空字符串,导致inputStream被阻塞,所以最终无法响应给浏览器端。

昨天的doService方法是这样写的:

public void doService(SyskeRequest request, SyskeResponse response) throws Exception {
        try {
            byte[] bytes = new byte[1024];
            int read;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            while ((read = request.getInputStream().read(bytes)) != -1) {
                byteArrayOutputStream.write(bytes);
            }
            byte[] toByteArray = byteArrayOutputStream.toByteArray();
            String requestStr = new String(toByteArray);
            System.out.println(String.format("请求参数:%s", requestStr));
            String[] split = requestStr.split("
");
            System.out.println("end");
            response.write("hello syskeCat");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

简单调整下,就可以了

public static void doService(SyskeRequest request, SyskeResponse response) {
        try {
            String readLine;
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()));
            while ((readLine = bufferedReader.readLine()) != null) {
                if(readLine.length() == 0){
                    break;
                }
                byteArrayOutputStream.write(bytes);
            }
            byte[] toByteArray = byteArrayOutputStream.toByteArray();
            String requestStr = new String(toByteArray);
            System.out.println(String.format("请求参数:%s", requestStr));
            String[] split = requestStr.split("
");
            System.out.println("end");
            response.write("hello syskeCat");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我把不一样的地方单独提出来:

这是原来的写法:

while ((read = request.getInputStream().read(bytes)) != -1) {
                byteArrayOutputStream.write(bytes);
            }

修改之后:

 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(request.getInputStream()));
            while ((readLine = bufferedReader.readLine()) != null) {
                if(readLine.length() == 0){
                    break;
                }
            }

其实,写这段代码就是为了拿到请求头和请求参数:

{Cookie= NMTID=00O3GJZgJXpkp8vI0cpmNRliUnGvYkAAAF4sPsi2w, 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, Connection= keep-alive, User-Agent= Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36, Sec-Fetch-Site= cross-site, Sec-Fetch-Dest= document, Host= localhost:8080, Accept-Encoding= gzip, deflate, br, Sec-Fetch-Mode= navigate, sec-ch-ua= " Not A;Brand";v="99", "Chromium";v="90", "Google Chrome";v="90", sec-ch-ua-mobile= ?0, Cache-Control= max-age=0, Upgrade-Insecure-Requests= 1, Sec-Fetch-User= ?1, Accept-Language= zh-CN,zh;q=0.9}

直接删除掉while循环也行,但是也就拿不到完整的请求信息了。而且经过我的实验,如果把空字符判断那块拿掉,就又会出现昨天的情况了。

项目已建好

昨天立的flag,今天已经把仓库建好了,写了一些简单的代码,以后就是慢慢添砖添瓦了。我们先来看看,现在是什么进展。

项目结构

创建了一个maven项目,目前只实现了简单的RequestResponse,可以实现简单的请求,但是还不能根据不同的请求返回对应的结果,这是下一步的工作。

关于这个项目,初步的计划是,后面会逐步实现Springboot的一些简单功能,最后希望能通过这个项目,能够更深入地了解springboot,了解web服务器。目前是基于传统的socket实现的,后面想以NIO的方式再写一遍,大概率会用Netty

服务器入口

这里启用了一个线程池来处理客户端的请求

public class SyskeBootServerApplication {
    private static final Logger logger = LoggerFactory.getLogger(SyskeBootServerApplication.class);
    private static final int SERVER_PORT = 8080;
    private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());

    public static void main(String[] args) {
        start();
    }

    /**
     * 启动服务器
     * @throws Exception
     */
    public static void start() {
        try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT) ) {
            Socket accept = null;
            logger.info("SyskeCatServer is starting……, port: {}", SERVER_PORT);
            while ((accept = serverSocket.accept()) != null){
                threadPoolExecutor.execute(new SyskeRequestHandler(accept));
            }
        } catch (Exception e) {
            logger.error("服务器后端异常");
        }
    }
}

请求处理线程

这里的核心在run方法,也就是在这里处理客户端的请求,后期要着重实现doDispatcher()方法,实现请求的分发和响应。在SyskeRequestHandler被实例化的时候,会根据客户端socket构建requstresponse

public class SyskeRequestHandler implements Runnable {
    private final Logger logger = LoggerFactory.getLogger(SyskeRequestHandler.class);
    private Socket socket;
    private SyskeRequest syskeRequest;
    private SyskeResponse syskeResponse;

    public SyskeRequestHandler(Socket socket) throws IOException{
        this.socket = socket;
        init();
    }

    private void init() throws IOException{
        this.syskeRequest = new SyskeRequest(socket.getInputStream());
        this.syskeResponse = new SyskeResponse(socket.getOutputStream());
    }

    @Override
    public void run() {
        try {
           doDispatcher();
        } catch (Exception e) {
            logger.error("系统错误:", e);
        }
    }

    /**
     * 请求分发处理
     * @throws Exception
     */
    public void doDispatcher() throws Exception{
        logger.info("请求头信息:{}", syskeRequest.getHeader());
        logger.info("请求信息:{}", syskeRequest.getRequestAttributeMap());
        syskeResponse.write(String.format("hello syskeCat, dateTime:%d", System.currentTimeMillis()));
        socket.close();
    }
}

请求封装

除了获取inputStream,还有获取请求参数和请求头的封装。后面还要继续完善请求头这一块,实现获取请求地址和具体参数的需求。

public class SyskeRequest implements Request {
    /**
     * 输入流
     */
    private InputStream inputStream;
    /**
     * 请求参数
     */
    private Map<String, String> requestAttributeMap;

    private String header;

    public SyskeRequest(InputStream inputStream) throws IOException{
        this.inputStream = inputStream;
        initRequest();
    }

    @Override
    public InputStream getInputStream() {
        return inputStream;
    }

    @Override
    public Map<String, String> getRequestAttributeMap() throws IOException{
        if (requestAttributeMap != null) {
            return requestAttributeMap;
        }
        initRequest();
        return requestAttributeMap;
    }

    /**
     * 初始化请求
     *
     * @throws IOException
     */
    private void initRequest() throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        header = bufferedReader.readLine();
        Map<String, String> attributeMap = Maps.newHashMap();
        String readLine = null;
        while ((readLine = bufferedReader.readLine()) != null) {
            if(readLine.length()==0) {
                break;
            }
            if (readLine.contains(":")) {
                String[] split = readLine.split(":", 2);
                attributeMap.put(split[0], split[1]);
            }
        }
        requestAttributeMap = attributeMap;
    }

    public String getHeader() {
        return header;
    }
}

响应封装

现在的响应信息太单一了,不论前端发送啥请求,后端始终响应的是html,这块也有再优化。下一步就是要实现根据请求头,响应对应的数据信息。

public class SyskeResponse implements Response {
    private OutputStream outputStream;

    public SyskeResponse(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public OutputStream getOutputStream() {
        return outputStream;
    }

    //将文本转换为字节流
    public void write(String content) throws IOException {
        StringBuffer httpResponse = new StringBuffer();
        // 按照HTTP响应报文的格式写入
        httpResponse.append("HTTP/1.1 200 OK
").append("Content-Type:text/html
").append("
")
            .append("<html><head><link rel="icon" href="data:;base64,="></head><body>").append(content)
            .append("</body></html>");
        // 将文本转为字节流
        outputStream.write(httpResponse.toString().getBytes());
        outputStream.flush();
        outputStream.close();
    }
}

总结

flag立好了,项目也建起来,后面就是实现自己的想法了。说实话,我现在还是挺期待的,感觉这是个很有意思的事情。可能看文章的小伙伴不一定能get到任何点,但对我来说真的是颠覆性的,昨天我也说了,今天依然有这样的感觉。以前,顶多是写写前端页面,写写后端接口,但是这个请求如何从前端到后端的,就是特别谜,也从来没想明白,但是昨天一下就醍醐灌顶了,感觉这一道大门终于被打通了,这感觉有一种发自心底的愉悦和兴奋,我甚至有种想马上把其他功能需求都实现的冲动。

下面是项目的开源仓库,有兴趣的小伙伴可以去看看,如果有想法的小伙伴,我真心推荐你自己动个手,自己写一下,真的感觉不错:

https://github.com/Syske/syske-boot

原文地址:https://www.cnblogs.com/caoleiCoding/p/14827749.html