首页 文章 精选 留言 我的

精选列表

搜索[编写],共10005篇文章
优秀的个人博客,低调大师

使用ePump框架编写TCP服务器

基于非阻塞、多线程、事件驱动模型的 ePump 框架可以很好地解决复杂的线程调度、高效通信等问题,使用 ePump 框架可快速开发各类通信服务器系统,像常见的HTTP Web服务器、RTMP流媒体服务器、及时通信消息系统等等。 一. 使用ePump框架开发前需下载安装的库 在使用ePump框架编程前,首先需要从GitHub下载并安装C语言基础库adif 数据结构和基础算法库和ePump框架,ePump框架依赖于adif基础库,adif 基础库 和 ePump 框架都是标准C语言开发,并以库的形式集成到应用程序中。下载这两个开源系统的代码到本地,make && make install 后,编译成功的动态库和静态库缺省地被安装到 /usr/local/lib 目录下,头文件则安装到 /usr/local/include/adif 和 /usr/local/include 中。 下面讲解如何使用ePump框架来开发一个echo功能的TCP服务器程序。 使用这两个库编程时,需要包含adif基础库和ePump框架的头文件: #include "adifall.ext" #include "epump.h" #include <signal.h> 其中adifall.ext文件包含了adif库中所有基础数据结构和算法模块的头文件,具体功能可以参考开源项目adif 数据结构和基础算法库。epump.h是调用ePump框架功能模块的头文件。由于需要处理信号,包含了signal.h文件。 二. 开发高性能通信服务器基本需求 大家都知道,创建TCP监听服务器时,最基本的通信开发启蒙知识三部曲,首先要创建socket_t文件描述符,绑定本地IP地址和端口,在指定端口上启动监听,等待客户端发起TCP连接请求。但是对于高级程序员或商业级服务器需求的系统来说,高性能是必须的终极需求,开发人员还需要严肃地考虑如下问题: TCP服务器系统既要支持IPv4地址,也要支持IPv6地址; 如何采用多线程或多进程来处理每一个连接请求; 短时间内产生大量的TCP连接请求时,如何在多个线程或多个进程间负载均衡; 由于线程或进程总数有限(小于1024),单台机器处理几十万并发的TCP连接时,如何采用多路复用技术解决大并发I/O; 这些问题直接考验了商业级通信服务器系统其性能的高低、吞吐能力的大小、CPU处理能力的高效运用等等,能解决好这些问题,无疑是一个能经受实战的好系统,ePump框架天生就是解决这些问题的好手。 三. 创建框架实例 使用ePump框架,先调用API接口,创建ePump框架实例, epcore_t * pcore = NULL; void * mlisten = NULL; int listenport = 8080; gpcore = pcore = epcore_new(65536, 1); 其中第一个参数是服务器系统能同时允许打开的文件描述符的最大数量,也就是这款TCP服务器能支撑的最大并发数量,第二个参数一般设置为1,指的是创建iodev_t等基础设备对象时,尽量将其派送到当前工作线程。 四. 启动TCP监听服务 使用ePump框架,启动TCP监听服务很简单,调用eptcp_mlisten接口即可, mlisten = eptcp_mlisten(pcore, NULL, listenport, NULL, echo_pump, pcore); if (!mlisten) goto exit; printf("EchoSrv TCP Port: %d being listened\n", listenport); 按照头文件中的描述,该接口函数定义如下: /* Note: automatically detect if Linux kernel supported REUSEPORT. if supported, create listen socket for every current running epump threads and future-started epump threads. if not, create only one listen socket for all epump threads to bind. */ void * eptcp_mlisten (void * vpcore, char * localip, int port, void * para, IOHandler * cb, void * cbpara); 最新的Linux操作系统对于通信编程做了很多优化,其中对于内核对象Socket使用REUSEPORT选项,来解决端口复用问题,这样使得每一个线程或进程都可以监听同一个端口,并接受新的连接请求。那么大量的客户端同时对监听端口发起TCP三路握手,想建立到达服务器的TCP连接时,这些连接到底交给哪一个启动了监听服务的线程或进程?这个问题大家自己做功课,这里不赘述。 ePump框架中采用 epoll / select等多路复用接口来监听连接到来和读写事件,监听设备和定时器并产生事件的线程是epump线程,处理事件的线程是worker线程。eptcp_mlisten自动地为每一个epump线程创建listen socket(支持REUSEPORT时),或创建一个listen socket但绑定到每一个epump线程中。这样大量的连接请求到来时,将会由epump线程处理其负载均衡。 eptcp_mlisten函数的第一个参数是ePump框架实例;第二个参数为绑定的本机IP地址,如果绑定所有本机IP地址,该值为NULL;第三个参数为监听的端口号;第四个参数是设置当前正在创建的监听设备的绑定参数,一般跟当前监听设备iodev_t有关的实例对象;第五个参数是设置当前正在创建的监听设备iodev_t对象当有连接请求过来时的回调函数;第六个参数是传递给回调函数的参数变量。 作者在程序中习惯为所有的事件回调处理设置为一个统一的回调函数echo_pump,当然大家可以根据自己的爱好和习惯,为每个iodev_t对象的读写事件设置不同的回调函数。 这样,启动这个TCP服务器监听服务时,前面提到的各种商业TCP服务器需面对的问题,这里都解决了。 五. 定时器的运用 这个sample程序中,给大家示范了定时器的用法。使用定时器可以定期做一些检查或校验工作,譬如一个TCP连接长时间没有数据往来,通过定时器机制来关闭着这些不活跃的TCP连接。 iotimer_start(pcore, 90*1000, 1001, NULL, echo_pump, pcore); 这是启动一个90秒后发送超时TIMEOUT事件的定时器,超时事件将由echo_pump函数来处理。本例的回调函数中没有处理不活跃TCP连接的代码,大家感兴趣可自行添加。 六. 启动ePump线程组 创建了TCP监听服务后,需要启动ePump框架的两类线程组:epump线程组 和 worker线程组,代码如下: cpunum = get_cpu_num(); epumpnum = cpunum * 0.2; if (epumpnum < 3) epumpnum = 3; workernum = cpunum - epumpnum; if (workernum < 3) workernum = 3; /* start 3 worker threads */ epcore_start_worker(pcore, workernum); /* start 3 epump threads */ epcore_start_epump(pcore, epumpnum - 1); /* main thread executing the epump_main_proc as an epump thread */ epump_main_start(pcore, 0); epcore_clean(pcore); printf("main thread exited\n"); return 0; exit: epcore_clean(pcore); printf("main thread exception exited\n"); return -1; 七. ePump框架的回调函数 以上介绍的是主程序,下面需要介绍的回调函数echo_pump的实现. 回调函数的原型定义如下: typedef int IOHandler (void * vmgmt, void * pobj, int event, int fdtype); 第一个参数是设置回调函数时给定的参数,第二个是当前产生事件的对象,或者是iodev_t对象,或者是iotimer_t对象,第三个参数是event事件类型,第四个参数是文件描述符类型。 其中的event事件类型如下: /* define the event type, including getting connected, connection accepted, readable, * writable, timeout. the working threads will be driven by these events */ #define IOE_CONNECTED 1 #define IOE_CONNFAIL 2 #define IOE_ACCEPT 3 #define IOE_READ 4 #define IOE_WRITE 5 #define IOE_INVALID_DEV 6 #define IOE_TIMEOUT 100 #define IOE_DNS_RECV 200 #define IOE_USER_DEFINED 10000 其中的文件描述符类型fdtype预定义值共有: /* the definition of FD type in the EventPump device */ #define FDT_LISTEN 0x01 #define FDT_CONNECTED 0x02 #define FDT_ACCEPTED 0x04 #define FDT_UDPSRV 0x08 #define FDT_UDPCLI 0x10 #define FDT_USOCK_LISTEN 0x20 #define FDT_USOCK_CONNECTED 0x40 #define FDT_USOCK_ACCEPTED 0x80 #define FDT_RAWSOCK 0x100 #define FDT_FILEDEV 0x200 #define FDT_TIMER 0x10000 #define FDT_USERCMD 0x20000 #define FDT_LINGER_CLOSE 0x40000 #define FDT_STDIN 0x100000 #define FDT_STDOUT 0x200000 八. 如何接受客户端连接请求 当被监听端口8080上收到一个TCP连接请求时,echo_pump函数会被回调,回调参数中event为IOE_ACCEPT,fdtype为FDT_LISTEN,其中pobj就是监听设备对象。 switch (event) { case IOE_ACCEPT: if (fdtype != FDT_LISTEN) return -1; while (1) { pdev = eptcp_accept(iodev_epcore(vobj), vobj, NULL, &ret, echo_pump, pcore, BIND_ONE_EPUMP); if (!pdev) break; printf("\nThreadID=%lu, ListenFD=%d EPumpID=%lu WorkerID=%lu " " ==> Accept NewFD=%d EPumpID=%lu\n", get_threadid(), iodev_fd(vobj), epumpid(iodev_epump(vobj)), workerid(worker_thread_self(pcore)), iodev_fd(pdev), epumpid(iodev_epump(pdev))); } break; 这里使用了一个while循环来调用eptcp_accept函数,目的是解决多个TCP连接同时到来,ePump框架使用一个事件通知驱动回调函数去处理和执行的情况,不使用循环处理,就会漏掉某些客户的TCP连接请求。 函数 eptcp_accept 接受TCP连接请求,并创建新连接对应的iodev_t设备对象pdev,设置该对象在数据可读时的回调函数,这个函数会自动处理多线程之间的连接设备对象的负载均衡。函数成功执行完后的结果是一个新的客户端TCP连接建立起来了,针对该新连接进行数据读取操作的回调函数也都设置了。 eptcp_accept函数的原型如下: void * eptcp_accept (void * vpcore, void * vld, void * para, int * retval, IOHandler * cb, void * cbpara, int bindtype); 第一个参数为ePump框架实例,第二个参数是监听设备iodev_t对象,由回调函数携带进来,第三个参数是新创建的客户端TCP连接设备iodev_t对象的内置参数,第四个参数为返回值,大于等于0表示连接建立成功,小于0失败,第五个和第六个参数为新创建的连接对象的回调函数,第七个参数是设置绑定epump线程的指令类型,共有如下几种: /* bind type specifying how iodev_t devices are bound to the underlying epump thread */ #define BIND_NONE 0 #define BIND_ONE_EPUMP 1 #define BIND_GIVEN_EPUMP 2 #define BIND_ALL_EPUMP 3 #define BIND_NEW_FOR_EPUMP 4 #define BIND_CURRENT_EPUMP 5 绑定epump线程的指令类型共有6个,其含义如下: BIND_NONE是初始值,不绑定任何epump线程; BIND_ONE_EPUMP是从当前epump线程中找一个负载最低的线程来绑定; BIND_GIVEN_EPUMP是指定一个epump线程来建立绑定; BIND_ALL_EPUMP是绑定所有的epump线程。这中情况一般是在监听设备对象创建后,一般在Linux内核版本低于3.9版本情况下,即不支持REUSEPORT功能时,使用这个类型。 BIND_NEW_FOR_EPUMP一般用于ePump框架内部,应用程序不建议使用。 BIND_CURRENT_EPUMP是绑定产生当前连接事件的epump线程。系统内部和操作系统内核对负载会实现均衡分配,一般建议应用开发时使用这个类型。 一旦绑定了epump线程,就可能立即产生可读事件,并驱动回调函数来处理。如果新的的pdev对象是由另外一个工作线程来处理时,上述这个例子中就可能出现打印语句还没结束,该新连接设备对象可读事件的回调函数就已经执行了。在商业级系统开发过程中,调用本函数接受客户端新连接并创建新的设备对象pdev后,需要做很多跟连接设备对象相关联的数据结构的初始化工作,在这些初始化操作完成之后,再调用 iodev_bind_epump 函数来绑定epump线程,所以,这种情况下接受新连接时,一般不设置绑定关系,而是将第七个参数设置为 BIND_NONE。 pdev = eptcp_accept(iodev_epcore(vobj), vobj, NULL, &ret, echo_pump, pcore, BIND_NONE); /* do some initialization of related objects, examples as following */ /* pcon = http_con_fetch(mgmt); pcon->pdev = pdev; iodev_para_set(pdev, (void *)pcon->conid); pcon->hl = hl; pcon->casetype = HTTP_SERVER; pcon->reqdiag = hl->reqdiag; pcon->reqdiagobj = hl->reqdiagobj; pcon->ssl_link = hl->ssl_link; str_cpy(pcon->srcip, iodev_rip(pcon->pdev)); str_cpy(pcon->dstip, iodev_lip(pcon->pdev)); pcon->srcport = iodev_rport(pcon->pdev); pcon->dstport = iodev_lport(pcon->pdev); pcon->createtime = time(&pcon->stamp); pcon->transbgn = pcon->stamp;*/ iodev_bind_epump(pdev, BIND_CURRENT_EPUMP, NULL); 九. 读取客户端的TCP请求数据 建立好TCP连接之后,客户端会发送数据到服务器,ePump框架中对所有socket文件描述符设置成了非阻塞模式,数据到达本机时,内核会产生可读事件,由ePump框架驱动回调函数来处理数据读操作。 case IOE_READ: ret = tcp_nb_recv(iodev_fd(vobj), rcvbuf, sizeof(rcvbuf), &num); if (ret < 0) { printf("Client %s:%d close the connection while receiving, epump: %lu\n", iodev_rip(vobj), iodev_rport(vobj), epumpid(iodev_epump(vobj)) ); iodev_close(vobj); return -100; } ret = tcp_nb_send(iodev_fd(vobj), rcvbuf, num, &sndnum); if (ret < 0) { printf("Client %s:%d close the connection while sending, epump: %lu\n", iodev_rip(vobj), iodev_rport(vobj), epumpid(iodev_epump(vobj))); iodev_close(vobj); return -100; } break; 采用非阻塞模式的读数据函数,读取客户端请求内容。这个读函数 tcp_nb_recv 是在adif基础库中实现的,调用系统调用read并一直读到出现 EAGAIN 错误为止,表示此次可读事件的所有数据都被读完。开发者需要注意的是,在回调函数中处理ePump框架的可读事件时,一定要将所有的位于内核缓冲区中的数据读取完,不建议读一部分数据、留一部分数据。 由于本sample程序实现的是echo回弹功能,读取了客户端多少数据,就返回客户端多少数据。所以立即使用 tcp_nb_send 函数发送这些数据到客户端。 十. 定时器超时事件回调处理 本例中示范了定时器的启动和超时处理,当定时器给定的时间逝去后,会产生TIMEOUT事件,并驱动回调函数来处理。ePump框架的定时器实例对象存活周期仅仅是在创建定时器到超时处理完成这段时间,即ePump框架的定时器是一次性的,超时处理完后,系统会自动销毁该定时器对象。对于循环定时器,需要在处理超时事件时,重新启动新的定时器实例。 case IOE_TIMEOUT: cmdid = iotimer_cmdid(vobj); if (cmdid == 1001) { printf("\nThreadID=%lu IOTimerID=%lu EPumpID=%lu timeout, curtick=%lu\n", get_threadid(), iotimer_id(vobj), epumpid(iotimer_epump(vobj)), time(0)); epcore_print(pcore); iotimer_start(pcore, 90*1000, 1001, NULL, echo_pump, pcore); } break; 定时器的用例非常广泛,开发人员可以根据实际需求来使用该功能。 十一. 完整的具有echo功能的TCP服务器代码 以上详细介绍了如何运用ePump框架实现一个完整的具有echo回弹功能的TCP服务器,代码详细如下: /* * Copyright (c) 2003-2021 Ke Hengzhong <kehengzhong@hotmail.com> * All rights reserved. */ #include "adifall.ext" #include <signal.h> #include "epump.h" epcore_t * gpcore = NULL; int echo_pump (void * vpcore, void * vobj, int event, int fdtype); static void signal_handler(int sig) { switch(sig) { case SIGHUP: printf("hangup signal catched\n"); break; case SIGTERM: case SIGKILL: case SIGINT: printf("terminate signal catched, now exiting...\n"); epcore_stop_epump(gpcore); epcore_stop_worker(gpcore); usleep(1000); break; } } int main (int argc, char ** argv) { epcore_t * pcore = NULL; void * mlisten = NULL; int listenport = 8080; signal(SIGCHLD, SIG_IGN); /* ignore child */ signal(SIGTSTP, SIG_IGN); /* ignore tty signals */ signal(SIGTTOU, SIG_IGN); signal(SIGPIPE, SIG_IGN); signal(SIGTTIN, SIG_IGN); signal(SIGHUP, signal_handler); /* catch hangup signal */ signal(SIGTERM, signal_handler); /* catch kill signal */ signal(SIGINT, signal_handler); /* catch SIGINT signal */ gpcore = pcore = epcore_new(65536, 1); /* do some initialization */ mlisten = eptcp_mlisten(pcore, NULL, listenport, NULL, echo_pump, pcore); if (!mlisten) goto exit; printf("EchoSrv TCP Port: %d being listened\n\n", listenport); iotimer_start(pcore, 90*1000, 1001, NULL, echo_pump, pcore); /* start 2 worker threads */ epcore_start_worker(pcore, 2); /* start 1 epump threads */ epcore_start_epump(pcore, 1); /* main thread executing the epump_main_proc as an epump thread */ epump_main_start(pcore, 0); epcore_clean(pcore); printf("main thread exited\n"); return 0; exit: epcore_clean(pcore); printf("main thread exception exited\n"); return -1; } int echo_pump (void * vpcore, void * vobj, int event, int fdtype) { epcore_t * pcore = (epcore_t *)vpcore; iodev_t * pdev = NULL; int cmdid; int ret = 0, sndnum = 0; char rcvbuf[2048]; int num = 0; switch (event) { case IOE_ACCEPT: if (fdtype != FDT_LISTEN) return -1; while (1) { pdev = eptcp_accept(iodev_epcore(vobj), vobj, NULL, &ret, echo_pump, pcore, BIND_ONE_EPUMP); if (!pdev) break; printf("\nThreadID=%lu, ListenFD=%d EPumpID=%lu WorkerID=%lu " " ==> Accept NewFD=%d EPumpID=%lu\n", get_threadid(), iodev_fd(vobj), epumpid(iodev_epump(vobj)), workerid(worker_thread_self(pcore)), iodev_fd(pdev), epumpid(iodev_epump(pdev))); } break; case IOE_READ: ret = tcp_nb_recv(iodev_fd(vobj), rcvbuf, sizeof(rcvbuf), &num); if (ret < 0) { printf("Client %s:%d close the connection while receiving, epump: %lu\n", iodev_rip(vobj), iodev_rport(vobj), epumpid(iodev_epump(vobj)) ); iodev_close(vobj); return -100; } printf("\nThreadID=%lu FD=%d EPumpID=%lu WorkerID=%lu Recv %d bytes from %s:%d\n", get_threadid(), iodev_fd(vobj), epumpid(iodev_epump(vobj)), workerid(worker_thread_self(pcore)), num, iodev_rip(vobj), iodev_rport(vobj)); printOctet(stderr, rcvbuf, 0, num, 2); ret = tcp_nb_send(iodev_fd(vobj), rcvbuf, num, &sndnum); if (ret < 0) { printf("Client %s:%d close the connection while sending, epump: %lu\n", iodev_rip(vobj), iodev_rport(vobj), epumpid(iodev_epump(vobj))); iodev_close(vobj); return -100; } break; case IOE_WRITE: case IOE_CONNECTED: break; case IOE_TIMEOUT: cmdid = iotimer_cmdid(vobj); if (cmdid == 1001) { printf("\nThreadID=%lu IOTimerID=%lu EPumpID=%lu timeout, curtick=%lu\n", get_threadid(), iotimer_id(vobj), epumpid(iotimer_epump(vobj)), time(0)); epcore_print(pcore); iotimer_start(pcore, 90*1000, 1001, NULL, echo_pump, pcore); } break; case IOE_INVALID_DEV: break; default: break; } printf("ThreadID=%lu event: %d fdtype: %d WorkerID=%lu\n\n", get_threadid(), event, fdtype, workerid(worker_thread_self(pcore))); return 0; } 这个示例中使用大量的多余的打印代码,看起没那么美观,有洁癖的程序员可以去掉。 使用gcc编译以上代码的命令如下: gcc -g -O3 -Wall -DUNIX -I/usr/local/include -I/usr/local/include/adif -L/usr/local/lib -lm -lpthread -ladif -lepump echosrv.c -o echosrv 编译完成后大家执行并调试,享受编程乐趣。 十二. 使用ePump框架开发高性能程序总结 以上用一个TCP服务器程序来展示如何使用ePump框架进行编程的实例,管中窥豹,以一概全,感兴趣的程序员可以下载和体验。 使用ePump框架最成功的案例是eJet Web服务器开源项目,这是一个轻量级、高性能、嵌入式Web服务器,各项功能不逊于Nginx。研究这个项目可以有助于理解ePump框架的工作原理。 简单总结ePump框架的功能特点: ePump框架封装了很多琐碎的容易出错误的细节,让开发人员将更多时间花在业务处理上; 将复杂的各个操作系统都互不兼容的多路复用技术封装后,提供了标准的接口给程序员,大大节省了应用开发周期; 高效利用事件驱动、多线程调度机制来实现多核CPU的并行运算能力; 使用ePump开发高性能程序,代码简单干练,可靠性高; 对IPv6、DNS等头提供了支持; ......太多了,写不下去了,大家补充吧 2021年2月17 春节期间 写于北京

