-
Notifications
You must be signed in to change notification settings - Fork 0
/
content.json
1 lines (1 loc) · 44.6 KB
/
content.json
1
{"meta":{"title":"Xc的博客","subtitle":null,"description":"java/C# 技术 后台开发","author":"Xiaochen Wu","url":"https://billxc.github.io"},"pages":[],"posts":[{"title":"搭建Chrome的clangd indexer server","slug":"cpp/clangd-index","date":"2022-09-09T14:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2022/09/09/cpp/clangd-index/","link":"","permalink":"https://billxc.github.io/2022/09/09/cpp/clangd-index/","excerpt":"","text":"前言最近的新工作是第一次在Mac上对进行Chrome的二次开发。作为一个VSCode爱好者,必须在VSCode上搭建一个用着顺手的开发环境。 Chrome使用了GN/Ninja作为构建系统,llvm/clang作为编译器。由于编辑过程比较复杂,自带的C/C++插件无法做到开箱即用,恰好llvm项目有一个clangd的子项目,提供语法分析的服务,而clangd恰好有VSCode的插件,而Chrome也很贴心地提供了clangd所需的compile_commands.json的生成脚本。 这个方案看起来很美好,但是在实际使用中,发现了一些问题: clangd有了compile_commands.json之后,除了会解析当前正在编辑的文件及其依赖,还会自动去解析项目中所有的文件,这个过程会非常耗时(大约七八个小时),这个过程还会占用大量的CPU资源,导致电脑风扇狂转,让本就散热捉急的MBP2019变得更加烫手。 分析完所有文件后,如果代码有大批量的更改,比如切换分支,对main分支进行rebase等等,clangd会自动重新分析,main分支一天的commit的代码更改,便会导致clangd对几乎所有的文件进行重新分析。 针对这个问题,clangd是有一个解决方案的,那就是clangd indexer server。只需要在本地生成一次clangd index文件,然后使用clangd index server加载,就组成了一台可以供多个客户端使用的clangd indexer server。这样就可以避免每次都要重新生成clangd index文件,而且可以在多个客户端共享。 虽然Chrome很复杂,但是Chrome也是可以搭建clangd index server的,Chrome官方就有也有自己的clangd index server。作为Chrome的二次开发者,我们是不能直接用Chrome的index server的,所以我们需要搭建自己的clangd index server。 一些挑战 Chrome使用的llvm版本并不是release版本,而是Chrome自己内部编译的版本,而我们的Chrome也使用了我们自己内部定制编译的llvm,而这个定制版本的llvm是不包含clangd的,所以我们需要自己编译clangd。 Chrome官方的clangd index server是针对Linux的,搜索了网上的内容,Mac版本的Chrome remote index并没有人分享过相关内容,需要自己蹚水。 准备工作 确保本地已经安装好了Chrome的编译环境,即已经编译过一次Chrome 安装cmake,git,ninja,brew 搭建步骤确定clangd的版本和gRPC的版本,并且下载其源码 在src目录下执行 gclient sync 在shell中运行third_party/llvm-build/Release+Asserts/bin/clang --version 你会得到类似于以下的信息 clang version 15.0.0 (https://github.com/llvm/llvm-project.git 4ba6a9c9f65bbc8bd06e3652cb20fd4dfc846137) Target: x86_64-apple-darwin21.6.0 Thread model: posix InstalledDir: /Users/xc/repos/llvm-project/build/bin 其中的4ba6a9c就是llvm的commit version 下载llvm源码,执行 git clone https://github.com/llvm/llvm-project 切换到llvm的commit version,执行git checkout 4ba6a9c(将4ba6a9c替换为你自己的commit version) 在llvm的clang-tools-extra/clangd/index/remote/README.md文件中,写有grpc的版本号,以及代码的下载方式(本文以v1.36.3为例) 下载grpc源码,执行 git clone -b v1.36.3 https://github.com/grpc/grpc 编译gRPC(作为clangd的依赖项)在上一步中的README.md中,有编译gRPC的步骤,如果已经按照步骤编译过gRPC,可以跳过这一步。PS:以下命令都是在grpc的根目录下执行12345678910111213141516171819202122232425# 使用brew安装编译gRPC所需的依赖brew install autoconf automake libtool shtool# 加载git submodule中的内容git submodule update --initmkdir build# 下一步中我们会用GRPC_INSTALL_PATH来指定gRPC的安装路径,这里我们指定为~/.grpc_installexport GRPC_INSTALL_PATH=$HOME/.grpc_installcmake -S . -B build \\-DCMAKE_BUILD_TYPE=Release \\ # Release模式-DgRPC_INSTALL=ON \\-DgRPC_BUILD_TESTS=OFF \\-DCMAKE_INSTALL_PREFIX=$GRPC_INSTALL_PATH \\-G Ninja# CMAKE_BUILD_TYPE=Release选项开启Release模式,gRPC的性能会更好# gRPC_INSTALL=ON选项可以在编译后对gRPC进行安装,# gRPC_BUILD_TESTS=OFF选项可以避免编译gRPC的测试用例# CMAKE_INSTALL_PREFIX=$GRPC_INSTALL_PATH 选项指定gRPC的安装路径# -G Ninja选项可以使用ninja来编译gRPC# 编译gRPC,并且安装到~/.grpc_install目录下cmake --build build --target install 编译clangd使用shell进入llvm的根目录,执行以下命令1234567891011121314151617makedir build# 使用上一步中的GRPC_INSTALL_PATH来指定gRPC的安装路径export GRPC_INSTALL_PATH=$HOME/.grpc_installcmake -S llvm -B build \\-DCMAKE_BUILD_TYPE=Release \\-DLLVM_ENABLE_PROJECTS=\"clang;clang-tools-extra\" \\-DCLANGD_ENABLE_REMOTE=On \\-DCMAKE_INSTALL_PREFIX=$GRPC_INSTALL_PATH \\-DGRPC_INSTALL_PATH=$GRPC_INSTALL_PATH \\-G Ninja# -DLLVM_ENABLE_PROJECTS=\"clang;clang-tools-extra\" llvm默认只编译clang,这里我们需要编译clangd,所以需要加上clang-tools-extra# -DCLANGD_ENABLE_REMOTE=On 开启clangd的remote模式(默认是关闭的)# -DCMAKE_INSTALL_PREFIX=$GRPC_INSTALL_PATH 指定gRPC的安装路径cmake --build build 生成compile_commands.json生成ninja build的中间文件生成clangd的remote indexTODO:未完待续…中间遇到的坑 版本不匹配 clangd和gRPC的版本不匹配,会导致编译失败。 我因为这个原因,编译了好几次,最终才摸索出找到正确的匹配的版本号的方法 中间文件缺失 后记…","categories":[],"tags":[{"name":"Chrome","slug":"Chrome","permalink":"https://billxc.github.io/tags/Chrome/"},{"name":"clang","slug":"clang","permalink":"https://billxc.github.io/tags/clang/"},{"name":"clangd","slug":"clangd","permalink":"https://billxc.github.io/tags/clangd/"},{"name":"llvm","slug":"llvm","permalink":"https://billxc.github.io/tags/llvm/"},{"name":"cpp","slug":"cpp","permalink":"https://billxc.github.io/tags/cpp/"},{"name":"cmake","slug":"cmake","permalink":"https://billxc.github.io/tags/cmake/"}]},{"title":"为SpringBoot的启动过程埋点","slug":"spring杂记/为SpringBoot的启动过程埋点","date":"2018-12-10T10:00:00.000Z","updated":"2022-09-12T04:18:03.617Z","comments":true,"path":"2018/12/10/spring杂记/为SpringBoot的启动过程埋点/","link":"","permalink":"https://billxc.github.io/2018/12/10/spring杂记/为SpringBoot的启动过程埋点/","excerpt":"","text":"简而言之,继承org.springframework.boot.SpringApplicationRunListener,并且让Spring加载进来。 为什么要为SpringBoot的启动过程埋点因为SpringBoot启动过慢,需要找出在Spring的哪一步遇到了性能问题,然后进行专项优化。 为什么不能用Spring的EventListener来处理EventListener的一种用法如下:1234@EventListenerpublic void handleEventObject(Object event) { System.out.println(\"event: \"+event.getClass());} 如果可以用,那EventHandler是侵入性很低,工作量又小的一种方式,但是EventListener只有在Spring容器初始化完毕后才会收到消息,无法胜任监听 SpringApplicationRunListeners VS SpringApplicationRunListenerSpringApplicationRunListener是Spring内部的一个接口,方法有(入参和出参省略): starting() environmentPrepared() contextPrepared() contextLoaded() void started() void running() void failed() 简单看一眼这些方法名就知道,这些都像是在Spring容器初始化的过程会调用到的回调函数,如果我们可以自己定义SpringApplicationRunListener并让Spring加载,就可以做到为SpringBoot的启动埋点。但翻遍文档后发现,Spring并未为开发者提供SpringApplicationRunListener的配置入口,于是便看了看SpringBoot的部分源码。 Spring 何时加载 SpringApplicationRunListener关键代码 new SpringApplication(primarySources).run(args);: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354/** * Run the Spring application, creating and refreshing a new * {@link ApplicationContext}. * @param args the application arguments (usually passed from a Java main method) * @return a running {@link ApplicationContext} */public ConfigurableApplicationContext run(String... args) { //... //Spring加载了SpringApplicationRunListeners SpringApplicationRunListeners listeners = getRunListeners(args); //回调starting方法 listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); //prepareEnvironment 会将listeners 传入,并在过程中调用listeners的回调函数 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); configureIgnoreBeanInfo(environment); Banner printedBanner = printBanner(environment); context = createApplicationContext(); exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); //prepareContext 会将listeners 传入,并在过程中调用listeners的回调函数 prepareContext(context, environment, listeners, applicationArguments, printedBanner); refreshContext(context); afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } //回调started方法 listeners.started(context); callRunners(context, applicationArguments); } catch (Throwable ex) { //异常时执行对应的回调 handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } try { //回调running listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context;} 简单阅读启动代码可知,listener的加载是在Spring读取配置文件之前的,因此Spring在配置文件中配置Listenr的难度很高,没有将这个接口暴露给用户也是正常的。如果我们需要自定义listener,需要研究getRunListeners这个方法 getRunListeners的实现12345private SpringApplicationRunListeners getRunListeners(String[] args) { Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class }; return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances( SpringApplicationRunListener.class, types, this, args));} SpringApplicationRunListeners 是 SpringApplicationRunListener的管理类,负责统一调用多个listener的回调方法。真正生成listener对象的是getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args)这段代码。 getSpringFactoriesInstances的相关代码: 12345678910111213private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // Use names and ensure unique to protect against duplicates // 找到所有实现了type类的类名 Set<String> names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader)); // 将类名对应的类初始化,并且作为list返回 List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names); AnnotationAwareOrderComparator.sort(instances); return instances;} SpringFactoriesLoader容易被误解是Spring的某个工厂类的Util方法,实际阅读下来后发现,其实这个方法其实是和工厂方法无关的,loadFactoryNames方法会从META-INF/spring.factories文件中读取配置,并返回所有的对应的接口的实现类。这次我们需要的接口是SpringApplicationRunListener,那我们就需要在文件中写org.springframework.boot.SpringApplicationRunListener=com.mine.SpringApplicationRunListener","categories":[{"name":"杂记","slug":"杂记","permalink":"https://billxc.github.io/categories/杂记/"}],"tags":[{"name":"spring","slug":"spring","permalink":"https://billxc.github.io/tags/spring/"}]},{"title":"记一次遇到的Spring的坑","slug":"spring杂记/spring遇到的一次坑","date":"2018-09-15T14:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/09/15/spring杂记/spring遇到的一次坑/","link":"","permalink":"https://billxc.github.io/2018/09/15/spring杂记/spring遇到的一次坑/","excerpt":"","text":"昨天我们使用spring mvc搭建的新系统遇到了一个bug。今天排查到了原因,感觉比较有意思,和大家分享一下。 背景我们使用SpringMVC的Controller来接收参数,并且使用了注解@RequestAttribute。这个注解有两个作用: 根据参数从当前请求的HttpRequest的Attribute中获取需要的参数(这个参数是我们之前使用Interceptor注入的) 这个注解有个required属性,默认为true,这个属性会在传入的Attribute 为null时抛出异常(这个异常我们会包装成参数异常并且向客户端返回对应的错误消息) 依赖于第二点,我们没有进行空指针检查,只进行了业务维度的逻辑验证。 发现问题在一次联调的时候突然发生了空指针的异常,当时我们需要的参数是一个map<Integer,Integer>,而客户端那边由于c#的一些神奇的优化,map变成了一个list,按照我们之前的预期,这种情况应该会产生异常,并且被我们的异常处理器捕获成参数异常。但实际却是没有在最开始的时候产生异常,而是代码进一步运行到了service,在使用null指针的时候报了NPE的错误。 排查结果经过排查,这个异常是是由两个坑合起来产生的: 坑1: Spring会convert你的源数据作为@RequestAttribute的最终值,比如你传入了一个string类型的”123”,而你需要的是int,这个值会变成int类型的123,而如果你传入的是一个Collection,而目标是个普通的object,他会把集合的第一个元素返回,然后进一步convert,作为你需要的参数。 坑2: required属性的检查在convert参数之前做的,如果源数据不为null,那么就不会因为参数为null而报对应的错,即使被convert之后的参数是null。 而我们的源数据正是一个list,并且第一个元素为null,导致了convert前不为null,绕过了判空检查,但convert之后为null,导致了程序出现NPE错误。 因此我们不能依赖于框架@RequestAttribute的判空处理,现在解决方法有几个:1.手动检查是否为空2.自己定义一个annotation,然后继承RequestAttributeMethodArgumentResolver,把annotation换成自己的,然后在这个自定义的方法里,在获得参数后进行判空处理3.使用spring aop在Controller层检查参数是否为空。 第一种会在代码里产生很多重复的非业务逻辑的空检查,我们把这个作为备选项。第二种侵入性过强,我们先放弃了。最终我们选择了第三种 结论 @RequestAttribute的判空处理不可靠,必须再进行一次判空以保证传入的参数正确 有时不能过分信任框架 后记给spring报了个bug,最后还是被关掉了。地址:https://jira.spring.io/browse/SPR-16438","categories":[],"tags":[{"name":"spring","slug":"spring","permalink":"https://billxc.github.io/tags/spring/"}]},{"title":"Aria2 源码阅读 - InitiateConnectionCommandFactory","slug":"aria2源码阅读笔记/aria2源码阅读-CommandFactory","date":"2018-05-30T16:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/05/30/aria2源码阅读笔记/aria2源码阅读-CommandFactory/","link":"","permalink":"https://billxc.github.io/2018/05/30/aria2源码阅读笔记/aria2源码阅读-CommandFactory/","excerpt":"","text":"Aria2中使用了抽象工厂模式,InitiateConnectionCommandFactory是一个生产command的抽象工厂 公共方法 createInitiateConnectionCommand(int cuid, Request;star; req, DownloadEngine;star; e) : ;star;Command 根据传入的参数,返回对应的command,当前只支持req.protocol为Http的请求,对应的Command为HttpInitiateConnectionCommand","categories":[{"name":"Aria2源码阅读","slug":"Aria2源码阅读","permalink":"https://billxc.github.io/categories/Aria2源码阅读/"}],"tags":[{"name":"aria2源码阅读","slug":"aria2源码阅读","permalink":"https://billxc.github.io/tags/aria2源码阅读/"},{"name":"源码阅读","slug":"源码阅读","permalink":"https://billxc.github.io/tags/源码阅读/"}]},{"title":"Aria2 源码阅读 - DownloadEngine","slug":"aria2源码阅读笔记/aria2源码阅读-DownloadEngine","date":"2018-05-30T10:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/05/30/aria2源码阅读笔记/aria2源码阅读-DownloadEngine/","link":"","permalink":"https://billxc.github.io/2018/05/30/aria2源码阅读笔记/aria2源码阅读-DownloadEngine/","excerpt":"","text":"DownloadEngine 是Aria2中负责执行下载命令的类,它持有SegmentMan的引用,负责Command的执行。 私有变量 rsockets : vector<Socket;star;> 读套接字 wsockets : vector<Socket;star;> 写读套接字 私有方法 waitData() : void 等待rsockets或者wsockets可用 addSocket(vector<Socket;star;>& sockets, ;star;Socket socket) : bool 遍历sockets,如果目标socket已经存在,则返回false,否则push socket,并且返回true deleteSocket(vector<Socket;star;>& sockets, ;star;Socket socket) : bool 遍历sockets,如果目标socket已经存在,则删除socket返回true,否则返回false 公共变量 nowait : bool 初始值为false commands : queue<Command;star;> 等待执行的任务列表 segmentMan : ;star;SegmentMan 分块管理器 diskWriter : ;star;DiskWriter logger : ;star;Logger option : ;star;Option 公共方法 run() : void 开始下载任务,将任务从commands里面取出,如果执行成功,删除任务。 每执行完一条任务,调用waitDate,等待可用的socket,打印下载的速度、连接数等情况,并且把nowait置为false 全部command执行完后,会调用segmentMan的removeIfFinished,删除下载的配置文件。 addSocketForReadCheck(Socket;star; socket) : bool 尝试在rsockets中添加socket deleteSocketForReadCheck(Socket;star; socket) : bool 尝试在rsockets中删除socket addSocketForWriteCheck(Socket;star; socket) : bool 尝试在wsockets中添加socket deleteSocketForWriteCheck(Socket;star; socket) : bool 尝试在wsockets中删除socket","categories":[{"name":"Aria2源码阅读","slug":"Aria2源码阅读","permalink":"https://billxc.github.io/categories/Aria2源码阅读/"}],"tags":[{"name":"aria2源码阅读","slug":"aria2源码阅读","permalink":"https://billxc.github.io/tags/aria2源码阅读/"},{"name":"源码阅读","slug":"源码阅读","permalink":"https://billxc.github.io/tags/源码阅读/"}]},{"title":"Aria2 源码阅读 - SegmentMan","slug":"aria2源码阅读笔记/aria2源码阅读-SegmentMan","date":"2018-05-23T00:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/05/23/aria2源码阅读笔记/aria2源码阅读-SegmentMan/","link":"","permalink":"https://billxc.github.io/2018/05/23/aria2源码阅读笔记/aria2源码阅读-SegmentMan/","excerpt":"","text":"SegmentMan是负责Aria2中某一次的下载的类(This class holds the download progress of the one download entry.)。职责包括:分割下载块,保存下载配置等。 变量 totalSize : long long int 总共需要下载的字节数,如果是分块传输编码Chunked transfer encoding,或者Content-Length不存在,则这个字段为0 isSplittable : bool 表示该下载是否可分,即是否启用多线程下载,当总共需要下载的字节数不可知时,会被DownloadCommand设为0 downloadStarted : bool 是否开始下载,默认是false segments : vector<Segment> Segment是一个简单的结构体,表示一个文件下载的分块。 代码如下,它有5个字段: sp和ep是文件分块的开始和结束的偏移量。这两个变量会被用于http头中: Range: bytes=sp-ep ds表示已经下载的字节数量 finish表示区块下载是否下载完成 cuid 表示一个唯一的ID1234567typedef struct { int cuid; long long int sp; long long int ep; long long int ds; bool finish;} Segment; filename : string 文件名,如果没有成功获得文件名,则为长度为0的string dir : string 下载目录 ufilename : string 用户指定的文件名 logger : ;star;Logger 用来打印日志的类 SEGMENT_FILE_EXTENSION : const ".aria2" 分区文件的后缀 私有方法 read(;star;FILE file) : void 从保存的aria2文件中,并且载入配置 openSegFile(string segFilename, string mode) : ;star;FILE 打开保存的aria2文件 公共方法 getFilePath() : string 将目录和文件名组合返回文件的路径,如果文件名为空,则返回默认值index.html getSegmentFilePath() : string 将文件路径和分区文件的后缀组合成新的文件名 unregisterId(int cuid) : void 将segments里所有的cuid匹配的segment的cuid设置为0 getSegment(Segment& segment, int cuid) : bool 返回cuid相匹配的Segment, 并且填充到传入的指针中。 如果能找到匹配cuid的segment,返回对应的cuid的。 如果为segments为空,则返回一个对应的cuid的0值的segment,并将segment push到segments中。 如果没有找到,并且isSplittable为false,则返回false。 如果没有找到,但是存在finish为false,且cuid为0的segment,则设定cuid,并返回。 如果没有找到,但是存在finish为false的segment,且segment的未完成的大小大于512K(524288 Byte),则将此segment分裂成为两个,将新的segment push进segments并返回。 updateSegment(const Segment& segment) : void 遍历segments,找出cuid,sp,ep相匹配的值,并进行替换。 从结果上是更新了符合条件的Segment的ds和finsh字段 segmentFileExists() : bool 返回分块文件对应的文件名(*.aria2)是否存在。如果分块不可分,则直接返回false load() : void 从segments信息文件载入segments,如果分块不可分,则不进行任何操作 save() : void 将segments信息保存入(.aria2)文件如果分块不可分,则不进行任何操作 remove() : void 移出.aria2文件 finished() : bool 返回是否下载完成 removeIfFinished() : void 如果下载完成,则移除.aria2文件 getDownloadedSize() : long long int 返回已经下载的文件大小。 计算方式:遍历segments,将所有的segment的已下载数量相加","categories":[{"name":"Aria2源码阅读","slug":"Aria2源码阅读","permalink":"https://billxc.github.io/categories/Aria2源码阅读/"}],"tags":[{"name":"aria2源码阅读","slug":"aria2源码阅读","permalink":"https://billxc.github.io/tags/aria2源码阅读/"},{"name":"源码阅读","slug":"源码阅读","permalink":"https://billxc.github.io/tags/源码阅读/"}]},{"title":"Aria2 源码阅读 - Command和AbstractCommand","slug":"aria2源码阅读笔记/aria2源码阅读-Command","date":"2018-05-15T14:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/05/15/aria2源码阅读笔记/aria2源码阅读-Command/","link":"","permalink":"https://billxc.github.io/2018/05/15/aria2源码阅读笔记/aria2源码阅读-Command/","excerpt":"","text":"Command是DownloadEngin执行的任务的基类,而AbstractCommand则基于Command添加了更多的功能。除了SleepCommand是直接继承Command的,其他所有的Command都是直接或者间接继承了AbstractCommand。 CommandCommand的定义十分简单,只有一个protected的cuid字段和对应的get方法,一个构造函数,一个析构函数和一个execute方法12345678910class Command { protected: int cuid; public: Command(int cuid):cuid(cuid) {} virtual ~Command() {} virtual bool execute() = 0; int getCuid() const { return cuid; }}; AbstractCommand公共方法 AbstractCommand(int cuid, Request;star; req, DownloadEngine;star; e, Socket;star; s= NULL) 构造函数 将变量赋值给类属性 Socket使用了传入的socket新建了Socket的拷贝,并且将socket加入downloadEngine的socket列表中 checkSocketIsReadable和checkSocketIsWritable设置为false checkPoint设为0 ~AbstractCommand() 析构方法 将Socket从DownloadEngine的sockets列表中删除,回收socket内存 execute() : bool protected 变量 req : Request;star; Request e : DownloadEngine;star; DownloadEngine socket : Socket;star; Socket checkSocketIsReadable : bool checkSocketIsWritable : bool protected方法 virtual prepareForRetry() : bool 使用当前Command的参数(cuid,req,e),新建一个新的command,加入DownloadEngine的下载列表中 virtual onError(Exception;star; e) : void Do nothing. 未实现的虚方法 executeInternal(Segment segment) : bool 私有变量 checkPoint : struct timeval 检查点,表示时间的变量,过来检查当前是否超过该时间点一定时间 私有方法 updateCheckPoint() : void 将checkPoint更新为现在的时间 isTimeoutDetected() : bool 检查是否超时,判断条件是,当前是否已经过去检查点五秒以上, 如果检查点为0,默认返回false","categories":[{"name":"Aria2源码阅读","slug":"Aria2源码阅读","permalink":"https://billxc.github.io/categories/Aria2源码阅读/"}],"tags":[{"name":"aria2源码阅读","slug":"aria2源码阅读","permalink":"https://billxc.github.io/tags/aria2源码阅读/"},{"name":"源码阅读","slug":"源码阅读","permalink":"https://billxc.github.io/tags/源码阅读/"}]},{"title":"Aria2 源码阅读","slug":"aria2源码阅读笔记/Aria2源码阅读","date":"2018-04-22T00:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/04/22/aria2源码阅读笔记/Aria2源码阅读/","link":"","permalink":"https://billxc.github.io/2018/04/22/aria2源码阅读笔记/Aria2源码阅读/","excerpt":"","text":"Aria2是一个用C++写的开源的流行的跨平台的文件下载工具https://github.com/aria2/aria2。 我之前在写百度网盘客户端的时候遇到了文件下载的问题,缺少解决方案,于是便萌生了学习别的开源下载工具的念头。之前用过Aria2,觉得这个软件很不错,于是就选择了它。 Aria2主分支上的代码库十分庞大,各个类的数量有几十个之多。我这次先选了Aria初次发布(0.1.0版)的代码进行学习,因为这个时候的代码库还没有现在这么庞大,可以比较简单地了解作者的思路。因此可以先从这里出发,等理解作者的思路之后,再去看最新的代码库,就能够事半功倍了。 我也会将自己学习的过程中的笔记整理出来,同步到博客上。","categories":[{"name":"Aria2源码阅读","slug":"Aria2源码阅读","permalink":"https://billxc.github.io/categories/Aria2源码阅读/"}],"tags":[{"name":"aria2源码阅读","slug":"aria2源码阅读","permalink":"https://billxc.github.io/tags/aria2源码阅读/"},{"name":"源码阅读","slug":"源码阅读","permalink":"https://billxc.github.io/tags/源码阅读/"}]},{"title":"在spring中定义自己的xsd和对应的解析器","slug":"spring杂记/spring_define_xsd","date":"2018-04-15T14:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/04/15/spring杂记/spring_define_xsd/","link":"","permalink":"https://billxc.github.io/2018/04/15/spring杂记/spring_define_xsd/","excerpt":"","text":"用途可以自定义自己的xml标签和解析器,封装实现类,简化使用者的代码,而且比较Coooooool, 配置文件Spring 会扫描每个包下的META-INF/spring.schemas文件,用户可以在这个文件下定义自己的xsd 对应方式为: xsd的虚拟html地址=xsd的文件地址 写完schema后需要有对应的handler来处理,handler和schema的定义关系保存在META-INF/spring.handlers中 对应方式为: xsd的虚拟html地址=Handler类名 JAVA代码Handler 需要继承NamespaceHandlerSupport,在init方法中绑定元素名称和对应的Parser. 例子META-INF/spring.schemas: 1http\\://autoconfig.jd.com/schema/autoconfig/autoconfig.xsd=META-INF/autoconfig.xsd META-INF/spring.handlers 1http\\://autoconfig.jd.com/schema/autoconfig=com.jd.autoconfig.AutoConfigNamespaceHandler AutoConfigNamespaceHandler.java 123456789101112131415161718192021222324252627282930313233package com.jd.autoconfig;public class AutoConfigNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { this.registerBeanDefinitionParser(\"enable-autoconfig\", new SwitchParser()); this.registerBeanDefinitionParser(\"ucc-provider\", new UccProviderParser()); } static class SwitchParser implements BeanDefinitionParser { @Override public BeanDefinition parse(Element element, ParserContext parserContext) { RootBeanDefinition beanDefinition = new RootBeanDefinition(); beanDefinition.setBeanClass(AutoConfigManager.class); return beanDefinition; } } static class UccProviderParser implements BeanDefinitionParser { @Override public BeanDefinition parse(Element element, ParserContext parserContext) { String id = element.getAttribute(\"id\"); String path = element.getAttribute(\"path\"); RootBeanDefinition beanDefinition = new RootBeanDefinition(); beanDefinition.setBeanClass(UCCProvider.class); beanDefinition.getPropertyValues().add(\"path\",path); parserContext.getRegistry().registerBeanDefinition(id, beanDefinition); return beanDefinition; } }}","categories":[],"tags":[{"name":"spring","slug":"spring","permalink":"https://billxc.github.io/tags/spring/"}]},{"title":"多线程文件下载的一种思路","slug":"MISC/多线程文件下载的一种思路","date":"2018-03-20T00:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/03/20/MISC/多线程文件下载的一种思路/","link":"","permalink":"https://billxc.github.io/2018/03/20/MISC/多线程文件下载的一种思路/","excerpt":"","text":"最近在尝试用Go写一个百度网盘的第三方客户端,其中涉及到了网盘文件的下载,虽然用简单粗暴的对接http流和文件流就可以做到下载,但下载速度总是不尽如人意而且听上去也不够高端。但如果要调用aria2等专业下载工具的话又显得太笨重,于是就想着自己实现一份简单的多线程下载的工具。 基本思路文件在下载开始的时候就预先分配好空间。在开始下载的时候给定一个下载线程的数量,每个线程负责一块文件的大小,使用HTTP头中的Range字段请求文件的局部数据,使用随机random access写入文件内容。 虽然使用了多线程下载,在有N条线程的情况下,理论上下载时间会变为1/N,但不可避免地,会有那么一条或者多条线程的下载速度特别慢,最终导致整体下载时间的延长。 解决方案也很容易就能想到,在其他的线程下载完成后,去帮助没有下载完成的线程去进行下载。虽然是个很简单的思路,但实现的时候却有很多因素需要考虑。 实现细节如何切分任务虽然说着切分下载任务,但文件不像蛋糕,可以切出一块来,因此需要一个切分文件的方案。我的方案是定义一个最小的文件块,我定了1MB,然后计算文件的块的数量,再去将这些个文件块分给各个下载线程 如何避免多个线程同时写造成的异常虽然在我自己的实验中没有发生,但是理论上会出现两个线程同时往文件中写数据,并且产生异常的情况。我想到了两个解决方案: 为写入文件加一个全局的锁,并且只有一个文件输出流,每当线程想要写文件的时候,就需要去获得文件的锁,然后使用唯一的文件输出流,往文件中写。 下载线程不负责写文件,文件写由另外一个线程完成,写线程维护一个队列,下载线程获得数据内容后,将数据放进队列中,而写线程从队列中读取数据,写到文件中 方法1的缺点在于,下载线程在等待锁的过程中什么事情都做不了,在网络速度较快的情况下可能影响下载速度,但胜在简单粗暴。而方法2的缺点也很明显,逻辑复杂,需要维护一个额外的和缓存队列。 我选择了方法1,毕竟我家的小水管还不会让磁盘速度成为下载的瓶颈。 如何设计算法,进行线程调度这一点是我认为最复杂的一点。首先调度器必须要: 知道当前文件的下载情况 知道所有线程的下载状态,HTTP请求的的启止点,当前的工作点 在得知以上信息后,选取合适的新的下载起止点 在选定起止点后,通知原有线程,任务内容有了变更 调度算法应该尽量的复用已有的HTTP连接,而不是优先去新建连接 第一点其实问题不大,可以通过维护一个全局的数据来实现,用不同的标志位来表示对应的文件块当前的下载情况。在有些调度算法中甚至不需要这个数据 第2、3、4、5点可以认为是一个完整的解决方案,分开来谈意义不大,有些解决方案并不需要其中一些数据。下面我给出我想到的几种实现方向 先插入一点,因为下载是时刻在进行的,所以调度算法可能在前一秒算出来的结果后一秒就过时了,因此我使用了一个全局的开关,所有线程都会去检查开关,当线程发现正在进行调度计算的时候,在完成当前文件块的的下载后,停止接收新的下载任务,等调度完成后继续下载。下面的讨论,如无特殊说明,均是在这个前提下的。 方案一 第一种方案也是我从最初实现衍生而来的。文件先等分为N份,分配给N个线程,每个线程知道自己的起点和终点,但并不会感知到其他线程的存在。 其他线程的在有一个线程完成下载后,线程自己去遍历文件的下载状态,找到最长的一块未被下载的空间,开始进行自己的下一次下载。 那么其他线程怎么知道自己的任务被抢了呢?每次线程拿任务前,都需要去检查要下载的下一个块有没有已经被他人抢先标记为下载中或者已下载,如果是,则认为本次下载完成。进入下载完成的处理逻辑 方案二 在思考的时候,先抛弃线程复用这个概念。将N个线程,改变为N个下载的资格,并且调度器持有所有线程的引用,知道所有线程当前的状态。 当一个线程下载完成后,下载资格就出现了空位,那么调度器只需要找到一个未完成任务最多的线程,对线程进行分裂,分裂成两个单独的线程,将旧有线程的下载等分,新线程使用空缺出来的下载资格。 这个方案不需要掌握整体的下载进度,因为在最初,文件已经被分到了所有的线程中,而我们之后的每一次分裂操作,都只是分裂了已有的下载任务,不会造成下载的缺失或重复 OK,我们的思路已经有了,我们再思考之前说的,线程真的不可以复用吗?答案当然是可以复用。分裂操作完全可以替换成两个旧有线程的状态变化。 其他的细节 识别不支持部分文件请求的URL,表现为,使用header中的range字段后,服务器返回的HTTP状态码不是206,而是200 对于有多次跳转的下载链接,应该先完成链接的挑战,再进行多线程的下载 加入任务拆分的阈值,如果当前任务已经小于拆分的阈值,则不进行拆分 由于网络原因,其中一个线程可能会下载失败,需要优雅地处理网络异常,并且尽可能继续完成下载工作 存在的继续改善的点 断点续传 很多现代的下载工具都具有断点续传的功能,但这个工具暂时还不具备","categories":[{"name":"杂记","slug":"杂记","permalink":"https://billxc.github.io/categories/杂记/"}],"tags":[{"name":"多线程","slug":"多线程","permalink":"https://billxc.github.io/tags/多线程/"},{"name":"Golang","slug":"Golang","permalink":"https://billxc.github.io/tags/Golang/"}]},{"title":"Hello Hexo","slug":"hello-world","date":"2018-03-20T00:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"2018/03/20/hello-world/","link":"","permalink":"https://billxc.github.io/2018/03/20/hello-world/","excerpt":"","text":"跟风用了Hexo搭自己的博客,偶尔在上面做一些笔记。 以前写的一些笔记也迁移了过来,创建时间一律设成了很早的时间,以示区分。一些老的笔记由于格式问题暂时还没有整理完毕。","categories":[],"tags":[]},{"title":"Hive学习笔记","slug":"Hive/Hive学习笔记","date":"1999-03-19T15:38:49.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"1999/03/19/Hive/Hive学习笔记/","link":"","permalink":"https://billxc.github.io/1999/03/19/Hive/Hive学习笔记/","excerpt":"","text":"MISC Hive不支持插入单条语句,只支持2种批量插入。从文件读取数据,或者从别的表读取数据 Hive 表的种类 内部表 12CREATE TABLE workers( id INT, name STRING) ROW FORMAT DELIMITED FIELDS TERMINATED BY '\\054'; 分区表 123create table partition_employee(id int, name string) partitioned by(daytime string) row format delimited fields TERMINATED BY '\\054'; 分区表可以用来加速查询,不同分区的数据会存储在hdfs不同的文件夹中 分区支持多级分区 桶表 外部表 数据并非由Hive存储(例如数据存储在Hive上) Hive表分区 为什么要分区 Hive表大多数以文件的形式存储在磁盘上,","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://billxc.github.io/categories/学习笔记/"}],"tags":[]},{"title":"ES的聚合","slug":"ES_learning/aggressions(聚合)","date":"1996-10-01T00:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"1996/10/01/ES_learning/aggressions(聚合)/","link":"","permalink":"https://billxc.github.io/1996/10/01/ES_learning/aggressions(聚合)/","excerpt":"","text":"scripted-metric-aggregationabstract可以执行脚本,并且返回结果,可用于复杂查询 只有map_script 允许的返回值- primitive types - String - Map (containing only keys and values of the types listed here) - Array (containing elements of only the types listed here) DEMO12345678910111213141516POST ledger/_search?size=0{ \"query\" : { \"match_all\" : {} }, \"aggs\": { \"profit\": { \"scripted_metric\": { \"init_script\" : \"params._agg.transactions = []\", \"map_script\" : \"params._agg.transactions.add(doc.type.value == 'sale' ? doc.amount.value : -1 * doc.amount.value)\", \"combine_script\" : \"double profit = 0; for (t in params._agg.transactions) { profit += t } return profit\", \"reduce_script\" : \"double profit = 0; for (a in params._aggs) { profit += a } return profit\" } } }} docs init_script 在找到文档之前就执行,一般会用来进行值的初始化 Executed prior to any collection of documents. Allows the aggregation to set up any initial state. In the above example, the init_script creates an array transactions in the _agg object. map_script 每次找到拿到一个文档就会执行。这是一个必须的参数。如果没有定义combine_script,则需要吧结果放在_agg这个对象中 Executed once per document collected. This is the only required script. If no combine_script is specified, the resulting state needs to be stored in an object named _agg. In the above example, the map_script checks the value of the type field. If the value is sale the value of the amount field is added to the transactions array. If the value of the type field is not sale the negated value of the amount field is added to transactions. combine_script 在每个分片上执行一次,在分片上完成文档收集后执行。允许聚合巩固每个分片上返回的状态。如果combine_script没有提供,则默认返回聚合对象 Executed once on each shard after document collection is complete. Allows the aggregation to consolidate the state returned from each shard. If a combine_script is not provided the combine phase will return the aggregation variable. In the above example, the combine_script iterates through all the stored transactions, summing the values in the profit variable and finally returns profit. reduce_script 在所有的分片都返回结果后会执行,这个脚本可以访问所有的aggs对象列表。若没有这个脚本,则直接返回_aggs对象 Executed once on the coordinating node after all shards have returned their results. The script is provided with access to a variable _aggs which is an array of the result of the combine_script on each shard. If a reduce_script is not provided the reduce phase will return the _aggs variable. In the above example, the reduce_script iterates through the profit returned by each shard summing the values before returning the final combined profit which will be returned in the response of the aggregation. Filter AggregationeditabstractDefines a single bucket of all the documents in the current document set context that match a specified filter. Often this will be used to narrow down the current aggregation context to a specific set of documents. 将搜索结果根据fitler分类 Example: POST /sales/_search?size=0{ “aggs” : { “t_shirts” : { “filter” : { “term”: { “type”: “t-shirt” } }, “aggs” : { “avg_price” : { “avg” : { “field” : “price” } } } } }}COPY AS CURLVIEW IN CONSOLEIn the above example, we calculate the average price of all the products that are of type t-shirt. Response: { … “aggregations” : { “t_shirts” : { “doc_count” : 3, “avg_price” : { “value” : 128.33333333333334 } } }} linkhttps://www.elastic.co/guide/en/elasticsearch/reference/6.1/search-aggregations-metrics-scripted-metric-aggregation.html","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://billxc.github.io/categories/学习笔记/"}],"tags":[{"name":"ES","slug":"ES","permalink":"https://billxc.github.io/tags/ES/"}]},{"title":"HQL优化","slug":"Hive/HQ优化","date":"1996-10-01T00:00:00.000Z","updated":"2022-09-12T04:18:03.613Z","comments":true,"path":"1996/10/01/Hive/HQ优化/","link":"","permalink":"https://billxc.github.io/1996/10/01/Hive/HQ优化/","excerpt":"","text":"根本思想 尽早过滤数据,减少每个阶段数量 减少job数量 解决数据倾斜 过滤数据列裁剪只查询必要的列,不被查询的列可以不被访问,提升性能 分区裁剪减少不必要的分区 利用Hive的优化机制减少job数量out join inner join,如果是join的key相同,不论表的数量,都会合并为一个mapReduce任务 job的输入输出优化善用muti-insert, union all,不同表的union all相当于multiple inputs,同一个表的union all,相当map一次输出多条 1234567891011insert overwrite table tmp1select … from a where 条件1; insert overwrite table tmp2 select… from a where 条件2; 扫描两次a表from ainsert overwrite table tmp1 select… where 条件1insert overwrite table tmp2 select… where 条件2; 只扫描一次a表 Join优化避免笛卡尔积123456789select … from woa_all_device_info_his Aleft outer join ( select * from woa_all_info_his B where (B.mobile <> ‘unknown’ or B.imsi <> ‘unknown’) and B.imei <> ‘unknown’ and B.pt = ‘$data_desc’) Con A.app_id = C.app_id and A.imei = C.imei 数据过滤在join前过滤掉不需要的数据 小表放前大表放后原则在编写带有join操作的代码语句时,应该将条目少的表/子查询放在join操作符的左边 因为在Reduce阶段,位于join操作符左边的表的内容会被加载进内存,载入条目较少的表可以有效减少OOM。所以对于同一个key来说,对应的value值小的放前,大的放后。 map join 来避免数据倾斜数据倾斜一般是由于代码中的join或group by或distinct的key分布不均导致的 Join算法一般有两种- (map side join) replication join:把其中一个表复制到所有节点,这样另一个表在每个节点上面的分片就可以跟这个完整的表join了 - (reduce side join) repartition join:把两份数据按照join key进行hash重分布,让每个节点处理hash值相同的join key数据,也就是做局部的join 合理使用left semi joinleft semi 是比 In exist 效率高的一种方式 合理使用动态分区","categories":[{"name":"学习笔记","slug":"学习笔记","permalink":"https://billxc.github.io/categories/学习笔记/"}],"tags":[{"name":"Hive","slug":"Hive","permalink":"https://billxc.github.io/tags/Hive/"},{"name":"HQL","slug":"HQL","permalink":"https://billxc.github.io/tags/HQL/"}]}]}