分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019

技术文章列表

  • Http Server实现原理解读

    Tinyhttpd 是 J. David Blackstone 在1999年写的一个不到 500 行的超轻量型 Http Server,用来学习非常不错,可以帮助我们真正理解服务器程序的本质。官网:http://tinyhttpd.sourceforge.net
    每个函数的作用处理从套接字上监听到的一个 HTTP 请求,在这里可以很大一部分地体现服务器处理请求流程。

    bad_request:返回给客户端这是个错误请求,HTTP 状态吗 400 BAD REQUEST
    cat:读取服务器上某个文件写到 socket 套接字
    cannot_execute:主要处理发生在执行 cgi 程序时出现的错误
    error_die:把错误信息写到 perror 并退出
    execute_cgi:运行 cgi 程序的处理,也是个主要函数
    get_line:读取套接字的一行,把回车换行等情况都统一为换行符结束
    headers:把 HTTP 响应的头部写到套接字
    not_found:主要处理找不到请求的文件时的情况
    sever_file:调用 cat 把服务器文件返回给浏览器
    startup:初始化 httpd 服务,包括建立套接字,绑定端口,进行监听等
    unimplemented:返回给浏览器表明收到的 HTTP 请求所用的 method 不被支持

    建议源码阅读顺序:main —> startup —> accept_request —> execute_cgi,通晓主要工作流程后再仔细把每个函数的源码看一看。
    工作流程1) 服务器启动,在指定端口或随机选取端口绑定 httpd 服务
    2) 收到一个 HTTP 请求时(其实就是 listen 的端口 accpet 的时候),派生一个线程运行 accept_request 函数
    3) 取出 HTTP 请求中的 method (GET 或 POST) 和 url,。对于 GET 方法,如果有携带参数,则 query_string 指针指向 url 中 ?后面的 GET 参数
    4) 格式化 url 到 path 数组,表示浏览器请求的服务器文件路径,在 tinyhttpd 中服务器文件是在 htdocs 文件夹下。当 url 以 / 结尾,或 url 是个目录,则默认在 path 中加上 index.html,表示访问主页
    5) 如果文件路径合法,对于无参数的 GET 请求,直接输出服务器文件到浏览器,即用 HTTP 格式写到套接字上,跳到(10)。其他情况(带参数 GET,POST 方式,url 为可执行文件),则调用 excute_cgi 函数执行 cgi 脚本
    6) 读取整个 HTTP 请求并丢弃,如果是 POST 则找出 Content-Length. 把 HTTP 200 状态码写到套接字
    7) 建立两个管道,cgi_input 和 cgi_output, 并 fork 一个进程
    8) 在子进程中,把 STDOUT 重定向到 cgi_outputt 的写入端,把 STDIN 重定向到 cgi_input 的读取端,关闭 cgi_input 的写入端 和 cgi_output 的读取端,设置 request_method 的环境变量,GET 的话设置 query_string 的环境变量,POST 的话设置 content_length 的环境变量,这些环境变量都是为了给 cgi 脚本调用,接着用 execl 运行 cgi 程序
    9) 在父进程中,关闭 cgi_input 的读取端 和 cgi_output 的写入端,如果 POST 的话,把 POST 数据写入 cgi_input,已被重定向到 STDIN,读取 cgi_output 的管道输出到客户端,该管道输入是 STDOUT。接着关闭所有管道,等待子进程结束。这一部分比较乱,见下图说明:
    管道初始状态

    管道最终状态

    10) 关闭与浏览器的连接,完成了一次 HTTP 请求与回应,因为 HTTP 是无连接的
    1 留言 2019-04-24 10:50:21 奖励15点积分
  • 内存映射文件

    背景
    内存映射文件提供了一组独立的函数,使应用程序能够通过内存指针像访问内存一样访问磁盘上的文件。通过内存映射文件函数可以将磁盘上的文件全部或者部分映射到进程的虚拟地址空间的某个位置。一旦完成映射,对磁盘文件的访问就可以像访问内存文件一样简单。这样,向文件中写入数据的操作就是直接对内存进行赋值,而从文件的某个特定位置读取数据也就是直接从内存中取数据
    当内存映射文件提供了对文件某个特定位置的直接读写时,真正对磁盘文件的读写操作是由系统底层处理的。而且在写操作时,数据也并非在每次操作时即时写入到磁盘,而是通过缓冲处理来提高系统整体性能。
    内存映射文件的一个好处之一是,程序代码以标准的内存地址形式来访问文件数据,按照页面大小周期从磁盘写入数据的操作发生在后台,由操作系统底层来实现,这个过程对应用程序是完全透明的。虽然,内存映射文件最终还是要将文件从磁盘读入内存,实质上并没有省略什么操作,整体性能可能并没有获得什么提高,但是程序的结构会从中受益,缓冲区边界等问题将不复存在。而且对文件内容更新后的写操作也有操作系统自动完成,有操作系统判断内存中的页面是否为脏页面并仅将脏页面写入磁盘,比程序自己将全部数据写入文件的效率要高很多。

    函数介绍CreateFile 函数
    可打开或创建以下对象,并返回可访问的句柄:控制台,通信资源,目录(只读打开),磁盘驱动器,文件,邮槽,管道。
    函数声明
    HANDLE WINAPI CreateFile( _In_ LPCTSTR lpFileName, _In_ DWORD dwDesiredAccess, _In_ DWORD dwShareMode, _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes, _In_ DWORD dwCreationDisposition, _In_ DWORD dwFlagsAndAttributes, _In_opt_ HANDLE hTemplateFile);
    参数

    lpFileName:要打开的文件的名或设备名。这个字符串的最大长度在ANSI版本中为MAX_PATH,在unicode版本中为32767。dwDesiredAccess:指定类型的访问对象。如果为 GENERIC_READ 表示允许对设备进行读访问;如果为 GENERIC_WRITE 表示允许对设备进行写访问(可组合使用);如果为零,表示只允许获取与一个设备有关的信息 。dwShareMode:如果是零表示不共享; 如果是FILE_SHARE_DELETE表示随后打开操作对象会成功只要删除访问请求;如果是FILE_SHARE_READ随后打开操作对象会成功只有请求读访问;如果是FILE_SHARE_WRITE 随后打开操作对象会成功只有请求写访问。lpSecurityAttributes: 指向一个SECURITY_ATTRIBUTES结构的指针,定义了文件的安全特性。dwCreationDisposition:创建方式。hTemplateFile:为一个文件或设备句柄,表示按这个参数给出的句柄为模板创建文件(就是将该句柄文件拷贝到lpFileName指定的路径,然后再打开)。它将指定该文件的属性扩展到新创建的文件上面,这个参数可用于将某个新文件的属性设置成与现有文件一样,并且这样会忽略dwAttrsAndFlags。通常这个参数设置为NULL,为空表示不使用模板,一般为空。
    返回值

    如执行成功,则返回文件句柄。INVALID_HANDLE_VALUE表示出错,会设置GetLastError。

    CreateFileMapping 函数
    创建一个新的文件映射内核对象。
    函数声明
    HANDLE WINAPI CreateFileMapping( _In_HANDLE hFile, _In_opt_LPSECURITY_ATTRIBUTES lpAttributes, _In_DWORD flProtect, _In_DWORD dwMaximumSizeHigh, _In_DWORD dwMaximumSizeLow, _In_opt_LPCTSTR lpName);
    参数

    hFile
    需要映射到进程地址空间的文件的句柄。如果指定为INVALID_HANDLE_VALUE时,表示创建一个无文件句柄的文件映射,这种主要用来共享内存。(注意CreateFile的失败时的返回值为INVALID_HANDLE_VALUE。如果不加以检查,可能会出现潜在的问题,即创建了一个共享内存)
    lpAttributes
    安全属性。一般为NULL,表示使用默认的安全属性。
    flProtect
    下述常数之一:PAGE_READONLY:以只读方式打开映射PAGE_READWRITE:以可读、可写方式打开映射PAGE_WRITECOPY:为写操作留下备份可组合使用下述一个或多个常数:SEC_COMMIT:为文件映射一个小节中的所有页分配内存SEC_IMAGE:文件是个可执行文件SEC_RESERVE:为没有分配实际内存的一个小节保留虚拟内存空间
    dwMaximumSizeHigh
    文件映射的最大长度的高32位。
    dwMaximumSizeLow
    文件映射的最大长度的低32位。如这个参数dwMaximumSizeHigh
    都是零,就用磁盘文件的实际长度。
    lpName
    以0为终止符的字符串,用来给文件映射对象指定一个名称。一般用于进程间共享文件映射内核对象的。

    返回值

    返回值为文件映射的对象句柄。无法创建时,返回NULL。

    MapViewOfFile 函数
    将一个文件映射对象映射到当前应用程序的地址空间。
    函数声明
    LPVOID WINAPI MapViewOfFile(   __in HANDLE hFileMappingObject,   __in DWORD dwDesiredAccess,   __in DWORD dwFileOffsetHigh,   __in DWORD dwFileOffsetLow,   __in SIZE_T dwNumberOfBytesToMap);
    参数

    hFileMappingObject:为CreateFileMapping()返回的文件映像对象句柄。dwDesiredAccess:映射对象的文件数据的访问方式,而且同样要与CreateFileMapping 函数所设置的保护属性相匹配。 可取以下值:FILE_MAP_ALL_ACCESS:等价于CreateFileMapping的 FILE_MAP_WRITE | FILE_MAP_READ,文件映射对象被创建时必须指定PAGE_READWRITE 选项;FILE_MAP_COPY:可以读取和写入文件.写入操作会导致系统为该页面创建一份副本,在调用CreateFileMapping时必须传入PAGE_WRITECOPY保护属性;FILE_MAP_EXECUTE:可以将文件中的数据作为代码来执行,在调用CreateFileMapping时可以传入PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ保护属性;FILE_MAP_READ:可以读取文件,在调用CreateFileMapping时可以传入PAGE_READONLY或PAGE_READWRITE保护属性;FILE_MAP_WRITE:可以读取和写入文件,在调用CreateFileMapping时必须传入PAGE_READWRITE保护属性;dwFileOffsetHigh:表示文件映射起始偏移的高32位。dwFileOffsetLow:表示文件映射起始偏移的低32位。(64KB对齐不是必须的)dwNumberOfBytes:指定映射文件的字节数。
    返回值

    如果成功,则返回映射视图文件的开始地址值。如果失败,则返回 NULL,可调用 GetLastError 查看错误。

    实现过程使用WIN32 API来实现内存映射文件,实现步骤如下:

    调用 CreatFile 打开想要映射的文件,获得句柄hFile
    调用 CreatFileMapping 函数生成一个建立在CreatFile函数创建的文件对象基础上的内存映射对象,得到句柄hFileMap
    调用 MapViewOfFile 函数把整个文件的一个区域或者整个区域映射到内存中,得到指向映射到内存的第一个字节的指针 lpMemory
    用该指针来读写文件
    调用 UnmapViewOfFile 来解除文件映射,传入参数为 lpMemory
    调用 CloseHandle 来关闭内存映射文件,传入参数为 hFileMap
    调用 CloseHandle 来关闭文件,传入函数为 hFile

    编码实现BOOL MemoryMapFile(char *pszFileName){ HANDLE hFile = NULL; HANDLE hFileMap = NULL; LPVOID lpMemory = NULL; // 打开文件获取文件句柄 hFile = ::CreateFile(pszFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ShowError("CreateFile"); return FALSE; } // 创建文件映射内核对象 hFileMap = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, NULL); if (NULL == hFileMap) { ShowError("CreateFileMapping"); return FALSE; } // 将文件的数据映射到进程地址空间 lpMemory = ::MapViewOfFile(hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); if (NULL == lpMemory) { ShowError("MapViewOfFile"); return FALSE; } // 用 lpMemory 指针来读写文件 ::RtlCopyMemory(lpMemory, "My Name Is Demon`Gan", 21); // 关闭句柄释放内存 ::UnmapViewOfFile(lpMemory); ::CloseHandle(hFileMap); ::CloseHandle(hFile); return TRUE;}
    程序测试我们直接运行程序,对 520.txt 文件数据进行修改。修改前,520.txt 的内容如下所示:

    程序执行完毕之后,成功修改了文件数据。

    总结内存映射文件的实现步骤都是固定的,大家理解清晰使用到的函数意义及其各个参数的意义就好。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2018-11-07 11:35:08 奖励10点积分
  • 在DLL中创建共享内存

    背景我们都知道在 32 位系统上,每个进程都有自己 4GB 大小的独立空间,互不影响。当然,对内核了解的同学则会质疑说,4GB 大小的内容可以大致分为两部分,低 2 GB内存空间是用户地址空间,高 2 GB是内核地址空间,而内核地址空间是共享的,并非独立。是的,这没错。所以,本文中所指的独立空间是用户地址空间。
    举个例子来说,进程1 的 0x400000 内存地址和 进程2 中 0x400000 内存地址是没有任何关联。任意修改其中一个内存里的数据,是不影响另一个的。这边是进程独立性。
    但是,本文要介绍的这个知识点,就是要突破这个独立性的显示,创建进程共享内存。确切的说是在DLL中创建共享内存,就是在DLL中创建一个变量,然后DLL被加载到多个进程空间,只要其中一个进程修改了该变量的值,其他进程DLL中的这个值也会改变,就相当于多个进程共享一块内存。
    实现原理实现原理比较简单,就是先为DLL创建一个数据段,然后再对程序的链接器进行设置,使其在程序编译完毕开始链接的时候,根据设置的链接选项,把指定的数据段链接为共享数据段。这样,就可以创建共享内存了。
    #pragma data_seg("MySeg") char g_szText[256] = {0};#pragma data_seg()#pragma comment(linker, "/section:MySeg,RWS")
    例如,在上面的代码中,我们使用 #pragma data_seg 创建了一个名为 MySeg 的数据段,接着使用 /section:MySeg,RWS 把 MySeg 数据段设置为可读、可写、可共享的共享数据段。
    程序测试我们开发一个程序加载这个创建了共享内存的DLL,然后调用它的导出函数 Change 来修改共享内存变量的值,调用 Show 来显示共享内存变量的值。发现,只要有一个进程修改了这个变量,其他所有进程里的变量也跟着改变。

    总结这个实现上比较简单,难点就是在思想上的转换,要理解什么是共享内存的思想。
    参考参考自《Windows黑客编程技术详解》一书
    3 留言 2018-11-07 11:28:40 奖励5点积分
  • 跨域

    一、什么是跨域?1.1 什么是同源策略及其限制内容?同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个IP地址,也非同源。
    同源策略限制内容有:

    Cookie、LocalStorage、IndexedDB 等存储性内容
    DOM 节点
    AJAX 请求发送后,结果被浏览器拦截了

    但是有三个标签是允许跨域加载资源:

    <imgsrc=XXX>

    <linkhref=XXX>

    <scriptsrc=XXX>



    1.2 常见跨域场景当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。
    特别说明两点:

    第一:如果是协议和端口造成的跨域问题“前台”是无能为力的
    第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”

    这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?
    跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
    你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?
    因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。
    同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。
    1.3 跨域解决方案1.3.1 jsonpJSONP原理
    利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。
    JSONP和AJAX对比
    JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)
    JSONP优缺点
    JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。
    JSONP的实现流程

    声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)
    创建一个 <script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)
    服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是 show(‘我不爱你’)
    最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作

    JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。
    1.3.2 corsCORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
    浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
    服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
    虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。
    简单请求
    只要同时满足以下两大条件,就属于简单请求

    条件1:使用下列方法之一:

    GETHEADPOST
    条件2:Content-Type 的值仅限于下列三者之一:

    text/plainmultipart/form-dataapplication/x-www-form-urlencoded

    请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。
    复杂请求
    不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。
    1.3.3 postMessagepostMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

    页面和其打开的新窗口的数据传递
    多窗口之间消息传递
    页面与嵌套的iframe消息传递
    上面三个场景的跨域数据传递

    postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
    otherWindow.postMessage(message, targetOrigin, [transfer]);
    message: 将要发送到其他 window的数据。
    targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串”*”(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送
    transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

    1.3.4 websocketWebsocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。
    但是WebSocket是一种双向通信协议,在建立连接之后,WebSocket的server与 client都能主动向对方发送或接收数据。
    同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
    原生WebSocket API使用起来不太方便,我们使用 Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
    1.3.5 Node中间件代理(两次跨域)实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。代理服务器,需要做以下几个步骤:

    接受客户端请求
    将 请求 转发给服务器
    拿到服务器 响应 数据
    将 响应 转发给客户端

    1.3.6 nginx反向代理实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。
    使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
    实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
    1.4 总结
    CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
    JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
    不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
    日常工作中,用得比较多的跨域方案是cors和nginx反向代理

    参考文章
    跨域资源共享 CORS 详解
    前端面试之道
    window.postMessage
    前端常见跨域解决方案(全)
    深入跨域问题(4) - 利用代理解决跨域
    3 留言 2019-04-08 11:06:29 奖励15点积分
  • 基于Python使用词云图

    1 引言词语图,也叫文字云,是对文本出现频率较高的“关键词”予以视觉化的展现,词云图过滤掉大量的低频低质的文本信息,使得浏览者一下就可以知道文章的主旨。
    2 模块准备import jieba # 分词模块import matplotlib.pyplot as plt # 画图模块from wordcloud import WordCloud # 文字云模块from scipy.misc import imread # 处理图像的函数,用于读取并处理背景图片
    3 实现的思路准备一份需要分析的文本材料,这里选用的是 19 年两会政府工作报告,首先用 jieba 模块对文本材料进行分词处理(即识别出一个个有意义的词语),然后对处理后的材料使用WordCloud 文字云模块生成相应的词云图片即可of course,你也可以选择一张背景图片,以此为背景生成特定的云图。
    4 代码实现def wordcloud(): """ 背景图片为自定义的一个矩阵 :return: 词云图 """ # 读取词源文件 二进制的形式 with open("./govreport.txt", "rb") as f: t = f.read() # 保存为str类型 ls = jieba.lcut(t) # 进行分词 txt = " ".join(ls) # 把分词用空格连起来 # 设置词云的参数 w = WordCloud( font_path="msyh.ttc", # 设置字体 width=1000, # 设置输出的图片宽度 height=700, # 设置输出的图片的高度 background_color="white", # 设置输出图片的背景色 ) w.generate(txt) # 生成词云 w.to_file("./wordColud.png") # 将图片保存 return Nonedef wordcloud2(): """ 用指定的图片生成词云图 :return: 词云图 """ # 词源的文本文件 wf = "./govreport.txt" word_content = open(wf, "r", encoding="utf-8").read().replace("\n", "") # 设置背景图片 img_file = "./map.jpg" # 解析背景图片 mask_img = imread(img_file) # 进行分词 word_cut = jieba.lcut(word_content) # 把分词用空格连起来 word_cut_join = " ".join(word_cut) # 设置词云参数 wc = WordCloud( font_path="SIMYOU.TTF", # 设置字体 max_words=2000, # 允许最大的词汇量 max_font_size=90, # 设置最大号字体的大小 mask=mask_img, # 设置使用的背景图片,这个参数不为空时,width和height会被忽略 background_color="white", # 设置输出的图片背景色 ) # 生成词云 wc.generate(word_cut_join) # 用于显示图片,需要配合plt.show()一起使用 plt.imshow(wc) plt.axis("off") # 去掉坐标轴 plt.savefig("./wordcloudWithMap.png") plt.show() return None
    5 效果展示不带背景图片的词云图

    带有中国地图的词云图
    1 留言 2019-04-21 10:18:25 奖励12点积分
  • 使用VS2013实现修改其他程序的图标

    背景之前,写了一个程序,程序中有一个功能就是获取一个EXE程序的图标,然后,把这个图标更改为另一个EXE程序的图标。在网上搜索了很久,都没有找到相关的例子。找到的大都是修改自己程序的图标,或者是使用一个 .ico 的图标文件,去更改指定程序的图标。虽然,这些功能和自己想要开发的功能有些不同,但,也还是具有一定的参考价值的。
    现在,我就对修改指定程序图标的方式进行总结。总结出 3 种修改方式:

    替换的图标存储在自己程序的资源中
    替换的图标以 .ico 图标文件形式提供
    替换的图标是其他一个EXE程序的图标

    现把程序实现的原理以及实现的过程,写成文档分享给大家,方便大家的参考。
    函数介绍FindResource介绍
    函数声明
    HRSRC FindResource( HMODULE hModule, LPCWSTR lpName,LPCWSTR lpType );
    参数

    hModule:处理包含资源的可执行文件的模块。NULL值则指定模块句柄指向操作系统通常情况下创建最近过程的相关位图文件。lpName:指定资源名称。若想了解更多的信息,请参见注意部分。lpType:指定资源类型。若想了解更多的信息,请参见注意部分。作为标准资源类型。这个参数的含义同EnumResLangProc\lpType。
    返回值

    如果函数运行成功,那么返回值为指向被指定资源信息块的句柄。为了获得这些资源,将这个句柄传递给LoadResource函数。如果函数运行失败,则返回值为NULL。若想获得更多错误信息,请调用GetLastError函数。

    SizeofResource 介绍
    函数声明
    DWORD SizeofResource( HMODULE hModule, // module handle HRSRC hResInfo // resource handle );
    参数

    hModule:包合资源的可执行文件模块的句柄。hReslnfo:资源句柄。此句柄必须由函数FindResource或FindResourceEx来创建。
    返回值

    如果函数运行成功,返回值资源的字节数。如果函数运行失败,返回值为零。若想获得更多的错误信息,请调用GetLastError函数。

    LoadResource 介绍
    函数声明
    HGLOBAL LoadResource( HMODULE hModule, // module handle HRSRC hResInfo // resource handle );
    参数

    hModule:处理包合资源的可执行文件的模块句柄。若hModule为NULL,系统从当前过程中的模块中装载资源。hReslnfo:将被装载资源的句柄。它必须由函数FindResource或FindResourceEx创建。
    返回值

    如果函数运行成功,返回值是相关资源的数据的句柄。如果函数运行失败,返回值为NULL。若想获得更多的错误信息,请调用GetLastError函数。

    LockResource 介绍
    函数声明
    LPVOID LockResource( HGLOBAL hResData // handle to resource );
    参数

    hResDate:被装载的资源的句柄。函数LoadResource可以返回这个句柄。
    返回值

    如果被装载的资源被锁住了,返回值是资源第一个字节的指针;否则为NULL。

    BeginUpdateResource 介绍
    该函数返回一个可被UpdateResource函数使用的句柄以便在一个可执行文件中增加、删除或替换资源。
    函数声明
    HANDLE BeginUpdateResource( LPCTSTR pFileName, // executable file name BOOL bDeleteExistingResources // deletion option );
    参数

    pFileName:指向一个表示结束的空字符串指针,它是用来指定用以更新资源的基于32-位可执行文件的文件名。应用程序必须获得访问这个文件的可写权限,并且此文件在当前状态下不能被执行。如果pFileName未被指定完全路径,系统将在当前路径下搜寻此文件。bDeleteExistingResources:说明是否删除pFileName参数指定的现有资源。如果这个参数为TRUE则现有的资源将被删除,而更新可执行文件只包括由UpdateResource函数增加的资源。如果这个参数为FALSE,则更新的可执行文件包括现有的全部资源,除非通过UpdateResource特别说明被删除或是替换的。
    返回值

    如果此函数运行成功,其值将通过使用UpdateResource和EndUpdateResource函数返回一个句柄。如果被指定的文件不是一个可执行文件,或者可执行文件已被装载,或者文件不存在,或是文件不能被打开写入时,则返回值为空。若想获得更多的错误信息,请调用GetLastError函数。

    UpdateResource 介绍
    增加 删除 或替文件中的资源。
    函数声明
    BOOL UpdateResource( HANDLE hUpdate, // update-file handle LPCTSTR lpType, // resource type LPCTSTR lpName, // resource name WORD wLanguage, // language identifier LPVOID lpData, // resource data DWORD cbData // length of resource data );
    参数

    hUpdate:指定更新文件句柄。此句柄由BeginUpdateResource函数返回。lpType:指向说明将被更新的资源类型的字符串,它以NULL为终止符。这个参数可以是一个通过宏MAKENTRESOURCE传递的整数值,含义参见EnumResLangProc\lpType。lpName:指向说明待被更新的资源名称的字符串,它以NULL为终止符。这个参数可以是一个通过宏MAKEINTRESOURCE传递的整数值。wLanguage:指定将被更新资源的语言标识。要了解基本的语言标识符以及由这些标识符组成的字语言标识符的列表,可参见宏MAKELANGID。lpData:指向被插入可执行文件的资源数据的指针。如果资源是预定义类型值之一,那么数据必须是有效且适当排列的。注意这是存储在可执行文件中原始的一进制数据,而不是由Loadlcon,LoadString或其他装载特殊资源函数提供的数据。所有包含字符串、文本的数据必须是Unicode格式;IpData不能指向ANSI数据。如果lpData为NULL,所指定的资源将从可执行文件中被删除。cbData:指定lpData中的资源数据数据大小,以字节计数。
    返回值

    如果函数运行成功,返回值为非零;如果函数运行失败,返回值为零。若想获得更多的错误信息,请调用GetLastError函数。

    EndUpdateResource 介绍
    终止在可执行文件中的资源更新。
    函数声明
    BOOL EndUpdateResource( HANDLE hUpdate, // update-file handle BOOL fDiscard // write option );
    参数

    hUpdate:用于资源更新的句柄。此句柄通过BeginUpdateResource函数返回。fDiscard:用来说明是否向可执行文件中写入资源更新内容。如果此参数为TRUE,则在可执行文件中无变化;如果此参数为FALSE,则在可执行文件中写入变化。
    返回值

    如果函数运行成功,并且通过调用UpdateResource函数指定的不断积聚的资源修正内容被写入指定的可执行文件,那么其返回值为非零。如果函数运行失败,其返回值为零。若想获得更多的错误信息,请调用GetLastError函数。

    程序实现原理有一个知识点大家需要了解的就是:
    EXE程序会默认把资源ID号最小的ICON类型的图标资源作为自己的程序图标。
    也就是说,要想更改EXE程序的图标,只需要把一个图标创建或替换程序中ICON类型的资源ID最小的图标就可以了。这样,程序就会默认把该图标资源作为自己的程序图标,显示出来。
    那么,图标修改的大致原理就是:

    获取替换图标的图标数据的存储地址
    使用 UpdateResource 函数定位出程序 RT_ICON 类型、资源ID为 1 的图标资源,并使用上述的替换图标数据进行创建或者替换

    这样,就成功替换或者创建了一个资源类型是 RT_ICON,而且资源ID为 1 的最小的图标资源。要注意的是,最小的资源ID号是以 1 开始计数,而不是以 0 开始的,这个需要注意。
    那么,对于上面提到的 3 种方式,主要是获取图标数据的方式上的区别而已。
    针对第 1 种方式:替换的图标存储在自己程序的资源中。获取图标数据的原理就是:

    首先,我们先定位到我们程序里的资源,主要是根据“资源类型”和“资源名称”进行定位,获取资源信息块的句柄
    然后,根据上面获取的资源信息块的句柄,把资源加载到我们程序的内存中
    最后,锁定加载到内存中的资源的内存,防止程序的其他操作影响到这块内存,获取图标数据的首地址

    针对第 2 种方式:替换的图标以 .ico 图标文件形式提供。获取图标数据的原理就是:

    首先,以读方式打开文件,并获取文件的大小
    然后,将文件数据内容全部都出去出来,这样便获取到了图标数据

    针对第 3 种方式:替换的图标是其他一个EXE程序的图标。获取图标数据的方法和第 1 种方式的方法类似,原理是:

    首先,我们先把想要提取图标的EXE将在到我们的程序中来,并获取加载句柄
    然后,根据程序加载句柄,定位到程序里的资源,主要是根据“资源类型”和“资源名称”进行定位,获取资源信息块的句柄
    接着,根据上面获取的资源信息块的句柄,把资源加载到我们程序的内存中
    最后,锁定加载到内存中的资源的内存,防止程序的其他操作影响到这块内存,获取图标数据的首地址

    编码实现在这 3 种方式的图标数据获取中,最麻烦,而且最能体现技术含量的就是第 3 中形式。所以,我们以第 3 种实现方式为例,写出实现的代码。对于其余两种,网上有很多相关的例子和参考资料,相信大家根据原理部分应该自己也可以独立写出来,在此就不给出代码了。
    BOOL ChangeIcon(char *pszChangedIconExeFileName, char *pszSrcIconExeFileName){ // 将在其他程序,并获取程序模块句柄 HMODULE hEXE = ::LoadLibrary(pszSrcIconExeFileName); if (NULL == hEXE) { FreeRes_ShowError("LoadLibrary"); return FALSE; } // 获取其他EXE程序图标资源数据 HRSRC hRsrc = ::FindResource(hEXE, (LPCSTR)1, RT_ICON); if (NULL == hRsrc) { FreeRes_ShowError("FindResource"); return FALSE; } // 获取资源大小 DWORD dwSize = ::SizeofResource(hEXE, hRsrc); if (0 >= dwSize) { FreeRes_ShowError("SizeofResource"); return FALSE; } // 加载资源到程序内存 HGLOBAL hGlobal = ::LoadResource(hEXE, hRsrc); if (NULL == hGlobal) { FreeRes_ShowError("LoadResource"); return FALSE; } // 锁定资源内存 LPVOID lpVoid = ::LockResource(hGlobal); if (NULL == lpVoid) { FreeRes_ShowError("LockResource"); return FALSE; } // 开始修改图标 HANDLE hUpdate = ::BeginUpdateResource(pszChangedIconExeFileName, FALSE); if (NULL == hUpdate) { FreeRes_ShowError("BeginUpdateResource"); return FALSE; } // 如果资源ID存在, 则替换资源; 否则创建资源 // 程序把ICON的最小的资源ID作为程序图标, 所以从1开始, 1最小 BOOL bRet = ::UpdateResource(hUpdate, RT_ICON, (LPCSTR)1, LANG_NEUTRAL, lpVoid, dwSize); if (FALSE == bRet) { FreeRes_ShowError("UpdateResource"); return FALSE; } ::EndUpdateResource(hUpdate, FALSE); // 释放模块 ::FreeLibrary(hEXE); return TRUE;}
    程序测试现在,我们在 main 函数中,调用上述封装好的函数进行测试,把 “520.exe” 程序的图标改成 “360.exe” 程序的图标。
    main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ // 把 520.exe 的图标更改为 360.exe 的图标 if (FALSE == ChangeIcon("C:\\Users\\DemonGan\\Desktop\\520.exe", "C:\\Users\\DemonGan\\Desktop\\360.exe")) { printf("Change Icon Error!\n"); } else { printf("Change Icon OK!\n"); } system("pause"); return 0;}
    在测试之前,“360.exe” 程序和 “520.exe” 程序图标如下图所示:

    运行修改图标的程序,提示执行成功:

    然后,再次查看 “360.exe” 程序和 “520.exe” 程序,发现 “520.exe” 图标成功更改了:

    由此可见,程序测试成功。
    总结改程序的核心原理就是:EXE程序会默认把资源ID号最小的ICON类型的图标资源作为自己的程序图标。
    其中,测试的时候,需要注意的一个问题就是,图标更改成功后,可能会看到图标仍然没有更改。那可能是因为系统缓存了原来程序的图标,并没有更新图标缓存的缘故。最简单的方法就是,把修改图标后的程序,拷贝到其他目录下进行查看,这样操作比较简便些。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2018-11-19 09:19:19 奖励8点积分
  • C语言-基于Huffman编码原理的译码解压缩程序

    C语言实现基于Huffman编码原理的译码解压缩程序
    huffman编码原理这里不做介绍,梳理一下解码的代码实现。
    解码解压缩部分输入已编码文本和编码表,输出解码文本。

    1.获取已编码文本void readfile(int op){ FILE* fp; int text_i = 0; char tempc; fp = fopen(".\\outfile.txt", "r"); if (fp != NULL) { while (fscanf(fp, "%c", &tempc) != EOF) { de_str_text[text_i] = tempc; text_i++; } de_str_text[text_i] = '\0'; fclose(fp); } else { printf("文件读取失败!"); getchar(); exit(0); }}2.获取码表并生成huffman树void bulid_detree(){ FILE *fp; de_root = (Hufftree *)malloc(sizeof(Hufftree)); Hufftree* p = de_root; Hufftree* t; int tag = 0; de_root->lchild = de_root->rchild = NULL; int i = 0, j = 1, k = 0; fp = fopen(".\\mabiao.txt", "r"); if (fp) { for (i = 0; i < charmaxsize; i++) { if ((fgets(mabiao[i], codesize, fp)) == NULL) { break; } else if (tag) { for (k = 0; k < codesize - 1; k++) { mabiao[i][codesize - 1 - k] = mabiao[i][codesize - 2 - k]; } mabiao[i][0] = mabiao[i - 1][0]; tag = 0; } if (mabiao[i][1] == '\0') { tag = 1; } } fclose(fp); } else { printf("码表读取失败"); return; } i = 0; while (mabiao[i][0] != '\0') { p = de_root; while (mabiao[i][j] != '\0') { if (mabiao[i][j] == '1') { if (p->rchild == NULL) { t = (Hufftree*)malloc(sizeof(Hufftree)); t->lchild = t->rchild = NULL; p->rchild = t; p = p->rchild; } else { p = p->rchild; } } if (mabiao[i][j] == '0') { if (p->lchild == NULL) { t = (Hufftree*)malloc(sizeof(Hufftree)); t->lchild = t->rchild = NULL; p->lchild = t; p = p->lchild; } else { p = p->lchild; } } j++; } p->character = mabiao[i][0]; j = 1; i++; } }3.已编码文本转化成伪二进制流void ConvertoBit(){ int bit_i = 0, str_i = 0,i = 0; char temp[7]; int asc; Hufftree *seek; //转化为伪二进制数组 for (str_i = 0; str_i < strmaxsize; str_i++) { if (de_str_text[str_i] != NULL) { asc = (int)de_str_text[str_i]-31; for (i = 0; i < 6; i++) { if (asc % 2 == 0) { temp[5 - i] = '0'; } else { temp[5 - i] = '1'; } asc = asc / 2; } temp[6] = '\0'; strcat(de_strbit_temp, temp); } else { break; } } puts(de_strbit_temp);}4.根据伪二进制流查找huffman树输出字符void decode(){//按照码表转换成原文本 seek = Root; str_i = 0; while (de_strbit_temp[bit_i] != NULL) { if (seek->character != NULL) { de_strcode[str_i] = seek->character; str_i++; seek = Root; } else if (de_strbit_temp[bit_i] == '1') { seek = seek->rchild; bit_i++; } else { seek = seek->lchild; bit_i++; } } de_strcode[str_i] ='\0';}输出解码文档void outfile(int op){ int i = 0; FILE* fp; fp = fopen(".\\decodefile.txt", "w"); fputs(de_strcode, fp); fclose(fp);}至此完成解码部分tip由于解码时,伪二进制流不足6位时会补零可能会导致出现多余字符。
    附:结构体定义//字符编码信息typedef struct Huff_char { char character = 0; int count = 0; char code[20];}Huff_char;//字符队列typedef struct Huffchar { char character = 0; int count = 0;//统计词频 struct Hufftree* self = NULL;}Huffchar;//哈夫曼树节点typedef struct Hufftree { char character;//节点字符信息 int power;//权值 struct Hufftree* self, *rchild, *lchild;//自身地址,左右孩子}Hufftree;
    1 留言 2018-11-09 21:04:01 奖励5点积分
  • 上传资源,获取积分 精华

    上传资源,获取积分“WRITE-BUG技术共享平台”是一个专注于校园计算机技术交流共享的平台,面向的主要目标群体是我们计算机相关专业的大学生。在平台上,大家既可以交流学校课内学习的心得体会,也可以分享自己课外积累的技术经验。
    为了充实平台的资源库,更好地服务于各位同学,平台决定推出“众源计划”,有偿征集同学们自己计算机专业的作业、课程设计或是毕业设计等资源。“众源计划”的主要目的是创建一个具有一定规模的“技术资源库”,资源库里的每一份资源,都必须有详细的开发文档和可编译的源代码。
    作业、课程设计或是毕业设计等资源是同学们自己辛苦付出的成果,也是自己技术进步的见证。这部分资源通常都有详细的开发文档和完整的程序源代码,能够帮助其他初学者更好地消化和吸收将要学习的技术,降低学习门槛。所以,平台决定积分奖励征集这些资源。
    具体要求活动对象
    在校或者已毕业的计算机相关专业大学生,院校不限
    奖励方式
    资源上传并审核通过后,根据资源质量,奖励每贴 10 - 100 点积分
    上传流程
    会员登录自己的账号上传资源
    资源上传后,管理员会在 24 小时之内审核资源
    审核通过后,管理员会立即发放奖励积分至所对应账户

    审核重点
    重点审核资源是否具有详细的文档和完整的源代码
    审查资源是否原创,切勿重复提交

    资源要求“众源计划”仅对两类资源进行积分奖励征集,分别是“课内资源”和“课外资源”,各类资源具体要求如下所示。

    课内资源

    内容范围:计算机相关专业课内的毕业设计、课程设计、小学期、大作业等课程内开发的程序,程序包括游戏、PC程序、APP、网站或者其他软件形式
    内容要求:资源必须要包括完整的程序源代码和详细的开发文档或报告
    具体“课内资源”征集程序列表见附录一

    课外资源

    内容范围:计算机相关专业的课外自己主导研究游戏、项目、竞赛、个人研究等,区别于课程设计和毕业设计等课内资源
    内容要求:资源必须要包括完整的程序源代码和详细的开发文档或报告
    具体“课外资源”征集程序列表见附录二


    附录一注意:“众源计划”的题目范围包括且不限于以下题目

    汇编语言课程设计题目列表

    屏幕保护程序分类统计字符个数计算机钢琴程序字符图形程序音乐盒程序电子闹钟程序俄罗斯方块打字游戏图形变换程序吃豆子程序其他
    C语言课程设计题目列表

    学生成绩管理系统图书信息管理系统设计销售管理管理系统飞机订票管理系统教师评价系统学校运动会管理系统文本文件加密技术英语字典电话簿管理系统流星雨的实现其他
    C++语言课程设计题目列表

    学生学籍管理系统高校人员信息管理系统学生成绩管理系统车辆管理系统职工工作量统计系统学生考勤管理系统单项选择题标准化考试系统图书管理系统超市商品管理系统模拟ATM机存取款管理系统其他
    JAVA语言课程设计题目列表

    简单投票管理系统数学练习题目自动生成系统华容道小游戏电子英汉词典加密与解密标准化考试系统排球比赛计分系统学籍管理系统绘图系统图书信息管理系统其他
    C#语言课程设计题目列表

    学生信息管理系统学生综合测评系统图书管理系统学校运动会管理系统个人通讯录管理系统教师工资管理系统教师工作量管理系统趣味小游戏物资库存管理系统图形图像处理系统其他
    JSP语言课程设计题目列表

    微博系统基于web的学生信息管理系统在线计算机等级考试报名系统在线问卷调查系统网上销售系统论坛系统图书借阅管理系统网上购物系统工资管理系统酒店管理系统其他
    数据结构与算法课程设计题目列表

    设计哈希表实现电话号码查询系统电报压缩/解压缩系统电费核算系统机房计费管理系统公交线路查询系统用二叉平衡树实现图书管理系统运动会赛事安排动态表达式求值用线性结构实现学生成绩管理求解迷宫问题其他
    编译原理课程设计题目列表

    First集和Follow集生成算法模拟LL(1)分析过程模拟FirstVT集和LastVT集生成算法模拟算符优先分析表生成模拟算符优先分析过程模拟LR分析过程模拟PL/0语言的词法分析程序C语言的预处理程序自动机的状态转换图表示数组越界检查工具其他
    操作系统课程设计题目列表

    动态分区分配方式的模拟进程调度模拟算法请求调页存储管理方式的模拟P、V操作及进程同步的实现银行家算法SPOOLING假脱机输出的模拟程序文件系统设计动态不等长存储资源分配算法磁盘调度算法处理机调度算法模拟其他
    数据库课程设计题目列表

    高校学籍管理系统在线投稿审稿管理系统产品销售管理系统高校人力资源管理系统高校课程管理系统酒店客房管理系统报刊订阅管理系统医药销售管理系统学生学籍管理系统餐饮管理系统其他
    计算机网络课程设计题目列表

    TCP通信功能实现网络游戏的开发基于UDP协议网上聊天程序Ping 程序的实现数据包的捕获与分析FTP客户端设计包过滤防火墙的设计与实现简单的端口扫描器简单Web服务器的设计与实现HTTP客户端的设计与实现其他
    软件工程课程设计题目列表

    学校教材订购系统网上选课管理系统简易办公系统图书馆管理系统校园交流论坛网站超市收银系统ATM柜员机模拟程序企业办公自动化管理系统学生成绩管理系统进销存管理系统其他
    VC++程序设计课程设计题目列表

    模拟时钟程序单向链表的操作演示程序电影院售票系统俄罗斯方块五子棋24点游戏背单词软件的设计与实现酒店管理系统餐厅就餐管理系统吹泡泡游戏其他
    其他课程设计

    PHP语言课程设计PYTHON语言课程设计计算机图形学课程设计机器学习课程设计密码学课程设计其他

    附录二注意:“众源计划”的题目范围包括且不限于以下题目

    人脸识别系统车牌识别系统旅游自助APP疲劳驾驶识别检测系统考试管理系统WINDOWS驱动级安全防御系统WINDOWS平台逆向调试器坦克大战小游戏情感分析系统人机博弈的国际象棋游戏其他
    最终解释权归 WRITE-BUG技术共享平台 所有
    11 留言 2018-12-20 10:09:45 奖励100点积分
  • 基于Python使用TensorBoard可视化工具

    首先展示一下代码:
    writer = tf.summary.FileWriter("./summary/linear-regression-0/", sess.graph)
    我们将数据流图的事件保存到当前目录下的/summary/linear-regression-0/中,记得后面关闭一下 writer.close(),我们来看一下运行后的结果。

    那么我们现在需要如何显示呢?写这个目的就是告诉到家是如何使用 TensorBoard 可视化的,步骤如下:

    Windows+R 打开 Terminal
    cd 到 summary 目录(注意不是 cd 到 linear-regression-0 目录)
    使用 TensorBoard —logdir=linear-regression-0

    我们来看一下图应该就能明白:

    如果打不开,中间的 DESKTOP-A0JMDP5 换成 localhost 试试!好了!现在让我们输入网址试试:
    1 留言 2019-04-20 12:26:23 奖励12点积分
  • 线程同步之临界区

    背景在学校学习《操作系统》这门课程的时候,进程线程的调度同步都是比较重要的知识点。关于线程的同步方法有很多种,例如互斥量、信号量、事件、临界区等。
    本文就从编程实现的角度谈谈编程中临界区在多线程同步中的使用方式,现在,把实现的思路和过程写成文档,分享给大家。
    实现原理有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
    临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用WIN32函数EnterCriticalSection 和 LeaveCriticalSection 去进入和离开一个临界区。
    所用到的CRITICAL_SECTION结构对象必须要事先经过 InitializeCriticalSection 的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
    临界区使用完毕之后,要使用 DeleteCriticalSection 函数删除临界区,释放资源。
    编码实现为了更好理解临界区的概念,我们开发了这样一个程序:开启 3 个线程,每个线程都循环打印显示同一个整型全局变量,并将它自增1。要求,每个数字只显示一遍,而且是按 1 递增显示。
    如果我们什么操作不加的话,直接创建 3 个多线程去打印显示的话,代码如下:
    UINT ThreadProc(LPVOID lpVoid){ while (TRUE) { printf("%d\n", g_iCount); g_iCount++; if (g_iCount > iStop) { break; } } return 0;}BOOL CriticalSectionTest(){ // 多线程1 HANDLE hThread1 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 多线程2 HANDLE hThread2 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 多线程3 HANDLE hThread3 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 等待线程结束 ::WaitForSingleObject(hThread1, INFINITE); ::WaitForSingleObject(hThread2, INFINITE); ::WaitForSingleObject(hThread3, INFINITE); // 关闭句柄 ::CloseHandle(hThread1); ::CloseHandle(hThread2); ::CloseHandle(hThread3); return TRUE;}
    那么,上面程序的显示结果如下图所示:

    可以看到,输出的每个数字并不是都唯一,有些都重复输出,而且并不是按递增 1 的规律输出的。原因细想一下应该就明白了,创建了 3 个多线程,有可能会出现当线程1执行完打印输出的时候,线程2也执行到打印输出,这是全局变量g_iCount值还是0,所以便会重复输出。而它们也会同时执行g_iCount++,那么之后的输出就不会按递增1进行输出了。
    所以,为了每次输出和全局变量递增的时候,只能有一个线程去的独占执行,这时就需要临界区的帮助了。
    加入临界区后的代码就变成下面这样子:
    UINT ThreadProc(LPVOID lpVoid){ while (TRUE) { // 进入临界区 ::EnterCriticalSection(&g_cs); printf("%d\n", g_iCount); g_iCount++; if (g_iCount > iStop) { break; } // 离开临界区 ::LeaveCriticalSection(&g_cs); } return 0;}BOOL CriticalSectionTest(){ // 初始化临界区 ::InitializeCriticalSection(&g_cs); // 多线程1 HANDLE hThread1 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 多线程2 HANDLE hThread2 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 多线程3 HANDLE hThread3 = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL); // 等待线程结束 ::WaitForSingleObject(hThread1, INFINITE); ::WaitForSingleObject(hThread2, INFINITE); ::WaitForSingleObject(hThread3, INFINITE); // 关闭句柄 ::CloseHandle(hThread1); ::CloseHandle(hThread2); ::CloseHandle(hThread3); // 删除临界区 ::DeleteCriticalSection(&g_cs); return TRUE;}
    代码的执行效果如下所示:

    这时,输出满足要求了,每个数字只显示 1 遍,而且是递增 1 输出。也就说明临界区起到了同步的效果,每次显示和递增,都保证了只能一个线程执行这部分的代码。
    总结在编程开发中,使用临界区之前一定记得要进行初始化操作,使用完毕后,要删除临界区。
    2 留言 2018-11-07 10:59:10 奖励5点积分
  • 使用VS2013编译Crypto++加密库

    背景近期写了一个关于AES和RSA加解密的小程序,其中加解密模块使用开源的Crypto++加密库写的。在程序使用Crypto++加密库之前,需要下载Crytpo++加密库的源码到本地,自己编译,得到库文件。
    当时的最新版Crypto++版本是5.6.5版本,支持到VS2010开发环境编译,所以,我使用VS2013去编译的话,还需要项目进行一些编译的设置。
    现把当时编译的过程详细描述出来,形成文档,方便有需要的人们参考。
    准备工作首先,你要有个VS2013开发环境。因为本文讲解的是使用VS2013开发环境进行编译的,理论上VS2010、VS2012、VS2015、VS2017等应该也适用的。
    其次,需要到Crypto++官网 (https://www.cryptopp.com) 下载Crypto++库的源码。本文以“Crypto++ 5.6.5”版本为例进行讲解。
    编译过程1. 升级解决方案将“Crypto++ 5.6.5”版本Crypto++库源码下载下来后,进行解压缩。使用VS2013开发环境打开“cryptest.sln”解决方案文件。VS2013提示说“升级VC++编译器和库”,这时我们点击“确定”。因为Crypto++项目工程原来使用VS2010开发的,现在我们使用VS2013重新进行编译,所以要对项目进行升级。

    升级完成后,我们可以在左侧看到项目列表,其中“cryptlib”项目工程就是我们将要进行编译的项目工程。

    2. 更改项目工程平台工具集选中要编译的“crytplib”项目工程,鼠标右击,选中“属性”,打开“属性页”。然后,在“平台工具集”的选项中,选择“Visual Studio 2013 - Windows XP (v120_xp)”,表示兼容XP平台,也就是在XP系统下,调用此Crypto++库文件,也能正常运行。

    3. 重新生成在设置完成之后,右击“cryptlib”项目工程,选择“重新生成(E)”。

    这样,就可以在生成目录下找到生成的库文件了。

    我们可以选择不同的编译模式:Debug模式或是Release模式,选择不同的位数:Win32或是x64,来生成我们所需的库文件。
    4. 加载到程序中我们编译得到库文件“cryptlib.lib”库文件之后,然后,把程序所用加密算法的头文件如:AES加密算法所需的头文件“aes.h”、RSA加密算法所需的头文件“rsa.h”、“randpool.h”、“hex.h”、“files.h”;连同库文件“cryptlib.lib”文件一起拷贝到我们自己的程序目录下,并导入到程序中。这样,我们就可以直接调用Crypto++加密库的函数了。

    总结步骤不是很复杂,要注意一点的是,注意编译的时候,“运行库”对应的设置,这样在自己程序调用编译好的Crypto++库的时候,也要选择相应的模式,进行相应的“运行库”设置。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2018-11-06 22:04:18 奖励3点积分
  • ring0下通过OUT指令实现强制关机和重启

    背景通常情况下,我们可以通过调用 NtShutdownSystem 未导出的内核 API 函数,来在内核层下实现计算机的关机和重启等操作。 这种实现方式虽然在内核层下实现,但是相对来说,还是太过于表层了。
    本文要介绍一种在内核层下实现的强制关机以及重启的方法,即通过汇编 OUT 指令,控制端口输出,从而实现计算机的关机和重启。这种方式,较为低层,而且更难防御和监控。现在,我就把程序实现过程和原理,整理成文档,分享给大家。
    实现原理我们通过汇编 OUT 指令,控制端口的输出,从而实现控制计算机关机或者重启。
    对于实现关机操作,则可以向 1004H 端口,写入 2001H,汇编代码为:
    mov ax, 2001h mov dx, 1004h out dx,ax ret
    则,对应的机器码为:
    {0x66,0xB8,0x01,0x20,0x66,0xBA,0x04,0x10,0x66,0xEF,0xC3}
    对于实现重启操作,则可以向 64H 端口,写入 0FEH,汇编代码为:
    mov al, 0FEh out 64h, al ret
    则,对应的机器码为:
    {0xB0,0xFE,0xE6,0x64,0xC3}
    所以,我们可以通过申请一块非分页内存,写入 Shellcode 数据,并声明函数指针,调用 Shellcode 数据并执行,从而实现相应的关机或者重启操作。当然,我们也可以直接写成汇编代码,但是,要注意 64 位系统下汇编代码要保存为 .asm 汇编文件,添加到工程中。
    编码实现强制关机// 强制关机BOOLEAN ShutdownForce(){// {0x66, 0xB8, 0x01, 0x20, 0x66, 0xBA, 0x04, 0x10, 0x66, 0xEF, 0xC3} UCHAR ShellCode[11] = { 0x66, 0xB8, 0x01, 0x20, 0x66, 0xBA, 0x04, 0x10, 0x66, 0xEF, 0xC3 }; ULONG ulShellcodeSize = 11;#ifdef _WIN64 // 64 位 typedef VOID(__fastcall *typedef_SHUTDOWNFUNC)();#else // 32 位 typedef VOID(__stdcall *typedef_SHUTDOWNFUNC)();#endif // 申请内存 typedef_SHUTDOWNFUNC ShutdownFunc = (typedef_SHUTDOWNFUNC)ExAllocatePool(NonPagedPool, ulShellcodeSize); if (NULL == ShutdownFunc) { DbgPrint("ExAllocatePool Error!\n"); return FALSE; } // 写入数据 RtlCopyMemory(ShutdownFunc, ShellCode, ulShellcodeSize); // 执行Shellcode代码实现关机 ShutdownFunc(); // 释放内存 ExFreePool(ShutdownFunc); return TRUE;}
    强制重启// 强制重启BOOLEAN RebootForce(){ // {0xB0, 0xFE, 0xE6, 0x64, 0xC3} UCHAR ShellCode[11] = { 0xB0, 0xFE, 0xE6, 0x64, 0xC3 }; ULONG ulShellcodeSize = 5;#ifdef _WIN64 // 64 位 typedef VOID(__fastcall *typedef_REBOOTFUNC)();#else // 32 位 typedef VOID(__stdcall *typedef_REBOOTFUNC)();#endif // 申请内存 typedef_REBOOTFUNC RebootFunc = (typedef_REBOOTFUNC)ExAllocatePool(NonPagedPool, ulShellcodeSize); if (NULL == RebootFunc) { DbgPrint("ExAllocatePool Error!\n"); return FALSE; } // 写入数据 RtlCopyMemory(RebootFunc, ShellCode, ulShellcodeSize); // 执行Shellcode代码实现重启 RebootFunc(); // 释放内存 ExFreePool(RebootFunc); return TRUE;}
    程序测试在 Win7 32 位系统下,驱动程序正常运行;在 Win10 64 位系统下,驱动程序正常运行。
    总结对于这个层序,虽然编码较为简单,但是要想兼容 32 位和 64 位计算机,则需要特别注意一个问题:
    在声明函数指针的时候,对于 32 位函数,调用约定为 __stdcall;对于 64 位函数,调用约定为 __fastcall。
    所以,一定要注意不同系统位数下,函数的调用约定声明。否则,会出现错误。
    1 留言 2019-04-15 16:24:29
  • 《Windows黑客编程技术详解》勘误——13.3 文件管理之NTFS解析

    在内核篇第13章的第3小节“文件管理之NTFS解析”一文中,由于有网友测试反馈说该小结代码偶尔会出现定位不到存在的文件问题!经过我的排查,是由于配套代码中,多个Data Run的处理逻辑有问题!
    现在为了方便大家理解修改后的源代码,我在此补充下多个Data Run的处理说明。
    在NTFS文件系统中,如果数据很大的话,通常会使用Data Run来记录开辟的新空间,而且这些数据可能会不连续,所以就会出现多个Data Run的情况。
    Data Run的含义分析如下:

    Data Run的第一个字节分高4位和低4位。其中,高4位表示文件内容的起始簇号在Data Run List中占用的字节数。低4位表示文件内容簇数在Data Run List中占用的字节数。
    Data Run的第二个字节开始表示文件内容的簇数,接着表示文件内容的起始簇号。
    Data Run可以指示空间的大小以及偏移位置,例如上述中给的例子,起始簇号为:A2 59 00(10639616),数据大小为:C0 14(49172)。
    现在需要补充的知识点是针对多个Data Run的,如下所示!!!
    对于多个Data Run的情况,第一个Data Run的起始簇号是一个正整数,而第二个Data Run开始,起始簇号偏移是分正负的。可以根据起始簇号偏移的最高位是否来判断,若为1,则是负整数(补码表示);否则,是正整数。而且,从第二个Data Run开始,起始簇号偏移都是相对于上一个Data Run的起始簇号来说的。下面举个例子,方便大家理解。
    例如,有这么一个Data Run如下所示:
    31 01 FD 0A 28 21 01 AB FA 21 01 4A F5 21 01 91 C1 00我们可以看到上面一共有4个Data Run,分别如下:
    第1个Data Run

    31 01 FD 0A 28
    正整数:第一个Data Run的起始簇号都是正整数起始簇号:28 0A FD(2624253)

    第2个Data Run

    21 01 AB FA
    负整数:起始簇号偏移FA AB的最高位是1,所以是负整数(补码),所以FA AB(-1365)起始簇号:相对于上一个Data Run的偏移,所以为:2624253-1365=2622888

    第3个Data Run

    21 01 4A F5
    负整数:起始簇号偏移F5 4A的最高位是1,所以是负整数(补码),所以F5 4A(-2742)起始簇号:相对于上一个Data Run的偏移,所以为:2622888-2742=2620146

    第4个Data Run

    21 01 91 C1
    负整数:起始簇号偏移C1 91的最高位是1,所以是负整数(补码),所以C1 91(-15983)起始簇号:相对于上一个Data Run的偏移,所以为:2620146-15983=2604163

    配套代码修正对该小节配套代码中的HandleAttribute_A0函数和FileContentOffset函数的Data Run处理修改为上述补充的多个Data Run处理:
    计算Data Run的其实簇号代码修改为如下所示:
    if (0 == llClusterOffet){ // 第一个Data Run for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; }}else{ // 第二个及多个Data Run // 判断正负 if (0 != (0x80 & lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + bHi])) { // 负整数 for (DWORD i = bHi; i > 0; i--) { // 补码的原码=反码+1 liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | (BYTE)(~lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]); } liDataRunOffset.QuadPart = liDataRunOffset.QuadPart + 1; liDataRunOffset.QuadPart = 0 - liDataRunOffset.QuadPart; } else { // 正整数 for (DWORD i = bHi; i > 0; i--) { liDataRunOffset.QuadPart = liDataRunOffset.QuadPart << 8; liDataRunOffset.QuadPart = liDataRunOffset.QuadPart | lpBuffer[wAttributeOffset + wIndxOffset + dwCount + bLo + i]; } }}// 注意加上上一个Data Run的逻辑簇号(第二个Data Run可能是正整数、也可能是负整数(补码表示), 可以根据最高位是否为1来判断, 若为1, 则是负整数, 否则是正整数)liDataRunOffset.QuadPart = llClusterOffet + liDataRunOffset.QuadPart;llClusterOffet = liDataRunOffset.QuadPart;
    修改后的源码,重新上传在下面的附件中了!!!若对书中内容有疑惑或者发现错误,可以直接戳下面的勘误收集链接哦
    https://www.write-bug.com/article/1966.html
    1 留言 2019-01-21 13:13:32
  • 基于WinPcap实现的UDP发包程序 精华

    背景一天,一位同学打电话给我说,让我帮忙开发一个基于WinPcap工具的UDP发包工具,还特地叮嘱是基于WinPcap,不要原始套接字Raw Socket。而且,时间只有一个白天,它晚上就要,而打电话给我的时候,已经临近中午了。我一听,同学一场,那就举手之劳吧。
    之前,自己就是用WinPcap开发过一些小程序,例如网卡遍历程序、Arp欺骗攻击程序等,所以,对WinPcap还算是熟悉。做这样的UDP发包程序,应该倒也不是那没事。
    现在,为了配合这篇文章的讲解,我特地对这个程序重新开发。把程序的实现原理和过程整理成文档,分享给大家。
    使用VS2013配置WinPcap开发环境程序是使用VS2013开发的,程序中要使用WinPcap工具提供的功能函数的话,就需要将对VS2013进行配置,将WinPcap库导入到程序中,现在,就先介绍WinPcap环境的配置过程:
    1.下载并安装WinPcap运行库http://www.winpcap.org/install/default.htm 。一些捕包软件会捆绑安装WinPcap,MentoHust也会附带WinPcap,这种情况下一般可以跳过此步。
    2.下载WinPcap开发包http://www.winpcap.org/devel.htm ,解压到纯英文路径。
    3.打开VS工程项目,在VS工程项目的 属性 —> VC++目录 中,包含目录 选项添加WpdPack\Include 目录,在 库目录 选项中添加 WpdPack\Lib 目录。
    4.在 属性 —> C/C++ —> 预处理器 中,添加 WPCAP 和 HAVE_REMOTE 这两个宏定义。
    5.在VS工程项目的 属性 —> 链接器 —> 输入 中,添加 wpcap.lib 和 ws2_32.lib 两个库。
    这样,就可以将WinPcap所需的库文件包含到工程项目中了。接下来,我们就开始讲解UDP发包程序的实现过程。
    实现过程1. 获取网卡设备列表首先,我们调用WinPcap函数pcap_findalldevs_ex获取设备列表信息,函数会将计算机上所有网卡设备信息返回到指向pcap_if_t结构体指针里。我们只需要对这个结构体指针进行遍历,就可以获取每个设备的详细信息。
    // 获取网卡设备列表 if (-1 == pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, szErr)) { ShowError("pcap_findalldevs_ex"); return; }
    程序将每个设备的信息显示在界面上,供用户选中使用哪个网卡进行操作。
    2. 设置网卡信息并打开网卡我们使用 pcap_open 函数来设置并打开网卡,函数中第 1 个参数stAdapterInfo. szAdapterName,表示网卡的名称;第 2 个参数 65535,表示设置保留数据包的长度,65535即每个数据包的前65535字节长度的数据被保留在缓冲区中;第 3 个参数 PCAP_OPENFLAG_DATATX_UDP 表示使用UDP协议来处理数据传输;第 4 个参数 1 ,表示以毫秒为单位的读取超时时间。读取超时用来处理,捕获一个数据包后,读操作并不必需要立即返回的情况。但这可能等待一些时间以允许捕获更多的数据包,这样用户层的一次读操作就可从操作系统的内核中读取更多的数据包。第 5 个参数为 NULL;第 6 个参数 errbuf,可以获取返回的出错信息。
    // 打开网卡 m_adhandle = pcap_open(stAdapterInfo.szAdapterName, 65535, PCAP_OPENFLAG_DATATX_UDP, 1, NULL, errbuf); if (NULL == m_adhandle) { ShowError("pcap_open"); return; }
    3. 构造UDP数据包我们根据源MAC地址、目的MAC地址、源IP地址、目的IP地址、源端口、目的端口、数据内容以及数据内容长度,构造UDP数据包。具体构造过程如下:

    首先,构造以太网帧头。
    memcpy((void*)FinalPacket, (void*)DestinationMAC, 6); memcpy((void*)(FinalPacket + 6), (void*)SourceMAC, 6); USHORT TmpType = 8; memcpy((void*)(FinalPacket + 12), (void*)&TmpType, 2);
    然后,构造IP头。
    memcpy((void*)(FinalPacket + 14), (void*)"\x45", 1); memcpy((void*)(FinalPacket + 15), (void*)"\x00", 1); TmpType = htons(TotalLen); memcpy((void*)(FinalPacket + 16), (void*)&TmpType, 2); TmpType = htons(0x1337); memcpy((void*)(FinalPacket + 18), (void*)&TmpType, 2); memcpy((void*)(FinalPacket + 20), (void*)"\x00", 1); memcpy((void*)(FinalPacket + 21), (void*)"\x00", 1); memcpy((void*)(FinalPacket + 22), (void*)"\x80", 1); memcpy((void*)(FinalPacket + 23), (void*)"\x11", 1); memcpy((void*)(FinalPacket + 24), (void*)"\x00\x00", 2); memcpy((void*)(FinalPacket + 26), (void*)&SourceIP, 4); memcpy((void*)(FinalPacket + 30), (void*)&DestIP, 4);
    接着,构造UDP头。
    TmpType = htons(SourcePort); memcpy((void*)(FinalPacket + 34), (void*)&TmpType, 2); TmpType = htons(DestinationPort); memcpy((void*)(FinalPacket + 36), (void*)&TmpType, 2); USHORT UDPTotalLen = htons(UserDataLen + 8); memcpy((void*)(FinalPacket + 38), (void*)&UDPTotalLen, 2); memcpy((void*)(FinalPacket + 42), (void*)UserData, UserDataLen);

    要注意的是,IP校验和以及UDP的校验和计算。这样,我们就可以成功构造UDP的数据包,接下来,就可以对数据包进行发送。
    4. 发送UDP数据包我们使用 pcap_sendpacket 函数发送单个数据包,第 1 个参数表示打开网卡的时候获取的句柄;第 2 个参数就是发送的数据内容;第 3 个参数表示发送数据内容的长度。注意,缓冲数据将直接发送到网络,而不会进行任何加工和处理。这就意味着应用程序需要创建一个正确的协议首部,来使这个数据包更有意义。
    // 发送数据包 if (0 != pcap_sendpacket(m_adhandle, FinalPacket, (UserDataLen + 42))) { char *szErr = pcap_geterr(m_adhandle); ShowError(szErr); return; }
    经过上面 4 步操作,便可以实现使用 WinPcap 发送 UDP 数据包了。要注意IP校验和以及UDP校验和的计算,这两个的值注意不要算错。
    程序测试我们以管理员权限运行程序,并对一个UDP程序发包,观察UDP程序能否接收到我们发包程序发送的UDP数据包。其中,UDP程序使用的是《Socket通信之UDP通信小程序》这篇文章中实现的UDP程序。
    经过测试结果,UDP程序成功接收到UDP数据包。

    总结这个程序是基于WinPcap实现的,所以,可能有很多人之前还没有接触过WinPcap方面的知识,所以,一下子使用和理解起来就比较困难。关键是耐下心来,把程序中调用到的不理解的WinPcap函数,在网上查找说明,对函数理解清晰。
    其中,要注意的是,使用WinPcap工具,需要有管理员权限。
    参考参考自《Windows黑客编程技术详解》一书
    3 留言 2018-11-28 11:22:48 奖励40点积分
  • 大数据 Spark平台5-3、spark-sql

    前文链接:https://write-bug.com/article/2361.html
    Spark-Sql官网:http://spark.apache.org/docs/latest/sql-getting-started.html
    这里对Spark家族进一步介绍,偏入门实践,优化概念会少一些。
    我们在学习Hive时,本质上是把sql语句转换成MR job,节省工作时间,提升效率,其在存储数据时,分为这几个层次:table / partition / bucket / hdfs
    spark sql同样也处理结构化数据,把数据源传来的数据用表格的形式解析并维护起来,与此同时也可和Hive结合使用(数据存储在Hive中)
    在spark streaming中我们通常开发一个模板——Dstream, SparkStreamingContext
    spark SQL同样也有类似的概念——DataFrame, 当成一个table(关系型表格)

    外部数据源(SQLContext):HDFS、网络接口、Mysql等
    Hive数据源(HiveContext):Hive

    两者关系:HiveContext继承自SQLContext,HiveContext只能支持HQL,SQLContext支持语法更多
    DataFrame(由很多RDD组成)让数据处理更为简单,是一个分布式的Table

    与RDD区别:传统RDD以行为单位读数据,DataFrame基于列的内部优化
    与RDD相同点:都具备懒惰机制(基于RDD的抽象)


    Spark SQL处理核心:Catalyst工作流程(本质:把sql、dataframe相结合,以树tree的形式来存储、优化)

    把sql语句和Dataframe输入形成未解析的逻辑计划,加上元数据Catalog的支持,形成逻辑计划,再经过优化,形成物理计划,最后再通过代价模型转化成可执行代码。
    优化点

    基于规则

    一种经验式、启发式优化思路(如sql语句优化)join算子——两张表做join
    外排
    大循环外排:A、B,两张表都很大,O(M*N)——不用游标式外排:归并排序(指针滑动比大小)
    内排:小表放内存,大表做遍历(hive中的mapside join)

    基于代价

    评估每种策略选择的代价,根据代价估算,确定代价最小的方案代价估算模型——调整join的顺序,减少中间shuffle数据的规模

    catalyst工作流程

    parser:针对sql解析
    词法分析:讲输入的sql语句串解析为一个一个的token
    语法分析:再词法分析基础上,将单词序列组合成各类语法短语,组成各个LogicPlan

    SELECT sum(v) FROM( SELECT score.id, 100+80+score.math_score+score.english_score AS vFROM people JOIN scoreWHERE people.id=score.id AND people.age>10 ) a

    analyzer:借助元数据(catalog)解析根据元数据表解析为包含必要列的表,并且相应字段解析为相应的数据类型,相应的计算逻辑解析为对应的函数


    optimizer:基于规则的优化策略经典规则:谓词下推(Predicate Pushdown)、常量累加(Constant Folding)和列值裁剪(Column Pruning)谓词下推:把过滤条件放在join之前执行

    常量累加(180)、列值裁剪(提前过滤掉不用的列值字段):


    物理计划:基于代价的优化策略用物理操作算子产生一个或者多个物理计划。然后用cost模型选择一个物理计划。目前基于cost-based的优化仅仅用 于选择join算法:对已知的很小的relations,sparksql会选择使用spark的提供的点对点的广播功能实现Broadcast join
    执行计划查看方式:

    Spark网页sql
    sql语句后面追加 . queryExecution方法查看

    官方:catalyst优化引擎,执行时间减少75%
    内存管理:Tungsten 内存管理器—— off-heap(堆外内存)

    本质:突破JVM内存管理限制,分配堆外内存(GC、与磁盘做交换dump)
    JVM:GC带来时间开销,可能出现“假死”情况

    实践
    1、基本demo:
    读数据:
    1)从hdfs的原始text中读数据:sqlTest
    //建立学生表Schemeval StudentSchema: StructType = StructType(mutable.ArraySeq( StructField("Sno", StringType, nullable = false), StructField("Sname", StringType, nullable = false), StructField("Ssex", StringType, nullable = false), StructField("Sbirthday", StringType, nullable = true), StructField("SClass", StringType, nullable = true) ))val sparkConf = new SparkConf().setMaster("local[2]").setAppName("sqltest") val sc = new SparkContext(sparkConf)//sqlContext入口 val sqlContext = new org.apache.spark.sql.SQLContext(sc)//RDD导入并解析数据 val StudentData = sc.textFile("hdfs://master:9000/sql_stu.data").map{ lines => val line = lines.split(",") Row(line(0),line(1),line(2),line(3),line(4)) }//把RDD数据和Schema通过sqlContext维护起来形成DataFrame val StudentTable = sqlContext.createDataFrame(StudentData, StudentSchema) StudentTable.registerTempTable("Student")//表名//sql语句使用 sqlContext.sql("SELECT Sname, Ssex, SClass FROM Student").show()2)从hdfs的原始text中读数据(json串):sqlJsonText
    val personInfo = sqlContext.read.json("hdfs://master:9000/person_info.json")//json串中的数据都是维护好的数据,不需要schema personInfo.registerTempTable("personInfo") sqlContext.sql("SELECT id, name, age FROM personInfo").show() println(personInfo.schema)3)从hive中读数据:sqlHiveTest
    启动mysql:]# systemctl start mariadb(hive中元数据)
    终端submit需jar包:lib/mysql-connector-java-5.1.41-bin.jar
    val hiveContext = new HiveContext(sc) hiveContext.table("movie_table").registerTempTable("movie_table")//可对现有表直接进行操作 hiveContext.sql("SELECT movieid, title FROM movie_table").show()2、UDF相关操作:
    1)udf:单条记录处理(map):sqlUdf
    sqlContext.udf.register("strlen", (input: String) => input.length)//函数名字注册,及简单实现功能 val personInfo = sqlContext.read.json("hdfs://master:9000/person_info.json") personInfo.registerTempTable("personInfo") sqlContext.sql("SELECT id, name, strlen(name), age FROM personInfo").show()//字段套用函数2)udaf:聚合场景(groupby)
    例子:每一个打分背后,有多少人参与
    class WordcountUdaf extends UserDefinedAggregateFunction { // 该方法指定具体输入数据的类型 override def inputSchema: StructType = StructType(Array(StructField("input", StringType, true))) //在进行聚合操作的时候所要处理的数据的结果的类型 override def bufferSchema: StructType = StructType(Array(StructField("count", IntegerType, true))) //指定UDAF函数计算后返回的结果类型 override def dataType: DataType = IntegerType // 确保一致性 一般用true override def deterministic: Boolean = true //在Aggregate之前每组数据的初始化结果 override def initialize(buffer: MutableAggregationBuffer): Unit = {buffer(0) =0} // 在进行聚合的时候,每当有新的值进来,对分组后的聚合如何进行计算 // 本地的聚合操作,相当于Hadoop MapReduce模型中的Combiner override def update(buffer: MutableAggregationBuffer, input: Row): Unit = { buffer(0) = buffer.getAs[Int](0) + 1 } //最后在分布式节点进行Local Reduce完成后需要进行全局级别的Merge操作 override def merge(buffer1: MutableAggregationBuffer, buffer2: Row): Unit = { buffer1(0) = buffer1.getAs[Int](0) + buffer2.getAs[Int](0) } //返回UDAF最后的计算结果 override def evaluate(buffer: Row): Any = buffer.getAs[Int](0)}val hiveContext = new HiveContext(sc) hiveContext.table("rating_table").registerTempTable("rating_table") hiveContext.udf.register("strlen", (input: String) => input.length) hiveContext.udf.register("wordCount", new WordcountUdaf)//注册 hiveContext.sql("select rating, wordCount(rating) as count, strlen(rating) as length" + " from rating_table group by rating").show()//这里两函数可做个对比3、终端
    /usr/local/src/spark-2.0.2-bin-hadoop2.6/bin/spark-sql \--master local[2] \--jars /usr/local/src/spark-2.0.2-bin-hadoop2.6/lib/mysql-connector-java-5.1.41-bin.jar测试与hive数据:
    select rating, count(*) from rating_table_ex group by rating limit 100;4、streaming+sql:sqlAndStreamingWC
    nc -l 9999//单例模式object SQLContextSingleton { @transient private var instance: SQLContext = _ def getInstance(sparkContext: SparkContext): SQLContext = { if (instance == null) { instance = new SQLContext(sparkContext) } instance }}if (args.length < 2) { System.err.println("Usage: NetworkWordCount <hostname> <port>") System.exit(1) } val sparkConf = new SparkConf().setMaster("local[2]").setAppName("sqlAndStreamingWC") val sc = new SparkContext(sparkConf) val ssc = new StreamingContext(sc, Seconds(30)) val lines = ssc.socketTextStream(args(0), args(1).toInt, StorageLevel.MEMORY_AND_DISK_SER) val words = lines.flatMap(_.split(" ")) words.foreachRDD((rdd: RDD[String], time: Time) => { val sqlContext = SQLContextSingleton.getInstance(rdd.sparkContext) import sqlContext.implicits._//每条数据都用一个对象操作内存,复用性 val wordsDataFrame = rdd.map(w => Record(w)).toDF() wordsDataFrame.registerTempTable("words") val wordCountsDataFrame = sqlContext.sql("select word, count(*) as total from words group by word") println(s"========= $time =========") wordCountsDataFrame.show() }) ssc.start() ssc.awaitTermination()5、streaming+sql + hbase:streamSqlHbase
    nc -l 9999object HbaseHandler { def insert(row: String, column: String, value: String) { // Hbase配置 val tableName = "sparkstream_kafkahbase_table" // 定义表名 val hbaseConf = HBaseConfiguration.create() hbaseConf.set("hbase.zookeeper.quorum", "master,slave1,slave2") hbaseConf.set("hbase.zookeeper.property.clientPort", "2181") hbaseConf.set("hbase.defaults.for.version.skip", "true") val hTable = new HTable(hbaseConf, tableName) val thePut = new Put(row.getBytes) thePut.add("info".getBytes,column.getBytes,value.getBytes) hTable.setAutoFlush(false, false) // 写入数据缓存 hTable.setWriteBufferSize(3*1024*1024) hTable.put(thePut) // 提交 hTable.flushCommits() }}//从套接字中读取到的信息遍历解析lines.foreachRDD((rdd: RDD[String], time: Time) => { val sqlContext = SQLContextSingleton.getInstance(rdd.sparkContext) import sqlContext.implicits._ val wordsDataFrame = rdd.map{ x=> (x.split(" ")(0),x.split(" ")(1),x.split(" ")(2)) }.map(w => (w._1, w._2, w._3)).toDF("key", "col", "val") wordsDataFrame.registerTempTable("words") val wordCountsDataFrame = sqlContext.sql("select key, col, val from words") println(s"========= $time =========") wordCountsDataFrame.show() //对dataframe行遍历插入 wordCountsDataFrame.foreach(x => HbaseHandler.insert( x.getAs[String]("key"), x.getAs[String]("col"), x.getAs[String]("val"))) })
    2 留言 2019-04-10 15:19:41 奖励10点积分
显示 45 到 60 ,共 15 条
eject