优秀的个人博客,低调大师

如何才能编写高性能的 Swift 代码

文档中的一些技巧可以帮助提升您的 Swift 程序质量,使您的代码不容易出错且可读性更好。显式地标记最终类和类协议是两个显而易见的例子。 然而文档中还有一些技巧是不符合规矩的,扭曲的,仅仅解决一些比编译器或语言的特殊的临时性需求。文档中的很多建议来自于多方面的权衡,例如:运行时、字 节大小、代码可读性等等。 启用优化 第一个应该做的事情就是启用优化。Swift 提供了三种不同的优化级别: -Onone: 这意味着正常的开发。它执行最小优化和保存所有调试信息。 -O: 这意味着对于大多数生产代码。编译器执行积极地优化,可以大大改变提交代码的类型和数量。调试信息将被省略但还是会有损害的。 -Ounchecked: 这是一个特殊的优化模式,它意味着特定的库或应用程序,这是以安全性来交换的。编译器将删除所有溢出检查以及一些隐式类型检查。这不是在通常情况下使用 的,因为它可能会导致内存安全问题和整数溢出。如果你仔细审查你的代码,那么对整数溢出和类型转换来说是安全的。 在 Xcode UI 中,可以修改的当前优化级别如下: … 整个组件优化 默认情况下 Swift 单独编译每个文件。这使得 Xcode 可以非常快速的并行编译多个文件。然而,分开编译每个文件可以预防某些编译器优化。Swift 也可以犹如它是一个文件一样编译整个程序,犹如就好像它是一个单一的编译单元一样优化这个程序。这个模式可以使用命令行 flag-whole-module-optimization 来激活。在这种模式下编译的程序将最最有可能需要更长时间来编译,单可以运行得更快。 这个模式可以通过 XCode 构建设置中的“Whole Module Optimization”来激活。 降低动态调度 Swift 在默认情况下是一个类似 Objective-C 的非常动态的语言。与 Objective-C 不同的是,Swift 给了程序员通过消除和减少这种特性来提供运行时性能的能力。本节提供几个可被用于这样的操作的语言结构的例子。 动态调度 类使用动态调度的方法和默认的属性访问。因此在下面的代码片段中,a.aProperty、a.doSomething() 和 a.doSomethingElse() 都将通过动态调度来调用: classA{ varaProperty:[Int] funcdoSomething(){...} dynamicdoSomethingElse(){...} } classB:A{ overridevaraProperty{ get{...} set{...} } overridefuncdoSomething(){...} } funcusingAnA(a:A){ a.doSomething() a.aProperty=... } 在 Swift 中,动态调度默认通过一个 vtable[1](虚函数表)间接调用。如果使用一个 dynamic 关键字来声明,Swift 将会通过调用 Objective-C 通知来发送呼叫代替。这两种情况中,这种情况会比直接的函数调用较慢,因为它防止了对间接呼叫本身之外程序开销的许多编译器优化[2]。在性能关键的代码 中,人们常常会想限制这种动态行为。 建议:当你知道声明不需要被重写时使用“final”。 final 关键字是一个类、一个方法、或一个属性声明中的一个限制,使得这样的声明不得被重写。这意味着编译器可以呼叫直接的函数调用代替间接调用。例如下面的 C.array1 和 D.array1 将会被直接[3]访问。与之相反,D.array2 将通过一个虚函数表访问: finalclassC{ //Nodeclarationsinclass'C'canbeoverridden. vararray1:[Int] funcdoSomething(){...} } classD{ finalvararray1[Int]//'array1'cannotbeoverriddenbyacomputedproperty. vararray2:[Int]//'array2'*can*beoverriddenbyacomputedproperty. } funcusingC(c:C){ c.array1[i]=...//CandirectlyaccessC.arraywithoutgoingthroughdynamicdispatch. c.doSomething()=...//CandirectlycallC.doSomethingwithoutgoingthroughvirtualdispatch. } funcusingD(d:D){ d.array1[i]=...//CandirectlyaccessD.array1withoutgoingthroughdynamicdispatch. d.array2[i]=...//WillaccessD.array2throughdynamicdispatch. } 建议:当声明的东西不需要被文件外部被访问到的时候,就用“private” 将 private 关键词用在一个声明上,会限制对其进行了声明的文件的可见性。这会让编辑器有能力甄别出所有其它潜在的覆盖声明。如此,由于没有了任何这样的声明,使得编 译器可以自动地推断出 final 关键词,并据此去掉对方面的间接调用和属性的访问。例如在如下的e.doSomething() 和 f.myPrivateVar 中,就将可以被直接访问,假定在同一个文件中,E,F 并没有任何覆盖的声明: privateclassE{ funcdoSomething(){...} } classF{ privatevarmyPrivateVar:Int } funcusingE(e:E){ e.doSomething()//Thereisnosubclassinthefilethatdeclaresthisclass. //ThecompilercanremovevirtualcallstodoSomething() //anddirectlycallA’sdoSomethingmethod. } funcusingF(f:F)->Int{ returnf.myPrivateVar } 高效的使用容器类型 通用的容器 Array 和 Dictionary 是有 Swift 标准库提供的一个重要的功能特性。本节将介绍如何用一种高性能的方式使用这些类型。 建议:在数组中使用值类型 在 Swift 中,类型可以分为不同的两类:值类型(结构体,枚举,元组)和引用类型(类)。一个关键的区分是 NSArray 不能含有值类型。因此当使用值类型时,优化器就不需要去处理对 NSArray 的支持,从而可以在数组上省去大部分消耗。 此外,相比引用类型,如果值类型递归地含有引用类型,那么值类型仅仅需要引用计数器。而如果使用没有引用类型的值类型,就可以避免额外的开销,从而释放数组内的流量。 //Don'tuseaclasshere. structPhonebookEntry{ varname:String varnumber:[Int] } vara:[PhonebookEntry] 记住要在使用大值类型和使用引用类型之间做好权衡。在某些情况下,拷贝和移动大值类型数据的消耗要大于移除桥接和持有/释放的消耗。 建议:当 NSArray 桥接不必要时,使用 ContiguousArray 存储引用类型。如果你需要一个引用类型的数组,而且数组不需要桥接到 NSArray 时,使用 ContiguousArray 替代 Array: classC{...} vara:ContiguousArray<C>=[C(...),C(...),...,C(...)] 建议:使用适当的改变而不是对象分配。 在 Swift 中所有的标准库容器都使用 COW(copy-on-write) 执行拷贝代替即时拷贝。在很多情况下,这可以让编译器通过持有容器而不是深度拷贝,从而省掉不必要的拷贝。如果容器的引用计数大于 1 并容器时被改变时,就会拷贝底层容器。例如:在下面这种情况:当 d 被分配给 c 时不拷贝,但是当 d 经历了结构性的改变追加 2,那么 d 将会被拷贝,然后 2 被追加到 b: varc:[Int]=[...] vard=c//Nocopywilloccurhere. d.append(2)//Acopy*does*occurhere. 如果用户不小心时,有时 COW 会引起额外的拷贝。例如,在函数中,试图通过对象分配执行修改。在 Swift 中,所有的参数传递时都会被拷贝一份,例如,参数在调用点之前持有一份,然后在调用的函数结束时释放。也就是说,像下面这样的函数: funcappend_one(a:[Int])->[Int]{ a.append(1) returna } vara=[1,2,3] a=append_one(a) 尽管由于分配,a 的版本没有任何改变 ,在 append_one后也没有使用 , 但 a 也许会被拷贝。这可以通过使用 inout 参数来避免这个问题: funcappend_one_in_place(inouta:[Int]){ a.append(1) } vara=[1,2,3] append_one_in_place(&a) 未检查操作 Swift 通过在执行普通计算时检查溢出的方法解决了整数溢出的 bug。这些检查在已确定没有内存安全问题会发生的高效的代码中,是不合适的。 建议:当你确切的知道不会发生溢出时使用未检查整型计算。 在对性能要求高的代码中,如果你知道你的代码是安全的,那么你可以忽略溢出检查。 a:[Int] b:[Int] c:[Int] //Precondition:foralla[i],b[i]:a[i]+b[i]doesnotoverflow! foriin0...n{ c[i]=a[i]&+b[i] } 泛型 Swift 通过泛型类型的使用,提供了一个非常强大的抽象机制 。Swift 编译器发出一个可以对任何 T 执行 MySwiftFunc<T> 的具体的代码块。生成的代码需要一个函数指针表和一个包含 T 的盒子作为额外的参数。MySwiftFunc<Int>和 MySwiftFunc<String> 之间的不同的行为通过传递不同的函数指针表和通过盒子提供的抽象大小来说明。一个泛型的例子: classMySwiftFunc<T>{...} MySwiftFunc<Int>X//WillemitcodethatworkswithInt... MySwiftFunc<String>Y//...aswellasString. 当优化器启用时,Swift 编译器寻找这段代码的调用,并试着确认在调用中具体使用的类型(例如:非泛型类型)。如果泛型函数的定义对优化器来说是可见的,并知道具体类 型,Swift 编译器将生成一个有特殊类型的特殊泛型函数。那么调用这个特殊函数的这个过程就可以避免关联泛型的消耗。一些泛型的例子: classMyStack<T>{ funcpush(element:T){...} funcpop()->T{...} } funcmyAlgorithm(a:[T],length:Int){...} //ThecompilercanspecializecodeofMyStack[Int] varstackOfInts:MyStack[Int] //Usestackofints. foriin...{ stack.push(...) stack.pop(...) } vararrayOfInts:[Int] //Thecompilercanemitaspecializedversionof'myAlgorithm'targetedfor //[Int]'types. myAlgorithm(arrayOfInts,arrayOfInts.length) 建议:将泛型的声明放在使用它的文件中 只有在泛型声明在当前模块可见的情况下优化器才能执行特殊化。这只有在使用泛型的代码和声明泛型的代码在同一个文件中才能发生。注意标准库是一个例外。在标准库中声明的泛型对所有模块可见并可以进行特殊化。 建议:允许编译器进行特殊化 只有当调用位置和被调函数位于同一个编译单元的时候编译器才能对泛型代码进行特殊化。我们可以使用一个技巧让编译器对被调函数进行优化,这个技巧就 是在被调函数所在的编译单元中执行类型检查。执行类型检查的代码会重新分发这个调用到泛型函数—可是这一次它携带了类型信息。在下面的代码中,我们在函数 play_a_game 中插入了类型检查,使得代码的速度提高了几百倍。 //Framework.swift: protocolPingable{funcping()->Self} protocolPlayable{funcplay()} extensionInt:Pingable{ funcping()->Int{returnself+1} } classGame<T:Pingable>:Playable{ vart:T init(_v:T){t=v} funcplay(){ for_in0...100_000_000{t=t.ping()} } } funcplay_a_game(game:Playable){ //Thischeckallowstheoptimizertospecializethe //genericcall'play' ifletz=gameas?Game<Int>{ z.play() }else{ game.play() } } ///-------------->8 //Application.swift: play_a_game(Game(10)) 大的值对象的开销 在 swift 语言中,值类型保存它们数据独有的一份拷贝。使用值类型有很多优点,比如值类型具有独立的状态。当我们拷贝值类型时(相当于复制,初始化参数传递等操作),程序会创建值类型的一个拷贝。对于大的值类型,这种拷贝时很耗费时间的,可能会影响到程序的性能。 让我们看一下下面这段代码。这段代码使用值类型的节点定义了一个树,树的节点包含了协议类型的其他节点,计算机图形场景经常由可以使用值类型表示的实体以及形态变化,因此这个例子很有实践意义 protocolP{} structNode:P{ varleft,right:P? } structTree{ varnode:P? init(){...} } 当树进行拷贝时(参数传递,初始化或者赋值)整个树都需要被复制.这是一项花销很大的操作,需要很多的 malloc/free 调用以及以及大量的引用计数操作 然而,我们并不关系值是否被拷贝,只要在这些值还在内存中存在就可以。 对大的值类型使用 COW(copy-on-write,写时复制和数组有点类似) 减少复制大的值类型数据开销的办法时采用写时复制行为(当对象改变时才进行实际的复制工作)。最简单的实现写时复制的方案时使用已经存在的写时复制 的数据结构,比如数组。Swift 的数据是值类型,但是当数组作为参数被传递时并不每次都进行复制,因为它具有写时复制的特性。 在我们的 Tree 的例子中我们通过将 tree 的内容包装成一个数组来减少复制的代价。这个简单的改变对我们 tree 数据结构的性能影响时巨大的,作为参数传递数组的代价从 O(n) 变为 O(1)。 structtree:P{ varnode:[P?] init(){ node=[thing] } } 但是使用数组实现 COW 机制有两个明显的不足,第一个问题是数组暴露的诸如 append 以及 count 之类的方法在值包装的上下文中没有任何作用,这些方法使得引用类型的封装变得棘手。也许我们可以通过创建一个封装的结构体并隐藏这些不用的 API 来解决这个问题,但是却无法解决第二个问题。第二个问题就是数组内部存在保证程序安全性的代码以及和 OC 交互的代码。Swift 要检查给出的下表是否搂在数组的边界内,当保存值的时候需要检查是否需要扩充存储空间。这些运行时检查会降低速度。 一个替代的方案是实现一个专门的使用 COW 机制的数据结构代替采用数组作为值的封装。构建这样一个数据结构的示例如下所示: finalclassRef<T>{ varval:T init(_v:T){val=v} } structBox<T>{ varref:Ref<T> init(_x:T){ref=Ref(x)} varvalue:T{ get{returnref.val} set{ if(!isUniquelyReferencedNonObjC(&ref)){ ref=Ref(newValue) return } ref.val=newValue } } } 类型 Box 可以代替上个例子中的数组 不安全的代码 Swift 语言的类都是采用引用计数进行内存管理的。Swift 编译器会在每次对象被访问的时候插入增加引用计数的代码。例如,考虑一个遍历使用类实现的一个链表的例子。遍历链表是通过移动引用到链表的下一个节点来完 成的:elem = elem.next,每次移动这个引用,Swift 都要增加 next 对象的引用计数并减少前一个对象的引用计数,这种引用计数代价昂贵但是只要使用 swift 类就无法避免 finalclassNode{ varnext:Node? vardata:Int ... } 建议:使用未托管的引用避免引用计数的负荷 在效率至上的代码中你可以选择使用未托管的引用。Unmanaged<T>结构体允许开发者对特别的引用关闭引用计数 varRef:Unmanaged<Node>=Unmanaged.passUnretained(Head) whileletNext=Ref.takeUnretainedValue().next{ ... Ref=Unmanaged.passUnretained(Next) } 协议 建议:将只有类实现的协议标记为类协议 Swift 可以指定协议只能由类实现。标记协议只能由类实现的一个好处是编译器可以基于这一点对程序进行优化。例如,ARC 内存管理系统能够容易的持有(增加该对象的引用计数)如果它知道它正在处理一个类对象。如果编译器不知道这一点,它就必须假设结构体也可以实现协议,那么 它就必须准备好持有或者释放不同的数据结构,而这代价将会十分昂贵。 如果限制只能由类实现某协议那么就标记该协议为类协议以获得更好的性能 protocolPingable:class{funcping()->Int} 脚注 【1】虚拟方法表或者 vtable 是被一个实例引用的一种包含类型方法地址的类型约束表。进行动态分发时,首先从对象中查找这张表然后查找表中的方法 【2】这是因为编译器并不知道那个具体的方法要被调用 【3】例如,直接加载一个类的字段或者直接调用一个方法 【4】解释 COW 是什么 【5】在特定情况下优化器能够通过内联和 ARC 优化技术移除 retain,release 因为没有引起复制 来源:51CTO

优秀的个人博客,低调大师

Trivy:Go 语言编写的开源安全扫描工具

Trivy 是 Aqua Security 开发的开源安全扫描工具,专注于检测容器镜像、文件系统、代码仓库等场景中的安全漏洞、配置错误及敏感信息泄露问题。 GitHub 地址:https://github.com/aquasecurity/trivy Trivy 凭借其全面的检测能力、易用性和开源特性,成为容器安全领域的核心工具。核心功能多场景覆盖: 支持扫描容器镜像、文件系统、Kubernetes 集群、Git 仓库及云环境,识别操作系统包(如 Alpine、Debian)和语言依赖项(如 npm、PyPI)的已知漏洞(CVE); 高精度匹配:内置实时更新的漏洞数据库(如 NVD、Red Hat),提供漏洞描述、修复建议及影响范围分析; 配置错误检查:基础设施即代码(IaC)扫描:检测 Terraform、Kubernetes 等 IaC 文件的配置错误,例如权限过大或敏感端口暴露; 敏感信息泄露检测:自动扫描代码中的密钥、密码等敏感数据; 软件物料清单生成:自动生成符合 CycloneDX 或 SPDX 标准的 SBOM

优秀的个人博客,低调大师

Rust 编写的 Zed 编辑器正式开源

Zed 是一款支持多人协作的代码编辑器,底层采用 Rust,主打 “高性能”。其开发团队今日宣布 Zed 正式开源。 Zed 团队称开源了大约 27 万行 Rust 代码,具体包括以下组件: 遵循 GPL 的编辑器代码 遵循 AGPL 的服务器组件 遵循Apache 2 的UI 框架 GPUI (https://github.com/zed-industries/zed/tree/main/crates/gpui) 开源地址:https://github.com/zed-industries/zed https://twitter.com/zeddotdev/status/1750203594186350876 Zed 是 Atom 编辑器原作者主导的新项目,他希望将 Zed 打造为世界上最先进的代码编辑器,并认为开源 Zed 将会使其成为最好的产品。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

Oracle

Oracle

Oracle Database,又名Oracle RDBMS,或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是目前世界上流行的关系数据库管理系统,系统可移植性好、使用方便、功能强,适用于各类大、中、小、微机环境。它是一种高效率、可靠性好的、适应高吞吐量的数据库方案。

Apache Tomcat

Apache Tomcat

Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache、Sun 和其他一些公司及个人共同开发而成。因为Tomcat 技术先进、性能稳定,而且免费,因而深受Java 爱好者的喜爱并得到了部分软件开发商的认可,成为目前比较流行的Web 应用服务器。

Eclipse

Eclipse

Eclipse 是一个开放源代码的、基于Java的可扩展开发平台。就其本身而言,它只是一个框架和一组服务,用于通过插件组件构建开发环境。幸运的是,Eclipse 附带了一个标准的插件集,包括Java开发工具(Java Development Kit,JDK)。