
暂无个人介绍
linux的作业要自己编译一个mysql,但是我的系统上已经有一个mysql了,再编译一个,最麻烦的地方不在于编译,而是让两个共存。 前面的编译环节和普通安装没有什么区别,还是configure,make,make install。其中configure的时候,选择一下安装的位置,否则可能导致原来的文件被覆盖(使用–prefix选项)。我的破电脑make的时候花了不少时候(具体多少我也不清楚,开始编译之后就去吃饭了),安装好之后,按照mysql的手册复制support-files/my-medium.cnf到安装目录里面的var目录下(没有安装数据库的时候这个文件夹不存在,自己在安装根目录下创建一个),然后修改里面的客户端通信使用的socket文件的名字sed -i -e ‘s/mysql.sock/mysql1.sock/’ /opt/mysql/var/my.cnf,这里把socket文件改成了mysql1.sock(原来的mysql默认的socket文件是mysql.sock,不修改会导致两个socket文件冲突)。如果要同时通过网络监听,可能还需要修改里面的监听端口,反正我只是当成作业演示一下,所以监听端口就没有修改。 后面的操作和手册上一样,偷懒了下,没有导出环境变量,所以要输入绝对路径。先改变了文件夹的拥有者,然后就用mysql_install_db –user=mysql创建了数据库(这里要防止使用原来PATH下的mysql程序)。最后启动mysql,mysqld_safe –user=mysql &(还是一样,注意不要用原来mysql的程序)。 这时,新的mysql已经启动,可以用mysql命令连接了(好像这里还是要用新的那个),创建了个数据库,测试了一下一切ok,没有对以前的mysql有影响。 关键点就是mysql的配置文件my.cnf文件要复制到mysql安装目录的var文件夹下(mysql用来保存数据库文件的文件夹),这样这个配置文件只对该服务器有效,不会影响到系统中默认的/etc下的那个配置文件。为了防止两个mysqld的监听冲突,最好修改配置文件里面的socket和监听端口(如果通过网络连接)。 在ubuntu上配置出现问题了。ubuntu好像不会用新的配置文件替换全局配置文件(mysql读取配置文件顺序是/etc/my.cnf->数据库安装目录/my.cnf->个人主目录/.my.cnf)所以在创建数据库(使用mysql_install_db)时,仍然需要指定my.cnf文件路径(使用–defaults-file选项)。在启动数据库时也同样需要。使用mysql命令连接数据库时,也需要指定连接的socket文件,或者my.cnf文件。 如果在安装数据库时出现unknown option ‘–skip-federated’,直接把自己的那份my.cnf文件里的skip-federated行注释。 转载自:https://coolex.info/blog/59.html
在看linux内核源代码的时候,经常在一些结构里看见struct list_head结构。找了一下源代码,在list.h中,有对这个结构的定义,这个就是linux内核中的链表结构。 仔细看看这个结构,就可以发现它和以前在讲数据结构的时候的链表有很大的差别——没有数据。list_head结构中仅仅包含了两个自己结构的指针,用来组建双向循环链表。最大的疑问就是,这个链表结构如何保存数据呢? 在list.h中,定义了list_entry宏。这个宏就是用来提取包含链表项的结构的指针。从list_entry宏的定义可以看到,它仅仅调用了kernel.h中的container_of()宏。后者真正实现了通过链表结构来获取包含链表的结构的地址(指针)。 container_of(ptr, type, member)宏需要三个参数。ptr:指向链表的指针;type:包含链表项结构的类型;member:链表项在结构中的名称。 其中的实现有两句语句: const typeof( ((type *)0)->member ) *__mptr = (ptr); //声明临时变量__mptr,储存链表的指针。这个变量的类型由gcc扩展函数typeof从结构的成员变量member中获取 //((type *)0)->member将地址0强制转换为结构的指针,并获取链表的对象 (type *)((char*) __mptr – offsetof(type, member)); //将上句获取到的地址减去链表项在结构中的偏移量,得到结构的真实地址,并将这个地址转换成所需类型结构的指针 //offsetof(type, member)函数定义在stddef.h中,它返回member在结构type中的偏移量,返回值为size_t 通过这宏,就返回了包含链表项的结构的指针,然后就可以通过这个指针来访问结构中的数据了。这样定义的链表,对于没有模板的c语言来说,可以有效的避免重复写很多包含不同数据类型的结构,不用在为每种不同的数据类型写一个链表节点项了。 (参考文章:http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html) 转载自:https://coolex.info/blog/75.html
vmime对邮件格式和邮件协议做了很好的封装,使用起来还是非常方便的。 vmime对于邮件协议都封装在vmime::net名字空间中,主要要用到的对象,有: vmime::net::session,主要用于维护和服务器之间的连接 vmime::net::store,表示一个邮件存储,这是一个基类,没种邮件协议都有自己的store(如POP3Store,IMAPStore) vmime::net::folder,表示邮件存储上的文件夹,和store一样,每种邮件协议,都有自己的folder实现 vmime::net::message,表示一封网络邮件,和vmime::message不同,vmime::net::message可能只有邮件的一部分,如邮件头等信息(由使用的邮件协议决定)。 vmime会根据session中设置的邮件协议,创建对应的store。 一些常用操作的实现(POP3协议): 连接邮箱: vmime::utility::ref<vmime::net::session> session = vmime::create<vmime::net::session>(); //创建session vmime::utility::ref<vmime::net::store> store = session->getStore(vmine_url); //获得store store->connect();//连接 vmime::utility::ref<vmime::net::folder>folder = store->getDefaultFolder();//创建folder,路径是默认路径(inbox) folder->open(vmime::net::folder::MODE_READ_WRITE);//以读写的形式打开 获取邮件: std::vector<vmime::utility::ref<vmime::net::message> > allMessages = folder->getMessages(); folder->fetchMessages(allMessages, vmime::net::folder::FETCH_ENVELOPE); //获取所有邮件的头部信息,包含sender, recipients, date, subject vmime::string mailContent; vmime::utility::outputStreamStringAdapter out(mailContent); resultMsg->extract(out); //找到需要的邮件后,下载到本地,保存到string中,这里vmime::string是std::string的typedef 删除邮件: folder->deleteMessage(resultMsg->getNumber()); //执行删除指令 folder->close(true);//关闭文件夹,真正对邮件进行删除 使用当中出现的问题: 按照vmime-book中的例子,在获取邮件的时候,增加了vmime::net::folder::FETCH_FLAGS标签后,会抛出异常,提示不支持该操作。 还有执行了folder->deleteMessage函数之后,邮件没有真正删除。通过抓包和查看源代码后发现,deleteMessage函数是对邮件服务器发送了DELE指令,但是邮件服务器不会立即执行,需要QUIT之后才会真正的删除。而在folder的析构函数中,调用的是folder->close(false)函数来关闭文件夹的,这样在发送QUIT命令之前,会向邮件服务器发送一个RSET命令,将已经被标记为删除的邮件状态充值,所以不会真正的删除邮件。目前只有在执行了删除命令后,显式执行close(true)函数,确保马上发送QUIT命令,让服务器删除邮件。 上述命令真正执行的POP3命令为: #连接 USER xxx #用户名 PASS xxx #密码 STAT #查询邮件数量和大小 TOP 1 0 #查看序号为1的邮件的头部 RETR 1 #接受第一封邮件的所有内容 DELE 1 #删除第一封邮件 QUIT #退出,服务器执行删除操作 转载自:https://coolex.info/blog/108.html
前文写了使用vmime库通过POP3协议收取邮件,继续写对邮件的解析。 解析邮件相对比较简单,需要将收取的邮件,重新从字符串转换成vmime::message格式,然后就可以获取到自己需要部分的内容了。 首先将vmime::string格式转换为vmime::message: vmime::utility::ref<vmime::message> mail = vmime::create<vmime::message>(); mail->parse(mailContent); vmime还提供了一个简单的帮助类vmime::messageParser方便对message进行解析。 message主要包含了邮件头和邮件内容,内容又因为multi-part的邮件格式规定,被拆分成了多个vmime::textPart。通常使用到的textPart的子类,有vmime::htmlTextPart和vmime::plainTextPart,分别对应邮件body中的content-type为text/html和text/plain。 代码: vmime::messageParser mp(mail); for (int i = 0; i < mp.getTextPartCount(); ++i) //遍历所有的textPart { vmime::utility::ref<const vmime::textPart> text = mp.getTextPartAt(i); if (text->getType().getSubType() == vmime::mediaTypes::TEXT_HTML) //text/html { vmime::utility::ref<const vmime::htmlTextPart> htmlText = text.dynamicCast<const vmime::htmlTextPart>(); vmime::utility::outputStreamStringAdapter htmlOut(htmlContent); vmime::utility::charsetFilteredOutputStream utf8Out(htmlText->getCharset(), vmime::charset(“utf-8″), htmlOut); //强制转换正文为utf8编码 htmlText->getText()->extract(utf8Out); utf8Out.flush(); } else if (text->getType().getSubType() == vmime::mediaTypes::TEXT_PLAIN) //text/plain { vmime::utility::ref<const vmime::plainTextPart> plainText = text.dynamicCast<const vmime::plainTextPart>(); vmime::utility::outputStreamStringAdapter plainOut(plainTextContent); vmime::utility::charsetFilteredOutputStream utf8Out(plainText->getCharset(), vmime::charset(“utf-8″), plainOut); plainText->getText()->extract(utf8Out); utf8Out.flush(); } } 对于html个是的邮件正文,还可以遍历获取里面的embeddedObject,如嵌入的附件图片等,不过目前没有这样的需求,就没有去尝试了。 在真正执行的时候,又发现了一个问题,必须在开始使用前,调用vmime::platform::setHandler<vmime::platforms::posix::posixHandler>();设置平台相关的handler,这里设置的是符合posix的平台,windows貌似也有对应的handler。 转载自:https://coolex.info/blog/113.html
以前对xslt的了解,主要是通过docbook,知道在将docbook的xml转换成html之前,可以自定义一份xsl文件,对xslt过程中的参数进行设置。这次,因为要把一个单元测试结果xml转换为html,第一次自己写了xsl文件。 首先纠结的是版本。发现w3cschool上的xslt教程是基于xslt1.0的,最初考虑可能需要生成多个html,xslt1.0没有这个功能,当时的设计就是一个输入对应一个输出。多文件输出是被定义在xslt1.1以后,或者还有个exsl的。最终还是选择了2.0版本的,但真正使用的,还都是1.0版本定义的标签,貌似libxsl也不支持2.0的。 其他的语法,和xml没有区别,只是定义了一系列的标签而已。xsl相关的标签,都是定义在xsl名字空间下的。所以需要在最外层标签上声明名字空间:xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”。xsl文件最外层标签为<xsl:stylesheet version=”2.0″ xmlns:xsl=”http://www.w3.org/1999/XSL/Transform”>定义了整个stylesheet,针对要生成的html,可以增加<xsl:output method=”html” encoding=”utf-8″ />这个标签,这样在生成的html中,会加上meta信息,定义content type和charset。 最常用的几个标签,基本就是xsl:template、xsl:apply-templates、xsl:value-of这些了。其中xsl:template定义了匹配到某个元素后进行的操作。比如最开始,当匹配到/节点的时候,我们需要输出html的最外层标签和头信息,就可以这样写: <xsl:template match=”/”> <html> <head> <title>结果报告</title> </head> … 为了xsl文件的可读性,一般会把标签分离出来单独用xsl:template定义转换的内容,所以需要使用xsl:apply-templates标签,这个标签定义了在此处,需要调用其他的转换模版。如: <xsl:template match=”/”> <html> <head> <title>结果报告</title> </head> <body> <ol> <xsl:apply-templates /> </ol> </body> </html> </xsl:template> 这样,处理匹配到/节点的模版,只是定义了html头,其他内容会由其他的模版进行定义。 xsl:value-of标签,是从处理的xml中取值。它的使用方法非常简单,就是通过定义xpath,将xpath找到的元素或者属性的值取出来。如: <li> <xsl:value-of select=”@name” /> <ol> <xsl:apply-templates select=”TestMethod” /> </ol> </li> 这里会取出当前节点的name属性的值。 如果要给html的元素添加属性,这个属性的值是动态的(由xml中的元素控制),就不能直接在结果里面用xsl:value-of取值,需要用过xsl:attribute标签来定义一个属性。如: <span > <xsl:attribute name=”class”> <xsl:choose> <xsl:when test=”@status = ‘PASS'”>pass</xsl:when> <xsl:when test=”@status = ‘SKIP'”>skip</xsl:when> <xsl:when test=”@status = ‘FAIL'”>fail</xsl:when> <xsl:otherwise>fail</xsl:otherwise> </xsl:choose> </xsl:attribute> 这里通过当前节点的status属性值作为判断,如果值为PASS,则给转换结果的span标签增加一个class=”pass”的属性,这样转换之后的结果,为: <span class=”pass”> 目前就基本使用了这几个标签完成了对xml的转换,要让浏览器能够直接进行转换,只要在原xml中增加: <?xml-stylesheet type=”text/xsl” href=”xxx.xsl”?> 就可以了。这样浏览器在打开xml的时候,就会进行xslt,展示出html页面了。 要在java代码中增加: handler.processingInstruction(“xml-stylesheet”,”type=\”text/xsl\” href=\”users.xsl\””); 这行代码就可以了。 转载自:https://coolex.info/blog/144.html
听说开源驱动现在已经不错了,就试着把系统里面的闭源驱动(fglrx)切换成开源驱动。 首先,先卸载闭源驱动:emerge -C x11-drivers/ati-drivers,因为现在用的xorg.conf是通过ati的命令生成的,先干掉。 然后,在make.conf中的VIDEO_CARDS环境变量中,将原来的fglrx改成radeon,重新emerge xorg-server和libdrm。 为了开启kms,需要重新改下内核,具体的做法可以参照官方的文档。需要注意的是,要将原来的framebuffer驱动都去掉(Device Drivers->Graphics support->Support for frame buffer devices中的所有驱动),然后将Device Drivers->Graphics support->Direct Rendering Manager和它下面的ATI Radeon和Enable modesetting on radeon by default这两个子项编译入内核(启动Enable modesetting on radeon by default是ATI Radeon的子项)。对于radeon,还有特别重要的一点,就是要将固件编译入内核。首先要确保已经安装了x11-drivers/radeon-ucode这个包,然后在内核中,Device Drivers->Generic Driver Options中选中Include in-kernel firmware blobs in kernel binary,在下面的External firmware blobs to build into the kernel binary中,填入radeon/R600_rlc.bin radeon/R700_rlc.bin(我的显卡是HD 3400 Series),在Firmware blobs root directory中填入/lib/firmware,然后就可以重新编译内核了。 重新编译、安装完内核之后,就是要编辑grub.cfg文件了,去掉原来为splash使用的initrd,直接使用新内核。 重新启动,可以看见字符界面中,已经自动使用了1280×800的分辨率。不过进入kde之后,图形界面非常卡,kde还因为速度慢自动关闭了混成特效。后来发现是mesa没有重新emerge,重新安装了mesa然后重新登陆,原先kwin的效果仍然可以使用,大功告成。 转载自:https://coolex.info/blog/151.html
很早之前就听说spice相对vnc来说要强大很多,之前因为安装的是32位的系统,没法进行尝试,安装了64位的系统之后,还没时间去折腾过。 上上周其实已经将以前的windows虚拟机的图形方式从vnc换成了spice,昨天又搞定了agent的启动和远程复制粘贴。 先说下安装,安装的过程相对比较方便,首先需要卸载原来安装的app-emulation/qemu-kvm,因为之后要安装的拥有spice功能的kvm和这个包是相互block的。卸载了之后,就需要重新emerge app-emulation/qemu-kvm-spice这个包。否则直接在virt-manager中将虚拟机的显示从vnc切换到spice,在启动的时候会提示这个版本的kvm不支持spice协议。其他和spice相关的包还有:app-emulation/spice,app-emulation/spice-protocol等,如果要在virt-manager中直接接入到spice界面,需要安装net-misc/spice-gtk这个包(不过我在kde中用的时候,发现用net-misc/spice-gtk的时候,会导致整个界面都变成透明的,估计是因为gtk和kwin的特效不太兼容)。 安装还是非常简单的,安装完成之后,就要重新配置原来的windows虚拟机了。在virt-manager中删掉原来的vnc显示,新增一个graphics,类型选择spice server。将原来视频中使用的虚拟显卡换成qxl,这样启动虚拟机之后,就会使用spice了。 前面说到了kde中使用spice-gtk有很多不正常的地方,所以我是直接使用spicec这个命令的,因为没有配置任何的加密,所以连接非常简单,直接使用: spicec -h 127.0.0.1 -p 5900 端口号可以在增加spice server的时候设置,如果选择自动分配,那么会从5900开始递增分配。 启动之后,需要给里面的windows安装驱动,windows需要的所有二进制文件,都可以在http://spice-space.org/download.html这里的Windows binaries中找到。首先下载qxl driver,在windows提示需要安装驱动文件的时候,安装这个qxl driver,就可以完成了spice显示的功能了。 之前按照http://www.linux-kvm.com/content/rhev-spice-guest-drivers-released-windows这个页面安装windows驱动,前面安装qxl非常方便,但是后面的vdi port driver却一直没有提示需要安装,在最后一步Install SPICE agent之后,这个服务一直无法启动,第一次的尝试spice就到此为止了。 之后又继续搜索了下这个spice agent无法启动,在redhat的bugzilla中发现原来现在sprice agent已经不通过vdi了,需要通过virtio-serial来进行交互。在virt-manager的界面上,怎么样都找不到如何添加这个设备的方法,最后只能通过编辑虚拟机配置文件的方式直接修改了。通过使用virsh edit XXX (XXX为虚拟机的名字),会打开虚拟机的配置文件,根据libvirt网站上的介绍,在devices标签中增加了两个channel: <channel type=’pty’> <target type=’virtio’ name=’arbitrary.virtio.serial.port.name’/> <address type=’virtio-serial’ controller=’0′ bus=’0′ port=’1’/> </channel> <channel type=’spicevmc’> <target type=’virtio’ name=’com.redhat.spice.0’/> <address type=’virtio-serial’ controller=’0′ bus=’0′ port=’2’/> </channel> 保存这个配置文件,启动虚拟机,这个时候windows又会提示发现了新硬件。需要使用spice下载页面上的Windows virtio-serial driver进行安装。安装完成之后,重新安装下spice agent,这个时候spice agent服务就能够正常启动了。通过这个服务,虚拟机就能够和宿主机共享剪切板了。 转载自:https://coolex.info/blog/160.html
今天终于把端午写的代码在windows上(mingw)编译过了。过程还是比较艰辛的,这里记录下(之前boost库的编译,可以参考前面的博客): 1、cmake查找系统上的boost库,之前直接失败,原因是FindBoost这个文件只支持到最高1.46.0版本,自己编译安装的boost版本是1.46.1。修改了这个文件支持的boost版本之后,能够找到目录了,但是boost thread和boost system都没法找到。解决方法是在cmake文件中添加: if(WIN32) set(Boost_USE_STATIC_LIBS ON) set(Boost_NO_SYSTEM_PATHS ON) set(Boost_USE_STATIC_RUNTIME ON) endif(WIN32) 之后,让boost去强制寻找静态库(之前编译boost只编译了静态库) 2、boost asio库在windows中链接的时候,需要指定链接win32 socket相关的库。mingw没有vs这样能够根据静态链接库自动去链接它依赖的库,所以在最终链接成可执行程序的时候,会报undefined reference。解决方法在cmake中添加: if(WIN32) find_library(MSwsock mswsock) target_link_libraries(http_static MSwsock) target_link_libraries(http MSwsock) find_library(WS2_32 ws2_32) target_link_libraries(http_static WS2_32) target_link_libraries(http WS2_32) ADD_DEFINITIONS(-DBOOST_THREAD_USE_LIB) endif(WIN32) 这样当编译环境在windows中时,会自动链接mswsock和ws2_32两个库。 3、boost thread库的静态链接。boost thread库在最后链接的时候,也会报有些符号找不到,差了半天原因,是因为boost在试图链接thread库的动态链接库。因为不想让这个程序在其他windows机器上跑要复制dll,解决方法前面已经有了,就是添加ADD_DEFINITIONS(-DBOOST_THREAD_USE_LIB)这个宏定义,强制boost寻找静态链接库。 4、静态链接mingw的gcc和stdc++库。之前编译完的程序,放到其他机器上还是会报有libgcc相关的动态链接库找不到,因为mingw默认是将内置的gcc和c++库做动态链接的。要修改这个,需要在编译的时候给编译器传递-static-libgcc -static-libstdc++这两个参数,在cmake中可以这样修改CMakeLists.txt文件: if(WIN32) set(CMAKE_CXX_FLAGS “-static-libgcc -static-libstdc++”) endif(WIN32) 转载自:https://coolex.info/blog/169.html
csv文件的结构很简单,最基本的规则,就是用逗号分隔每一个单元格,用换行( 或者 )分隔每一列。其中需要注意的就是双引号为特殊的转义字符。详细的csv文件格式定义,在rfc4180中,主要的定义为: file = [header CRLF] record *(CRLF record) [CRLF] header = name *(COMMA name) record = field *(COMMA field) name = field field = (escaped / non-escaped) escaped = DQUOTE *(TEXTDATA / COMMA / CR / LF / 2DQUOTE) DQUOTE non-escaped = *TEXTDATA COMMA = %x2C CR = %x0D DQUOTE = %x22 LF = %x0A CRLF = CR LF TEXTDATA = %x20-21 / %x23-2B / %x2D-7E 根据这个定义,用状态机的方法,定义状态的枚举: enum CSV_STATE { CELL_BEGIN,//单元格开始 CELL,//单元格 CELL_END,//单元格结束 QUOTATION_1,//第一个引号 QUOTATION_2,//第二个引号 QUOTATION_3,//第三个引号 ESCAPED_CELL, //转义的单元格 }; 之前画的状态迁移图没有保存,直接上代码: case CELL_BEGIN: if (*iter == ‘\”‘) //第一个引号开始 { _state = QUOTATION_1; } else if(*iter == ‘,’)//逗号,一个空的单元格 { row._rowData.push_back(“”); _cell.clear(); } else//有中文,其他字符都保存 { _cell.push_back(*iter); _state = CELL; } break; case CELL: if(*iter == ‘\”‘)//rfc4180 未转义的单元格内不能出现引号 { //简单处理,直接抛出异常 throw std::runtime_error(“error parse csv format arround: ” + line); } else if(*iter == ‘,’) //逗号,改单元格结束 { row._rowData.push_back(_cell); //rfc4180规定单元格能够有空格,后面的空格暂时不管 _cell.clear(); _state = CELL_BEGIN; } else//因为有中文,不过滤rfc4180中排除的其他字符 { _cell.push_back(*iter); } break; case QUOTATION_1: if(*iter == ‘\”‘)//第二个引号 { _state = QUOTATION_2; } else { _cell.push_back(*iter); _state = ESCAPED_CELL; } break; case QUOTATION_2: if(*iter == ‘\”‘)//第三个引号,为引号的转义 { _cell.push_back(*iter); _state = ESCAPED_CELL; } else if(*iter == ‘,’)//没有第三个引号,单元格结束 { row._rowData.push_back(_cell); _cell.clear(); _state = CELL_BEGIN; } else//其他字符,全部忽略 { row._rowData.push_back(_cell); _cell.clear(); _state = CELL_END; continue; } break; case CELL_END: if(*iter == ‘,’)//忽略其他字符,遇到逗号,表示新的单元格开始了 { _state = CELL_BEGIN; } break; case ESCAPED_CELL: if(*iter == ‘\”‘) //第二个引号 { _state = QUOTATION_2; } else { _cell.push_back(*iter); } break; 每次读取一行进行解析,开始解析前,初始状态是CELL_BEGIN,解析完成后,有两种可能的状态: 1、处于CELL或者QUOTATION_2状态,因为最后一个单元格是没有逗号的,解析完成后,将最后一个单元格的数据push_back进去 2、处于ESCAPED_CELL状态,因为转义的单元格中间是可以包含换行的,所以需要返回false,继续下一行的读取 3、处于其他状态,应该是不可能的, if(_state == ESCAPED_CELL) //转义单元格可以包含换行,还在这个单元格,需要继续解析下一行 { _cell.push_back(‘\n’);//getline把本来应该存在的换行吃了,补回去 return false; } else if (_state == CELL || _state == QUOTATION_2) //最后一个单元格,保存下 { row._rowData.push_back(_cell); _cell.clear(); _state = CELL_BEGIN; } 这里做的和rfc文档不同的是,rfc定义的文本字符,是 %x20-21 / %x23-2B / %x2D-7E,但因为csv中可能包含中文,所以除了特殊字符(逗号和双引号)之外,没有再判断字符的有效性。 转载自:https://coolex.info/blog/171.html
杯具的写完代码才发现应用原来依赖的cglib使用了1.x的asm库,从最初使用3.x版本到2.x,然后使用1.x才搞定asm的兼容性。这里记录下不同版本如何读取annotation。 asm3.2: 这个版本非常方便,实现ClassVisitor接口,里面有个visitAnnotation方法,方法签名是: AnnotationVisitor visitAnnotation(String desc, boolean visible) ,其中desc是annotation的类型(Lxxx/xxx/xxx;),稍微处理下就可以将java byte code的表示形式变成Annotation的类名;visible表示该annotation是否运行时可见,因为我需要类加载后读取annotation的值,可以通过这个参数来判断。因为我只是用来获取annotation的名字,不需要读取annotation里面的key和value,所以直接返回null就可以了。如果要继续深入解析这个annotation,可以通过实现AnnotationVisitor接口并返回来解析。 asm2.2: 降到了这个版本,是运行时cglib库提示ClassWriter没有传入boolean参数的构造函数,这个应该算是asm升级到3.x之后没有做到向下兼容的一个地方。降级到asm2.2,修改的地方很少,就是ClassReader的accept函数,是否跳过debug信息标签,从int型的flag变成了个boolean型。读取annotation的方法没有变化。 asm1.5.3: 改用了2.2之后,还有运行时错误,木有找到CodeVisitor。只能降级到1.5.3了。降级到这个版本修改上面读取annotation的代码成本还是比较高的,这个版本里面ClassVisitor没有了visitAnnotation这个函数,而是把annotation当成了一个Attribute,所以只能通过visitAttribute函数。不过反而这个版本读取annotation官方有个文档:http://asm.ow2.org/doc/tutorial-annotations.html 当然我不需要这么复杂做动态代理什么的,只是读取annotation的名字。大体上就是,先通过visitAttribute函数,判断当前传入的Attribute是否是RuntimeVisibleAnnotations的实例,如果是就能够获取里面的公有变量annotations(类型是List<Annotation>)。我所需要的annotation名字,就在Annotation对象的type字段。这里的type也是bytecode中的类表示方法,如果需要获取能够直接被Class识别的类名,可以自己做简单的字符串处理,或者: String type = annotation.type; Type t = Type.getType(type); String cname = t.getClassName(); 这样来拿到className。 这次对向下兼容深有体会啊~~ 转载自:https://coolex.info/blog/192.html
gentoo安装软件的优势在于overlay具多,本来还不太清楚要怎么安装的,有了overlay一切就方便了。 首先要用overlay要安装layman,这个估计都已经安装了,修改下配置文件,增加自定义overlay地址: 在/etc/layman/layman.cfg文件中的overlays项里面增加: https://qt.gitorious.org/qt-labs/symbian-overlay/blobs/raw/master/repositories.xml 然后执行layman -a qt-symbian-overlay 添加了之后,就可以安装了,这个overlay中大致包含了几个东西: runonphone:用来直接将sis包在symbian手机上执行的脚本 qt-symbian-libs:qt的包,目前只有4.7.1的 s60-sdk:symbian s60开发包,目前版本是5.0 gcce:交叉编译器,用来生成arm架构的代码 安装之前,需要确认证书,s60-sdk和gcce都带有各自的license,在/etc/portage/package.license: >=dev-libs/s60-sdk-5.0-r1 nokia-eula >=sys-devel/gcce-4.4.172-r2 sourcery-g++ 这样就能够安装了。貌似安装s60-sdk的时候,需要依赖wine的,有一堆windows的东西。 安装完成后,可以在qt creator中配置刚刚安装的qmake,qt creator自己会识别出来。这个位置在/usr/share/qt4/qt-symbian/bin/qmake,或者有个软连接在/usr/bin/qmake-symbian,指向的也是刚刚那个位置。 安装好之后,就可以创建个项目试试了。在qt creator(我用的是2.3.1)中新建“qt控件项目->qt移动应用”,然后选择下工程创建的位置(注意:不知道为什么,工程必须和s60-sdk在同一分区!!),目标设置中选择塞班设备,后面就是针对设备的设置,选择之后qt creator会针对这些设置生成代码。 不过,runonphone貌似在我这里没有用,对生成的Makefile执行make sis之后,创建了symbian的sis包,但是runonphone无法执行,我的5800安装了trk之后,在电脑上还是没法从usb设备变成ttyUSB这样的设备,只能复制过去手工安装了。 对了,在安装自己应用的包之前,别忘了安装qt和s60-sdk中的依赖包。在/usr/share/qt4/qt-symbian/lib中有Qt.sis和QtWebKit.sis,即使手机上已经安装了qt,最好也用这个安装,否则可能会出问题。安装之前,可能还需要安装s60-sdk提供的包,在/usr/s60-sdk/nokia_plugin中,有:├── openc │ ├── Openc_new_api_guide.doc │ ├── s60opencreleasenotes.txt │ ├── s60opencsis │ │ ├── openc_glib_s60_1_6_ss.sis │ │ ├── openc_ssl_s60_1_6_ss.sis │ │ ├── pips_s60_1_6_ss.sis │ │ └── stdioserver_s60_1_6_ss.sis │ └── Symbian Redistribution and Use Licence v1.0 12.03.07 final.doc └── opencpp ├── s60opencppreleasenotes.txt └── s60opencppsis qt安装了之后,同时会安装对应的qt mobility,不过我还没有在本地安装后面用到再说了。 转载自:https://coolex.info/blog/198.html
条款4:非必要不提供default constructor 这里主要是列举下默认构造函数的优点和缺点。 如果没有默认构造函数,定义对象数组会比较麻烦,因为对象数组初始化的时候没法传递非默认构造函数的值,如果要使用,书中提到的方法是给数组每个变量初始化的时候调用构造函数,另一个就是使用指针数组。 第一个的缺点很明显,没法声明类似A a[10];这样的数组,在堆上申请,还得用到placement new这个之前没讲过的东西,另外还得一个个去初始化;后者的缺点当然是,数组里面的每个指针都需要记得去delete掉。 另外,一些模板容器也可能无法使用,因为一般模板容器为了通用,一般都会直接调用默认构造函数。 如果给这样的类增加默认构造函数,有个很打的缺点,就是类成员变量无法控制是否初始化,这样类函数可能会花大力气去判断每个变量是否都已经初始化。 所以我觉得这条还是要看这个类要怎么用,没有绝对的是否要提供默认构造函数。 条款5:对定制的“类型转换函数”保持警惕 自己定制“类型转换函数”的情况应该不多吧,这里主要记录下为什么要给类的构造函数前增加explicit关键字。 类似这样的代码:[cce lang=”cpp”] #include <iostream> class A { public: A(int a){_a = a;} bool operator==(const A &lhs) {return _a == lhs._a;} private: int _a; };int main() { A a(1); if(a == 1) std::cout << “equal”; else std::cout << “not equal”; return 0; } [/cce] 这段代码的输出是equal,因为在if(a == 1)的时候,编译器发现A类型的operator==只能比较A类型,刚好A又有一个参数为int的构造函数,就在这里对后面的1做了隐式转换,通过A(1)这个构造函数构造了A对象,然后调用A.operator==进行比较。 这样的隐式转换可能会存在风险,当不期望这样的转换,就需要在构造函数之前加上explicit关键字,禁止编译器进行隐式转换。如果加了explicit之后,编译会有这样的错误:test.cpp: 在函数‘int main()’中: test.cpp:15:10: 错误:‘operator==’在‘a == 1’中没有匹配 test.cpp:7:7: 附注:备选是: bool A::operator==(const A&) 转载自:https://coolex.info/blog/236.html
一般游戏涉及到的配置文件信息都以kv的形式存在且数量非常之少,这个时候搞一个sqlite的数据库显然有点奢侈,搞个配置文件合适一点,但是android的dev guide显然提供出来一套更合适的做法,SharedPreferences。 从存储的模式上来说 SharedPreferences实际上是系统专门提供的kv存储模式,针对不同的application提供私有的存储,虽说私有但是还是可以通过Provider来向别的程序提供这类私有数据。。好吧,我们不需要关注这些。。 SharedPreferences提供保存任意类型的私有数据:boolean,float,int,long,当然还有string,即便你的app进程被kill掉,这些数据依然完好,这个正是我们需要的。 你可以通过getSharedPreferences()方法或者getPreferences()方法来获得这个对象,当然前者需要你提供针对的app名称作为第一参数。 实例化这个对象之后可以通过这个对象调用类似getBoolean()的方法来获得结果,而写的步骤会复杂一点点。 你需要通过这个对象的edit()方法获得一个SharedPreferences.Editor对象,再使用后者的putBoolean()类似的方法来写入,最后需要调用commit()方法来提交你的私有数据。具体的实例如下,摘自android-sdk/docs/guide/topics/data/data-storage.html [cc lang=”java”]public class Calc extends Activity { public static final String PREFS_NAME = “MyPrefsFile”; @Override protected void onCreate(Bundle state){ super.onCreate(state); . . . // Restore preferences SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); boolean silent = settings.getBoolean(“silentMode”, false); setSilent(silent); } @Override protected void onStop(){ super.onStop(); // We need an Editor object to make preference changes. // All objects are from android.context.Context SharedPreferences settings = getSharedPreferences(PREFS_NAME, 0); SharedPreferences.Editor editor = settings.edit(); editor.putBoolean(“silentMode”, mSilentMode); // Commit the edits! editor.commit(); } }[/cc] 转载自:https://coolex.info/blog/242.html
条款8:了解各种不同意义的new和delete 这里讲了3种new,分别是:new operator, operator new, placement new。 new operator最简单,它就是我们平时常用的new关键字,需要注意的是,它是不能被重载的。new operator的语义是先分配内存,然后调用对象的构造函数。 operator new:这个是这三个new里面唯一能够重载的,平时我们重载的就是这个操作符。它的声明是: [cc lang=”cpp”] void *operator new(size_t size); [/cc] 但是operator new即使重载了,也只能负责分配内存,没法完成new operator的后面那步:指定构造函数。 placement new:我们没法直接在代码中调用构造函数,placement new的作用,就是在给定的内存上构造对象。也就是完成了new operator的第二步。用的最多的可能就是在已经申请的共享内存上构造对象。 也就是说:new operator = operator new + placement new 摘录下总结:“如果你希望将对象产生于heap,请使用new operator。它不但分配内存而且为该对象调用一个constructor。如果你只是打算分配内存,请调用operator new,那就没有任何constructor会被调用。如果你打算在heap objects产生时自己决定内存分配方式,请写一个自己的operator new。如果你打算在已分配(并拥有指针)的内存中构造对象,请使用placement new。” delete没有这么复杂,只有两个:delete operator和operator delete 看了前面的解释,这个就简单多了,delete operator是c++内置的不能修改,所做的事情,就是执行析构函数,并释放内存。 operator delete是能够重载的,一般会和operator new一起重载,以自定义内存的释放方式。 delete operator = deconstructor + operator delete 用代码来总结下前面提到的: [cce lang=”cpp”] #include <new> #include <iostream> class Obj { public: Obj(int a) : _a(a){} ~Obj(){std::cout<<“die”<<std::endl;} int _a; }; int main() { void *rawMem = operator new(sizeof(Obj)); //operator new Obj *obj = new(rawMem) Obj(10); //placement new std::cout << obj->_a << std::endl; //对象构造完毕 //和Obj *obj = new Obj(10)相同 obj->~Obj(); //deconstructor operator delete (obj); //operator delete //对象销毁完毕,和delete obj相同 return 0; } [/cce] 最后简单说下数组,c++分配数组和删除数组,需要加一个[],这是因为执行类似string *p = new string[10];的时候,new operator会去调用operator new[],删除的时候必须使用delete [] p;同样是因为,需要让delete operator能够正确的调用operator delete[]。 转载自:https://coolex.info/blog/245.html
原先的设计是通过已有的库,将数据通过rmi写入到远程服务器;现在有需求需要将支持多个,而且是自定义的远程服务器ip。 因为整个接口原先都是通过spring配置文件,包括rmi的地址。同时,为了维护方便,不能直接将打好的jar包拿过来改,于是就采用了复制原有的bean definition,动态注册新的bean到那个spring容器中。 首先需要获取jar包中的spring容器,这里需要将applicationContext接口转换成为真正的实现:DefaultListableBeanFactory,这样才能使用其中的注册和获取beandefinition方法。 当要获取bean的时候,首先先通过beanfactory的containsBean方法判断是否已经有bean了,如果没有,就继续以下的步骤: [cce lang=”java”] RootBeanDefinition remoteDataServerClientDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition(BASE_BEAN_NAME); //获取“模板”bean的定义 RootBeanDefinition remoteDataServerClientNewDefinition = new RootBeanDefinition(remoteDataServerClientDefinition); //深度复制 MutablePropertyValues properties = remoteDataServerClientNewDefinition.getPropertyValues(); //拿到原bean的属性 BeanDefinitionHolder defaultDataSourceFactoryDefinitionHolder = //这里获取属性值 (BeanDefinitionHolder) properties.getPropertyValue(“serviceFactory”).getValue(); //下面省略,主要是对属性值的修改和重新赋值 //向bean factory中注册新的bean,第一个参数是bean的名字,第二个参数是bean的定义 beanFactory.registerBeanDefinition(udasClientBeanName, remoteDataServerClientNewDefinition); beanFactory.getBean(udasClientBeanName); //然后就可以从容器中拿bean了 [/cce] 整个过程非常简单,其中注意一个地方,就是MutablePropertyValues和spring配置文件的关系。一个beandefinition的MutablePropertyValues就是配置文件中的一系列property标签,可以获取其中的value。如果property是一个bean,那个获取过来的Object对象,是BeanDefinitionHolder对象,就是对属性中引用bean的定义,以此类推。 比如,spring的配置文件里面有这样的bean定义: [cce lang=”xml”] <bean id=”a” class=”x.y.z.A”> <property name=”p1″ value=”test” /> <property name=”p2″> <bean class=”x.y.z.B” /> </propery> < /bean> [/cce] 那么通过getPropertyValues()方法获取到的MutablePropertyValues就包含两个属性,可以通过MutablePropertyValues的getPropertyValue(“p1″)获取到test这个值,getPropertyValue(“p2″)获取到的就是x.y.z.B这个类对应的beandefinition。 转载自:https://coolex.info/blog/253.html
条款12:了解“抛出一个exception”与“传递一个参数”或“调用一个虚函数”之间的差异 第一,exception object总是会被复制,如果以by value方式捕捉,它们甚至被复制两次。至于传递给函数参数的对象不一定得复制。第二,“被抛出成为exceptions”的对象,其被允许的类型转换动作,比“被传递到函数去”的对象少。第三,catch子句以其“出现于源代码的顺序”被编译器检查比对,其中第一个匹配成功者便被执行;而当我们以某个对象调用一个虚函数,被选中执行的是那个“与对象类型最佳吻合”的函数。 条款13:以by reference方式捕捉exceptions 上面两个条款都提到了被抛出对象的复制问题,通过下面的几个例子试了下: [cce lang=”cpp”] #include <iostream> static int count = 0; class e_class { public: e_class() { std::cout << “in constructor: count: ” << ++count << std::endl; } e_class(const e_class &right) { std::cout << “in copy constructor: count: ” << ++count << std::endl; } }; int main(){ try { e_class e_c; //创建对象,调用构造函数count累加 throw e_c; //抛出异常,e_c被复制,调用拷贝构造函数,count累加 } catch (e_class e) { //以by value形式catch,对象再次被复制,count累加 } return 0; } [/cce] 这里count被加了3次,第一次是e_class对象构造的时候,然后抛出去的时候,对象被复制,然后catch的时候因为按值传递,传入对象再次被复制。执行结果为: in constructor: count: 1 in copy constructor: count: 2 in copy constructor: count: 3 这里能被避免的就是catch这里,如果按照引用传递,这次复制就可以避免: [cce lang=”cpp”] #include <iostream> static int count = 0; class e_class { public: e_class() { std::cout << “in constructor: count: ” << ++count << std::endl; } e_class(const e_class &right) { std::cout << “in copy constructor: count: ” << ++count << std::endl; } }; int main(){ try { e_class e_c; //创建对象,调用构造函数count累加 throw e_c; //抛出异常,e_c被复制,调用拷贝构造函数,count累加 } catch (e_class &e) { //以by reference形式catch,不会再次被复制 } return 0; } [/cce] 采用按引用方式catch后,和函数按引用传递参数一样,对象不再被复制,执行结果为: in constructor: count: 1 in copy constructor: count: 2 条款12中还介绍了再次抛出异常的对象复制问题: [cce lang=”cpp”] #include <iostream> static int count = 0; class e_class { public: e_class() { std::cout << “in constructor: count: ” << ++count << std::endl; } e_class(const e_class &right) { std::cout << “in copy constructor: count: ” << ++count << std::endl; } }; int main(){ try { try { e_class e_c; throw e_c; } catch (e_class &e) { throw e; //这里抛出的是e的副本,会对e进行复制后抛出 } }catch(…) { } return 0; } [/cce] 这里在catch子句中重新抛出了异常,但是这种方式抛出的其实是异常对象的副本,对象会再次被复制,执行结果为: in constructor: count: 1 in copy constructor: count: 2 in copy constructor: count: 3 如果改成这样: [cce lang=”cpp”] #include <iostream> static int count = 0; class e_class { public: e_class() { std::cout << “in constructor: count: ” << ++count << std::endl; } e_class(const e_class &right) { std::cout << “in copy constructor: count: ” << ++count << std::endl; } }; int main(){ try { try { e_class e_c; throw e_c; } catch (e_class &e) { throw; //直接抛出e,不会进行复制 } }catch(…) { } return 0; } [/cce] 这里throw语句不带任何参数,抛出的是当前异常本身,当前的异常对象不会再次被复制,执行结果为: in constructor: count: 1 in copy constructor: count: 2 转载自:https://coolex.info/blog/267.html
之前使用beamer写slides,都是直接找的图片,所以直接采用贴图的方式。前几天搜索到了tikz这个宏包,发现它可以用来绘制图表,并结合到beamer中形成动画效果,因此找了一些资料,并尝试画简单的流程图。 首先,需要在源文件中指定引用tikz宏包,并且定义一些简单的图形: [cce lang=’latex’] \usepackage{tikz} \tikzstyle{block} = [rectangle, draw, fill=blue!20, text width=4em, text centered, rounded corners] \tikzstyle{hugeBlock} = [rectangle, draw, fill=blue!20, text width=5em, text centered, rounded corners, minimum height=4em] \tikzstyle{line} = [draw, -latex’] [/cce] 这里定义了两个图形,一个是普通的“块”,用蓝色填充,文字居中,有圆角;还有一个“大块”,其他都一样,只是文字宽度和“块”最小宽度不同。 试着画一个简单的静态图: [cce lang=’latex’] \begin{frame}{集中式工作流} 工作流程和svn类似,基本形态如图: \begin{tikzpicture}[node distance = 2cm, auto] \node [block] (center) {共享仓库}; \node[block, below of=center, node distance = 3cm](centerDevelop){开发者2}; \node[block, left of=centerDevelop, node distance = 3cm](leftDevelop){开发者1}; \node[block, right of=centerDevelop,node distance = 3cm](rightDevelop){开发者3}; \path[line,<->](center) — (leftDevelop); \path[line,<->](center) — (centerDevelop); \path[line,<->](center) — node{push/pull} (rightDevelop); \end{tikzpicture} \end{frame} [/cce] 首先是开始一个tikzpicture绘图区域,然后使用\node绘制节点,节点都是使用前面定义的“块”。可以通过位置参数来控制绘制的节点和其他节点的相对位置关系。绘制完节点之后,绘制节点的连线:通过\path指令。这里使用双向箭头,所以在参数里面增加了<->符号。连接节点的方式很简单,用前面定义的节点名字,之间用–连起来就好了。这幅图是参照pro git中的集中式工作流介绍中的插图画的,源图为: 绘制出来的图为: 虽然不是太好看,当然也可以通过修改“块”的图形,弄的更好看些。 将tikz用在beamer中,最主要的当然是为了让生成的pdf有“动画”效果了,也就是能够自动生成多张pdf页面,在播放的时候有类似分布展现的效果。 [cce lang=’latex’] \begin{frame}\frametitle{集成管理员工作流}\framesubtitle{参与者流程} \begin{tikzpicture}[node distance = 3cm, auto] \path[use as bounding box] (-1,0) rectangle (10,-2); \path[line]<1-> node[block](fock){fock工程}; \path[line]<2-> node[block, right of=fock, node distance=3cm](clone){克隆到本地} (fock) — (clone); \path[line]<3-> node[block, right of=clone, node distance=3cm](edit){修改} (clone) — (edit); \path[line]<4-> node[block, below of=edit, node distance=3cm](commit){提交} (edit) — (commit); \path[line]<5-> node[block, left of=commit, node distance=3cm](push){推送远程} (commit) — (push); \path[line]<6-> node[block, left of=push, node distance=3cm](mergeRequst){请求merge} (push) — (mergeRequst); \end{tikzpicture} \end{frame} [/cce] 这里主要靠\path指令,用法和beamer中的itemize/item相同,通过指定<n->,让tex在后面第n页上绘制。需要特别注意的是第一个path。必须要绘制一个bounding box,否则在排版的时候会自动把节点进行居中等重排,导致后面的页面和前面的节点绝对位置有所移动。这里每个path会生成一页,这一个frame会包含6页。 转载自:https://coolex.info/blog/298.html
之前c++代码用过cppunit,然后开始用gtest,对于qt代码,第一次尝试使用QtTest框架。 要使用QtTest,首先需要在.pro文件中增加配置,让qmake知道要添加qt测试框架: [cce] CONFIG += qtestlib [/cce] 然后,就可以新建一个cpp文件,编写测试代码了: [cce lang=”cpp”] #include <QtTest/QtTest> #include "../src/twitterapi.h" class TwitterApiTest : public QObject { Q_OBJECT private slots: void testRequestToken(); }; void TwitterApiTest::testRequestToken() { Setting s; s.twitterUrl = "https://api.twitter.com/1"; TwitterApi api(&s); QSignalSpy spy(&api, SIGNAL(authUrlCreated(bool,QString,Token*))); bool result = api.authUrl(); QVERIFY(result); QTest::qWait(10000); QVERIFY(spy.isValid()); QCOMPARE(spy.count(), 1); QList<QVariant> arguments = spy.takeFirst(); QVERIFY(arguments.at(0).toBool() == true); qDebug() << arguments.at(1); } QTEST_MAIN(TwitterApiTest) #include "TwitterApiTest.moc" [/cce] QtTest相关的函数和宏都在QtTest/QtTest中,直接都include进来即可,我主要用到了里面的QSignalSpy类。 测试类和普通qt类一样,需要继承QObject,所有的测试函数,都声明为私有槽函数(private slots)。这里的测试函数,主要使用了QSignalSpy类,可以用这个类来检测对应的信号是否已经发出,还可以获取信号的参数。QtTest还提供了一些断言宏,具体可以查看assistant中QTest的文档。 每个测试类都需要使用QTEST_MAIN宏生成一个最终的main函数,make之后会生成一个对应的可执行文件。最后,如果测试类声明和实现在一个cpp文件中,需要手工include生成的moc文件。 感觉QtTest没有像cmake中的test那样好的和makefile进行结合,没有增加执行测试的阶段。需要手工运行生成的可执行程序: [cce] jinlingjie@babydragon ~/work/aflatoxin-build-desktop-Qt_in_PATH___ $ ./aflatoxin ********* Start testing of TwitterApiTest ********* Config: Using QTest library 4.8.1, Qt 4.8.1 PASS : TwitterApiTest::initTestCase() QWARN : TwitterApiTest::testRequestToken() Don’t know how to handle ‘Token*’, use qRegisterMetaType to register it. QDEBUG : TwitterApiTest::testRequestToken() oauth head: “OAuth oauth_callback=”——-“,oauth_consumer_key=”———–“,oauth_nonce=”———–“,oauth_signature=”———–“,oauth_signature_method=”HMAC-SHA1″,oauth_timestamp=”1333445034″,oauth_version=”1.0″” QWARN : TwitterApiTest::testRequestToken() content-type missing in HTTP POST, defaulting to application/octet-stream QDEBUG : TwitterApiTest::testRequestToken() request token reply: “oauth_token=———&oauth_token_secret=———–&oauth_callback_confirmed=true” QDEBUG : TwitterApiTest::testRequestToken() QVariant(QString, “https://api.twitter.com/oauth/authorize?oauth_token=————-“) PASS : TwitterApiTest::testRequestToken() PASS : TwitterApiTest::cleanupTestCase() Totals: 3 passed, 0 failed, 0 skipped ********* Finished testing of TwitterApiTest ********* [/cce] 转载自:https://coolex.info/blog/306.html
昨天晚上更新developer-mode的时候,更新完成之后n9就再也没有起来过,屏幕上一直显示nokia,然后就没有然后了。 记录下早上刷机的过程。 首先安装flasher,下载地址在,需要下载里面的3.12.1版本。 然后下载固件,找了个国行的pr1.2固件:大概有1.2G,还好公司的网速比较快,2min搞定。 在下载的间隙,为flasher写了个ebuild文件,方便卸载: [cce] EAPI=4 MY_A=”${PN}_3.12.1_amd64.deb” DESCRIPTION=”Maemo Flasher-3.12.1 Tool for Harmattan, installation package for Debian based Linuxes (x86, 64-bit)” HOMEPAGE=”http://tablets-dev.nokia.com/maemo-dev-env-downloads.php” SRC_URI=”${MY_A}” LICENSE=”Nokia-EULA” SLOT=”0″ KEYWORDS=”~amd64″ IUSE=”” RESTRICT=”fetch strip” DEPEND=”virtual/libc app-arch/deb2targz” RDEPEND=”${DEPEND}” pkg_nofetch() { elog “Please obtain ${P} from http://tablets-dev.nokia.com/maemo-dev-env-downloads.php and place it in ${DISTDIR}” } src_unpack() { unpack ${A} mkdir -p ${S} tar xf data.tar.gz -C ${S} } src_install() { cp -R ${S}/* ${D}/ || die “Install failed!” } [/cce] 调试了下ebuild,这个时候固件早已经下载好了,然后就是刷机了。 在命令行执行(需要在root权限下执行,否则会提示:Error claiming USB interface: Operation not permitted) [cce lang=”bash”] flasher -F DFL61_HARMATTAN_30.2012.07-1_PR_LEGACY_003-OEM1-958_ARM.bin -f [/cce] -F后面的参数是刚才下载的固件名字。回车之后,控制台会提示usb接口没有找到设备: [cce] flasher 3.12.1 (Oct 5 2011) Harmattan WARNING: This tool is intended for professional use only. Using it may result in permanently damaging your device or losing the warranty. Suitable USB interface (bootloader/phonet) not found, waiting… [/cce] 这个时候手机插上usb线,然后按住电源键,直到屏幕上出现一个usb图标,flasher应该就可以识别到手机了。一直等到提示成功之后就OK了。 手机之后会自动重启,第一次启动后,和刚买来一样,会有教程什么的,不过开机后遇到个问题,信息、通讯录等图标有重复。这个时候再到设置里面去恢复设置(这样数据不会丢失)问题就解决了。 这样整个过程貌似只会对/分区有影响,/home/user中的数据分区没有影响,通讯录、短信、照片等都还在,就是之前安装的应用都没有了,又要重新安装。 转载自:https://coolex.info/blog/318.html
最近突然要清理闲置服务器,最简单的指标当然是看下有多少服务器是最近没有人登录过的。当登录服务器的时候,init, tty等会将登录和登出信息记录到/var/log/wtmp文件中,通过last命令可以查询服务器的登录情况。 但是直接到服务器上执行last命令有几个问题,一是希望查询一个时间段内的登录情况,而last命令的-t参数只能设置最近时间点;另一个是服务器上的last比较老,不支持-F参数,显示的事件没有年字段,计算的时候可能会存在问题。 最方便的解决方法当然是自己来读取wtmp文件了。linux提供了utmp.h头文件,里面包含了utmp数据结构和操作函数。关于utmp这个结构,可以通过man utmp进行查询。其中比较常用的几个属性有:ut_type(登录/登出类型,具体上面手册中有详细的宏定义说明)、ut_tv(操作的事件)、ut_user(操作用户)、ut_host(操作主机名/ip)。 手册中强调了,登录和登出都会单独记录一条,唯一的区别是登出不记录ut_user等字段,所以估计last命令中每次登录的事件计算,应该是根据每次登录和之后的一次登出的ut_line匹配上进行计算的。 再来说下utmp的操作函数,主要操作流程为:setutent -> getutent -> endutent,感觉和文件读写顺序差不多。首先需要通过setutent重新定位到utmp的文件头,循环调用getutent获取每一次的utmp记录,最后通过endutent结束读取。从api可以看出,所有的记录都要顺序读出,然后再进行过滤和运算。 需求是在给定事件段内打印出最后一次登录的时间,所以不去判断那次登录是否有登出操作,大致的实现: [cce lang=”c”] time_t from = atol(argv[1]); time_t to = atol(argv[2]); struct utmp *line = NULL; struct utmp *result = NULL; time_t timestamp; utmpname("/var/log/wtmp"); setutent(); while( (line = getutent()) != NULL) { if (line->ut_type == USER_PROCESS ) { timestamp = line->ut_tv.tv_sec; if(timestamp >= from && timestamp <= to) { if(result != NULL) { free(result); } result = malloc(sizeof (*result)); memcpy (result, line, sizeof (*result)); } } } if(result != NULL) { timestamp = result->ut_tv.tv_sec; printf("%s %s %s %s", result->ut_line, result->ut_user, result->ut_host, asctime(localtime(&timestamp))); } endutent(); [/cce] 获取的参数是两个时间段,为了方便运算,直接要求输入时间对应的unix时间戳,可以通过date命令获取。遍历所有记录,只读取USER_PROCESS类型的,然后比较这次登录时间是否在给定时间段内。因为这个顺序刚好是登录时间的顺序,所以最后一次登录的时间,就是最后一条符合if判断的时间。最后打印出来就OK了。 转载自:https://coolex.info/blog/326.html
为了将一个不知道什么类型的java对象存到数据库里面,用了个很恶心的方法:将对象序列化之后,保存到数据库的blob对象。这样整个序列化和反序列化过程,都期望在ibatis中完成,上层就能直接获取到需要的对象了。 第一次安装网上的方式,想直接参考spring的BlobByteArrayTypeHandler,将blob对象保存到数据库。因为这个类是需要的是byte数组,还不满足我的需求,就参照这个类,继承了AbstractLobTypeHandler,重写getResultInternal和setParameterInternal。然后将这个type handler配置到ibatis的sqlmap文件中。一切看上去很顺利,结果启动的时候ibatis报ClassCastException,说这个handler无法强制转换成TypeHandlerCallback。 这个异常看上去很奇怪,去网上搜索了InlineParameterMapParser类,里面的逻辑大致是: [cce lang=”java”] } else if (“handler”.equals(field)) { try { value = typeHandlerFactory.resolveAlias(value); Object impl = Resources.instantiate(value); if (impl instanceof TypeHandlerCallback) { mapping.setTypeHandler(new CustomTypeHandler((TypeHandlerCallback) impl)); } else if (impl instanceof TypeHandler) { mapping.setTypeHandler((TypeHandler) impl); } else { throw new SqlMapException (“The class ” + value + ” is not a valid implementation of TypeHandler or TypeHandlerCallback”); } } catch (Exception e) { throw new SqlMapException(“Error loading class specified by handler field in ” + token + “. Cause: ” + e, e); } [/cce] 也就是说,如果在sql中配置的handler是同时兼容TypeHandlerCallback和TypeHandler接口的,spring的AbstractLobTypeHandler类,实现的是BaseTypeHandler,应该没有问题的啊。 反编译了应用中试用的ibatis(版本是2.1.6),发现里面的逻辑完全不一样: [cce lang=”java”] } else if (“handler”.equals(field)) { try { value = typeHandlerFactory.resolveAlias(value); mapping.setTypeHandler(new CustomTypeHandler((TypeHandlerCallback)Resources.classForName(value).newInstance())); } catch (Exception e) { throw new SqlMapException(“Error loading class specified by handler field in ” + token + “. Cause: ” + e, e); } } else { throw new SqlMapException(“Unrecognized parameter mapping field: ‘” + field + “‘ in ” + token); } [/cce] 这个古董是直接初始化之后强转为TypeHandlerCallback的。没办法,只能自己实现这个接口。 [cce lang=”java”] public class JavaSerializeBlobHandler implements TypeHandlerCallback { /* * (non-Javadoc) * @see com.ibatis.sqlmap.engine.type.TypeHandler#valueOf(java.lang.String) */ public Object valueOf(String s) { return s; } /* * (non-Javadoc) * @see * com.ibatis.sqlmap.client.extensions.TypeHandlerCallback#getResult(com.ibatis.sqlmap.client.extensions.ResultGetter * ) */ public Object getResult(ResultGetter paramResultGetter) throws SQLException { byte[] buf = paramResultGetter.getBytes(); if(buf == null) { return null; } try { ObjectInputStream inputStream = new ObjectInputStream(new ByteArrayInputStream(buf)); return inputStream.readObject(); } catch (IOException e) { throw new IllegalArgumentException(e); } catch (ClassNotFoundException e) { throw new IllegalArgumentException(e); } } /* * (non-Javadoc) * @see com.ibatis.sqlmap.client.extensions.TypeHandlerCallback#setParameter(com.ibatis.sqlmap.client.extensions. * ParameterSetter, java.lang.Object) */ public void setParameter(ParameterSetter paramParameterSetter, Object paramObject) throws SQLException { if (paramObject == null) { paramParameterSetter.setNull(Types.BLOB); } else { try { ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteStream); objectOutputStream.writeObject(paramObject); paramParameterSetter.setBytes(byteStream.toByteArray()); } catch (IOException e) { throw new IllegalArgumentException(e); } } } } [/cce] 不过接口中的valueOf方法,到底是干什么用的,什么时候调用的还没搞清楚。将这个类配置在sqlmap的resultMap或sql中内联,都能够正常的对对象进行存取。 转载自:https://coolex.info/blog/330.html
git svn主要使用流程: 从svn仓库初始化成本地git仓库: [cce lang=”bash”] git svn clone -s SVN_URL [/cce] 注意:SVN_URL是svn仓库的基础目录,不包含trunk,branches这些目录。这里加上一个-s是–stdlayout的缩写,标识svn地址是符合标准svn目录结构的,既有SVN_URL/trunk,SVN_URL/branches,SVN_URL/tags这些目录(当然后面两个目录是非必须的,如果没有,后续拉分支的时候可能会失败) 执行后,整个svn分支上的版本都会重放一边,git生成自己的commit记录,如果分支提交记录比较多,速度会比较慢的, 切换svn分支: [cce lang=”bash”] git checkout -b local/local_branch remotes/remote_branch [/cce] 当然,切换分支之前,可以通过git branch -a或者git branch -r查看svn上的分支,这里显示的分支名称,是SVN_URL/branches目录中的名字。上述切换分支的命令,会在本地创建一个git分支,然后将这个git分支的remote refs设置成远程的svn分支。 提交本地修改: [cce lang=”bash”] git commit git svn dcommit [/cce] 首先本地是一个git仓库,所以所有的修改,必须先提交到本地的git仓库,然后git svn提供了dcommit命令,可以将本地git仓库修改,提交到远程的svn仓库。 注意:执行svn dcommit之前,需要养成良好习惯,先更新svn分支改动。 更新svn分支改动: [cce lang=”bash”] git svn rebase [/cce] git svn没有update的概念,因为本地可能有优先于svn仓库。因此,需要采用类似git rebase的概念,先更新svn的提交,然后将git的本地提交重放。所以如果第一个本地提交版本就和svn版本冲突,就等着哭去吧,每次重放可能都要解决冲突,这个时候可能本次再拉一个git分支,采用merge的方式会方便一点。 上面应该是最常用的几个git svn命令,下面是使用过程中一些特殊情况的处理: 创建svn分支: [cce lang=”bash”] git svn branch -m “message” BRANCH_NAME [/cce] 看了命令输出就知道了,这样其实就是执行svn copy操作。当然,需要确保SVN_URL/branches目录存在,因为git svn需要知道分支路径(当然也可以在.git/config文件中修,但总是大家都按照规范来比较方便)。创建svn分支之后,可以再用之前的方式将本地git分支切换并关联到这个新创建的svn分支上。 更新svn信息: [cce lang=”bash”] git svn fetch [/cce] 如果你的项目svn分支不是你拉的,本地git svn clone的又比较早,你会发现新创建的分支在git branch -r中不存在,也就没法切换过去了。这个时候就需要运行git svn fetch命令,将svn仓库变更同步下来,然后才能对新分支进行操作。 合并svn分支 [cce lang=”bash”] git checkout master git merge –squash GIT_BRANCH_NAME git commit git svn dcommit [/cce] 和之前说的一样,合并操作需要先合并git仓库。首先先切换到master分支(或者其他引用了svn trunk的git分支),然后进行merge。需要注意的是,merge的时候要使用squash的方式,也就是应用分支改动,但是不修改HEAD。如果直接merge,会把当前分支的远程svn引用也修改,变成svn分支了。然后单独提交,最后提交到svn主干上。 svn目录突然变了: 前段时间配管老是改svn目录,对应git svn引用也需要修改。git svn会在.git/config中增加[svn-remote “svn”]这么一项,里面包含了主干、分支、标签的目录,和svn仓库基础目录。如果原先引用的svn目录变了,这里需要做对应的修改。修改完成后,会发现无法进行git svn rebase操作了,因为远程引用整个变了。没办法,只能先进行git svn fetch,获取新分支,然后重新从新的svn目录中拉分支,进行修改。这样最大的好处,是原先本地git的提交还能够保留。 转载自:https://coolex.info/blog/334.html
java网页写多了,没事用c++写着玩。cgi,应该算是非常老了,它的最大好处,就是弄成可执行程序放进去就OK了。 因为平时只会用apache,这里使用的http服务器都是apache2。 为了使用类似java servlet方式,自己来绑定uri和执行方法,通过写一个dispatcher和rewrite来搞定。 首先,需要将所有访问重写到这个dispatcher上,这里是一个简单的rewrite规则: [cce] RewriteEngine On RewriteRule ^/cgi-bin/(.*)$ /cgi-bin/cppweb/$1 [L,PT,QSA] [/cce] 这里cppweb是一个编译出来的可执行程序,rewrite后面的参数: L:最后一条匹配的规则 PT:执行完成后继续执行其他处理(这里主要是为了应用alias配置) QSA:追加query string 这样,如果请求/cgi-bin/a,内部会被重写成/cgi-bin/cppweb/a。 后面就是cppweb的实现了。为了方便,对于cgi的处理,采用了gnu的cgicc库,它对很对cgi操作进行了封装。 大致实现: [cce lang=”cpp”] int main(int argc, char **argv) { REGISTER_URL("/list", List, handle); REGISTER_URL("/video/new", VideoNew, handle); cgicc::Cgicc cgi; cgicc::CgiEnvironment env = cgi.getEnvironment(); cgicc::CgiInput input = cgicc::CgiInput(); ctemplate::Template::SetTemplateRootDirectory(input.getenv("TEMPLATE_PATH")); Context ctx = {cgi, input, std::cout}; Handler::instance().handleFunc(env.getPathInfo(), ctx); return 0; } [/cce] 前面两行暂时忽略,是用来对url和处理的函数进行绑定用的,后面进行介绍。这里先创建cgicc对象,它主要封装了cgi环境变量和输入、输出。这里后面可以参考cgicc里面的实例,将cgi改造成fastcgi。刚刚重写的url,这里可以通过cgi中的环境变量path info来或者,cgicc中将这个环境变量封装成了一个独立的方法。如刚才重写后的url /cgi-bin/cppweb/a,这里获取到的path info就是/a,然后就可以通过/a和最终处理业务逻辑的函数绑定在一起就搞定了。 这里还介绍下自定义环境变量的用法。上面的代码里面获取了环境变量TEMPLATE_PATH,最为输出模板的查询根路径,直接在apache配置中增加: [cce] SetEnv TEMPLATE_PATH /tmp/mycppweb/templates [/cce] 这样就能在运行时获取到这个模板路径了,网站可以通过类似的方式定义其他参数,类似j2ee里面的web.xml差不多吧。 转载自:https://coolex.info/blog/338.html
可能是因为公司里写代码用velocity习惯了,找了google的ctemplate来实现类似的功能。ctemplate相对velocity比较简单,只有变量替换,简单的引用、循环。这里有ctemplate的详细文档,相对来说,就是不能在模板上有太多的逻辑。 首先,前一篇里面贴了dispatcher,通过apache中设置环境变量,设置模板跟路径: [cce lang=”cpp”] ctemplate::Template::SetTemplateRootDirectory(input.getenv(“TEMPLATE_PATH”)); [/cce] 然后,通过绑定的处理函数,调用真正的处理uri的函数后,将输出的dictionary对象,和模板合并: [cce lang=”cpp”] std::string templatePath; if(context.dict) //has dict { //get template templatePath = context.dict->name() + ".tpl"; //expend std::string out; bool expandResult = ctemplate::ExpandTemplate(templatePath, ctemplate::STRIP_WHITESPACE, context.dict.get(), &out); if(expandResult) //good, we expend template success { context.ostream << cgicc::HTTPHTMLHeader(); context.ostream << out; } else //oops, we response 500 { context.ostream << cgicc::HTTPStatusHeader(500, "Internal Server Error"); context.ostream << "fail to expand template: " << templatePath; } } else { //如果没有字典,由对应的函数自己输出 } [/cce] 这里是有字典输出的逻辑。获取根字典的名字,加上后缀,在根路径中查找对应的模板。然后调用ctemplate的函数,将模板展开。最后,将展开后的内容,通过流输出。 如果没有字典输出,默认当成处理函数中已经自己进行了输出(这里主要是为了让处理函数自己输出json内容) 另外一个分支,如果没有绑定,那么直接输出模板内容: [cce lang=”cpp”] // find if there is a template std::string templatePath = path.substr(1) + ".tpl"; if(ctemplate::mutable_default_template_cache()->LoadTemplate(templatePath, ctemplate::STRIP_WHITESPACE)) { //expend std::string out; bool expandResult = ctemplate::ExpandTemplate(templatePath, ctemplate::STRIP_WHITESPACE, _emptyDict.get(), &out); if(expandResult) //good, we expend template success { context.ostream << cgicc::HTTPHTMLHeader(); context.ostream << out; } else //oops, we response 500 { context.ostream << cgicc::HTTPStatusHeader(500, "Internal Server Error"); context.ostream << "fail to expand template: " << templatePath; } } else //not bind and not find a template file { context.ostream << cgicc::HTTPStatusHeader(404, "not find"); context.ostream << "not find"; } [/cce] 如果没有绑定,用一个空的字典,展开模板,也就是直接输出模板。如果这都没有找到,那么就返回404。 一个绑定函数的例子: [cce lang=”cpp”] void handle(Context &context) { std::vector<Volume> volumes; //FIXME: mock数据 Volume v1 ("1", "a", "cover_1.png", 5); Volume v2 ("2", "b", "cover_2.png", 1); volumes.push_back(v1); volumes.push_back(v2); boost::shared_ptr<ctemplate::TemplateDictionary> listPageDict(new ctemplate::TemplateDictionary("list")); for(int i = 0; i < volumes.size(); ++i) { ctemplate::TemplateDictionary *listSection = listPageDict->AddSectionDictionary("LIST_SECTIONS"); listSection->SetIntValue("id", volumes[i].id()); listSection->SetValue("name", volumes[i].name()); listSection->SetValue("cover_img_path", volumes[i].cover_path()); } context.dict = listPageDict; } [/cce] 对应的模板文件: [cce] < !DOCTYPE html> < html> <head> <meta charset="utf-8" /> <title>title</title> </head> <body> <ol> {{#LIST_SECTIONS}} <li>name: {{name}}</li> {{/LIST_SECTIONS}} </ol> </body> < /html> [/cce] 最后输出就是刚在Volume对象的name字段。 转载自:https://coolex.info/blog/341.html
前面两篇博客讲了从uri到模板输出,还没有提到中间处理参数的部分。 首先,参数绑定简单的放在一个map中,这里用的是boost的unordered_map(也就是hashmap),其实已经可以使用c++11提供的unordered_map了。 [cce lang=”cpp”] boost::unordered::unordered_map<std::string, RequestHandleFunc> _mapping; [/cce] 这个map的value,是一个functor,具体定义为: [cce lang=”cpp”] typedef boost::function<void (Context &context)> RequestHandleFunc; [/cce] 也就是没有返回值,参数是Context的函数。 Context结构非常简单,主要是封装了输入和输出,也就是cgi、ostream、dict等对象,没有进行太多的抽象。 [cce lang=”cpp”] struct Context { cgicc::Cgicc &cgi; cgicc::CgiInput &input; std::ostream &ostream; boost::shared_ptr<ctemplate::TemplateDictionary> dict; }; [/cce] 这里增加了cgiInput,主要是因为cgicc封装了常用cgi环境变量,没法取到自定义环境变量(前一篇博客介绍过)。 绑定函数非常简单,就是直接插入,需要注意的是,unordered_map非线程安全,没有线程类似java的concurrent hashmap(google了下intel tbb库有类似的数据结构),所以采用了boost thread库中的互斥变量: [cce lang=”cpp”] void bind(const std::string &path, RequestHandleFunc func) { boost::unique_lock<boost::shared_mutex> lock(_mutex); _mapping.insert(std::pair<std::string, RequestHandleFunc>(path, func)); } [/cce] 注意,boost的锁分为unique_lock和shared_lock,这里是“写者”,所以需要独占锁。 处理方法前文已经贴了代码了,同样要注意的是,需要在搜索map的时候加锁。这里是“读者”,所以使用shared_lock即可: [cce lang=”cpp”] boost::shared_lock<boost::shared_mutex> lock(_mutex); [/cce] 为了方便绑定,写了个宏,直接绑定。 [cce lang=”cpp”] #define REGISTER_URL(URL, CLASS, METHOD) \ CLASS c##CLASS; Handler::instance().bind(URL, boost::bind(&CLASS::METHOD, &c##CLASS, _1)) [/cce] 转载自:https://coolex.info/blog/351.html
上文介绍了如何通过ajax异步上传文件,html5对file的新接口,可以使得在页面上,对用户也有更好的体验。 页面上要做的,仅仅是添加一个html标签: [cce lang=”html”] <input id="track" name="track" type="file" multiple="multiple" onchange="showInfo();" /> [/cce] 这里在input的change事件上绑定了showInfo函数,当选择文件的时候,就会被触发。属性中增加multiple,表明这个标签支持多个文件。 showInfo的作用,顾名思义,就是读取上传文件的内容,展示给用户。这里是所有的代码: [cce lang=”javascript”] function showInfo() { var fileList = document.getElementById('track').files; var container = document.getElementById('sort_table'); //first clear while (container.firstChild) { container.removeChild(container.firstChild); } for(var i = 0; i < fileList.length; i++) { var file = fileList[i]; var tr = document.createElement('tr'); tr.setAttribute("id", i); tr.innerHTML = "<td>" + file.name + "</td><td>" + file.size + "</td"; container.appendChild(tr); } } [/cce] 首先,先获取刚才那个标签包含的所有文件对象。然后清空显示容器。最后,遍历所有的文件,绘制一个table来显示。可以看见,我们可以额通过file接口方便的获取文件名称和大小。 这里可以实现的功能就是,用户选择多个文件,然后页面上就会显示出这些文件的名称和大小。有了类似的功能,可以让用户在上传之前确认自己要上传的文件,排序文件上传顺序(这里container id叫sort_table,就是因为使用了jquery-ui的sortable插件,支持用户通过拖拽排序文件)等操作。 转载自:https://coolex.info/blog/358.html
最近写一个前台页面,因为不用考虑太多兼容性问题,尝试了很多css3的东西,记录下。 1、渐变和边框阴影 最初的视觉稿,上面有很多地方颜色使用了渐变,为了不使用图片,用了很挫的方式来实现:从图片渐变开始、中间、结束部分,用gimp的吸管获取颜色,然后转换成css的渐变。不过因为渐变每个浏览器支持方式不太一样,有一些在线生成css的网站还是不错的,比如这个和这个。 边框阴影,也就是box-shadow属性。按照MDN的文档,值的组成是这样的: box-shadow: none | [inset? && [ <offset-x> <offset-y> <blur-radius>? <spread-radius>? <color>? ] ] 现在的浏览器支持的不错了,有些框外面加点阴影,效果会好点。 2、绑定事件 很多地方为了突出(其实大部分因为上头有人说要弄的炫一点),需要在鼠标移动过去之后,增加点效果。比如增加padding、改变颜色或者展示浮出层。css有一些dynamic pseudo-classes(不知道怎么翻译,从这里抄过来的),可以选择有指定状态的元素。比较常见的应该就是hover,在鼠标移入,但没有触发元素的时候(The :hover pseudo-class applies while the user designates an element with a pointing device, but does not necessarily activate it)。 比如,菜单中,如果鼠标移动到某个菜单项,但是还没有点击的时候,需要突出这个菜单项,可以这么做(这里菜单项是个简单的a元素): [cce lang=”css”] ul#main-nav > li > ul > li > a { display: block; width: 100%; text-decoration: none; font-size: 13px; color: black; } ul#main-nav > li > ul > li > a:hover { color: #FE9900; font-size: 14px; } [/cce] 这样,在鼠标移动到菜单项上,会使下面一个选择器生效,将当前的字体放大,颜色变色。 3、css选择器 上面那个例子,已经用了一些css选择器了,这里再记录下其他用到的选择器: id选择器(#ID):页面上带了id属性的,精确而且方便 类选择器(.class):元素class属性带的,用的应该也是最多的 直接选择元素(tagname):直接使用元素名称,我一般习惯和class或者id共用,不太喜欢给全局某个元素加上很多样式,虽然统一,但是场景太多,有些地方不好控制。 属性选择器([attr=”xxx”]):这个用来兼容老的标签比较好,比如表格单元格要居中显示,原先可能会直接写成: [cce lang=”html”] <td valign="middle"></td> [/cce] 但是表格加了样式后,这些属性会失效(这个没去找原因),可以在样式表中这样写: [cce lang=”css”] td[valign='middle'] { vertical-align: middle; } [/cce] 这样就能给在标签中加了valign属性的元素,再加上vertical-align的样式属性了。 伪选择器:前面用到了:hover这个伪类,其他类似的还有:focus等,能够用来标识元素的不同状态。另一类伪类选择器,可以从一堆元素中筛选需要的元素,对我最有用的就是:nth-child了。:nth-child(an+b)像一个函数,通过设置不同的a和b,计算出可以取的正整数: [cce lang=”css”] tr:nth-child(2n+1) /* 表格中的奇数行,也可以用tr:nth-child(odd)表示 */ tr:nth-child(2n+0) /* 偶数行,也可以用tr:nth-child(even)表示 */ [/cce] 另外还有一个很有用的伪选择器,是可以创建伪元素,这样就可以在需要的元素前后增加自己需要的东西(图标等)。 [cce lang=”css”] .btn-icon-add::before { content: url(“icons/edit_add.png”); } [/cce] 这样能在加了btn-icon-add class的元素前面,增加一个元素,内容是url中的图片。这里遇到过一个问题,就是到底用一个冒号,还有两个冒号。在ff和chrome中,一个冒号和两个冒号都可以,但是ie8(其他ie没试过)只能识别一个冒号的。google了一下,有人说是两个冒号是css3定义的,为了区别css2中定义的其他伪选择器,但是可以用一个冒号,以保持向下兼容。 组合选择器:用的最多的应该就是空格、>、+等,空格表示空格后面元素是前者的后裔,和>的区别是>表示直接子元素。加号表示临近的兄弟元素。不过目前没法选择父元素,这个比较恶心。 转载自:https://coolex.info/blog/363.html
这篇博客应该是和之前的重拾cgi一起的。当时为了模仿java的web框架,从页面的模板,到数据库的ORM,都找个对应的库来进行尝试。数据库用的就是ODB,官方网站是http://www.codesynthesis.com/products/odb/。 1、安装 odb是直接提供源代码的,主要包含这几个部分:odb、libodb、libodb-sqlite等,用途分别是: odb是ODB编译器,类似于qt的moc,将c++源码中包含ODB特定宏的代码,生成对应的c++代码。 libodb是运行时库,ORM映射的主要逻辑都在这里 libodb-sqlite等,是odb提供的针对不通数据库的驱动,以实现对数据库的底层操作。其他还有profile、example等包,没有用到就没去了解。 这些包都是很标准的源码包,通过configure、make等就可以进行安装了。给自己的gentoo系统,针对这些用到的包写了ebuild。 [cce] # Copyright 1999-2012 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ EAPI=4 inherit eutils autotools-utils versionator DESCRIPTION="ODB Compiler" HOMEPAGE="http://www.codesynthesis.com/products/odb/" SRC_URI="http://www.codesynthesis.com/download/${PN}/$(get_version_component_range 1-2)/${P}.tar.bz2" LICENSE="GPLv2" SLOT="0" KEYWORDS="~amd64" IUSE="" DEPEND="dev-cpp/libcutl" RDEPEND="${DEPEND}" src_prepare() { epatch "${FILESDIR}/${PN}-distdir.patch" } [/cce] 这里要注意下,odb编译依赖libcutl,这个貌似也是这个codesynthesis上的,对此也写了一个ebuild: [cce] # Copyright 1999-2012 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ EAPI=4 inherit autotools-utils versionator DESCRIPTION="Library of C++ utilities — meta-programming tests, smart pointers, containers" HOMEPAGE="http://codesynthesis.com/projects/libcutl/" SRC_URI="http://codesynthesis.com/download/${PN}/$(get_version_component_range 1-2)/${P}.tar.bz2" LICENSE="MIT" SLOT="0" KEYWORDS="~amd64 ~x86 ~mips" IUSE="static-libs" DEPEND="dev-libs/boost" RDEPEND="${DEPEND}" src_configure() { local myeconfargs=( –with-external-boost –docdir=/tmp/dropme ) autotools-utils_src_configure } src_install() { autotools-utils_src_install rm -r "${D}"/tmp/dropme || die } [/cce] libodb的ebuild: [cce] # Copyright 1999-2012 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ EAPI=4 DESCRIPTION="ODB is an open-source, cross-platform and cross-database object-relational mapping (ORM) system for C++." HOMEPAGE="http://www.codesynthesis.com/products/odb/" SRC_URI="http://www.codesynthesis.com/download/odb/2.0/${P}.tar.bz2" LICENSE="GPLv2" SLOT="0" KEYWORDS="~amd64 ~x86 ~mips" IUSE="" DEPEND="dev-db/odb" RDEPEND="${DEPEND}" [/cce] libodb-sqlite的ebuild: [cce] # Copyright 1999-2012 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ EAPI=4 DESCRIPTION=”ODB is an open-source, cross-platform and cross-database object-relational mapping (ORM) system for C++.” HOMEPAGE=”http://www.codesynthesis.com/products/odb/” SRC_URI=”http://www.codesynthesis.com/download/odb/2.0/${P}.tar.bz2″ LICENSE=”GPLv2″ SLOT=”0″ KEYWORDS=”~amd64 ~x86 ~mips” IUSE=”” DEPEND=”dev-db/libodb dev-db/sqlite” RDEPEND=”${DEPEND}” [/cce] 转载自:https://coolex.info/blog/374.html
2、使用 首先,需要定义一个对象,用来和数据库字段对应: [cce lang=”cpp”] #ifndef VOLUME_H #define VOLUME_H #include <string> #include <odb/core.hxx> #pragma db object class Volume { public: Volume(const std::string &name, const std::string &location, const std::string &cover_path, int trackCount) : _name(name), _location(location), _cover_path(cover_path), _trackerCount(trackCount) {} unsigned long long id() { return _id; } void id(unsigned long long i) { _id = i;} const std::string &name() {return _name;} void name(const std::string &n) {_name = n;} const std::string &location() {return _location;} void location(const std::string &l) {_location = l;} const std::string &cover_path() {return _cover_path;} void cover_path(const std::string &c) {_cover_path = c;} int trackCount() {return _trackerCount;} void trackCount(int c) {_trackerCount = c;} private: friend class odb::access; Volume () {} #pragma db id auto unsigned long long _id; std::string _name; std::string _location; std::string _cover_path; int _trackerCount; }; [/cce] 首先是引入core.hxx这个头文件,包含access这个类。在类上面添加#pragma db object宏,标识这是个数据库对象。在主键上增加宏#pragma db id auto,标识这个是主键,并且自增。这两个宏都是提供信息给odb,用来生成最终c++代码的。因为数据库对应字段都是私有类型,所以需要将odb::access声明为友元。 为了方便,这里连接数据库都使用sqlite,因此,需要引入sqlite相关的包。创建数据库连接(对sqlite来说,就是打开数据库文件): [cce lang=”cpp”] std::shared_ptr<odb::database> sqliteDB(new odb::sqlite::database("mycppweb.db", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE)); [/cce] 注:这里需要引入头文件odb/sqlite/database.hxx,创建数据库连接的第一个参数(只针对sqlite)是数据库文件名,后面是打开的flag,这里主要是提示如果数据库文件不存在,默认创建。另外,这里使用了c++11的shared_ptr,g++需要添加参数-std=c++0x 插入对象: [cce lang=”cpp”] { odb::transaction t(db->begin()); volumeId = db->persist(volume); t.commit(); } [/cce] 插入的时候,使用了事务,需要引入odb/transaction.hxx头文件,并且尽量减少transaction的生命周期。 通过odb命令生成对应的代码: [cce lang=”bash”] odb –database sqlite \ –hxx-suffix .hpp –ixx-suffix .ipp –cxx-suffix .cpp \ –output-dir /tmp \ –generate-query –generate-schema –schema-format embedded volume.h [/cce] 这里指定输出的数据库是sqlite,创建schema的语句嵌入到代码里面。 执行之后,会生成volume-odb.hpp、volume-odb.cpp和volume-odb.ipp三个文件。查看volume-odb.cpp就会发现,里面包含了volume.h中指定的类和数据库表的关系。如果希望通过代码来创建数据库(貌似需要自己来判断是否已经存在,否则第二次运行又会重新创建表,导致数据丢失),可以通过: [cce lang=”cpp”] { odb::transaction t (sqliteDB->begin ()); odb::schema_catalog::create_schema (*sqliteDB); t.commit (); } [/cce] odb的查询,还没有去尝试,具体文档在http://www.codesynthesis.com/products/odb/doc/manual.xhtml 转载自:https://coolex.info/blog/376.html
了解taskstats的最初目的,是为了监控服务器的IO,防止jmeter因为受压机异常,打印大量日志,把磁盘空间用光。后来发现,由于服务器内核版本比较低(2.6.19),所以没法直接通过读取proc里面的IO来获取进程IO。但是也同样是因为内核版本太低,taskstats结构中,也比新内核少了磁盘写入和读取的统计,只能获取到磁盘延迟写入块数,所以还没有实战过。 关于taskstats,内核文档有非常详细的文档和示例代码,见:http://www.kernel.org/doc/Documentation/accounting/ 首先说下taskstats结构,按张文档的说法“Taskstats is a netlink-based interface for sending per-task and per-process statistics from the kernel to userspace.”注意这里说明了,通过taskstats能过获得per-task或者per-process的统计数据,也就是内核的所谓pid和tgid的概念。 taskstats的优势在于: efficiently provide statistics during lifetime of a task and on its exit unified interface for multiple accounting subsystems extensibility for use by future accounting patches 第一点比较有用,可以注册一个进程,当进程推出的时候收到taskstats消息。其他没用到。 taskstats通过netlink和内核进行交互,也就是说交互是异步的,在创建了netlink的fd之后,所有的操作和普通的socket也差不多了,就是需要根据netlink判断状态,并且取出真正的payload,也就是taskstats结构。 taskstats的结构在linux/taskstats.h文件中可以看见,里面有几个比较有用的成员,如: cpu_count、cpu_delay_total,blkio_count、blkio_delay_total,swapin_count、swapin_delay_total。通过注释可以大概了解到,xxx_count is the number of delay values recorded,xxx_delay_total is the corresponding cumulative delay in nanoseconds,这样就能算出delay的量,大概需要多少时间能够消耗完,从侧面可以了解负载情况。我现在在使用的内核,还有很多IO统计的数据,但是2.6.19的内核不支持,就没办法使用了。 具体的使用,内核文档的getdelays.c已经很详细、很通用了,我参照这个,写了个比较简单的。 首先,是创建netlink连接,直接使用里面的create_nl_socket函数就好了,和创建普通socket差不多,只是类型上的区别。前面已经提到,taskstats使用的是NETLINK_GENERIC方式创建的netlink,然后就是发送netlink数据包给内核,具体的发送方式send_cmd函数已经进行了封装。通过枚举TASKSTATS_CMD_ATTR_TGID和TASKSTATS_CMD_ATTR_PID,可以获取对应的pid或者tgid的数据了。 转载自:https://coolex.info/blog/380.html
一、通过mount加上偏移 首先需要确认下镜像分区开始的偏移: [cce lang=”bash”] fdisk -l vm-xp-qa-new.img [/cce] 这个文件的输出为: Disk vm-xp-qa-new.img: 21.5 GB, 21474836480 bytes, 41943040 sectors Units = 扇区 of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 512 bytes I/O size (minimum/optimal): 512 bytes / 512 bytes Disk identifier: 0x4dcc4dcc 设备 Boot Start End Blocks Id System vm-xp-qa-new.img1 * 63 41913584 20956761 7 HPFS/NTFS/exFAT 从这里可以看见,这个windows分区,是从第63个块开始的,每个块的大小是512字节,最后得出的挂载偏移为32256字节。 因此,最终挂载的命令就是: [cce lang=”bash”] mount -o loop,offset=32256 -t ntfs vm-xp-qa-new.img /mnt/ [/cce] 这里指定了挂载分区的格式为ntfs,挂载偏移为32256字节。 然后就可以在/mnt目录中看见这个虚拟机磁盘镜像,直接修改里面的文件了。 二、通过映射成loop设备 首先获取最近一个可用的loop设备: [cce lang=”bash”] losetup -f –show vm-linux-qa-master.img [/cce] 这里会输出一个loop设备,如果之前没有映射过,应该是/dev/loop0 然后生成对应的块设备: [cce lang=”bash”] kpartx -a /dev/loop0 [/cce] 执行之后,会在/dev/mapper/中多出一个loop0p1文件,也就是刚那个磁盘镜像的分区块文件了(对应硬盘的sda和sda1这样的关系)。 然后就可以直接将/dev/mapper/loop0p1文件mount到指定位置,就可以读写里面的文件了。 清理的时候,除了umount /dev/mapper/loop0p1这个文件的挂载点之外,还要清除loop0的使用: [cce lang=”bash”] kpartx -d /dev/loop0 losetup -d /dev/loop0 [/cce] ps:在gentoo中,losetup应该已经在的,在sys-apps/util-linux这个包中;kpartx由sys-fs/multipath-tools这个包提供,直接emerge即可 转载自:https://coolex.info/blog/385.html
文件监控直接通过了linux的inotify接口实现。这里没有考虑移植性,也就没像tailf那样,通过宏来判断是否支持inotify,如果不支持,降级使用循环轮寻的方式读取。 inotify的使用还是比较方便的基本上就是:inotify_init,inotify_add_watch,然后配合read系统调用,获取文件修改信息。因此实现也非常方便。 首先是在构造函数里面初始化inotify: [cce lang=”cpp”] inotifyFd = inotify_init(); [/cce] 然后提供一个watch接口,通过传入前文描述的TFile对象和内容读取的回调函数,添加对应文件的监控和回调。 [cce lang=”cpp”] void FileWatcher::watch ( boost::shared_ptr< TFile > tFile, std::list< FileWatcher::ReadCallBack > callBackList ) { if(!tFile->hasError() && !callBackList.empty()) { int wd = inotify_add_watch(inotifyFd, tFile->name().c_str(), IN_MODIFY); if(wd > 0) { tFileMap.insert(std::make_pair<int, boost::shared_ptr<TFile> >(wd, tFile)); callBackMap.insert(std::make_pair<int, std::list<ReadCallBack> >(wd, callBackList)); //init read std::string initContent = tFile->read(); BOOST_FOREACH(ReadCallBack &callback, callBackList) { callback(initContent); } } } } [/cce] 这里通过TFile的文件名,向内核注册添加该文件的modify事件,并且在注册成功之后,进行初始读取(这里有个小问题,由于后面websocket端没有做缓存,所以由于初始读取的时候还没有任何websocket客户端连接,所以通过web无法读取初始内容,也就是文件最后10行)。同时,这个类维护两个hashmap,分别是监听描述符wd->tFile和wd->callbacklist。 监听完成后,就是启动监听,也就是通过读取fd,感知被监听文件的变更,由于这里只监听了文件修改,那么读取到这个事件之后,就可以对该文件进行增量读取(前文已经描述了读取方法)。 [cce lang=”cpp”] char * buffer = new char[inotifyBufferSize]; while(!_interrupted) { if(read(inotifyFd, buffer, inotifyBufferSize) < 0) { if(errno == EINTR) { // interrupt delete buffer; return; } } struct inotify_event *event = ( struct inotify_event * ) buffer; int wd = event->wd; BOOST_AUTO(tFileIter, tFileMap.find(wd)); if(tFileIter != tFileMap.end()) { boost::shared_ptr<TFile> tFile = tFileIter->second; std::string content = tFile->read(); BOOST_AUTO(iter, callBackMap.find(wd)); if(iter != callBackMap.end()) { std::list<ReadCallBack> callbacks = iter->second; BOOST_FOREACH(ReadCallBack &callback, callbacks) { callback(content); } } } } delete buffer; [/cce] 这里参照inotify的文档,首先读取缓冲区大小设置为 [cce lang=”cpp”] static const int inotifyBufferSize = sizeof(struct inotify_event) + NAME_MAX + 1; [/cce] 也就是inotify_event结构的长度,和名字最大值。由于inotify_event是变长字段(包含一个可选的文件名字段),所以这里采用了系统限制的文件名最大值NAME_MAX,这个宏在climits中定义,在linux中大小为255字节。 然后通过系统调用read,读取文件描述符inotifyFd,这里如果没有新的事件产生,read会进入阻塞状态,节省系统资源。如果有返回,则处理返回的inotify_event对象(注意在监听modify事件的时候,是没有文件名的)。通过结构中的wd,从之前保存的hashmap中获取对应的tFile对象进行增量读取,然后再读取wd对应的回调函数,将读取内容返回。 这里有个小问题需要处理,就是如何中断读取。之前为了在gtest中能够通过单元测试的方式进行测试,通过查看手册可以知道,如果read调用被系统信号中断,会标记错误码为EINTR。所以,当读取失败的时候,可以通过对ERRNO检查,判断是否是信号中断。 由于程序会一直运行,知道通过信号终止,所以析构变的不是很重要了。这里析构函数里面通过调用inotify_rm_watch将之前保存的wd全部去掉,然后通过close调用交inotify的文件描述符关闭即可: [cce lang=”cpp”] FileWatcher::~FileWatcher() { if(inotifyFd > 0) { boost::unordered::unordered_map<int, boost::shared_ptr<TFile> >::iterator iter; for(iter = tFileMap.begin(); iter != tFileMap.end(); ++iter) { inotify_rm_watch(inotifyFd, iter->first); } close(inotifyFd); } } [/cce] 转载自:https://coolex.info/blog/406.html
之前将盒子作为下载机,为了能够直接播放上面的电影和电视,就通过minidlna,将视频共享出来,这样能够支持通过nexus 10平板播放这些电影。但是,即使升级到了1.1.0版本,minidlna还是不能将设置的视频目录中的rmvb文件共享出来。 参照网上搜索到的让 minidlna 支持 rmvb、gb2312 mp3 标签这篇文章,根据里面的patch,修改了1.1.0版本的minidlna之后,可以在平板上看见rmvb文件了。 由于前文提到的补丁,是针对1.0版本的minidlna,参照补丁修改的位置,自己对补丁进行了修改,重新写了一个ebuild文件之后,搞定了。新的补丁文件: [cce lang=”diff” ] diff -ur minidlna-1.1.0/metadata.c minidlna-1.1.0.new/metadata.c — minidlna-1.1.0/metadata.c 2013-03-09 08:03:03.000000000 +0800 +++ minidlna-1.1.0.new/metadata.c 2013-09-07 20:19:31.700278182 +0800 @@ -903,6 +903,8 @@ xasprintf(&m.mime, "video/x-matroska"); else if( strcmp(ctx->iformat->name, "flv") == 0 ) xasprintf(&m.mime, "video/x-flv"); + else if( strcmp(ctx->iformat->name, "rm") == 0 ) + asprintf(&m.mime, "video/x-pn-realvideo"); if( m.mime ) goto video_no_dlna; diff -ur minidlna-1.1.0/upnpglobalvars.h minidlna-1.1.0.new/upnpglobalvars.h — minidlna-1.1.0/upnpglobalvars.h 2013-04-05 07:39:12.000000000 +0800 +++ minidlna-1.1.0.new/upnpglobalvars.h 2013-09-07 20:21:40.564283420 +0800 @@ -168,7 +168,8 @@ "http-get:*:audio/mp4:*," \ "http-get:*:audio/x-wav:*," \ "http-get:*:audio/x-flac:*," \ – "http-get:*:application/ogg:*" + "http-get:*:application/ogg:*,"\ + "http-get:*:video/x-pn-realvideo:*" #define DLNA_FLAG_DLNA_V1_5 0x00100000 #define DLNA_FLAG_HTTP_STALLING 0x00200000 diff -ur minidlna-1.1.0/utils.c minidlna-1.1.0.new/utils.c — minidlna-1.1.0/utils.c 2013-04-03 07:29:21.000000000 +0800 +++ minidlna-1.1.0.new/utils.c 2013-09-07 20:18:40.796283001 +0800 @@ -375,6 +375,7 @@ ends_with(file, ".m2t") || ends_with(file, ".mkv") || ends_with(file, ".vob") || ends_with(file, ".ts") || ends_with(file, ".flv") || ends_with(file, ".xvid") || + ends_with(file, ".rm") || ends_with(file, ".rmvb") || #ifdef TIVO_SUPPORT ends_with(file, ".TiVo") || #endif [/cce] ebuild文件就是在src_prepare阶段,增加了:epatch “${FILESDIR}”/${PN}-1.1.0-rmvb.patch,打上这个补丁即可。 转载自:https://coolex.info/blog/414.html
尝试通过cpp-netlib来做http服务器,但是这个库只能简单的解析http结构,像cookie等结构,都要自己解析,了解到spirit可以通过类似bnf范式格式定义字符串格式并解析。 boost本身有个类似的例子,解析的是通过分号或者&符号分割的键值对字符串,并放到对应的map中去。具体代码可以参照这里。所以基于这个代码,简单的进行修改之后,就能解析http cookie了。 首先,http cookie的格式,定义在rfc6265上。这里定义了服务器发送给浏览器的Set-Cookie头格式,和浏览器发给服务器的Cookie头的BNF范式。这里定义的太复杂,解析的时候没有考虑到这么多字符(特别是排除一些控制字符),大致的代码: [cce lang=”cpp”] namespace parser { namespace qi = boost::spirit::qi; typedef std::map<std::string, std::string> pairs_type; template <typename Iterator> struct cooke_sequence : qi::grammar<Iterator, pairs_type()> { qi::rule<Iterator, pairs_type()> query; qi::rule<Iterator, std::pair<std::string, std::string>()> pair; qi::rule<Iterator, std::string()> key, value; cooke_sequence() : cooke_sequence::base_type(query) { query = pair >> *(qi::lit(';') >> pair); pair = *qi::lit(' ') >> key >> *qi::lit(' ') >> -('=' >> *qi::lit(' ') >> value >> *qi::lit(' ')); key = qi::char_("a-zA-Z_%") >> *qi::char_("a-zA-Z_0-9%"); value = +(qi::char_ – ';'); } }; } [/cce] 这里简化了key和value,特别是value,只要是非分号的,都能解析到value中。使用也非常简单: [cce lang=”cpp”] namespace qi = boost::spirit::qi; parser::cooke_sequence<std::string::const_iterator> p; parser::pairs_type value; if(qi::parse(c.begin(), c.end(), p, value)) { for(auto &entry : value) { cookies.insert(std::make_pair(entry.first, entry.second)); } } [/cce] 直接实例化cooe_sequence,将cookie字符串传入,就可以解析成map,然后再放入到自己的结构体中。这里直接抄了示例中的代码,所以直接使用了map,一般cookie没必要排序,可以直接使用unordered_map,通过hash表存放。 转载自:https://coolex.info/blog/421.html
chrome插件将js直接注入页面有两种方式,一种是通过Manifest文件中指定js文件,一种是通过chrome.tab.executeScript方式注入。具体可以参考这个官方文档。 由于各种需求,需要将部分js从后端服务器中进行加载。所以只能通过tab.executeScript的方式。具体函数的定义,可以参考官方文档。大致就是能够给定一个文件或者一段代码,在指定的tab中运行。 首先,这个函数必须在background中执行,页面中的content-script本身,是没有权限调用chrome.tab api的。所以,如果页面中的js要执行,需要通过chrome插件的通信机制(port, message),通知background才能运行。 其次,运行的目标tab,可以指定,也可以传null,如果目标tab为null,则表示需要执行的tab是“当前tab”。所以如果使用了null,则在注入前千万不能换tab,否则就注入到其他tab中去了。 然后,参数InjectDetails中可以指定是执行code,还是file。如果是file,可以是本地文件(插件中打包的文件),也可以是远程文件。特别注意,通过file执行远程文件,有着严格的约束(参照这里):‘Currently, we allow whitelisting origins with the following schemes: HTTPS, chrome-extension, and chrome-extension-resource.’也就是说,只能执行插件内部打包的文件,或者基于https的远程地址。其他还有例外的就是给开发者用的localhost了。我们要加载的资源,不可能放在本地,也不可能打包到插件中,也没有办法去搞到https的证书。所以不能通过执行file的方式加载远程js。 要解决这个问题,只能通过执行code的方式。由于我们在manifest文件中配置了background拥有跨域请求资源的权限,我们可以直接在background中,远程通过xhr的方式获取js内容,然后进行code的执行。这样就能绕开之前遇到的无法执行http协议的js文件的问题。大致的代码如下: [cce lang=”javascript”] var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if (xhr.readyState == 4 && xhr.status == 200) { var code = xhr.responseText; chrome.tabs.executeScript(null, {code: code, allFrames: true}, function(){ if(typeof callback === 'function') { callback(); } }); } } var ts = new Date().getTime(); var u; if(url.indexOf('?') === -1){ u = url + '?_t=' + ts; } else { u = url + '&_t=' + ts; } xhr.open('GET',u,true); xhr.send(null); [/cce] 最后,还有一个小问题,就是InjectDetails中的allFrames参数。这个参数大致的作用,就是如果为true,js会在页面的所有frame中进行执行,否则就只在页面的top frame中进行执行。由于frame之间的document会互相隔离,所有如果要让frame之间能够通信,就需要设置成true,把代码注入到所有的frame中去。 转载自:https://coolex.info/blog/424.html
redis主备同步非常方便,通过slaveof命令即可同步。上周应用切换了redis,想让新的redis和旧的redis进行数据同步,结果把新redis中其他应用已经写入的所有数据都删除了。。。这里记录下恢复方法。 首先先恢复redis的dump文件。还好现在阿里云弹性计算集群给每个镜像每天都进行了备份,通过操作前一天的备份镜像,快照制作当时的整个redis目录复制了出来,作为恢复的基础。 然后将恢复回来的文件再复制一份备份,原先那份修改redis的配置文件,修改启动端口(我从默认的6379改成了6479),然后启动旧的redis,成功将当时的dump文件载入到新的redis实例中。 查询到redis有migrate命令,能够将key迁移到另外一个redis实例中(具体参考这里)。通过bash命令,循环的将备份的实例所有key,都试图通过migrate命令迁移到新的redis中。 迁移过程中,迁移成功的key,在旧的实例中会被删除,失败的key,可以看见失败原因都是key is busy。也就是说,要迁移的key在新的实例中已经存在了。 和使用方确定了所有已经存在的key,是hash类型的,都是要保留的数据。因此,通过一个简单的shell脚本,读取备份实例中hash类型key,并添加到新的redis中。bash脚本为: [cce lang=”bash”] #!/bin/bash OLD_REDIS="./redis-cli -p 6479" NEW_REDIS="./redis-cli" KEYS=`$OLD_REDIS keys '*'` for k in $KEYS do key_type=`$OLD_REDIS type $k` if [ $key_type = "hash" ]; then hash_keys=`$OLD_REDIS HKEYS $k` for hash_key in $hash_keys do hash_value=`$OLD_REDIS HGET $k $hash_key` $NEW_REDIS HSET $k $hash_key $hash_value echo "merge $k $hash_key to new redis" done #eval "$OLD_REDIS DEL $k" fi done [/cce] 逻辑非常简单,就是先通过keys命令获取到所有迁移失败的key,然后通过type命令获取类型。如果是hash类型的key,遍历所有的hash key,然后读取出一个key下所有的键值对,并通过hset命令放到新的redis实例中。最后将合并完成的key从备份实例中删除。这里由于业务上只需要合并hash类型的,其他容器类型(list, set等)也可以通过类似的方式做新老合并。 转载自:https://coolex.info/blog/427.html
之前参照yegal的文章在mac上安装了gentoo-prefix。但是在emerge git的时候,会发现如果增加了subversion这个USE,就会编译失败。 从编译失败的错误上,可以看出,编译失败的来源是svn相关的代码,然后错误是链接的时候提示一些符号找不到: [cce] Undefined symbols for architecture x86_64: “_libintl_ngettext”, referenced from: _show_date_relative in libgit.a(date.o) “_libintl_gettext”, referenced from: _show_date_relative in libgit.a(date.o) _warn_on_inaccessible in libgit.a(wrapper.o) _xgetpwuid_self in libgit.a(wrapper.o) ld: symbol(s) not found for architecture x86_64 [/cce] 大致可以看出,是intl相关的库没有链接。在gentoo的bugzilla上也查到了类似的bug。按照附件提供的补丁,需要判断当前系统为mac的时候,增加-lintl,以链接intl这个库。 除了这个库之外,还有一个iconv相关的符号找不到。bug里面没有描述。在另一台gentoo的机器上用e-file查询了之后,发现原生linux的iconv是由glibc提供的。但是prefix是不能自由安装glibc的库的。但是系统里面已经安装了dev-libs/libiconv这个包,提供了iconv相关的库。因此和前面一样,需要手工在链接的时候增加-liconv。 最后修改完的ebuild文件大致为: [cce lang=”diff”] — git-1.9.2.ebuild.old 2014-04-20 15:10:34.000000000 +0800 +++ git-1.9.2.ebuild 2014-04-20 15:09:54.000000000 +0800 @@ -324,6 +324,7 @@ if use subversion ; then cd “${S}”/contrib/svn-fe + [[ ${CHOST} = *-darwin* ]] && EXTLIBS=”${EXTLIBS} -lintl -liconv” git_emake EXTLIBS=”${EXTLIBS}” || die “emake svn-fe failed” if use doc ; then git_emake svn-fe.{1,html} || die “emake svn-fe.1 svn-fe.html failed” [/cce] 转载自:https://coolex.info/blog/431.html
SDS(Simple Dynamic String):对C字符串的封装,可修改、可自动伸缩的字符串实现。Redis默认的字符串实现。 SDS定义:(sds.h) [cce lang=”c”] struct sdshdr { unsigned int len; unsigned int free; char buf[]; }; [/cce] 与C字符串的区别: * 常数复杂度获取字符串长度(字符串长度已经记录在结构体中) * 杜绝缓冲区溢出(每次操作前都会检查空间是否充足,自动扩张和收缩) * 减少修改字符串带来的内存重分配次数: * * 空间预分配(提前预留空间) * 惰性空间释放(释放的空间暂时保留,防止扩张) * 二进制安全(不采用\0表示结束) * 兼容部分C字符串函数(buf数组多保存了一个\0,用于兼容部分C字符串函数) API: typedef char *sds; 创建一个字符串:sds sdsnew(const char *init); [cce lang=”c”] sds sdsnew(const char *init) { size_t initlen = (init == NULL) ? 0 : strlen(init); return sdsnewlen(init, initlen); } sds sdsnewlen(const void *init, size_t initlen) { struct sdshdr *sh; //根据sdshdr结构分配内存,多一个用来放\0 if (init) { sh = zmalloc(sizeof(struct sdshdr)+initlen+1); } else { sh = zcalloc(sizeof(struct sdshdr)+initlen+1); } if (sh == NULL) return NULL; sh->len = initlen; sh->free = 0; //初始字符串不为NULL,复制过去,然后最后补上\0 if (initlen && init) memcpy(sh->buf, init, initlen); sh->buf[initlen] = ‘\0′; return (char*)sh->buf; } [/cce] 拼接字符串:sds sdscat(sds s, const char *t); [cce lang=”c”] sds sdscat(sds s, const char *t) { return sdscatlen(s, t, strlen(t)); } sds sdscatlen(sds s, const void *t, size_t len) { struct sdshdr *sh; size_t curlen = sdslen(s); s = sdsMakeRoomFor(s,len); if (s == NULL) return NULL; sh = (void*) (s-(sizeof(struct sdshdr))); memcpy(s+curlen, t, len); sh->len = curlen+len; sh->free = sh->free-len; s[curlen+len] = ‘\0′; return s; } sds sdsMakeRoomFor(sds s, size_t addlen) { struct sdshdr *sh, *newsh; size_t free = sdsavail(s); size_t len, newlen; //仍然有空闲,直接返回 if (free >= addlen) return s; len = sdslen(s); sh = (void*) (s-(sizeof(struct sdshdr))); newlen = (len+addlen); //新的空间比最大分配空间小,扩容两倍 //#define SDS_MAX_PREALLOC (1024*1024) if (newlen < SDS_MAX_PREALLOC) newlen *= 2; else newlen += SDS_MAX_PREALLOC; //重新分配空间:sdshdr+字符串长度+1(\0) newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); if (newsh == NULL) return NULL; newsh->free = newlen – len; return newsh->buf; } static inline size_t sdsavail(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->free; } [/cce] 转载自:https://coolex.info/blog/434.html
Redis字典采用哈希表实现。 哈希表: [cce lang=”c”] typedef struct dictht { //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表掩码,用于计算索引值,总是等于size – 1 unsigned long size mask; //已有的节点数量 unsigned long used; } dictht; [/cce] 哈希表节点: [cce lang=”c”] typedef struct dictEntry { //键 void *key; //值 union { void *val; uint64_t u64; int64_t s64; double d; } v; //下一个哈希表节点 struct dictEntry *next; } dictEntry; [/cce] 字典: [cce lang=”c”] typedef struct dict { //类型特定的函数 dictType *type; //私有数据 void *privdata; //哈希表 dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ int iterators; /* number of iterators currently running */ } dict; [/cce] * type属性是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数 * privdata保存了需要传给类型特定函数的可选参数 [cce lang=”c”] typedef struct dictType { //计算哈希值的函数 unsigned int (*hashFunction)(const void *key); //复制键的函数 void *(*keyDup)(void *privdata, const void *key); //复制值的函数 void *(*valDup)(void *privdata, const void *obj); //对比键的函数 int (*keyCompare)(void *privdata, const void *key1, const void *key2); //销毁键的函数 void (*keyDestructor)(void *privdata, void *key); //销毁值的函数 void (*valDestructor)(void *privdata, void *obj); } dictType; [/cce] ht属性包含两个项的数组。一般情况下只使用ht[0]哈希表,ht[1]哈希表只在对ht[0]进行rehash的时候才会使用。 哈希算法: 计算: hash = dict->type->hashFunction(key) index = hash & dict->ht[x].sizemask 当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2(https://code.google.com/p/smhasher/wiki/MurmurHash2)算法计算键的哈希值。 int dictAdd(dict *d, void *key, void *val); [cce lang=”c”] int dictAdd(dict *d, void *key, void *val) { dictEntry *entry = dictAddRaw(d,key); if (!entry) return DICT_ERR; dictSetVal(d, entry, val); return DICT_OK; } dictEntry *dictAddRaw(dict *d, void *key) { int index; dictEntry *entry; dictht *ht; //#define dictIsRehashing(d) ((d)->rehashidx != -1) if (dictIsRehashing(d)) _dictRehashStep(d); /* Get the index of the new element, or -1 if * the element already exists. */ if ((index = _dictKeyIndex(d, key)) == -1) return NULL; /* Allocate the memory and store the new entry */ ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0]; entry = zmalloc(sizeof(*entry)); entry->next = ht->table[index]; ht->table[index] = entry; ht->used++; /* Set the hash entry fields. */ dictSetKey(d, entry, key); return entry; } static int _dictKeyIndex(dict *d, const void *key) { unsigned int h, idx, table; dictEntry *he; /* Expand the hash table if needed */ if (_dictExpandIfNeeded(d) == DICT_ERR) return -1; /* Compute the key hash value */ //#define dictHashKey(d, key) (d)->type->hashFunction(key) h = dictHashKey(d, key); for (table = 0; table <= 1; table++) { idx = h & d->ht[table].sizemask; /* Search if this slot does not already contain the given key */ he = d->ht[table].table[idx]; while(he) { //比较key是否已经存在,已经存在返回-1 if (dictCompareKeys(d, key, he->key)) return -1; he = he->next; } if (!dictIsRehashing(d)) break; } return idx; } static int _dictExpandIfNeeded(dict *d) { /* Incremental rehashing already in progress. Return. */ if (dictIsRehashing(d)) return DICT_OK; /* If the hash table is empty expand it to the initial size. */ //#define DICT_HT_INITIAL_SIZE 4 if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE); /* If we reached the 1:1 ratio, and we are allowed to resize the hash * table (global setting) or we should avoid it but the ratio between * elements/buckets is over the “safe” threshold, we resize doubling * the number of buckets. */ //static unsigned int dict_force_resize_ratio = 5; /* dict_can_resize设置: void updateDictResizePolicy(void) { if (server.rdb_child_pid == -1 && server.aof_child_pid == -1) dictEnableResize(); else dictDisableResize(); } 当有同步硬盘进程的时候改成不能扩充 */ if (d->ht[0].used >= d->ht[0].size && (dict_can_resize || d->ht[0].used/d->ht[0].size > dict_force_resize_ratio)) { return dictExpand(d, d->ht[0].used*2); } return DICT_OK; } int dictExpand(dict *d, unsigned long size) { dictht n; /* the new hash table */ unsigned long realsize = _dictNextPower(size); /* the size is invalid if it is smaller than the number of * elements already inside the hash table */ if (dictIsRehashing(d) || d->ht[0].used > size) return DICT_ERR; /* Allocate the new hash table and initialize all pointers to NULL */ n.size = realsize; n.sizemask = realsize-1; n.table = zcalloc(realsize*sizeof(dictEntry*)); n.used = 0; /* Is this the first initialization? If so it’s not really a rehashing * we just set the first hash table so that it can accept keys. */ if (d->ht[0].table == NULL) { d->ht[0] = n; return DICT_OK; } /* Prepare a second hash table for incremental rehashing */ d->ht[1] = n; d->rehashidx = 0; return DICT_OK; } int dictReplace(dict *d, void *key, void *val); /* Add an element, discarding the old if the key already exists. * Return 1 if the key was added from scratch, 0 if there was already an * element with such key and dictReplace() just performed a value update * operation. */ int dictReplace(dict *d, void *key, void *val) { dictEntry *entry, auxentry; /* Try to add the element. If the key * does not exists dictAdd will suceed. */ if (dictAdd(d, key, val) == DICT_OK) return 1; /* It already exists, get the entry */ entry = dictFind(d, key); /* Set the new value and free the old one. Note that it is important * to do that in this order, as the value may just be exactly the same * as the previous one. In this context, think to reference counting, * you want to increment (set), and then decrement (free), and not the * reverse. */ auxentry = *entry; dictSetVal(d, entry, val); dictFreeVal(d, &auxentry); return 0; } dictEntry *dictFind(dict *d, const void *key) { dictEntry *he; unsigned int h, idx, table; if (d->ht[0].size == 0) return NULL; /* We don’t have a table at all */ if (dictIsRehashing(d)) _dictRehashStep(d); h = dictHashKey(d, key); for (table = 0; table <= 1; table++) { idx = h & d->ht[table].sizemask; he = d->ht[table].table[idx]; while(he) { if (dictCompareKeys(d, key, he->key)) return he; he = he->next; } if (!dictIsRehashing(d)) return NULL; } return NULL; } [/cce] int dictRehash(dict *d, int n); [cce lang=”c”] int dictRehash(dict *d, int n) { if (!dictIsRehashing(d)) return 0; while(n–) { dictEntry *de, *nextde; /* Check if we already rehashed the whole table… */ //已经完成hash,释放ht[0]。将ht[0]指向ht[1] if (d->ht[0].used == 0) { zfree(d->ht[0].table); d->ht[0] = d->ht[1]; _dictReset(&d->ht[1]); d->rehashidx = -1; return 0; } /* Note that rehashidx can’t overflow as we are sure there are more * elements because ht[0].used != 0 */ assert(d->ht[0].size > (unsigned long)d->rehashed); //如果rehash索引为空,跳过 while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++; de = d->ht[0].table[d->rehashidx]; /* Move all the keys in this bucket from the old to the new hash HT */ //移动一个桶里面的所有key到新的哈希表 while(de) { unsigned int h; nextde = de->next; /* Get the index in the new hash table */ h = dictHashKey(d, de->key) & d->ht[1].sizemask; de->next = d->ht[1].table[h]; d->ht[1].table[h] = de; d->ht[0].used–; d->ht[1].used++; de = nextde; } d->ht[0].table[d->rehashidx] = NULL; d->rehashidx++; } return 1; } //为了防止占用太多的CPU时间,限制一次rehash的CPU时间 int dictRehashMilliseconds(dict *d, int ms) { long long start = timeInMilliseconds(); int rehashes = 0; while(dictRehash(d,100)) { rehashes += 100; if (timeInMilliseconds()-start > ms) break; } return rehashes; } [/cce] 调用者(redis.c):每次尝试渐进式rehash执行1ms [cce lang=”c”] int incrementallyRehash(int dbid) { /* Keys dictionary */ if (dictIsRehashing(server.db[dbid].dict)) { dictRehashMilliseconds(server.db[dbid].dict,1); return 1; /* already used our millisecond for this loop… */ } /* Expires */ if (dictIsRehashing(server.db[dbid].expires)) { dictRehashMilliseconds(server.db[dbid].expires,1); return 1; /* already used our millisecond for this loop… */ } return 0; } [/cce] 转载自:https://coolex.info/blog/439.html
snoopy是什么?刚了解这货的时候,是公司服务器上有snoopy的so无法加载的错误,然后是系统日志里面一堆日志,导致机器空间不足。官方说明是: Snoopy is designed to aid a sysadmin by providing a log of commands executed.也就是说这货会监控服务器上的命令执行,并记录到syslog。 首先来看下这个功能是怎么被完成的。首先会发现,服务器上的/etc/ld.so.preload这个文件被修改,强制可执行程序加载之前加载snoopy的so: [cce] #cat /etc/ld.so.preload /usr/local/snoopy/lib/snoopy.so [/cce] 关于ld.so.preload和LD_PRELOAD,可以参考man ld.so: LD_PRELOAD A whitespace-separated list of additional, user-specified, ELF shared libraries to be loaded before all others. This can be used to selectively override functions in other shared libraries. For set-user-ID/set-group-ID ELF binaries, only libraries in the standard search directories that are also set-user-ID will be loaded. /etc/ld.so.preload File containing a whitespace separated list of ELF shared libraries to be loaded before the program. 也就是说,snoopy.so会在所有ELF文件加载之前预加载,确保将一些系统调用被这货劫持。 如果在机器上执行 [cce lang=”bash”] ls /notfound [/cce] 会在系统日志中记录类似: snoopy[25505]: [uid:0 sid:9701 tty:/dev/pts/15 cwd:/root filename:/bin/ls]: ls /notfound 这样的内容。通过ldd,可以看下一个命令加载的动态链接库: [cce lang=”bash”] #ldd /bin/ls linux-vdso.so.1 => (0x00007fff99fff000) /usr/local/snoopy/lib/snoopy.so (0x00007fc407938000) librt.so.1 => /lib64/librt.so.1 (0x0000003e4f600000) libacl.so.1 => /lib64/libacl.so.1 (0x0000003e50200000) libselinux.so.1 => /lib64/libselinux.so.1 (0x0000003e4fa00000) libc.so.6 => /lib64/libc.so.6 (0x0000003e4e200000) libdl.so.2 => /lib64/libdl.so.2 (0x0000003e4e600000) libpthread.so.0 => /lib64/libpthread.so.0 (0x0000003e4ea00000) /lib64/ld-linux-x86-64.so.2 (0x0000003e4de00000) libattr.so.1 => /lib64/libattr.so.1 (0x0000003e4f200000) libsepol.so.1 => /lib64/libsepol.so.1 (0x0000003e4fe00000) [/cce] 果然snoopy被最早加载了。 那么如果想不要snoopy加载,又有什么办法呢? 搜索了半天,没有看见Linux中有什么办法能够将设置在ld.so.preload中预先加载的so给卸载掉,但是有一个值得注意的是:LD_PRELOAD加载的优先级高于ld.so.preload。 我们能不能通过更早的加载被劫持的系统调用,来避免被劫持呢?答案是肯定的。 首先,我们可以猜测出来这货是不会劫持太多的系统调用,通过nm命令看下snoopy的符号表: [cce lang=”bash”] #nm /usr/local/snoopy/lib/snoopy.so … 0000000000000890 T execv 0000000000000b00 T execve … [/cce] 这两个系统调用应该是非常熟悉的,函数定义在unistd.h中,主要用于创建新的进程,并加载另一个可执行程序。只要截获了这几个系统调用,就能知道要执行什么命令了。 我们还可以再用bash来验证下: [cce lang=”bash”] #strace bash -c “ls /abc” … connect(3, {sa_family=AF_FILE, path=”/dev/log”…}, 110) = 0 sendto(3, “<86>Sep 23 04:34:56 snoopy[28052″…, 104, MSG_NOSIGNAL, NULL, 0) = 104 execve(“/bin/ls”, [“ls”, “/abc”], [/* 29 vars */]) = 0 … [/cce] 从这里可以看出两个地方,首先bash在执行ls的时候,的确是通过execve这个系统调用的;其次,这个系统调用已经被snoopy劫持了(向日志设备发送了数据)。 最后,通过代码也验证了这个猜想:代码在这里 [cce lang=”c”] int execv (const char *filename, char *const argv[]) { static int (*func)(const char *, char **); FN(func,int,”execv”,(const char *, char **const)); snoopy_log_syscall_execv(filename, argv); return (*func) (filename, (char **) argv); } int execve (const char *filename, char *const argv[], char *const envp[]) { static int (*func)(const char *, char **, char **); FN(func,int,”execve”,(const char *, char **const, char **const)); snoopy_log_syscall_execve(filename, argv, envp); return (*func) (filename, (char**) argv, (char **) envp); } [/cce] 知道了原理,想到绕开,就很简单了。首先,找到execve真实的提供者:从之前ls命令的动态链接库就可以看见大部分系统的系统调用,都在/lib64/libc.so.6中,用nm命令验证下: [cce lang=”bash”] #nm /lib64/libc.so.6 | grep execv 0000003e4e29acf0 t __GI_execvp 0000003e4e29a880 t __execve 0000003e4e29a980 T execv 0000003e4e29a880 W execve 0000003e4e29acf0 T execvp 0000003e4e29a8b0 T fexecve [/cce] 因此,然后尝试执行: [cce lang=”bash”] LD_PRELOAD=”/lib64/libc.so.6″ bash -c “ls /abc” [/cce] 再去查看系统日志,会发现只记录了bash -c “ls /abc”这个命令,真正执行的ls /abc这个命令没有记录。 通过strace也可以看见,在执行ls之前,没有了和系统日志交互的连接了。 stat(“/bin/ls”, {stmode=SIFREG|0755, st_size=91272, …}) = 0 access(“/bin/ls”, X_OK) = 0 access(“/bin/ls”, R_OK) = 0 rtsigaction(SIGINT, {SIGDFL, [], SARESTORER, 0x3e4e2302d0}, {SIGDFL, [], SA_RESTORER, 0x3e4e2302d0}, 8) = 0 rtsigaction(SIGQUIT, {SIGDFL, [], SARESTORER, 0x3e4e2302d0}, {0x1, [], SARESTORER, 0x3e4e2302d0}, 8) = 0 rtsigaction(SIGCHLD, {SIGDFL, [], SARESTORER, 0x3e4e2302d0}, {0x436360, [], SARESTORER, 0x3e4e2302d0}, 8) = 0 execve(“/bin/ls”, [“ls”, “/abc”], [/* 30 vars */]) = 0 也就是说,如果你登陆服务器之后,执行了: [cce lang=”bash”] LD_PRELOAD=”/lib64/libc.so.6″ bash [/cce] 用当前被snoopy劫持的bash再创建一个没有被snoopy劫持的bash,之后执行的所有命令,都不会再被记录。 转载自:https://coolex.info/blog/445.html
Redis使用跳跃表和字典共同来实现有序集合键(sorted set)。 定义: 跳跃表节点: [cce lang=”c”] typedef struct zskiplistNode { //成员对象 robj *obj; //分值 double score; //后退指针 struct zskiplistNode *backward; //层结构 struct zskiplistLevel { //前进指针 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; } zskiplistNode; [/cce] 跳跃表定义: [cce lang=”c”] typedef struct zskiplist { //跳跃表头、尾指针 struct zskiplistNode *header, *tail; //跳跃表长度 unsigned long length; //跳跃表层数 int level; } zskiplist; [/cce] zskiplist *zslCreate(void); [cce lang=”c”] zskiplist *zslCreate(void) { int j; zskiplist *zsl; zsl = zmalloc(sizeof(*zsl)); zsl->level = 1; zsl->length = 0; //创建头节点,头节点永远有最高层 //#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */ zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); //初始化头节点的每一层 for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) { zsl->header->level[j].forward = NULL; zsl->header->level[j].span = 0; } zsl->header->backward = NULL; zsl->tail = NULL; return zsl; } zskiplistNode *zslCreateNode(int level, double score, robj *obj) { zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); zn->score = score; zn->obj = obj; return zn; } [/cce]zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj); [cce lang=”c”] zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) { zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; unsigned int rank[ZSKIPLIST_MAXLEVEL]; int i, level; redisAssert(!isnan(score)); x = zsl->header; //遍历查找当前score对应的插入位置 for (i = zsl->level-1; i >= 0; i–) { /* store rank that is crossed to reach the insert position */ rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; while (x->level[i].forward && (x->level[i].forward->score < score || (x->level[i].forward->score == score && compareStringObjects(x->level[i].forward->obj,obj) < 0))) { //累加跨度 rank[i] += x->level[i].span; x = x->level[i].forward; } //当前层要更新的节点 update[i] = x; } /* we assume the key is not already inside, since we allow duplicated * scores, and the re-insertion of score and redis object should never * happen since the caller of zslInsert() should test in the hash table * if the element is already inside or not. */ //随机一个当前节点的层数 level = zslRandomLevel(); //如果是最高层,需要修正 //在以前最高层以上的,跨度都修复为跳跃表原长度,前置节点都改为头结点 if (level > zsl->level) { for (i = zsl->level; i < level; i++) { rank[i] = 0; update[i] = zsl->header; update[i]->level[i].span = zsl->length; } //跳跃表的总层数改成最新的层数 zsl->level = level; } x = zslCreateNode(level,score,obj); //每一层的链表构建 for (i = 0; i < level; i++) { x->level[i].forward = update[i]->level[i].forward; update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */ //当前节点每层跨度计算 x->level[i].span = update[i]->level[i].span – (rank[0] – rank[i]); //当前节点每层插入节点跨度修改 update[i]->level[i].span = (rank[0] – rank[i]) + 1; } /* increment span for untouched levels */ for (i = level; i < zsl->level; i++) { update[i]->level[i].span++; } //修改当前节点的前置节点等 x->backward = (update[0] == zsl->header) ? NULL : update[0]; if (x->level[0].forward) x->level[0].forward->backward = x; else zsl->tail = x; zsl->length++; return x; } int zslRandomLevel(void) { int level = 1; //#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */ while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) level += 1; return (level } [/cce])> )> 转载自:https://coolex.info/blog/450.html)>
整数集合(intset)用于集合键。当一个集合只包含整数值元素,并且数量不多的时候,会使用整数集合作为集合键的底层实现。相对于直接保存字符串,整数集合能够很好地节约内存,但是由于是数组保存,需要特别关注数组长度。 定义:(intset.h) [cce lang=”c”] typedef struct intset { //编码方式 uint32_t encoding; //集合包含的元素数量 uint32_t length; //保存元素的数组 int8_t contents[]; } intset; [/cce] encoding: INTSETENCINT16:content数组每一项都是一个int16_t类型的数值(有符号) INTSETENCINT32:content数组每一项都是一个int32_t类型的数值 INTSETENCINT64:content数组每一项都是一个int64_t类型的数值 整数集合支持长度升级,但是不支持长度降级。当插入的最大长度超过当前编码方式容纳的最大值的时候,会对编码类型进行升级。但是如果删除了一个大数字,整体整数集合不会再进行降级。 intset *intsetNew(void);(创建一个新的整数集合) [cce lang=”c”] intset *intsetNew(void) { intset *is = zmalloc(sizeof(intset)); //对于x86架构采用小端序,#define intrev32ifbe(v) (v) // 初始化的时候编码为INTSET_ENC_INT16 is->encoding = intrev32ifbe(INTSET_ENC_INT16); is->length = 0; return is; } [/cce]intset *intsetAdd(intset *is, int64t value, uint8t *success);(添加到整数集合) [cce lang=”c”] intset *intsetAdd(intset *is, int64_t value, uint8_t *success) { //判断当前值需要的编码类型 uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 1; /* Upgrade encoding if necessary. If we need to upgrade, we know that * this value should be either appended (if > 0) or prepended (if < 0), * because it lies outside the range of existing values. */ //如果当前值的编码类型大于当前整数集合的编码,需要进行升级 if (valenc > intrev32ifbe(is->encoding)) { /* This always succeeds, so we don’t need to curry *success. */ return intsetUpgradeAndAdd(is,value); } else { /* Abort if the value is already present in the set. * This call will populate “pos” with the right position to insert * the value when it cannot be found. */ //查找当前数值是否已经存在 if (intsetSearch(is,value,&pos)) { if (success) *success = 0; return is; } is = intsetResize(is,intrev32ifbe(is->length)+1); //移动插入点以后的元素,空出插入位置 if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1); } _intsetSet(is,pos,value); is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } static uint8_t _intsetValueEncoding(int64_t v) { if (v < INT32_MIN || v > INT32_MAX) return INTSET_ENC_INT64; else if (v < INT16_MIN || v > INT16_MAX) return INTSET_ENC_INT32; else return INTSET_ENC_INT16; } //升级整数集合 static intset *intsetUpgradeAndAdd(intset *is, int64_t value) { uint8_t curenc = intrev32ifbe(is->encoding); uint8_t newenc = _intsetValueEncoding(value); int length = intrev32ifbe(is->length); int prepend = value < 0 ? 1 : 0; /* First set new encoding and resize */ //设置新的编码 is->encoding = intrev32ifbe(newenc); //重新分配整数集合大小 is = intsetResize(is,intrev32ifbe(is->length)+1); /* Upgrade back-to-front so we don’t overwrite values. * Note that the “prepend” variable is used to make sure we have an empty * space at either the beginning or the end of the intset. */ //倒着重新设置值,防止内存覆盖 while(length–) _intsetSet(is,length+prepend,_intsetGetEncoded(is,length,curenc)); /* Set the value at the beginning or the end. */ if (prepend) //插入的值小于0,放到最前面 _intsetSet(is,0,value); else //插入的值大于0,放到最后面 _intsetSet(is,intrev32ifbe(is->length),value); //修改整数集合长度 is->length = intrev32ifbe(intrev32ifbe(is->length)+1); return is; } //重新分配整数集合大小 static intset *intsetResize(intset *is, uint32_t len) { uint32_t size = len*intrev32ifbe(is->encoding); is = zrealloc(is,sizeof(intset)+size); return is; } //获取指定位置的值 static int64_t _intsetGetEncoded(intset *is, int pos, uint8_t enc) { int64_t v64; int32_t v32; int16_t v16; if (enc == INTSET_ENC_INT64) { memcpy(&v64,((int64_t*)is->contents)+pos,sizeof(v64)); memrev64ifbe(&v64); return v64; } else if (enc == INTSET_ENC_INT32) { memcpy(&v32,((int32_t*)is->contents)+pos,sizeof(v32)); memrev32ifbe(&v32); return v32; } else { memcpy(&v16,((int16_t*)is->contents)+pos,sizeof(v16)); memrev16ifbe(&v16); return v16; } } //插入到指定位置 static void _intsetSet(intset *is, int pos, int64_t value) { uint32_t encoding = intrev32ifbe(is->encoding); if (encoding == INTSET_ENC_INT64) { ((int64_t*)is->contents)[pos] = value; memrev64ifbe(((int64_t*)is->contents)+pos); } else if (encoding == INTSET_ENC_INT32) { ((int32_t*)is->contents)[pos] = value; memrev32ifbe(((int32_t*)is->contents)+pos); } else { ((int16_t*)is->contents)[pos] = value; memrev16ifbe(((int16_t*)is->contents)+pos); } } //查找数值是否存在(二分查找) static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) { int min = 0, max = intrev32ifbe(is->length)-1, mid = -1; int64_t cur = -1; /* The value can never be found when the set is empty */ //为空直接退出 if (intrev32ifbe(is->length) == 0) { if (pos) *pos = 0; return 0; } else { /* Check for the case where we know we cannot find the value, * but do know the insert position. */ //如果插入数值比最大的大或者比最小的小,直接退出,设置pos if (value > _intsetGet(is,intrev32ifbe(is->length)-1)) { if (pos) *pos = intrev32ifbe(is->length); return 0; } else if (value < _intsetGet(is,0)) { if (pos) *pos = 0; return 0; } } //折半查找 while(max >= min) { mid = ((unsigned int)min + (unsigned int)max) >> 1; cur = _intsetGet(is,mid); if (value > cur) { min = mid+1; } else if (value < cur) { max = mid-1; } else { break; } } if (value == cur) { if (pos) *pos = mid; return 1; } else { if (pos) *pos = min; return 0; } } //从指定位置开始移动到最尾 static void intsetMoveTail(intset *is, uint32_t from, uint32_t to) { void *src, *dst; uint32_t bytes = intrev32ifbe(is->length)-from; uint32_t encoding = intrev32ifbe(is->encoding); if (encoding == INTSET_ENC_INT64) { src = (int64_t*)is->contents+from; dst = (int64_t*)is->contents+to; bytes *= sizeof(int64_t); } else if (encoding == INTSET_ENC_INT32) { src = (int32_t*)is->contents+from; dst = (int32_t*)is->contents+to; bytes *= sizeof(int32_t); } else { src = (int16_t*)is->contents+from; dst = (int16_t*)is->contents+to; bytes *= sizeof(int16_t); } memmove(dst,src,bytes); } intset *intsetRemove(intset *is, int64_t value, int *success);(移除元素) intset *intsetRemove(intset *is, int64_t value, int *success) { uint8_t valenc = _intsetValueEncoding(value); uint32_t pos; if (success) *success = 0; //匹配当前编码并查找元素 if (valenc <= intrev32ifbe(is->encoding) && intsetSearch(is,value,&pos)) { uint32_t len = intrev32ifbe(is->length); /* We know we can delete */ if (success) *success = 1; /* Overwrite value with tail and update length */ //找到后,向前移动数组 if (pos < (len-1)) intsetMoveTail(is,pos+1,pos); //收缩数组 is = intsetResize(is,len-1); is->length = intrev32ifbe(len-1); } return is; } [/cce] 转载自:https://coolex.info/blog/452.html
Redis对基础数据类型进行了封装,构建出上层的对象系统,这个系统包含:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。 Redis对象结构: [cce lang=”c”] typedef struct redisObject { //类型 unsigned type:4; //编码 unsigned encoding:4; //LRU时间 unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */ //引用计数 int refcount; //底层实现数据结构的指针 void *ptr; } robj; [/cce] 相关宏定义: [cce lang=”c”] #define REDIS_LRU_BITS 24 #define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */ #define REDIS_LRU_CLOCK_RESOLUTION 1 /* LRU clock resolution in seconds */ [/cce] Redis对象类型:(使用type命令查看) [cce lang=”c”] /* Object types */ #define REDIS_STRING 0 #define REDIS_LIST 1 #define REDIS_SET 2 #define REDIS_ZSET 3 #define REDIS_HASH 4 //对象编码类型:(使用object encoding命令) #define REDIS_ENCODING_RAW 0 /* Raw representation */ #define REDIS_ENCODING_INT 1 /* Encoded as integer */ #define REDIS_ENCODING_HT 2 /* Encoded as hash table */ #define REDIS_ENCODING_ZIPMAP 3 /* Encoded as zipmap */ #define REDIS_ENCODING_LINKEDLIST 4 /* Encoded as regular linked list */ #define REDIS_ENCODING_ZIPLIST 5 /* Encoded as ziplist */ #define REDIS_ENCODING_INTSET 6 /* Encoded as intset */ #define REDIS_ENCODING_SKIPLIST 7 /* Encoded as skiplist */ [/cce] 不同类型和编码的对象: 类型 编码 对象 REDIS_STRING REDIS_ENCODING_INT 使用整数值实现的字符串对象 REDIS_STRING REDIS_ENCODING_EMBSTR 这货redis3.0引入的,不管 REDIS_STRING REDIS_ENCODING_RAW sds实现的字符串对象 REDIS_LIST REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象 REDIS_LIST REDIS_ENCODING_LINKEDLIST 使用双向链表实现的列表对象 REDIS_HASH REDIS_ENCODING_ZIPLIST 使用压缩列表实现的哈希对象 REDIS_HASH REDIS_ENCODING_HT 使用字典实现的哈希对象 REDIS_SET REDIS_ENCODING_INTSET 使用整数集合实现的集合对象 REDIS_SET REDIS_ENCODING_HT 使用字典实现的集合对象 REDIS_ZSET REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象 REDIS_ZSET REDIS_ENCODING_SKIPLIST 使用跳跃表想和字典实现的有序集合对象 字符串对象: 整数(long)被保存为int类型 浮点保存为字符串,浮点运算(INCRYBYFLOAT)redis先转成浮点,进行运算后再转回字符串 列表对象: 列表对象同时满足一下两个条件时,列表对象使用ziplist编码 列表对象保存的所有字符串元素的长度都小于64个字节(list-max-ziplist-value) 列表对象保存的元素数量小于512个(list-max-ziplist-entries) 不能满足条件的都需要使用linkedlist编码。 哈希对象: 使用压缩列表,键和值分别作为节点按顺序放入列表 使用ziplist的条件: 哈希对象保存的所有键值对的键和值的字符串长度小于64个字节(hash-max-ziplist-value) 哈希对象保存的键值对数量小于512个(hash-max-ziplist-entries) 集合对象: hashtable编码,字典的每个键都是一个字符串对象,字典的值全部设置为NULL 使用intset编码条件: 集合对象保存的所有元素是整数值 集合对象保存的元素数量不超过512个(set-max-intset-entries) 不满足条件的集合对象需要使用hashtable编码 有序集合对象: ziplist编码的有序集合,每个集合元素使用两个紧挨在一起的压缩节点列表来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。 skiplist编码的有序集合,采用一个skiplist和一个hashtable实现。 使用ziplist编码条件: 有序集合保存的元素数量小于128个(zset-max-ziplist-entries) 有序集合保存的所有元素成员的长度都小于64字节(zset-max-ziplist-value) 对象共享: Redis在初始化时,会创建1w个字符串对象(REDIS_SHARED_INTEGERS),包含整数0~9999,当需要使用这些对象的时候,会使用这些对象的引用(引用计数)而不新创建。 [cce lang=”c”] if (value >= 0 && value < REDIS_SHARED_INTEGERS) { incrRefCount(shared.integers[value]); o = shared.integers[value]; } [/cce] 转载自:https://coolex.info/blog/454.html
Redis数据库定义: typedef struct redisDb { dict *dict; /* The keyspace for this DB */ dict *expires; /* Timeout of keys with a timeout set */ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */ dict *ready_keys; /* Blocked keys that received a PUSH */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ int id; long long avg_ttl; /* Average TTL, just for stats */ } redisDb; dict dict数组保存所有的数据库,Redis初始化的时候,默认会创建16个数据库 #define REDIS_DEFAULT_DBNUM 16 默认情况下,Redis客户端的目标数据库是0 号数据库,可以通过select命令切换。 注意,由于Redis缺少获取当前操作的数据库命令,使用select切换需要特别注意 读写数据库中的键值对的时候,Redis除了对键空间执行指定操作外,还有一些额外的操作: 读取键之后(读和写操作都会先读取),记录键空间命中或不命中次数 读取键之后,更新键的LRU 读取时发现已经过期,会先删除过期键 如果有客户端使用watch命令监视了key,会在修改后标记为dirty 修改之后,会对dirty键计数器加1,用于持久化和复制 如果开启了数据库通知,修改之后会发送相应通知 robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) { robj *o = lookupKeyRead(c->db, key); if (!o) addReply(c,reply); return o; } robj *lookupKeyRead(redisDb *db, robj *key) { robj *val; //查询是否已经过期 expireIfNeeded(db,key); val = lookupKey(db,key); if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val; } robj *lookupKey(redisDb *db, robj *key) { dictEntry *de = dictFind(db->dict,key->ptr); if (de) { robj *val = dictGetVal(de); /* Update the access time for the ageing algorithm. * Don’t do it if we have a saving child, as this will trigger * a copy on write madness. */ if (server.rdb_child_pid == –1 && server.aof_child_pid == –1) //设置lru时间 val->lru = server.lruclock; return val; } else { return NULL; } } expires 通过exprire或者pexpire命令,可以设置键的TTL,如果键的TTL为0,会被自动删除。 expires字典保存了数据库中所有键的过期时间。 过期字典的键是指向某个数据中的键对象 过期字段的值是long long类型的整数,保存这个键的过期时间 void expireCommand(redisClient *c) { expireGenericCommand(c,mstime(),UNIT_SECONDS); } void expireGenericCommand(redisClient *c, long long basetime, int unit) { robj *key = c->argv[1], *param = c->argv[2]; long long when; /* unix time in milliseconds when the key will expire. */ if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK) return; if (unit == UNIT_SECONDS) when *= 1000; when += basetime; /* No key, return zero. */ if (lookupKeyRead(c->db,key) == NULL) { addReply(c,shared.czero); return; } /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past * should never be executed as a DEL when load the AOF or in the context * of a slave instance. * * Instead we take the other branch of the IF statement setting an expire * (possibly in the past) and wait for an explicit DEL from the master. */ if (when <= mstime() && !server.loading && !server.masterhost) { robj *aux; redisAssertWithInfo(c,key,dbDelete(c->db,key)); server.dirty++; /* Replicate/AOF this as an explicit DEL. */ aux = createStringObject(“DEL“,3); rewriteClientCommandVector(c,2,aux,key); decrRefCount(aux); signalModifiedKey(c->db,key); notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,“del“,key,c->db->id); addReply(c, shared.cone); return; } else { //放到expires字典中 setExpire(c->db,key,when); addReply(c,shared.cone); signalModifiedKey(c->db,key); notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,“expire“,key,c->db->id); server.dirty++; return; } } 过期键删除策略 惰性删除:每次执行命令前,都会调用expireIfNeeded函数检查是否过期,如果已经过期,改函数会删除过期键 定时删除:定时执行activeExpireCycleTryExpire函数expireIfNeeded int expireIfNeeded(redisDb *db, robj *key) { mstime_t when = getExpire(db,key); mstime_t now; if (when < 0) return 0; /* No expire for this key */ /* Don’t expire anything while loading. It will be done later. */ if (server.loading) return 0; /* If we are in the context of a Lua script, we claim that time is * blocked to when the Lua script started. This way a key can expire * only the first time it is accessed and not in the middle of the * script execution, making propagation to slaves / AOF consistent. * See issue #1525 on Github for more information. */ now = server.lua_caller ? server.lua_time_start : mstime(); /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ if (server.masterhost != NULL) return now > when; /* Return when this key has not expired */ if (now <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED, “expired“,key,db->id); return dbDelete(db,key); } activeExpireCycleTryExpire while (num–) { dictEntry *de; long long ttl; if ((de = dictGetRandomKey(db->expires)) == NULL) break; ttl = dictGetSignedIntegerVal(de)-now; if (activeExpireCycleTryExpire(db,de,now)) expired++; if (ttl < 0) ttl = 0; ttl_sum += ttl; ttl_samples++; } AOF、RDB和复制功能对过期键的处理 生成RDB文件时,已过期的键不会被保存到新的RDB文件中 载入RDB文件: 主服务器载入时,会忽略过期键 从服务器载入时,都会被载入(但是很快会因为同步被覆盖) AOF写入,已过期未删除的键没有影响,被删除后,会追加一条del命令 AOF重写,会对键进行检查,过期键不会保存到重写后的AOF文件 复制: 主服务器删除一个过期键后,会显式向所有从服务器发送DEL命令 从服务器执行读命令,及时过期也不会删除,只有接受到主服务器DEL命令才会删除 转载自:https://coolex.info/blog/459.html
复制(Redis2.8) 设置主服务器的地址和端口(SLAVE OF命令) SLAVEOF host port Redis的主从复制设置非常方便,只需要在从服务器上设置主服务器的IP和端口即可。如果需要关闭主从同步,只需要执行SLAVEOF NO ONE即可。 该命令的具体描述见官方文档 void slaveofCommand(redisClient *c) { // 先处理no one,解除现有的主从同步 if (!strcasecmp(c->argv[1]->ptr,“no“) && !strcasecmp(c->argv[2]->ptr,“one“)) { if (server.masterhost) { replicationUnsetMaster(); redisLog(REDIS_NOTICE,“MASTER MODE enabled (user request)“); } } else { long port; if ((getLongFromObjectOrReply(c, c->argv[2], &port, NULL) != REDIS_OK)) return; /* Check if we are already attached to the specified slave */ if (server.masterhost && !strcasecmp(server.masterhost,c->argv[1]->ptr) && server.masterport == port) { redisLog(REDIS_NOTICE,“SLAVE OF would result into synchronization with the master we are already connected with. No operation performed.“); addReplySds(c,sdsnew(“+OK Already connected to specified master\r\n“)); return; } /* There was no previous master or the user specified a different one, * we can continue. */ // 设置新的主从同步,这里只是设置,然后直接返回 replicationSetMaster(c->argv[1]->ptr, port); redisLog(REDIS_NOTICE,“SLAVE OF %s:%d enabled (user request)“, server.masterhost, server.masterport); } addReply(c,shared.ok); } void replicationSetMaster(char *ip, int port) { sdsfree(server.masterhost); server.masterhost = sdsdup(ip); server.masterport = port; //如果当前slave以前是master,断开所有原先的连接 if (server.master) freeClient(server.master); disconnectSlaves(); /* Force our slaves to resync with us as well. */ replicationDiscardCachedMaster(); /* Don’t try a PSYNC. */ freeReplicationBacklog(); /* Don’t allow our chained slaves to PSYNC. */ cancelReplicationHandshake(); server.repl_state = REDIS_REPL_CONNECT; server.master_repl_offset = 0; } 可以看到,slaveof命令是一个异步命令,执行的时候只是设置了新的主服务器,然后就立马返回结果了。真正执行连接等操作的, 是在定时器中执行的。 /* Replication cron function — used to reconnect to master and * to detect transfer failures. */ run_with_period(1000) replicationCron(); 建立套接字连接 提醒哦那个每隔1秒钟,会调用replicationCron函数,该函数会根据状态执行定时操作。当状态为REDIS_REPL_CONNECT的时候 执行逻辑为: void replicationCron(void) { … /* Check if we should connect to a MASTER */ if (server.repl_state == REDIS_REPL_CONNECT) { redisLog(REDIS_NOTICE,“Connecting to MASTER %s:%d“, server.masterhost, server.masterport); if (connectWithMaster() == REDIS_OK) { redisLog(REDIS_NOTICE,“MASTER <-> SLAVE sync started“); } } … } int connectWithMaster(void) { int fd; fd = anetTcpNonBlockConnect(NULL,server.masterhost,server.masterport); if (fd == –1) { redisLog(REDIS_WARNING,“Unable to connect to MASTER: %s“, strerror(errno)); return REDIS_ERR; } if (aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) == AE_ERR) { close(fd); redisLog(REDIS_WARNING,“Can’t create readable event for SYNC“); return REDIS_ERR; } server.repl_transfer_lastio = server.unixtime; server.repl_transfer_s = fd; server.repl_state = REDIS_REPL_CONNECTING; return REDIS_OK; } 如果发现当前主从状态是REDIS_REPL_CONNECT(刚执行slaveof的时候设置的),就会去连接主服务器。当socket连接建立之后, 会注册syncWithMaster这个回调,并且设置主从状态为REDIS_REPL_CONNECTING。 发送PING命令 PING命令都很熟悉了,jedis pool中用来检测当前连接是否有效,用的就是这个命令。手工执行PING命令,Redis会返回一个PONG作为响应。 这里发送PING命令,主要也是为了检测当前和master连接是否正常,master是否能够正常处理命令。 void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { … if (server.repl_state == REDIS_REPL_CONNECTING) { redisLog(REDIS_NOTICE,“Non blocking connect for SYNC fired the event.“); /* Delete the writable event so that the readable event remains * registered and we can wait for the PONG reply. */ aeDeleteFileEvent(server.el,fd,AE_WRITABLE); server.repl_state = REDIS_REPL_RECEIVE_PONG; /* Send the PING, don’t check for errors at all, we have the timeout * that will take care about this. */ syncWrite(fd,“PING\r\n“,6,100); return; } … } 这里当状态是REDIS_REPL_CONNECTING的时候,向master发送了PING命令,然后就等待master返回PONG的响应。 PONG响应也是在这个函数中进行处理的: void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { … /* Receive the PONG command. */ if (server.repl_state == REDIS_REPL_RECEIVE_PONG) { char buf[1024]; /* Delete the readable event, we no longer need it now that there is * the PING reply to read. */ aeDeleteFileEvent(server.el,fd,AE_READABLE); /* Read the reply with explicit timeout. */ buf[0] = ‘\0‘; if (syncReadLine(fd,buf,sizeof(buf), server.repl_syncio_timeout*1000) == –1) { redisLog(REDIS_WARNING, “I/O error reading PING reply from master: %s“, strerror(errno)); goto error; } /* We accept only two replies as valid, a positive +PONG reply * (we just check for “+”) or an authentication error. * Note that older versions of Redis replied with “operation not * permitted” instead of using a proper error code, so we test * both. */ if (buf[0] != ‘+‘ && strncmp(buf,“-NOAUTH“,7) != 0 && strncmp(buf,“-ERR operation not permitted“,28) != 0) { redisLog(REDIS_WARNING,“Error reply to PING from master: ‘%s‘“,buf); goto error; } else { redisLog(REDIS_NOTICE, “Master replied to PING, replication can continue…“); } } /* AUTH with the master if required. */ if(server.masterauth) { err = sendSynchronousCommand(fd,“AUTH“,server.masterauth,NULL); if (err[0] == ‘–‘) { redisLog(REDIS_WARNING,“Unable to AUTH to MASTER: %s“,err); sdsfree(err); goto error; } sdsfree(err); } /* Set the slave port, so that Master’s INFO command can list the * slave listening port correctly. */ { sds port = sdsfromlonglong(server.port); err = sendSynchronousCommand(fd,“REPLCONF“,“listening-port“,port, NULL); sdsfree(port); /* Ignore the error if any, not all the Redis versions support * REPLCONF listening-port. */ if (err[0] == ‘–‘) { redisLog(REDIS_NOTICE,“(Non critical) Master does not understand REPLCONF listening-port: %s“, err); } sdsfree(err); } … error: close(fd); server.repl_transfer_s = –1; server.repl_state = REDIS_REPL_CONNECT; return; } 如果读取master返回值失败,直接跳转到error,关闭连接,重新将连接状态设置为REDIS_REPL_CONNECT(也就是SLAVEOF执行完成之后的状态), 等待下次定时器重连; 读取响应成功,判断响应值是否为PONG,如果为PONG则表示连接检测完成,将发送当前slave端口信息,用于master同步数据 如果判断是需要认证,切设置了masterauth,则发送AUTH命令,向master发起授权。 如果授权成功,将继续后续的同步流程 如果授权失败,则进入error流程,关闭连接,并等待下次重试 发送端口信息 前面的PONG响应流程里面已经提到了,当正确接收到了PONG响应,或者是完成了认证之后,slave会发起一个REPLCONF命令,将自己的端口发送给master。 master接受到这个命令之后,将slave的端口信息记录到这个slave对应的client对象的slave_listening_port属性中。 void replconfCommand(redisClient *c) { … if (!strcasecmp(c->argv[j]->ptr,“listening-port“)) { long port; if ((getLongFromObjectOrReply(c,c->argv[j+1], &port,NULL) != REDIS_OK)) return; c->slave_listening_port = port; } … } 这时,在master上通过INFO命令,就可以看见slave的端口信息: INFO replication 同步 还是在syncWithMaster函数中。发送完端口信息之后,slave会尝试进行增量同步: void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { … psync_result = slaveTryPartialResynchronization(fd); if (psync_result == PSYNC_CONTINUE) { redisLog(REDIS_NOTICE, “MASTER <-> SLAVE sync: Master accepted a Partial Resynchronization.“); return; } /* Fall back to SYNC if needed. Otherwise psync_result == PSYNC_FULLRESYNC * and the server.repl_master_runid and repl_master_initial_offset are * already populated. */ if (psync_result == PSYNC_NOT_SUPPORTED) { redisLog(REDIS_NOTICE,“Retrying with SYNC…“); if (syncWrite(fd,“SYNC\r\n“,6,server.repl_syncio_timeout*1000) == –1) { redisLog(REDIS_WARNING,“I/O error writing to MASTER: %s“, strerror(errno)); goto error; } } … 如果不支持增量同步,会向master发送SYNC命令做全量同步。增量同步是在Redis2.8中支持的,所以全量同步就不管了。大致的操作流程就是 master做一次BGSAVE,然后将保存的rdb文件通过TCP连接发送给slave,slave加载这个rdb文件。 这里着重了解增量同步: #define PSYNC_CONTINUE 0 #define PSYNC_FULLRESYNC 1 #define PSYNC_NOT_SUPPORTED 2 int slaveTryPartialResynchronization(int fd) { char *psync_runid; char psync_offset[32]; sds reply; /* Initially set repl_master_initial_offset to -1 to mark the current * master run_id and offset as not valid. Later if we’ll be able to do * a FULL resync using the PSYNC command we’ll set the offset at the * right value, so that this information will be propagated to the * client structure representing the master into server.master. */ server.repl_master_initial_offset = –1; if (server.cached_master) { psync_runid = server.cached_master->replrunid; snprintf(psync_offset,sizeof(psync_offset),“%lld“, server.cached_master->reploff+1); redisLog(REDIS_NOTICE,“Trying a partial resynchronization (request %s:%s).“, psync_runid, psync_offset); } else { redisLog(REDIS_NOTICE,“Partial resynchronization not possible (no cached master)“); psync_runid = “?“; memcpy(psync_offset,“-1“,3); } /* Issue the PSYNC command */ reply = sendSynchronousCommand(fd,“PSYNC“,psync_runid,psync_offset,NULL); if (!strncmp(reply,“+FULLRESYNC“,11)) { char *runid = NULL, *offset = NULL; /* FULL RESYNC, parse the reply in order to extract the run id * and the replication offset. */ runid = strchr(reply,‘ ‘); if (runid) { runid++; offset = strchr(runid,‘ ‘); if (offset) offset++; } if (!runid || !offset || (offset-runid-1) != REDIS_RUN_ID_SIZE) { redisLog(REDIS_WARNING, “Master replied with wrong +FULLRESYNC syntax.“); /* This is an unexpected condition, actually the +FULLRESYNC * reply means that the master supports PSYNC, but the reply * format seems wrong. To stay safe we blank the master * runid to make sure next PSYNCs will fail. */ memset(server.repl_master_runid,0,REDIS_RUN_ID_SIZE+1); } else { memcpy(server.repl_master_runid, runid, offset-runid-1); server.repl_master_runid[REDIS_RUN_ID_SIZE] = ‘\0‘; server.repl_master_initial_offset = strtoll(offset,NULL,10); redisLog(REDIS_NOTICE,“Full resync from master: %s:%lld“, server.repl_master_runid, server.repl_master_initial_offset); } /* We are going to full resync, discard the cached master structure. */ replicationDiscardCachedMaster(); sdsfree(reply); return PSYNC_FULLRESYNC; } if (!strncmp(reply,“+CONTINUE“,9)) { /* Partial resync was accepted, set the replication state accordingly */ redisLog(REDIS_NOTICE, “Successful partial resynchronization with master.“); sdsfree(reply); replicationResurrectCachedMaster(fd); return PSYNC_CONTINUE; } /* If we reach this point we receied either an error since the master does * not understand PSYNC, or an unexpected reply from the master. * Return PSYNC_NOT_SUPPORTED to the caller in both cases. */ if (strncmp(reply,“-ERR“,4)) { /* If it’s not an error, log the unexpected event. */ redisLog(REDIS_WARNING, “Unexpected reply to PSYNC from master: %s“, reply); } else { redisLog(REDIS_NOTICE, “Master does not support PSYNC or is in “ “error state (reply: %s)“, reply); } sdsfree(reply); replicationDiscardCachedMaster(); return PSYNC_NOT_SUPPORTED; } 首先设置同步偏移量为-1,表示第一次增量更新(其实也就是个全量更新) 向master发送PSYNC命令,告知master自己的id和同步偏移量 master返回全量更新(FULLRESYNC),保存master返回的偏移量和运行id,清除之前缓存的master信息 确认可以增量同步后,由于第一次是全量同步,因此操作和原全量同步相同: void syncWithMaster(aeEventLoop *el, int fd, void *privdata, int mask) { … /* Prepare a suitable temp file for bulk transfer */ while(maxtries–) { snprintf(tmpfile,256, “temp-%d.%ld.rdb“,(int)server.unixtime,(long int)getpid()); dfd = open(tmpfile,O_CREAT|O_WRONLY|O_EXCL,0644); if (dfd != –1) break; sleep(1); } if (dfd == –1) { redisLog(REDIS_WARNING,“Opening the temp file needed for MASTER <-> SLAVE synchronization: %s“,strerror(errno)); goto error; } /* Setup the non blocking download of the bulk file. */ if (aeCreateFileEvent(server.el,fd, AE_READABLE,readSyncBulkPayload,NULL) == AE_ERR) { redisLog(REDIS_WARNING, “Can’t create readable event for SYNC: %s (fd=%d)“, strerror(errno),fd); goto error; } server.repl_state = REDIS_REPL_TRANSFER; server.repl_transfer_size = –1; server.repl_transfer_read = 0; server.repl_transfer_last_fsync_off = 0; server.repl_transfer_fd = dfd; server.repl_transfer_lastio = server.unixtime; server.repl_transfer_tmpfile = zstrdup(tmpfile); return; } 创建一个临时文件,用于保存master传回的rdb文件 开始读取master传输回来的rdb文件,注册readSyncBulkPayload回调函数来处理 设置当前的状态为REDIS_REPL_TRANSFER,并保存传输文件等中间内容 readSyncBulkPayload函数用于接收master传输的rdb文件,并加载到Redis中,大致流程: 读取文件长度 读取文件内容,并保存到本地rdb临时文件中 读取完成之后,清空Redis数据库 加载rdb文件 创建一个master -> slave的通道,将当前slave作为master的client,以继续执行master同步过来的命令 将同步状态改成REDIS_REPL_CONNECTED,并回写同步偏移量等 开启aof如果需要(server.aof_state != REDIS_AOF_OFF) master对PSYNC命令的处理 void syncCommand(redisClient *c) { /* ignore SYNC if already slave or in monitor mode */ if (c->flags & REDIS_SLAVE) return; /* Refuse SYNC requests if we are a slave but the link with our master * is not ok… */ if (server.masterhost && server.repl_state != REDIS_REPL_CONNECTED) { addReplyError(c,“Can’t SYNC while not connected with my master“); return; } /* SYNC can’t be issued when the server has pending data to send to * the client about already issued commands. We need a fresh reply * buffer registering the differences between the BGSAVE and the current * dataset, so that we can copy to other slaves if needed. */ if (listLength(c->reply) != 0 || c->bufpos != 0) { addReplyError(c,“SYNC and PSYNC are invalid with pending output“); return; } redisLog(REDIS_NOTICE,“Slave asks for synchronization“); /* Try a partial resynchronization if this is a PSYNC command. * If it fails, we continue with usual full resynchronization, however * when this happens masterTryPartialResynchronization() already * replied with: * * +FULLRESYNC <runid> <offset> * * So the slave knows the new runid and offset to try a PSYNC later * if the connection with the master is lost. */ if (!strcasecmp(c->argv[0]->ptr,“psync“)) { if (masterTryPartialResynchronization(c) == REDIS_OK) { server.stat_sync_partial_ok++; return; /* No full resync needed, return. */ } else { char *master_runid = c->argv[1]->ptr; /* Increment stats for failed PSYNCs, but only if the * runid is not “?”, as this is used by slaves to force a full * resync on purpose when they are not albe to partially * resync. */ if (master_runid[0] != ‘?‘) server.stat_sync_partial_err++; } } else { /* If a slave uses SYNC, we are dealing with an old implementation * of the replication protocol (like redis-cli –slave). Flag the client * so that we don’t expect to receive REPLCONF ACK feedbacks. */ c->flags |= REDIS_PRE_PSYNC; } /* Full resynchronization. */ server.stat_sync_full++; /* Here we need to check if there is a background saving operation * in progress, or if it is required to start one */ if (server.rdb_child_pid != –1) { /* Ok a background save is in progress. Let’s check if it is a good * one for replication, i.e. if there is another slave that is * registering differences since the server forked to save */ redisClient *slave; listNode *ln; listIter li; listRewind(server.slaves,&li); while((ln = listNext(&li))) { slave = ln->value; if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) break; } if (ln) { /* Perfect, the server is already registering differences for * another slave. Set the right state, and copy the buffer. */ copyClientOutputBuffer(c,slave); c->replstate = REDIS_REPL_WAIT_BGSAVE_END; redisLog(REDIS_NOTICE,“Waiting for end of BGSAVE for SYNC“); } else { /* No way, we need to wait for the next BGSAVE in order to * register differences */ c->replstate = REDIS_REPL_WAIT_BGSAVE_START; redisLog(REDIS_NOTICE,“Waiting for next BGSAVE for SYNC“); } } else { /* Ok we don’t have a BGSAVE in progress, let’s start one */ redisLog(REDIS_NOTICE,“Starting BGSAVE for SYNC“); if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) { redisLog(REDIS_NOTICE,“Replication failed, can’t BGSAVE“); addReplyError(c,“Unable to perform background save“); return; } c->replstate = REDIS_REPL_WAIT_BGSAVE_END; /* Flush the script cache for the new slave. */ replicationScriptCacheFlush(); } if (server.repl_disable_tcp_nodelay) anetDisableTcpNoDelay(NULL, c->fd); /* Non critical if it fails. */ c->repldbfd = –1; c->flags |= REDIS_SLAVE; server.slaveseldb = –1; /* Force to re-emit the SELECT command. */ listAddNodeTail(server.slaves,c); if (listLength(server.slaves) == 1 && server.repl_backlog == NULL) createReplicationBacklog(); return; } 首先判断自己是slave的时候不能执行psync 判断是否需要全量同步,如果不需要,直接退出 如果需要全量同步,创建一个rdb文件 如果已经在写rdb文件,尽量复用当前的文件 如果没有,则发起一个bgsave 判断是否需要全量同步: int masterTryPartialResynchronization(redisClient *c) { long long psync_offset, psync_len; char *master_runid = c->argv[1]->ptr; char buf[128]; int buflen; /* Is the runid of this master the same advertised by the wannabe slave * via PSYNC? If runid changed this master is a different instance and * there is no way to continue. */ if (strcasecmp(master_runid, server.runid)) { /* Run id “?” is used by slaves that want to force a full resync. */ if (master_runid[0] != ‘?‘) { redisLog(REDIS_NOTICE,“Partial resynchronization not accepted: “ “Runid mismatch (Client asked for ‘%s‘, I’m ‘%s‘)“, master_runid, server.runid); } else { redisLog(REDIS_NOTICE,“Full resync requested by slave.“); } goto need_full_resync; } /* We still have the data our slave is asking for? */ if (getLongLongFromObjectOrReply(c,c->argv[2],&psync_offset,NULL) != REDIS_OK) goto need_full_resync; if (!server.repl_backlog || psync_offset < server.repl_backlog_off || psync_offset > (server.repl_backlog_off + server.repl_backlog_histlen)) { redisLog(REDIS_NOTICE, “Unable to partial resync with the slave for lack of backlog (Slave request was: %lld).“, psync_offset); if (psync_offset > server.master_repl_offset) { redisLog(REDIS_WARNING, “Warning: slave tried to PSYNC with an offset that is greater than the master replication offset.“); } goto need_full_resync; } /* If we reached this point, we are able to perform a partial resync: * 1) Set client state to make it a slave. * 2) Inform the client we can continue with +CONTINUE * 3) Send the backlog data (from the offset to the end) to the slave. */ c->flags |= REDIS_SLAVE; c->replstate = REDIS_REPL_ONLINE; c->repl_ack_time = server.unixtime; listAddNodeTail(server.slaves,c); /* We can’t use the connection buffers since they are used to accumulate * new commands at this stage. But we are sure the socket send buffer is * emtpy so this write will never fail actually. */ buflen = snprintf(buf,sizeof(buf),“+CONTINUE\r\n“); if (write(c->fd,buf,buflen) != buflen) { freeClientAsync(c); return REDIS_OK; } psync_len = addReplyReplicationBacklog(c,psync_offset); redisLog(REDIS_NOTICE, “Partial resynchronization request accepted. Sending %lld bytes of backlog starting from offset %lld.“, psync_len, psync_offset); /* Note that we don’t need to set the selected DB at server.slaveseldb * to -1 to force the master to emit SELECT, since the slave already * has this state from the previous connection with the master. */ refreshGoodSlavesCount(); return REDIS_OK; /* The caller can return, no full resync needed. */ need_full_resync: /* We need a full resync for some reason… notify the client. */ psync_offset = server.master_repl_offset; /* Add 1 to psync_offset if it the replication backlog does not exists * as when it will be created later we’ll increment the offset by one. */ if (server.repl_backlog == NULL) psync_offset++; /* Again, we can’t use the connection buffers (see above). */ buflen = snprintf(buf,sizeof(buf),“+FULLRESYNC %s %lld\r\n“, server.runid,psync_offset); if (write(c->fd,buf,buflen) != buflen) { freeClientAsync(c); return REDIS_OK; } return REDIS_ERR; } 主要场景有两个: 当前请求的id和server的id不匹配 当前Redis保存的日志无法满足slave要求的偏移量 master还没有back log master back log长度不够 同时,每次rdb文件保存完毕的时候,都会调用updateSlavesWaitingBgsave函数,处理保存的rdb文件。 void updateSlavesWaitingBgsave(int bgsaveerr) { listNode *ln; int startbgsave = 0; listIter li; listRewind(server.slaves,&li); while((ln = listNext(&li))) { redisClient *slave = ln->value; if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) { startbgsave = 1; slave->replstate = REDIS_REPL_WAIT_BGSAVE_END; } else if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_END) { // rdb文件写入完毕 struct redis_stat buf; if (bgsaveerr != REDIS_OK) { freeClient(slave); redisLog(REDIS_WARNING,“SYNC failed. BGSAVE child returned an error“); continue; } // 打开刚写入的rdb文件 if ((slave->repldbfd = open(server.rdb_filename,O_RDONLY)) == –1 || redis_fstat(slave->repldbfd,&buf) == –1) { freeClient(slave); redisLog(REDIS_WARNING,“SYNC failed. Can’t open/stat DB after BGSAVE: %s“, strerror(errno)); continue; } slave->repldboff = 0; slave->repldbsize = buf.st_size; slave->replstate = REDIS_REPL_SEND_BULK; aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE); // 开始发送rdb文件 if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) { freeClient(slave); continue; } } } if (startbgsave) { /* Since we are starting a new background save for one or more slaves, * we flush the Replication Script Cache to use EVAL to propagate every * new EVALSHA for the first time, since all the new slaves don’t know * about previous scripts. */ replicationScriptCacheFlush(); if (rdbSaveBackground(server.rdb_filename) != REDIS_OK) { listIter li; listRewind(server.slaves,&li); redisLog(REDIS_WARNING,“SYNC failed. BGSAVE failed“); while((ln = listNext(&li))) { redisClient *slave = ln->value; if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) freeClient(slave); } } } } 命令传播 上面的流程结束,slave已经包含了master BGSAVE时所包含的所有数据。后续就需要master一直将自己的命令发送给slave。 void call(redisClient *c, int flags) { … /* Propagate the command into the AOF and replication link */ if (flags & REDIS_CALL_PROPAGATE) { int flags = REDIS_PROPAGATE_NONE; if (c->flags & REDIS_FORCE_REPL) flags |= REDIS_PROPAGATE_REPL; if (c->flags & REDIS_FORCE_AOF) flags |= REDIS_PROPAGATE_AOF; if (dirty) flags |= (REDIS_PROPAGATE_REPL | REDIS_PROPAGATE_AOF); if (flags != REDIS_PROPAGATE_NONE) propagate(c->cmd,c->db->id,c->argv,c->argc,flags); } … } void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc, int flags) { if (server.aof_state != REDIS_AOF_OFF && flags & REDIS_PROPAGATE_AOF) feedAppendOnlyFile(cmd,dbid,argv,argc); if (flags & REDIS_PROPAGATE_REPL) replicationFeedSlaves(server.slaves,dbid,argv,argc); } 在调用任何命令的时候,都会将命令分发到slave上去(除了AOF加载或者命令加了REDIS_CMD_SKIP_MONITOR标签)。 replicationFeedSlaves函数主要作用有两个: 将命令发送给所有在线的slave 将命令写入到back log中,方便后续增量同步 void replicationFeedSlaves(list *slaves, int dictid, robj **argv, int argc) { listNode *ln; listIter li; int j, len; char llstr[REDIS_LONGSTR_SIZE]; /* If there aren’t slaves, and there is no backlog buffer to populate, * we can return ASAP. */ if (server.repl_backlog == NULL && listLength(slaves) == 0) return; /* We can’t have slaves attached and no backlog. */ redisAssert(!(listLength(slaves) != 0 && server.repl_backlog == NULL)); /* Send SELECT command to every slave if needed. */ if (server.slaveseldb != dictid) { robj *selectcmd; /* For a few DBs we have pre-computed SELECT command. */ // 每次都增加一个SELECT命令,防止弄错db if (dictid >= 0 && dictid < REDIS_SHARED_SELECT_CMDS) { selectcmd = shared.select[dictid]; } else { int dictid_len; dictid_len = ll2string(llstr,sizeof(llstr),dictid); selectcmd = createObject(REDIS_STRING, sdscatprintf(sdsempty(), “*2\r\n$6\r\nSELECT\r\n$%d\r\n%s\r\n“, dictid_len, llstr)); } /* Add the SELECT command into the backlog. */ // 将select命令写入到backlog中 if (server.repl_backlog) feedReplicationBacklogWithObject(selectcmd); /* Send it to slaves. */ // 将select命令发送给slave listRewind(slaves,&li); while((ln = listNext(&li))) { redisClient *slave = ln->value; addReply(slave,selectcmd); } if (dictid < 0 || dictid >= REDIS_SHARED_SELECT_CMDS) decrRefCount(selectcmd); } server.slaveseldb = dictid; /* Write the command to the replication backlog if any. */ // 将命令写入到backlog中 if (server.repl_backlog) { char aux[REDIS_LONGSTR_SIZE+3]; /* Add the multi bulk reply length. */ aux[0] = ‘*‘; len = ll2string(aux+1,sizeof(aux)-1,argc); aux[len+1] = ‘\r‘; aux[len+2] = ‘\n‘; feedReplicationBacklog(aux,len+3); for (j = 0; j < argc; j++) { long objlen = stringObjectLen(argv[j]); /* We need to feed the buffer with the object as a bulk reply * not just as a plain string, so create the $..CRLF payload len * ad add the final CRLF */ aux[0] = ‘$‘; len = ll2string(aux+1,sizeof(aux)-1,objlen); aux[len+1] = ‘\r‘; aux[len+2] = ‘\n‘; feedReplicationBacklog(aux,len+3); feedReplicationBacklogWithObject(argv[j]); feedReplicationBacklog(aux+len+1,2); } } /* Write the command to every slave. */ // 将命令发送到所有的slave listRewind(slaves,&li); while((ln = listNext(&li))) { redisClient *slave = ln->value; /* Don’t feed slaves that are still waiting for BGSAVE to start */ if (slave->replstate == REDIS_REPL_WAIT_BGSAVE_START) continue; /* Feed slaves that are waiting for the initial SYNC (so these commands * are queued in the output buffer until the initial SYNC completes), * or are already in sync with the master. */ /* Add the multi bulk length. */ addReplyMultiBulkLen(slave,argc); /* Finally any additional argument that was not stored inside the * static buffer if any (from j to argc). */ for (j = 0; j < argc; j++) addReplyBulk(slave,argv[j]); } } 注:backlog大小可以设置,默认的大小为1M,如果超过,覆盖最初的日志 #define REDIS_DEFAULT_REPL_BACKLOG_SIZE (1024*1024) /* 1mb */ 心跳检测和命令丢失补偿 在命令传播阶段,slave每秒一次向master发送REPLCONF命令,发送当前的offset,让master检测是否有命令丢失。 这个也是在定时器中发送的。 void replicationCron(void) { … if (server.masterhost && server.master && !(server.master->flags & REDIS_PRE_PSYNC)) replicationSendAck(); … } void replicationSendAck(void) { redisClient *c = server.master; if (c != NULL) { c->flags |= REDIS_MASTER_FORCE_REPLY; addReplyMultiBulkLen(c,3); addReplyBulkCString(c,“REPLCONF“); addReplyBulkCString(c,“ACK“); addReplyBulkLongLong(c,c->reploff); c->flags &= ~REDIS_MASTER_FORCE_REPLY; } } 同时,master在接收到这个ACK包的时候,会记录slave的ack offset和ack时间: void replconfCommand(redisClient *c) { … else if (!strcasecmp(c->argv[j]->ptr,“ack“)) { /* REPLCONF ACK is used by slave to inform the master the amount * of replication stream that it processed so far. It is an * internal only command that normal clients should never use. */ long long offset; if (!(c->flags & REDIS_SLAVE)) return; if ((getLongLongFromObject(c->argv[j+1], &offset) != REDIS_OK)) return; if (offset > c->repl_ack_off) c->repl_ack_off = offset; c->repl_ack_time = server.unixtime; /* Note: this command does not reply anything! */ return; } … } 还是在定时器中,每次调用的时候都会清理已经超时的slave: void replicationCron(void) { … /* Disconnect timedout slaves. */ if (listLength(server.slaves)) { listIter li; listNode *ln; listRewind(server.slaves,&li); while((ln = listNext(&li))) { redisClient *slave = ln->value; if (slave->replstate != REDIS_REPL_ONLINE) continue; if (slave->flags & REDIS_PRE_PSYNC) continue; if ((server.unixtime – slave->repl_ack_time) > server.repl_timeout) { char ip[REDIS_IP_STR_LEN]; int port; if (anetPeerToString(slave->fd,ip,sizeof(ip),&port) != –1) { redisLog(REDIS_WARNING, “Disconnecting timedout slave: %s:%d“, ip, slave->slave_listening_port); } freeClient(slave); } } } … } 这里的repl_ack_time由slave每次发送的ack包写入,server.repl_timeout默认值是60s: #define REDIS_REPL_TIMEOUT 60 增量同步 master断开了slave连接之后,slave为了能够进行增量同步,freeClient的实现,针对master的slave client,也有不同的处理: void freeClient(redisClient *c) { … /* If it is our master that’s beging disconnected we should make sure * to cache the state to try a partial resynchronization later. * * Note that before doing this we make sure that the client is not in * some unexpected state, by checking its flags. */ if (server.master && c->flags & REDIS_MASTER) { redisLog(REDIS_WARNING,“Connection with master lost.“); if (!(c->flags & (REDIS_CLOSE_AFTER_REPLY| REDIS_CLOSE_ASAP| REDIS_BLOCKED| REDIS_UNBLOCKED))) { replicationCacheMaster(c); return; } } … } void replicationCacheMaster(redisClient *c) { listNode *ln; redisAssert(server.master != NULL && server.cached_master == NULL); redisLog(REDIS_NOTICE,“Caching the disconnected master state.“); /* Remove from the list of clients, we don’t want this client to be * listed by CLIENT LIST or processed in any way by batch operations. */ // 首先将slave从client列表中删除 ln = listSearchKey(server.clients,c); redisAssert(ln != NULL); listDelNode(server.clients,ln); /* Save the master. Server.master will be set to null later by * replicationHandleMasterDisconnection(). */ // 把slave的master保存到cached_master中 server.cached_master = server.master; /* Remove the event handlers and close the socket. We’ll later reuse * the socket of the new connection with the master during PSYNC. */ // 清理slave连接,释放资源 aeDeleteFileEvent(server.el,c->fd,AE_READABLE); aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE); close(c->fd); /* Set fd to -1 so that we can safely call freeClient(c) later. */ c->fd = –1; /* Invalidate the Peer ID cache. */ if (c->peerid) { sdsfree(c->peerid); c->peerid = NULL; } · /* Caching the master happens instead of the actual freeClient() call, * so make sure to adjust the replication state. This function will * also set server.master to NULL. */ replicationHandleMasterDisconnection(); } void replicationHandleMasterDisconnection(void) { server.master = NULL; server.repl_state = REDIS_REPL_CONNECT; server.repl_down_since = server.unixtime; /* We lost connection with our master, force our slaves to resync * with us as well to load the new data set. * * If server.masterhost is NULL the user called SLAVEOF NO ONE so * slave resync is not needed. */ if (server.masterhost != NULL) disconnectSlaves(); } 经过这些处理,一个断开连接的slave,复制状态变成了REDIS_REPL_CONNECT。按照之前的流程,定时器会去尝试连接master, 发送PING命令,然后再发送PSYNC命令的时候,由于已经有了cached_master,会在PSYNC命令中带上之前master的id和偏移量。 相关slave和master的处理逻辑,前面代码中已经有了。 转载自:https://coolex.info/blog/463.html
有的时候会有这样一个需求,页面上有个大表格,我需要复制里面的一列到本地。比如,我要从表格里面,复制列出来的所有机器IP(这样比数据库导出方便点~) 首先,先用chrome的开发者工具,找到要复制的列中的某一个单元格,然后选择复制xpath。这样会复制下来这个元素的xpath路径,比如: //*[@id=”machineGroupTable”]/tbody/tr[2]/td[2] chrome的console,支持用$x()函数直接用xpath来定位元素,因此,可以通过类似这样的js,来获取刚选中单元格所在的所有列: [cce lang=”javascript”] $x(‘//[@id=”machineGroupTable”]/tbody//td[2]’) [/cce] 这样返回的是chrome经过处理的xpath结果,直接就是dom的数组,因此可以直接遍历,获取单元格中的文本。 [cce lang=”javascript”] var ip=[]; $x(‘//[@id=”machineGroupTable”]/tbody//td[2]’).forEach(function(e){ip.push(e.innerText)}) [/cce] 这样就把这列的所有内容,放到了ip这个数组中。 最后,把ip数组复制出来: [cce lang=”javascript”] copy(ip.join(‘\n’)) [/cce] copy也是chrome console内置的命令,可以把传进去的参数复制到剪切板。注意这里要自己join下,不然会直接输出json格式的字符串。 这样,表格的列已经被复制到了剪切板,直接粘贴到需要的文本中即可。 转载自:https://coolex.info/blog/466.html
Sentinel(Redis 3.0.0-rc1) Sentinel是Redis HA方案,一个或多个Sentinel实例组成的Sentinel系统,可以监视任意多个主服务器(master), 以及这些主服务器属下的所有从服务器(slave),并在被监视的主服务器进入下线状态时,自动在将被下线主服务器属下的某个从服务器升级为新的主服务器, 然后由新的主服务器代替已下线的主服务器继续处理命令请求。 基础数据结构 typedef struct sentinelRedisInstance { // 当前实例的类型和状态(master, slave, sentinel,是否下线) int flags; // 主机名,ip:port char *name; // 实例的runid char *runid; // 配置纪元 uint64_t config_epoch; // 实例的地址 sentinelAddr *addr; redisAsyncContext *cc; /* Hiredis context for commands. */ redisAsyncContext *pc; /* Hiredis context for Pub / Sub. */ int pending_commands; /* Number of commands sent waiting for a reply. */ mstime_t cc_conn_time; /* cc connection time. */ mstime_t pc_conn_time; /* pc connection time. */ mstime_t pc_last_activity; /* Last time we received any message. */ mstime_t last_avail_time; /* Last time the instance replied to ping with a reply we consider valid. */ mstime_t last_ping_time; /* Last time a pending ping was sent in the context of the current command connection with the instance. 0 if still not sent or if pong already received. */ mstime_t last_pong_time; /* Last time the instance replied to ping, whatever the reply was. That's used to check if the link is idle and must be reconnected. */ mstime_t last_pub_time; /* Last time we sent hello via Pub/Sub. */ mstime_t last_hello_time; /* Only used if SRI_SENTINEL is set. Last time we received a hello from this Sentinel via Pub/Sub. */ mstime_t last_master_down_reply_time; /* Time of last reply to SENTINEL is-master-down command. */ mstime_t s_down_since_time; /* Subjectively down since time. */ mstime_t o_down_since_time; /* Objectively down since time. */ // 无响应多少毫秒之后,进入主观下线 mstime_t down_after_period; mstime_t info_refresh; /* Time at which we received INFO output from it. */ /* Role and the first time we observed it. * This is useful in order to delay replacing what the instance reports * with our own configuration. We need to always wait some time in order * to give a chance to the leader to report the new configuration before * we do silly things. */ int role_reported; mstime_t role_reported_time; mstime_t slave_conf_change_time; /* Last time slave master addr changed. */ /* Master specific. */ dict *sentinels; /* Other sentinels monitoring the same master. */ dict *slaves; /* Slaves for this master instance. */ // 判断为客观下线需要的支持票数 unsigned int quorum; // 故障转移时,可以同时对新的master进行同步的slave数量 int parallel_syncs; /* How many slaves to reconfigure at same time. */ char *auth_pass; /* Password to use for AUTH against master & slaves. */ /* Slave specific. */ mstime_t master_link_down_time; /* Slave replication link down time. */ int slave_priority; /* Slave priority according to its INFO output. */ mstime_t slave_reconf_sent_time; /* Time at which we sent SLAVE OF <new> */ struct sentinelRedisInstance *master; /* Master instance if it's slave. */ char *slave_master_host; /* Master host as reported by INFO */ int slave_master_port; /* Master port as reported by INFO */ int slave_master_link_status; /* Master link status as reported by INFO */ unsigned long long slave_repl_offset; /* Slave replication offset. */ /* Failover */ char *leader; /* If this is a master instance, this is the runid of the Sentinel that should perform the failover. If this is a Sentinel, this is the runid of the Sentinel that this Sentinel voted as leader. */ uint64_t leader_epoch; /* Epoch of the 'leader' field. */ uint64_t failover_epoch; /* Epoch of the currently started failover. */ int failover_state; /* See SENTINEL_FAILOVER_STATE_* defines. */ mstime_t failover_state_change_time; mstime_t failover_start_time; /* Last failover attempt start time. */ // 刷新故障迁移状态的最大时限 mstime_t failover_timeout; /* Max time to refresh failover state. */ mstime_t failover_delay_logged; /* For what failover_start_time value we logged the failover delay. */ struct sentinelRedisInstance *promoted_slave; /* Promoted slave instance. */ /* Scripts executed to notify admin or reconfigure clients: when they * are set to NULL no script is executed. */ char *notification_script; char *client_reconfig_script; } sentinelRedisInstance; struct sentinelState { // 当前纪元 uint64_t current_epoch; // 监控的master字典,key是master名称,value是sentinelRedisInstance对象 dict *masters; // 是否处于TILT模式 int tilt; // 目前正在执行脚本的数量 int running_scripts; // 进入TITL模式时间 mstime_t tilt_start_time; // 最后一次执行时间处理器的时间 mstime_t previous_time; // 用户脚本执行队列 list *scripts_queue; } sentinel; Sentinel初始化 启动命令: redis-sentinel /path/to/sentinel.conf 或者 redis-server /path/to/sentinel.conf --sentinel Sentinel启动的时候,必须指定配置文件,最小配置类似: sentinel monitor mymaster 127.0.0.1 6379 2 sentinel down-after-milliseconds mymaster 60000 sentinel failover-timeout mymaster 180000 sentinel parallel-syncs mymaster 1 这个配置文件表示,当前sentinel监视一个Redis master(mymaster),ip为127.0.0.1,端口为6379, 需要两个sentinel声明下线,才进行主备切换。mymaster 60000ms未响应标记为失效。 在main函数中,会直接对sentinel启动特殊的配置: int main(int argc, char **argv) { ... if (server.sentinel_mode) { initSentinelConfig(); initSentinel(); } ... } 首先是覆盖redis server的端口设置,sentinel会默认监听在26379端口: #define REDIS_SENTINEL_PORT 26379 void initSentinelConfig(void) { server.port = REDIS_SENTINEL_PORT; } 然后覆盖server的命令表格,初始化sentinel对象: void initSentinel(void) { unsigned int j; /* 清空Redis支持的命令表格,改成sentinel支持的命令表格 dictEmpty(server.commands,NULL); for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) { int retval; struct redisCommand *cmd = sentinelcmds+j; retval = dictAdd(server.commands, sdsnew(cmd->name), cmd); redisAssert(retval == DICT_OK); } /* Initialize various data structures. */ sentinel.current_epoch = 0; sentinel.masters = dictCreate(&instancesDictType,NULL); sentinel.tilt = 0; sentinel.tilt_start_time = 0; sentinel.previous_time = mstime(); sentinel.running_scripts = 0; sentinel.scripts_queue = listCreate(); sentinel.announce_ip = NULL; sentinel.announce_port = 0; }Remove usual Redis commands from the command table, then just add * the SENTINEL command. */ // sentinel只能支持监控相关的命令,无法执行通常的Redis命令,sentinel可以支持的命令表格为: struct redisCommand sentinelcmds[] = { {"ping",pingCommand,1,"",0,NULL,0,0,0,0,0}, {"sentinel",sentinelCommand,-2,"",0,NULL,0,0,0,0,0}, {"subscribe",subscribeCommand,-2,"",0,NULL,0,0,0,0,0}, {"unsubscribe",unsubscribeCommand,-1,"",0,NULL,0,0,0,0,0}, {"psubscribe",psubscribeCommand,-2,"",0,NULL,0,0,0,0,0}, {"punsubscribe",punsubscribeCommand,-1,"",0,NULL,0,0,0,0,0}, {"publish",sentinelPublishCommand,3,"",0,NULL,0,0,0,0,0}, {"info",sentinelInfoCommand,-1,"",0,NULL,0,0,0,0,0}, {"role",sentinelRoleCommand,1,"l",0,NULL,0,0,0,0,0}, {"shutdown",shutdownCommand,-1,"",0,NULL,0,0,0,0,0} }; 同时,由于sentinel不接受Redis普通命令,因此初始化的时候,也不会去加载rdb文件等原始数据。 转载自:https://coolex.info/blog/468.html
通信 初始化完成之后,sentinel会主动和master、slave进行通信,获取他们的信息。 获取主服务器信息 首先,sentinel会和master建立两个连接,分别是命令连接和订阅连接(分别保存在sentinelRedisInstance的cc和pc字段中)。 void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { sentinelReconnectInstance(ri); ... } #define SENTINEL_HELLO_CHANNEL "__sentinel__:hello" 命令连接用于后续获取master的信息(包括slave的信息),订阅(sentinel:hello频道)用于获取master的掉线状态等消息的推送。 连接完成之后,sentinel会定时发送消息: void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) { mstime_t now = mstime(); mstime_t info_period, ping_period; int retval; /* Return ASAP if we have already a PING or INFO already pending, or * in the case the instance is not properly connected. */ if (ri->flags & SRI_DISCONNECTED) return; /* For INFO, PING, PUBLISH that are not critical commands to send we * also have a limit of SENTINEL_MAX_PENDING_COMMANDS. We don't * want to use a lot of memory just because a link is not working * properly (note that anyway there is a redundant protection about this, * that is, the link will be disconnected and reconnected if a long * timeout condition is detected. */ if (ri->pending_commands >= SENTINEL_MAX_PENDING_COMMANDS) return; /* If this is a slave of a master in O_DOWN condition we start sending * it INFO every second, instead of the usual SENTINEL_INFO_PERIOD * period. In this state we want to closely monitor slaves in case they * are turned into masters by another Sentinel, or by the sysadmin. */ // INFO命令默认发送间隔为 10s #define SENTINEL_INFO_PERIOD 10000 // 如果为已经宕机的master的slave,改1s if ((ri->flags & SRI_SLAVE) && (ri->master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS))) { info_period = 1000; } else { info_period = SENTINEL_INFO_PERIOD; } /* We ping instances every time the last received pong is older than * the configured 'down-after-milliseconds' time, but every second * anyway if 'down-after-milliseconds' is greater than 1 second. */ // ping命令默认间隔1s #define SENTINEL_PING_PERIOD 1000 ping_period = ri->down_after_period; if (ping_period > SENTINEL_PING_PERIOD) ping_period = SENTINEL_PING_PERIOD; if ((ri->flags & SRI_SENTINEL) == 0 && (ri->info_refresh == 0 || (now - ri->info_refresh) > info_period)) { /* Send INFO to masters and slaves, not sentinels. */ retval = redisAsyncCommand(ri->cc, sentinelInfoReplyCallback, NULL, "INFO"); if (retval == REDIS_OK) ri->pending_commands++; } else if ((now - ri->last_pong_time) > ping_period) { /* Send PING to all the three kinds of instances. */ sentinelSendPing(ri); } else if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) { /* PUBLISH hello messages to all the three kinds of instances. */ sentinelSendHello(ri); } } 处理INFO消息的返回: void sentinelInfoReplyCallback(redisAsyncContext *c, void *reply, void *privdata) { sentinelRedisInstance *ri = c->data; redisReply *r; REDIS_NOTUSED(privdata); if (ri) ri->pending_commands--; if (!reply || !ri) return; r = reply; if (r->type == REDIS_REPLY_STRING) { sentinelRefreshInstanceInfo(ri,r->str); } } 这里针对向master发送的INFO,sentinel会: 1. 获取master run_id记录,检查master的role,更新自己维护的master列表 2. 通过复制字段,获取master对应的slave列表,更新自己维护的slave列表 获取slave信息 同样,sentinel也会以同样的频率向slave发送INFO命令,并且提取以下参数: * run_id * role * master的host和port * 主从服务器的连接状态(master_link_status) * slave优先级(slave_priority) * slave复制偏移量(slave_repl_offset) 并更新自己维护的sentinelRedisInstance结构。 发送和接受订阅信息 sentinel每秒通过命令连接向所有master和slave发送信息。 int sentinelSendHello(sentinelRedisInstance *ri) { char ip[REDIS_IP_STR_LEN]; char payload[REDIS_IP_STR_LEN+1024]; int retval; char *announce_ip; int announce_port; sentinelRedisInstance *master = (ri->flags & SRI_MASTER) ? ri : ri->master; sentinelAddr *master_addr = sentinelGetCurrentMasterAddress(master); if (ri->flags & SRI_DISCONNECTED) return REDIS_ERR; /* Use the specified announce address if specified, otherwise try to * obtain our own IP address. */ if (sentinel.announce_ip) { announce_ip = sentinel.announce_ip; } else { if (anetSockName(ri->cc->c.fd,ip,sizeof(ip),NULL) == -1) return REDIS_ERR; announce_ip = ip; } announce_port = sentinel.announce_port ? sentinel.announce_port : server.port; /* Format and send the Hello message. */ snprintf(payload,sizeof(payload), "%s,%d,%s,%llu," /* Info about this sentinel. */ "%s,%s,%d,%llu", /* Info about current master. */ announce_ip, announce_port, server.runid, (unsigned long long) sentinel.current_epoch, /* --- */ master->name,master_addr->ip,master_addr->port, (unsigned long long) master->config_epoch); retval = redisAsyncCommand(ri->cc, sentinelPublishReplyCallback, NULL, "PUBLISH %s %s", SENTINEL_HELLO_CHANNEL,payload); if (retval != REDIS_OK) return REDIS_ERR; ri->pending_commands++; return REDIS_OK; } 发送内容包括: * sentinel ip(announce_ip) * sentinel端口(announce_port) * sentinel运行id(server.runid) * sentinel配置纪元(sentinel.current_epoch) * master名称(master->name) * master IP(master_addr->ip) * master端口(master_addr->port) * master纪元(master->config_epoch) 同时,所有连接到这个master上的sentinel都会收到这个消息,然后做出回应: * 更新sentinels字典 * 创建连接其他sentinel的命令连接 转载自:https://coolex.info/blog/471.html
etcd etcd用于服务发现的基础注册和通知,功能类似于zk,通过注册和监听,实现基础的服务发现。 安装 etcd安装非常简单,可以用go自己编译,etcd也提供了可以直接使用的二进制包(64位)。 具体的安装提示页面在github上, 直接按照上面的描述下载即可。为了方便,把里面的etcd相关的二进制文件(etcd, etcdctl等) 复制到了/usr/local/bin中,方便后续使用。 运行 首先尝试单机版启动,参照手册先直接启动,etcd默认监听的是localhost,既只监听了lo设备, 这样会导致启动后集群中的其他机器无法访问,因此在启动的时候将默认的localhost改成0.0.0.0, 确保etcd监听了所有网卡。 etcd -listen-client-urls "http://0.0.0.0:4001" -listen-peer-urls="http://0.0.0.0:7001" 启动之后可通过rest接口或者etcdctl执行命令: curl -L http://127.0.0.1:4001/version 输出结果为: {“releaseVersion”:”2.0.0″,”internalVersion”:”2″} 简单写入和读取: curl -L http://127.0.0.1:4001/v2/keys/message -XPUT -d value="Hello world" {“action”:”set”,”node”:{“key”:”/message”,”value”:”Hello world”,”modifiedIndex”:3,”createdIndex”:3}} curl -L http://127.0.0.1:4001/v2/keys/message {“action”:”get”,”node”:{“key”:”/message”,”value”:”Hello world”,”modifiedIndex”:3,”createdIndex”:3}} 集群启动 之前的启动方式为单机启动,集群创建官方给了很多种方式,这里尝试动态添加机器的方式。 etcd -name "node1" -initial-cluster "node1=http://10.211.55.11:2380" -initial-advertise-peer-urls "http://10.211.55.11:2380" -listen-client-urls "http://0.0.0.0:4001" -listen-peer-urls="http://0.0.0.0:2380" 启动之后,通过member api,可以查看到当前集群: curl -L http://10.211.55.11:4001/v2/members {“members”:[{“id”:”f0b31008acf03099″,”name”:”node1″,”peerURLs”:[“http://10.211.55.11:2380″,”http://10.211.55.11:7001″],”clientURLs”:[“http://localhost:2379″,”http://localhost:4001″]}]} 向集群中添加一台机器: etcdctl member add node2 http://10.211.55.12:2380 Added member named node2 with ID 6d345c68496f80fc to cluster ETCD_NAME=”node2″ ETCD_INITIAL_CLUSTER=”node2=http://10.211.55.12:2380,node1=http://10.211.55.11:2380,node1=http://10.211.55.11:7001″ ETCD_INITIAL_CLUSTER_STATE=”existing” 这里为了方便,没有只用rest接口,直接使用了etcdctl,该命令会提示在启动新的节点时所需要配置的环境变量。 启动新的节点: export ETCD_NAME="node2" export ETCD_INITIAL_CLUSTER="node1=http://10.211.55.11:2380,node2=http://10.211.55.12:2380" export ETCD_INITIAL_CLUSTER_STATE="existing" etcd -listen-client-urls http://0.0.0.0:4001 -advertise-client-urls http://0.0.0.0:4001 -listen-peer-urls http://10.211.55.12:2380 -initial-advertise-peer-urls http://10.211.55.12:2380 启动之后,通过member接口查看当前集群机器: etcdctl member list 293ea5ba1d70f5f1: name=node2 peerURLs=http://10.211.55.12:2380 clientURLs=http://0.0.0.0:4001 bd93686a68a54c2d: name=node1 peerURLs=http://10.211.55.11:2380 clientURLs=http://localhost:2379,http://localhost:4001 同样的方式再添加一台,成为基础的3台机器集群: export ETCD_NAME="node3" export ETCD_INITIAL_CLUSTER="node2=http://10.211.55.12:2380,node3=http://10.211.55.13:2380,node1=http://10.211.55.11:2380" export ETCD_INITIAL_CLUSTER_STATE="existing" etcd -listen-client-urls http://0.0.0.0:4001 -advertise-client-urls http://0.0.0.0:4001 -listen-peer-urls http://10.211.55.13:2380 -initial-advertise-peer-urls http://10.211.55.13:2380 最终集群为: 293ea5ba1d70f5f1: name=node2 peerURLs=http://10.211.55.12:2380 clientURLs=http://0.0.0.0:4001 76610041ace6c4f8: name=node3 peerURLs=http://10.211.55.13:2380 clientURLs=http://0.0.0.0:4001 bd93686a68a54c2d: name=node1 peerURLs=http://10.211.55.11:2380 clientURLs=http://localhost:2379,http://localhost:4001 在node1节点机器上运行: etcdctl set /message hello 之后,在三台机器中执行: etcdctl get /message 都能够正确的获取这个key的值:hello。 转载自:https://coolex.info/blog/481.html
confd confd通过读取配置(支持etcd,consul,环境变量),通过go的模板,生成最终的配置文件。 安装 安装和etcd一样,非常方便,已经提供了64位的可执行程序,下载下来之后直接放到PATH中(/usr/local/bin)即可(别忘了+x)。 haproxy配置生成 confd配置文件默认在/etc/confd中,可以通过参数-confdir指定。目录中包含两个子目录,分别是:conf.d templates。 confd会先读取conf.d目录中的配置文件(toml格式),然后根据文件指定的模板路径去渲染模板。 基于之前配置的etcd集群,读取/services/web中的目录和key。结构为: /services/web/$DOMAIN/$HOST/ip |-port 其中DOMAIN表示注册应用的域名,HOST表示注册机器的主机名。 首先创建confd配置文件: [template] src = "haproxy.cfg.tmpl" dest = "/home/babydragon/haproxy/haproxy.cfg" keys = [ "/services/web", ] #reload_cmd = "/etc/init.d/haproxy reload" 现在只测试模板生成,所以不关注检查、重启等命令,配置模板名称、目标路径和获取的key即可。 着重看下模板变动部分(前面就是写死的一些haproxy的标准配置,如日志等) frontend webin bind :80 {{$domains := lsdir "/services/web"}} {{range $domain := $domains}} acl is_{{$domain}} hdr(host) -i {{$domain}} {{end}} {{range $domain := $domains}} use_backend {{$domain}}_cluster if is_{{$domain}} {{end}} {{range $domain := $domains}} backend {{$domain}}_cluster cookie SERVERID insert indirect nocache {{$domain_dir := printf "/services/web/%s" $domain}}{{range $host := lsdir $domain_dir}} server {{base $host}} {{$ip_key := printf "/services/web/%s/%s/ip" $domain $host}}{{getv $ip_key}}:{{$port_key := printf "/services/web/%s/%s/port" $domain $host}}{{getv $port_key}} cookie {{base $host}} check {{end}} {{end}} 这里主要有两个循环,第一个循环所有的域名,第一个循环每一个域名下的所有主机。 haproxy需要通过设置acl的方式来进行按照域名做负载均衡。因此首先循环域名,为每个域名创建一个acl规则和一个规则的使用。 下面再通过一个循环,创建没个域名对应的后段。 confd模板详细文档可以参考github上的文档。 大致的意思是通过lsdir获取当前目录下的所有子目录。第一层子目录为域名,根据域名即可生成acl规则、规则使用、后端名称等数据。 再重新通过瓶装域名目录,对域名目录执行lsdir,读取目录下的每个主机名,创建后端的server条目(一个域名下的负载均衡后段服务器), 同时获取挂在这个目录下的属性键值对(这里只有ip和port)。 创建完模板和配置之后,先构造一些测试数据: etcdctl mkdir /services/web etcdctl mkdir /services/web/a.abc.com etcdctl mkdir /services/web/b.abc.com etcdctl mkdir /services/web/a.abc.com/server1 etcdctl mkdir /services/web/a.abc.com/server2 etcdctl mkdir /services/web/b.abc.com/server1 etcdctl set /services/web/a.abc.com/server1/ip 10.0.0.1 etcdctl set /services/web/a.abc.com/server1/port 10000 etcdctl set /services/web/a.abc.com/server2/port 10001 etcdctl set /services/web/a.abc.com/server2/ip 10.0.0.1 etcdctl set /services/web/b.abc.com/server1/ip 10.0.0.2 etcdctl set /services/web/b.abc.com/server1/port 12345 这里模拟三个容器,其中两个作为域名a.abc.com运行容器,一个作为b.abc.com容器。 然后执行confd,检查生成的配置文件: confd -confdir ./confd -onetime -backend etcd -node 127.0.0.1:4001 刚才那段模板渲染后为: frontend webin bind :80 acl is_a.abc.com hdr(host) -i a.abc.com acl is_b.abc.com hdr(host) -i b.abc.com use_backend a.abc.com_cluster if is_a.abc.com use_backend b.abc.com_cluster if is_b.abc.com backend a.abc.com_cluster cookie SERVERID insert indirect nocache server server1 10.0.0.1:10000 cookie server1 check server server2 10.0.0.1:10001 cookie server2 check backend b.abc.com_cluster cookie SERVERID insert indirect nocache server server1 10.0.0.2:12345 cookie server1 check 转载自:https://coolex.info/blog/483.html