1. 1 不可撤销
  2. 2 小年兽 程嘉敏
  3. 3 手放开 李圣杰
  4. 4 迷人的危险3(翻自 dance flow) FAFA
  5. 5 山楂树之恋 程佳佳
  6. 6 summertime cinnamons / evening cinema
  7. 7 不谓侠(Cover 萧忆情Alex) CRITTY
  8. 8 神武醉相思(翻自 优我女团) 双笙
  9. 9 空山新雨后 音阙诗听 / 锦零
  10. 10 Wonderful U (Demo Version) AGA
  11. 11 广寒宫 丸子呦
  12. 12 陪我看日出 回音哥
  13. 13 春夏秋冬的你 王宇良
  14. 14 世界が终わるまでは… WANDS
  15. 15 多想在平庸的生活拥抱你 隔壁老樊
  16. 16 千禧 徐秉龙
  17. 17 我的一个道姑朋友 双笙
  18. 18 大鱼  (Cover 周深) 双笙
  19. 19 霜雪千年(Cover 洛天依 / 乐正绫) 双笙 / 封茗囧菌
  20. 20 云烟成雨(翻自 房东的猫) 周玥
  21. 21 情深深雨濛濛 杨胖雨
  22. 22 Five Hundred Miles Justin Timberlake / Carey Mulligan / Stark Sands
  23. 23 斑马斑马 房东的猫
  24. 24 See You Again Wiz Khalifa / Charlie Puth
  25. 25 Faded Alan Walker / Iselin Solheim
  26. 26 Natural J.Fla
  27. 27 New Soul Vox Angeli
  28. 28 ハレハレヤ(朗朗晴天)(翻自 v flower) 猫瑾
  29. 29 像鱼 王贰浪
  30. 30 Bye Bye Bye Lovestoned
  31. 31 Blame You 眠 / Lopu$
  32. 32 Believer J.Fla
  33. 33 书信 戴羽彤
  34. 34 柴 鱼 の c a l l i n g【已售】 幸子小姐拜托了
  35. 35 夜空中最亮的星(翻自 逃跑计划) 戴羽彤
  36. 36 慢慢喜欢你 LIve版(翻自 莫文蔚) 戴羽彤
  37. 37 病变(翻自 cubi) 戴羽彤
  38. 38 那女孩对我说 (完整版) Uu
  39. 39 绿色 陈雪凝
  40. 40 月牙湾 LIve版(翻自 F.I.R.) 戴羽彤
夜空中最亮的星(翻自 逃跑计划) - 戴羽彤
00:00 / 04:10

夜空中最亮的星 能否听清

那仰望的人 心底的孤独和叹息

夜空中最亮的星 能否记起

那曾与我同行 消失在风里的身影

我祈祷拥有一颗透明的心灵

和会流泪的眼睛

给我再去相信的勇气

越过谎言去拥抱你

每当我找不到存在的意义

每当我迷失在黑夜里

噢喔喔 夜空中最亮的星

请指引我靠近你

夜空中最亮的星 是否知道

那曾与我同行的身影 如今在哪里

夜空中最亮的星 是否在意

是等太阳先升起 还是意外先来临

我宁愿所有痛苦都留在心底

也不愿忘记你的眼睛

哦 给我再去相信的勇气

哦 越过谎言去拥抱你

每当我找不到存在的意义

每当我迷失在黑夜里

噢喔喔 夜空中最亮的星

请照亮我向前行 哒~

我祈祷拥有一颗透明的心灵

和会流泪的眼睛 哦

给我再去相信的勇气

哦 越过谎言去拥抱你

每当我找不到存在的意义

每当我迷失在黑夜里

噢喔喔 夜空中最亮的星

请照亮我向前行

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

前言

前几天一直被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

posted @ 2021-06-09 13:38  云中志  阅读(159)  评论(0编辑  收藏  举报