|
|
着急的打火机 · 地铁五线并行织密本地路网 ...· 1 年前 · |
|
|
健身的铁链 · 《花少2》展示过最真实的郑爽|许晴|杨洋|井 ...· 1 年前 · |
|
|
粗眉毛的高山 · jQuery根据ID、CLASS、等获取对象 ...· 2 年前 · |
|
|
爱喝酒的茶叶 · ubuntu 22.04 ssh ...· 2 年前 · |
|
|
安静的铅笔 · Responsividade | ...· 3 年前 · |
两种高效事件处理模式&并发模式来源如下,侵删。游双-《Linux高性能服务器编程》本来想做个笔记的,但是发现这块内容书中很多都感觉是有用的,所以很大篇幅的搬了过来,其中加入了我的理解,并有重点标注。服务器编程框架服务器程序种类繁多,但是基本框架都一样,不同之处在于逻辑处理。下图所示,服务器基本框架。该图既能用来描述一台服务器,也能用来描述一个服务器机群。各模块概念模块单个服务器框架服务器机群I/O逻辑单元处理客户连接,读写网络数据作为接入服务器,实现负载聚恒逻辑单元业务进程或线程逻辑服务器网络存储单元本地数据库、文件或缓存数据库服务器请求队列各单元之间的通信方式各服务器之间的永久TCP连接I/O处理单元I/O处理单元是服务器管理客户端连接的模块。它通常要完成一下工作:等待并接受新的客户链接;接收客户数据;将服务器响应的数据返回给客户端。数据的收发不一定在I/O处理单元中执行,也可能在逻辑单元中执行,具体在何处执行取决于事件处理模式。对于一个服务器机群来说,I/O处理单元是一个专门的接入服务器。它实现负载均衡,从所有逻辑服务器中选取负荷最小的一台来为新客户服务。逻辑单元一个逻辑单元通常是一个进程或线程。它分析并处理客户数据,然后将结果传递给I/O处理单元或者直接发送给客户端。具体使用哪种方式取决于事件处理模式。对服务器机群而言,一个逻辑单元本身就是一台逻辑服务器。服务器通常拥有多个逻辑单元, 以实现对多个客户任务的并行处理。网络存储单元网络存储单元可以是数据库、缓存和文件,甚至是一台独立的服务器。它不是必须的,例如:ssh、telnet等登录服务器就不需要这个单元。请求队列请求队列是各个单元之间通信方式的抽象。I/O处理单元接收到客户请求时,需要以某种方式来通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件。请求队列通常被实现为池的一部分。对于服务器机群而言,请求队列是各台服务器之间预先建立的、静态的、永久的TCP链接。这种TCP连接能提高服务器之间交换数据的效率,因为它避免了动态建立TCP导致的额外系统开销。两种高效的事件处理模式服务器通常需要处理三类事件:I/O事件信号定时事件下面介绍两种高效的事件处理模式: Reactor与Proactor。同步I/O模型通常用于实现Reactor模式,异步I/O模型则用于实现Proactor模式。可以以使用同步I/O模型去模拟Proactor模式。Reactor模式Reactor模式中,主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(即,逻辑单元,下同)除此之外,主线程不做任何其它实质性的工作。读写数据,接收新的连接,以及处理客户请求(业务逻辑)均在工作线程中完成。使用同步I/O模型(以epoll_wait为例)实现的Reactor模式的工作流程:主线程往epoll内核事件表中注册socket上的读就绪事件。(监听socket与连接socket成功建立连接后,以下socket都指的是连接socket)主线程调用epoll_wait等待socket上有数据可读。当socket上有数据可读时,epoll_wait通知主线程。主线程则将socket可读事件放入请求队列。睡眠在请求队列上的某个工作线程被唤醒,它从连接socket读取数据,并处理客户请求,然后往epoll内核事件表中注册该socket上的写就绪事件。主线程调用epoll_wait等待scoket可写。当socket可写时,epoll_wait通知主线程。主线程将socket可写事件放入请求队列。睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果。总结: 主线程仅负责监听socket看是否有发生事件,然后就通知工作线程读取,处理数据,如为写事件(即,要应答),再在epoll内核事件表上注册该连接socket的可写事件,然后再由某个工作线程接管,处理,执行应答,读事件同理。Reactor模式工作流程图如下所示:工作线程从请求队列中取出事件后,将根据事件的类型决定如何处理它。对于可读事件,执行读数据和处理请求的操作;对于可写事件,执行写数据的操作;因此,如上图所示的Reactor模式中,没必要区分所谓的“读工作线程”和“写工作线程”。Proactor模式与Reactor模式不同,Proactor模式将所有I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。因此,Proactor模式更符合服务器基本框架图中的描述。使用异步I/O模型(以aio_read和aio_write为例)实现的Proactor模式的工作流程:主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序(这里以信号为例,详情请参考sigevent的man手册)。主线程继续处理其它逻辑。当socket上的数据被读入用户缓冲区后,内核用户向应用程序发送一个信号,以通知应用程序数据已经可用。应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操完成时如何通知应用程序(仍以信号为例)。总结: 内核用户与主线程进行I/O操作,由信号通知主线程唤醒一个工作线程进行处理数据(业务逻辑),业务处理完,再交给内核用户与主线程进行I/O操作(服务器应答)。Proactor模式工作流程图如下图所示:上图中,连接socket上的读写事件是通过aio_read/aio_write向内核注册的,因此内核将通过信号来向应用程序报告连接socket上的读写事件。所以,主线程上的epoll_wait调用仅能用来检测监听socket上的连接请求事件,而不能用来检测连接socket上的读写事件。对比&总结Reactor模式与Proactor相对比:I/O操作:Reactor: 工作线程来完成。对应socket上可以读(写)数据了,唤醒一个工作线程在对应socket上读(写)。Proactor: 主线程和内核来完成。对应socket上可以读(写)数据了,内核用户后读取完成通知应该程序,由定义好的信号处理函数选择一个工作线程进行数据业务处理,然后通知内核可以写回去了(服务器应答)。数据业务处理:Reactor: 工作线程来完成。Proactor: 工作线程来完成。模拟Proactor模式使用同步I/O方式模拟出Proactor模式。原理是: 主线程执行读写操作,读写完成之后,主线程向工作线程通知这一"完成事件"。从工作线程的角度来看,它们就直接获取了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理。使用同步I/O模型(仍以epoll_wait为例)模拟出的Proactor模式的工作流程如下:(其中socket为连接socket)主线程往epoll内核事件表中注册socket上的读就绪事件。主线程调用epoll_wait等待socket上有数据可读。当socket上有数据可读时,epoll_wait通知主线程。主线程从socket上循环读取数据,将读取到的数据封装成一个请求对象并插入到请求队列中。睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核表中注册socket上的写就绪事件。主线程调用epoll_wait等待socket可读。当socket可写时,epoll_wait通知主线程。主线程往socket上写入服务器处理客户请求的结果。总结: 主线程负责I/O操作,工作线程仅负责数据的处理(业务逻辑)。工作流程如下图所示:两种高效的并发模式并发编程的目的是让程序“同时”执行多个任务。如果程序是计算密集型的,并发编程并没有优势,反而由于任务的切换使效率降低。如果程序是I/O密集型的,比如经常读写文件,访问数据等,因为I/O操作的速度远没有CPU的计算速度快,所以让程序阻塞于I/O操作将浪费大量的CPU时间。如果程序有多个执行线程,则当前被I/O操作所阻塞的执行线程可主动放弃CPU(由操作系统来调度),并将执行线程转移到其他线程。这样一来,CPU就可以做更加有意义的事情(除非所有线程都同时被I/O操作所阻塞),而不是等待I/O操作完成,从而显著提升CPU的利用率。实现上: 并发编程主要有多进程和多线程两种方式。对于下图来说,并发模式是指I/O处理单元和多个逻辑单元之间协调完成任务的方法。服务器主要有两种并发编程模式:半同步/半异步模式(half-sync/half-async)领导者/追随者模式(Leader/Followers)半同步/半异步模式这里的半同步/半异步模式中的“同步"与”异步“与I/O模型中的“同步"与”异步“是完全不同的概念。I/O模型中:“同步"与”异步“区分的是内核向应用程序通知的是何种I/O事件(是就绪事件还是完成事件);以及该由谁来完成I/O读写(是应用程序还是内核)。在并发模式中:"同步"指的是程序完全按照代码序列的顺序执行;“异步”指的是程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。下图a描述了同步的读操作,下图b描述了异步的读操作。按照同步方式运行的线程称为同步线程,按照异步方式运行的线程称为异步线程。相比于同步线程,`异步线程的执行效率更高,实时性强。——(异步线程优点)但编写以异步方式执行的程序相对复杂,难于调试和扩展,而且不适合于大量的并发。——(异步线程缺点)同步线程则相反,虽然它效率相对较低,实时性较差,但是逻辑简单。——(同步线程优缺点)因此,对于像服务器这种及要求较好的实时性,又要求能同时处理多个客户请求的应用程序,我们就应该同时使用同步线程与异步线程来实现,即采用半同步/半异步模式来实现。半同步/半异步模式中:同步线程用于处理客户逻辑,相当于服务器基本框架图中的逻辑单元;异步线程用于处理I/O请求, 相当于服务器基本框架图中的I/O处理单元。工作线程处理I/O操作,所以半同步/半异步模式采用的是Reactor事件处理模式。异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中。请求队列将通知某个工作在同步模式的工作线程来读取并处理该请求对象。具体选择哪个工作线程来为新的客户请求服务器,则取决于请求队列的设计。比如最简单的轮流选取工作线程的Round Robin算法,也可以通过条件变量或信号量来随机地选择一个工作线程。半同步/半异步模式的工作流程如下图所示:半同步/半反应堆模式在服务器程序中,如果结合考虑两种事件处理模式的几种I/O模型,则半同步/半异步模式就存在多种变体。其中一种就叫做半同步/半反应堆模式(half-sync/half-reactive),如下图所示:half-reactive体现在工作线程读写连接socket上的数据,详见下面。如上图所示,异步线程只有一个,由主线程来充当,它负责监听所有socket上的事件。如果监听socket上有可读事件发生(监听socket在服务端当然只能发生可读事件,哪有自己给自己发消息的,即在监听socket上写),即有新的连接到来,主线程就接受以得到新的连接socket,然后往epoll内核事件表中注册该socket的读写事件。如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,主线程就将该连接socket插入请求队列中。所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争(比如申请互斥锁)来获得任务的接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务,这是很合理的。上图中,主线程插入请求中队列中的任务是就绪的连接socket(即,该连接socket上有读写事件发生)。这说明该图所示的半同步/半反应堆模式采用的事件处理模式是Reactor模式,它要求工作线程自己从socket上读取客户数据和往socket上写入服务器应答。这就是其名字(half-reactive)的含义。半同步/半反应堆也可以模拟Proactor事件处理模式,即由主线程完成数据的读写。在这种情况下:主线程一般会将应用程序数据、任务类型等信息封装成一个任务对象(即把对应socket上的数据读出来,封装到一个任务对象中);然后将其(或者指向该任务对象的一个指针)插入请求队列;工作线程从请求队列中取得任务对象之后,即可处理之,无需读写操作。半同步/半反应堆模式存在如下缺点:主线程和工作线程共享请求队列。主线程往请求队列中添加任务,或者工作线程从请求队列中取出任务,都需要对请求队列加锁保护,从而白白耗费CPU时间。每个工作线程在同一时间只能处理一个客户请求。如果客户数量较多,而工作线程较少,则请求队列中奖堆积很多任务对象,客户端的响应速度将越来越慢。如果通过增加工作线程来解决这一问题,则工作线程的切换也将耗费大量CPU时间。相对高效的半同步/半异步模式下图描述了一种相对高效的半同步/半异步模式,它的每个工作线程都能同时处理多个客户连接。上图中:主线程只管理监听socket,连接socket由工作线程来管理。当有新的连接到来时,主线程就接受之并将新返回的连接socket派发给某个工作线程。此后该连接socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接。主线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据。工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接到来。如果是,则把该新的连接socket上的读写事件注册到自己的epoll内核事件表中。以后该连接socket上的所有I/O事件都由此工作线程进行监听与操作,直到客户关闭连接。(与上面重复了,这里再写一遍我想印象会深一些。)如上图所示,每个线程(主线程与工作线程)都维持自己的事件循环,它们各自独立地监听不同的事件。因此,在这种高效的半同步/半异步模式中,每个线程都工作在异步模式,所以它并非严格意义上的半同步/半异步模式。领导者/追随者模式领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。在任意时间点,程序都仅有一个领导者线程,它负责监听I/O事件。其它线程都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到I/O事件,首先要从线程池中推选出新的领导者线程,然后处理I/O事件。此时,新的领导者等待新的I/O事件,而原来的领导者则处理当前检测到的I/O事件,二者实现了并发。领导者/追随者模式包含如下几个组件:句柄集(HandleSet)线程集(ThreadSet)事件处理器(EventHandler)具体的事件处理器(ConcreteEventHandler)它们的关系如下图所示:句柄集:句柄(Handle)用于表示I/O资源,在Linux下通常就是一个文件描述符。句柄集管理众多句柄,它使用wait_for_event方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程。领导者线程调用绑定到Handle上的事件处理器来处理事件。领导者将Handle和事件处理器绑定是通过调用句柄集中的register_handle方法实现的。线程集:线程集是所有工作线程(包括领导者线程和追随者线程)的管理者。它负责各个线程之间的同步,以及新领导者线程的推选。线程集中的线程在任一时间必处于如下三种状态之一;Leader: 线程单当前处于领导者身份,负责等待句柄集上的I/O事件。Processing: 线程正在处理事件。领导者检测到I/O事件之后,可以转移到Processing状态来处理事件,并调用promote_new_leader方法推选新的领导者;也可以指定其他追随者来处理事件(Event Handoff),此时领导者的地位不变。当处于Processing状态的线程处理完事件之后,如果当前线程集中没有领导者,则它将成为新的领导者,否则它就直接转变为追随者。Follower: 线程当前处于追随者身份,通过调用线程集的join方法等待成为新的领导者,也可能被当前的领导者指定来处理新的任务。如下图所示这三种状态之间的转换关系:需要注意的是,领导者线程推选新的领导者和追随者等待成为新的领导者,这两个操作都将修改线程集,因此线程集提供一个成员Synchronizer来同步这两个操作,以避免竞态条件。事件处理器和具体的事件处理器:事件处理器通常包含一个过多个回调函数(handle_event)。这些回调函数用于处理事件对应的业务逻辑。事件处理器在使用前需要被绑定到某个句柄上,当该句柄上有事件发生时,领导者就执行与之绑定的事件处理器中的回调函数。具体的事件处理器是事件处理器的派生类,它们必须重新实现基类handle_event方法,以处理特定的任务。综上所述,领导者/追随者工作流程如下图所示:由于领导者线程自己监听I/O事件并处理客户请求,因而领导者/追随者模式不需要在线程之间传递任何额外的数据,也无须像半同步/半反应堆那样在线程之间同步对请求队列的访问。但领导者/追随者的一个明显缺点是仅支持一个事件源集合,因此也无法像高效的半同步/半异步模式那样,让每个工作线程独立地管理多个客户链接。
跳表SkipList解析原项目链接——基于跳表实现的轻量级键值数据库添加注释后——SkipList什么是跳表这里不做介绍,详见:跳表──没听过但很犀利的数据结构拜托,面试别再问我跳表了!代码解析主要理解点先来张图各个节点是如何相连接(关联)的?通过每个节点的forward数组,forward数组存储当前节点,在每一层的下一个节点。以头节点为例,头结点的forward存储的是每一层的第一个节点。然后通过第一个节点的forward[level],拿到该层的后面元素...... 以次类推,即可遍历该层所有节点。与普通单链表的区别,我们可以大概理解为,上面多了几层简化的结果,如上面动画所示。update存的是什么?每层中最后一个key小于要插入节点key的节点。节点生成的level是多少,该节点就从0层到level层一直都出现。其它详见具体代码中的注释。相关类&成员函数节点类Nodetemplate<typename K, typename V> class Node { public: // 无参构造 Node() {} // 有参构造-最后一个参数为层数 Node(K k, V v, int); // 析构 ~Node(); // 拿到当前节点key值 K get_key() const; // 拿到当前节点value值 V get_value() const; // 设置value值 void set_value(V); // forward-存储当前结点在i层的下一个结点。 Node<K, V> **forward; //所在层级 int node_level; private: K key; V value; };跳表类SkipListtemplate <typename K, typename V> class SkipList { public: // 有参构造-参数为最大层数 SkipList(int); // 析构 ~SkipList(); // 生成随机层数 int get_random_level(); // 创建节点-最后一个参数为所在层数 Node<K, V>* create_node(K, V, int); // 插入数据 int insert_element(K, V); // 打印数据-每层都打印 void display_list(); // 查找元素 bool search_element(K); // 删除元素 void delete_element(K); // 存入文件 void dump_file(); // 加载文件 void load_file(); // 返回节点个数 int size(); private: // 从文件中的一行读取key和value void get_key_value_from_string(const std::string& str, std::string* key, std::string* value); // 是否为有效的string bool is_valid_string(const std::string& str); private: // 该跳表最大层数 int _max_level; // 该跳表当前层数 int _skip_list_level; // 跳表头节点 Node<K, V> *_header; // 文件操作 std::ofstream _file_writer; std::ifstream _file_reader; // 该跳表当前的元素数 int _element_count; 详解insert函数节点的插入遍历拿到update数组Node<K, V> *update[_max_level+1];//使用_max_level+1开辟,使空间,肯定够,因为创建节点的时候,会对随机生成的key进行限制。 memset(update, 0, sizeof(Node<K, V>*)*(_max_level+1)); // 从跳表左上角开始查找——_skip_list_level为当前所存在的最高的层(一共有多少层则需要+1,因为是从level=0层开始的) for(int i = _skip_list_level; i >= 0; i--) {//控制当前所在层,从最高层到第0层 //从每一层的最左边开始遍历,如果该节点存在并且,key小于我们要插入的key,继续在该层后移。 while(current->forward[i] != NULL && current->forward[i]->get_key() < key) {//是不是继续往后面走 current = current->forward[i]; //提示: forward存储该节点在当前层的下一个节点 update[i] = current;//保存 //切换下一层 //将current指向第0层第一个key大于要插入节点key的节点。 //下面用current用来判断要插入的节点存key是否存在 //即,如果该key不存在的话,准备插入到update[i]后面。存在,则修改该key对应的value。 current = current->forward[0];更新层次// 为当前要插入的节点生成一个随机层数 int random_level = get_random_level(); //如果随机出来的层数大于当前链表达到的层数,注意:不是最大层,而是当前的最高层_skip_list_level。 //更新层数,更新update,准备在每层([0,random_level]层)插入新元素。 if (random_level > _skip_list_level) { for (int i = _skip_list_level+1; i < random_level+1; i++) { update[i] = _header; _skip_list_level = random_level; }插入节点 // 创建节点 Node<K, V>* inserted_node = create_node(key, value, random_level); // 插入节点 for (int i = 0; i <= random_level; i++) { //在每一层([0,random_level]) //先将原来的update[i]的forward[i]放入新节点的forward[i]。 //再将新节点放入update[i]的forward[i]。 inserted_node->forward[i] = update[i]->forward[i];//新节点与后面相链接 update[i]->forward[i] = inserted_node;//新节点与前面相链接 //std::cout << "Successfully inserted key:" << key << ", value:" << value << std::endl; _element_count++;//元素总数++相关参考Skip List | Set 2 (Insertion)什么是跳表skiplist基于跳表的数据库服务器和客户端以及知识星球中牲活老哥的相关分享。-跳表总结
高并发内存池设计高并发下传统方式的弊端在传统C语言中,我们使用malloc、calloc、realloc、free来进行内存的申请分配与释放,函数原型如下。C++中则是new、delete。void *malloc(size_t size);malloc在内存的动态存储区中分配了一块长度为size字节的连续区域返回该区域的首地址。void *calloc(size_t nmemb, size_t size);与malloc相似,参数size为申请地址的单位元素长度,nmemb为元素个数,即在内存中申请,nmemb*size个字节大小的连续地址空间,内存会初始化为0。void realloc(void ptr, size_t size);给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,size是重新申请的地址长度,若为NULL,它就等同于malloc。void free(void *ptr);弊端弊端1:高并发时较小内存块的使用,导致系统调用频繁,降低了系统的执行效率。弊端2: 频繁使用时增加了系统内存的碎片,降低内存使用效率。内存碎片-已经被分配出去(能明确指出属于哪个进程)却不能被利用的内存空间。产生根源:内存分配必须起始于可被4、8、16整除(视处理器体系结构而定)的地址。MMU的分页机制的限制,操作系统按页给空间。弊端3:没有垃圾回收机制,容易造成内存泄漏,导致内存枯竭。示例1:malloc后没有freevoid log_error(char *reason) { char *p1; p1 = malloc(100); sprintf(p1,"The f1 error occurred because of '%s'.", reason); log(p1); }示例2:打开文件同样需要申请内存资源,fopen后没有fclose。int getkey(char *filename) { FILE *fp; int key; fp = fopen(filename, "r"); fscanf(fp, "%d", &key); //fclose(fp); return key; 弊端4:内存分配与释放的逻辑在程序中相隔较远时,从而降低了程序的稳定性。例如:由于相隔久远,误将非malloc的变量进行free。解决弊端系统层——内存管理组件的选择解决方案:使用高性能内存管理组件Tcmalloc、Jemalloc,替换Glibc Ptmalloc,以解决优化效率和碎片问题。内存管理组件的选择:应用层——内存池技术什么是内存池技术?就是真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若剩余的内存块不够了,就再继续申请新的内存, 统一对程序所使用的内存进行分配和回收。这样做的一个显著优点是,使得内存分配的效率得到很大提升。个人理解:什么是内存池 & 内存池的用处。减少频繁的系统调用以减少时间开销,一次性申请一块大内存,然后给需要的程序进程分配,不够了就再要。内存池如何解决弊端?高并发时系统调用频繁,降低了系统的执行效率。内存池提前预先分配大块内存,统一释放,极大的减少了malloc和free等函数的调用。频繁使用时增加了系统内存的碎片,降低了内存使用效率。内存池每次请求分配大小适度的内存块,避免了碎片的产生。没有垃圾回收机制,容易造成内存泄漏。在声明周期结束后统一释放内存,完全避免了内存泄漏的产生。内存分配与释放的逻辑在程序中相隔较远时,降低了程序的稳定性。在声明周期结束后统一释内存,避免重复释放指针或释放空指针等情况。高并发时内存池如何实现?高并发——是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。高并发的特点:响应时间短吞吐量大每秒响应请求数QPS并发用户数高内存池设计考虑设计逻辑应该尽量简单,避免不同请求之间相互影响,尽量降低不同模块之间的耦合。内存池生存时间应该尽可能短,与请求或者连接具有相同的周期,减少碎片堆积和内存泄漏。实现思路——分而治之对于每个请求或者连接都会建立相应相应的内存池,建立好内存池之后,我们可以直接从内存池中申请所需要的内存,不用去管内存的释放,当内存池使用完成之后一次性销毁内存池。区分大小内存块的申请和释放,大于池尺寸的定义为大内存块,使用单独的大内存块链表保存,即时分配和释放;小于等于池尺寸的定义为小内存块,直接从预先配的内存块中提取,不够就扩充池中的内存,在生命周期内对小块内存不做释放,直到最后统一销毁。Nginx内存池结构设计主要结构图:更详细一点的:详情见部分源码解析。ngx_pool_t(内存池头结点)结构示意图,图中没有示意出large后面链接的大内存块。部分源码解析注意:部分Nginx源代码可能有所删减,这里仅分析有关内存池的部分内容。下面文字中,小内存即一般内存。nginx内存池流程,首先创建一个头结点,头结点不同于其它普通内存块,除了待分配内存区,还存储着当前内存池的其它相关信息。后续内存不够需要扩充,将扩充出来的内存块链接到内存池头结点后,扩充内块分为大内存块和一般内存块,分别链接到不同的位置,大内存链表和小内存链表。由申请内存时不够自动个扩充。申请内存时,首先判断申请的大小,如果超过当前内存池设定的最大申请大小,则进行大内存申请,反之则进行小内存申请。申请大内存时,首先申请好所需要的空间,然后在内存池头结点的大内存节点中,寻找有没有可重用的结点,如果有就将刚创建好的内存链接上去。反之创一个新节点,将创建好的内存链接上去,插入到大内存链表中。申请小内存时,首先在头节点后链接的其它小内存块中寻找,是否有有可用空间的小内存块。有则使用,无则重新申请,同样链接到内存池头节点的小内存块链表中。重置内存池,将内存池的大块内存释放,然后重置每个小块内存。主要数据结构:内存池头的结构体:ngx_pool_s struct ngx_pool_s { ngx_pool_data_t d; // 内存池当前的数据区指针的结构体 size_t max; // 当前数据块最大可分配的内存大小(Bytes) ngx_pool_t *current; // 当前正在使用的数据块的指针 ngx_pool_large_t *large; // pool 中指向大数据块的指针(大数据快是指 size > max 的数据块) typedef struct ngx_pool_s ngx_pool_t;小内存块的结构体: ngx_pool_data_ttypedef struct { u_char *last; // 保存当前数据块中内存分配指针的当前位置。每次Nginx程序从内存池中申请内存时,从该指针保存的位置开始划分出请求的内存大小,并更新该指针到新的位置。 u_char *end; // 保存内存块的结束位置 ngx_pool_t *next; // 内存池由多块内存块组成,指向下一个数据块的位置。 ngx_uint_t failed; // 当前数据块内存不足引起分配失败的次数 } ngx_pool_data_t;大内存块的结构体:ngx_pool_large_sstruct ngx_pool_large_s { ngx_pool_large_t *next; // 指向下一块大内存块的指针 void *alloc; // 大内存块的起始地址 typedef struct ngx_pool_large_s ngx_pool_large_t;创建内存池:ngx_create_poolngx_pool_t * ngx_create_pool(size_t size) ngx_pool_t *p; //开辟空间 p = ngx_memalign(NGX_POOL_ALIGNMENT, size); if (p == NULL) { return NULL; p->d.last = (u_char *) p + sizeof(ngx_pool_t);//指向实际要分配内存的位置,也就是跳过了前面那些 p->d.end = (u_char *) p + size;//指向末端 p->d.next = NULL; p->d.failed = 0; size = size - sizeof(ngx_pool_t); p->max = (size < NGX_MAX_ALLOC_FROM_POOL) ? size : NGX_MAX_ALLOC_FROM_POOL;//检测剩余空间 p->current = p;//正在工作(分配)的内存块 p->large = NULL; return p; }其中负责开辟空间的函数ngx_memalign,其实就是将malloc进行了简单的封装。这里为了省事,将Nginx源代码简化。//将Nginx源代码简化 #define ngx_memalign(alignment, size) ngx_alloc(size) //开辟空间,将malloc进行简单的封装。 void * ngx_alloc(size_t size) void *p; p = malloc(size); if (p == NULL) { fprintf(stderr,"malloc(%zu) failed", size); if(debug) fprintf(stderr, "malloc: %p:%zu", p, size); return p; }从内存池中申请空间:ngx_pallocvoid * ngx_palloc(ngx_pool_t *pool, size_t size) #if !(NGX_DEBUG_PALLOC) if (size <= pool->max) { return ngx_palloc_small(pool, size, 1);//如果要分配小内存块-从池子中取 #endif return ngx_palloc_large(pool, size);//如果要分配大内存块-扩展 }申请小内存空间:ngx_palloc_small//从内存池头结点中所挂的一块一块普通内存块中搜索看是否有容量够的。 static inline void * ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align){ u_char *m; ngx_pool_t *p; p = pool->current; m = p->d.last; if (align){//是否边界对齐 //对齐到为是对齐数的倍数 m = ngx_align_ptr(m, NGX_ALIGNMENT); //剩余内存是否够用 if ((size_t) (p->d.end - m) >= size) { p->d.last = m + size; return m; p = p->d.next;//切换到下一块进行剩余容量判断 } while (p); //实在不行了,重新创建内存块。 return ngx_palloc_block(pool, size); }创建普通内存块:ngx_palloc_blockstatic void * ngx_palloc_block(ngx_pool_t *pool, size_t size){ u_char *m; size_t psize; ngx_pool_t *p, *new; psize = (size_t) (pool->d.end - (u_char *) pool); m = ngx_memalign(NGX_POOL_ALIGNMENT, psize); if (m == NULL) { return NULL; new = (ngx_pool_t *) m; new->d.end = m + psize; new->d.next = NULL; new->d.failed = 0; m += sizeof(ngx_pool_data_t); m = ngx_align_ptr(m, NGX_ALIGNMENT); new->d.last = m + size; //将内存池中链接的所有结点都遍历一遍,将它们的尝试次数+1,大于4的直接淘汰。 for (p = pool->current; p->d.next; p = p->d.next) { if (p->d.failed++ > 4) { pool->current = p->d.next;// >4淘汰掉 p->d.next = new; return m; }申请大空间(大内存块):ngx_palloc_largestatic void * ngx_palloc_large(ngx_pool_t *pool, size_t size){ void *p; ngx_uint_t n; ngx_pool_large_t *large; p = ngx_alloc(size);//申请一块新的够用大空间 if (p == NULL) { return NULL; n = 0; //先找有没有之前不用的闲置大内存。 for (large = pool->large; large; large = large->next) {//遍历池子的大内存链表 if (large->alloc == NULL) { large->alloc = p; return p; if (n++ > 3) {//就尝试找4次 break; //创建一个大内存结点 large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);//第一次肯定会直接执行这个 if (large == NULL) { ngx_free(p); return NULL; //类似链表头插法 large->alloc = p; large->next = pool->large; pool->large = large; return p; }销毁内存池:ngx_destroy_pool//销毁内存池 ngx_destroy_pool(ngx_pool_t *pool) ngx_pool_t *p, *n; ngx_pool_large_t *l; #if (NGX_DEBUG) for (l = pool->large; l; l = l->next) { fprintf(stderr,"free: %p", l->alloc); for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { fprintf(stderr,"free: %p, unused: %zu", p, p->d.end - p->d.last); if (n == NULL) { break; #endif //真正的销毁处理流程 for (l = pool->large; l; l = l->next) {//处理大块 if (l->alloc) { ngx_free(l->alloc); for (p = pool, n = pool->d.next; /* void */; p = n, n = n->d.next) { ngx_free(p); if (n == NULL) { break; }清理大内存块:ngx_pfreengx_int_t ngx_pfree(ngx_pool_t *pool, void *p) ngx_pool_large_t *l; //遍历寻找目标-大内存块p for (l = pool->large; l; l = l->next) { if (p == l->alloc) { fprintf(stderr,"free: %p", l->alloc); ngx_free(l->alloc); l->alloc = NULL; return NGX_OK; return NGX_DECLINED; 清理内存块:ngx_free //ngx_free 为free的别名 #define ngx_free free重置内存池:ngx_reset_poolvoid ngx_reset_pool(ngx_pool_t *pool) ngx_pool_t *p; ngx_pool_large_t *l; //释放大块 for (l = pool->large; l; l = l->next) { if (l->alloc) { ngx_free(l->alloc); //重置每个小块 for (p = pool; p; p = p->d.next) { p->d.last = (u_char *) p + sizeof(ngx_pool_t); p->d.failed = 0; pool->current = pool; //pool->chain = NULL; pool->large = NULL; 示例:#include "mem_core.h" #define BLOCK_SIZE 16 //每次分配内存块大小 #define MEM_POOL_SIZE (1024 * 4) //内存池每块大小 int main(int argc, char **argv){ int i = 0, k = 0; int use_free = 0; ngx_pagesize = getpagesize(); if(argc >= 2){//不使用线程池 use_free = 1; printf("use malloc/free\n"); } else {//使用线程池 printf("use mempool.\n"); if(!use_free){ char * ptr = NULL; for(k = 0; k< 1024 * 500; k++){ ngx_pool_t * mem_pool = ngx_create_pool(MEM_POOL_SIZE);//创建 for(i = 0; i < 1024 ; i++){ ptr = ngx_palloc(mem_pool,BLOCK_SIZE);//申请 if(!ptr) fprintf(stderr,"ngx_palloc failed. \n"); else {//模拟使用 *ptr = '\0'; *(ptr + BLOCK_SIZE -1) = '\0'; ngx_destroy_pool(mem_pool); } else { char * ptr[1024]; for(k = 0; k< 1024 * 500; k++){ for(i = 0; i < 1024 ; i++){ ptr[i] = malloc(BLOCK_SIZE); if(!ptr[i]) fprintf(stderr,"malloc failed. reason:%s\n",strerror(errno)); else{ *ptr[i] = '\0'; *(ptr[i] + BLOCK_SIZE - 1) = '\0'; for(i = 0; i < 1024 ; i++){ if(ptr[i]) free(ptr[i]); return 0; 对比:补充在C语言中void类型的指针可以直接赋值给其它指针类型, 但是在C++中不行,必须将void*强制转换为对应类型再赋值。例如:malloc默认返回void*,这允许用于分配任何类型。C语言中的malloc函数的返回值就是一个void *型指针,我们可以把它直接赋给一个其他类型的指针,但从安全的编程风格角度以及兼容性上讲,最好还是将返回的指针强制转换为所需的类型。参考文章:C语言指针的初始化和赋值void pointer in C在C语言风格的字符串中,手动添加一个'\0',用printf打印输出时会截至到第一个\0,也就是遇到\0停止,但是实际的大小并不会改变。如下代码所示:#include <stdio.h> int main(void){ char c[] = "123"; printf("%d\n",sizeof(c));//4 c[1] = '\0'; printf("%d\n",sizeof(c));//4 printf("%s\n",c);//1 return 0;
进程间的通信-共享内存共享内存机制共享内存机制是允许两个或多个进程(不相关或有亲缘关系)访问同一逻辑内存的机制。它是共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。两种常用的共享内存方式System V版本的共享内存 shmm多进程直接共享内存文件映射mmap如果一个文件需要频繁进行读写,那么将它映射到内存中。将特殊文件进行匿名内存映射,为有关联的进程提供共享内存空间。为无关联的进程提供共享内存空间,将一个普通文件映射到内存中。System V版本的共享内存 shmm原理:利用共享内存完成进程间通信,两个进程都可以通过虚拟地址空间到用户页表,然后通过用户级页表映射到物理内存的相同一块内存区域。共享内存的使用ftok作用:创建一个唯一的key,来标识一块共享内存。(用不用都行)函数原型:key_t ftok(const char *pathname,int proj_id)参数:使用下面两个创建一个唯一的keypathname:proj_id:返回值:返回一个key。shmget作用:创建一个共享内存块,返回这个共享内存块的标识符shmid。函数原型:int shmget(key_t key,size_t size,int shmflg)参数说明:key:由ftok函数生成。size:申请的共享内存的大小,为4k的整数倍。因为x86系列的cpu在linux系统下面基本上,内存也都是以4kb为单位。shmflg:权限标识。详见-shmget(2) — Linux manual pageIPC_CREAT:不存在则创建。IPC_EXCL:与IPC_CREAT一起使用,若以存在则创建失败。返回值:返回创建的这个共享内存块的标识符shmid相关参考shmget(2) — Linux manual pageshmat作用:挂接共享内存,将共享内存挂接到当前进程的地址空间,即允许本进程访问一块共享内存。函数原型:void shmat(int shmid,const void shmaddr, int shmflg)参数:shmid:挂接的共享内存ID,由shmget函数获得。shmaddr: 一般为0,表示连接到由内核选择的第一个可用地址上。shmflg:标记,一般为0。返回值:指向内存地址的指针。相关参考:shmat 函数的使用shmdt作用:取消共享内存映射函数原型:int shmdt(const void *shmaddr);参数:shmaddr:由shmat返回的指向内存的指针。返回值:成功,则返回0失败,则返回-1,并设置errno。相关参考:shmdt-Linux API速查手册shmctl作用: 用于控制共享内存。函数原型:int shmctl(int shmid, int cmd, struct shmid_ds *buf);参数: 详见shmctl(2) - Linux man pageshmid:共享内存标识码,由shmget获取的。cmd:将要采取的动作(可取值:IPC_STAT、IPC_SET、IPC_RMID...)。buf:指向着一个保存着共享内存的模式状态和访问权限的数据结构。相关参考:shmctl(2) - Linux man page示例shmwrite.c#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/shm.h> struct Conn_stat{ int count; char ip[64]; int main(void){ void *shm = NULL; int shmid = 0;//存储共享内存的标识 struct Conn_stat stat = {0,"127.0.0.1"}; //创建共享内存 shmid = shmget((key_t)1234,sizeof(struct Conn_stat),0666 | IPC_CREAT); if(shmid == -1){ fprintf(stderr, "shmget failed\n"); exit(1); //将共享内存挂接到当前进程的地址空间 shm = shmat(shmid,(void*)0,0); if(shm == (void*)-1){ fprintf(stderr, "shmat failed\n"); exit(2); printf("Memory attached at %p\n", shm); //设置共享内存 struct Conn_stat *p = (struct Conn_stat*)shm; memcpy(p,&stat,sizeof(struct Conn_stat)); //修改共享内存中的数据 int i = 0; while((i++) < 50){ p->count++; sleep(1); //把共享内存从当前进程中分离 if(shmdt(shm) == -1){ fprintf(stderr, "shmdt failed\n"); exit(3); return 0; }shmread.c#include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <sys/shm.h> #include <string.h> #include <errno.h> struct Conn_stat{ int count; char ip[64]; int main(void){ void *shm = NULL;//分配的共享内存的原始首地址 struct Conn_stat *stat = NULL; int shmid;//共享内存标识符 //创建共享内存 shmid = shmget((key_t)1234, sizeof(struct Conn_stat), 0666|IPC_CREAT); if(shmid == -1){ fprintf(stderr, "shmget failed\n"); exit(0); //将共享内存挂接到当前当前进程的地址空间 shm = shmat(shmid, 0, 0); if(shm == (void*)-1){ fprintf(stderr, "shmat failed\n"); exit(1); printf("\nMemory attached at %p\n", shm); //设置共享内存 stat = (struct Conn_stat*)shm; int i = 0; while((i++) < 10){ printf("ip = %s ,count: %d\t\t\n", stat->ip, stat->count); sleep(1); //把共享内存从当前进程中分离 if(shmdt(shm) == -1){ fprintf(stderr, "shmdt failed\n"); exit(2); //删除共享内存 if(shmctl(shmid, IPC_RMID, 0) == -1){ fprintf(stderr, "shmctl(IPC_RMID) failed, reason: %s\n",strerror(errno)); exit(3); return 0; }存储映射共享I/O mmap原理:将一个普通文件或者其它对象映射进内存。使用普通文件提供的内存映射。使用特殊文件提供匿名内存映射。使用mmap作用:mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。函数原型: void mmap(void addr, size_t length, int prot, int flags,int fd, off_t offset);参数:addr:指向欲映射的内存起始地址,通常设为NULL,代表让系统自动选定地址,映射成功后返回该地址。length:将文件中多大的部分映射到内存。prot:映射区域的保护方式。可以为以下几种方式的组合:PROT_EXEC:执行PROT_READ:读取PROT_WRITE:写入PROT_NONE:不能存取flags:影响映射区域的各种特性。MAP_SHARED:映射区域数据与文件对应,允许其他进程共享。MAP_PRIVATE:映射区域生成文件的copy,修改不同步文件。MAP_ANONYMOUS:建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。MAP_DENYWRITE:允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。MAP_LOCKED:将映射区域锁定住,这表示该区域不会被置swap。fd: 要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。offset:文件映射的偏移量,通常设置为0。代表从文件最前方开始对应,offset必须是分页大小的整数倍。返回值:成功:返回指向映射区域的指针。失败:MAP_FAILED,其值为(void *)-1,并设置errno。相关参考:认真分析mmap:是什么 为什么 怎么用munmap作用:解除映射。函数原型: int munmap(void *addr, size_t length); 参数:addr: 由mmap返回的地址。length:映射区的大小。返回值:成功:返回0。失败:返回-1,并设置errno。相关参考:认真分析mmap:是什么 为什么 怎么用示例map_write.c#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<fcntl.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/mman.h> #include<string.h> struct Conn_stat{ int count; char ip[64]; //这个进程创建映射区进行写 int main(int argc,char* argv[]){ if(argc != 2){ printf("Usage: %s file.\n",argv[0]); exit(1); struct Conn_stat stat = {0,"127.0.0.1"}; //打开文件 int fd = open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0644); if(fd<0){ perror("open"); exit(2); //将文件截断为第二个参数这么大 ftruncate(fd,sizeof(struct Conn_stat)); //创建一个结构体大小的共享映射区。共享映射区我们可以当做数组区看待。 struct Conn_stat *p = (struct Conn_stat*)mmap(NULL,sizeof(struct Conn_stat),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(p == MAP_FAILED){ perror("mmap"); exit(3); close(fd);//关闭不用的文件描述符 memcpy(p,&stat,sizeof(struct Conn_stat)); while(p->count < 30){ p->count++; sleep(1); //解除映射 int ret = munmap(p,sizeof(struct Conn_stat)); if(ret < 0){ perror("mmumap"); exit(4); return 0; }map_read.c#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<fcntl.h> #include<sys/types.h> #include<sys/stat.h> #include<sys/mman.h> struct Conn_stat{ int count; char ip[64]; int main(int argc,char *argv[]){ if(argc != 2){ printf("Usage: %s file.\n",argv[0]); exit(1); //打开文件-只读,注意与写中的参数不同 int fd = open(argv[1],O_RDONLY,0644); if(fd < 0){ perror("open"); exit(2); struct Conn_stat stat; //注意与写中的参数不同 struct Conn_stat *p = (struct Conn_stat*)mmap(NULL,sizeof(struct Conn_stat),PROT_READ,MAP_SHARED,fd,0); if(p == MAP_FAILED){ perror("mmap"); exit(3); close(fd); int i = 0; while((i++) < 10){ printf("ip = %s ,count: %d\t\t\n",p->ip,p->count); sleep(1); //接触映射 int ret = munmap(p,sizeof(stat)); if(ret < 0){ perror("mmumap"); exit(4); return 0;
进程间的通信-信号量信号量就类似与马路上的红绿灯,来控制人们在各个路口朝各个方向上的行进,从而更好地有规划的使用这条道路。在程序中,信号则对进程们的执行进行控制。什么是信号量问题:在程序中,有时会存在一种特殊代码,同一时间只允许一个进程执行该部分代码。这部分区域,被称为"临界区"。然后在多进程并发执行中,当一个进程进入临界区,因某种原因被挂起时,其他进程就有可能也进入该区域。解决办法:——使用信号量。什么是信号量?信号量是一种特殊的变量。我们只能对信号量执行P操作和V操作。P操作:申请资源。如果信号量的值>0,则把该信号量-1。如果信号量的值=0,则挂起该进程。V操作:释放资源。如果有进程因该信号量而被挂起,则恢复当前进程运行。如果没有进程因该信号量而被挂起,则把该信号量+1。注意:P操作、V操作都是原子操作,即,其在执行期间,不会被中断。这里指的信号量是指System V IPC的信号量,与线程所使用的信号量不同。该信号量用于进程间通信。信号量的使用信号量的获取semget函数原型:int semget(key_t key, int nsems, int semflg);功能:获取一个已存在的、或创建一个新的信号量,并返回该信号量的标识符。参数:key:键值,该键值对应一个唯一的信号量。类似于共享内存的键值。不同的可通过该键值和semget获取唯一的信号量。特殊键值——IPC_PRIVAT,该信号量只允许创建者进程才可以访问,可用于父子进程间通信。nsems:需要的信号量数目,一般为1。semflag:访问权限。若设置为IPC_CREAT,则如果该信号量未存在,则创建该信号量,如果该信号量已经存在,也不会发生错误。返回值:成功:返回一个正整数。失败:返回-1。信号量的操作semop函数原型:int semop(int semid, struct sembuf *sops, unsigned nsops);功能:改变信号量的值,即对信号量执行P操作、V操作。参数:semid:信号量标识符,即semget函数的返回值。sops:是一个数组,元素类型为struct sembuf。struct sembuf { short sem_num; //信号量组中的编号(即指定对哪个信号量操作) //semget实际是获取一组信号量 //信号量组中的编号从0开始 short sem_op; //操作类型:-1表示P操作; 1表示V操作 short sem_flg; // 通常为SEM_UNDO,使操作系统跟踪信号, // 并在进程没有释放该信号量而终止时,操作系统释放信号量 } nsops:表示第二个参数sops所表示的数组大小,即有几个struct sembuf。返回值:成功:返回0。失败:返回-1。相关参考与补充:Linux进程间通信(五):信号量 semget()、semop()、semctl() 信号量的控制semctl函数原型:int semctl(int semid, int sem_num, int cmd, ...);功能:对信号量进行控制。参数:semid:信号量标识符。sem_num:信号量组中的编号,如果只有一个信号量,则取0。cmd:通常是下面两个值的其中一个。SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。参数四类型为:union semun。union semun { int val; // SETVAL命令要设置的值 struct semid_ds *buf; unsigned short *array; }union semun有些Linux发行版在sys/sem.h中定义,有些则没有定义,可自行定义:#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED) #else union semun { int val; struct semid_ds *buf; unsigned short int *array; struct seminfo *__buf; #endif 返回值:略,详见-semctl(2) — Linux manual page相关参考与补充:Linux进程间通信(五):信号量 semget()、semop()、semctl() 示例示例1:不使用信号量,并发执行多个程序,观察对临界区的访问。#include <stdlib.h> #include <stdio.h> int main(void) { int i; pid_t pd = fork(); for (i=0; i<5; i++) { /* 模拟临界区----begin */ printf("Process(%d) In\n", getpid()); sleep(1); printf("Process(%d) Out\n", getpid()); /* 模拟临界区----end */ sleep(1); return 0; }可见并不是我们想要的效果,我们想要的是一个进去了,另外一个就不可以进去了,出去一个,另外一个才可以进去。示例2:使用信号量,并发指定多个进程,观察对临界区的访问。#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdlib.h> #include <stdio.h> #if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED) #else union semun { int val; struct semid_ds *buf; unsigned short int *array; struct seminfo *__buf; #endif //信号的初始化 static sem_initial(int semid){ int ret; union semun semun; semun.val = 1; ret = semctl(semid,0,SETVAL,semun); if(ret == -1){ fprintf(stderr, "semctl failed!\n"); return ret; //将p v操作封装成函数 //p操作 static int sem_p(int semid){ int ret; struct sembuf sembuf; sembuf.sem_op = -1;//操作类型,设置为-1即p操作。 sembuf.sem_num = 0;//指定信号量在信号量组中的编号 sembuf.sem_flg = SEM_UNDO;//让操作系统跟踪信号,如果进程忘记释放,则操作系统进行负责释放。 ret = semop(semid,&sembuf,1);//根据设置对信号量进行操作 if (ret == -1) { fprintf(stderr, "sem_p failed!\n"); return ret; //v操作 static int sem_v(int semid){ int ret; struct sembuf sembuf; sembuf.sem_op = 1;//操作类型,设置为1即v操作。 sembuf.sem_num = 0;//指定信号量在信号量组中的编号 sembuf.sem_flg = SEM_UNDO;//让操作系统跟踪信号,如果进程忘记释放,则操作系统进行负责释放。 ret = semop(semid,&sembuf,1);//根据设置对信号量进行操作 if (ret == -1) { fprintf(stderr, "sem_v failed!\n"); return ret; int main(int argc, char* argv[]) { int semid; //获取信号 semid =semget((key_t)1234,1,0666 | IPC_CREAT); if(semid == -1){ printf("semget failed!\n"); exit(1); //信号量的初始化 if (argc > 1) { int ret = sem_initial(semid); if (ret == -1) { exit(1); for(;;){ if(sem_p(semid) == -1){//p操作,申请,若无可用资源,则挂起等待。 exit(1); /* 模拟临界区----begin */ printf("Process(%d) In\n", getpid()); sleep(3); printf("Process(%d) Out\n", getpid()); /* 模拟临界区----end */ if(sem_v(semid) == -1){//v操作,释放。 exit(1); return 0; }可以看到,一个出来,另一个才可以进去。
进程间的通信-消息队列什么是消息队列?消息队列,用于从一个进程向另一个进程发送数据。但仅仅把数据发送到一个"队列"中,而不指定由哪个进程来接收。消息队列独立于发送消息的进程和接收消息的进程。每个消息队列都有一个标识,只有持有这个标识的进程才可以去里面拿消息。消息队列有最大长度限制:MSGMNB。消息队列中的单条消息最大长度限制:MSGMAX。消息队列的获取msgget作用:获取或创建一个消息队列。函数原型: int msgget(key_t key, int msgflg);参数:key:消息队列标识。msgflg:访问权限。IPC_CREAT——如果key不存在,则创建,类似open函数的O_CREAT。(来源详见补充1)IPC_EXCL——如果key存在,则返回失败,类似open函数的O_EXCL。(来源详见补充1)返回值:成功:正整数,即消息队列标识符。失败:返回-1,并设置errno。参考补充:Linux API速查手册-msggetmsgget(2) — Linux manual page消息的发送msgsnd作用:发送一个消息,即把一个消息放到某一个消息队列中。函数原型:int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);参数:msgid:消息队列标识符。msgp:消息指针。消息的类型需要自己的定义。但要求其第一个结构成员为long int。例如:struct my_msg_st { long int msg_type; /* 消息的类型,取>0, 接收消息时可使用该值 */ /*other info */ } msgsz:消息的长度(不包含第一个成员msg_type)msgflg:IPC_NOWAIT:如果包含此选项,则消息队列满时,不发送该消息,立即返回-1。反之,如果不包含此选项,则消息队列满时,挂起本进程,直到消息队列由空间可用。返回值:成功:返回0。失败:返回-1。参考补充:Linux manual page消息的接收msgrcv函数原型:ssize_t msgrcv (int msqid, void *msgp, size_t msgsz, long msgtype, int msgflg);功能: 从消息队列中接收一条消息。参数msgid: 消息队列标识符。msgp: 用于接收消息的缓存。msgsz:要接收的消息长度(不包括其第一个成员)msgtype: 指定接收消息的类型0:从消息队列中获取第一个消息,以实现顺序接收(先发现收)。大于0:从消息队列中获取相同类型的第一个消息。小于0:从消息队列中获取消息类型小于等于msgtype绝对值的第一个消息。msgflg:如果包含 IPC_NOWAIT,则当前消息队列汇总没有指定类型的消息时,立即返回-1。如果不包含 IPC_NOWAIT,则当消息队列中没有指定类型的消息时,挂起本进程,直到收到指定类型的消息。返回值:成功:返回接收到的消息的长度(不包含第一个成员msg_type)。失败: 返回-1。消息的控制msgctl函数原型:int msgctl(int msqid, int cmd, struct msqid_ds *buf);功能:用来对消息队列的基本属性进行控制、修改。参数:msqid:消息队列标识符。cmd: 执行的控制命令。详见补充。buf:详见补充。返回值:成功:返回0。失败:返回-1。补充:msgctl(2) — Linux manual page示例示例1:程序1:发送消息到消息队列,程序2进行接收。msg_send1.c#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define MSG_SIZE 80 struct my_msg_st { long int msg_type; char msg[MSG_SIZE]; int main(void){ int msgid; int ret; struct my_msg_st msg; msgid = msgget((key_t)1235, 0666|IPC_CREAT); if (msgid == -1) { printf("msgget failed!\n"); exit(1); msg.msg_type = 1; strcpy(msg.msg, "Hello world!");//要发送的内容 ret = msgsnd(msgid, &msg, MSG_SIZE, 0); if (ret == -1) { printf("msgsnd failed!\n"); exit(1); return 0; }msg_recv2.c#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #define MSG_SIZE 80 struct my_msg_st { long int msg_type; char msg[MSG_SIZE]; int main(void) int msgid; int ret; struct my_msg_st msg; msgid = msgget((key_t)1235, 0666|IPC_CREAT); if (msgid == -1) { printf("msgget failed!\n"); exit(1); msg.msg_type = 0; ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0); if (ret == -1) { printf("msgrcv failed!\n"); exit(1); printf("received: %s\n", msg.msg); ret = msgctl(msgid, IPC_RMID, 0);//全局性的删除-IPC_RMID if (ret == -1) { printf("msgctl(IPC_RMID) failed!\n"); exit(1); return 0; }示例2:进程1:循环等待用户输入字符串,将收到的每个字符串发送给进程2,直到用户输入exit。进程2:接收进程1发来的信息,并打印输出,直到接收到exit。msg_send.c#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define MSG_SIZE 80 struct my_msg_st { long int msg_type; char msg[MSG_SIZE]; int main(void){ int msgid; int ret; struct my_msg_st msg; msgid = msgget((key_t)1235, 0666|IPC_CREAT); if (msgid == -1) { printf("msgget failed!\n"); exit(1); while(1) { fgets(msg.msg, sizeof(msg.msg), stdin); msg.msg_type = 1; ret = msgsnd(msgid, &msg, MSG_SIZE, 0); if (ret == -1) { printf("msgsnd failed!\n"); exit(1); if (strncmp(msg.msg, "exit", 4) == 0) { break; return 0; }msg_recv.c#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #define MSG_SIZE 80 struct my_msg_st { long int msg_type; char msg[MSG_SIZE]; int main(void){ int msgid; int ret; struct my_msg_st msg; //第二个参数为权限控制 msgid = msgget((key_t)1235, 0666|IPC_CREAT); if (msgid == -1) { printf("msgget failed!\n"); exit(1); while(1) { msg.msg_type = 0; ret = msgrcv(msgid, &msg, MSG_SIZE, 0, 0); if (ret == -1) { printf("msgrcv failed!\n"); exit(1); printf("received: %s\n", msg.msg); if (strncmp(msg.msg, "exit", 4) == 0) { break; ret = msgctl(msgid, IPC_RMID, 0); if (ret == -1) { printf("msgctl(IPC_RMID) failed!\n"); exit(1); return 0;
项目日志——Log4cpp开源的C++类库Log4cpp提供了很多功能,帮助我们的应用程序更方便地记录日志。安装与构建Log4cpp官网——http://log4cpp.sourceforge.net/wget https://nchc.dl.sourceforge.net/project/log4cpp/log4cpp1.1.x%20%28new%29/log4cpp-1.1/log4cpp-1.1.3.tar.gztar zxvf log4cpp-1.1.3.tar.gz cd log4cpp # root用户 ./configure --prefix=执行路径 # ./configure --prefix=/home/xuanxuan/share_bike/third/lib/log4cpp make install使用包含头文件#include <log4cpp/Category.hh> #include <log4cpp/FileAppender.hh> #include <log4cpp/PatternLayout.hh> #include <log4cpp/OstreamAppender.hh>初始化日志输出的目的地示例1:输出到标准输出std::coutlog4cpp::Appender *appender = new log4cpp::OstreamAppender("root", &std::cout);示例2:输出到log文件log4cpp::Appender *appender = new log4cpp::FileAppender("root", "test.log");输出方式如下:log4cpp::FileAppender // 输出到文件 log4cpp::RollingFileAppender // 输出到回卷文件,即当文件到达某个大小后回卷 log4cpp::OstreamAppender // 输出到一个ostream类 log4cpp::RemoteSyslogAppender // 输出到远程syslog服务器 log4cpp::StringQueueAppender // 内存队列 log4cpp::SyslogAppender // 本地syslog log4cpp::Win32DebugAppender // 发送到缺省系统调试器 log4cpp::NTEventLogAppender // 发送到win 事件日志注意:实际上日志输出到终端或文件中是很慢的,可能会引起IO中断,所以我们可以先输出到内存中(StringQueueAppender),然后在输出到其它地方,这样效率较高。设置日志输出格式log4cpp::PatternLayout *patternLayout = new log4cpp::PatternLayout(); patternLayout->setConversionPattern("%d [%p] - %m%n"); appender->setLayout(patternLayout);日志输出格式控制如下:log4cpp::PatternLayout Class Reference%%%% - a single percent sign %c - the category %d - the date Date format: The date format character may be followed by a date format specifier enclosed between braces. For example, %d{%H:%M:%S,%l} or %d{%d %m %Y %H:%M:%S,%l}. If no date format specifier is given then the following format is used: "Wed Jan 02 02:03:55 1980". The date format specifier admits the same syntax as the ANSI C function strftime, with 1 addition. The addition is the specifier %l for milliseconds, padded with zeros to make 3 digits. %m - the message %n - the platform specific line separator %p - the priority %r - milliseconds since this layout was created. %R - seconds since Jan 1, 1970 %u - clock ticks since process start %x - the NDC设置输出类别和日志优先级log4cpp::Category &root = log4cpp::Category::getRoot(); root.setPriority(log4cpp::Priority::NOTICE); root.addAppender(appender);日志的级别如下:NOTSET < DEBUG < INFO < NOTICE < WARN < ERROR < CRIT < ALERT < FATAL = EMERG。低于该级别的日志不会被记录。定义宏来减少代码量#define LOG(__level) log4cpp::Category::getRoot() << log4cpp::Priority::__level << __FILE__ << " " << __LINE__ << ": "使用:LOG(DEBUG)<<"DEBUG.";测试什么是单例?程序中仅有一个的对象。配置文件:log.conf#定义Root category的属性 log4cpp.rootCategory=DEBUG, RootLog #定义RootLog属性 log4cpp.appender.RootLog=RollingFileAppender log4cpp.appender.RootLog.layout=PatternLayout log4cpp.appender.RootLog.layout.ConversionPattern=%d{%m-%d %H:%M:%S %l} [%t][%p]%m%n log4cpp.appender.RootLog.fileName=/var/log/share_bike.log log4cpp.appender.RootLog.maxFileSize=268435456 #256MB log4cpp.appender.RootLog.fileNamePattern=share_bike_%i.log log4cpp.appender.RootLog.maxBackupIndex=256封装为一个类Logger.h#ifndef DISTRIBUTED_LOGGER_H_ #define DISTRIBUTED_LOGGER_H_ #include <string> #include <log4cpp/Category.hh> class Logger{ public: bool init(const std::string& log_conf_file); static Logger* instance(){//返回单例——全局唯一对象 return &instance_; log4cpp::Category* GetHandle(){ return category_; protected: static Logger instance_; log4cpp::Category* category_; #define LOG_INFO Logger::instance()->GetHandle()->info #define LOG_DEBUG Logger::instance()->GetHandle()->debug #define LOG_ERROR Logger::instance()->GetHandle()->error #define LOG_WARN Logger::instance()->GetHandle()->warn #endifLogger.cpp#include "Logger.h" #include <iostream> #include <log4cpp/Category.hh> #include <log4cpp/FileAppender.hh> #include <log4cpp/PatternLayout.hh> #include <log4cpp/OstreamAppender.hh> #include <log4cpp/PropertyConfigurator.hh> Logger Logger::instance_; bool Logger::init(const std::string& log_conf_file){ log4cpp::PropertyConfigurator::configure(log_conf_file); }catch(log4cpp::ConfigureFailure& f){ std::cerr << " load log config file " << log_conf_file.c_str() << " failed with result : " << f.what()<< std::endl; return false; category_ = &log4cpp::Category::getRoot(); return true; }测试main.cpp#include "iniconfig.h" //注意路径,会在CMake中进行调整 #include "configdef.h" #include "Logger.h" int main(int argc,char** argv){ if(argc != 3){//缺少参数 printf("Please input shbk <config file path> <log file config>!\n"); return -1; if(!Logger::instance()->init(std::string(argv[2]))){ fprintf(stderr,"init log module failed.\n"); return -2; Iniconfig config; if(!config.loadfile(std::string(argv[1]))){ //printf("load %s failed.\n",argv[1]); LOG_ERROR("load %s failed.",argv[1]); return -3; //测试是否读取成功 st_env_config conf_args = config.getconfig(); LOG_INFO("[database]ip:%s\n [database]port:%d\n [database]user:%s\n [database]pwd:%s\n [database]db:%s\n [server]port:%d\n ", conf_args.db_ip.c_str(), conf_args.db_port, conf_args.db_user.c_str(), conf_args.db_pwd.c_str(), conf_args.db_name.c_str(), conf_args.svr_port); return 0; }注意链接库:下面为示例,请根据自身情况调整。TARGET_LINK_LIBRARIES(common log4cpp)TARGET_LINK_LIBRARIES(share_bike pthread) TARGET_LINK_LIBRARIES(${PROJECT_NAME} liblog4cpp.a)执行:./share_bike ../conf/share_bike.ini ../conf/log.conf查看:在log.conf中。/var/log/share_bike.log为保存路径。log4cpp.appender.RootLog.fileName=/var/log/share_bike.log
项目日志——输出重定向前言在通常情况下,Linux/UNIX每个程序在开始运行的时候,都会自动打开三个标准stream(流),分别为标准输入流stdin(文件描述符0),标准输出流stdout(文件描述符1),标准错误输出流stderr(文件描述符2)。Linux的本质就是一切皆文件,输入输出设备也是以文件形式存在和管理的。stderr是不缓存的,stdout则进行行间缓存,即遇到\n才打印输出。示例如下:#include <stdio.h> #include <unistd.h> int main(int argc, char** argv) for(int i = 0; i < 5; i++){ fprintf(stdout, "This is stdout[%d]", i); sleep(1); fprintf(stdout,"\n");//stdout sleep(1); for(int i = 0; i < 5; i++){ fprintf(stderr, "This is stderr[%d]", i); sleep(1); sleep(60); return 0; }可以发现,第一个循环(stdout)的内容会被一次性打印出来,第二个循环(stderr)的内容会一个一个的打印出来。重定向有时我们使用printf来打印调试信息,但是如果终端关掉,我们就无法查看到信息了。所以需要我们保存在文件中,以便永久保存。>将标准输出重定向到文件./test > test.txt将标准输入和标准出错重定向到文件./test 1 > testout.txt ./test 2 > testerr.txt>>以附加方式重定向到文件例如:将标准输入和标准出错以追加的方式重定向到文件中./test > test1.txt 2>&1代码中实现重定向操作#include <stdio.h> int main(void){ FILE *out = freopen("stdout.txt", "w", stdout); printf("%s\n", "redirect test"); return 0; }通常在生产环境下,会将stdin,stdout,stderr这三个流重定向到其他文件。日志中最好加上日期、时间、文件等信息。printf涉及到文件,可能会引起IO中断(会进行一系列的系统调用),因此执行printf比一般指令的效率要低很多。一般情况下不建议用printf。
项目配置工具iniparser前言 对于很多程序中要用的参数如果是可变的,那么最好的处理方式就是通过main函数进行参数传递,或者从别的地方去获取,这其中之一的做法就是使用配置文件,在一个成熟和架构完善的系统中,一般都会做到自动配置,自动部署。 所以有的系统里会有一个单独的配置服务存在,其它的每个服务配置都会从配置中心服务获取。 运维人员会通过操作界面把配置信息下发到配置中心服务,其余服务从配置中心获取变更信息。几乎每个大型互联网项目都会涉及到项目配置。安装与构建git clonegit clone https://github.com/ndevilla/iniparser.gitcd iniparsermake编译生成,.a为静态库,.so为动态库测试本测试项目目录树。share_bike |-- conf | `-- share_bike.ini |-- git | `-- iniparser | |--略 |-- src | |-- CMakeLists.txt | |-- common | | |-- CMakeLists.txt | | |-- configdef.h | | |-- iniconfig.cpp | | |-- iniconfig.h | |-- main.cpp |-- test `-- third |-- include | `-- iniparser | |-- dictionary.h | `-- iniparser.h `-- lib `-- iniparser |-- libiniparser.a `-- libiniparser.so.1注意:ubuntu下可以使用tree命令打印目录树。 treesudo apt-get install tree乱码问题——linux下tree命令产生乱码,通过修改字符集解决/src/common/configdef.h#ifndef SHBK_COMMON_CONFIGDEF_H_ #define SHBK_COMMON_CONFIGDEF_H_ #include <string> typedef struct st_env_config{ //数据库的配置 std::string db_ip; unsigned short db_port; std::string db_user; std::string db_pwd; std::string db_name; //服务器的配置 unsigned short svr_port; //构造函数 st_env_config(){ st_env_config(const std::string& db_ip, unsigned int db_port, const std::string& db_user, const std::string& db_pwd, const std::string& db_name, unsigned short svr_port){ this->db_ip = db_ip; this->db_port = db_port; this->db_user = db_user; this->db_pwd = db_pwd; this->db_name = db_name; this->svr_port = svr_port; st_env_config& operator =(const st_env_config& config){ if (this != &config){ this->db_ip = config.db_ip; this->db_port = config.db_port; this->db_user = config.db_user; this->db_pwd = config.db_pwd; this->db_name = config.db_name; this->svr_port = config.svr_port; return *this; }_st_env_config; #endif/src/common/iniconfig.h#ifndef SHBK_COMMON_INICONFIG_H_ #define SHBK_COMMON_INICONFIG_H_ #include <string> #include "configdef.h" class Iniconfig{ public: Iniconfig(); ~Iniconfig(); //加载配置 bool loadfile(const std::string& path); //获取配置 const st_env_config& getconfig(); private: st_env_config _config; bool _isloaded; #endif //SHBK_COMMON_INICONFIG_H_/src/common/iniconfig.cpp#include "iniconfig.h" #include <iniparser/iniparser.h> Iniconfig::Iniconfig():_isloaded(false){ Iniconfig::~Iniconfig(){ bool Iniconfig::loadfile(const std::string& path){ dictionary* ini = NULL; ini = iniparser_load(path.c_str()); if(ini == NULL){ fprintf(stderr,"cannot parse file:%s\n",path.c_str()); return false; //拿到信息,字典,——区段:区段中的信息,默认值(如果没找到就取这个默认值) const char* ip = iniparser_getstring(ini,"database:ip","127.0.0.1"); const int port = iniparser_getint(ini, "database:port", 3306); const char* user = iniparser_getstring(ini, "database:user", "root"); const char* pwd = iniparser_getstring(ini, "database:pwd", "123456"); const char* db = iniparser_getstring(ini, "database:db", "dongnaobike"); const int sport = iniparser_getint(ini, "server:port", 9090); //信息赋值 _config = st_env_config(std::string(ip), port, std::string(user), std::string(pwd), std::string(db), sport); //释放字典资源 iniparser_freedict(ini); _isloaded = true; return _isloaded; const st_env_config& Iniconfig::getconfig(){ return _config; }/src/common/CMakeLists.txtCMAKE_MINIMUM_REQUIRED(VERSION 2.8) # 搜集所有在指定路径下的源文件的文件名,将输出结果存储在指定的变量中 aux_source_directory(. SOURCES_COMMON_FILES) # 构建库供其他模块使用 ADD_LIBRARY(common ${SOURCES_COMMON_FILES}) # 用来显式的定义变量 SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -rdynamic -Wall -m64 -pipe -std=c++0x -lrt -Wno-reorder -Wdeprecated-declarations") # 将指定目录添加到编译器的有文件搜索路径之下 INCLUDE_DIRECTORIES(../../third/include) # 将指定目录添加到需要链接的库文件目录之下 LINK_DIRECTORIES(../../third/lib/iniparser) # 将目标文件与库文件进行链接 TARGET_LINK_LIBRARIES(common iniparser) TARGET_LINK_LIBRARIES(common dl)/src/main.cpp#include "iniconfig.h" //注意路径,会在CMake中进行调整 #include "configdef.h" int main(int argc,char** argv){ if(argc != 2){//缺少参数 printf("Please input shbk <config file path>!\n"); return -1; Iniconfig config; if(!config.loadfile(std::string(argv[1]))){ printf("load %s failed.\n",argv[1]); return -2; //测试是否读取成功 st_env_config conf_args = config.getconfig(); printf("[database]ip:%s\n [database]port:%d\n [database]user:%s\n [database]pwd:%s\n [database]db:%s\n [server]port:%d\n ", conf_args.db_ip.c_str(), conf_args.db_port, conf_args.db_user.c_str(), conf_args.db_pwd.c_str(), conf_args.db_name.c_str(), conf_args.svr_port); return 0; }/src/CMakeLists.txt# CMake要求最低版本 CMAKE_MINIMUM_REQUIRED(VERSION 2.8) # 工程名 PROJECT(share_bike) # 将指定目录添加到编译器的头文件搜索路径之下 INCLUDE_DIRECTORIES(../third/include) INCLUDE_DIRECTORIES(./common) # 将指定目录添加到需要链接的库文件目录之下 LINK_DIRECTORIES(../third/lib/iniparser) # 刚才在子文件夹中生成的库 LINK_DIRECTORIES(./common) # 搜集所有在指定路径下的源文件的文件名,将输出的结果列表存储在指定的变量中 # 内置变量CMAKE_SOURCE_DIR 定义了顶级CMakeLists.txt所在的文件夹 # PROJECT_SOURCE_DIR 定义了包含最近的project()命令的CMakeLists.txt 所在的文件夹 aux_source_directory(${PROJECT_SOURCE_DIR} SOURCE_FILES) # 使用给定的源文件,为工程引入一个可执行文件 ADD_EXECUTABLE(share_bike ${SOURCE_FILES}) # GCC编译选项 SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -rdynamic -Wall -m64 -pipe -std=c++0x -lrt -Wno-reorder -Wdeprecated-declarations") TARGET_LINK_LIBRARIES(share_bike iniparser) TARGET_LINK_LIBRARIES(share_bike common) # 增加子目录 ADD_SUBDIRECTORY(common) SET(CMAKE_INSTALL_PREFIX ${CMAKE_BINARY_DIR}) INSTALL(TARGETS share_bike DESTINATION bin)/conf/share_bike.ini[database] ip = 127.0.0.1 ; port = 3306 ; user = root ; pwd = 123456 ; db = qiniubike; [server] port = 9090;编译,在/src目录下cmake . make 执行./share_bike ../conf/share_bike.ini
快速上手GoogleTest是Google的一套用于编写 C++测试的框架,可以运行在多个平台上(包括Linux、MacOS X、Windows、Cygwin等)。基于xUnit架构,支持很多好用的特性,包括自动识别测试、丰富的断言、断言自定义、死亡测试、非终止的失败、生成XML报告等等。以下简称GTest。GTest的优点好的测试应包括如下特点。测试应该是独立的、可重复的。一个测试的结果不应该作为另一个测试的前提。GTest中每个测试运行在独立的对象中。如果某个测试失败了,可以单独地调试它。测试应该是有清晰的结构的。GTest的测试有很好的组织结构,易于维护。测试应该是可移植和可复用的。有很多代码是不依赖平台的,因此它们的测试也需要不依赖于平台。GTest可以在多种操作系统、多种编译器下工作,有很好的可移植性。测试失败时,应该给出尽可能详尽的信息。GTest在遇到失败时并不停止接下来的测试,而且还可以选择使用非终止的失败来继续执行当前的测试。这样一次可以测试尽可能多的问题。测试框架应该避免让开发者维护测试框架相关的东西。GTest可以自动识别定义的全部测试,你不需要一一列举它们。测试应该够快。GTest在满足测试独立的前提下,允许你复用共享数据,它们只需创建一次。GTest采用的是xUnit架构,你会发现和JUnit、PyUnit很类似,所以上手非常快。搭建测试框架GTest的项目地址:https://github.com/google/googletest安装流程如下:git clone https://github.com/google/googletest.gitcd googletestmkdir buildcd buildcmake ..makesudo make install注意:如果make过程中报错,可能是gcc版本过低,从官方项目中的issus中我们可以的值,gcc 4.8版本已经不再支持,如下图所示。[linux make[1]: * [googletest/CMakeFiles/gtest.dir/all] Error 2#3639](https://github.com/google/googletest/issues/3639)安装gcc-5(或更高版本),Ubuntu20.04安装gcc-5。成功编译构建。其中,/build/lib目录中会生成:libgmock.a libgmock_main.a libgtest.a libgtest_main.a文件。测试示例sample1.h#ifndef GTEST_SAMPLES_SAMPLE1_H_ #define GTEST_SAMPLES_SAMPLE1_H_ // 阶乘 // Returns n! (the factorial of n). For negative n, n! is defined to be 1. // 返回n!(n的阶乘)。对于负n,n!定义为1。 int Factorial(int n); // 判断是否为质数 // Returns true iff n is a prime number. bool IsPrime(int n); #endif sample1.cc#include "sample1.h" // Returns n! (the factorial of n). For negative n, n! is defined to be 1. int Factorial(int n) { int result = 1; for (int i = 1; i <= n; i++) { result *= i; return result; // Returns true iff n is a prime number. bool IsPrime(int n) { // Trivial case 1: small numbers if (n <= 1) return false; // Trivial case 2: even numbers if (n % 2 == 0) return n == 2; // Now, we have that n is odd and n >= 3. // Try to divide n by every odd number i, starting from 3 for (int i = 3; ; i += 2) { // We only have to try i up to the square root of n if (i > n/i) break; // Now, we have i <= n/i < n. // If n is divisible by i, n is not prime. if (n % i == 0) return false; // n has no integer factor in the range (1, n), and thus is prime. return true; sample1_unittest.cc#include <limits.h> #include "sample1.h" #include <gtest/gtest.h> namespace { //TEST(测试套,测试用例名称) TEST(FactorialTest, Negative) { //调用对应函数,结果是否为1,判断测试用例是否通过 EXPECT_EQ(1, Factorial(-5)); EXPECT_EQ(1, Factorial(-1)); EXPECT_GT(Factorial(-10), 0); TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); TEST(FactorialTest, Positive) { EXPECT_EQ(1, Factorial(1)); EXPECT_EQ(2, Factorial(2)); EXPECT_EQ(6, Factorial(3)); EXPECT_EQ(40320, Factorial(8)); // Tests IsPrime() TEST(IsPrimeTest, Negative) { EXPECT_FALSE(IsPrime(-1)); EXPECT_FALSE(IsPrime(-2)); EXPECT_FALSE(IsPrime(INT_MIN)); TEST(IsPrimeTest, Trivial) { EXPECT_FALSE(IsPrime(0)); EXPECT_FALSE(IsPrime(1)); EXPECT_TRUE(IsPrime(2)); EXPECT_TRUE(IsPrime(3)); TEST(IsPrimeTest, Positive) { EXPECT_FALSE(IsPrime(4)); EXPECT_TRUE(IsPrime(5)); EXPECT_FALSE(IsPrime(6)); EXPECT_TRUE(IsPrime(23)); } // namespace 编译g++ sample1.cc sample1_unittest.cc -lgtest -std=c++14 -lgtest_main -lpthread -o test1注意:本文发布时,GTest已不支持C++11,所支持最低版本为C++14, 所以-std=c++11会报错,错误解决(原issues)——Commit e009c3d fails to build with "error: wrong number of template arguments (0, should be 1),将c++版本指定为c++14。(开始我还以为是我ubuntu的版本太低了,特意换了台机子。)上面的sample1_unittest.cc测试程序中,我们并没有写main函数,因为我们链接了gtest_main这个库。测试时,选择有代表性的用例进行测试。执行./test1测试用例全部通过接着,我们修改一处代码(本次修改判断质数函数中的一处代码,将原来返回false的位置改为ture。),重新编译执行,发现错误。手写main函数int main(int argc, char** argv){ testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }编译:这是编译时就可去掉gtest_main库。g++ sample1.cc sample1_unittest.cc -lgtest -std=c++14 -lpthread -o test1
内存检测工具ValgrindValgrind是运行在Linux上的一套基于仿真技术的程序调试和分析工具,作者是获得过Google-O'Reilly开源大奖的Julian Seward,它包含一个内核——一个软件合成的CPU,和一系列的小工具,每个工具都可以完成一项任务——调试,分析,测试等。内存检测,使用它的Memcheck工具。Valgrind安装官网http://valgrind.orgubuntusudo apt-get install valgrindMemcheck检测范围Memcheck用来检测C/C++程序中出现的内存问题,所有对内存的读写都会被检测到,一切malloc()/free()/new/delete的调用都会被捕获,所以,它能检测以下问题:对未初始化内存的使用;读/写释放后的内存块读/写超出malloc等分配的动态内存范围读/写不适当的栈中内存块内存泄漏,指向一块内存的指针丢失不正确的malloc/free或new/delete匹配memcpy()相关函数中的dst和src指针重叠问题Memcheck检查步骤及注意事项在编译程序的时候打开调试模式(gcc编译器的-g选项),以便显示行号,编译时去掉-O1 -O2等优化选项,检查的是C++程序的时候,考虑加上选项:-fno-inline,这样它的函数调用链接会很清晰。执行:valgrind --tool=memcheck --leak-check=full --log-file=./log.txt ./YourProgram程序运行结束,打开log.txt查看结果。结果分析Valgrind(memcheck)包含这7类错误illegal read/illegal write errors —— 非法读取/非法写入错误use of uninitialised values —— 使用未初始化的区域use of uninitialised or unaddressable values in system calls —— 系统调用时使用了未初始化或不可寻址的地址illegal frees —— 非法的释放when a heap block is freed with an inappropriate deallocation function —— 分配和释放函数不匹配overlapping source and destination blocks —— 源和目的内存块重叠memory leak detection —— 内存泄漏检测Still reachable —— 内存指针还在,还有机会使用或者释放,指针指向的动态内存还没有被释放就退出了Definitely lost —— 确定的内存泄露,已经不能够访问这块内存Indirectly lost —— 指向该内存的指针位于内存泄露处Possibly lost —— 可能的内存泄露,仍然存在某个指针能够访问某块内存,但该指针指向的已经不是该内存首位置Suppressed —— 某些库产生的错误不予以提示,这些错误会被统计到suppressed项目示例产生错误的代码:#include <stdlib.h> void f(void){ int *x = malloc(10*sizeof(int)); x[10] = 0;//越界访问 //内存没释放 int main(void){ return 0; }编译:gcc -g demo.c -o demo检测:valgrind --tool=memcheck --leak-check=full --log-file=./log.txt ./demo查看错误:
signal(signum,SIG_DFL); size = backtrace(array,100); strings = (char**)backtrace_symbols(array,size); fprintf(stderr,"Launcher received SIG:%d Stack trace:\n",signum); for(i = 0;i <size;i++){ fprintf(stderr,"%d %s\n",i,strings[i]); free(strings); int func(int* p){ int y = *p; return y; int main(void){ int *p = NULL; signal(SIGSEGV,handle_segv); signal(SIGABRT,handle_segv); return func(p); }编译:gcc -g demo.c -o demo执行:找到错误代码行号:使用addr2line命令示例:addr2line -a 0x4007b6 -e demo回到我们的源文件,对应的位置。输入最后第一个地址会显示??:0暂时还未解决,还请知道的小伙伴评论区解答下。
Coredump 调试Coredump是什么?Linux环境下,当程序异常退出(发生段错误)时,会产生一个core文件,该文件记录了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成的一个文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。前期设置设置core文件生成的目录,其中%e表示程序文件名,%p表示进程ID,否则会在程序的当前目录生成dore文件。echo /home/xuanxuan/data/coredump/core.%e.%p >/proc/sys/kernel/core_pattern当前执行程序的用户对core目录有写权限且右足够的空间存储core文件。生成不受限制的core文件。ulimit -c unlimited 什么情况下会导致程序异常退出?非法指针的访问,堆栈溢出。如何调试编译的时候添加-g选项,增加调试信息。gdb program core_file示例:一个会产生异常退出的程序,非法指针访问。demo.c#include <stdio.h> int func(int* p){ int y = *p; return y; int main(void){ int *p = NULL; return func(p); }编译,运行,错误已产生。如下图所示。执行调试命令,结果如下图所示。gdb demo /home/xuanxuan/data/coredump/core.demo.3102bt/where命令查看堆栈调用信息如果要查看某一层的信息,你需要切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的就是切换当前。frame/f nframe n f nn是一个从0开始的整数,是栈中的层编号,比如:frame 0,表示栈顶,frame 1,表示栈的第二层。upup n表示向栈的上面移动n层,不指定n,则为向上移动一层。downdown n表示向栈的下面移动n层,不指定n,则为向下移动一层。如若不想打印对应栈层的信息,可以使用如下三个命令。 select-frame n #对应于 frame 命令 up-silently n #对应于 up 命令 down-silently n #对应于 down 命令frame/fframe/f查看当前栈层信息。会打印出:栈的层编号当前的函数名函数参数值函数所在文件及行号函数执行到的语句info frame/f可以查看到更详细的当前栈层的信息,只不过,大多数都是运行时的内存地址。比如:函数地址,被调用函数的地址,目前的函数是由什么样的程序语言写的,函数的参数地址及值,局部变量的地址等。如下图所示。info args打印出当前函数的参数名及其值。info locals打印出当前函数中所有局部变量及其值。
Crow的安装与构建前言项目地址:https://github.com/crowcpp/crow官方网站及文档说明:https://crowcpp.org/master/缘由:前几天一直在寻找一款C++的Web框架,通过不断的在Github、StackOverFlow以及搜索引擎上搜索,最终我还是选择了Crow这一框架,其他相关框架还有,restbed,served,nrgest,微软的C++ REST SDK等,Github相关搜索C++ REST framework...结果在测试用例构建时候,似乎遇到了一个bug,于是我提交了一个Issues,得到了开发人员的回复,最终问题得到了解决,在此,再次感谢项目维护人员的付出!配置环境本篇文章为Linux安装。其他平台或具体最低要求等,还请见官方说明文档。Ubuntu 20.0.4GCC 9.4.0Boost 1.71ASIO 1.22.1Cmake 3.24.0到目前为止,Corw依赖使用的ASIO已经为脱离BOOST的独立版。相关安装参考文章:Boost与独立版的ASIO安装详见——独立版的Asio安装与使用](https://www.cnblogs.com/kolane/p/12057777.html)CMake安装详见——ubuntu安装cmake相关问题解决参考文章:Unable to locate package ——Linux新手入门:Unable to locate package错误解决办法安装Crow从源码安装,详见官方说明文档。git clone https://github.com/CrowCpp/Crow.git进入Crow目录,创建build目录,并进入。mkdir build cd buildcmake .. -DCROW_BUILD_EXAMPLES=OFF -DCROW_BUILD_TESTS=OFFmake installHello world#include <crow.h> int main(){ crow::SimpleApp app; CROW_ROUTE(app, "/test")([](){ return "Hello world"; app.port(18888).multithreaded().run(); }编译g++g++ main.cpp -o main -lpthreadCMake# 指定版本 cmake_minimum_required(VERSION 3.24) # 项目名 project(main) # 使用指定源文件来生成目标可执行文件 add_executable(main main.cpp) # 引入外部依赖包 find_package(Crow REQUIRED) # 目标文件与库文件进行链接 target_link_libraries(main PUBLIC Crow::Crow)测试我的第一个程序 CROW_ROUTE(app, "/low_price_recommend").methods("POST"_method)([](const crow::request& req){ auto x = crow::json::load(req.body); crow::json::wvalue return_ret({});//返回结果 if (!x){//参数不全 return_ret["status_code"] = std::string("0"); return crow::response(return_ret); }else{ return_ret = myfounction.low_price_recommend( x["flight_date"].s(), x["flight_departure"].s(), x["flight_arrival"].s(), x["flight_agent"].s(), x["flight_retCounts"].s()); return crow::response(return_ret); });myfounction为我封装的一个类创建的对象,low_price_recommend为其中的一个方法。返回一个crow::json::wvalue。变量x为具体传进来的参数crow::json::wvalue为返回的json格式数据可能出现的问题找不到asio库,例如如下所示:CMake Error at /usr/local/share/cmake-3.24/Modules/CMakeFindDependencyMacro.cmake:47 (find_package): By not providing "Findasio.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "asio", but CMake did not find one. Could not find a package configuration file provided by "asio" with any of the following names: asioConfig.cmake asio-config.cmake Add the installation prefix of "asio" to CMAKE_PREFIX_PATH or set "asio_DIR" to a directory containing one of the above files. If "asio" provides a separate development package or SDK, be sure it has been installed. Call Stack (most recent call first): /usr/local/lib/cmake/Crow/CrowConfig.cmake:32 (find_dependency) CMakeLists.txt:6 (find_package) -- Configuring incomplete, errors occurred! See also "/root/flight_query_system/query_system/build/CMakeFiles/CMakeOutput.log".解决打开并编辑这个文件/usr/local/lib/cmake/Crow/CrowConfig.cmake大概在30行左右,将这行list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)替换为list(APPEND CMAKE_MODULE_PATH ${CROW_CMAKE_DIR})再次感谢Crow项目维护成员Luca Schlecker老哥的解答,以及所有维护此项目的成员。官方GITTER讨论聊天室——crowfork/community解决跨域问题原ISSUES。#include <crow.h> #include "query/query.h" #include "common/common.h" #include "crow/middlewares/cors.h" int main(){ crow::App<crow::CORSHandler> app; auto& cors = app.get_middleware<crow::CORSHandler>(); .global() .origin("*") .headers("origin, x-requested-with, accept, access-control-allow-origin, authorization, content-type") .methods("POST"_method, "GET"_method, "PUT"_method, "DELETE"_method, "PATCH"_method, "OPTIONS"_method); CROW_ROUTE(app, "/hello")([](){ return "Hello world"; app.port(18888).multithreaded().run();
Crow的安装与构建前言项目地址:https://github.com/crowcpp/crow官方网站及文档说明:https://crowcpp.org/master/缘由:前几天一直在寻找一款C++的Web框架,通过不断的在Github、StackOverFlow以及搜索引擎上搜索,最终我还是选择了Crow这一框架,其他相关框架还有,restbed,served,nrgest,微软的C++ REST SDK等,Github相关搜索C++ REST framework...结果在测试用例构建时候,似乎遇到了一个bug,于是我提交了一个Issues,得到了开发人员的回复,最终问题得到了解决,在此,再次感谢项目维护人员的付出!配置环境本篇文章为Linux安装。其他平台或具体最低要求等,还请见官方说明文档。Ubuntu 20.0.4GCC 9.4.0Boost 1.71ASIO 1.22.1Cmake 3.24.0到目前为止,Corw依赖使用的ASIO已经为脱离BOOST的独立版。相关安装参考文章:Boost与独立版的ASIO安装详见——独立版的Asio安装与使用](https://www.cnblogs.com/kolane/p/12057777.html)CMake安装详见——ubuntu安装cmake相关问题解决参考文章:Unable to locate package ——Linux新手入门:Unable to locate package错误解决办法安装Crow从源码安装,详见官方说明文档。git clone https://github.com/CrowCpp/Crow.git进入Crow目录,创建build目录,并进入。mkdir build cd buildcmake .. -DCROW_BUILD_EXAMPLES=OFF -DCROW_BUILD_TESTS=OFFmake installHello world#include <crow.h> int main(){ crow::SimpleApp app; CROW_ROUTE(app, "/test")([](){ return "Hello world"; app.port(18888).multithreaded().run(); }编译g++g++ main.cpp -o main -lpthreadCMake# 指定版本 cmake_minimum_required(VERSION 3.24) # 项目名 project(main) # 使用指定源文件来生成目标可执行文件 add_executable(main main.cpp) # 引入外部依赖包 find_package(Crow REQUIRED) # 目标文件与库文件进行链接 target_link_libraries(main PUBLIC Crow::Crow)测试我的第一个程序 CROW_ROUTE(app, "/low_price_recommend").methods("POST"_method)([](const crow::request& req){ auto x = crow::json::load(req.body); crow::json::wvalue return_ret({});//返回结果 if (!x){//参数不全 return_ret["status_code"] = std::string("0"); return crow::response(return_ret); }else{ return_ret = myfounction.low_price_recommend( x["flight_date"].s(), x["flight_departure"].s(), x["flight_arrival"].s(), x["flight_agent"].s(), x["flight_retCounts"].s()); return crow::response(return_ret); });myfounction为我封装的一个类创建的对象,low_price_recommend为其中的一个方法。返回一个crow::json::wvalue。变量x为具体传进来的参数crow::json::wvalue为返回的json格式数据通过x[""].s()将对应的参数转成string可能出现的问题CMake 找不到asio库,例如如下所示:CMake Error at /usr/local/share/cmake-3.24/Modules/CMakeFindDependencyMacro.cmake:47 (find_package): By not providing "Findasio.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "asio", but CMake did not find one. Could not find a package configuration file provided by "asio" with any of the following names: asioConfig.cmake asio-config.cmake Add the installation prefix of "asio" to CMAKE_PREFIX_PATH or set "asio_DIR" to a directory containing one of the above files. If "asio" provides a separate development package or SDK, be sure it has been installed. Call Stack (most recent call first): /usr/local/lib/cmake/Crow/CrowConfig.cmake:32 (find_dependency) CMakeLists.txt:6 (find_package) -- Configuring incomplete, errors occurred! See also "/root/flight_query_system/query_system/build/CMakeFiles/CMakeOutput.log".解决打开并编辑这个文件/usr/local/lib/cmake/Crow/CrowConfig.cmake大概在30行左右,将这行list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)替换为list(APPEND CMAKE_MODULE_PATH ${CROW_CMAKE_DIR})再次感谢Crow项目维护成员Luca Schlecker老哥的解答,以及所有维护此项目的成员。官方GITTER讨论聊天室——crowfork/community解决跨域问题官方文档原ISSUES。#include <crow.h> #include "query/query.h" #include "common/common.h" #include "crow/middlewares/cors.h" int main(){ crow::App<crow::CORSHandler> app; auto& cors = app.get_middleware<crow::CORSHandler>(); .global() .origin("*") .headers("origin, x-requested-with, accept, access-control-allow-origin, authorization, content-type") .methods("POST"_method, "GET"_method, "PUT"_method, "DELETE"_method, "PATCH"_method, "OPTIONS"_method); CROW_ROUTE(app, "/hello")([](){ return "Hello world"; app.port(18888).multithreaded().run();
前言:使用C++调用SQLite数据库进行数据读取,调用sqlite3_prepare_v2进行语句合法检查后,使用sqlite3_column_count获取列数,然后调用sqlite3_step进行多次读取,使用sqlite3_column_text获取具体数据。具体问题:sqlite3_column_text的返回值为 const unsigned char,于是我用const unsigned char存,具体代码如下://如果返回SQLITE_ROW则,进行多次执行 for (result = sqlite3_step(m_stmt); result == SQLITE_ROW; result = sqlite3_step(m_stmt)) { //获取数据 //将每条数据插入vector for (int i = 0; i < count_col; i++) { m_tmp.push_back(sqlite3_column_text(m_stmt, i)); m_data_array.push_back(m_tmp); m_tmp.clear(); counts++; }数据类型 sqlite3* m_db; std::string m_db_path; sqlite3_stmt* m_stmt; std::vector<std::vector<const unsigned char *> > m_data_array; std::vector<const unsigned char *> m_tmp;查出来的几个字段值先存到m_tmp中,然后在放入m_data_array.但是我发现,只有在循环内直接访问,才能看到结果,除了这个循环就为乱码。注意: 我是将SQLite的API又封装了下,这是其中的执行语句模块,上面声明的数据类型,是类中的成员变量。最后通过get_result()获取结果集,然后在其他地方查看数据,于是发现乱码。get_result()函数声明std::vector<std::vector<const unsigned char *> > get_result(); //获取结果集开始我以为是编码问题,于是我搜索,SQLite是UTF-8,C++是ASICC编码,但是出现乱码问题的都是读取中文字符,但是我是数字和字母。不符合该问题。然后我抱着试一试的心态,将sqlite3_column_text 的返回值(const unsigned char)强转成char,然后用string类型的vector来存,并且读取,就成功了。修改后部分代码如下: std::vector<std::vector<std::string> > m_data_array; std::vector<std::string> m_tmp;执行SQL语句模块:int flight_query::my_sqlite::exec_query(std::string sql) { //语句检查——合法 if (sqlite3_prepare_v2(m_db,sql.c_str(),sql.length(),&m_stmt,NULL) == SQLITE_OK) { int result = 0; int counts = 0; int count_col = sqlite3_column_count(m_stmt);//获取列数 //如果返回SQLITE_ROW则,进行多次执行 for (result = sqlite3_step(m_stmt); result == SQLITE_ROW; result = sqlite3_step(m_stmt)) { //获取数据 std::string tmm; //将每条数据插入vector for (int i = 0; i < count_col; i++) { m_tmp.push_back((char*)sqlite3_column_text(m_stmt, i)); m_data_array.push_back(m_tmp); m_tmp.clear(); counts++; if (result == SQLITE_DONE) { sqlite3_finalize(m_stmt);//清理语句句柄,准备下一个语句 return FLIGHT_SUCCESS; }else{ sqlite3_finalize(m_stmt);//清理语句句柄,准备下一个语句 return SQL_EXEC_FAILED; } else {//不合法 fprintf(stderr, "sql exec failed.error code is %d,error reason is %s\n", sqlite3_errcode(m_db), sqlite3_errmsg(m_db)); sqlite3_finalize(m_stmt);//清理语句句柄,准备下一个语句 return SQL_WORDS_ERROR; }补充:除此之外还有个问题,就是本来我是想直接用那个嵌套的vector直接push_back,但是发生段错误。但是我并没有看出哪错了,还请看出来的小伙伴给我留言,谢谢!m_data_array[counts].push_back((char*)sqlite3_column_text(m_stmt, i));相关参考:unsigned char与char 转换
Linux系统调用前言操作系统——管理计算机硬件与软件资源的软件,是用户和系统交互的操作接口,为它上面运行的程序提供服务。操作系统内核——操作系统的内核,负责管理系统的进程、内存、设备驱动程序、文件和网络系统。一个内核不是一套完整的操作系统。例如Linux。Linux操作系统——基于Linux内核的操作系统。通常由Linux内核、shell(特殊的应用程序,提供运行其他程序的接口)、文件系统和应用程序组成。常见的有:Redhat、Fedora、Centos、Ubuntu和Android等。Linux的运行空间:Linux的运行空间:内核空间+用户空间内核空间——存放的是整个内核代码和所有内核模块,以及内核所维护的数据。用户空间——用户程序的代码和数据。什么是系统调用?操作系统提供给用户程序调用系统服务(硬件设备)的一组"特殊"接口。系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个信使,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。为什么要设置系统调用?把用户从底层的硬件编程中解放出来与具体的硬件完全隔离,用户不需要面向具体的硬件编码,降低了开发的复杂度和难度。极大的提高了系统的安全性将用户进程隔离,实现内核"保护",用户进程不允许访问内核数据,也无法使用内核函数。用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不允许肆意跳入内核。有了这样的内核访问路径限制,才能保证内核安全无误。使用户程序具有可移植性对于不同平台不同硬件来说。系统调用的实现通过软件中断实现。软件中断:它是通过软件指令触发的中断。Linux系统内核响应软件中断,从用户态切换到内核态,执行相应的系统调用。系统调用控制程序执行软件中断的过程如下:在进程的内核态堆栈中保存大多数寄存器的内容(即保存恢复进程到用户态执行所需要的上下文)。根据用户态传递的系统调用号,确定系统调用的执行程序。调用相应的执行程序来处理系统调用。从系统调用返回。系统调用号:每个系统调用被赋予一个系统调用号,与具体的系统调用相关联。系统调用表:内核维护系统调用表,保存系统调用函数的起始位置,系统调用号对应该系统调用在调用表中的偏移量。执行系统调用的方法还有系统中断。GLIBC库函数Glibc实现操作系统提供的系统服务,即为系统调用的封装。每个特定的系统调用对应了至少一个glibc封装的库函数。多个API也可能只对应同一个系统调用。返回值-1在多数情况下表示内核不能满足进程的请求。Libc中定义的errno变量包含特定的出错码。示例:更改文件权限#include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <stdio.h> int main(void) { int rc = chmod("./test.txt",S_IWUSR | S_IWGRP | S_IWOTH); if (rc == -1) { fprintf(stderr,"chmod failed,errno=%d\n",errno); } else { printf("chmod success!\n"); return 0; }Syscall直接调用函数原型:long int syscall(long int sysno,...);传参说明:sysno是系统调用号,唯一标识系统调用号,详见sys/syscall.h...为剩余可变长的参数,为系统调用所带的参数,根据系统调用的不同,可带0~5个不等的参数,如果超过特定系统调用能带的参数,多余的参数被忽略。返回值:该函数返回值为特定系统调用的返回值,在系统调用成功之后你可以将返回值转化为特定的类型,如果系统调用失败则返回-1,错误代码存放在errno中。示例:更改文件权限#include <sys/syscall.h> #include <unistd.h> #include <errno.h> #include <sys/stat.h> #include <stdio.h> int main(void) { int rc = syscall(SYS_chmod, "./test.txt", S_IROTH | S_IRGRP | S_IRUSR); if (rc == -1) { fprintf(stderr,"chmod failed,errno=%d\n",errno); } else { printf("chmod success!\n"); return 0; }对比如何高效执行系统调用系统调用的次数会影响程序的执行效率。示例:调用write往文件里写数据,更改每次写的大小进行对比。#include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> //宏是直接替换,注意括号 #define TOTAL_SIZE (1024*1024*500) #define BUF_SIZE 4096 int main(int argc,char**argv) { char buffer[BUF_SIZE]; const char* filename = "./write.txt"; int fd = -1; int i = 0; memset(buffer, '8', BUF_SIZE); fd = open(filename, O_RDWR | O_TRUNC | O_CREAT); if (fd < 0) { fprintf(stderr,"fopen failed,reason:%s.\n exit\n",strerror(errno)); return -1; int num = TOTAL_SIZE / BUF_SIZE; for (i = 0; i < num; i++) { if (write(fd, buffer, BUF_SIZE) < 0) { fprintf(stderr,"write failed!reason:%s.\n",strerror(errno)); close(fd); return 0; }将 BUF_SIZE 更改为 16后,每次写的量减少了,增加了系统调用的次数。两次时间对比:1-4096,2-16。罪魁祸首-用户态和内核态的切换保存用户进程现场合法性检查(如内存)参数传递恢复现场一多一少解决效率问题:
Linux文件操作Linux中,一切皆文件(网络设备除外)。硬件设备也“是”文件,通过文件来使用设备。目录(文件夹)也是一种文件。Linux文件的结构root:该目录为系统管理员(也称作超级管理员)的用户主目录。bin:bin是Binary的缩写,这个目录存放着最经常使用的命令。boot:这里存放的是启动Linux时使用的一些核心文件,包括一些连接文件和镜像文件。deb:deb是Device(设备)的缩写,该目录下存放的是Linux的外部设备,在Linux中访问设备的方式和访问文件的方式是相同的。etc:所有的配置文件,所有的系统管理所需要配置文件和子目录都存放在这里。home:用户的主目录,在Linux系统中,每个用户都有一个自己目录,一般该目录名是以用户的账号命名的。var:存放着不断变化的文件数据,我们习惯将那些经常被修改的目录放在这个目录下。包括各种日志文件。lib:这个目录里存放着系统最基本的动态链接共享库,其作用类似于Windows里的DLL文件。几乎所有的应用程序都要使用到这个共享库。usr:系统用户工具和程序bin:用户命令sbin:超级用户使用的比较高级的管理程序和系统守护程序include:标准头文件lib:库文件src:内核源代码tmp:用来存放一些临时文件media:Linux系统会自动识别一些设备,例如U盘、光驱等,当识别后,Linux会把识别的设备挂载到这个目录下。mnt:临时挂载其他文件。proc:包含了进程的相关信息。Linux文件的操作方式文件描述符fdfd是一个大于等于0的整数。每打开一个文件,就创建一个文件描述符,通过文件描述符来操作文件。预定义的文件描述符:0: 标准输入,对应于已打开的标准输入设备(键盘)1: 标准输出,对应于已打开的标准输出设备(控制台)2: 标准错误,对应于已打开的标准错误输出设备(控制台)(运行程序在proc文件夹中的对应进程文件夹下查看fd文件夹)多次打开同一个文件,可以得到多个不同的文件描述符。使用底层文件操作(系统调用)例如:read使用I/O库函数例如: freadLinux底层文件操作(关于文件的系统调用)write往一个文件描述符中写数据。返回值:成功:返回实际写入的字节数失败:返回-1,设置错误号errno,用strerror(errno)查看注意:从文件当前指针位置开始写入。文件刚打开时从文件指针指向文件头。示例:#include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> int main(void) { char buff[] = "hello world\n"; //1 2 都是输出到控制台 int len = 0; len = write(1, buff, sizeof(buff));//标准输出 if (len < 0) { printf("write failed.reason:%s\n",strerror(errno)); len = write(2, buff, sizeof(buff));//标准出错输出 if (len < 0) { printf("write failed.reason:%s\n", strerror(errno)); return 0; }read从一个文件描述符中读取count个字节到buff中。返回值:大于0——实际读取的字节数0——已读到文件尾-1——出错注意:参数3表示最多能接受的字节数,而不是指一定要输入的字节数。示例:char buffer[1024]; int cnt = read(0,buffer,sizeof(buffer));//从标准输入读openint open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode);打开文件返回值:成功:文件描述符失败:-1打开方式:O_RDONLY 只读O_WRONLY 只写O_RDWR 读写O_CREAT 如果文件不存在,则创建该文件,并使用第3个参数设置权限,如果文件存在 ,则只打开文件。O_EXCL 如果同时使用O_CREAT而且该文件又已经存在时,则返回错误, 用途:以防止多个进程同时创建同一个文件O_APPEND 尾部追加方式(打开后,文件指针指向文件的末尾)O_TRUNC 若文件存在,则长度被截为0,属性不变参数3:设置权限略注意:返回的文件描述符是该进程未打开 的最小的文件描述符。示例:#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <string.h> #define FILE_RW_LEN 1024 int main(void) { //第二个参数-文件存在则无法打开 //O_AOOEND —— 追加 int fd = open("./test_open.txt", O_CREAT | O_EXCL | O_RDWR,S_IRWXU | S_IRGRP |S_IXGRP | S_IROTH); int count = 0; char buffer[FILE_RW_LEN] = "hello i am test"; if (fd < 0) { printf("open failed!,reason :%s\n",strerror(errno)); exit(-1); count = write(fd, buffer, strlen(buffer)); printf("written: %d bytes\n",count); close(fd); return 0; }提示:!gcc——重新执行gcc上次编译的命令closeint close(int fd);关闭文件终止指定文件描述符与对应文件之间的关联。并释放该文件描述符,即该文件描述符可被重新使用。返回值:成功:0失败:-1示例:#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <string.h> #define FILE1_NAME "file1.txt" #define FILE2_NAME "file2.txt" int main(void) { int file1, file2; char buffer[4096]; int len = 0; file1 = open(FILE1_NAME, O_RDONLY); if (file1 < 0) { printf("open file failed! reason:%s\n",strerror(errno)); exit(-1); file2 = open(FILE2_NAME, O_CREAT | O_WRONLY ,S_IRUSR | S_IWUSR); if (file2 < 0) { printf("open file failed! reason:%s\n", strerror(errno)); exit(-2); while (len = read(file1, buffer, sizeof(buffer) > 0)) { write(file2,buffer,len); close(file2);//实战记得判断是否关闭成功 close(file1); return 0; }提示:观察耗时:time xxx程序总的执行时间程序本身所消耗的时间系统调用所消耗的时间lseekoff_t lseek(int fd, off_t offset, int whence);重新定义读写文件的偏移。返回值:成功:返回新的文件位置与文件头之间的偏移。失败:-1示例:从一个文件偏移100处,拷贝100字节到另一个文件。#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <errno.h> #include <string.h> #define FILE1_NAME "lseek_test.c" #define FILE2_NAME "lseek_test2.txt" #define SIZE 100 int main(void) { int file1, file2; char buffer[1024]; int ret; file1 = open(FILE1_NAME, O_RDONLY); if (file1 < 0) { printf("open file failed! reason:%s\n", strerror(errno)); exit(-1); file2 = open(FILE2_NAME, O_CREAT | O_WRONLY, S_IRUSR | S_IWUSR); if (file2 < 0) { printf("open file failed! reason:%s\n", strerror(errno)); exit(-2); //文件句柄,偏移量,从哪偏移 ret = lseek(file1, 0, SEEK_END); printf("file size: %d\n", ret); ret = lseek(file1, 100, SEEK_SET); printf("lseek ret:%d\n", ret); ret = read(file1, buffer, SIZE); if (ret > 0) { buffer[ret] = '\0'; write(file2,buffer,SIZE); close(file1); close(file2); return 0; }ioctl嵌入式相关ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性接口进行控制,例如串口的传输波特率、马达的转速等等。是设备驱动程序中设备控制接口函数,用来控制设备。int ioctl(int fd, int cmd,[int *argdx, int argcx]);fd是用户程序打开设备时使用open函数返回的文件标示符,cmd是用户程序对设备的控制命令,后面是一些补充参数,一般最多一个,这个参数的有无和cmd的意义相关。
Makefile编写程序的编译和链接使用C、C++编写可执行文件,首先要把源文件编译成中间代码文件,Linux下是.o文件,即Object File,这个动作叫做编译(complie)。然后再把大量的Object File合成执行文件,这个动作叫链接(link)。一个项目会拥有成百上千个源程序文件,再使用G++ or GCC会很麻烦。于是Makefile闪亮登场。Makefile确定整个工程的编译规则,只需要一个make命令,就可以实现“自动化编译”。make是一个解释Makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下的GNU的make。Make工作原理通常在一个项目中,我们的规则是:如果这个工程没有被编译过,那么我们的所有C文件都要编译并被链接。如果这个工程的某几个C文件被修改过,那么我们只需要编译被修改的那几个C文件,并链接成生可执行文件。(链接目标程序)如果这个工程的头文件被改变了,那么我们需要编译引用了这个几个头文件的C文件,并链接生成可执行文件。(链接目标程序)只要我们的Makefile写的够好,我们只用一个make命令就可以完成,make命令会自动智能地根据当前文件的修改情况来确定哪些文件需要重新编译,从而自己编译需要的文件和链接生成可执行文件。Makefile的规则:target ... :prerequisites...command......target是一个目标文件,可以是Object File,也可以是可执行文件。还可以是一标签。prerequisites就是要生成那个target所需要的文件或是目标。command就是make需要执行的命令。(任意的Shell命令)这是一个文件依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义要在command中。说白一点,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则,也就是Makefile中最核心的内容。示例:test_demo:test01.o test02.o main.o gcc -o test_demo test01.o test02.o main.o test01.o:test01.h test01.c gcc -c test01.c test02.o:test02.h test02.c gcc -c test02.c main.o: test01.h test01.c test02.h test02.c main.c gcc -c test01.c test02.c main.c clean: rm test_demo test01.o test02.o main.oMake工作流程make会在当前目录下找到名字叫做"Makefile" 或 "makefile"的文件。如果找到,它会找文件中的第一个目标文件(target),例如上面示例中的test_demo,并把这个文件作为最终的目标文件。如果目标文件(target)不存在,或是target后依赖的.o文件的文件修改时间要比目标文件新,那么,它就会执行后面所定义的命令来重新生成目标文件。如果目标文件(target)所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖,如果找到则再根据那个规则生成.o文件。如果文件依赖都齐全,则回正常执行,先生成.o文件,再链接生成目标文件。Make变量一个Makefile中我们发现经常会有重复的内容,例如上面示例中的:test01.o test02.o main.o如果我们需要再加入一个新的.o文件,那么好几个地方都需要修改,可能会忘记并导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串。可以理解为C语言中的宏。变量定义:objects = test01.o test02.o main.o # 使用Shell script的语法示例:objects = test01.o test02.o main.o test_demo: $(objects) gcc -o test_demo $(objects) test01.o:test01.h test01.c gcc -c test01.c test02.o:test02.h test02.c gcc -c test02.c main.o: test01.h test01.c test02.h test02.c main.c gcc -c test01.c test02.c main.c clean: rm $(objects) test_demoMake自动推导make很强大, 它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o文件后写上类似的命令,因为make会自动识别,自己推导命令。只要make看到一个.o文件,它就会自动的把.c文件加在依赖关系中,如果make找到一个test01.o,那么test01.c就是它的依赖文件。并且gcc -c test01.c也会被推导出来。示例:objects = test01.o test02.o main.o test_demo: $(objects) gcc -o test_demo $(objects) test01.o:test01.h test02.o:test02.h main.o: test01.h test02.h clean: rm $(objects) test_demo还可以再简洁些objects = test01.o test02.o main.o test_demo: $(objects) gcc -o test_demo $(objects) $(objects):test01.h test02.h clean: rm $(objects) test_demo关于我:我的博客半生瓜のblog or 半生瓜のblog我的微信公众号【四次猿口袋】我的QQ交流群:805814463
相关参考——Qt Designer生成的图形可以自适应窗口的大小变化重点:设置子部件的sizePolicy属性。设置子部件间的布局。设置子部件布局的layoutStretch属性设置子部件间的比例。设置父部件布局属性。示例:设置sizePolicy为两个子QWidget部件设置水平布局,并设置layoutStretch属性。设置父部件布局为水平布局。注意:这里的父部件是最上面那层MainWindowClass,之后再给其它父部件设置布局,为当前部件的上一层,等下会再演示往下一层。设置完成,为了便于区分,左侧的黄色为绿色,为两个子部件,后面的红色是父部件,我也不知道为什么设置完后会变小一圈,还请知道的老哥评论回复一下,但总体来说不影响使用。效果展示接着演示再往下设置子部件:为m_showWidget添加子部件m_showArea,并设置背景颜色为浅蓝色。设置其父部件m_showWidget的布局属性如下图所示最终效果最后:关于我:我的博客半生瓜のblog or 半生瓜のblog我的微信公众号【四次猿口袋】我的QQ交流群:805814463
补发:2021-11-13MFC基础篇以C++类的形式对WINDOWS API进行封装。MFC框架理论关键类CWinApp:MFC应用程序抽象,管理DocumentTemplate。CFrameWnd: 框架窗口,负责创建应用主窗口,含标题栏,菜单栏,工具栏,状态栏等 。CView: 负责展示应用数据,View其实是一个没有边框的窗口, 客户区 。CDocument: 负责存储应用数据 。CDocTemplate:DocumentTemplate: 隐藏Boss, 负责管理MainFrame,View和Document。窗口与窗口对象关系:每个窗口对象都包含一个窗口窗口对象使用Create方法创建窗口,在Create方法个中将窗口对象与窗口绑定窗口对象的与窗口的生命周期不同,并不同生共死。(menu.Detach()将对象与它绑定在窗口的这一块区域分离(分离窗口对象和窗口))afx微软mfx小组。消息映射把消息ID和相应函数关联起来。 当这种消息发送出来后,对应的函数就会触发。BEGIN_MESSAGE_MAP(CMFCApplicationView, CView) ON_COMMAND(ID_MSG_DRAW_PEN,&CMFCApplicationView::OnPen) ON_COMMAND(ID_MSG_DRAW_RECT,&CMFCApplicationView::OnRect) ...... END_MESSAGE_MAP()重绘元文件元文件(Metafile)是记录所有"绘图指令"的集合,拥有录制,重放的功能。view文件中的代码示例: //声明一个Metefile的对象 HMETAFILE metafile; //结束录制,保存文件 metafile = m_dcMetaFile.Close(); //重放文件 pDC->PlayMetaFile(metafile); //准备一个新的录制 m_dcMetaFile.Create(); //在新的录制中重放旧元文件 m_dcMetaFile.PlayMetaFile(metafile); //删除元文件 DeleteMetaFile(metafile); //无效界面-请求重绘 Invalidate();兼容设备描述表兼容设备是一种内存设备,在显示图像的过程中,可以先在内存中准备这些图像,之后可以直接拷贝内存数据到真实的设备中,完成图像的显示。起到一个二次缓冲的效果自定义重绘定义一个类,略......。文件文件读写写入文件void CMFCApplicationView::OnFileWirte() // TODO: 在此添加命令处理程序代码 CString hello = _T("hello world\n"); //打开文件对话框 CFileDialog fileDlg(FALSE); if (IDOK==fileDlg.DoModal()) //获取对话框返回的文件名 //打开文件 //写入文件 //关闭文件 CString path = fileDlg.GetPathName(); CFile file(path, CFile::modeWrite | CFile::modeCreate); int len = hello.GetLength()* sizeof(TCHAR); file.Write(hello, len); file.Close(); 读取文件void CMFCApplicationView::OnFileRead() // TODO: 在此添加命令处理程序代码 CFileDialog fileDlg(TRUE); if (IDOK == fileDlg.DoModal()) //获取文件名 CString path = fileDlg.GetPathName(); //以读取方式打开文件 CFile file(path, CFile::modeRead); //准备缓冲区 TCHAR* pBuf; int cnt = file.GetLength(); int chars = cnt / sizeof(TCHAR); pBuf = new TCHAR[chars + 1]; //读取文件内容到缓冲区 file.Read(pBuf, cnt); //设置字符串结束位置 pBuf[chars] = 0; MessageBox(pBuf); file.Close(); delete[] pBuf; }类的序列化类的序列化是将程序中对象以一种二进制格式存放到设备中,如文件/数据库等,以实现"永生"或随意"流动"。类要从CObject派生,或是CObject的派生类派生实现Serialize方法在类声明中使用DECLARE_SERIAL宏DECLARE_SERIAL(类名)定义不带参数的构造函数在类实现中添加IMPLEMNET_SERIAL宏IMPLEMNET_SERIAL(类名,父类名,版本号)常用控件与使用者进行交互。获得控件GetDlgItem(控件ID)获取相应的控件通过绑定控件和变量使用 UpdateData(TRUE|FALSE)同步, 默认为True,将控件内容同步到变量,FALSE,将变量同步回控件按钮Radio Button——单选Check Box——多选响应事件,如点击按钮时间.示例:多选按钮。void CContrlObjectDlg::OnBnClickedButton1() // TODO: 在此添加控件通知处理程序代码 UpdateData();//同步控件与变量 CString str; str.Format(_T("上衣=%d\n裤子=%d\n鞋子=%d\n"),m_clothes,m_pants,m_shoes); MessageBox(str); }多选按钮:文本框和列表List Box——列表Edit Control——文本框示例:将文本框的内容放到列表中void CContrlObjectDlg::OnBnClickedButton2() // TODO: 在此添加控件通知处理程序代码 //获取文本框中的内容 CString text; //得到文本框对象 CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT1); //将文本框中的文字存放到text中 edit->GetWindowText(text); //得到列表对象 CListBox* list = (CListBox*)GetDlgItem(IDC_LIST1); //将文本框中的文字添加到列表框中 list->AddString(text); //清空文本框的输入框 edit->SetWindowText(_T("")); 示例2:查看在列表中选择了什么void CContrlObjectDlg::OnBnClickedButton2() // TODO: 在此添加控件通知处理程序代码 //储存获取的内容 CString text; CListBox* list = (CListBox*)GetDlgItem(IDC_LIST1); //获取当前在列表中的选择,返回一个整型,该选项在列表中的位置 int n = list->GetCurSel(); //返回选项放到text中 list->GetText(n,text); MessageBox(text); }下拉列表ComboBox——下拉列表类似于ListBox,基本上操作一直,只是ListBox直接展开所有选项,而ComboBox需要点开查看。示例:void CContrlObjectDlg::OnBnClickedButton2() // TODO: 在此添加控件通知处理程序代码 //储存获取的内容 CString text; CComboBox* list = (CComboBox*)GetDlgItem(IDC_COMBO1); int n = list->GetCurSel(); list->GetLBText(n,text); MessageBox(text); }进度条Progress Control——进度条示例:BOOL CContrlObjectDlg::OnInitDialog()中 //得到进度条控件 CProgressCtrl* prog = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1); //设置范围 prog->SetRange(0,100); //根据条件进行进度条的变动 prog = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1); num = prog->GetPos(); prog->SetPos(++num); prog->GetRange(low, high); if (num>= high) prog->SetPos(low); }图片Picture Control——图片BOOL CMFCPicODlg::OnInitDialog()中 //例如:加载bitmap,以局部变量的方式 BitMap bitmap; m_bitmap.LoadBitmap(IDB_BITMAP1); m_pic.SetBitmap(m_bitmap); bitmap.Detach();表格List Control例如:Windows文件查看示例://绘制表头 m_list.InsertColumn(0, _T("ADDRESS"), LVCFMT_LEFT, 400); m_list.InsertColumn(0, _T("AGE"), LVCFMT_LEFT, 50); m_list.InsertColumn(0, _T("NAME"), LVCFMT_LEFT, 100); m_list.InsertColumn(0, _T("ID"), LVCFMT_LEFT, 50); //插入第一行数据 int index; index = m_list.InsertItem(0, _T("0001")); m_list.SetItemText(index, 1, _T("kenny")); m_list.SetItemText(index, 2, _T("26")); m_list.SetItemText(index, 3, _T("Franch")); //插入第二行数据 index = m_list.InsertItem(1, _T("0002")); m_list.SetItemText(index, 1, _T("olofm")); m_list.SetItemText(index, 2, _T("29")); m_list.SetItemText(index, 3, _T("Sweden"));树Tree Control示例:HTREEITEM root; HTREEITEM first_sub; HTREEITEM second_sub; m_tree.ModifyStyle(0, TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT); root = m_tree.InsertItem(_T("Root")); //root的子节点 first_sub = m_tree.InsertItem(_T("First_Sub"), root); //与first同级的四个 m_tree.InsertItem(_T("1"),root); m_tree.InsertItem(_T("2"),root); m_tree.InsertItem(_T("3"),root); m_tree.InsertItem(_T("4"),root); //first的子结点 second_sub = m_tree.InsertItem(_T("Second"),first_sub); //second的四个子节点 m_tree.InsertItem(_T("5"), second_sub); m_tree.InsertItem(_T("6"), second_sub); m_tree.InsertItem(_T("7"), second_sub); m_tree.InsertItem(_T("8"), second_sub);引入、加载图片//当前2张,还能加4张 m_imgList.Create(32, 32, ILC_MASK | ILC_COLOR32, 2, 4); for (int i = 0; i < 2; i++) HICON icon = theApp.LoadIcon(IDI_ICON1+i); m_imgList.Add(icon); m_tree.SetImageList(&m_imgList,TVSIL_NORMAL); //这两个1分别代表的属性 //分别是:未选中时显示哪张图片,被选中时显示哪在张。 m_tree.InsertItem(_T("1"),1,1,root);
补发:发表于2021-11-08链接Windows编程应用程序分类控制台程序ConsoleDOS程序,本身没有窗口,通过Windows DOS窗口执行。(DOS是操作系统预留的)窗口程序拥有自己的窗口,可以与用户交互。库程序存放代码、数据的程序、执行文件可以从中取出代码执行和获取数据静态库程序:扩展名LIB,在编译链接程序时,将代码放入到执行文件中。动态库程序:扩展名DLL,在执行文件时从中获取代码 。静态库中的代码是直接嵌入到你的项目中,而动态库中的内容是通过地址来找到。静态库程序无法执行,也就是说它最终生成的文件无法进入内存。动态库程序有入口函数,可以执行。但是它不能独立运行。谁调动态库里面的东西,它就依附于谁。应用程序对比入口函数控制台程序-main窗口程序-WinMain动态库程序-DllMain静态库程序-无入口函数文件存在方式控制台程序、窗口程序-EXE文件动态库程序-DLL文件静态库程序-LIB文件编译工具编译器CL.EXE,将源代码编译成目标代码.obj。链接器LINK.EXE,将目标代码、库链接生成最终文件。资源编译器RC.EXE,(.rc)将资源编译,最终通过链接器存入最终文件库文件和头文件库文件kernel32.dll-提供核心的API,消息进程,线程,内存管理等。user32.dll-提供了窗口、消息等API。gdi32.dll-绘图相关的API头文件windows.h——所有windows头文件的集合windef.h——windows数据类型winbase.h——kernel32的APIwingdi.h——user32的APIwinnt.h——UNICODE字符集支持相关函数int WINAPI wWinMain( HINSTANCE hInstance,//当前程序的实例句柄,找到你当前进程所占据的那块内存 HINSTANCE hPrevInstance,//当前程序前一个示例句柄,废弃了 PWSTR pCmdLine, //命令行参数字符串 int nCmdShow//窗口的显示方式 );暂时可以将句柄理解成,句柄是用来找到内存的东西,但绝对不是指针。int MessageBox( [in, optional] HWND hWnd,//父窗口句柄 [in, optional] LPCTSTR lpText,//显示在消息框中的文字 [in, optional] LPCTSTR lpCaption,//显示在标题栏中的文字 [in] UINT uType//消息框中的按钮、图标显示了类型 );//返回点击的按钮ID 能够将程序暂停在这里,说明它是个阻塞函数。它执行,可能不会立即返回。如何理解分析一个阻塞函数?1.这个函数什么情况下阻塞。2.这个函数什么情况下解除阻塞返回。看到数据类型以H开头,多半就是个句柄。程序编译过程可以直接用vs集成好的环境。编译程序-CL,CL.EXE -C xxx.c,生成obj文件链接程序-LINK,LINK.EXE xxx.obj xxx.lib执行生成的exe文件编写资源的文件,.rc资源脚本文件编译rc文件,RC.EXE将资源链接到程序中,LINK.EXE.res文件和.obj文件统称为目标文件(中间文件,因为不是最终文件)编译链接过程:将代码转换为机器语言,将生成的res文件和obj文件加上使用的库链接到一起,整合出一个exe文件,这是用编译器所感受不到的。窗口创建过程定义WinMain函数定义窗口的处理函数(自定义,消息处理)注册窗口类(向操作系统中写入一些数据)创建窗口(内存中创建窗口)显示窗口(绘制窗口的图像)消息循环(获取/翻译/派发消息)消息处理第一个windows窗口#include<windows.h> //窗口处理函数(自定义、处理消息) LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam) return DefWindowProc(hWnd, msgID, wParam, lParam); //入口函数 int CALLBACK WinMain(HINSTANCE hIns, HINSTANCE hPerIns, LPSTR lpCmdLine, int nCmdShow) //注册窗口类 WNDCLASS wc = { 0 }; //申请两种不用的缓冲区 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.hCursor = NULL; wc.hIcon = NULL; wc.hInstance = hIns; wc.lpfnWndProc = WndProc; wc.lpszClassName = "myWindow"; wc.lpszMenuName = NULL; wc.style = CS_HREDRAW | CS_VREDRAW; //将上面赋的这些值全部写入操作系统 RegisterClass(&wc); //在内存中创建窗口 HWND hWnd = CreateWindow("myWindow", "menu", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL); //显示窗口 ShowWindow(hWnd, SW_SHOW); //再画一遍(刷新窗口) UpdateWindow(hWnd); //消息循环 MSG nMsg = { 0 }; while (GetMessage(&nMsg,NULL,0,0)) TranslateMessage(&nMsg); DispatchMessage(&nMsg);//将消息交给窗口处理函数来处理 return 0; }窗口有无与进程退不退没有关系。Unit04字符编码编码历史背景ASC——7位代表一个字符ASCII——8位代表一个字符DBCS——单双字节混合编码,没有同一标准,存在解析问题。UNICODE——字符集,有多种编码方式,一般windows只的是utf-16(所有的字符无论中文汉子还是英文字母,都按两个字符编码。),linux utf-8,有统一标准,不存在解析问题。宽字节数据类型这个数据类型下的所有字符, 都占2个字节。wchar_t每个字符占2个字节,wchar_t实际上是unsigned short类型,定义时,需要增加‘L’,通知编译器按照双字节编译字符串,采用UNICODE编码。需要使用支持wchar_t函数操作宽字节字符串,例如:wchar_t* s1= L"123456";wprintf(L"%s\n",s1);有操作char类型字符串的函数,肯定就有对应操作宽字节字符串的函数。#include<stdio.h> #include<windows.h> int main(void) const wchar_t* s1 = L"hello"; printf("%d\n", wcslen(s1));//5,有效字符个数 return 0; }TCHAR如果定义了unicode宏,该字符串为wchar类型,反之为char 类型。打印UNICODE字符UNICODE字符打印,wprintf对UNICODE字符打印支持不完善。在windows下使用WriteConsole打印UNICODE字符。GetStdHandle获取标准句柄(标准输入句柄、标准输出句柄、标准错误句柄)。WriteConsole(标准输出句柄,pszText,wcslen(pszText),NULL,NULL); 下面的使用案例 short width = LOWORD(lParam); short height = HIWORD(lParam); char szText[256] = { 0 }; sprintf_s(szText, "WM_SIZE:宽度%d,高度:%d\n", width, height); WriteConsole(g_HOUTPUT, szText, strlen(szText), NULL, NULL);创建项目时候为什么要改成多字节字符集?如果项目的属性选择是UNICODE字符集,编译器会自动给你增加一个UNICODE的宏定义。反之,则不会。系统调用函数的参数类型:(对已有的类型进行重命名)LPSTR => char* LPSTR => const char* LPWSTR => wchar_t* LPCWSTR => const wchar_t* 用的多的是这两个 LPTSTR => TCHAR LPCTSTR => const TCHAR*注册窗口类窗口类的概念窗口类是包含了窗口的各种参数信息的数据结构。每个窗口都具有窗口类,基于窗口类创建窗口。每个窗口类都具有一个名称,使用前必须注册到系统。在操作系统内核里存着就叫窗口类,在程序里存着就叫窗口类。窗口类的分类:系统窗口类系统已经定义好的窗口类,所有应用程序都可以直接使用。不需要注册,直接使用窗口类即可。系统已经注册好了。例如:按钮-BUTTON,编辑框-EDIT应用程序全局窗口类由用户自己定义,当前应用程序所有模块都可以使用。应用程序局部窗口类由用户自己定义,当前应用程序中本模块可以直接使用。全局及局部窗口类 :注册窗口类的函数(ATOM——unsigned short)ATOM RegisterClass( CONST WNDCLASS *lpWndClass//窗口类的数据);//注册成功后 ,返回一个数字标识。(0失败,非0成功。)style窗口类风格应用程序全局窗口类的注册,需要在窗口类的风格中添加CS_GLOBALCLASS。应用程序局部类窗口类注册,无需添加如上风格。不建议使用全局窗口类——因为局部窗口类能完成全局窗口类的功能,并且全局窗口类可能会产生冗余。CS_HREDRAW ——当窗口水平变化时,窗口重新绘制 CS_VREDRAW ——当窗口垂直变化时,窗口重新绘制 CS_DBLCLKS ——允许窗口接收鼠标双击 CS_NOCLOSE ——窗口没有关闭按钮窗口创建窗口创建:CreateWindow / CreateWindowExCreateWindow内部是如何实现的系统(CreateWindows函数内部)根据传入的窗口类名称,在应用程序局部窗口类中查找,如果找到执行2 ,没找到执行3。比较局部窗口与创建窗口时传入的HINSTANCE变量。如果有发现相等。创建和注册类在同一模块,创建窗口返回。如果不相等,继续执行3。在应用程序全局窗口类,如果找到,执行4, 没找到执行5。使用找到的窗口类信息,创建窗口返回。在系统窗口类中查找,如果找到创建窗口返回,否则创建窗口失败。#include<windows.h> //窗口处理函数(自定义、处理消息) LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam) switch (msgID) case WM_DESTROY: PostQuitMessage(0); break; default: break; return DefWindowProc(hWnd, msgID, wParam, lParam); //入口函数 int CALLBACK WinMain(HINSTANCE hIns, HINSTANCE hPerIns, LPSTR lpCmdLine, int nCmdShow) //注册窗口类 WNDCLASS wc = { 0 }; //申请两种不用的缓冲区 wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.hCursor = NULL; wc.hIcon = NULL; wc.hInstance = hIns; wc.lpfnWndProc = WndProc; wc.lpszClassName = "myWindow"; wc.lpszMenuName = NULL; wc.style = CS_HREDRAW | CS_VREDRAW; //将上面赋的这些值全部写入操作系统 RegisterClass(&wc); //在内存中创建窗口 HWND hWnd = CreateWindow("myWindow", "menu", WS_OVERLAPPEDWINDOW, 100, 100, 500, 500, NULL, NULL, hIns, NULL); //显示窗口 ShowWindow(hWnd, SW_SHOW); //再画一遍(刷新窗口) UpdateWindow(hWnd); //消息循环 MSG nMsg = { 0 }; while (GetMessage(&nMsg, NULL, 0, 0)) TranslateMessage(&nMsg); DispatchMessage(&nMsg);//将消息交给窗口处理函数来处理 return 0; }子窗口创建过程创建时要设置父窗口句柄创建风格要增加WS_CHILD | WS_VISBLE(根据注册的窗口类,来创建多个窗口。)(个人理解发现:注册的窗口可以复用(覆盖创建一个新的))。HWND hChild1 = CreateWindowEx(0, "Child", "C1", WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, 0, 0, 200, 200, hWnd, NULL, hIns, NULL); HWND hChild2 = CreateWindowEx(0, "Child", "C2", WS_CHILD | WS_VISIBLE | WS_OVERLAPPEDWINDOW, 200, 0, 200, 200, hWnd, NULL, hIns, NULL);消息基础消息的概念和作用消息组成(windows平台下)窗口句柄消息ID消息的两个参数(两个附带信息)消息产生的时间消息产生时的鼠标位置消息的作用当系统通知窗口工作时,就采用消息的方式(DispatchMessage)派发给(调用)窗口的窗口处理函数(将MSG的前四个信息传递给消息处理函数)。每一个窗口都有窗口处理函数MSG结构体接收消息结构体定义如下://对应解释同上消息组成 typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; #ifdef _MAC DWORD lPrivate; #endif } MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;DispatchMessage如何找到窗口处理函数nMsg.hwnd->保存窗口数据的内存->找到对应的窗口处理函数->WndProc 回到你自己定义的消息处理函数->传递参数->处理消息 LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam) 传递这四个参数,不用关系后两个窗口处理函数每个窗口都必需有窗口处理函数,只要基于窗口类创建窗口,就肯定要有个窗口处理函数。窗口处理依照如下结构定义:LRESULT CALLBACK WindowProc( HWND hWnd;//窗口句柄 UINT uMsg;//消息ID WPARAM wParam;//消息参数 LPARAM lParam;//消息参数 );当系统通知窗口时,(DispatchMessage)会调用窗口处理函数,同时将消息ID和消息参数传递给窗口处理函数。在窗口处理函数中,不处理的消息,使用缺省窗口处理函数。例如:DefWindowProc(可以给各种消息做默认处理)。消息循环中的相关函数(浅谈)GetMessage-到系统的某个地方抓本进程的消息函数原型如下:BOOL GetMessage( LPMSG lpMsg,//存放获取到消息的BUFF, HWND hWndp;//窗口句柄,要是定为NULL,将会抓取本进程中所有窗口中的消息 UNIT wMsgFilterMin,//获取的最小ID UNIT wMsgFilterMax//获取消息的最大ID //最后两个参数都为0,就是不管ID有多大,只要是本进程的消息都抓过来 );其中后三个参数可以限制抓取消息的范围,如果设置为NULL,0,0那其实就是没有进行限制,只要是本进程的消息我都把它抓过来。GetMessage的返回值消息WM_QUIT会使GetMessage返回0,从而中终止消息接收。PostQuitMessage(0);会在进程中扔出WM_QUIT这个消息,get后从而使得消息循环终止。TranslateMessage-翻译消息——它可不是什么消息都翻译。将按键(可见字符按键,a~z)消息翻译成字符消息。所以进入到它的内部, 它所做的第一件事就是检查这个消息是否合法,是否是它要翻译的消息类型。 如果不是按键类型消息,不做任何处理,继续执行。函数原型如下:BOOL TranslateMessage CONST MSG* lpMsg;//要翻译的消息地址 }DispatchMessage-派发消息(调用对应窗口的消息处理函数)函数原型如下LRESULT DispatchMessage( CONST MSG* lpmsg//要派发的消息 );常见消息如何学习一个消息1.掌握这个消息的产生时间。分析每个消息附带的两个信息。这个消息可以用来干什么(一般用法)。WM_DESTORY产生时间:窗口被销毁时产生附带信息:wParam:为0,lParam:为0一般用法:常用于在窗口被销毁前,做相应的善后处理,例如资源、内存等(该回收回收,该释放释放。)。WM_SYSCOMMAND产生时间:当点击窗口最大化,最小化,关闭等。附带信息:wParam:具体点击的位置,例如关闭SC_CLOSE等,lParam:鼠标光标的位置(这个不重要,我们只需要知道点没点就行,具体在哪个位置其实无所谓(具体情况具体使用)),LOWORD(lParam);水平位置,HIWORD(lParam);垂直位置。(高两字节传纵坐标,低两字节传横坐标。)一般用法:常用在窗口关闭时,提示用户处理。WM_CREATE产生时间:在窗口创建成功但还没显示时。附带信息:wParam:为0lParam:为CREATESTRUCT类型的指针(强转成这个类型再用),通过这个指针可以获取CreatWindowEx中全部12个参数的信息。一般用法:常用于初始化窗口函数、资源等等,包括创建子窗口等。WM_SIZE产生时间:在窗口的大小发生变化后。附带信息:wParam:窗口大小变化的原因。lParam:窗口变化后的大小LOWORD(lParam)变化后的宽度HIWORD(lParam)变化后的高度一般用法:常用于窗口大小发生变化后,调整窗口内各个部分的布局。WM_QUIT产生时间:程序员发送。附带信息:wPram:PostQuitmessage函数传递的参数。lParam:0。一般用法:用于结束消息循环,当GetMessage收到这个消息后,会返回FALSE,结束while处理,退出消息循环。这个消息不用我们去处理,进不去我们定义的窗口处理函数,GetMessage()返回了0,无法进入循环获取消息。WM_PAINT产生时间:当窗口需要绘制的时候 or( GetMessage没消息可抓的时候,详情请看下面。)附带信息:(没用)wParam: 0lParam:0专职用法:就是用于绘图。第一个WM_PAINT是系统发送的,当第一次创建窗口。也就是说产生WM_SIZE消息的同时肯定回产生WM_PAINT消息,重新绘制。相关函数窗口无效区域:需要重新绘制的区域。调用这个函数,让窗口需要重新绘制,GetMessAge会发送WM_PAINT消息,注意,是谁发送消息。 (就可以理解说这个函数告诉GetMessage说,这个窗口需要绘制了,GetMessage发送消息到消息队列,然后转发到对应进程的消息队列中,开始执行。)BOOL InvalidateRect( [in] HWND hWnd,//窗口句柄 [in] const RECT *lpRect,//区域的矩形坐标,对窗口的哪一部分进行重新绘制 [in] BOOL bErase//重绘前是否先擦除 );调试技巧设置一个全局变量 HANDLE g_HOUTPUT = 0;//接收标准输出句柄 增加一个dos窗口 AllocConsole(); g_HOUTPUT = GetStdHandle(STD_OUTPUT_HANDLE);dos窗口只能接收字符串类型sprintf转,writeconsole打印消息循环的原理消息循环的阻塞GetMessage-从系统获取消息,将消息从系统中移除,阻塞函数。当系统无消息时,会等候下一条消息。对人来说消息是一直存在的,但是对于CPU来说(速度接近光速),消息不是经常有的,所以会经常发生阻塞。这样程序的效率就不高,从而引出下面这个函数。PeekMessage-以查看的方式从系统中获取消息,可以不将消息从系统出移除,非阻塞函数。当系统无消息时,返回FALSE,继续执行后续代码。函数原型如下:(前四个参数同GetMessage)最后一个参数是,是否赋予它抓取消息的能力,一般是不给它的,也就是填写BOOL PeekMessageA( [out] LPMSG lpMsg, [in, optional] HWND hWnd, [in] UINT wMsgFilterMin, [in] UINT wMsgFilterMax, [in] UINT wRemoveMsg );也就是说,更好的流程是,先派PeekMessage去侦查是否有消息,有就告诉GetMessage让它来处 理。没有就不要派Get去了,因为它会一直在那里等着消息的出现。例如:while (1) if (PeekMessage(&nMsg, NULL, 0, 0, PM_NOREMOVE)) //有消息-判断是否是WM_QUIT if (GetMessage(&nMsg, NULL, 0, 0)) TranslateMessage(&nMsg); DispatchMessage(&nMsg); return 0; //没有消息——空闲处理 WriteConsole(g_HOUTPUT, "空闲ing\n", strlen("空闲ing"), NULL, NULL); }发送消息Windows平台上的消息,都是它们两个造出来的。SendMessage-发送消息,会等候消息处理的结果。PostMessage-投递消息,消息发出后立刻返回,不等候消息执行结果。函数原型如下:LRESULT SendMessage( [in] HWND hWnd,//消息发送的目的创建 [in] UINT Msg,//消息ID [in] WPARAM wParam,//消息参数 [in] LPARAM lParam//消息参数 );这四个参数就是一个消息的前四个参数,剩下的两个参数函数内部以某种手段自加来获取。 (其中,sendmessage的消息扔到哪去,我们不知道,在之后的课程中会补充。postmessage的消息会扔到getmessage能接收到的区域)消息分类系统消息-ID范围0~0x03FF由系统定义好的消息,可以在程序中直接使用。程序员只负责一头,要么发送不用处理,要么处理不用发送。用户自定义的消息-ID范围0x0400(WM_USER) - 0x7FFF(31743)由用户自己定义,满足用户自己的需求。由用户自己发出消息,并响应处理。由程序员,自己定制,自己发送,自己处理。自定义消息宏:WM_USER(叫什么都行)例如:定义消息名称#define WM_MYMESSAGE WM_USER+1001发送,在哪发都可以,附加消息,你自己的,附加什么都行。PostMessage/SendMessage(hWnd, WM_MYMESSAGE, 1, 2);消息队列消息队列的概念消息队列是用于存放消息的队列。消息在队列中先进先出。所有窗口都具有消息队列。程序(GetMessage())可以从队列中获取消息。消息队列的分类系统消息队列——由系统维护的消息队列。存放系统产生的消息,例如鼠标、键盘等。程序消息队列——属于每个应用程序的(线程)的消息队列。由应用程序(线程)维护。每个进程都有一个消息队列,都有GetMessage(),在本进程的消息队列中抓取消息。细节解释:所有进程产生的消息都先进系统消息队列,操作系统会每个一段时间,将消息转发到各个进程中去。所以才能在本进程中抓到本进程的消息。如何做到正确转发:消息的第一个参数是窗口句柄,保存窗口数据的内存,通过它可以找到当前程序实例句柄,找到本进程对应内存,从而正确转发。SendMessage既没有扔到系统消息队列里,也没有扔到进程消息队列中。消息和消息队列的关系消息和消息队列的关系当鼠标、键盘产生消息时,会将消息存放到消息队列中。系统会根据存放的消息,找到对应程序的消息队列。将消息投递到程序的消息队列中根据消息和消息队列之间使用关系,将消息分成两类队列消息-消息的发送和获取,都是通过消息队列完成。消息发送后,首先放入队列,然后通过消息循环,从队列中获取。GetMessage-从消息队列中获取消息PostMessage-将消息投递到消息队列常见的消息队列:WM_PAINT、键盘、鼠标、定时器非队列消息-消息的发送和获取,是直接调用消息的窗口处理函数完成。消息发送时,首先找到消息接收窗口的窗口处理函数,直接调用处理函数,完成消息。SendMessage-直接将消息发送窗口的处理函数,并等待处理结果。常见消息:WM_CREATE(它是必须不能进队列的,否则就没办法把它抓出来了)、WM_SIZE等。GetMessage在程序(线程)消息队列查找消息,如果队列有消息,检查消息是否满足指定条件(HWND,ID范围),不满足条件就不会取出消息,否则从队列取出消息返回。如果程序(线程)消息队列没有消息,会向系统消息队列获取(找系统要,发起请求)属于班本程序的消息(之前说系统会定时分发属于对应进程的消息,如果这个时间没到,但是有的进程没消息了,会打破这个时间,提前分发。)。如果系统队列的当前消息属于本程序,系统会将消息转发到程序消息队列中。如果系统消息也没有消息(那也不闲着),检查当前进程的所有窗口需要重新绘制的区域,如果发现有需要绘制的区域,产生WM_PAINT消息扔到系统的消息队列,取得消息返回处理。如果没有重新绘制区域,检查定时器,如果有到时的定时器,产生WM_TIMER,返回处理执行。如果没有到时的定时器,整理程序的资源、内存等等。GetMessage会继续等候下一条消息。PeekMessage会返回FASLE,交出程序的控制权。注意:GetMessage如果获取到的是WM_QUIT,函数会返回FALSE。总结:总结起来就是,GetMessage非常的死心眼,没消息就找系统要,要不来就自己造,造不来就整理下资源(干点打扫卫生的活儿),还没有消息来,那就阻塞,等待消息的传来,但是依然不返回。阻塞了,就说明GetMessage已经尽力了。PeekMessage也会干上面的事儿,但是它最后不会傻等。键盘消息键盘消息分类WM_KEYDOWN-按键被按下时产生WM_KEYUP-按键被放开时产生WM_SYSKEYDOWN-系统键按下时产生,比如ALT、F10WM_SYSKEYUP-系统键放开时产生附带信息:.WPARAM-按键的Virtual Key(每个这个对应一个按键-这个虚拟键码值无法区分大小写,所以我们才需要去翻译消息)LPARAM-按键的参数,例如按下的次数字符消息(WM_CHAR)TranslateMessage在转换WM_KEYDOWN消息时,对于可见字符可以产生WM_CHAR,不可见字符无此消息。附带信息:WMPARAM-输入的字符的ASCII字符编码值LPARAM-按键的相关参数翻译消息的内部大致流程,如下图所示:文字解释:先检查是否有按键被按下, 没有直接return,有则判断是否是可见字符消息(刚才的按键消息再一次被GET过来时,已经变成了字符消息,不是按键消息,在这部就直接return了),不是直接return,都通过了,那就判断大写键是否打开,根据大小写发出相应的消息。鼠标消息鼠标消息的分类基本鼠标消息WM_LBUTTONDOWN-鼠标左键按下WM_LBUTTONUP-鼠标左键抬起WM_RBUTTONDOWN-鼠标右键按下WM_RBUTTONUO-鼠标右键抬起WM_MOUSEMOVE-鼠标移动消息双击消息WM_ LBUTTONDBLCLK-鼠标左键双击WM_RBUTTONDBLCLK-鼠标右键双击滚轮消息WM_MOUSEWHEEL-鼠标滚轮消息鼠标基本消息附带信息WPARAM:其他按键的状态,例如Ctrl/Shift等。LPARAM:鼠标的位置,窗口客户区坐标系。LOWWORD X坐标位置HIWORD Y坐标位置一般情况鼠标按下/抬起成对出现。在鼠标移动过程中,会根据移动速度产生一系列的WM_ MOUSEMOVE消息。鼠标双击消息附带消息:WPARAM-其它按键的状态,例如CTRL/SHIFT。LPARAM-鼠标的位置,窗口客户区坐标系。LOWORD(lParam)-X坐标位置HIWORD(lParam)-Y坐标位置消息产生顺序一左键双击为例:WM_LBUTTONDOWNWM_LBUTTONUPWM_LBUTTONDBLCLKWM_LBUTTONU使用时需要在注册窗口类的时候添加CS_DBLCLKS风格。鼠标滚轮消息附带消息WPARAMLOWORD-其他按键的状态HIWORD-滚轮的偏移量(120的倍数 ),通过正负值表示滚轮的方向正-向前滚动,负-向后滚动LPARAM:鼠标当前的位置,屏幕坐标系LOWORD-X坐标HIWORD-Y坐标使用:通过偏移量,获取滚轮的方向和距离定时器消息定时器消息介绍产生时间:在程序中创建定时器,当到达时间间隔时,定时器会向程序发送一个WM_TIMER消息。定时器的精度是毫秒,但是准确度很低。例如时间间隔为1000ms,但是会在非1000毫秒到达消息。附带信息:WPARAM:定时器ID(到时间的)LPARAM:定时器处理函数的指针GetMessage肯定有没事干的时候,所以WM_TIMER消息肯定会产生。按照一定周期去执行,时间要求不严格,都可以用这个定时器。创建销毁定时器创建定时器函数原型如下UINT_PTR SetTimer( [in, optional] HWND hWnd,//定时器窗口句柄 [in] UINT_PTR nIDEvent,//定时器ID [in] UINT uElapse,//时间间隔(毫秒为单位) [in, optional] TIMERPROC lpTimerFunc;//定时器处理函数指针(一般不使用,为NULL)创建成功,返回非0。 );关闭定时器函数原型如下BOOL KillTimer( [in, optional] HWND hWnd,//窗口句柄 [in] UINT_PTR uIDEvent//定时器ID );菜单资源菜单分类窗口的顶层菜单(不需要也不能设置ID,因为它的唯一作用的就是弹出下拉次菜单)弹出式菜单系统菜单HMENU(菜单句柄)类型表示菜单,ID表示菜单项。资源相关资源脚本文件:.rc文件编译器:RC.EXE菜单资源的使用添加菜单资源-添加文件.rc文件加载菜单资源注册窗口类时设置菜单(wc.lpszMenuName = (char*)IDR_MENU1;//菜单设置)创建窗口传参设置菜单CreateWindow的导数第三个参数在主窗口WM_CREATE消息中利用SetMenu函数设置菜单LoadMenuW函数原型:HMENU LoadMenuW( [in, optional] HINSTANCE hInstance, [in] LPCWSTR lpMenuName );SetMenu函数原型:BOOL SetMenu( [in] HWND hWnd, [in, optional] HMENU hMenu );命令消息(WM_COMMAND)处理附带消息:WPARAM:HIWORD-对于菜单为0(没用)LOWRD-被鼠标点击的菜单项的IDLPARAM对于菜单项为0(没用)Windows资源图标资源指的就是.ico为后缀的图片添加资源注意图标的大小, 一个图标文件中,可以有多个不同大小的图标。加载函数原型如下:到本进程的内存中找图标的数据同loadMenu HICON LoadIconA( [in, optional] HINSTANCE hInstance, [in] LPCSTR lpIconName );//成功返回HICON句柄 wc.hIcon = LoadIcon(hIns,(char*)IDI_ICON1);设置注册窗口类光标资源添加光标的资源光标的大小默认是32X32像素,每个光标有HotSpot,是当前鼠标的热点(点击图标生效的那个点)。加载资源函数原型如下:HCURSOR LoadCursorW( [in, optional] HINSTANCE hInstance,//可以为NULL,获取系统默认的Cursor [in] LPCWSTR lpCursorName );设置资源在注册窗口时,设置光标。wc.hCursor = LoadCursor(hIns,(char*)IDC_CURSOR1);使用SetCursor设置光标。HCURSOR SetCursor( HCURSOR hCursor //必须放在处理下面这个消息的时候调用WM_SETCURSOR消息参数WPARAM-当前使用的光标句柄LPARAM-LOWORD-当前区域代码(Hit-Test code)HICLIENT/HTCAPTION...HIWORD-当前鼠标消息ID字符串资源添加字符串资源添加字符串资源,在表中增加字符串。字符串资源的使用函数原型如下int LoadStringW( [in, optional] HINSTANCE hInstance, [in] UINT uID,//字符串ID [out] LPWSTR lpBuffer,//存放字符串 [in] int cchBufferMax );//成功返回字符串长度,失败0使用:例如实现中英文两版的软件。使用这个就非常方便。字符串能写就写在这张字符串表中,容易修改。用loadstring去读,然后在字符串表中改。char szTitle[256] = { }; LoadString(hIns, IDS_WIND, szTitle, 256);加速键资源什么ctrl+c,v之类的,就是快捷键呗。能使用加速键(组合键)的功能,在菜单栏中也对应有一个,一般是绑定使用。添加,资源添加加速键表,增加命令ID对应的加速键。使用:函数原型如下:加载加速键表HACCEL LoadAcceleratorsA( [in, optional] HINSTANCE hInstance, [in] LPCSTR lpTableName//加速键表资源ID );//返回加速键表句柄翻译加速键int TranslateAcceleratorA( [in] HWND hWnd,//处理消息的句柄 [in] HACCEL hAccTable,//加速键句柄 [in] LPMSG lpMsg//消息 );//如果是加速键,返回非0 位置:一定是放在GetMessage的后面,因为它不抓哪来的消息。 并且放在TranslateMessage的后面,假如我按了ctrl+m,我是想让他区分按的是大M还是小m吗?不是,我为的是实现对应的功能(产生对应的消息WM_COMMAND。) 如果按的是加速键,在内部会发送出WM_COMMAND消息。 代码示例: HACCEL hAcc = LoadAccelerators(hIns, (char*)IDR_ACCELERATOR1);//加载加速键表 while (GetMessage(&nMsg, NULL, 0, 0)) if (!TranslateAccelerator(hWnd, hAcc, &nMsg))//不是加速键 TranslateMessage(&nMsg); DispatchMessage(&nMsg);//将消息交给窗口处理函数来处理 }注意:由TranslateAccelerator发出的WM_COMMMAND消息,HIWORD(WPARAM)为1。而鼠标点击菜单资源的HIWORD(WPARAM)为0。LOWORD(WPARAM)都代表命令ID。LPARAM为0。绘图编程绘图基础绘图设备(上下文/描述表)DC(Device Context)绘图上下文/绘图描述表HDC - DC句柄,表示绘图设备,绘图设备句柄。GDI-Windows graphics device interface(Win32提供的绘图API)颜色计算机使用红、绿、蓝(RGB)每个点颜色是3个字节24位保存0~2^24 - 1种颜色16位:5,5,632位:8,8,8,8绘图或透明度颜色的使用COLORREF-实际DWORD例如:COLORREF nColor = 0;赋值使用RGB宏例如:nColor = RGB(0,0,255);获取RGB值GetRBValue/GetGValue/GetBValue例如:BYTE nRed =GetRValue(nColor);基本图形绘制在绘图消息中绘图绘制点SetPixel设置指定点的颜色函数原型如下:COLORREF SetPixel( [in] HDC hdc,//绘图设备句柄 [in] int x,//X坐标 [in] int y,//Y坐标 [in] COLORREF color//设置的颜色 );//返回点原来的颜色例如: PAINTSTRUCT PS = { };//抓到画家 HDC hdc = BeginPaint(hwnd, &PS);//开始绘图 SetPixel(hdc,100,100,RGB(255,0,0)); EndPaint(hwnd,&PS);//放掉画家线的使用(直线、弧线)MoveToEx-指名窗口当前点(更改窗口当前点)BOOL MoveToEx( [in] HDC hdc, [in] int x, [in] int y, [out] LPPOINT lppt//为空则不返回上一个点 );LineTo-从窗口当前点到指定点绘制一条直线(并且致命窗口当前点我)BOOL LineTo( [in] HDC hdc, [in] int x, [in] int y );当前点:上一次绘图时的最后一点,初始为(0,0)点封闭图形:能够使用画刷填充的图形(反之则不是)。Rectangle/Ellipse——(圆形/矩形)内切圆,参数相同。GDI绘图对象画笔画笔的作用线的颜色、线型、线粗。HPAN-画笔句柄画笔的使用1.创建画笔,函数原型如下HPEN CreatePen( [in] int iStyle,//画笔的样式 [in] int cWidth,//画笔的粗细 [in] COLORREF color//画笔的颜色 );//创建成功画笔句柄PS_SOILD-实心线,可以支持多个像素宽,其它线型只能是一个像素宽。2.将画笔应用到DC中HGDIOBJ SelectObject( [in] HDC hdc,//绘图设备句柄 [in] HGDIOBJ h//GDI绘图对象句柄,(包括)兼容画笔句柄 );//返回原来的GDI绘图对象句柄3.绘图4.取出DC中的画笔将原来的画笔,使用SelectObject函数,放入到设备DC中,就会将我们创建的画笔取出。5.释放画笔,函数原型如下BOOL DeleteObject( [in] HGDIOBJ ho//GDI绘图对象句柄,画笔句柄。 );只能删除不被DC使用的画笔,所以在释放前,必须从将画笔从DC中取出。大致过程就是,创建-交换-绘画-取出-释放示例: PAINTSTRUCT PS = { };//抓到画家 HDC hdc = BeginPaint(hwnd, &PS); HPEN hPen = CreatePen(PS_SOLID,20,RGB(255,0,0));//创建 HGDIOBJ nOldPen = SelectObject(hdc, hPen);//交换 Ellipse(hdc, 100, 100, 300, 300); SelectObject(hdc, nOldPen);//交换,这里可以不用接收,上面创建时的句柄可以找到这跟笔,让画家松手就行了,我们就能销毁了。 DeleteObject(hPen); EndPaint(hwnd,&PS);画刷画刷相关画刷-封闭图形的填充颜色、图案HBRUSH-画刷句柄画刷的使用套路就跟画笔一样默认是画刷颜色是白色的画刷的使用1.创建画刷创建实心画刷 HBRUSH CreateSolidBrush( [in] COLORREF color ); 创建纹理画刷 HBRUSH CreateHatchBrush( [in] int iHatch, [in] COLORREF color );2.将画刷应用到DC中SelectObject3.绘图4.将画刷从DC中取出SelectObject5.删除画刷DeleteObject其他可以使用GetStockObject函数获取系统维护的画刷,画笔等。如果不使用画刷填充,需要使用NULL_BRUSH参数,获取不填充的画刷。GetStockObject返回的画刷不需要DeleteObject。示例://向操作系统借一把透明刷子 HGDIOBJ hBrush = GetStockObject(NULL_BRUSH);位图位图绘制位图相关光栅图形-记录图像中每一点的颜色等信息矢量图形-记录图像算法、绘图指令等。HBITMAP-位图句柄位图的使用1.在资源中添加位图资源2.从资源中加载位图——loadBitMap3.创建一个与当前DC相匹配的DC(内存DC)(在内存的一个虚拟的区域画)HDC CreateCompatibleDC( [in] HDC hdc//当前DC句柄,可以为NULL(使用屏幕DC) );//返回创建好的DC句柄兼具资源的步骤和GDI绘图对象的步骤。4.将位图放入匹配的DC中SelectObject5.成像(1:1)BOOL BitBlt( [in] HDC hdc,//目的DC [in] int x,//目的左上X坐标 [in] int y,//目的左上Y坐标 [in] int cx,//目的宽度 [in] int cy,//目的高度 [in] HDC hdcSrc,//源DC [in] int x1,//源左上X坐标 [in] int y1,//源左上Y坐标 [in] DWORD rop//成像方法SRCCOPY );缩放成像BOOL StretchBlt( [in] HDC hdcDest, [in] int xDest, [in] int yDest, [in] int wDest, [in] int hDest, [in] HDC hdcSrc, [in] int xSrc, [in] int ySrc, [in] int wSrc, [in] int hSrc, [in] DWORD rop );6.取出位图SelectObject7.释放位图DeleteObject8.释放匹配的DCDeleteDC代码示例:PAINTSTRUCT ps = { }; HDC hdc = BeginPaint(hwnd, &ps); HBITMAP hBmp = LoadBitmap(g_HINSTANCE, (char*)IDB_BITMAP1); HDC hMemdc = CreateCompatibleDC(hdc);//创建一个内存DC,并构建一个虚拟区域,并且内存DC在虚拟区域中绘图 HGDIOBJ nOldBmp = SelectObject(hMemdc,hBmp);//将位图数据送给内存DC,内存DC在虚拟区域中将位图绘制出来,返回旧位图(原来并没有,这是个假的,只是为了我们将来能够换回来) BitBlt(hdc, 100, 100, 48, 48, hMemdc, 0, 0, SRCCOPY);//将虚拟区域绘制好的图像成像到窗口中 StretchBlt(hdc, 200, 200, 96, 96,hMemdc,0,0,48,48,SRCCOPY);//缩放成像 //开辟的区域比要成像的图像大,就是放大图像。 SelectObject(hMemdc, nOldBmp);//换回来 DeleteObject(hBmp);//释放画的位图 DeleteDC(hMemdc);//释放内存DC文本绘制文字的绘制TextOut-将文字绘制在指定坐标位置BOOL TextOutW( [in] HDC hdc, [in] int x, [in] int y, [in] LPCWSTR lpString, [in] int c );DrawTextint DrawText( [in] HDC hdc,//DC句柄 [in, out] LPCTSTR lpchText,//字符串 [in] int cchText,//字符数量 [in, out] LPRECT lprc,//绘制文字的矩形框 [in] UINT format//绘制的方式 );文字颜色和背景文字颜色:SetTextColorCOLORREF SetTextColor( [in] HDC hdc, [in] COLORREF color );文字背景色:SetBkColor——只适用在不透明文字背景模式:SetBkMode(OPAQUE/TRANSPARENT)(透明(默认)/非透明)字体字体相关:window常用的字体为TrueType格式的字体文件字体名-标识字体类型HFONT-字体句柄(保存字体的数据信息——外观形状)字体的使用1.创建字体CreateFontHFONT CreateFontA( [in] int cHeight,//字体高度 [in] int cWidth,//字体宽度 [in] int cEscapement,//字符串倾斜角度 [in] int cOrientation,//字体的旋转角度 [in] int cWeight,//字体的粗细 [in] DWORD bItalic,//斜体 [in] DWORD bUnderline,//字符下划线 [in] DWORD bStrikeOut,//删除线 [in] DWORD iCharSet,//字符集-GB2312_CHARSET //下面的4参数全写0即可 [in] DWORD iOutPrecision,//输出精度 [in] DWORD iClipPrecision,//剪切精度 [in] DWORD iQuality,//输出质量 [in] DWORD iPitchAndFamily,//匹配字体 //这个得写 [in] LPCSTR pszFaceName//字体名称 );代码示例:PAINTSTRUCT ps = { }; HDC hdc = BeginPaint(hwnd,&ps); SetTextColor(hdc, RGB(255, 0, 0)); SetBkColor(hdc, RGB(0, 255, 0)); HFONT hFront = CreateFont(30,0,45,0,900,1,1,1, GB2312_CHARSET,0,0,0,0,"黑体"); HGDIOBJ nOldFron = SelectObject(hdc, hFront);//将创建的字体给它 const char* szText = "我是TEXTOUT"; TextOut(hdc,100,100,szText,strlen(szText)); //DrawText在矩形的范围内画,所以首先要确定一个范围 RECT rc; rc.left = 100; rc.top = 150; rc.right = 200; rc.bottom = 200; //DrawText有矩形作为限制边界(可以打破) DrawText(hdc, szText, strlen(szText),&rc,DT_LEFT | DT_TOP); //DT_VCENTER和DT_BOTTOM只适用于DT_SINGLELINE和DT_WORDBREAK冲突 SelectObject(hdc, nOldFron);//让它放开 DeleteObject(hFront);//字体更要记得释放,因为字体占的内存较大 EndPaint(hwnd,&ps);对话框处理消息的方式与普通窗口相反,缺省的函数调用自定义函数。对话框原理对话框的分类模式对话框-当前对话框显示时,会禁止本进程其它窗口和用户交互操作。无模式对话框-在对话框显示后,本进程其它窗口仍然可以和用户进行操作。对话框基本使用对话框窗口处理函数注册窗口函数(不使用)创建对话框对话框的关闭谁注册窗口类,窗口处理函数就由谁来实现。对话框处理函数(并非真正的对话框处理函数,真正的系统内部。)DLGPROC Dlgproc; INT_PTR Dlgproc( HWND unnamedParam1,//窗口句柄 UINT unnamedParam2,//消息ID WPARAM unnamedParam3,//消息参数 LPARAM unnamedParam4//消息参数 );返回TRUE——缺省处理函数不需要处理。返回False——交给缺省处理函数处理。不需要调用缺省对话框处理函数。模式对话框void DialogBoxA( [in, optional] hInstance,//当前程序实例句柄 [in] lpTemplate,//对话框资源ID [in, optional] hWndParent,//对话框父窗口 [in, optional] lpDialogFunc//自定义函数 );DialogBox是一个阻塞函数,只有当对话框关闭后,才会返回,继续执行后续代码。返回值是通过EndDialog设置。对话框的关闭BOOL EndDialog( [in] HWND hDlg,//关闭的对话框窗口 [in] INT_PTR nResult//关闭的返回值,能指定返回值就说明能接触阻塞 );关闭模式对话框,只能使用EndDialog,不能使用DestoryWindow等函数。nRsult是DiglogBox函数退出时的返回值。对话框的消息WM_INITDIALOG-对话框创建之后在显示之前,通知对话框窗口处理函数,可以完成自己的初始化相关的操作。EndDialog销毁对话框,并且解除阻塞。而DestroyWindow只能销毁对话框,并不能解除阻塞。无模式对话框创建对话框HWND CreateDialog( HINSTANCE hInstance,//应用程序实例句柄 LPCTSTR lpName,//模板资源ID HWND hWndParent hWndParent,//父窗口 DLGPROC lpDialogFunc//自定义函数 );非阻塞函数,创建成功后返回窗口句柄,需要使用ShowWindow函数显示对话框。对话框关闭关闭时使用DestroyWindow销毁窗口,不能使用EndDiaglog关闭对话框。代码示例:HWND noModel = CreateDialog(g_Hinstance, (char*)IDD_DIALOG1,hwnd, DlgProc); ShowWindow(noModel, SW_SHOW); DestroyWindow(hwnd);静态库Windows上静态库和Linux上的静态库在原理上没有任何区别,都是封装一堆东西等着别人去掉。静态库的特点运行不存在。没有如何,不能执行,生成的文件无法形成静态影像,无法进内存。静态库源码被链接到调用程序中。目标程序的归档。C语言静态库C静态库的创建创建一个静态库程序。添加库程序,源文件使用C文件。C静态库的使用库路径设置:可以使用#pragma关键字设置pragma comment(lib,"../lib/clib.lib")C++静态库C++静态库的创建创建一个静态库项目添加库程序,源文件使用CPP文件。C++静态库的使用库路径设置:可以使用pragma关键字设置pragma comment(lib,"../xx/xxx.lib")示例:#include<iostream> using namespace std; //给编译器看 int CLIB_add(int add1, int add2); int CLIB_sub(int add1, int add2); //给链接器看 #pragma comment(lib,"../Debug/CPPLIB.lib") int main(void) cout << CLIB_add(5, 2) << endl; cout << CLIB_sub(5, 2) << endl; return 0; }C++调库中函数需要在前面声明。告诉编译器到底换不换名。C++编译器编译时会更改函数名。解决:extern"C" 函数声明;告诉编译器以C语言方式编译(不要给我改名)。动态库动态库的特点运行时独立存在(依附着其它程序运行起来的之后)源码不会链接到执行程序使用时加载(使用动态库必须使动态库执行)与静态库进行比较由于静态库是将代码嵌入到使用程序中,多个程序使用时,会有多分代码,所以代码体积会增大。动态库的代码只需要存一份,其他程序通过函数地址使用,所以代码体积较小。静态库发生变化后,新的代码需要重新链接嵌入到执行程序中。动态库发生变化后,如果库中函数的定义(或地址)未变化(仅仅是函数的实现发生了变化),其他使用DLL的程序不需要重新链接。动态库的创建创建动态库项目添加库程序库程序导出-提供给使用库者库中的函数等信息。声明导出:使用_declspec(dllexport)导出函数。将函数的地址存放在动态库的文件头中。(换名之后的函数)注意:动态库编译链接后,也会有LIB文件(与该DLL配套生成的),是作为动态库函数映射使用,与静态库不完全相同。LIB中存放每个函数的名字和它对应的标号,还有与它配套的DLL文件名——xxx.dll模块定义文件.def例如: LIBRARY DLLFunc //库 EXPORTS //库导出表 DLL_Mul //导出的函数动态库的使用隐式链接(操作系统负责使动态库执行,系统偷偷的把动态库扔到内存中。)头文件和函数原型,在函数原型的声明前,增加_declspec(dllimport)导入动态库的LIB文件在程序中使用函数隐式的链接的情况,dll文件可以存放的路径(编译器可以找到的位置):与执行文件中同一个目录下当前工作目录Windows目录,(三个window..不建议使用,因为是系统路径, 公共场所,容易出现问题。)Windows/System32目录Windows/System环境变量PATH指定目录显式链接(程序员自己负责使动态库执行)1.定义函数指针类型 typedef2.加载动态库HMODULE LoadLibrary( LPCTSTR lpFileName//动态库文件名或全路径 );//返回DLL的实例局句柄(HINSTANCE)3.获取函数(绝对/真实)地址FARPROC GetProcAddress( HMODULE hModule,//DLL句柄 LPCSTR lpProcName//函数名称 );//成功返回函数地址4.使用函数5.卸载动态库(释放那块内存,结束动态库执行。)BOOL FreeLibrary( HMODULE hModule//DLL的实例句柄 );代码示例:LIBRARY Dll3 EXPORTS Print @1 #include<windows.h> #include<iostream> using namespace std; typedef void(*PRINT)(); int main(void) HINSTANCE hDll = LoadLibrary("Dll3.dll"); PRINT MYPRINT = (PRINT)GetProcAddress(hDll, "Print"); MYPRINT(); FreeLibrary(hDll); return 0; }动态库中封装类类导出的其实也是函数地址。在类名称前增加_declspec(dllexport)定义,例如:class _declspec(dllexport)CMath }通常使用预编译开关切换类的导入导出定义,例如:代码示例:(如果生成错误就改一下代码顺序)动态库中封装类.h#ifndef _DLLCLASS_H #define _DLLCLASS_H #ifdef DLLCLASS_EXPORTS #define EXT_CLASS _declspec(dllexport)//导出 #else #define EXT_CLASS _declspec(dllimport)//导入 #endif class EXT_CLASS Print public: void PRINT(); #endif动态库中实现.cpp#include "pch.h" #include<iostream> #include"CLASSDLL.h" using namespace std; #define DLLCLASS_EXPORTS void Print::PRINT() cout << "123" << endl; }使用该动态库#include<iostream> using namespace std; #include"../CLASSDLL/CLASSDLL.h" #pragma comment(lib,"../Debug/CLASSDLL.lib") int main(void) Print p1; p1.PRINT(); return 0; }线程线程基础Windows的线程是可以执行代码的实例。系统是以线程为单位调度程序。一个程序当中可以有多个线程,实现多任务的处理。进程开启意味着分内存,线程开辟意味着程序的执行。Windows线程的特点:线程都具有1个ID每个线程都具有自己的内存栈同一进程中的线程使用同一个地址空间线程的调度:操作系统将CPU的执行时间划分成时间片,依次根据时间片执行不同的线程线程轮询:线程A->线程B->线程A......在一个时间点上,CPU只能执行一个线程。创建线程创建线程函数原型如下:HANDLE CreateThread( [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,//安全属性 [in] SIZE_T dwStackSize,//线程栈的大小 [in] LPTHREAD_START_ROUTINE lpStartAddress,//线程处理函数的函数地址 [in, optional] __drv_aliasesMem LPVOID lpParameter,//传递给线程处理函数的参数 [in] DWORD dwCreationFlags,//线程的创建方式 [out, optional] LPDWORD lpThreadId//创建成功,返回线程的ID );//创建成功,返回线程句柄。只要看到安全属性这个参数就通通置空,这个参数已经被废弃了。线程栈大小,按照1MB对齐。看到处理函数,就说明这个函数由我们程序员去定义,由系统调用。线程的创建方式有两种,立即执行方式,挂起方式。多线程,宏观上同时开始(并行),真实是,一个时间点只能执行一个线程(串行)。定义线程处理函数DWORD WINAPI ThreadProc( LPVOID lpParameter//创建线程时,传递给线程的参数。 );代码示例:#include<windows.h> #include<iostream> using namespace std; DWORD CALLBACK TestProc(LPVOID pParam) while (1) cout << (char*)pParam << endl; Sleep(1000); return 0; DWORD CALLBACK TestProc1(LPVOID pParam) while (1) cout << (char*)pParam << endl; Sleep(1000); return 0; int main(void) DWORD nID = 0; const char* text = "123"; HANDLE hTread = CreateThread(NULL, 0, TestProc, (void*)text, 0, &nID); const char* text1 = "abc"; HANDLE hTread1 = CreateThread(NULL, 0, TestProc1, (void*)text1, 0, &nID); getchar(); return 0; }线程挂起/销毁挂起: DWORD SuspendTread( HANDLE hThread //线程句柄 );唤醒:DWORD ResumeTread( HANDLE hThread//线程句柄 );结束指定线程:BOOL Terminate Thread( HANDLE hThread,//线程句柄 DWORD dwExitCode//退出码,没有实际意义 );结束函数所在的线程:VOID ExitThread( DWORD dwExitCode//退出码 );线程相关操作获取当前线程IDGetCurrentThreadId();获取当前线程的句柄GetCurrentThread();等候(可等候)单个句柄有信号——线程句柄VOID WaitForSingleObject( HANDLE handle,//句柄BUFF的地址 DOWRD dwMilliseconds//等候时间INFINITE(无限大) );一个可等候的句柄,一定有有信号和无信号的两种状态。等候的时间已经到了,该句柄还是无信号那就返回了。同时等候多个句柄有信号DWORD WaitForMultipleObjects( DOWRD nCount,//要等候的句柄数量 CONST HANDLE*lpHandle,//句柄BUFF的地址 BOOL bWaitAll,//等候方式 //TRUE-所有事件都有信号才返回 //FALSE-有一个事件有信号就返回 DWORD dwMilliseconds//等候时间INFINITE );bWaitAll-等候方式TRUE-表示所有句柄都有信号,才结束等候。FALSE-表示句柄中只要1个有信号,就结束等候。线程句柄执行的时候无信号,结束的时候有信号。线程同步原子锁相关问题多个线程对同一个数据进行原子操作,会产生结果丢失。比如执行++运算时。错误代码分析当线程A执行g_value++时,如果线程切换时间正好是在线程A再次被切换回来之后,会将原来线程A保存的值保存到g_values上,线程B进行的加法操作被覆盖。(汇编角度)当CPU在执行某个线程的时候,一旦时间到了它要离开,它会将这个线程已经执行到的位置保护起来,一般是压栈保护。将位置信息压到本线程的栈中。再来,先弹栈读取,恢复战场。图示解释/错误分析:第一个线程刚要将+1后的值赋给g_value,cpu给它的时间就到了,这时切换到线程2,线程2执行完毕,g_value == 1,切换回线程1,先弹栈恢复,执行完毕得到g_value == 1,这就丢失了一次数据。解决:使用原子锁函数,来++DWORD CALLBACK TestProc1(LPVOID pParam) for (int i = 0; i < 1000000; i++) //g_value++; InterlockedIncrement(&g_value); return 0; DWORD CALLBACK TestProc2(LPVOID pParam) for (int i = 0; i < 1000000; i++) //g_value++; InterlockedIncrement(&g_value); return 0; 使用原子锁函数InterlockedIncrement ...... 每一个操作符都有对应的一个原子锁函数原子锁的实现:直接对数据所在的内存进行操作,并且在任何一个瞬间只能有一个线程访问这块内存。(++并不是直接对内存进行操作,而是对寄存器进行操作,然后再赋值给变量。)锁的是数据所在的这块内存,并不是CPU。解释:先执行线程1,进来直接给g_value(内存)上锁,如果这时候CPU给的时间到了,那就压栈保护,然后转去执行线程2,线程2一看已经g_value已经被锁了,不会重复锁,发生阻塞,等到时间的耗尽,又转去执行线程1,弹栈恢复,完成执行完后,给g_value解锁,再去执行线程2,线程2就能给g_value上锁了......重复上述过程。简单来说就是,上了锁之后可以保证一个线程对该变量,完成一次完整的操作后,另一个线程再进行一次完整的操作。使用原子锁之后执行效率变慢,因为这两个线程老是对着睡觉。重要的是保证结果的正确性,如果保证不了正确性,宁可牺牲效率也要保证结果正确。局限性&优点只能对运算符进行原子锁。而且有大量函数。但是在所有的枷锁机制中,它的效率最高。互斥相关的问题多线程下代码或资源的共享使用互斥的使用一.创建互斥HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes,//安全属性 BOOL bInitialOwner,//初识的拥有者TRUE/FALSE LPCTSTR lpName//命名 );//创建成功返回互斥句柄互斥不光能对操作符进行枷锁,也能对其它东西进行枷锁,比如说锁定一段或一行代码。原子锁能解决的问题,互斥都能解决,但是互斥能解决的问题,原子锁不一定能解决。互斥句柄也是可等候句柄。特点:1.任何一个时间点上只能有一个线程拥有互斥。其它线程只能等待这个线程把互斥扔掉才能拥有。独占性和排他性、当任何一个线程都不拥有互斥,互斥句柄有信号。一旦某个线程有互斥,互斥句柄无信号。谁先等待互斥,谁先得到互斥。二.等到互斥WaitForSingleObject/WaitForMultipleObjects...互斥的等候遵循谁先等候谁先获取三.释放互斥BOOL ReleaseMutex( HANDLE hMutex//互斥句柄 );四.关闭互斥句柄CloseHandle代码示例:#include<windows.h> #include<iostream> using namespace std; HANDLE g_hMutex = 0;//接收互斥句柄 DWORD CALLBACK TestProc(LPVOID pParam) char* text = (char*)pParam; while (1) WaitForSingleObject(g_hMutex, INFINITE); for (int i = 0; i < strlen(text); i++) cout << text[i]; Sleep(333); cout << endl; ReleaseMutex(g_hMutex); return 0; DWORD CALLBACK TestProc1(LPVOID pParam) char* text = (char*)pParam; while (1) WaitForSingleObject(g_hMutex, INFINITE); for (int i = 0; i < strlen(text); i++) cout << text[i]; Sleep(333); cout << endl; ReleaseMutex(g_hMutex); return 0; int main(void) //创建互斥的这个主线程也不拥有它, g_hMutex = CreateMutex(NULL,FALSE,NULL); DWORD nID = 0; const char* text = "123"; HANDLE hTread = CreateThread(NULL, 0, TestProc, (void*)text, 0, &nID); const char* text1 = "abc"; HANDLE hTread1 = CreateThread(NULL, 0, TestProc1, (void*)text1, 0, &nID); getchar(); CloseHandle(g_hMutex); return 0; }解释:互斥句柄在主函数中创建出来,设备FALSE,谁都不拥有,互斥句柄有信号,等待抢占,假设CPU先执行线程1, 拿到互斥句柄,互斥句柄变为无信号,执行打印,到时间后,压栈保存,转去执行线程2,此时WaitForSingObject就无法通过,阻塞,等待时间结束,又执行线程1,恢复现场,假设执行完毕,线程1丢掉互斥,互斥句再次变为有信号,线程2WaitForSingObject通过,拿到互斥......。原子锁和互斥都是枷锁机制,实现的都是多个线程之间有排斥关系。 多个线程之间去竞争临界资源。下面这两个同步技术,事件和信号量,实现的是线程之间的协调工作关系。事件相关问题程序(线程)之间的通知的问题。两个线程要协调工作,它们两个就得通信。事件的使用1.创建事件HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes,//安全属性 BOOL bManualReset,//事件重置(复位(将事件变为无信号))方式,TRUE手动,FALSE自动 BOOL bInitialState,//事件初识状态,TRUE有信号 LPCTSTR lpName//事件命名 );//创建成功返回事件句柄2.等候事件WaitForSingleObject/WaitForMultipleObjects3.触发事件(将事件设置成有信号状态)BOOL SetEvent( HANDLE hEvent //事件句柄 )4.复位事件(将事件设置成无信号状态)BOOL ResetEvent( HANDLE hEvent//事件句柄 );5.关闭事件CloseHandle代码示例:#include<iostream> #include<Windows.h> using namespace std; HANDLE g_hEvent = 0;//接收事件句柄 DWORD CALLBACK PrintProc(LPVOID pParam) while (1) WaitForSingleObject(g_hEvent,INFINITE); //ResetEvent(g_hEvent);//将事件设置成无信号 cout << "*************" << endl; return 0; //通过事件进行通信 DWORD CALLBACK CtrlProc(LPVOID pParam) while (1) Sleep(1000); SetEvent(g_hEvent);//解救线程1 int main(void) //FALSE为自动复位,当经过WaitForSingleObject函数之后 //TRUE为手动复位,手动在WaitForSingleObject后ResetEvent g_hEvent = CreateEvent(NULL,FALSE,FALSE,NULL); DWORD nID = 0; HANDLE hThread[2] = { 0 }; hThread[0] = CreateThread(NULL,0,PrintProc,NULL,0,&nID); hThread[1] = CreateThread(NULL,0,CtrlProc,NULL,0,&nID); WaitForMultipleObjects(2,hThread,TRUE,INFINITE); return 0; }注意:小心事件死锁。信号量相关的问题类似于事件(不是原理类似时间,是作用类似事件),解决通知的相关问题。但提供一个计数器,可以设置次数。信号量的使用1.创建 信号量HANDLE CreateSemaphoreW( [in, optional] LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,//安全属性 [in] LONG lInitialCount,//初始化信号量数量(初识计数值) [in] LONG lMaximumCount,//信号量(计数器)的最大值 [in, optional] LPCWSTR lpName//命名 );//创建成功返回信号量句柄信号量的计数器不为0时,信号量句柄有信号。2.等候信号量WaitForSingleObject/WaitForMultipleObjects每等候一次,信号量减1,直到为0阻塞。3.给信号量指定计数值BOOL ReleaseSemaphore( [in] HANDLE hSemaphore,//信号量句柄 [in] LONG lReleaseCount,//释放数量 [out, optional] LPLONG lpPreviousCount//释放前原来信号量的数量,可以为NULL );4.关闭句柄CloseHandle 代码示例:#include<windows.h> #include<iostream> using namespace std; HANDLE h_handle = 0;//信号量句柄 DWORD CALLBACK TestProc(LPVOID pParam) while (1) WaitForSingleObject(h_handle, INFINITE); cout << "******" << endl; return 0; int main(void) h_handle = CreateSemaphore(NULL, 3, 10, NULL); DWORD nID = 0; HANDLE hThread = CreateThread(NULL, 0, TestProc, NULL, 0, &nID); getchar();//回车之后信号量数量改为5 ReleaseSemaphore(h_handle, 5, NULL); WaitForSingleObject(hThread, INFINITE); CloseHandle(h_handle); return 0;
Flask框架简介Flask是一个使用 Python 编写的轻量级 Web 应用框架。来源-百度百科MVC框架对比框架概述Flask全能框架、学习成本较大Django微框架、易学习、Jinja2模板、内置服务器、扩展丰富、易扩展Tornado高性能、异步处理、扩展不多Bottle小巧Flask为什么可以独立运行Werkzeug是一个WSGI工具包,也可以作为一个Web框架的底层库。示例:Werkzeug实现简单的web Serverfrom werkzeug.wrappers import Request,Response class Shortly(object): def __call__(self, environ,start_response): # start_response('200 ok',{('Content-Type','text/plain')}) # return [b"hello world"] request = Request(environ) text = "hello flask,%s"%(request.args.get('a','flask')) response = Response(text,mimetype="text/plain") return response(environ,start_response) if __name__ == "__main__": from werkzeug.serving import run_simple app = Shortly() run_simple("0.0.0.0",5000,app)CGI:CGI:通用网关接口,可以让一个客户端,从网页浏览器向执行在网络服务器上的程序请求数据。CGI描述了服务器和请求处理程序之间传输数据的一种标准。定义了客户端服务器之间如何传数据。FastCGI:快速通用网关接口,是一种让交互程序与Web服务器进行通信的协议。FastCGI是早期通用网关接口(CGI)的增强版本。FastCGI致力于减少网页服务器与CGI程序之间的互动开销,从而使服务器可以同时处理更多的网页请求。我们常见的Nginx、Apache都有实现该协议。WSGI:Web服务器网关接口,是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。自WSGI被开发出来以后,许多其他语言中也出现了类似接口。uWSGI:一个Web Server,即一个实现了WSGI的服务器。uwsgi:是uWSGI服务器实现的独有的协议。
解决:StatusCodeError: 403 - ......The request signature we calculated does not match the signature you provided. Check your key and signing method.......如果你确定PicGo的相关设置没有填错。可以尝试如下方法重新安装PicGo。重新创建用户,再设置图床。复制KeyId & KeySecret时,检查一下。
网络通信Socket通信的3要素通信的目的地址使用的端口号使用的传输层协议(如TCP、UDP)Socket通信模型简单的回声服务器实现服务端#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <string.h> #include <ctype.h> #include <arpa/inet.h> #define SERVER_PORT 666 int main(void){ int sock;//信箱 struct sockaddr_in server_addr; //创建信箱 sock = socket(AF_INET,SOCK_STREAM,0); //清空标签 bzero(&server_addr,sizeof(server_addr)); //写上地址和端口号 server_addr.sin_family = AF_INET; //选择协议族ipv4 server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址-几个网卡就有几个IP地址 server_addr.sin_port = htons(SERVER_PORT); //绑定端口号 //实现标签贴到收信的信箱上 bind(sock,(struct sockaddr*)&server_addr,sizeof(server_addr)); //邮箱一次性只能接收128封信件 listen(sock,128); printf("等待客户端链接\n"); int done = 1; while(done){ struct sockaddr_in client; int client_socket; int len; char client_ip[64]; char buf[256]; socklen_t client_addr_len; client_addr_len = sizeof(client); client_socket = accept(sock,(struct sockaddr*)&client,&client_addr_len); printf("客户端地址:%s\t端口:%d\n",inet_ntop(AF_INET,&client.sin_addr.s_addr,client_ip,sizeof(client_ip)),ntohs(client.sin_port)); //读取数据 len = read(client_socket,buf,sizeof(buf)-1); buf[len] = '\n';//read读过来的数据不会自动加字符串结束符 printf("收到[%d]:%s\n",len,buf); len = write(client_socket,buf,len);//发回去 printf("写完,len:%d",len); close(client_socket); return 0; }可以使用telnet来模拟客户端链接服务器。例如:客户端#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #define SERVER_PORT 666 #define SERVER_IP "127.0.0.1" int main(int argc, char* argv[]) { int sockfd; char* message; struct sockaddr_in servaddr; int n; char buf[64]; //echo_client "xxxx" ——所以判断是否为2 if (argc != 2) { fputs("Usage: ./echo_client message \n",stderr); exit(1); message = argv[1]; printf("message: %s\n",message); sockfd = socket(AF_INET,SOCK_STREAM,0); memset(&servaddr,'\0',sizeof(struct sockaddr_in)); servaddr.sin_family = AF_INET; //inet_pton(AF_INET,SERVER_IP,&servaddr.sin_addr); inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr); servaddr.sin_port = htons(SERVER_PORT); //转换为网络字节序 connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)); write(sockfd,message,strlen(message)); n = read(sockfd, buf, sizeof(buf)-1); if (n > 0) { buf[n] = '\n'; printf("receive: %s\n",buf); } else { perror("error!"); printf("finished!\n"); close(sockfd); return 0;
前言例如我想实现一个程序,用户先进行登录,登录成功后传递用户账号信息到下一个窗口中。这就需要我在验证用户登录成功之后,new出下一个窗口,在构造函数中传递参数,注意构造的位置,不要在当前窗口close()后再构造。正确使用 MainWindow* mainwindow = new MainWindow(gUserAccount); close(); mainwindow->show();因为调用close()会自动销毁窗口类对象。
操作系统接口命令界面接口程序接口联机命令接口联机命令的类型系统访问类磁盘操作类文件操作类目录操作类通信类其他命令联机命令的操作方式键入式:手动敲入选择式: 点击视窗型命令界面:交互窗口最终都会转化为命令。系统访问类键盘操作命令文件操作命令目录操作命令其他命令键盘终端处理程序命令解释程序的作用工作流程系统调用基本概念1.运行在不同的系统状态2.通过软中断进入3.返回问题4.嵌套调用系统调用的类型进程控制类系统调用文件操作类系统调用进程通信类系统调用系统调用的实现中断和陷入硬件机构系统调用号和参数的设置系统调用的处理步骤系统调用处理子程序的处理过程图形用户接口略总结系统调用是操作系统专门提供给应用程序的接口,也是应用程序获取操作系统服务的唯一途径。内部命令是由系统定义的、常驻内存的处理程序的集合。操作系统为用户提供了三种类型的用户接口。命令控制界面接口,图形用户界面接口,程序接口。系统态又叫管态,核心态。系统调用和一般调用的区别——状态的转变。
操作系统引论操作系统的目标、地位、作用操作系统的目标目前存在着多种类型的OS,不同类型的OS,其目标各所侧重。通常在计算机硬件上配置的OS,其目标有一下几点:有效性方便性可扩充性开放性操作系统的地位操作系统地位操作系统在计算机系统层次结构上起到了一个承上启下的作用,是所有软件中最靠近硬件的系统软件常被看做是计算机硬件的第一层扩充。操作系统的作用1.一般用户观点,OS作为用户与计算机硬件系统之间的接口。2.OS作为计算机系统资源的管理者OS是各类资源的管理者,计算机系统中各种各样的硬件和软件资源归纳起来可分为四类:处理器存储器I/O设备以及信息(数据和程序)它负责分配、回收及协调各程序和用户对资源的使用冲突,提高资源的利用率。3.虚拟机器观点从服务器用户的机器扩充的观点来看,OS为用户使用计算机提供了许多服务功能和良好的工作环境。用户不用直接使用硬件计算机(裸机),而是通过OS来控制和使用计算机,从而把计算机扩充为功能更强、使用更加方便的计算机系统(称为虚拟计算机)。它在虚拟机中充当管理员和协调员的觉得,管理计算机中的软硬件资源,并协调多任务、多进程的运行。4.作业组织的观点该观点认为,操作系统是计算机系统工作流程的组织者,它负责协调在系统中运行的各个应用软件的运行次序。操作系统负责组织、协调各作业的运行,报告执行结果或错误信息。减少了人工干预,提高了系统效率。这种工作方式有利于造价高并且性能强大的主机资源。操作系统的定义操作系统是计算机系统中的一个系统软件,管理和控制计算机系统的硬件和软件资源,合理地组织计算机的工作流程,以便有效利用这些资源为用户提供一个功能强、使用方便的工作环境,从而在计算机和用户之间起到接口作用。推动操作系统发展的主要动力器件的不断更新换代方便用户不断提供计算机资源利用率计算机体系结构的不断发展操作系统的发展与分类第一代计算机1946-1955:计算机主要由电子管组成,无操作系统。缺点:1.用户独占全机2.CPU等待人工操作脱机输入/输出方式增加外围机与磁盘来辅助。优点:1.减小了CPU的空闲时间2.提高了I/O速度单道批处理系统特征:1.自动性2.顺序性3.单道性4.交互性差什么是单道批处理?外存中存储一批作业,用监控程序,在收集的一批作业中,自动取其中的一道,进去到主机中,由主机进行处理,结果输入到磁盘上,监控程序再取一道,进入主机,开始处理...单刀批处理系统,某一时刻,内存中只有一道作业在执行。上一个作业未完成,下一个作业无法进入,哪个作业先进去到内存,哪个就先被执行。多道批处理系统多道:允许多个程序同时存在于主存,按照某种原则分配处理机,逐个执行这些程序。输入,计算,输出。三项工作在多道任务上存在时间上的重叠。批处理后备队列一批作业在外存中等待执行所排成的队列。就绪队列等待进去cpu执行的队列。处理机的自动切换CPU为一个程序服务,转变为为另一个程序服务的过程。(多道批处理系统下)单道与多道对比多道批处理系统,宏观上多个程序都在同时执行。对于单处理机来说CPU只有1个,真正在运行的程序只有1个。不同策略对应着不同的调度算法。区别:内存中有一道程序或多道程序。引入多道程序设计技术带来的好处:1.提高CPU的利用率2.可提高内存和I/O设备利用率3.增加系统吞吐量多道批处理系统的特征多道性无序性调度性多道批处理系统的优缺点系统利用率高系统吞吐量大平均周转时间长无交互能力多道批处理系统需要解决的问题处理机管理问题内存管理问题I/O设备管理问题文件管理问题作业管理问题分时操作系统如果说,推动多道批处理系统形成和发展的主要动力,是提高资源利用率和系统吞吐量,那么,推动分时系统形成和发展的主要动力,是用户的需求。或者说,分时需用是为了满足用户需求所形成的一种新型OS。分时操作系统的实质:在多道程序设计的基础上,为多个用户配置一个联机主机。所谓分时操作系统是指将多个用户程序装入内存,系统把CPU的运行时间分为一个个的时间段,每个时间段称为一个时间片。时间片大小通常为几十毫秒,用户程序轮流获得CPU的时间片。当系统分给用户程序的时间片用完后,系统强行回收CPU,该用户程序等待下次获得时间片时再继续执行。分时操作系统与多道批处理系统之间,有着截然不同的性能差别。用户的需求具体表现在以下几个方面:人机交互共享主机便于用户上机分时系统实现中的关键问题及时接收及时处理补充:(1)引入缓冲区。(2)分时系统并没有经过作业调度,在用户登录的时候在内存就开辟好了自己的空间。用户的命令直接到达内存。分时系统的特征多路性独立性及时性交互性实时系统所谓”实时“,是表示”及时“,而实时系统是指系统能及时(即时)相应外部事件的请求,在规定的时间内完成对该事件的处理,并控制所有实时任务协调一致地运行。外部设备所发出的激励信号并无明显的周期性,但都必须联系着一个截止时间。总结批处理系统工作方式:将用户意图、数据、程序利用系统提供的作业控制命令提交给操作员,操作员将其输入外存,由OS控制、调度各作业的运行,最后输入结果。是一种非人工的干预方式。有单道和多道批处理系统。目标:提高作业的吞吐量,同时兼顾作业的周转时间。特征:“批量”,成批的完成。优点:作业吞吐量大,资源利用率高,OS开销较小。缺点:用户使用起来不太方便;交互能力较弱;作业平均周转时间长。分时系统分时:把CPU的运行时间划分为若干个时间片,并把这些时间片依次轮流分配给各终端用户程序。分时系统:多个用户同时使用同一台计算机,系统能分时轮流的为各个终端用户服务,并及时地对用户的请求予以响应。设计目标:对用户响应的及时性,及时地响应和服务于联机用户。特点:同时性(多路性);及时性;独立性(独占性);交互性。实时系统按场合作用,分两类。实时控制系统;实时信息处理系统。目标:实时响应及处理的能力和高可靠性。特征:交互性;安全可靠性;多路性。操作系统的基本特性并发与并行并行性是指两个或多个事件在统一时刻发生。并发性是指两个或多个时间在同一时间间隔内发生。共享在os环境下,所谓共享是指系统中的资源可供内存中多个并发执行的进程(线程)共同使用。主要两种资源共享方式互斥共享方式同时共享方式并发和共享的关系并发和共享是操作系统的两个最基本的特征,它们又是互为存在的条件。### 虚拟技术现在操作系统中主要有两种虚拟技术:时分复用技术和空分复用技术异步性异步性是指在多道程序设计环境下,系统中每到程序的推进时间、顺序以及完成时间由于受其运行环境的影响是不确定的、不可预知的。有的文献也成异步性为不确定性、随机性。操作系统的主要功能处理机管理内存管理设备管理文件管理接口功能
windows访问虚拟机linux文件教程使用版本vmware 15.5ubuntu server 14.04.4samba服务器只能用在局域网。Ubuntu安装samba服务器确认安装: dpkg -l | grep samba安装: sudo apt-get install samba samba-common卸载: sudo apt-get autoremove sambasamba服务器配置sudo vi /etc/samba/smb.conf在文件最后添加注意用户名[Share]comment=This is samba dirpath=/home/你的用户名/writable=yesbrowseable=yes增加samba用户sudo smbpasswd -a 你的用户名重启samba关闭Samba服务器: sudo service smbd stop启动Samba服务器: sudo service smbd start在window文件窗口地址栏输入\\你虚拟机的ip地址 输入账号密码进入示例不同平台tab大小可能不同。
QML与C++混合编程使用QQuickViewpro文件中添加quick模块#include<QApplication> #include<QQuickView> int main(int argc,char* argv[]){ QApplication app(argc,argv); //加载qml文件到视图 QQuickView view; view.setSource(QUrl("column.qml")); view.show(); return app.exec(); }使用QQmlApplicationEngine创建Qt Quick Application#include <QGuiApplication> #include <QQmlApplicationEngine> int main(int argc, char *argv[]) //支持高频率屏幕 QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); //创建qml引擎 QQmlApplicationEngine engine; //引擎加载qml文件 engine.load(QUrl(QStringLiteral("qrc:/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); QML调用C++函数#ifndef APPLICATIONDATA_H #define APPLICATIONDATA_H #include <QObject> #include <QDateTime> class ApplicationData : public QObject Q_OBJECT public: explicit ApplicationData(QObject *parent = nullptr); //QML中调用C++函数,这个函数需以Q_INVOKABLE进行标记——注册一个方法到QML //或者该函数是Qt的槽函数 Q_INVOKABLE QDateTime getCurrentDateTime()const{ return QDateTime::currentDateTime(); signals: public slots: #endif // APPLICATIONDATA_H #include <QApplication> #include <QQuickView> #include "applicationdata.h" #include <QQmlContext> int main(int argc,char* argv[]){ QApplication app(argc,argv); QQuickView view; //将C++对象作为属性注册到QML ApplicationData data; view.rootContext()->setContextProperty("applicationData",&data); view.setSource(QUrl("item.qml")); view.show(); return app.exec(); import QtQuick 2.0 Text{ text: applicationData.getCurrentDateTime() C++调用QML函数import QtQuick 2.0 Text{ text: applicationData.getCurrentDateTime() function qmlFunction(msg){ console.log("Message comes:",msg) return "abc" #include <QApplication> #include <QQuickView> #include "applicationdata.h" #include <QQmlContext> #include <QQmlApplicationEngine> #include <QQmlComponent> int main(int argc,char* argv[]){ QApplication app(argc,argv); QQmlApplicationEngine engine; QQmlComponent component(&engine,"item.qml"); QObject* object = component.create(); QVariant msg = "Hello qml!"; QVariant returnedValue; //invokeMethod的 Q_RETURN_ARG Q_ARG参数必须指定为QVariant类型 QMetaObject::invokeMethod( object, "qmlFunction", Q_RETURN_ARG(QVariant,returnedValue),//用于接收返回值 Q_ARG(QVariant,msg));//用于传递函数函数 qDebug()<<"QML function returned:"<<returnedValue.toString(); return app.exec(); }C++接收QML信号import QtQuick 2.0 Item { id:item width:100 height: 100 signal qmlSignal(string msg) MouseArea{ anchors.fill: parent onClicked: item.qmlSignal("Hello qml") #include <QApplication> #include <QQuickView> #include<QQuickItem> #include "myclass.h" int main(int argc,char* argv[]){ QApplication app(argc,argv); QQuickView view(QUrl::fromLocalFile("quicksignal.qml")); QObject* item = view.rootObject(); MyClass myClass; QObject::connect(item, SIGNAL(qmlSignal(QString)), &myClass, SLOT(slotPrint(QString))); view.show(); return app.exec(); #ifndef MYCLASS_H #define MYCLASS_H #include <QObject> #include <QDebug> class MyClass : public QObject Q_OBJECT public: explicit MyClass(QObject *parent = nullptr); signals: public slots: void slotPrint(QString s){ qDebug()<<s; #endif // MYCLASS_H
Qt图形视图框架、动画框架Qt提供了图形视图框架(GraphicsView Framework)、动画框架(The Animation Framework)、状态机框架(The State Machine Framework)来实现更加高级的图形与动画应用。使用这些框架可以快速设计出动态GUI应用和各种动画、游戏程序。图形视图框架提供了基于图像项模型的模型视图编程方法,主要由场景、视图和图形项这三部分组成,这三部分分别由QGraphicsScene、QGraphicsView、QGraphicsItem这三个类来表示。多个视图可以查看一个场景,场景中包含了各种几个形状的图像项。框架中包含一个事件传播架构,提供了和场景中的图形项进行精确的双精度交互能力,如将场景时间传递给图形项,也可以管理图形项目之间的事件传播。图形项可以处理键盘事件,鼠标事件,如鼠标按下事件、移动、释放及双击事件,还可以跟踪鼠标的移动。图形视图框架使用一个BSP(Binary Space Partitioning)树来快速发现图形项,正因为如此,可实时显示巨大的场景,甚至包括上百万个图形项。场景、视图、图像项场景一个场景分为3层:图形项层、前景层。场景绘制顺序:背景层->图像项层->场景层场景作用:提供用于管理大量图像项的高速接口;传播事件到每一个图形项;管理图像项的状态,如选择和处理焦点;提供无变换的渲染功能,主要用于打印;常用接口:QGraphicsScene::setForegroundBrush()//设置前景刷 QGraphicsScene::setBackgroundBrush()//设置背景刷 QGraphicsScene::itemAt() //返回指定坐标处最顶层的图形项 QGraphicsScene::selectedI tems() //获取当前选取的所有图形项的列表 QGraphicsScene::setFocusItem() //为图形项设置焦点 QGraphicsScene::focusItem() //获取当前获得焦点的图形项 QGraphicsScene::render() //将场景中的一部分渲染到绘图设备上 QGraphicsScene::setSelectionArea() //传递一个任意形状来选择场景中指定的图形项视图QGraphicsView提供了视图部件,它用来使场景中的内容可视化。可连接多个视图到同一个场景来为相同的数据集提供多个视图,视图部件是一个可滚动的区域,提供了一个滚动条来浏览大场景。常用接口:QGraphicsView::setForegroundBrush() //设置前景色 QGraphicsView::setBackgroundBrush() //设置背景色 QGraphicsView::resize() //重置视图大小 QGraphicsView::show() //显示视图 QGraphicsView::setDragMode() //拖动场景,常以QGraphicsView::ScrollHandDrag为参数使光标变为手掌形状,从而拖动场景,若以QGraphicsView::RubberBandDrag为参数可在视图上使用鼠标拖出橡皮筋框来选择图形项示例:#include <QApplication> #include <QGraphicsScene> #include <QGraphicsRectItem> #include <QGraphicsView> int main(int argc,char* argv[]){ QApplication app(argc,argv); //新建场景 QGraphicsScene scene; //创建矩形图形项 QGraphicsRectItem *item = new QGraphicsRectItem(0,0,100,100); //场景添加图像项 scene.addItem(item); //为场景创建视图 QGraphicsView view(&scene); //设置场景前景色 view.setForegroundBrush(QColor(255,21,23,100)); //设置背景 view.setBackgroundBrush(QPixmap("picture.jpg")); view.resize(400,400); view.show(); return app.exec(); 图形项QGraphicsItem是场景中图像项的基类,图形项框架为典型的形状提供了标准的图像项,比如矩形QGraphicsRectItem、椭圆QGraphicsEllipseItem、文本QGraphicsTextItem,自定义图形项能发挥QGraphicsItem的强大功能。图形项支持如下功能:鼠标按下、移动、释放、双击、悬停、滚轮和右键菜单事件键盘输入焦点和键盘事件拖放事件分组,使用QGraphicsItemGroup通过parent-child关系来实现。碰撞检测常用接口:QGraphicsItem::boundingRect() //返回绘图区域 QGraphicsItem::paint() //绘图操作坐标系统图形视图框架基于笛卡尔坐标系统,一个图像项在场景的位置和几何形状由X坐标和Y坐标来表示。图形视图框架中有3个有效的坐标系统;图形项坐标、场景坐标、视图坐标。为方便使用,图像视图框架提供了一些便捷函数来完成3个坐标系统的映射,进行绘图时,场景坐标对应QPainter的逻辑坐标,视图坐标对应设备坐标。图形项坐标图像项使用自己的本地坐标系统,坐标通常以它们的中心为原点(0,0),而这也是所有变换的中心。当创建一个自定义的图形项时,只需要考虑图形项的坐标系统,QGraphicsScene和QGraphicsView会完成其它所有的转换。图像项的位置是指图像项的原点在其父图像项或场景中的位置。如果没有图像项,则为顶层图像项,其均会在场景的坐标系统中。所有的图像项都会使用确定的顺序来进行绘制,这个顺序也决定了单机场景时哪个图像项会先获得鼠标的输入。所有的图像项都包含一个z值来设置他们的堆叠顺序,一个图像项的z值默认为0,可使用QGraphicsItem::sizeZValue()来改变一个图像项的z值。场景坐标场景坐标是所有图形项的基本坐标系统,坐标的原点在场景的中心。视图的坐标视图的坐标就是部件的坐标,视图坐标的每一个单位对应一个像素,原点总是在QGraphicsView视口的左上角,而右下角是宽高。所有的鼠标事件和拖放事件都是使用视图坐标来接收的。图形视图框架的映射函数:事件处理与传播图形视图框架中的事件都是由视图进行接收的,然后传递给背景,再由背景传递给响应的图像项。对于键盘事件,会传递给获得焦点的图像项,若场景中没有获得焦点,则键盘事件被丢弃。可通过setFocus()获取焦点。一个图像项可以接收悬停事件,当鼠标进入它的区域之中时,它就会收到一个QGraphicsSceneHoverEnter事件,鼠标在图像项的区域移动时,QGraphicsScene就会向该图像项发送GraphicsSceneHoverLeave事件,可通过QGraphicsItem::setAcceptHoverEvents()视图图像项接收悬停事件(默认不接收)。碰撞检测图像视图框架提供了图像项之间的碰撞检测,碰撞检测可以使用两种方法来实现:1.重写QGraphicsItem::shape()函数来返回图像项准确的形状,然后使用collidesWithItem()接口通过两个图像项形状之间的交集来判断是否发生碰撞。如果没有重新实现shape()函数,则它会调用默认boundingRect()函数返回一个简单的矩形。2.重写collidesWithItem()来提供一个自定义的图形碰撞算法。collidesWithItem()判断是否与指定的图形项进行了碰撞; collidesWithPath()判断是否与指定的路径碰撞;collidingItems()获取与该图形项碰撞的所有图形项的列表;下面这几个函数都有一个Qt::ItemSelectionMode参数来指定怎样进行图形项的选取,选取模式如下:动画框架动画框架的目的是提供一种简单的方法来创建平滑的、具有动画效果的GUI界面。该框架是通过控制Qt的属性来实现动画的,可以应用在窗口的部件和其他QOBject对象上,也可以应用在图像视图框架中。动画框架的主要类关系图如下:缓和曲线使用 enum QEasingCurve::Type来设置缓和曲线,枚举值如下:动画组使用QAnimationGroup类可以实现复杂的动画,它的两个子类QSequentialAnimationGroup,QParalleAnimationGroup分别提供了串行动画组和并行动画组。状态机框架状态机框架提供一些类来创建和执行状态图,状态图为一个系统如何对外界进行反应提供了一个图形化模型,该模型通过定义一些系统可能进入的状态以及系统怎样从一个状态切换到另一个状态来实现的。Qt的事件系统来驱动状态机。
UDP、TCPUDPUDP(User Datagram Protocol,用户数据报协议)UDP是一个轻量级、不可靠、面向数据报的、无连接的协议,多用与可靠性不严格,不是非常重要的传输。QUdpSocket类继承自QAbstractSocket,用来发送和接收UDP数据报。“Socket”即套接字,即IP地址+端口号。其中IP地址指定了网络中的一台主机,端口号则指定了该主机上的一个网络程序,使用套接字即可实现网络上的两个应用程序之间的通信。QUdpSocket支持IPv4广播,要广播数据报,则只需要发送到一个特殊的地址,QHostAddress::Broadcast(255.255.255.255),数据报一般建议发送字节数小于512字节。端口号选择1024~65535(1024一下的常用作保留端口号,如FTP常用端口号21,Telnet常用端口号23,DNS域名服务器常用端口号53等)。示例:发送端#include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) ui->setupUi(this); m_sender = new QUdpSocket(this); Widget::~Widget() delete ui; void Widget::on_pushButton_clicked() QByteArray dataGram = ui->lineEdit->text().toUtf8(); m_sender->writeDatagram(dataGram.data(), dataGram.size(), QHostAddress::Broadcast, 6666); }接收端#include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) ui->setupUi(this); m_receiver = new QUdpSocket(this); m_receiver->bind(6666,QUdpSocket::ShareAddress); //有数据来了,就处理。 connect(m_receiver,&QUdpSocket::readyRead,this,&Widget::processData); Widget::~Widget() delete ui; void Widget::processData() //有未处理的数据报 while(m_receiver->hasPendingDatagrams()){ QByteArray dataGram; dataGram.resize(m_receiver->pendingDatagramSize()); m_receiver->readDatagram(dataGram.data(),dataGram.size()); ui->label_2->setText(dataGram); TCPTCP(Transmission Control Protocol,传输控制协议)多个网络协议包括HTTP和FTP都是基于TCP协议,TCP是面向数据流和连接的可靠的传输协议。QTcpSocket继承自QAbstractSocket,与QUdpSocket传输的数据报不同的是,QTcpSocket传输的是连续的数据流,尤其适合连续的数据传输,TCP一般分为客户端和服务端,即C/S(Client/Server)模型。QTcpSocket代表了两个独立的数据流,一个用来读取数据,一个用来写入数据,分别采用QTcpSocket::read()及QTcpSocket::write()操作,读取数据前先调用QTcpSocket::bytesAvaliable来确定已有足够的数据可用。QTcpServer处理客户端的连接,可通过QTcpServer::listen()监听客户端发来的连接请求,每当有客户端连接时会发射newConnection()信号,QTcpSocket可用于读取客户端发来的数据报,亦可以发送数据报。示例:客户端#include "widget.h" #include "ui_widget.h" #include <QDebug> const int gTcpPort = 8888; Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) ui->setupUi(this); m_tcpSocket = new QTcpSocket(this); //收到数据开始处理 connect(m_tcpSocket,&QTcpSocket::readyRead, this,&Widget::onReadMessage); connect(m_tcpSocket,SIGNAL(SocketError), this,SLOT(onDisplayError(QAbstractSocket::SocketError))); ui->hostLineEdit->setText("127.0.0.1"); ui->portLineEdit->setText(QString::number(gTcpPort)); Widget::~Widget() delete ui; void Widget::onReadMessage(){ QByteArray bt; bt.resize(m_tcpSocket->bytesAvailable()); m_tcpSocket->read(bt.data(),bt.size());//读取到bt中 ui->msgLabel->setText(bt); void Widget::onDisplayError(QAbstractSocket::SocketError e){ qDebug()<<"SocketError:"<<e<<endl <<m_tcpSocket->errorString(); void Widget::on_connectBtn_clicked() m_tcpSocket->abort();//终止 //ip地址,端口 m_tcpSocket->connectToHost(ui->hostLineEdit->text(),gTcpPort);//链接到主机 void Widget::on_sendBtn_clicked() m_tcpSocket->write(ui->sendLineEdit->text().toUtf8()); m_tcpSocket->flush();//清空缓冲区 服务端#include "widget.h" #include "ui_widget.h" #include <QDebug> Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) ui->setupUi(this); m_tcpServer = new QTcpServer(this); m_tcpSocket = new QTcpSocket(this);//保存链接进来的客户端 //监听指定客户端发来的请求 if(!m_tcpServer->listen(QHostAddress::Any,8888)){ qDebug()<<m_tcpServer->errorString(); close(); //有新的链接请求 connect(m_tcpServer,&QTcpServer::newConnection,this,&Widget::onNewConnect); // 给链接的客户端反馈信息 connect(m_tcpServer,&QTcpServer::newConnection,this,&Widget::onSendMessage); Widget::~Widget() delete ui; void Widget::onNewConnect() m_tcpSocket = m_tcpServer->nextPendingConnection();//获取当前新来的链接 //断开链接——销毁 connect(m_tcpSocket,&QTcpSocket::disconnected,m_tcpServer,&QTcpSocket::deleteLater); //来消息后发送readyRead信号,与读取数据方法进行链接 connect(m_tcpSocket,&QTcpSocket::readyRead,this,&Widget::onReadMessage); void Widget::onSendMessage() QString str = "你好,client"; m_tcpSocket->write(str.toUtf8());//写入socket ui->label->setText("发送数据成功!"); void Widget::onReadMessage() QByteArray bt = m_tcpSocket->readAll(); ui->readLabel->setText(bt);
Lambda表达式Qt5中有全新的信号与槽语法,在Qt4基础上提高了信号与槽的灵活性,允许使用任意函数作为槽函数。但如果想更好地执行异步代码,来函数名都不想定义,则可以通过Lambda表达式来达到这个目的。Lambda表达式是C++11新增加的特性。Lambda表达式的引入符引入符说明[]不捕获任何变量[=]以传值的方式捕获所有外部变量[&]以传引用的方式捕获所有外部变量[x,&y]x以传值的方式捕获,y以传引用的方式捕获[=,&x]x以传引用的方式捕获,其余变量以传值的方式捕获[&,x]x以传值的方式捕获,其余变量以传引用的方式捕获格式:[捕获列表] (参数列表-可以省略) (函数选项-可以省略) -> (返回值类型-可以省略){函数体}详见—链接示例1:收到拒绝文件信号,执行的槽方法功能就是直接返回,没有进行任何的外部变量捕获。connect(recvFile,&ReceiveFile::refuseFile(),[](){ return; });示例2:传值方式捕获外部变量在函数体内部使用connect(label,&QClickLabel::clicked,[row,column,colorList]{ NotifyManager::getInstance()->notifyOtherWindowChangeSkin(colorList.at(row *4 +column)); })示例3:传址方式捕获使用this的updateSearchStyle()成员函数connect(NotifyManager::getInstance(), &NotifyManager::signalSkinChanged,[this]() { updateSearchStyle(); });注意事项:捕获列表不允许变量重复传递,如[=,a]则以传值方式捕获所有外部变量,又重复捕获a,就会报错。
QString、QLatin1String、QStringLiteralQStringQString:QString提供了一个Unicode字符串,QString存储的是一串QChar,而QChar提供了16位的Unicode字符,后台使用隐式共享减少了内存的开销。假设QObject::setObject函数参数列表是QString类型,给它传递参数"test",这里有一个通过构造产生的从const char*到QString的隐式转换,产生一个新的QString对象,字符串数据拷贝,并将字符串从UTF-8转为UTF-16的unicode字符。这样增大了内存的开销,减慢了运行效率。解决——重载运算符。重载运算可以直接操作原始char*,不必为了我们的字符串常量去创建临时QString对象。QString提供了很多的成员函数使用const char*而不是QString类型的字符串实现重载,包括拷贝构造、赋值运算、比较运算及其他的函数,如插入insert()、替换replace()、和indexOf()。这些函数经过优化后避免在函数调用的使用从const char* 数据构造一个QSting类型的对象。如果应用程序定义了QT_NO_CAST_FROM_ASCII宏DEFINES += QT_NO_CAST_FROM_ASCII则程序无法使用const char* 相关的API函数。QLatin1String在Qt5中,我们把char* 字符串的默认编码改成了UTF-8。但是相对纯ASCII或者latin1而言,很多算法处理UTF-8编码数据的时候会慢很多。为避免不必要的内存开销(构造QString对象时涉及的内存分配、拷贝数据及数据转换成QString的内部编码这些开销)QLatin1String产生了,它是const char* 的一层薄薄的封装。它将字符串传递给函数时,函数通过QLatin1String的重载避免转换成QString对象,这样做的效率显著提高。一些接收QLatin1String为参数的重载函数能够直接对纯latin1数据进行处理,不必进行编码转换。QStringLiteral但是,函数只能接受QString类型的参数时,无论我们给一个字面字符串或QLatin1String,都会隐式构造一个临时的QString对象,构造这个对象需要在栈上申请一定的内存空间,然后把字符串拷贝过去,如果这样的调用比较多,那还是一笔不小的开销。此时,我们可以使用QStringLiteral来减小这个开销。QStringLiteral其实是一个宏,从字符串常量创建QString对象的宏。宏在编译时,从字符串文字生成QString数据,QString的内部数据将在编译时生成,在运行时不会发生任何转换或内存分配,使用QStringLiteral来代替C++中的双重数值传递将会在编译的时候显著的提升运行效率。优点:使用QStringLiteral而不是双引号的普通C++的字符串可显著加快从编译时已知的数据创建QString示例的速度,相对来说有QLatin1String参数重载的比使用QStringLieral更高效。总结:参数能使用const char*类型或QLatin1String类型的地方使用相应的参数。参数需要QStirng参数类型的地方,若值不被修改,则使用QStirngLiteral宏。参数需要QString类型且QStirng可能被修改,则直接使用QString或隐式转换。
基本图形绘制QPainter绘制基本图形注意:QPainter画弧度所使用的角度值,是以1/16°为单位的,在画弧度的时候1°用16表示。例如:30°——30 x 16兼容模式QPainer::CompositionMode例如:可用于图片叠加,达到你想要的效果。QPainterPath绘制简单图形QPainterPath为QPainter类提供一个存储容器,里面包含了所要绘制的内容的集合及绘制顺序,如长方形、多边形、曲线等各种任意图形。当需要绘制此预先存储在QPainterPath对象中的内容时,只需调用QPainter类的DrawPath()即可,如addRect()加入一个方形,addEllipse加入一个椭圆形,addText()加入文本。画笔笔刷风格画笔风格——Qt::PenStyle笔帽风格——Qt::PenCapStyleQt::SquareCap 方形线端,不覆盖线的端点Qt::FlatCap 方形线端,覆盖线的端点,并以线框一半向外延伸Qt::RoundCap 圆线端连接点——Qt::PenJoinStyleQt::BevelJoin 两条线相汇形成方形连接Qt::MiterJoin 两条线相汇形成尖角连接Qt::RoundJoin两条线相汇形成圆角连接填充模式——Qt::FillRuleQt::OddEventFill填充规则:从图形中某一点画一条水平线到图形外,若这条线与图形边线的交点为奇数则说明此点位与图形的内部;若交点为偶数则此点在图形的外部。Qt::WindingFill填充规则:从图形中某一点画一条水平线到图形外,每个交点外边线方向可能向上、向下、方向相反的相互抵消,若结果不为0表示此点在图形内,若为0则在图形外。其中边线的方向是由QPainterPath创建时根据描述的顺序决定的,如果采用addRect()或addPolygon()等函数加入的图形默认为顺时针方向。铺展效果——Qt::Gradient::TypeQGradient::PadSpread :默认风格QGradient::RepeatSpread:线性渐变QGradient::ReflectSpread:圆形渐变画刷风格——Qt::BrushStyle示例mainwidget.h#ifndef MAINWIDGET_H #define MAINWIDGET_H #include <QWidget> #include<paintarea.h> #include<QLabel> #include<QComboBox> #include<QSpinBox> #include<QPushButton> #include<QGridLayout> class MainWidget : public QWidget Q_OBJECT public: MainWidget(QWidget *parent = 0); ~MainWidget(); void initWidgets(); private slots: void ShowShape(int value); void ShowPenColor(); void ShowPenWidth(int value); void ShowPenStyle(int value); void ShowPenCap(int value); void ShowPenJoin(int value); void ShowFillRule(int value); void ShowSpreadStyle(int value); void ShowBrushColor(); void ShowBrush(int value); private: PaintArea* m_paintArea; //绘图区域 QLabel* m_shapeLabel ; //形状 QComboBox* m_shapeCombox; QLabel* m_penWidthLabel;//线宽 QSpinBox *m_penWithSpinBox; QLabel* m_penColorLabel;//画笔颜色 QFrame* m_penColorFrame; QPushButton* m_penColorBtn;//确认选择画笔颜色按钮 QLabel* m_penStyleLabel;//画笔风格 QComboBox* m_penStyleComboBox; QLabel* m_penCapLabel;//笔帽风格 QComboBox* m_penCapComboBox; QLabel* m_penJoinLabel;//画笔连接点 QComboBox* m_penJoinComboBox; QLabel* m_fillRuleLabel;//填充模式 QComboBox* m_fillRuleComboBox; QLabel* m_spreadLabel;//铺展效果 QComboBox* m_spreadComboBox; QGradient::Spread m_spread; QLabel* m_brushColorLabel;//画刷颜色 QFrame* m_brushColorFrame; QPushButton* m_brushColorBtn; QLabel* m_brushStyleLabel;//画刷风格 QComboBox* m_brushStytleComboBox; QGridLayout* m_rightLayout; #endif // MAINWIDGET_Hmainwidget.cpp#include "mainwidget.h" #include<QColorDialog> MainWidget::MainWidget(QWidget *parent) : QWidget(parent) initWidgets(); MainWidget::~MainWidget() //初始化部件 void MainWidget::initWidgets() m_paintArea = new PaintArea; m_shapeLabel = new QLabel(tr("形状")); m_shapeCombox = new QComboBox; m_shapeCombox->addItem(tr("Line"),PaintArea::Line); m_shapeCombox->addItem(tr("Rectangle"),PaintArea::Rectangle); m_shapeCombox->addItem(tr("RoundedRect"),PaintArea::Rectangle); m_shapeCombox->addItem(tr("Ellipse"),PaintArea::Ellipse); m_shapeCombox->addItem(tr("Polygon"),PaintArea::Polygon); m_shapeCombox->addItem(tr("Polyline"),PaintArea::Polyline); m_shapeCombox->addItem(tr("Points"),PaintArea::Points); m_shapeCombox->addItem(tr("Arc"),PaintArea::Arc); m_shapeCombox->addItem(tr("Path"),PaintArea::Path); m_shapeCombox->addItem(tr("Text"),PaintArea::Text); m_shapeCombox->addItem(tr("Pixmap"),PaintArea::Pixmap); connect(m_shapeCombox,SIGNAL(activated(int)),this,SLOT(ShowShape(int))); m_penColorLabel = new QLabel(tr("画笔颜色")); m_penColorFrame = new QFrame; m_penColorFrame->setFrameStyle(QFrame::Panel | QFrame::Sunken);//设置风格 m_penColorFrame->setAutoFillBackground(true); m_penColorFrame->setPalette(QPalette(Qt::black)); m_penColorBtn = new QPushButton(tr("更改")); connect(m_penColorBtn,SIGNAL(clicked(bool)),this,SLOT(ShowPenColor())); m_penWidthLabel = new QLabel(tr("画线宽度")); m_penWithSpinBox = new QSpinBox; m_penWithSpinBox->setRange(1,20); connect(m_penWithSpinBox,SIGNAL( valueChanged(int)),SLOT(ShowPenWidth(int))); m_penStyleLabel = new QLabel(tr("画笔风格")); m_penStyleComboBox = new QComboBox; m_penStyleComboBox->addItem(tr("SolidLine"),static_cast<int>(Qt::SolidLine )); m_penStyleComboBox->addItem(tr("DashLine"),static_cast<int>(Qt::DashLine)); m_penStyleComboBox->addItem(tr("DotLine"),static_cast<int>(Qt::DotLine)); m_penStyleComboBox->addItem(tr("DashDotLine"),static_cast<int>(Qt::DashDotLine )); m_penStyleComboBox->addItem(tr("DashDotDotLine"),static_cast<int>(Qt::DashDotDotLine )); m_penStyleComboBox->addItem(tr("CustomDashLine"),static_cast<int>(Qt::CustomDashLine)); connect(m_penStyleComboBox,SIGNAL(activated(int)),this,SLOT(ShowPenStyle(int))); m_penCapLabel = new QLabel(tr("画笔笔帽")); m_penCapComboBox = new QComboBox; m_penCapComboBox->addItem(tr("SquareCap"),Qt::SquareCap); m_penCapComboBox->addItem(tr("FlatCap"),Qt::FlatCap); m_penCapComboBox->addItem(tr("RoundCap"),Qt::RoundCap); connect(m_penCapComboBox,SIGNAL(activated(int)),this,SLOT(ShowPenCap(int))); m_penJoinLabel = new QLabel(tr("画笔连接点")); m_penJoinComboBox = new QComboBox; m_penJoinComboBox->addItem(tr("BevelJoin"),Qt::BevelJoin); m_penJoinComboBox->addItem(tr("MiterJoin"),Qt::MiterJoin); m_penJoinComboBox->addItem(tr("RoundJoin"),Qt::RoundJoin); connect(m_penJoinComboBox,SIGNAL(activated(int)),this,SLOT(ShowPenJoin(int))); m_fillRuleLabel = new QLabel(tr("填充模式")); m_fillRuleComboBox = new QComboBox; m_fillRuleComboBox->addItem(tr("Odd Even"),Qt::OddEvenFill); m_fillRuleComboBox->addItem(tr("Winding"),Qt::WindingFill); connect(m_fillRuleComboBox,SIGNAL(activated(int)),this,SLOT(ShowFillRule(int))); m_spreadLabel = new QLabel(tr("铺展效果")); m_spreadComboBox = new QComboBox; m_spreadComboBox->addItem(tr("PadStread"),QGradient::PadSpread); m_spreadComboBox->addItem(tr("RepeatSpread"),QGradient::RepeatSpread); m_spreadComboBox->addItem(tr("ReflectSpread"),QGradient::ReflectSpread); connect(m_spreadComboBox,SIGNAL(activated(int)),this,SLOT(ShowSpreadStyle(int))); m_brushColorLabel = new QLabel(tr("画刷颜色")); m_brushColorFrame = new QFrame; m_brushColorFrame->setFrameStyle(QFrame::Panel | QFrame::Sunken); m_brushColorFrame->setAutoFillBackground(true); m_brushColorFrame->setPalette(QPalette(Qt::green)); m_brushColorBtn = new QPushButton(tr("更改")); connect(m_brushColorBtn,SIGNAL(clicked()),this,SLOT(ShowBrushColor()) ); m_brushStyleLabel = new QLabel(tr("画刷风格")); m_brushStytleComboBox = new QComboBox; m_brushStytleComboBox->addItem(tr("SolidPattern"),static_cast<int>(Qt::SolidPattern)); m_brushStytleComboBox->addItem(tr("Dense1Pattern"),static_cast<int>(Qt::Dense1Pattern)); m_brushStytleComboBox->addItem(tr("Dense2Pattern"),static_cast<int>(Qt::Dense2Pattern)); m_brushStytleComboBox->addItem(tr("Dense3Pattern"),static_cast<int>(Qt::Dense3Pattern)); m_brushStytleComboBox->addItem(tr("Dense4Pattern"),static_cast<int>(Qt::Dense4Pattern)); m_brushStytleComboBox->addItem(tr("Dense5Pattern"),static_cast<int>(Qt::Dense5Pattern)); m_brushStytleComboBox->addItem(tr("Dense6Pattern"),static_cast<int>(Qt::Dense6Pattern)); m_brushStytleComboBox->addItem(tr("Dense7Pattern"),static_cast<int>(Qt::Dense7Pattern)); m_brushStytleComboBox->addItem(tr("HorPattern"),static_cast<int>(Qt::HorPattern)); m_brushStytleComboBox->addItem(tr("VerPattern"),static_cast<int>(Qt::VerPattern)); m_brushStytleComboBox->addItem(tr("CrossPattern"),static_cast<int>(Qt::CrossPattern)); m_brushStytleComboBox->addItem(tr("BDiagPattern"),static_cast<int>(Qt::BDiagPattern)); m_brushStytleComboBox->addItem(tr("FDiagPattern"),static_cast<int>(Qt::FDiagPattern)); m_brushStytleComboBox->addItem(tr("DiagCrossPattern"),static_cast<int>(Qt::DiagCrossPattern)); m_brushStytleComboBox->addItem(tr("LinearGradientPattern"),static_cast<int>(Qt::LinearGradientPattern)); m_brushStytleComboBox->addItem(tr("ConicalGradientPattern"),static_cast<int>(Qt::ConicalGradientPattern)); m_brushStytleComboBox->addItem(tr("RadialGradientPattern"),static_cast<int>(Qt::RadialGradientPattern)); m_brushStytleComboBox->addItem(tr("TexturePattern"),static_cast<int>(Qt::TexturePattern)); connect(m_brushStytleComboBox,SIGNAL(activated(int)),this,SLOT(ShowBrush(int))); //添加布局 m_rightLayout = new QGridLayout; m_rightLayout->addWidget(m_shapeLabel,0,0); m_rightLayout->addWidget(m_shapeCombox,0,1); m_rightLayout->addWidget(m_penColorLabel,1,0); m_rightLayout->addWidget(m_penColorFrame,1,1); m_rightLayout->addWidget(m_penColorBtn,1,2); m_rightLayout->addWidget(m_penWidthLabel,2,0); m_rightLayout->addWidget(m_penWithSpinBox,2,1); m_rightLayout->addWidget(m_penStyleLabel,3,0); m_rightLayout->addWidget(m_penStyleComboBox,3,1); m_rightLayout->addWidget(m_penCapLabel,4,0); m_rightLayout->addWidget(m_penCapComboBox,4,1); m_rightLayout->addWidget(m_penJoinLabel,5,0); m_rightLayout->addWidget(m_penJoinComboBox,5,1); m_rightLayout->addWidget(m_fillRuleLabel,6,0); m_rightLayout->addWidget(m_fillRuleComboBox,6,1); m_rightLayout->addWidget(m_spreadLabel,7,0); m_rightLayout->addWidget(m_spreadComboBox,7,1); m_rightLayout->addWidget(m_brushColorLabel,8,0); m_rightLayout->addWidget(m_brushColorFrame,8,1); m_rightLayout->addWidget(m_brushColorBtn,8,2); m_rightLayout->addWidget(m_brushStyleLabel,9,0); m_rightLayout->addWidget(m_brushStytleComboBox,9,1); QHBoxLayout* mainLayout = new QHBoxLayout(this); mainLayout->addWidget(m_paintArea); mainLayout->addLayout(m_rightLayout); ShowShape(m_shapeCombox->currentIndex()); //变更每项设置后,更新图像 void MainWidget::ShowShape(int value) PaintArea::Shape shape =PaintArea::Shape (m_shapeCombox->itemData(value).toInt());//获取用户选择的形状 m_paintArea->setShape(shape); void MainWidget::ShowPenColor() QColor color = QColorDialog::getColor(Qt::black); m_penColorFrame->setPalette(QPalette(color)); int value = m_penWithSpinBox->value(); int styleIndex = m_penStyleComboBox->currentIndex(); Qt::PenStyle style = Qt::PenStyle(m_penStyleComboBox->itemData(styleIndex).toInt()); int capIndex = m_penCapComboBox->currentIndex(); Qt::PenCapStyle cap = Qt::PenCapStyle(m_penCapComboBox->itemData(capIndex).toInt()); int joinIndex = m_penJoinComboBox->currentIndex(); Qt::PenJoinStyle join = Qt::PenJoinStyle(m_penJoinComboBox->itemData(joinIndex).toInt()); //将当前画笔的各个参数传递-设置 m_paintArea->setPen(QPen(color,value,style,cap,join)); void MainWidget::ShowPenWidth(int value) QColor color = m_penColorFrame->palette().color(QPalette::Window);//获取颜色框中关于窗体的颜色值 m_penColorFrame->setPalette(QPalette(color)); int penWidth =value; int styleIndex = m_penStyleComboBox->currentIndex(); Qt::PenStyle style = Qt::PenStyle(m_penStyleComboBox->itemData(styleIndex).toInt()); int capIndex = m_penCapComboBox->currentIndex(); Qt::PenCapStyle cap = Qt::PenCapStyle(m_penCapComboBox->itemData(capIndex).toInt()); int joinIndex = m_penJoinComboBox->currentIndex(); Qt::PenJoinStyle join = Qt::PenJoinStyle(m_penJoinComboBox->itemData(joinIndex).toInt()); //将当前画笔的各个参数传递-设置 m_paintArea->setPen(QPen(color,penWidth,style,cap,join)); void MainWidget::ShowPenStyle(int value) QColor color = m_penColorFrame->palette().color(QPalette::Window);//获取颜色框中关于窗体的颜色值 m_penColorFrame->setPalette(QPalette(color)); int penWidth = m_penWithSpinBox->value(); Qt::PenStyle style = Qt::PenStyle(m_penStyleComboBox->itemData(value).toInt()); if(style == Qt::CustomDashLine)//如果是自定义的 QVector<qreal> dashes; qreal space = 4; dashes<<1<<space<<2<<space<<3<<space<<4<<space; m_paintArea->m_pen.setDashPattern(dashes); m_paintArea->update(); int capIndex = m_penCapComboBox->currentIndex(); Qt::PenCapStyle cap = Qt::PenCapStyle(m_penCapComboBox->itemData(capIndex).toInt()); int joinIndex = m_penJoinComboBox->currentIndex(); Qt::PenJoinStyle join = Qt::PenJoinStyle(m_penJoinComboBox->itemData(joinIndex).toInt()); //将当前画笔的各个参数传递-设置 m_paintArea->setPen(QPen(color,penWidth,style,cap,join)); void MainWidget::ShowPenCap(int value) QColor color = m_penColorFrame->palette().color(QPalette::Window);//获取颜色框中关于窗体的颜色值 m_penColorFrame->setPalette(QPalette(color)); int penWidth =m_penWithSpinBox->value(); int styleIndex = m_penStyleComboBox->currentIndex(); Qt::PenStyle style = Qt::PenStyle(m_penStyleComboBox->itemData(styleIndex).toInt()); // int capIndex = m_penCapComboBox->currentIndex(); Qt::PenCapStyle cap = Qt::PenCapStyle(m_penCapComboBox->itemData(value).toInt()); int joinIndex = m_penJoinComboBox->currentIndex(); Qt::PenJoinStyle join = Qt::PenJoinStyle(m_penJoinComboBox->itemData(joinIndex).toInt()); //将当前画笔的各个参数传递-设置 m_paintArea->setPen(QPen(color,penWidth,style,cap,join)); void MainWidget::ShowPenJoin(int value) QColor color = m_penColorFrame->palette().color(QPalette::Window);//获取颜色框中关于窗体的颜色值 m_penColorFrame->setPalette(QPalette(color)); int penWidth =m_penWithSpinBox->value(); int styleIndex = m_penStyleComboBox->currentIndex(); Qt::PenStyle style = Qt::PenStyle(m_penStyleComboBox->itemData(styleIndex).toInt()); int capIndex = m_penCapComboBox->currentIndex(); Qt::PenCapStyle cap = Qt::PenCapStyle(m_penCapComboBox->itemData(capIndex).toInt()); // int joinIndex = m_penJoinComboBox->currentIndex(); Qt::PenJoinStyle join = Qt::PenJoinStyle(m_penJoinComboBox->itemData(value).toInt()); //将当前画笔的各个参数传递-设置 m_paintArea->setPen(QPen(color,penWidth,style,cap,join)); void MainWidget::ShowFillRule(int value) Qt::FillRule rule = Qt::FillRule(value); m_paintArea->setFillRule(rule); void MainWidget::ShowSpreadStyle(int value) m_spread = QGradient::Spread(value); void MainWidget::ShowBrushColor() QColor color = QColorDialog::getColor(Qt::green); m_brushColorFrame->setPalette(QPalette(color)); ShowBrush(m_brushStytleComboBox->currentIndex()); void MainWidget::ShowBrush(int value) QColor color = m_brushColorFrame->palette().color(QPalette::Window); int brush = m_brushStytleComboBox->itemData(value).toInt(); Qt::BrushStyle style = Qt::BrushStyle(brush); //线性渐变 if(style == Qt::LinearGradientPattern) //指定线性渐变的区域 QLinearGradient lineGradient(0,0,400,400); //指定某个位置的渐变色,pos取值范围0~1 lineGradient.setColorAt(0.0,Qt::white); lineGradient.setColorAt(0.2,color); lineGradient.setColorAt(1.0,Qt::black); lineGradient.setSpread(m_spread); m_paintArea->setBrush(lineGradient); //环形渲染 else if(style == Qt::RadialGradientPattern) //中心坐标,半径长度,焦点坐标,如果需要对称,则中心坐标和焦点坐标保持一致 QRadialGradient radiaGrdient(200,200,150,200,200); radiaGrdient.setColorAt(0.0,Qt::white); radiaGrdient.setColorAt(0.2,color); radiaGrdient.setColorAt(1.0,Qt::black); m_paintArea->setBrush(radiaGrdient); //弧度渲染 else if(style == Qt::ConicalGradientPattern) //中心坐标,角度 QConicalGradient conicaoGradient(200,200,30); conicaoGradient.setColorAt(0.0,Qt::white); conicaoGradient.setColorAt(0.2,color); conicaoGradient.setColorAt(1.0,Qt::black); m_paintArea->setBrush(conicaoGradient); else if(style == Qt::TexturePattern) m_paintArea->setBrush(QBrush(QPixmap("test1.jpg"))); m_paintArea->setBrush(QBrush(color,style)); paintarea.h#ifndef PAINTAREA_H #define PAINTAREA_H #include <QWidget> #include<QPen> #include<QBrush> class PaintArea : public QWidget Q_OBJECT public: enum Shape{Line,Rectangle,RoundRect,Ellipse,Polygon,Polyline,Points,Arc,Path,Text,Pixmap}; explicit PaintArea(QWidget *parent = nullptr); public: void setShape(Shape shape); void setPen(QPen pen); void setBrush(QBrush brush); void setFillRule(Qt::FillRule rule); protected: void paintEvent(QPaintEvent *event)override; public: QPen m_pen; private: Shape m_shape; QBrush m_brush; Qt::FillRule m_fillrule; signals: public slots: #endif // PAINTAREA_Hpaintarea.cpp#include "paintarea.h" #include<QPainter> PaintArea::PaintArea(QWidget *parent) : QWidget(parent) setPalette(QPalette(Qt::white)); setAutoFillBackground(true);//设置背景自动填充 setFixedSize(400,400); void PaintArea::setShape(PaintArea::Shape shape) m_shape = shape; update(); void PaintArea::setPen(QPen pen) m_pen = pen; update(); void PaintArea::setBrush(QBrush brush) m_brush = brush; update(); void PaintArea::setFillRule(Qt::FillRule rule) m_fillrule = rule; update(); void PaintArea::paintEvent(QPaintEvent *event) QPainter painter(this); painter.setPen(m_pen); painter.setBrush(m_brush); QRect rect(50,100,300,200);//起始位置,宽高 const QPoint points[4]= { QPoint(150,100), QPoint(300,150), QPoint(100,300), QPoint(100,300) int startAngle = 30 * 16;//1/16 int spanAngle = 120 * 16; QPainterPath path; path.addRect(150,150,100,100); path.moveTo(100,200); path.cubicTo(300,100,200,200,300,300); path.cubicTo(100,300,200,200,100,100); path.setFillRule(m_fillrule); switch(m_shape) case Line: painter.drawLine(rect.topLeft(),rect.bottomRight()); break; case Rectangle: painter.drawRect(rect); break; case RoundRect: painter.drawRoundRect(rect); break; case Ellipse: painter.drawEllipse(rect); break; case Polygon://多边形,默认为4变形 painter.drawPolygon(points,4); break; case Polyline: painter.drawPolyline(points,4); break; case Points: painter.drawPoints(points,4); break; case Arc: painter.drawArc(rect, startAngle,spanAngle); break; case Path: painter.drawPath(path); break; case Text: painter.drawText(rect,Qt::AlignCenter,tr("Hello world!")); break; case Pixmap: painter.drawPixmap(50,100,QPixmap("test1.jpg")); break; default: break;
Qt样式表Qt样式表介绍Qt样式表是一个可以自定义部件外观的强大机制,样式表的概念、术语、语法均受到HTML层叠样式表(CSS)的启发。样式表可通过QApplication::setStyleSheet()函数将其设置到整个应用程序上,也可以使用QWidget::setStyleSheet()将其设置到指定的部件或子部件上,不同级别均可设置样式表,称为样式表的层叠。样式表也可通过设计模式编辑样式表。Qt样式表语法及使用样式规则每个样式规则由选择器和声明组成。选择器:指定受该规则影响的部件。声明:指定则个部件上要设置的属性。例如:QPushButton{color:red} QPushButton是选择器; {color:red}是声明; color是属性; red是值。 选择器{属性1:值1;属性2:值2;属性3:值3}此规定指定了QPushButton和它子类使用红色作为前景色。样式表中一般不区分大小写,如color与COLOR表相同属性,但类名、对象名以及Qt属性名区分大小写。声明中的多组"属性 : 值"列表以分号;隔开。例如:QPushButton{color:red;background-color:white}选择器类型选择器就是要被设置样式的对象(部件)。子部件对于一些复杂的部件修改样式,可能需要访问它们的子部件,如QComboBox的下拉按钮,QSpinBox的向上、向下箭头等。例如:下来按钮设置图片属性:QComboBox::drop-down{image:url(dropdown.png)}伪状态1.选择器可以包含伪状态来限制规则在部件的指定状态上的应用。伪状态在选择器之后,用冒号隔离。如:鼠标悬停在按钮上时被应用: QPushButton:hover{color:white} 鼠标不悬停在按钮上时被应用(!表否定) QPushButton.!hover{color:white}2.伪状态可以多个连用,达到逻辑与效果。如:鼠标悬停在有一个被选中的QCheckBox部件上时才应用规则: QCheckBox:hover:checked{color:white}3.伪状态可通过逗号达到逻辑或效果。如:鼠标悬停或勾选时,应用后面的样式 QCheckBox:hover,checked{color:white}4.伪状态与子部件联合使用。如:QComboBox::drop-down:hover{image:url(dropdown_bright.png)冲突解决几个样式规则对相同的属性指定不同的值时会产生冲突。如:QPushButton#okButton{color:gray} QPushButton{color:red}冲突原则:特殊的选择器优先。此例中QPushButton#okButton代表的是单一对象,而不是一个类的所有实例,所以okButton的文本颜色会是灰色的。同样的有伪状态的比没有伪状态的优先。1.层叠样式表设置在QApplication上、父部件上或子部件上。部件有效的样式表是通过部件的祖先样式表和QApplication上的样式表合并得到的,当发生冲突时,部件自己的样式表优先于任何继承的样式表。2.继承当使用Qt样式时,部件并不会自动从父部件继承字体和颜色样式设置 。如一个QPushButton包含在一个QGroupBox中,这里 对QGroupBox设置样式表:qApp->setStyleSheet("QGroupBox{color:red;}");但是没有对QPushButton设置样式表,这时QPushButton会使用系统颜色,而不会继承QGroupBox的颜色, 如果想要QGroupBox的颜色设置到其子部件上,可以这样:qApp->setStyleSheet("QGroupBox,QGroupBox*{color:red;}")3.设置QObject属性从Qt4.3开始,任何可设计的Q_PROPERTY都可以使用"qproperty-属性 名称"的语法来设置样式表。如:MyLabel{qproperty-pixmap:url(pixmap.png);} MyGroupBox{qproperty-titleColor:rgb(100,200,100);} QPushButton{qproperty-iconSize:20px 20px;}盒子模型使用样式表时,每个部件被看作拥有4个同心矩形的盒子,四个矩形的内容分别为内容(content)、填衬(padding)、边框(border)、边距(margin。边距、边框宽度和填衬等属性的默认值都是0,这样四个矩形正好重叠。示例代码模式设置: //设置样式 //按钮设置黄色背景色 ui->pushButton->setStyleSheet("background:yellow"); //设置按钮前景色——文字颜色 ui->pushButton->setStyleSheet("color:red"); //后面的会将前面的覆盖掉,如果都想显示,就要将他们写在一起, 中间用分号隔开 setStyleSheet("QPushButton{background:yellow;color:red}"); ui->horizontalSlider->setStyleSheet("background:blue");设计模式设置:右键空白处,选择"改变样式表",然后进行样式的编辑。(可以右键单击指定某个部件进行修改。)部件属性区选择styleSheet也可进行样式的设置。
前言上个月,我重启了DIY显示器的这个想法。于是,我买了显示器面板,驱动板,一些螺丝,以及几块亚克力板,由于我对屏幕的大小以及外壳要做的大小没有什么概念,加上我不会CAD,所以,我准备手工切割,用胶水拼接一个外壳。先看一下效果吧。制作流程大致制作流程如下:1.用一块亚克力板做底板,根据显示器面板、驱动板的大小以及四个角的孔位打孔,确定摆放位置。并将螺丝+双通尼龙柱固定在显示器面板所要放置的四个角落中。2.根据需要的大小将四周多余的切掉。3.根据大小切割出四周所需要的板子。4.用热熔胶将周围固定。5.根据需要在四周开孔,用水将粉末碎屑冲洗干净,加胶固定。6.连接固定各个零件,完成。注意在制作的过程中,最费时费力以及最难的就是亚克力板的切割了,没想到3mm就这么难切。先用笔画线,然后用尺子抵着用勾刀划。划呀划呀划, 差不多了,一掰就开了(熟练了先用尺子比着用勾刀划个几刀后就可以把尺子拿走了,沿着痕迹划就行)。打孔也比较费劲,可以用电钻开孔,然后用边上蹭,或用锉来回蹭,开始切得七扭八歪的,后来慢慢熟练了,会好一些。本来准备用亚克力胶水粘的,感觉效果不好,所以我就用热熔胶了。热熔胶凝固的速度很快,开始我想先挤一道,然后将板子放到上面,结果我刚放就凝固了,所以要先将板子立好,然后沿着根底下挤胶。各个螺丝的点位如果高度不够可以用垫片垫。做之前量一下或估计一下所需要的各个材料大小。如果都测量好了,建议让商家裁剪,会CAD更好。TN屏的可视角度较小,相对于IPS屏来说。看久了高色域屏,看低色域会有所不适应。不游戏的话,高低刷屏幕差别不大。热熔胶凝固的速度很快,开始我想先挤一道,然后将板子放到上面,我刚放就凝固了,所以要先将板子立好,然后沿着根底下挤胶。所需材料及工具材料:40cm 30cm 3mm的亚克力板2块,可以多买几块裁着练手。M2螺丝25mm若干M2双通尼龙六角柱18mm若干2mm 4.5mm 0.6mm尼龙塑料垫片若干M2螺丝8mm若干M2螺母若干15.6英寸显示器面板驱动板(注意支持你的显示器面板),电源。hdmi线工具:电钻钳子2mm钻头,以及更大的钻头用来开孔。锉M2螺丝对应的螺丝刀,建议可拆卸用电钻往里打。热熔胶枪+热熔胶勾刀钢尺笔最后主要花费:物品价格显示器面板228驱动板电源套装75.9亚克力板 x 460hdmi线9.9螺丝25.14钻头 x 24总计402.94我的材料买多了,所以花费多一些,还有显示器面板,如果多蹲一蹲是有可能在小黄鱼蹲到更便宜的。我这个不叫"DIY便携式显示器"是因为我不会背着它来回跑,只是放到固定的位置,由我来用。录的视频还没有剪,做好了我会将链接放到文章最后。最后感谢你的阅读,如果对你有帮助,希望能给我点个赞。视频链接——链接
nonebot学习(1)具体的安装使用部署流程还请见其他作者的文章。使用nb run指令运行从window下迁移到linux环境,记得安装相应的包。报错提示ModuleNotFoundError:No module named 'xxx'ubuntu环境下部署下载对应的版本,更改权限(777),安装脚手架pip install nb-cli,安装失败,可能是权限不够,切换至root用户安装。ubuntu挂到后台nohup xxx &,例如:nohup nb run &。jobs查看nohup挂到后台的程序,关闭终端后就看不见了。若关闭进程,使用ps -ef查看所有进程,找到对应的ID,kill掉。ls -a查看当前目录下所有文件(包括隐藏文件)。ubuntu窗口screen -R xxx新建窗口, alt+a+d退出窗口,screen -r xxx恢复窗口。
模型/视图编程Qt中的模型/视图架构用来实现大量的数据存储、处理及显示。MVC(Model-View-Controller)包括了3个组件:模型(model)是应用对象,用来表示数据;视图(View)是模型的用户界面,用来显示数据;控制(Controller)定义了用户界面对用户输入的反应方式。委托(Delegate)用于定制数据的渲染和编辑方式。模型所有的模型都基于QAbstractItemModel类,该类提供了十分灵活的接口来处理各种视图,这些视图的数据表现形式为表格(table)、列表(list)、树(tree)。Qt提供了一些现成的模型来处理数据项:QStringListModel存储简单的QString项目列表;QStandardItemModel管理复杂的属性结构数据项,每一个数据项可以包含任意的数据;QFileSystemModel、QSqlTableModel和QSqlRelationTableModel用来访问数据库。当标准模型还无法满足需要时,可子类化QAbstractItemModel、QAbstractListModel或QAbstractTableModel来创建自定义的模型。常见的3种模型为列表模型、表格模型、树模型,如下图所示:为确保数据的表示与数据获取相分离,Qt引入了模型索引的概念,输入和委托均可通过模型索引来请求数据并显示。只有模型需要知道怎样获取数据,被模型管理的数据类型可以被广泛的定义。模型索引包含一个指针,指向创建他们的模型,使用多个模型时可避免混淆。模型索引QModeIIndex类提供对一块数据的临时引用, 用来修改或检索模型中的数据,获取一个数据项的模型索引必须指定模型的3个属性:行号、列号和父项的模型索引。如:QModelIndex index = model->index(row,column,parent);也可以通过模型指定的相关数据项对应的模型索引以及特定的角色来获取需要的类型数据,如:QVariant value = model->data(index,role);常用的角色类型:示例:#include<QApplication> #include<QDebug> #include<QTreeView> #include<QStandardItemModel> int main(int argc,char* argv[]) QApplication a(argc,argv); //创建标准项模型 QStandardItemModel model; //获取标准项模型的根项,跟项式不可见的 QStandardItem* parentItem = model.invisibleRootItem(); //创建标准箱item0,并设置文本 QStandardItem* item0 = new QStandardItem; item0->setText(QString("A")); QPixmap pixmap0(50,50); pixmap0.fill(Qt::red); item0->setIcon(QIcon(pixmap0)); item0->setToolTip(QString("A项")); //将item0作为父项的子项 parentItem->appendRow(item0); parentItem = item0; //创建item0的子项 QStandardItem* item1 = new QStandardItem; item1->setText(QString("B")); QPixmap pixmap1(50,50); pixmap1.fill(Qt::blue); item1->setIcon(pixmap1); item1->setToolTip(QString("B项")); QStandardItem* item2 = new QStandardItem; QPixmap pixmap2(50,50); pixmap2.fill(Qt::green); item2->setData("C",Qt::EditRole); item2->setData("indexC",Qt::ToolTipRole); item2->setData(QIcon(pixmap2),Qt::DecorationRole); parentItem->appendRow(item1); parentItem->appendRow(item2); //在树视图中显示数据 QTreeView view; view.setModel(&model); view.show(); //模型问数据 QModelIndex indexA = model.index(0,0,QModelIndex()); qDebug()<<"indexA row count:"<<model.rowCount(indexA); QModelIndex indexB = model.index(0,0,indexA); qDebug()<<"indexB text:"<<model.data(indexB,Qt::EditRole).toString(); qDebug()<<"indexB toolTip:"<<model.data(indexB,Qt::ToolTipRole).toString(); return a.exec(); }视图Qt提供了QListView、QTabelView视图、QTreeView视图分别实现列表、表格与树视图效果。QListView将数据项显示为一个列表;QTableView将模型中的数据显示在一个表格中;QTreeView将模型中的数据项显示在具有层次的列表中。QTableView和QTreeView在显示项目的时候同时还可以显示标头,通过QHeaderView类实现。自定义视图类是基于QAbstractItemView抽象基类,如实现条形图,饼状图等特殊显示方式。示例1:#include<QApplication> #include<QAbstractItemModel> #include<QAbstractItemView> #include<QDirModel> #include<QTreeView> #include<QListView> #include<QTableView> #include<QSplitter> int main(int argc,char* argv[]) QApplication app(argc,argv); //创建模型 QDirModel model; //创建树视图、列表视图、表格视图 QTreeView tree; QListView list; QTableView table; //视图设置模型 tree.setModel(&model); list.setModel(&model); table.setModel(&model); //设置视图对象的选择方式为多选,list/table选择方式同tree tree.setSelectionMode(QAbstractItemView::MultiSelection); list.setSelectionMode(tree.selectionMode()); table.setSelectionMode(tree.selectionMode()); //树视图双击信号发射后,列表及表格视图刷新内容 QObject::connect(&tree,SIGNAL(doubleClicked(QModelIndex)),&list,SLOT(setRootIndex(QModelIndex)) ); QObject::connect(&tree,SIGNAL(doubleClicked(QModelIndex)),&table,SLOT(setRootIndex(QModelIndex)) ); QSplitter* splitter = new QSplitter; splitter->addWidget(&tree); splitter->addWidget(&list); splitter->addWidget(&table); splitter->setWindowTitle(QString("模型/视图")); splitter->show(); return app.exec(); }示例2:#include "weaponmodel.h" Weaponmodel::Weaponmodel(QObject* parent):QAbstractTableModel(parent) armyMap[1] = QString("空军"); armyMap[2] = QString("海军"); armyMap[3] = QString("陆军"); armyMap[4] = QString("海军陆战队"); weaponMap[1] = QString("战斗机1"); weaponMap[2] = QString("战斗机2"); weaponMap[3] = QString("战斗机3"); weaponMap[4] = QString("战斗机4"); weaponMap[5] = QString("战斗机5"); weaponMap[6] = QString("战斗机6"); weaponMap[7] = QString("战斗机7"); weaponMap[8] = QString("战斗机8"); populateModel(); int Weaponmodel::rowCount(const QModelIndex &parent)const return army.size(); int Weaponmodel::columnCount(const QModelIndex &parent)const return 3; //返回指定索引的数据 QVariant Weaponmodel::data(const QModelIndex &index, int role) const if(!index.isValid()) return QVariant(); if(role == Qt::DisplayRole) switch (index.column()) { case 0: return army[army[index.row()]]; break; case 1: return weaponMap[weaponType[index.row()]]; break; case 2: return weapon[index.row()]; break; default: return QVariant(); break; return QVariant(); QVariant Weaponmodel::headerData(int section, Qt::Orientation orientation, int role) const if(role == Qt::DisplayRole && orientation == Qt::Horizontal) return header[section]; return QAbstractTableModel::headerData(section,orientation,role); void Weaponmodel::populateModel() header<<QString("军种")<<QString("种类")<<QString("武器"); army<<1<<2<<3<<4<<2<<4<<3<<1; weaponType<<1<<3<<5<<7<<4<<8<<6<<2; weapon<<QString("B-2")<<QString("尼尔兹级")<<QString("阿帕奇")<<QString("黄蜂级")<<QString("比例伯克级")<<QString("AAAV")<<QString("M1A1")<<QString("F-22"); }示例3#include "stringlistmodel.h" int StringListModel::rowCount(const QModelIndex &parent) const return m_stringList.count(); QVariant StringListModel::data(const QModelIndex &index, int role) const if(!index.isValid()) return QVariant(); if(index.row() == m_stringList.size()) return QVariant(); if(role == Qt::DisplayRole || role == Qt::EditRole) return m_stringList.at(index.row()); return QVariant(); QVariant StringListModel::headerData(int section, Qt::Orientation orientation, int role) const if(role != Qt::DisplayRole) return QVariant(); //水平表头 if(orientation == Qt::Horizontal) return QString("column %1").arg(section); return QString("Row %1").arg(section); Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const if(!index.isValid()) return Qt::ItemIsEnabled; return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; bool StringListModel::setData(const QModelIndex &index, const QVariant &value, int role) //检测索引有效且项目可编辑 if(index.isValid() && role == Qt::EditRole) m_stringList.replace(index.row(),value.toString()); emit dataChanged(index,index); return true; return false; bool StringListModel::inseratRows(int position, int rows, const QModelIndex &index) //告知其它组建指定的行开始插入操作 beginInsertRows(QModelIndex(),position,position + rows -1 ); for(int row = 0 ; row < rows; row++) m_stringList.insert(position,QString("你好")); //告知其它组件完成操作 endInsertRows(); return true; bool StringListModel::removeRows(int position, int rows, const QModelIndex &index) //告知其他组件指定的行开始删除操作 beginRemoveRows(QModelIndex(),position,position + rows - 1); for(int row = 0 ; row < rows;row++) m_stringList.removeAt(position); //告知其它组件完成操作 endRemoveRows(); return true; #include<QApplication> #include"stringlistmodel.h" #include<QListView> #include<QTableView> int main(int argc,char* argv[]) QApplication app(argc,argv); QStringList list; list<<QString("太阳")<<QString("地球")<<QString("月亮")<<QString("木星"); StringListModel model(list);//创建模型 model.insertRows(3,2); model.removeRows(0,1); QListView listView;//创建列表视图 listView.setModel(&model);//视图设置模型 listView.show();//视图显示 QTableView tableView;//创建表格视图 tableView.setModel(&model);//视图设置模型 tableView.show();//视图显示 return app.exec(); }示例4:#include "mainwindow.h" #include "ui_mainwindow.h" #include<QStandardItem> #include<QDebug> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) ui->setupUi(this); //创建标准项模型,7行4列 QStandardItemModel* model = new QStandardItemModel(7,4,this); for(int row = 0 ; row <7 ;row++) for(int colum= 0;colum < 4;colum++) QStandardItem* item = new QStandardItem(QString("%1").arg(row+4 +colum)); //标准模型设置数据项 model->setItem(row,colum,item); m_tableView = new QTableView; m_tableView->setModel(model); setCentralWidget(m_tableView);//设置主窗口的中心部件为表格视图 QItemSelectionModel *selectionModel = m_tableView->selectionModel();//获取视图的选择模式 QModelIndex topLeft;//左上角模型索引 QModelIndex bottomRight;//右下角模型索引 topLeft = model->index(1,1); bottomRight = model->index(5,2); //创建模型选择 QItemSelection selection(topLeft,bottomRight); //以选择的方式来选择项目 selectionModel->select(selection,QItemSelectionModel::Select); //添加动作addAction(动作文本,响应者,槽方法) ui->menuBar->addAction(QString("当前项目"),this,&MainWindow::getCurrenItemData); ui->menuBar->addAction(QString("切换选择"),this,&MainWindow::toggleSection); //关联选择模型的选择改变、当前项改变的信号 connect(selectionModel,&QItemSelectionModel::selectionChanged,this,&MainWindow::updateSelection); connect(selectionModel,&QItemSelectionModel::currentChanged,this,&MainWindow::changeCurrent); m_tableView2 = new QTableView; m_tableView2 ->setWindowTitle("tableview2"); m_tableView2->resize(400,300); m_tableView2->setModel(model); m_tableView2->setSelectionModel(selectionModel); m_tableView2->show(); MainWindow::~MainWindow() delete ui; void MainWindow::getCurrenItemData() qDebug()<<QString("当前项数据:")<<m_tableView->selectionModel()->currentIndex().data().toString(); void MainWindow::toggleSection() //左上角模型索引 QModelIndex topLeft = m_tableView->model()->index(0,0,QModelIndex()); //右下角模型索引 QModelIndex bottomRight = m_tableView->model()->index(m_tableView->model()->rowCount((QModelIndex))-1,m_tableView->model()->columnCount(QModelIndex())-1,QModelIndex()); //项选择 QItemSelection curSelection(topLeft,bottomRight); m_tableView->selectionModel()->select(curSelection,QItemSelectionModel::Toggle); void MainWindow::updateSelection(const QItemSelection &selected, const QItemSelection &deselected) QModelIndex index; //indexes返回所有选择项的模型索引 QModelIndexList list = selected.indexes(); //给现在选择的项目填充数据 foreach (index,list) QString text = QString("%1,%2").arg(index.row()).arg(index.column()); m_tableView->model()->setData(index,text); //清空 上一次选择的内容 list = deselected.indexes(); foreach (index,list) m_tableView->model()->setData(index,""); void MainWindow::changeCurrent(const QModelIndex &current, const QModelIndex &previous) qDebug()<<QString("从(%1,%2)到(%3,%4)").arg(previous.row()).arg(previous.column()).arg(current.row()).arg(current.column()); }委托在模型/视图框架中,QAbstractItemDelegate是委托类的抽象基类,Qt默认的委托实现由QStyledItemDelegate类提供,这也被用作Qt标准视图的默认委托,选择 QStyledItemDelegate或QItemDelegate中其一来为视图中的项目绘制和提供编辑器。不同的是QStyledItemDelegate使用当前的样式来绘制项目,实现自定义委托建议使用QStyledItemDelegate作为基类。Qt提供了项目试图的便捷类,这些类底层通过模型/视图框架实现。这些部件分别是QListWidget提供一个项目列表,QTreeWidget显示一个多层次的树结构,QTableWidget提供了一个以项目作为单元的表格。它们每一个类都继承了QAbstractItemView类的行为。之所以成为便捷因其用起来比较简单,使用于少量的数据的存储和显示。因没有将视图与模型分离,所以没有视图类灵活,不能和任意的模型一起使用。通过自定义委托来实现更高级的渲染。示例:QSpinbox#include "spinboxdelexgate.h" #include<QSpinBox> SpinBoxDelexgate::SpinBoxDelexgate(QObject* parent):QItemDelegate(parent) QWidget *SpinBoxDelexgate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const QSpinBox *editor = new QSpinBox(parent); editor->setMinimum(0);//最小值 editor->setMaximum(100);//最大值 return editor; void SpinBoxDelexgate::setEditorData(QWidget *editor, const QModelIndex &index) const int value = index.model()->data(index,Qt::EditRole).toInt(); //类型转换:QWidget* 转QSpinBox* QSpinBox* spinBox = static_cast<QSpinBox*>(editor); //编辑器设置数据 spinBox->setValue(value); void SpinBoxDelexgate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const //视图设置自定义委托 editor->setGeometry(option.rect); }便捷部件类示例: #include<QApplication> #include<QDebug> #include<QListWidget> #include<QTreeWidget> #include<QTableWidget> int main(int argc,char* argv[]) QApplication app(argc,argv); QListWidget listWidget; //添加项目 new QListWidgetItem("天涯",&listWidget); QListWidgetItem* listWidgetItem = new QListWidgetItem; listWidgetItem->setText("海角"); QPixmap pixmap(50,50); pixmap.fill(Qt::blue); listWidgetItem->setIcon(QIcon(pixmap)); listWidgetItem->setToolTip("大海的角落"); //部件插入项目 listWidget.insertItem(1,listWidgetItem); //部件设置排序(降序) listWidget.sortItems(Qt::DescendingOrder); listWidget.show(); //创建树部件 QTreeWidget treeWidget; //设置列数 treeWidget.setColumnCount(2); QStringList headers; headers<<"名字"<<"数量"; //设置头 treeWidget.setHeaderLabels(headers); //添加项目 QTreeWidgetItem *item1 = new QTreeWidgetItem(&treeWidget);//指定父项 item1->setText(0,"开心超人"); QTreeWidgetItem *item11 = new QTreeWidgetItem(item1); item11->setText(0,"甜心超人"); item11->setText(1,"小心超人"); //添加项目,并指定它的前一个项目是item1 QTreeWidgetItem* item2 = new QTreeWidgetItem(&treeWidget,item1); item2->setText(0,"小小怪"); treeWidget.show(); //创建表格部件 QTableWidget tableWidget(3,2); QTableWidgetItem *tableWidgetem = new QTableWidgetItem("Tom"); tableWidget.setItem(1,1,tableWidgetem); //创建表格项目,作为表头 QTableWidgetItem* headerV = new QTableWidgetItem("Cat"); tableWidget.setVerticalHeaderItem(0,headerV); QTableWidgetItem* headerH = new QTableWidgetItem("What"); tableWidget.setHorizontalHeaderItem(0,headerH); tableWidget.show(); return app.exec();
富文本QTextEdit支持富文本处理,即文档中可使用多种格式,如文字、图片、表格等。PlainText为纯文本。由此可类比, windows的记事本就是纯文本编辑器,word就是富文本编辑器。文档的光标主要基于QTextCursor类,文档的框架主要基于QTextDocument类。一个富文本的文档结构主要分为几种元素:框架(QTextFrameFormat)、文本块(QTextBlock)、表格(QTextTable)、列表(QTxtList)。每种元素的格式有相应的format类表示:框架格式(QTextDFrameFormat)、文本块格式(QTextBlockFormat)、表格格式(QText)、列表格式(QTextListFormat)。这些格式通常配合QTextCursor类使用。QTextEdit类就是一个富文本编辑器,在构建QTextEdit类对象时就已经构建了一个QTextDocument类对象和一个QTextCursor类对象。只需调用他们相应的操作即可。文档边框格式示例: ui->setupUi(this); //获取文档对象 QTextDocument* document = ui->textEdit->document(); //获取根 框架 QTextFrame *rootFrame = document->rootFrame(); //文档格式框架 QTextFrameFormat format; format.setBorderBrush(Qt::red);//边框颜色 format.setBorder(3);//边界宽度 //设置文档框架格式 rootFrame->setFrameFormat(format); //设置文本边框风格 QTextFrameFormat frameFormat; frameFormat.setBackground(Qt::lightGray); frameFormat.setMargin(10);//设置边距 frameFormat.setPadding(5);//设置填衬 frameFormat.setBorder(2); frameFormat.setBorderStyle(QTextFrameFormat::BorderStyle_DotDash ); QTextCursor cursor = ui->textEdit->textCursor(); cursor.insertFrame(frameFormat);文本个格式、文本块格式、字符格式 //添加工具栏行选项 QAction* action_textFrame = new QAction("框架",this); connect(action_textFrame,&QAction::triggered,this,&MainWindow::showTextFrame); ui->mainToolBar->addAction(action_textFrame); QAction* action_textBlock = new QAction("文本块",this); connect(action_textBlock,&QAction::triggered,this,&MainWindow::showTextBlock); ui->mainToolBar->addAction(action_textBlock);//添加到工具栏中 QAction* action_textFont = new QAction("字体",this); action_textFont->setCheckable(true); connect(action_textFont,&QAction::triggered,this,&MainWindow::setTextFont); ui->mainToolBar->addAction(action_textFont);void MainWindow::showTextFrame() QTextDocument* document = ui->textEdit->document();//获取文档对象 QTextFrame* frame = document->rootFrame();//获取根框架 QTextFrame::iterator it; for(it = frame->begin();!(it.atEnd());it++) QTextFrame*childFrame = it.currentFrame();//获取当前框架指针 QTextBlock childBlock = it.currentBlock();//获取当前文本块 if(childFrame) qDebug()<<"frame"; else if(childBlock.isValid()) qDebug()<<"block:"<<childBlock.text(); void MainWindow::showTextBlock() QTextDocument* document = ui->textEdit->document(); QTextBlock block = document->firstBlock(); //document->blockCount()返回文本块个数 for(int i =0 ; i < document->blockCount();i++) qDebug()<<QString("文本块%1,文本块首行行号为:%2,长度:%3,内容%4").arg(i).arg(block.firstLineNumber()).arg(block.length()).arg(block.text()); block = block.next(); void MainWindow::setTextFont(bool checked) if(checked) QTextCursor cursor = ui->textEdit->textCursor(); //文本块格式 QTextBlockFormat blockFormat; //居中对齐 blockFormat.setAlignment(Qt::AlignCenter); cursor.insertBlock(blockFormat); //字符格式 QTextCharFormat charFormat; //设置背景色 charFormat.setBackground(Qt::lightGray); //设置字符前景色(字符颜色) charFormat.setForeground(Qt::blue); charFormat.setFont(QFont(QString("宋体"),12,QFont::Bold,true)); //下划线 charFormat.setFontUnderline(true); //设置字符格式 cursor.setCharFormat(charFormat); cursor.insertText("嘻嘻"); }文档插入表格、列表、图片void MainWindow::insertTable() QTextCursor cursor = ui->textEdit->textCursor(); QTextTableFormat format;//表格格式 format.setCellSpacing(2);//表格外边空白 format.setCellPadding(10);//表格内边空白 cursor.insertTable(3, 3,format); void MainWindow::insertList() QTextListFormat format;//列表格式 format.setStyle(QTextListFormat::ListDecimal);//数字编号 ui->textEdit->textCursor().insertList(format); void MainWindow::insertImage() QString filePath = QFileDialog::getOpenFileName(this,"选择图片",".","JPEG(*.jpg *.jpeg);;""GIF(*.gif);;""PNG(*.png)"); QUrl url(QString("file://%1").arg(filePath)); QImage image = QImageReader(filePath).read(); QTextDocument* document = ui->textEdit->document(); //文档添加图片资源 document->addResource(QTextDocument::ImageResource,url,QVariant(image)); QTextCursor cursor =ui->textEdit->textCursor(); QTextImageFormat imgFormat; imgFormat.setWidth(image.width()); imgFormat.setHeight(image.height()); imgFormat.setName(url.toString()); cursor.insertImage(imgFormat); }语法高亮void MySyntaxHighlighter::highlightBlock(const QString &text) QTextCharFormat format;//字符格式 format.setFontWeight(QFont::Bold); format.setBackground(Qt::red); format.setForeground(Qt::green) ; QString pattern = "\\bgood\\b";//匹配单词边界 QRegExp expression(pattern); int index = text.indexOf(expression); while(index >= 0 ) int length = expression.matchedLength();//匹配到的字符长度 setFormat(index,length,format); index = text.indexOf(expression,index + length); }字符查找QAction* action_textFind = new QAction("查找",this); connect(action_textFind,&QAction::triggered,this,&MainWindow::textFind); ui->mainToolBar->addAction(action_textFind); m_findDialg = new QDialog(this);//查找对话框 m_lineEdit = new QLineEdit(m_findDialg);//查找输入框 QPushButton* btn = new QPushButton(m_findDialg); btn->setText("查找下一个"); connect(btn,&QPushButton::clicked,this,&MainWindow::textNext); QVBoxLayout* layout = new QVBoxLayout; layout->addWidget(m_lineEdit); layout->addWidget(btn); m_findDialg->setLayout(layout);void MainWindow::textFind() m_findDialg->show(); void MainWindow::textNext() QString strFind = m_lineEdit->text(); bool isFind = ui->textEdit->find(strFind,QTextDocument::FindBackward); if(isFind) qDebug()<<QString("行号:%1,列号:%2") .arg(ui->textEdit->textCursor().blockNumber()) .arg(ui->textEdit->textCursor().columnNumber());
菜单栏、工具栏、状态栏主窗口框架(QMainWindow)拥有自己的布局,包括以下组件:菜单栏菜单栏包含了一个下拉菜单项的列表,这些菜单项通过QAction动作类实现。一个主窗口只能有一个菜单栏。工具栏工具栏一般用于常用的菜单项,也可以插入其他窗口部件,一个主窗口可以拥有多个工具栏。中心部件主窗口中心区域可以放入一个窗口部件作为中心部件,是应用程序的主要功能实现区域,一个主窗口只能有一个中心部件。dock部件dock部件又称为停靠部件,或停靠窗口,可停靠在窗口部件的四周,用来设置一些部件来实现一些功能,类似一个工具箱,一个主窗口可拥有多个dock部件。状态栏状态栏用于显示程序的一些状态信息,在主窗口的最底下,一个主窗口只能拥有一个状态栏。菜单栏、工具栏、状态栏代码添加示例。 //菜单 QMenu* editMenu = ui->menuBar->addMenu("编辑(&E)"); QAction *action_copy = editMenu->addAction(QIcon("test.png"),QString("复制(&C)")); action_copy->setShortcut(QKeySequence("Ctrl+C")); //添加到主窗口工具栏——工具栏添加动作按钮 ui->mainToolBar->addAction(action_copy); //动作按钮组,互斥选项,只能选择这其中的一个 QActionGroup* group = new QActionGroup(this); QAction* action_L = group->addAction("左对齐(&L)"); QAction* action_R = group->addAction("右对齐(&R)"); QAction* action_C = group->addAction("居中对齐(&C)"); QAction* action_J = group->addAction("两端对齐(&J)"); //设置可选 action_L->setCheckable(true); action_R->setCheckable(true); action_C->setCheckable(true); action_J->setCheckable(true); //菜单栏中添加分隔线 editMenu->addSeparator(); //添加到窗口菜单 editMenu->addAction(action_L); editMenu->addAction(action_R); editMenu->addAction(action_C); editMenu->addAction(action_J); // 工具栏添加部件 QToolButton* toolBtn = new QToolButton(this); toolBtn->setText("颜色"); QMenu* colorMenu = new QMenu(this); colorMenu->addAction("红色"); colorMenu->addAction("绿色"); toolBtn->setMenu(colorMenu); //设置模式为弹出式菜单 toolBtn->setPopupMode(QToolButton::MenuButtonPopup); ui->mainToolBar->addWidget(toolBtn); //添加调节大小窗口 QSpinBox* spinBox = new QSpinBox(this); ui->mainToolBar->addWidget(spinBox); //显示临时信息 ui->statusBar->showMessage(QString("欢迎......"),3000); //添加状态栏永久显示 QLabel* tag = new QLabel("https://doraemon-hub-art.github.io"); ui->statusBar->addPermanentWidget(tag);自定义菜单栏自定义菜单需要定义一个继承QWidgetAction的类, 并重写createWidget方法,在该方法中自定义部件设置。示例MyAction.h#ifndef MYACTION_H #define MYACTION_H #include<QWidgetAction> #include<QLineEdit> class MyAction : public QWidgetAction Q_OBJECT public: MyAction(QObject* parent = 0); protected: QWidget* createWidget(QWidget* parent); signals: void getText(const QString& string); private slots: void sendText(); private: QLineEdit* lineEdit; #endif // MYACTION_HMyAction.cpp#include "myaction.h" #include<QSplitter> #include<QLabel> MyAction::MyAction(QObject* parent):QWidgetAction(parent) lineEdit = new QLineEdit; connect(lineEdit,&QLineEdit::returnPressed,this,&MyAction::sendText); QWidget *MyAction::createWidget(QWidget *parent) //是否继承自菜单或者工具栏 //是则创建父部件的子部件并返回子部件 if(parent->inherits("QMenu")||parent->inherits("QToolBar")) QSplitter * splitter = new QSplitter(parent); QLabel* label = new QLabel("插入文本:"); splitter->addWidget(label); splitter->addWidget(lineEdit); return splitter; void MyAction::sendText() emit getText(lineEdit->text()); lineEdit->clear(); qDebug()<<str; qDebug().noquote()<<str;//不带双引号输出 qDebug()<<"hello"<<"world";//默认输入,这两个单词中间会有空格 qDebug().nospace()<<"hello"<<"world";//去掉空格 //控制台程序会输入到黑窗口 //桌面应用程序会输出到-应用程序输出
文件处理QFile类用于文件操作,它提供了读写文件的接口,可以读写文件、二进制文件和Qt资源文件。处理文本文件和二进制文件,可以使用QTextStream类和QDataStream类。处理临时文件可以使用QTemporaryFile,获取信息可以使用QFileInfo,处理目录可以使用QDir,监视文件和目录变化可以使用QFileSystemWatcher。QFile类示例#include <QCoreApplication> #include<QFile> #include<QDebug> int main(int argc, char *argv[]) QCoreApplication a(argc, argv); //文件读取 QFile file("hello.txt"); if(file.open(QIODevice::ReadOnly)) char buffer[100]; qint32 n = file.readLine(buffer,sizeof(buffer)); if(n != -1) qDebug()<<n<<" "<<buffer; file.close();//关闭文件 qDebug()<<file.errorString(); //文件写入 QFile dataFile; double dPI = 3.1415926; int age =19; dataFile.setFileName("data.txt"); if(dataFile.open(QFile::WriteOnly | QFile::Truncate)) QTextStream out(&dataFile); //设置精度 out.setRealNumberPrecision(3); //设置字段宽度 out.setFieldWidth(20); //设置对齐方式 out.setFieldAlignment(QTextStream::AlignCenter); out<<QString("PI:")<<dPI; //将整数以二进制方式写入 out<<bin<<age; //十六进制写入,并显示进制前缀 out<<hex<<showbase<<age; //科学计数法 out<<scientific<<dPI; return a.exec(); }操作二进制文件#include <QCoreApplication> #include<QFile> #include<QDebug> #include<QDate> #include<QDataStream> int main(int argc, char *argv[]) //往文件中写入数据 QCoreApplication a(argc, argv); QFile binFile("info.bat"); if(binFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) QDataStream out(&binFile); out<<QString("半生瓜のblog")<<QDate::fromString("2022-01-24","yyyy-MM-dd")<<(qint32)19; binFile.close(); qDebug()<<binFile.errorString(); binFile.setFileName("info.bat");//指明要操作的文件 if(binFile.open(QIODevice::ReadOnly)) QDataStream in(&binFile); //怎么写怎么读 QString Name; QDate NowTime; qint32 Age; in>>Name>>NowTime>>Age; qDebug()<<Name<<NowTime<<Age; binFile.close(); qDebug()<<binFile.errorString(); return a.exec(); }示例:获取文件信息void FileInfo::getFileInfo(QString &fileName) //通过这个类来获取文件的信息 QFileInfo info(fileName); qint64 size = info.size();//获取文件大小 QDateTime createTime = info.created();//获取文件创建时间 QDateTime lastModTime = info.lastModified();//获取文件最后一次被的修改的时间 QDateTime lasReadTime = info.lastRead();//获取文件最后一次被读取的时间 //文件的属性信息 bool isDir = info.isDir();//是不是目录 bool isFile = info.isFile();//是不是文件 bool isSymLink = info.isSymLink();//是不是链接 bool isHid = info.isHidden();//是否隐藏 bool isReadable = info.isReadable();//是否可读 bool isWritable = info.isWritable();//是否可写 bool isExcutable = info.isExecutable();//是否可执行 //根据读取信息设置控件信息 ui->FileSizeLineEdit->setText(QString::number(size));//整型转字符串 ui->CreateTimeLineEdit->setText(createTime.toString());//QDate转字符串 ui->LastModTimeLineEdit->setText(lastModTime.toString()); ui->LastCheckTimeLineEdit->setText(lasReadTime.toString()); ui->isDirCheckBox->setChecked(isDir); ui->isFileCheckBox->setChecked(isFile); ui->isExecutableCheckBox->setChecked(isExcutable); ui->isSymLinkCheckBox->setChecked(isSymLink); ui->isHiddenCheckBox->setChecked(isHid); ui->isWritableCheckBox->setChecked(isWritable); ui->isReadableCheckBox->setChecked(isReadable); void FileInfo::on_browerFileBtn_clicked() //选择文件 QString fileName = QFileDialog::getOpenFileName(this,"打开文件",".","files(*)"); //选择目录QString path = QFileDialog::getExistingDirectory(this,"选择目录","."); ui->FileNameLineEdit->setText(fileName);//文件名(带路径的) getFileInfo(fileName);
QT正则表达式正则表达式即一个文本匹配字符串的一种模式。Qt中QRegExp类实现使用正则表达式进行模式匹配,且完全支持Unicode,主要应用:字符串验证、搜索、查找替换、分割。正则表达式中字符及字符集正则表达式中的量词正则表达式中的断言QRegExp支持通配符示例: //完整匹配 QRegExp reg("a"); qDebug()<<reg.exactMatch("a"); //任意个数字+两个非数字 QRegExp reg0("\\d*\\D{2}"); qDebug()<<reg0.exactMatch("123ab"); //使用通配符匹配 QRegExp rx("*.txt"); //设置匹配语法 rx.setPatternSyntax(QRegExp::Wildcard);//支持通配符 qDebug()<<rx.exactMatch("123.txt"); //匹配单词边界 QRegExp reg1; //设置匹配模式 reg1.setPattern("\\b(hello|Hello)\\b"); qDebug()<<reg1.indexIn("Hello everyone.");//返回起始下标 //捕获匹配的文本 //由(?:开始。)结束 QRegExp regHight("(\\d+)(?:\\s*)(cm|inchi)"); qDebug()<<regHight.indexIn("Mr.WM 170cm"); qDebug()<<regHight.cap(0);//完整 qDebug()<<regHight.cap(1);//第一部分 //断言?! QRegExp reg2; reg2.setPattern("面(?!包)");//面后面不是包才匹配成功 QString str ="我爱吃面食,面包也行吧。"; str.replace(reg2,"米");//我爱吃米食,面包也行吧 qDebug()<<str; //Qt5引入了新的类 QRegularExpression regExp("hello"); //结果QRegularExpressionMatch(Valid, has match: 0:(0, 5, "hello") qDebug()<<regExp.match("hello world"); regExp.setPattern("[A-Z]{3,8}"); //设置匹配模式-大小写不敏感 regExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); qDebug()<<regExp.match("hello"); QRegularExpression reDate("^(\\d\\d)/(\\d\\d)/(\\d\\d\\d\\d)$");//日期 QRegularExpressionMatch match0 = reDate.match("01/24/2022"); QString strMatch = match0.captured(0); qDebug()<<strMatch; qDebug()<<match0; QString sPattern; sPattern = "^(Jan|Feb|Mar|Apr|May) \\d\\d \\d\\d\\d\\d$"; QRegularExpression rDate1(sPattern); QString ss("Apr 01"); QRegularExpressionMatch match2; match2 = rDate1.match(ss,0,QRegularExpression::PartialPreferCompleteMatch); //部分匹配 qDebug()<<match2.hasMatch();//完整匹配 qDebug()<<match2.hasPartialMatch();//部分匹配 qDebug()<<match2;
相关视频——计算机网络微课堂(有字幕无背景音乐版)前言因特网概述网络、互联(连)网和因特网网络是由若干结点和连接这些结点的链路组成的。多个网络还可以通过路由器互联起来,这样就构成了一个范围更大的网络,即互联网。因此,互联网是网络的网络。因特网是世界上最大的互连网络。internet与Internetinterne互联网是一个通用名词,它泛指由多个计算机网络互连而成的网络。在这些网络之间的通信协议可以是任意的。Internet因特网则是一个专用名词,它指当前全球最大的、开放的、由众多网咯相互连接而成的特定计算机网络,它采用TCP/IP协议族作为通信的规则,前身是美国的ARPANET。因特网发展的三个阶段因特网服务提供者ISPInternet Service Provider基于ISP的三层结构的因特网因特网的标准化工作面向公众。因特网协会ISOC。因特网的组成边缘部分由所有连接在因特网上的主机组成,这部分是用户直接使用的,用来通信(传送数据、音频或视频)和资源共享。核心部分由大量网络和连接这些网络的路由器组成。这部分是边缘部分提供服务的(提供连通性和交换性)。三种交换方式电路交换电路交换Circuit Switching电话交换机接通电话线的方式称为电路交换。从通信资源的分配角度来看,交换(Switching)就是按照某种方式动态地分配传输线路的资源。电路交换的三个步骤:建立连接(分配通信资源)通话(一直占用通信资源)释放连接(归还通信资源)电路交换示意图:当使用电路交换来传送计算机数据时,其线路的传输效率往往很低。分组交换Pactet Switching表示一整块的数据称为报文。发送方-构造分组-发送分组路由器-缓存分组-转发分组(查表转发)接收方-接收分组-还原报文报文交换过时,不做讲解。对比计算机网络的定义和分类计算机网络的定义没有公认的唯一定义。计算机网路最简单的定义是:一些相互连接的、自治的计算机的集合。互连:是指计算机之间可以通过有线或无线的方式进行数据通信。自治:是指独立的计算机,它有自己的硬件和软件,可以单独运行使用。集合:是指至少需要两台计算机。计算机网络较好的定义是:计算机网络主要是由一些,通用的、可编程的硬件互连而成的,而这些硬件并非专门用来实现某一特定目的(例如传送数据或视频信号)。这些可编程的硬件能够用来传送多种不同类型的数据,并能支持广泛的和日益增长的应用。计算机网络的分类按交换技术分类:电路交换网络报文交换网络分组交换网络按使用者分类:公用网专用网按传输介质分类:有线网络无限网络按覆盖范围分类:广域网WAN城域网MAN局域网LAN个域网PAN按拓扑结构分类:总线型网络星型网络环型网络网状型网络计算机网络的性能指标速率带宽吞吐量吞吐量表示在单位时间内容通过某个网络(或信道、接口)的数据量。吞吐量被经常用于对现实世界中的网络的一种测量,以便知道实际上到底有多少数据量能够通过网络。吞吐量受网络的带宽或额定速率的限制。时延完成一次通信在路上各个环节所花费的时间。时延带宽积往返时间利用率信道利用率:用来表示某信道有百分之几的时间是被利用的(有数据通过)。网络利用率:全网络的信道利用率的加权平均。丢包率丢包率即分组丢失了率,是指在一定的时间范围内,传输过程中丢失的分组数量与总分组数量的比率。可以分为接口丢包率、结点丢包率、链路丢包率、路径丢包率、网络丢包率等。丢包率反映了网络的拥塞情况。计算机网络体系结构常见的计算机网络体系结构计算机网络体系结构分层的必要性计算机网络体系结构分层思想举例计算机网络体系结构中的准用术语来源于OSI七层网络结构,也使用与TCP/IP的四层体系结构和五层原理体系结构。实体:任何可发送或接收信息的硬件或软件进程。对等实体:收发双方相同层次中的实体。协议:控制两个对等实体进行逻辑通信规则的集合。之所以称为逻辑通信,是因为真是不存在,提出来是为了方便我们研究其中的某一层而不受其他层干扰。协议的三个要素语法:定义所交换信息的格式。(哪些字段,由哪些数据构成)语义:定义收发双方所要完成的操作。同步:定义收发双方的时序关系。服务:在协议的控制下,两个对等实体间的逻辑通信使得本层能够向上一层提供服务。要实现本层协议,还需要使用下面一层所提供的服务。协议是"水平的",服务是"垂直的"。实体看得见相邻下层提供的服务,但并不知道实现该服务的具体协议。也就是说,下面的协议对上面的实现是"透明"的。服务访问点:在同一系统中相邻两层的实体交换信息的逻辑接口,用于区分不同的服务类型。数据链路层的服务区分那个问点为帧的"类型"字段。网络层的服务访问点为IP数据报首部中的"协议字段"。运输层的服务访问点为"端口号"。协议数据单元PDU:对等层次之间传送的数据包称为该层的协议数据单元。(详情如下图所示)服务数据单元SDU:同一系统内,层与层之间交换的数据包称为服务数据单元。多个SDU可以合成为一个PDU,一个SDU也可以划分为几个PDU。物理层(需要计算)物理层的基本概念物理层考虑的是怎样才能在连接各种计算机的传输媒体上传输数据比特流。物理层为数据链路层屏蔽了各种传输媒体的差异,使数据链路层只考虑如何完成本层的协议和服务,而不必考虑网络具体的传输媒体是什么。物理层协议的主要任务机械特性:指明接口所用接线器的形状和尺寸、引脚数目和排列、固定和锁定装置。电气特性:指明在接口电缆的各条线上出现的电压的范围。功能特性:指明某条线上出现的某一电平的电压表示何种意义。过程特性:指明对于不同功能的各种可能事件的出现顺序。物理层下面的传输媒体传输方式编码与调制码元:(就是构成信号的一段波形)在使用时间域的波形表示数字信号时,代表不同离散数值的基本波形。常用编码基本调制方法使用基本调制方法,1个码元只能包含有一个比特信息。混合调制信道的极限容量失真因素码元传输速率信号传输距离噪声干扰传输媒体质量奈氏准则:在假定的理想条件下,为了避免码间串扰,码元传输速率是有上限的。理想低通道的最高码元传输速率=2W Baud = 2W码元/秒理想带通信道的最高码元传输速率= W Baud = W 码元/秒W:信道带宽(单位为Hz)Baud: 波特,即码元/秒码元传输速率又称为波特率、调制速率、波形速率或符号速率。它与比特率有一定关系:当1个码元只携带1比特的信息时,则波特率(码元/秒)与比特率(比特/秒)在数值上是相等的;当1个码元携带n比特的信息量时,则波特率转换成比特率时,数值要乘以n。要提高信息传输速率(比特率),就必须设法使每个码元能携带更多个比特的信息量。这需要才用多元制。实际的信道所能传输的最高码元速率,要明显低于奈氏准则给出的这个上限数值。香农公式:带宽受限且有高斯白噪声干扰的信道的极限信息传输速率。在信道带宽一定的情况下,根据奈氏准则和香农同时,先要提高信息的传输速率就必须采用多元制(更好的调制方法)和努力提高信道中的信噪比。自从香农公式发表后,各种新的信号处理和调制方法就不断出现,其目的都是为了尽可能地接近香农公式给出的传输速率极限。数据链路层数据链路层概述数据链路层在网络体系结构中所处的地位链路(Link)就是从一个结点到相邻结点的一段物理线路,而中间没有任何其他的交换结点。数据链路(Data Link)是指把实现通信协议的硬件和软件加到链路上,就构成了数据链路。数据链路层的三个重要问题封装成帧差错检测可靠传输:尽管误码是不能完全避免的,但若能实现发送方发送什么,接收方就能收到什么,就成为可靠传输。封装成帧封装成帧是指数据链路层给上层交付的协议数据单元添加帧头和帧尾使之成为帧。帧头和帧尾中包含有重要的控制信息。帧头和帧尾的作用之一就是帧定界。透明传输是指数据链路层对上层交付的传输数据没有任何限制,就好像数据链路层不存在一样。面向字节的物理链路使用字节填充(或称字符填充)的方法实现透明传输。面向比特的物理链路层使用比特填充的方法实现透明传输。为了提高帧的传输效率,应当使帧的数据的长度尽可能大些。考虑到差错控制等多种因素,每一种数据链路层协议都规定了帧的数据部分长度上限,即最大传送单元MTU(Maximum Transfer Unit)。差错检测 实际的通信链路都不是理想的,比特在传输过程中可能会产生差错:1可能会变为0,0可能变为1.这称为比特差错。在一段时间内,传输错误的比特占所传输比特总数的比率称为误码率BER(Bit Error Rate)。使用差错检测码来检测数据在传输过程中是否产生了比特差错,是数据链路层所要解决的重要问题之一。奇偶校验在待发送的数据后面添加1位奇偶校验位,使整个数据(包括所添加的校验位在内)中"1"的个数为奇数(奇校验)或偶数(偶校验)。如果有奇数个位发生误码,则奇偶性发生变化,可以检查出误码。如果有偶数个位发生误码,则奇偶性不会发生变化,不能检查出误码(漏检);循环冗余校验CRC(Cyclic Redundancy Check)收发双方约定好一个生成多项式G(x);发送方基于待发送的数据和生成多项式计算出差错检测码(冗余码),将其添加到待传输数据的后面一起传输;接口双方通过生成多项式来计算收到的数据是否产生了误码。示例:检测码只能检测出帧在传输过程中出现了差错,但并不能定位错误,因此无法纠正错误。想要纠正传输中的差错,可以使用冗余信息更多的纠错码来进行向前纠错。但纠错码的开销比较大,在计算机网络中较少使用。循环冗余校验CRC有很好的检错能力(漏检率非常低),虽然计算比较复杂,但非常有易于用硬件实现,因此被广泛应用于数据链路层。在计算机网络中通常采用我们后续课程中将要讨论的检测重传方式来纠正传输中的差错,或者仅仅是丢弃检测到差错的帧,这取决于 数据链路层向其上层提供的是可靠传输服务还是不可靠传输服务。可靠传输的基本概念使用差错检测技术(例如循环冗余校验CRC),接收方的数据链路层就可检测出帧在传输过程中是否产生了误码(比特错误)。数据链路层向上层提供的服务类型不可靠传输服务:仅仅丢弃有误码的帧,其他什么也不做;可靠传输服务:想办法实现发送端发送什么,接收端就收到什么。一般情况下,有线链路的误码率比较低,为了减小开销,并不要求数据链路层向上提供可靠传输服务。其实出现了误码,可靠传输的问题由其上层处理。无线链路易受干扰,误码率比较高,因此要求数据链路层必须向上层提供可靠传输服务。比特差错只是传输差错中的一种。从整个计算机网络体系结构来看,传输差错还包括分组丢失、分组失序以及分组重复。分组丢失、分组失序以及分组重复这些传输差错,一般不会出现在数据链路层,而会出现在其上层。可靠传输服务并不仅局限于数据链路层,其它各层均可选择实现可靠传输。可靠传输的实现比较复杂,开销也比较大,是否使用取决于应用需求。三种可靠的传输机制停止-等待协议SW回退N帧协议GBN选择重传协议SR这三种可靠传输机制的基本原理并不仅限于数据链路层,可以应用到计算机网络体系结构的各层协议中。可靠传输的实现机制——停止-等待协议SW(Stop-and-Wait)注意事项:接收端检测到数据分组有误码时,将其丢弃并等待发送方的超时重传。但对于误码率较高的点对点链路,为使发送方尽早重传,也可以给发送方发送NAK分组。为了让接收方能够判断所收到的数据分组是否是重复的,需要给数据分组编号。由于停止-等待协议的停等特性,只需1个比特编号就够了,即编号0和1。为了让发送方能够判断所收到的ACK分组是否是重复的,需要给ACK分组编号,所用比特数量与数据分组编号所用的比特数量一样,数据链路层一般不会出现ACK分组迟到的情况,因此在数据链路成实现停止-等待协议可以不用给ACK分组编号。超级计时器设置的重传时间应仔细选择。一般可将重传时间选为略大于"从发送方到接收方的平均往返时间"。在数据链路层点对点的往返时间比较确定,重传时间比较好设定。然而在运输层,由于端到端往返时间非常不确定,设置合适的重传时间有时并不容易。信道利用率放往返时延RTT远大于数据帧发送时延TD时(例如使用卫星链路),信道利用率非常低。若出现重传,则对于传送有用的数据信息来说,信道利用率还要降低。为了克服停止-等待协议信道利用率很低的缺点,就产生了另外两种协议,即后退N帧协议GBN和选择重传协议SR。像停止等待协议这种通过确认和重传机制实现的可靠传输协议,常称为自动请求重传协议ARQ,意思是重传的请求时自动进行的。因为不需要接收方显式地请求发送方重传某个出错的分组。回退N帧协议GBN回退N帧协议在流水线传输的基础上利用发送窗口来限制发送方连续发送数据分组的数量,是一种连续ARQ协议。在协议的工作过程中发送窗口和接收窗口不断向前滑动,因此这类协议又称为滑动窗口协议。由于回退N帧协议的特性,当通信线路质量不好时,其信道利用率并不比停止-等待协议高。选择重传协议点对点协议PPP点对点协议PPP(Point-to-Protocol)是目前使用最广泛的点对点数据链路层协议。PPP协议为在点对点链路传输各种协议数据报提供了一个标准方法,主要由一下三部分构成:对各种协议数据报的封装方法(封装成帧)链路控制协议LCP,用于建立、配置以及测试数据链路的连接一套网络控制协议NCPs,其中的每一个协议支持不同的网络协议层帧格式透明传输面向字节的异步链路采用插入转义字符的字节填充法发送方的处理:出现的每一个7E(PPP帧的定界符)字节转变成2字节序列(7D,5E)。出现的每一个7D(转义字符)变成2个字节序列(7D,5D)。出现的一个ASCII码控制字符(数值小于0x20的字符),则在该字符前面插入一个7D字节,同时将该字符的编码加上0x20。接收方的处理:进行反变换即可恢复出原来的帧的数据部分。面向比特的同步链路采用插入比特0的比特填充法。发送方的处理:对帧的数据部分进行扫描(一般由硬件实现)。只要发现5个连续的比特1,则立即填充1个比特0。接收方的处理:对帧的数据部分进行扫描(一般由硬件实现)。只要发现5个连续的比特1,就把其后的1个比特0删除。差错检测接收方每收到一个PPP帧,就进行CRC检验。若CRC检验正确,就收下这个帧;反之,就丢弃这个帧。使用PPP的数据连路程向上不提供可靠传输服务。工作状态媒体接入控制基本概念共享信道要着重考虑的一个问题就是如何协调多个发送和接受站点对 一个共享传输媒体的占用,即 媒体介入控制MAC。随着技术的发展,交换技术的成熟和成本的降低,具有更高性能的点对点链路和链路层交换机的交换式局域网在有线领域已经完全取代了共享式局域网,但由于无线信道的广播天性,无限局域网仍然使用的是共享媒体技术。静态划分信道信道复用复用(Multiplexing)是通信技术中的一个重要概念。复用就是通过一条物理线路同时传输多路用户的信号。当网络中传输媒体的传输容量大于多条单一信道传输的总通信量时,可利用复用技术在一条物理线路上建立多条通信信道充分利用传输媒体的带宽。常见的几种信道复用技术频分复用FDM频分复用的所有用户同时占用不同的频带资源并行通信。时分复用TDM波分复用WDM码分复用CDM复用和多址复用是将单一媒体的频带资源划分为很多子信道,这些子信道之间相互独立,互不干扰。从媒体的整体频带资源上看,每个子信道只占用该媒体频带资源的一部分。多址( 更确切说应该称为多点接入)处理的是动态分配信道给用户。这在用户仅仅暂时性地占用信道的应用中是必须的,而所有的移动通信系统基本上都属于这种情况。相反,在信道用酒地分配给用户的应用中,多址是不需要的(对于无线广播或电视广播站就是这样)。某种程度上,FDMA、TDMA、CDMA可以分别看成是FDM、TDM、CDM的应用。动态接入控制——随机接入CSMA/CD协议——争用期(碰撞窗口)CSMA/CD——总线局域网使用的协议CSMA/CD协议——最小帧长以太网规定最小帧长为64字节,即521比特(512比特时间为争用期);如果要发送的数据非常少,那么必须加入一些填充字节,使帧长不小于64字节。以太网的最小帧长确保了主机可在帧发送完成之前就检测到该帧的发送过程中是否遭遇了碰撞。如果在争用期(共发送64字节)没有检测到碰撞,那么后续发送的数据就一定不会发生碰撞。如果在争用期内检测到碰撞,就立即终止发送,这时已经发送出去的数据一定小于64字节,因此凡长度小于64字节的帧都是由于碰撞而异常中止的无效帧。CSMA/CD协议——最大帧长CSMA/CD协议——截断二进制指数退避算法若连续多次发生碰撞,就表明可能有较多的主机参与竞争信道。但使用上述退避算法可使重传需要推迟的平均时间随重传次数而增大(这也称为动态退避),因而减小发生碰撞的概率,有利于整个系统的稳定。当重传达16次仍不能成功时,表明同时打算发送帧的主机太多,以至于连续发生碰撞,则丢弃该帧,并向高层报告。CSMA/CA——信道利用率CSMA/CD协议——帧发送流程CSMA/CD协议——帧接收流程动态接入控制——随机接入无线局域网使用的协议——CSMA/CA802.11无线局域网使用CSMA/CA协议,在CSMA的基础上增加了一个碰撞避免CA功能。由于不可能避免所有的碰撞,并且无线信道误码率较高,802.11标准还使用了数据链路层确认机制(停止-等待协议)来保证数据被正确接收。802.11的MAC层标准定义了两种不用的媒体接入控制方式:帧间间隔IFSCSMA/CA协议的工作原理当站点检测到信道是空闲的,并且所发送的数据帧不是成功发送完上一个数据帧之后立即连续发送的数据帧,则不使用退避算法。以下情况必须使用退避算法在发送数据帧之后检测到信道处于忙状态时;在每一次重传一个数据帧时;在每一次成功发送后要连续发送下一个帧时(这是为了避免一个站点长时间占用信道。)CSMA/CA协议的退避算法CSMA/CA协议的信道预约和虚拟载波监听集线器与交换机的区别使用集线器的以太网在逻辑上扔是一个总线网,各站共享总线资源,使用的还是CSMA/CD协议;集线器只工作在物理层,它的每个接口仅简单地转发比特,不进行碰撞检测(由各站的网卡进行检测);集线器一般都有少量的容错能力和 网络管理功能。例如:若网络中某个网卡出现了故障, 不停的发送帧。此时,集线器可以检测到这个问题,在内部断开与出故障网卡的连线,使整个以太网仍然可以正常工作。使用集线器HUB在物理层扩展以太网以太网交换机以太网交换机通常都有多个接口。每个接口都可以直接与一台主机或另一个以太网交换机相连。一般都工作在全双工方式。以太网交换机具有并行性,能同时连通多对接口,使多对主机能同时通信,无碰撞(不使用CSMA/CD协议)。以太网交换机一般都具有多种速率的接口 ,例如:10mb/s、100mb/s、1Gb/s、10Gb/s接口的多种组合。以太网交换机工作在数据链路层(也包括物理层),它收到帧后,在帧交换表中查找帧的目的MAC地址所对应的接口号,然后通过该接口转发帧。以太网交换机是一种即插即用设备,其内部的帧交换变是通过自学习算法自动地逐渐建立起来的。、帧的两种转发方式:存储转发直通交换:采用基于硬件的交叉矩阵(交换时延非常小,但不检查帧是否有差错)。交换机可以隔离碰撞域对比以太网交换机自学习和转发帧的流程交换机工作在数据链路层(也包括物理层)以太网交换机收到帧后,在帧交换表中查找帧的目的AMC地址所对应的接口号,然后通过该接口转发帧。以太网交换机是一种即插即用的设备,刚上电启动时其内部的帧交换表是空的。随着网络中各主机间的通信,以太网交换机通过自学习算法自动主键建立起帧交换表。每条记录的都有自己的有效时间,到期自动删除,这是因为MAC地址交换机接口的对应关系不是永久性的。以太网交换机的生成树协议STP如何提高以太网的可靠性?添加冗余链路可以提高以太网的可靠性,同时也会带来负面效应——形成网络环路。网络环路会带来以下问题广播风暴——会大量消耗网络资源,使得网络无法正常转发其他数据帧;主机收到重复的广播帧,大量消耗主机资源;交换机的帧交换表震荡(漂移)。以太网交换机使用生成树协议STP可以在增加冗余链路来提高网路可靠性的同时又避免网络环路带来的各种问题。不论交换机之间采用怎样的物理连接,交换机都能自动计算并构建一个逻辑上没有环路的网络,其逻辑拓扑结构必须是树型的(无逻辑环路);最终生成的树型逻辑拓扑要确保连通整个网络;当首次链接交换机或网络物理拓扑发生变化时(有可能是认为改变或故障),交换机都将进行生成树的重新计算。虚拟局域网VLAN概述以太网交换机工作在数据链路层(也包括物理层)使用一个或多个以太网交换机互连起来的交换机以台网,其所有站点都属于同一个广播域。随着交换式以太网规模的扩大,广播域相应扩大。巨大的广播域会带来很多弊端广播风暴难以管理和维护潜在的安全问题广播风暴会浪费网络资源和各主机的CPU资源。网络中会频繁使用广播信息TCP/IP协议栈中的很多协议都会使用广播地址解析协议ARP(已知IP地址,找出其相应的MAC地址)路由信息协议RIP(一种小型的内部路由协议)动态主机配置协议DHCP( 用于自动配置IP地址)分割广播域的方法使用路由器可以分割广播域因为路由器默认情况下不对广播数据包进行转发,因此路由器很自然的就可以隔绝广播域,但是成本比较高,在这种情况下,虚拟局域网孕育而生。虚拟局域网VLAN(Virtual Local Area Network)是一种将局域网内的设备划分成与物理层位置无关的逻辑组的技术,这些逻辑组具有某些共同的需求。也就是说同一个VLAN中可以广播通信,不用 VLAN之间不能广播通信。虚拟局域网VLAN的实现机制交换机的端口类型有以下三种AccessTrunkHybrid交换机各端口的缺省VLAN ID在思科交换机上称为Native VLAN,即本征VLAN。在华为交换机上称为Port VLAN ID,即端口VLAN ID,简称PVID。MAC地址、IP地址以及ARP协议MAC地址是以太网的MAC子层所使用的地址;(数据链路层)IP地址是TCP/IP体系结构网际层所使用的地址;(网际层)ARP协议属于TCP/IP体系结构的网际层,其作用是已知设备所分配到的IP地址,使用ARP协议可以通过该IP地址所获取到设备的MAC地址;(网际层)尽管IP地址和ARP协议属于TCP/IP体系结构的网际层(而不属于数据链路层),但是它们与MAC地址存在一定的关系,并且我们日常的网络应用都离不开MAC地址 、IP地址以及ARP协议。因此我们将这三者放在一起讨论。MAC地址使用点对点信道的数据链路层不需要使用地址。当多个主机连接在同一个广播信道上,要想实现两个主机之间的通信,则每个主机都必须有一个唯一的标识,即一个数据链路层地址;在每个主机发送的帧中必须携带标识发送主机和接收主机的地址。由于这类地址是用于媒体接入控制MAC(Media Access Control),因此这类地址被称为MAC地址。MAC地址一般是被固化在网卡(网络适配器)的电可擦可编程只读存储器EEPROM中,因此MAC地址也被称为硬件地址;MAC地址有时也被称为物理地址。请注意:这并不意味着MAC地址属于网络体系结构中的物理层。 (属于数据链路层。)一般情况下,用户主机会包含两个网络适配器:有线局域网适配器(有线网卡)和无线局域网适配器(无线网卡),每个网络适配器都有一个全球唯一的MAC地址。而交换机和路由器往往拥有更多的网络接口,所以会拥有更多的MAC地址。综上所述,严格来说,MAC地址是对网络上各接口的唯一标识,而不是对网络上各个设备的唯一标识。IEEE 802局域网的MAC地址格式IEE 802局域网的MAC地址发送顺序字节发送顺序: 第一字节——》第六字节字节内的比特发送顺序: b0——》b7单播MAC地址举例广播MAC地址举例判断地址是否是多播地址随机MAC地址略......IP地址IP地址属于网络层的范畴。IP地址是因特网(Internet)上的主机和路由器所使用的地址,用于标识两部分信息:网络编号:标识因特网上数以百万计的网络主机编号:标识同一网络上不同主机(或路由器各接口)从网络体系结构看IP地址与MAC地址上层无需看懂上一层的数据结构和内容,只需要添加首部相关信息即可,然后继续交付给下一层。数据包转发过程中IP地址与MAC地址的变化情况(可以想象成对应层进行水平通信)数据包转发过程中源IP地址和目的IP地址保持不变;数据包转发过程中源MAC地址和目的MAC地址逐个链路(或逐个网络)改变;ARP协议不能跨网路使用。通过ARP协议来通过IP地址找到对应的MAC地址。网络层网络层的主要任务是实现网络互连,进而实现数据包在各网络之间的传输。要实现网络层任务,需要解决以下主要问题:网络层向运输层提供怎样的服务("可靠传输"还是"不可靠传输")网络层寻址问题路由选择问题因特网(Internet)是目前全世界用户数量最多的互联网,它使用TCP/IP协议栈。由于TCP/IP协议栈中的网络层使用网际协议IP,它是整个协议栈的核心协议,因此在TCP/IP协议栈中网络层常称为网际层。综上所述 ,我们通过学习TCP/IP协议栈的网际层来学习网络层的理论知识和实践技术。网络层提供的两种服务面向连接的虚电路服务可靠通信由网络来保证必须建立网络层的连接——虚电路VC(Virtual Circuit)通信双方沿着已建立的虚电路发送分组目的主机的地址仅在连接建立阶段使用,之后每个分组的首部只需要携带一条虚电路的编号(构成虚电路的每一段电路都有一个虚电路编号)。这种通信方式如果再使用可靠传输协议的网络协议,就可使所发送的分组最终正确到达接收方(无差错按序到到达、不丢失、不重复)。通讯结束之后,需要释放之前所建立的虚电路。很多广域分组交换网都使用面向无连接的虚电路服务。例如,曾经的X.25和逐渐过时的帧中继FR,异步传输模式ATM等。无连接的数据报服务可靠通信应由用户主机来保证不需要建立网络层链接每个分组可走不同的路径每个分组的首部必须携带目的主机的完整地址这种通信方式所传送的分组可能误码、丢失、重复和失序。由于网络本身 不提供端到端的可靠传输服务,这就使网络中的路由器可以做得比较简单,而且价格低廉(与电信网的交换机相比较)。因特网采用了这种设计思想,也就是将复杂的网络处理功能置于因特网的边缘(用户主机和其内部的运输层),而将相对简单的尽最大努力的分组交换功能置于因特网核心。、采用这种设计思想的好处是,网络的造假大大降低,运行方式灵活,能够适应多种应用。由于TCP/IP体系结构的因特网的网际层提供的是简单灵活、无连接的、尽最大努力交付的数据报服务,因此本章主要围绕网际层如何传送IP数据报这个主题进行讨论。IPv4地址概述IPv4地址就是给因特网上的每一台主机(或路由器)的每一个接口分配一个在全世界范围内是唯一的32比特的标识符。IP地址由因特网名字和数字分配机构ICANN进行分配我国用户可向亚太网络信息中西APNIC申请IP地址,需要缴费,且一般不接受个人申请。IPv4地址的编址方法经历了如下三个历史阶段:32比特的IPv4地址采用点分十进制表示方法以方便用户使用。分类编址的IPv4地址A类地址B类地址C类地址划分子网的IPv4地址为新增网络申请新的网络号会带来以下弊端等待需要时间和花费更多的费用会浪费其他路由器中路由表记录的数据浪费原有网络中剩余的大量IP地址解决办法:从主机号部分借用一部分作为子网号32 比特的子网掩码可以表明分类IP地址的主机号部分被借用了几个比特作为子网号。子网掩码使用连续的比特1来对应网络号和子网号子网掩码使用连续的比特0来对应主机号将划分子网的IPv4地址与其相应的子网掩码进行逻辑与运算就可以得到IPv4地址所在子网的网络地址默认的子网掩码是指在未划分子网的情况下使用的子网掩码无分类编址的IPv4地址划分子网在一定程度上缓解了因特网在发展中遇到的困难,但是数量巨大的C类网因为其地址空间太小并没有得到充分使用,而因特网的IP地址仍在加速消耗,整个IPv4地址空间面临全部消耗的威胁。为此,引因特网工程任务组IETF又提出了采用无分类编址的方法来解决IP地址紧张的问题,同时还专门成立IPv6工作组负责研究新版本IP工作组负责研究新版本IP以彻底解决IP地址耗尽问题。1993年,IETF发布了五分类域间路由器选择CIDR的RFC文档,RFC 1517 ~ 1519和1520。CIDR消除了传统的A类、B类和C类地址,以及划分子网的概念CIDR可以更加有效的分配IPv4的地址空间,并且可以在新的IPv6使用之前允许因特网的规模继续增长。CIDR使用“斜线记法”,或称为CIDR记法。 即在IPv4地址后面加上斜线“/”,在斜线后面写上网络前缀所占的比特数量。CIDR实际上是将网络前缀都相同的连续IP地址组成一个"CIDR地址块"。我们只要知道CIDR地址块中的任何一个地址,就可以知道该地址块的全部细节;地址块的最小地址地址块的最大地址地址块中的地址数量地址块聚合某类网络(A 类、B类或C类)的数量地址掩码(也可继续称为子网掩码)IPv4地址的应用规划定长的子网掩码LFSM使用同一子网掩码来划分子网每个子网所分配的IP地址数量相同,造成IP地址的浪费子网划分方式不灵活,只能划分出2的n次方个子网(n是从主机号部分借用的用来作为子网号的比特数量)变长的子网掩码VLSM使用不同的子网掩码来划分子网每个子网所分配的IP地址数量可以不同,尽可能减少对IP地址的浪费子网划分方式灵活,可以按需分配。IP数据报的发送和转发过程IP数据报的发送和转发过程包含以下两个部分主机发送IP数据报路由器转发IP数据报主机发送IP数据报判断目的主机是否与自己在同一网络若在同一个网络,则属于直接交付若不在同一个网络,则属于间接交付,传输给主机所在网络的默认网关(路由器),由默认网关帮忙转发。路由器转发IP数据报检查IP数据报首部是否出错若出错,则直接丢弃该IP数据报并通报源主机;若没有出错,则进行转发。根据IP数据报的目的地址在路由表中查找匹配的条目若找到匹配条目,则转发给条目中知识的下一跳;若找不到,则丢弃该IP数据报并通告源主机。静态路由配置及其可能产生的路由环路问题静态路由配置是指用户或网络管理员使用路由器的相关命令给路由器人工配置路由表。这种人工配置方式简单、开销小。但不能及时适应网络状态(流量、拓扑等)的变化。一般只在小规模网络中可采用。使用静态路由配置可能出现一下情况导致产生路由环路的错误配置错误聚合了不存在的网络网络故障路由条目的类型直连网络静态路由(人工配置)动态路由(路由选择协议)特殊的静态路由条目默认路由(目的网络为0,0,0,0,地址掩码为0,0,0,0)特定主机路由(目的网络为特定主机的IP地址,地址掩码为255,255,255,255)黑洞路由(下一条为null0)路由选择协议概述静态路由选择由人工配置的网络路由、默认路由、特定主机路由、黑洞路由等都属于静态路由。这种人工配置方式简单、开销小。但不能即使适应网络状态(流量、拓扑等)的变化。一般只在小规模网络中采用。动态路由选择路由器通过选协议自动获取路由信息。比较复杂、开销比较大。适用于大规模网络。因特网所采用的路由选择协议主要特点自适应:动态路由选择,能较好地适应网络状态的变化。分布式:路由器之间交换路由信息。分层次:将整个因特网划分为许多较小的自治系统AS(Autonomous System)常见的路由选择协议路由器的基本结构路由信息协议RIP的基本工作原理路由信息协议RIP是内部网关协议IGP中最先得到广泛使用的协议之一,其相关标准文档为RFC 1058。RIP要求自治系统AS内的每一个路由器都要维护从它自己到AS内其它每一个网络的距离记录。这是一组距离,称为"距离向量D-V"。RIP使用跳数(Hop Count)作为度量(Metric)来衡量到达目的网络的距离。路由器到直连网络的距离定义为1。路由器到非直连网络的距离定义为所经过的路由器数加1。允许一条路径最多只能包含15个路由器。"距离"等于16时相当于不可达。因此,RIP只适用于小型互联网。RIP认为好的路由就是"距离短"的路由,也就是所通过路由器数量最少的路由。当到达同一目的网络有多条"距离相等"的路由时,可以进行等价负载均衡。RIP包含以下三个要点:和谁交换信息,仅和相邻路由器交换信息交换什么信息,自己的路由表何时交换信息,周期性交换(例如每30秒)RIP存在"坏消息传播 "的问题。"坏消息传播得慢"又称为路由环路或距离无穷计数问题,这是距离向量算法的一个固有问题。可以采取多种措施减少出现该问题的概率或减小该问题带来的危害。限制最大路径距离为15(16表示不可达)当路由表发生变化时就立即发送更新报文(即"触发更新"),而不仅是周期性发送。让路由器记录收到某特定路由信息的接口,而不让同一路由信息再通过此接口向反方向传送(即"水平分割")开放最短路径优先OSPF的基本工作原理开放最短路径优先OSPF,是为了客服RIP的缺点在1989年开发出来的。"开放"表明OSPF协议不是受某一家厂商控制,而是公开发表的。"最短路径优先"是因为使用了Dijkstra提出的最短路径算法SPF。OSPF是基于链路状态的,而不像RIP那样是基于距离向量的。OSPF采用SPF算法计算路由,从算法上保证了不会产生路由环路。OSPF不限制网络规模,更新效率高,收敛速度快。链路状态是指本路由器都和哪些路由器相邻,以及相应链路的"代价"(cost)。"代价"用来表示费用、距离、时延、带宽、等等。这些都由网络管理人员来决定。OSPF相邻路由器之间通过交互问候(Hello)分组,建立和维护邻居关系。Hello分组封装在IP数据报中,发往组广播地址224.0.0.5。发送周期为10秒。40秒未收到来自邻居路由器的Hello分组,则认为该邻居路由器不可达。使用OFS的每个路由器都会产生链路状态通告LSA,LAS中包含了以下内容:直连网络的链路状态信息邻居路由器的链路状态信息LSA被封装在链路状态更新分组LSU中,采用洪泛法发送。使用OSPF的每个路由器都有一个链路状态数据路LSDB,用于从存储LSA。通过各路由器洪泛法送封装有自己LSA的LSU分组,各路由器的LSDB最终将达到一致。使用OSPF的各路由器基于LSDB进行最短路径优先SPF计算,构建出各自到达其他各路由器的最短路径,即构建各自的路由表。OSPF有以下五种分组类型。OSPF的基本工作过程OSPF在多点接入网络中路由器邻居关系的建立选举指定路由器DR和备用的指定路由器BDR所有的非DR/BDR只与DR/BDR建立邻居关系表非DR/BDR之间通过DR/BDR交换信息为了使OSPF能够用于规模很大的网络,OSPF把一个自治系统再划分为若干个更小的范围,叫做区域。边际网关协议BGP的基本工作 原理因特网采用分层次的路由选择协议内部网关协议IGP(例如路由信息协议RIP或开放最短路径优先OSPF)设法使分组在一个自治系统内尽可能有效地从开源网络传输到目的网络无需考虑自治系统外部其他方面的策略外部网关协议EGP(例如边界网关协议BGP)在不同的自治系统内,度量路由的"代价"(距离,带宽,费用等)可能不同。因此,对于自治系统之间的路由选择,使用"代价"作为度量来寻找最佳路由是不行的。自治系统之间的路由选择必须考虑相关策略(政治,经济,安全等)在配置BGP时,每个自治系统的管理员要选择至少一个路由器作为该自治系统的"BGP发言人"。在此TCP连接上交换BGP报文以建立BGP会话。利用BGP会话交换路由信息(例如,增加新的路由,或撤销过时的路由,以及报告出错的情况。)使用TCP连接交换路由信息的两个BGP发言人,彼此称为对方的邻站或对等站。BGP发言人除了运行BGP之外,还必须运行自己所在自治系统所使用的内部网关协议IGP,例如OSPF或RIP。BGP发言人交换网络可达性的信息(要到达某个网络所要经过的一些系列自治系统)当BGP发言人互相交换了网络可达的信息后,各BGP发言人就根据所采用的策略从收到的路由器信息中找出到达各自自治系统的较好的路由。也就是构造出树型结构、不存在回路的自治系统连通图。BGP适用于多级结构的因特网BGP-4有以下四种报文OPEN(打开)报文:用来与相邻的另一个BGP发言人建立关系,使通信初始化。UPDATE(更新报文):用来通告某一路由的信息,以列出要撤出的多条路由。KEEPALIVE(保活)报文:用来周期性地证实邻站的连通性。NOTIFICATION(通知)报文:用来发送检测到的差错。IPv4数据报的首部格式版本占4比特,表示IP协议的版本。通信双方使用的IP协议的版本必须一致。目前广泛使用的IP协议版本号为4(即IPv4)。首部长度占4个比特,表示IP数据报首部的长度。该字段的取值以4字节为单位。最小十进制取值为5,表示IP数据报首部只有20字节固定部分。最大十进制取值为15,表示IP数据报首部包含20字节固定部分和最大40字节可变部分。可选字段长度从1字节到40个字节不等。用来支持排错、测量及安全等措施。可选字段增加了IP数据报的功能,但这同时也使得IP数据报的首部长度成为可变的。这就增加了每一个路由器处理IP数据报的开销。实际上可选字段很少被使用。填充字段确保首部长度为4字节的整数倍。使用全0填充。区分服务占8比特,用来获得更好的服务。该字段在旧标准中叫作服务类型,但实际上一直没有被使用过。1998年,因特网工程任务组IETF把这个字段改名为区分服务。利用该字段的不同数值可提供不同等级的服务质量。只有在使用区分服务时,该字段才起作用。一般情况下都不使用该字段。总长度占16比特,表示IP数据报的总长度(首部+数据载荷)。最大取值为十进制的65535,以字节为单位。标识占16比特,属于同一个数据报的各分片数据报应该具有相同的标识。IP软件维持一个计数器,每产生一个数据报,计数器值加1,并将此值赋给标识字段。与下面两个字段共同用于IP数据报分片。标志占3个比特, 各比特含义如下:DF位:1表示不允许分片,0表示允许分片MF位:1表示"后面还有分片",0表示"这是最后一个分片"片偏移占13个比特,指出分片数据报的数据载荷部分偏移其在原数据报的位置有多少个单位。片偏移以8个字节为单位。生存周期TT占8比特,最初以秒为单位,最大生存周期为255秒;路由器转发IP数据报时,将IP数据报首部中的该字段的值减去IP数据报在本路由器上所耗费的时间,若不为0就转发,否则就丢弃。 现在以“跳数”为单位,路由器转发IP数据报时,将IP数据报首部中的该字段的值减1,若不为0就转发,否则就丢弃。协议占8比特,指明IPv4数据报的数据部分是何种协议数据单元。常用的一些协议和相应协议字段值如下。例如:首部检验和占16比特,用来检测首部在传输过程中是否出现差错。比CRC检验码简单,称为因特网检验和。IP数据报每经过一个路由器,路由器都要重新计算首部检验和,因为某些字段(生存时间、标志、片偏移等)的取值可能发生变化。由于IP层本身并不提供可靠传输的服务,并且计算首部校验和是一项耗时的操作,因此在IPv6中,路由器不再计算首部校验和,从而更快转发IP数据报。源IP地址和目的IP地址各占32比特,用来填写发送该IP数据报的源主机的IP地址和接收该IP数据报的目的主机的IP地址。网际控制报文协议ICMP为了更有效地转发IP数据报和提高交付成功的机会,在网际层使用了网际控制报文协议ICMP(Internet Control Message Protocol)。主机或路由器使用ICMP来发送差错报告报文和询问的报文。ICMP报文被封装在IP数据报中发送。ICMP差错报告报文共有以下5种:终点不可达当路由器或主机不能交付数据报时,就向源点发送终点不可达报文。具体可再根据ICMP的代码字段细分为目的网络不可达、目的主机不可达、目的协议不可达、目的端口不可达、目的网络未知、目的主机未知等13种错误。源点抑制当路由器或主机由于拥塞而丢弃数据报时,就向源点发送源点抑制报文,使源点知道应当把数据报的发送速率放慢。时间超过当路由器收到一个目的IP地址不是自己的IP数据报,会将其生存时间TTL字段的值减1。若结构不为0,则将该IP数据报转发出去;若结果为0,除丢弃该IP数据报外,还要向源点发送时间超过报文。另外,当终点在预先规定的时间内不能收到一个数据报的全部报片时,就把已收到的数据报片都丢弃,也会向源点发送时间超过报文。参数问题当路由器或目的主机收到IP数据报后,根据其首部中的检验和字段发现首部在传输过程中出现了误码,就丢弃该数据报,并向源点发送参数问题报文。改变路由(重定向)路由器把改变路由报文发送给主机,让主机知道下次应将数据报发送给另外的路由器(可通过更改好的路由)。以下情况不应发送ICMP差错报告报文:对ICMP差错报告不再发送ICMP差错报告报文对第一个分片的数据数据报片的所有后序数据报片都不发送ICMP差错报告报文对具有多播地址的数据报都不发送ICMP差错报告报文对具有特殊地址(如127.0.0.0或0.0.0.0)的数据报不发送ICMP差错报告常见的ICMP询问报文有以下两种:回送请求和回答:ICMP回送请求报文是由主机或路由器向一个特定的目的主机发出的询问。收到此报文的主机必须给源主机或路由器发送ICMP回送回答报文。这种询问报文用来测试目的站是否可达及了解其有关状态。时间戳请求和回答:ICMP时间戳请求报文是请某个主机或路由器回答当前的日期和时间。在ICMP时间戳回答报文中有一个32位的字段,其中写入的整数代表从1900年1月1日起到当前时刻一共有多少秒。这种询问报文用来进行时钟同步和测量时间。ICMP应用举例分组网间探测PING(Packet InterNet Groper)用来测试主机或路由器间的连通性应用层直接使用网际层的ICMP(没有通过运输层的TCP或UDP)使用ICMP回送请求和报文回答跟踪路由(traceroute)用来测试IP数据报从源主机到达目的主机要经过哪些路由器Windows版本tracert命令应用层直接使用网际层ICMP使用了ICMP回送请求和回答报文以及差错报告报文。Unix版本traceroute命令在运输层使用UDP协议仅使用ICMP差错报告报文虚拟专用网VPN与网络地址转换NAT虚拟专用网VPN(Virtual Private Network)利用公用的因特网作为本机构各专用网之间的通信载体,这样的专用网又称虚拟专用网。由于IPv4地址的紧缺,一个机构能够申请到的IPv4地址数量往往小于本机构所拥有的主机数量。因此,虚拟专用网中的各主机所分配的地址应该是本机构可自由分配的专用地址,而不是需要申请的、在因特网上使用的公有地址。如下图所示,同一个机构内不同部门的内部网络所构成的虚拟专用网VPN又称为内联网VPN。有时一个机构的VPN需要有某些外部机构(通常就是合作伙伴)参加进来。这样的VPN就称为外联网VPN。在外地工作的员工需要访问公司内部的专用网络时,只要在任何地点接入到因特网,运行驻留在员工PC中的VPN软件,在员工的PC和公司的主机之间建立VPN隧道,即可访问专用网络中的资源。这种VPN称为远程接入VPN。网络地址转换NAT(Network Address Translation)虽然因特网采用了无分类编制方式来减缓IPv4地址空间耗尽的速度,但由于因特网用户数目的激增,特别是大量小型办公室网络和家庭网络接入因特网的需求不断增加,IPv4地址空间即将面临耗尽的危险仍然没有被解除。1994年提出了一种网络地址转换NAT的方法再次缓解了IPv4地址空间即将耗尽的问题。NAT能使大量使用内部专用地址的专用网络用户共享少量外部全球地址来访问因特网上的主机和资源。由于绝大多数的网络应用都是使用运输层协议TCP或UDP来传送数据,因此可以利用运输层的端口号和IP地址一起进行转换。这样,用一个全球IP地址就可以使多个拥有本地地址的主机同时和因特网上的主机进行通信。这种将端口和IP地址一起进行转换的技术叫做NAPT(Network Address and Port Translation)。对于一些P2P网络应用,需要外网主机主动与内网主机进行通信,在通过NAT时会遇到问题,需要网络应用自己使用一些特殊的NAT穿越技术来解决问题。另外,由于NAT对外网屏蔽了内网主机的网络地址,能为内网主机提供一定的安全保护。运输层计算机网络体系中的物理层、数据链路层以及网络层它们共同解决了将主机通过异构网络互联起来所面临的问题,实现了主机到主机的通信。但实际上在计算机网络中进行通信的真正实体是位于通信两端主机中的进程。如何为运行在不同主机上的应用进程提供直接的通信服务是运输层的任务,运输层协议又称为端到端协议。运输层直接为应用进程间的逻辑通信提供服务。运输层向高层用户屏蔽了下面网络核心的细节(如网络拓扑、所采用的路由选择协议等),它使应用进程看见的就好像在两个运输层实体之间有一条端到端的逻辑通信信道。根据应用需求的不同,因特网的运输层为应用层提供了两种不同的运输协议,即面向连接的TCP和面向无连接的UDP。运输层端口号、复用与分用的概念运行在计算机上的进程使用进程标识符PID来标志。因特网上的计算机并不是统一的操作系统,不同的操作系统又使用不同格式的进程标识符。为了使运行不同操作系统的计算机的应用进程之间能够进行网络通信,就必须使用统一的方法对TCP/IP体系的应用进程之间进行标识。TCP/IP体系的运输层使用端口号来区分应用层的不同应用进程。端口号使用16比特来表示,取值范围0~65535熟知端口号:0~1023,IANA把这些端口号指派给了TCP/IP体系中最重要的一些应用协议,例如:FTP使用21/20,HTTP使用80,DNS使用53。登记端口号:1024~49151,为没有熟知端口号的应用程序使用。使用这类端口号必须在IANA按照规定的手续登记,以防止重复。例如:Microsoft RDP微软远程桌面使用端口3389。短暂端口号:49152~65535,留给客户进程选择暂时使用。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的动态端口号。通信结束后,这个端口号可供其他客户进程以后使用。端口号只具有本地意义,即端口号知识为了标识本计算机的应用层中的各进程,在因特网中,不同计算机中的相同端口号是没有联系的。发送方的复用和接收方的分用TCP/IP体现的应用层常用协议所使用的运输层熟知端口号。UDP和TCP的对比UDP和TCP是TCP/IP体系结构运输层中的两个重要协议。总结:TCP的流量控制一般来说,我们总是希望数据传输的快一点。但如果发送方把数据发送得过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收。利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制。TCP接收方利用自己的接收窗口的大小来限制发送方发送窗口的大小。TCP发送方收到接收方的零窗口通知后,应启动持续计时器。持续计时器超时后,向接收方发送零窗口探测报文。TCP的拥塞控制在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络性能就要变坏。这种情况就叫做拥塞。在计算机网络中的链路容量(即带宽)、交换结点中的缓存和处理机等,都是网络的资源。若出现拥塞而不进行控制,整个网络的吞吐量将随输入负荷的增大而下降。TCP四种拥塞控制算法假定如下条件:数据是单方向传送,而另一个方向只传送确认。接收方总是有足够大的缓存空间,因而发送方发送窗口的大小由网络的拥塞程度来决定。以最大报文段MSS的个数为讨论问题的单位,而不是以字节为单位。慢开始和拥塞避免算法是1988天提出的TCP拥塞控制算法(TCP Tahoe版本)。1990年又增加了两个新的拥塞控制算法(改进TCP的性能),这就是快重传和快恢复(TCP Reno版本)有时个别报文段会在网络中丢失,但实际上网络并未发生拥塞。这将导致发送方超时重传,并误认为网络发生了拥塞;发送方把拥塞窗口cwnd又设置为最小值1,并错误地启动慢开始算法,因而降低了传输效率。采用快重传算法可以让发送方尽早知道发生了个别报文段的丢失。所谓快重传,就是使发送方尽快进行重传,而不是等超时重传计时器超时再重传。要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认。即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认。发送方一旦收到了3个连续的重复确认,就将相应的报文段立即重传,而不是等待该报文段的超时重传计时器超时再重传。对于个别丢失的报文段,发送方不会出现超时重传,也就不会误认为出现了拥塞(进而降低拥塞窗口cwnd为11)。使用快重传可以使整个网络的吞吐量提高约20%。发送方一旦接收3个重复确认,就知道现在只是丢失了个别的报文段。于是不启动慢开始算法,而执行快恢复算法。发送方将慢开始门限ssthresh值和拥塞窗口值调整为当前窗口的一半;开始执行拥塞避免算法。也有的快恢复实现是把快恢复开始时的拥塞窗口cwnd值再增大一些,即等于新的ssthresh+3。既然发送方收到3个重复的确认,就表明有3个数据报文已经离开了网络;这3个报文段不再小号网络资源而是停留在接收方的接受缓存中;可见现在网络中不是堆积了报文段而是减少了3个报文段。因此可以适当把拥塞窗口扩大些。TCP超时重传时间的选择超时重传时间的选择是TCP最复杂的问题之一TCP可靠传输的实现TCP基于以字节为单位的滑动窗口来实现发送方个各部分解释如何描述发送窗口的状态?接收方各部分解释虽然发送方的发送窗口是根据接收方的接收窗口设置的,但在同一时刻,发送方的发送窗口并不总是和接收方的接受窗口一样大。网络传送窗口值需要经历一定的时间滞后,并且这个时间还是不确定的。发送方还可能根据网路当时的拥塞情况适当减小自己的发送窗口尺寸。对于不按序到达的数据应如何处理,TCP并无明确规定。如果接收方把不按序到达的数据一律丢弃,那么接收窗口的管理将会比较简单,但这样对网络资源的利用不利,因为发送方会重复传送较多的数据。TCP通常对不按序到达的数据是先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程。TCP要求接收方必须有积累确认和捎带确认机制,这样可以减小传出开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送的时候把确认信息顺便捎带上。接收方不应过分推迟发送确认,否则会导致发送方不必要的超时重传,这反而浪费了网络的资源。TCP标准规定,确认推迟的时间不应超过0.5秒。若收到一连串具有最大长度的报文段,则必须每隔一个报文段就发送一个确认[RFC 1122]捎带确认实际上并不经常发生,因为大多数应用程序很少同时在两个方向上发送数据。TCP的通信是全双工通信。通信中的每一方都在发送和接收报文段。因此,每一方都有自己的发送窗口和接收窗口。在谈到这些窗口时,一定要弄清是哪一方的窗口。TCP的运输连接管理——TCP的连接建立TCP是面向连接的协议,它基于运输连接来传送TCP报文段。TCP运输连接的建立和释放是每一次面向连接的通信中必不可少的过程。TCP运输连接有以下三个阶段:建立TCP连接数据传送释放TCP连接TCP的运输连接管理就是使运输连接的建立和释放都能正常地进行。TCP的连接过程建立要解决以下三个问题:使TCP双方能够确知对方的存在;使TCP双方能够协商一些参数(如:最大窗口值、是否使用窗口扩大选项和时间戳选项以及服务质量等);使TCP双方能够对运输实体资源(如:缓存大小、连接表中的项目等)进行分配。TCP使用"三报文握手"建立连接将TCP建立连接的过程比喻为"握手"。注意:TCP的标准规定,SYN=1的报文段不能携带数据,但要消耗掉一个序号。TCP的标准规定,普通的确认报文段如果不携带数据,则 不消耗序号。TCP的运输连接管理——TCP的连接释放TCP通过"四报文挥手"来释放连接TCP服务器进程每收到一次TCP客户进程的数据,就重新设置并启动保活计时器(2小时定时)。若保活计时器定时周期未收到TCP客户进程发来的数据,则当保活计时器到时后,TCP服务器进程就向TCP客户进程发送一个探测报文段,以后则每个75秒发送一次。若发送10个探测报文段后仍无TCP客户进程的响应,TCP服务器进程就认为TCP客户进程所在主机出了故障,接着就关闭这个连接。TCP报文段的首部格式为了实现可靠传输,TCP采用了面向字节流的方式。但TCP在发送数据时,是从发送缓存中去除一部分或全部自己并给其添加一个首部使之成为TCP报文段后进行发送。一个TCP报文段由首部和数据载荷两部分构成。TCP的全部功能都体现在它首部中各字段的作用。TCP报文段的首部格式源端口:占16比特,写入源端口号,用来标识发送该TCP报文段的应用进程。目的端口号:占16比特,写入目的端口号,用来标识接收该TCP报文段的应用进程。序号:占32比特,取值范围[0,2的32次方-1],序号增加到最后一个后,下一个序号就又回到0。指出本TCP报文段数据载荷的第一个字节的序号。确认号:占32比特,取值范围[0,2的32次方-1],确认号增加到最后一个后,下一个确认号就又回到0。指出期望收到对方下一个TCP报文段的数据的第一个字节的序号,同时也是对之前收到的所有数据的确认。若确认好为n,则表明到序号n-1为止的所有数据都已正确接收,期望接收序号为n的数据。确认标志位ACK:取值为1时确认号字段才有效;取值为0时确认号字段无效。数据偏移:占4比特,并以4字节为单位。用来指出TCP报文段的数据载荷部分的起始处距离TCP报文段的起始处有多远。这个字段实际上是指出了TCP报文段的首部长度。首部固定长度为20字节,因此数据偏移字段的最小值为0101(2进制)。首部最大长度为60字节,因此数据偏移字段的最大值为1111(2进制)。保留字段:占6比特,保留为今后使用,但目前应置为0。窗口:占16比特,以字节为单位。指出发送本报文段的一方的接受窗口。窗口值作为接收方让发送方设置其发送窗口的依据。这是以接受方的接收能力来控制发送方的发送能力,称为流量控制。校验和:占16比特,检查范围包括TCP报文段的首部和数据载荷两部分。在计算校验和时,要在TCP报文段的前面加上12字节的伪首部。同步标志位SYN:在TCP连接建立时用来同步序号。终止标志位FIN:用来释放TCP连接。复位标志位RST:用来复位TCP连接。当RST=1时,表明TCP连接出现了异常,必须释放连接,然后再重新建立连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个TCP连接。推送标志位PSH:接收方的TCP收到该标志位为1的报文段会尽快上交应用进程,而不必等到接收缓存都填满后再向上交付。紧急标志位URG:取值为1时紧急指针字段有效;取值为0时紧急指针字段无效。紧急指针:占16比特,以字节为单位,用来指明紧急数据的长度。当发送方有紧急数据时,可将紧急数据插队到发送缓存的最前面,并立刻封装到一个TCP报文段中进行发送。紧急指针会指出本报文段数据载荷部分包含了多长的紧急数据,紧急数据之后是普通数据。选项(长度可变)字段最大报文段长度MSS选项:TCP报文段数据载荷部分的最大长度。窗口扩大选项:为了扩大窗口(提高吞吐率)。时间戳选项:用来计算往返时间RTT用于处理序号超范围的情况,又称为防止序号绕回PAWS。填充字段:由于选项的长度可变,因此使用填充来确保报文段首部能被4整除(因为数据偏移字段,也就是首部长度字段,是以4字节为单位的)。应用层应用层概述应用层是计算机网路体系结构的最顶层,是设计和建立计算机网络的最终目的,也是计算机网络中发展最快的部分。早起基于文本的应用(电子邮件、远程登录、文件传输、新闻组)20世纪90年代将因特网带入千家万户的万维网WWW当今流行的即时通信、P2P文件共享及各种音视频应用计算设备的小型化和"无处不在",宽带住宅接入和无线接入的日益普及和迅速发展,为未来更多的新型应用提供了广阔的舞台。客户/服务器方式(C/S方式)和对等方式(P2P方式)网络应用程序运行在处于网络边缘的不同的端系统上,通过彼此间的通信来共同完成某项任务。开发一种新的网络应用首先要考虑的问题就是网络应用程序在各种端系统上的组织方式和它们之间的关系。目前流行的主要有一下两种:客户/服务器(Client/Server,C/S)方式对等(Peer-to-Peer,P2P)方式客户/服务器方式客户和服务器是指通信中所涉及的两个应用进程。客户/服务器方式所描述的是进程之间服务和被服务的关系。客户是服务请求方,服务器是服务提供方。服务器总是处于运行状态,并等待客户的服务请求。服务器具有固定端口号(例如HTTP服务器的默认端口号为80),而运行服务器的主机也具有固定的IP地址。C/S方式是因特网上传统的、同时也是最成熟的方式,很多我们熟悉的网络应用采用的都是C/S方式。包括万维网WWW、电子邮件、文件传输FTP等。基于C/S方式的应用服务通常是服务集中型的,即应用服务集中在网络中比客户计算机少得多的服务器计算机上。由于一台服务器计算机要为多个客户机提供服务,在C/S应用中,常会出现服务器计算机跟不上众多客户机请求的情况。为此,在C/S应用中,常用计算机集群(或服务器场)构建一个强大的虚拟服务器。对等方式在P2P方式中,没有固定的服务请求者和服务提供者,分部在网络边缘的各端系统中的应用进程是对等的,被称为对等方。对等方相互之间直接通信,每个对等方既是服务的请求者,又是服务的提供者。目前,在因特网上流行的P2P应用主要包括P2P文件共享、即时通信。P2P流媒体、分布式存储等。基于P2P的应用是服务分散型的,因为服务不是集中在少数几个服务器计算机中,而是分部在大量对等计算机中,这些计算机并不为服务提供商所有,而是为个人控制的桌面计算机和笔记本电脑,它们通常位于住宅、校园和办公室中。P2P方式的最突出特性之一就是它的可扩展性。因为系统每增加一个对等方,不仅增加的是服务的请求者,同时也增加了服务的提供者,系统性能不会因规模的增大而降低。P2P方式具有成本上的优势,因为它通常不需要庞大的服务器设施和服务器带宽。为了降低成本,服务提供商对于将P2P方式用于应用的兴趣越来越大。动态主机配置协议DHCPDHCP的作用动态主机配置协议DHCP提供了一种机制,称为即插即用连网。这种机制允许一台计算机加入新网络时可自动获取IP地址等网络配置信息,而不用手工参与。DHCP的工作过程DHCP中继代理在每一个网络上都设置一个DHCP服务器会使DHCP服务器的数量太多。因此现在是使每一个网络至少有一个DHCP中继代理(通常是一台路由器),它配置了DHCP服务器的IP地址信息,作为各网络中计算机与DHCP服务器的桥梁。域名系统DNS(Domain Name System)域名系统DNS的作用因特网是否可以只使用一台DNS服务器?这种做法并不可取。因为因特网的规模很大, 这样的又名服务器很定会因为超负荷而无法正常工作,而且一旦域名服务器出现故障,整个因特网就会瘫痪。早在1983年,因特网就开始采用层次结构的命名树作为主机的名字(即域名),并使用分布式的域名系统DNS。DNS使大多数域名都在本地解析,仅少量解析需要在因特网上通信,因此系统效率很高。由于DNS是分布式系统,即使单个计算机出了故障,也不会妨碍整个系统的正常运行。因特网采用层次树状结构的域名结构域名的结构由若干个分量组成,各分量之间用"点"隔开,分别代表不同级别的域名。例如:......三级域名.二级域名.顶级域名每一级的域名都由英文字母和数字组成,不超过63个字符,不区分大小写字母。级别最低的域名写在最左边,而级别最高的顶级域名写在最右边。完整的域名不超过255个字符。域名系统既不规定一个域名需要包含多少个下级域名,也不规定每一级的域名代表什么意思。各级域名由其上一级的域名管理机构管理,而最高的顶级域名则是由因特网名称与数字地址分配机构ICANN进行管理。举例:因特网的域名空间为了提高DNS的查询效率,并减轻根域名服务器的负荷和减少因特网上的DNS查询报文数量,在域名服务器中广泛地使用了高速缓存,高速缓存用来存放最近查询过的域名以及从何处获得域名映射信息的记录。由于域名到IP地址的映射关系并不是永久不变,为保持高速缓存表中的内容正确, 域名服务器应为每项内容设置计时器并删除超过合理时间的项(例如,每个项目只存放两天)。不但在本地域名服务器中需要高速缓存,在用户主机中也很需要。许多用户主机在启动时从本地域名服务器下载域名和IP地址的全部数据库,维护存放自己最近使用的域名的高速缓存,并且只在从缓存中找不到域名时才向域名服务器查询。同理,主机也需要保持高速缓存中内容的正确性。文件传送协议FTP将某台计算机中的文件通过网络传送到可能相距很远的另一台计算机中,是一项基本的网络应用,即文件传送。文件传送协议FTP是因特网上使用最广泛的文件传送协议。FTP提供交互式的访问, 允许客户指明文件的类型与格式(如指明是否使用ASCII码),并允许文件具有存取权限(如访问文件的用户必须经过授权,并输入有效的口令)。FTP屏蔽了各计算机系统的细节,因而适用于在异构网络中任意计算机之间传送文件。在因特网发展的早期阶段,用FTP传送文件约占整个因特网的通信量的三分之一,而由电子邮件和域名系统所产生的通信量还要小于FTP所产生的通信量。只是到了1995年,万维网WWW的通信量才首次超过FTP。FTP的常见用途是在计算机之间传输文件,尤其是用于批量传输文件。FTP的另一个常见用途是让网站设计者将构成网站内容的大量文件批量上传到他们的Web服务器。FTP的基本工作原理电子邮件电子邮件系统采用客户/服务器方式;电子邮件系统的三个主要组成构建:用户代理,邮件服务器,以及电子邮件所需的协议。用户代理是用户与电子邮件系统的接口,又称为电子邮件客户端软件。邮件服务器是电子邮件系统的基础设施,因特网上所有的LSP都有邮件服务器,其功能是发送和接收邮件,同时还要负责维护用户的邮箱。协议包括邮件发送协议(例如SMTP)和邮件读取协议(例如POP3,IMAP)。简单的邮件传送协议SMTP的基本工作原理电子邮件的信息格式并不是SMTP定义的,而是在RFC 822中单独定义的。这个RFC文档已在2008年更新为RFC 5322。一个电子邮件有信封和内容两部分。而内容又由首部和主体两部分构成。 SMTP协议只能传送ASCII码文本数据,不能传送可执行文件或其他的二进制对象。SMTP不能满足传送多媒体邮件(例如带有图片、音频或视频数据)的需要。并且许多其他非英语国家的文字(例如中文、俄文、甚至带有重音符号法文或德文)也无法用SMTP传送。为了解决SMTP传送非ASCII码文本的问题,提出了多用途因特网邮件扩展MIME。增加了5个新的邮件首部字段,这些字段提供了有关邮件主体的信息。定义了许多邮件内容的格式,对多媒体电子邮件的表示方法进行了标准化。定义了传送编码,可对任何内容格式进行转换,而不会被邮件系统改变。实际上,MIME不仅仅用于SMTP,也用于后来的同样面向ASCII字符的HTTP。常用的邮件读取协议有以下两个:邮局协议POP,POP3是其第三个版本,是因特网正式标准。POP3非常简单、功能有限的有限读取协议。用户只能以下载并删除方式或下载并保留方式从邮件服务器下载邮件到用户方计算机。不允许用户在邮件服务器上管理自己的邮件。(例如创建文件夹,对邮件进行分类管理等)。因特网邮件访问协议IMAP,IMAP4是其第四个版本,目前还只是因特网建议标准。IMAP4功能比POP3强大的邮件读取协议。用户在自己的计算机上就可以操控邮件服务器中的邮件,就像在本地操控一样,因此IMAP是一个联机协议。POP3和IMAP4都采用基于TCP连接的客户/服务器方式。POP3使用熟知端口110,IMAP4使用熟知端口143。基于万维网的电子邮件通过浏览器登录(提供用户名和口令)邮件服务器万维网网站就可以撰写、收发。阅读和管理电子邮件。这种工作模式与IMAP很类似,不同的是用户计算机无需安装专门的用户代理程序,只需要使用通用的万维网浏览器。邮件服务器网站通常都提供非常强大和方便的邮件管理功能,用户可以在邮件服务器网站上管理和处理自己的邮件,而不需要将邮件下载到本地进行管理。这种工作模式在用户浏览器与邮件服务器之间使用HTTP协议,而邮件服务器之间使用SMTP协议。万维网WWW浏览器最重要的部分是渲染引擎,就是浏览器内核。负责对网页内容进行解析和显示。不同的浏览器内核对网页内容的解析也有不同,因此同一网页在不同内核的浏览器里显示的效果可能不同;网页编写者需要在不同内核的浏览器中测试网页显示效果 。为了方便地访问在世界范围的文档,万维网使用同一资源定位符URL来指明因特网上任何种类"资源"的位置。URL的一般形式由以下四个部分组成:超文本传输协议HTTPHTTP定义了浏览器(即万维网客户进程)怎样向万维网服务器请求万维网文档,以及万维网服务器怎样把万维网文档传送给浏览器。HTTP/1.0采用非持续连接方式。在该方式下,每次浏览器要请求一个文件都要与服务器建立TCP连接,当收到相应后就立即关闭连接。每请求一个文档就要两倍的RTT的开销。若一个网页上有多个引用对象(例如图片等),那么请求每个对象都需要花费2RTT的时间。为了减小时延,浏览器通常会建立多个并行的TCP连接同时请求多个对象。但是,这会大量占用万维网服务器的资源,特别是万维网服务器往往要同时服务于大量客户的请求,这会使其负担很重。HTTP/1.1采用持续连接方式。在该方式下,万维网服务器在发送响应后仍然保持这条连接,使同一客户(浏览器)和该服务器可以继续在这条连接上传送后续的HTTP请求报文和响应报文。这并不局限于传送同一个页面上引用的对象,而是只要这些文档都在同一个服务器上就行。为了进一步提高效率,HTTP/1.1的持续连接还可以使用流水线方式工作,即浏览器在收到HTTP的响应报文之前就能够连续发送多个请求报文。这样的一个接一个的请求报文到达服务器后,服务器就发回一个接一个的响应报文。这样就节省了很多个RTT时间,使TCP连接中的空闲时间减少,提高了下载文档的效率。HTTP的报文格式HTTP是面向文本的,其报文中的每一个字段都是一些ASCII码串,并且每个字段的长度都是不确定的。HTTP请求报文支持以下方法HTTP相应报文格式相应报文中常见的状态行使用Cookie在服务器上记录用户信息万维网缓存与代理服务器
【PicGo+OSS】解决报错The request signature we calculated does not match the signature you provided.Check..