手写web服务器:基于NIO重构服务器,实现post请求处理

共 17427字,需浏览 35分钟

 ·

2021-06-07 10:59

前言

前几天一直被post请求处理的问题卡着,因此web服务器这边也没啥进展,再加上昨天又突然被告知要去加班,所以这个问题就一直被一次次往后拖,还好今天有时间,就抽空把这个问题彻底解决了,然后服务这边也彻底从原来的socket,被我重构成NioServerSocketChannel,也就是我们前面说的非阻塞式socket,今天主要介绍整个重构过程,nio的知识点暂时也不打算讲,因为我也没有搞得特别清楚。好了,话不多说,直接重构。

重构

手写我们重新写了sokcet的核心程序,实现方式彻底改变了,首先是一个服务器接收客户端请求的线程:

接收服务器请求线程

 static class AcceptSocketThread extends Thread {
        volatile boolean runningFlag = true;

        @Override
        public void run() {
            try {
                ServerSocketChannel serverChannel = ServerSocketChannel.open();
                serverChannel.bind(new InetSocketAddress(30000));
                serverChannel.configureBlocking(false);

                while (runningFlag) {
                    SocketChannel channel = serverChannel.accept();

                    if (null == channel) {
                        logger.info("服务端监听中.....");
                    } else {
                        channel.configureBlocking(false);
                        logger.info("一个客户端上线,占用端口 :{}", channel.socket().getPort());
                        keys.put(channel.socket().getPort(), channel);
                        new ResponseThread().start();
                    }
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

在线程内部,我们通过ServerSocketChannel.open创建了一个ServerSocketChannel通信频道,并设置频道端口是30000;

configureBlocking是设置当前通信是否阻塞,这里我们设置的是false,也就是非阻塞通信;

然后通过一个死循环监听服务器serverChannel是否被连接,这里serverChannel.accept()返回值为null表示未建立连接或者连接被关闭;

如果建立连接,我们将通信频道放进keys通信频道队列中:

public static volatile Map<Integer, SocketChannel> keys =
        Collections.synchronizedMap(new HashMap<>());

并启动一个响应请求线程去处理这个频道中的请求,下面我们看处理线程

处理请求线程

在写这些文字时候,我发现这里其实没必要创建队列存放会话频道,可以直接把这块的队列传进线程,并处理(因为我这块代码是参考别人的,然后进行了大改,后面还需要进一步优化)

/**
     * 处理客户端请求
     */

    static class ResponseThread extends Thread {
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        @Override
        public void run() {
            int num = 0;
            Iterator<Integer> ite = keys.keySet().iterator();
            while (ite.hasNext()) {
                int key = ite.next();
                StringBuffer stb = new StringBuffer();
                try {
                    SocketChannel socketChannel = keys.get(key);
                    if (Objects.isNull(socketChannel)) {
                        break;
                    }
                    while ((num = socketChannel.read(buffer)) > 0) {
                        buffer.flip();
                        stb.append(charset.decode(buffer).toString());
                        buffer.clear();
                    }
                    if (stb.length() > 0) {
                        MsgWrapper msg = new MsgWrapper();
                        msg.key = key;
                        msg.msg = stb.toString();
                        logger.info("端口:{}的通道,读取到的数据:{}",msg.key, msg.msg);
                        msgQueue.add(msg);
                        threadPoolExecutor.execute(new SyskeRequestNioHandler(socketChannel, msg.msg));
                        ite.remove();
                    }
                } catch (Exception e) {
                    ite.remove();
                    logger.error("error: 端口占用为:{},的连接的客户端下线了", keys.get(key).socket().getPort(), e);
                }
            }
            logger.info("读取线程监听中......");
        }

    }

因为原代码,作者的接收线程、处理线程都是在main方法启动的,所以他这样定义是ok的,但我这里其实就没必要了。

看了上面的代码,大家会发现,nio中不再有InputStream或者OutputStream这样的类,这是因为nio的底层实现采用了新的架构,有一个selector进行频道管理,当某个频道有数据进来的时候,selector会切换到这个频道进行数据处理,如果没有数据他会去处理其他频道的数据,不像我们之前的I/O,一次通信就一个管道,没有数据就一直等待,所以也就不会导致阻塞。

我觉得有个例子能很好地说明这两种模型,传统的I/o就好比一个单位的电话,电话虽然很多,但是线路只有一条,同时只能有一个电话进行通话,电话不断,其他人根本就打不进去,也没法接电话,只能等着这个接收电话的人打完电话;

Nio就相当于这个单位为了解决同时只能有一个人打电话这种情况,专门雇了一个接线员负责线路切换,当有电话进来以后,接线员会把对应的电话借给对应的人,这样即提高了线路的效率,也避免了阻塞的情况。

做完上面的改动后,我们的post请求就不再阻塞了,然后我们还优化了request的初始化。

优化请求初始化

现在不论get请求,还是post请求,最终都会拿到一个纯文本的请求参数,然后我我把它分别处理成header(请求方法、请求地址)、requestAttributeMap(请求头参数)、requestBody(请求体):

 private void initRequest() throws IllegalParameterException{
        logger.info("SyskeRequest start init");
        String[] inputs = input.split("\r\n");
        System.out.println(Arrays.toString(inputs));
        Map<String, Object> attributeMap = Maps.newHashMap();
        boolean hasBanlk = false;
        StringBuilder requestBodyBuilder = new StringBuilder();
        for (int i = 0; i < inputs.length; i++) {
            if(i == 0) {
                String[] headers = inputs[0].split(" "3);
                String requestMapping = headers[1];
                if (requestMapping.contains("?")) {
                    int endIndex = requestMapping.lastIndexOf('?');
                    String requestParameterStr = requestMapping.substring(endIndex + 1);
                    requestMapping = requestMapping.substring(0, endIndex);
                    String[] split = requestParameterStr.split("&");
                    for (String s : split) {
                        String[] split1 = s.split("=");
                        attributeMap.put(StringUtil.trim(split1[0]), StringUtil.trim(split1[1]));
                    }

                }
                this.header = new RequestHear(RequestMethod.match(headers[0]), requestMapping);
            } else {
                if (StringUtil.isEmpty(inputs[i])) {
                    hasBanlk = true;
                }
                if (inputs[i].contains(":") && Objects.equals(hasBanlk, Boolean.FALSE)) {
                    String[] split = inputs[i].split(":"2);
                    attributeMap.put(split[0], split[1]);
                } else {
                    // post 请求
                    requestBodyBuilder.append(inputs[i]);
                }
            }
        }
        requestAttributeMap = attributeMap;
        requestBody = JSON.parseObject(requestBodyBuilder.toString());
        logger.info("requestBodyBuilder: {}", requestBodyBuilder.toString());
        logger.info("SyskeRequest init finished. header: {}, requestAttributeMap: {}", header, requestAttributeMap);
    }

这里就很简单了,就是通过\r\n分割即可。

getpost唯一的区别就是,get请求的参数都在requestAttributeMap,而post的请求参数在requestBody

/**
     * 处理post请求
     * @param method
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws InvocationTargetException
     */

    private Object doPost(Method method) throws IllegalAccessException, InstantiationException, InvocationTargetException {
        JSONObject requestBody = (JSONObject)request.getRequestBody();
        return doRequest(method, requestBody);
    }

    /**
     * 处理get请求
     * @param method
     * @param requestAttributeMap
     * @return
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */

    private Object doGet(Method method, Map<String, Object> requestAttributeMap) throws InvocationTargetException, IllegalAccessException, InstantiationException {
       return doRequest(method, requestAttributeMap);
    }

测试

然后我们测试下看下,这里就只测试post了,这里我用的postMan

看下后台:

请求体已经有数据了,后面的就很简单了

总结

我现在越来越觉得,作为一个web后端工程师,网络编程是一个特别重要的技能,因为你不了解数据在网络中的传输过程,不了解各种协议,不了解各种请求头,那你再遇到具体问题的时候,是根本没有任何思路的。

可能在你眼里你可能会觉得一切你解决不了的问题,都是玄学问题,但事实并非如此。

所以,对我现在而言,学习的方向大概分为这几种:

  • 多线程:这个应该是一个比较核心,掌握的好,你的工作真的会事半功倍的
  • 网络编程:这个原因我前面说了
  • 算法,包括数据结构等:帮助你构建更好的模型,让你的程序运行更快,性能更好
  • 虚拟化相关知识,比如dockerk8s等,以及jenkins自动化构建,这一块现在是比较主流的技术
  • 主流开源框架学习,这里我会花比较少的时间,以搞清楚具体的原理和实现方式为目的

今天把这个问题解决了,后面又可以继续实现springboot的其他注解了,继续搞事情。好了,今天就到这里吧!

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

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


浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报