精华内容
下载资源
问答
  • 目录 eventfd 介绍 接口 提供的方法 _ _thread 右值引用与move 用法: std::move 的函数原型定义 emplace_back SIGPIPE 限制服务器的最大并发连接数 为什么要限制并发连接数? 优雅关闭连接 ......

    目录

    eventfd 介绍

    接口

    提供的方法

    _ _thread

    右值引用与move

    用法:

    std::move 的函数原型定义

    emplace_back

    SIGPIPE

    限制服务器的最大并发连接数

    为什么要限制并发连接数?

    优雅关闭连接

    关闭tcp nagle算法

    将线程锁封装为一个类

    C++11 中std::function和std::bind的用法

    线程的栈

    这里特别说一下pthread_join的返回值:

    虚表是属于类的还是属于对象的

    多线程编程之锁和条件变量

    互斥锁mutex:

    条件变量

    noncopyable类

    epoll_create1(int flag)

    pthread_once

    线程安全的单例模式

    enable_shared_from_this

    手工处理HTTP请求头:

    日志模块:

    epoll源码:

    stat

    sprintf和read、write


    这篇博客是我在学习muduo时,参考他动手写多线程服务器端模型的笔记,果然自己多动手写代码才能学到更多东西呀。。

    eventfd 介绍

    https://cloud.tencent.com/developer/article/1160842

    Linux 2.6.27后添加了一个新的特性,就是eventfd,是用来实现多进程或多线程的之间的事件通知的。

    eventfd是一个用来通知事件的文件描述符,timerfd是的定时器事件的文件描述符。二者都是内核向用户空间的应用发送通知的机制,可以有效地被用来实现用户空间的事件/通知驱动的应用程序。

    (对于timerfd,还有精准度和实现复杂度的巨大差异。由内核管理的timerfd底层是内核中的hrtimer(高精度时钟定时器),可以精确至纳秒(1e-9秒)级,完全胜任实时任务。而用户态要想实现一个传统的定时器,通常是基于优先队列/二叉堆,不仅实现复杂维护成本高,而且运行时效率低,通常只能到达毫秒级。)

    接口

    #include <sys/eventfd.h>
    int eventfd(unsigned int initval, int flags);

    这个函数会创建一个事件对象(eventfd object),返回一个文件描述符,用来实现进程或线程间的等待/通知(wait/notify)机制。内核为这个对象维护了一个无符号的64位整形计数器 counter,用第一个参数(initval)初始化这个计数器,创建时一般可将其设为0,后面有例子测试这个参数产生的效果。

    flags 可以使用三个宏:

    EFD_CLOEXEC:给这个新的文件描述符设置 FD_CLOEXEC 标志,即 close-on-exec,这样在调用 exec 后会自动关闭文件描述符。因为通常执行另一个程序后,会用全新的程序替换子进程的正文,数据,堆和栈等,原来的文件描述符变量也不存在了,这样就没法关闭没用的文件描述符了。
    EFD_NONBLOCK:设置文件描述符为非阻塞的,设置了这个标志后,如果没有数据可读,就返回一个 EAGAIN 错误,不会一直阻塞。
    EFD_SEMAPHORE:这个标志位会影响read操作,具体可以看read方法中的解释。

     

    提供的方法

    从上面可以看出来,eventfd 支持三种操作:read、write、close。

    read 返回值的情况如下:

    1. 读取 8 字节值,如果当前 counter > 0,那么返回 counter 值,并重置 counter 为 0。(设置了EFD_SEMAPHORE标志位,则返回1,且计数器中的值也减去1。没有设置EFD_SEMAPHORE标志位,则返回计数器中的值,且计数器置0。)
    2. 如果调用 read 时 counter 为 0,那么 1)阻塞直到 counter 大于 0;2)非阻塞,直接返回 -1,并设置 errno 为 EAGAIN。如果 buffer 的长度小于 8 字节,那么 read 会失败,并设置 errno 为 EINVAL。

            可以看出来 eventfd 只允许一次 read,对应两种状态:0和非0。下面看下 write。
    write :

    1. 写入一个 64 bit(8字节)的整数 value 到 eventfd。
    2. 返回值:counter 最大能存储的值是 0xffff ffff ffff fffe,write 尝试将 value 加到 counter 上,如果结果超过了 max,那么 write 一直阻塞直到 read 操作发生,或者返回 -1 并设置 errno 为 EAGAIN。

          可多次 write,一次 read。close 就是关掉 fd。

    以上大概就是我了解的 eventfd,它相比于 pipe来说,少用了一个文件描述符,而且不必管理缓冲区单纯的事件通知的话,方便很多(它的名字就叫做 eventfd),它可以和事件通知机制很好的融合。
     

     

     

    看个线程间唤醒的例子:

    /*
     * @filename:    eventfd_pthread.c
     * @author:      Tanswer
     * @date:        2018年01月08日 22:46:38
     * @description:
     */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/eventfd.h>
    #include <pthread.h>
    #include <unistd.h>
    
    int efd;
    
    void *threadFunc()
    {
        uint64_t buffer;
        int rc;
        while(1){
            rc = read(efd, &buffer, sizeof(buffer));        //一次就会将多次对他写入的所有值的和读出来,类似管道
    
            if(rc == 8){                    //起到一个唤醒线程的作用
                printf("notify success\n");
            }
    
            printf("rc = %llu, buffer = %lu\n",(unsigned long long)rc, buffer);
        }//end while
    }
    
    int main()
    {
        pthread_t tid;
        int rc;
        uint64_t buf = 1;
    
        efd = eventfd(0,0);     // blocking
        if(efd == -1){
            perror("eventfd");
        }
    
        //create thread
        if(pthread_create(&tid, NULL, threadFunc, NULL) < 0){
            perror("pthread_create");
        }
    
        while(1){
            rc = write(efd, &buf, sizeof(buf));
    
            if(rc != 8){
                perror("write");
            }
            sleep(2);
        }//end while
        close(efd);
        return 0;
    }

    我们程序中具体使用时,是让eventloop线程阻塞在epoll上,当有事件来了,就写eventfd唤醒他(用channel)

     

    这里有一篇文章讲得比较细:

    http://blog.chinaunix.net/uid-25929161-id-3781524.html

    struct eventfd_ctx {
        struct kref kref;   /* 这个就不多说了,file计数用的,用于get/put */
        wait_queue_head_t wqh; /* 这个用来存放用户态的进程wait项,有了它通知机制才成为可能 */
    /*
    * Every time that a write(2) is performed on an eventfd, the
    * value of the __u64 being written is added to "count" and a
    * wakeup is performed on "wqh". A read(2) will return the "count"
    * value to userspace, and will reset "count" to zero. The kernel
    * side eventfd_signal() also, adds to the "count" counter and
    * issue a wakeup.
    */
        __u64 count;  /* 这个就是一个技术器,应用程序可以自己看着办,read就是取出然后清空,write就是把value加上 */
        unsigned int flags;  /* 所有的file都有的吧,用来存放阻塞/非阻塞标识或是O_CLOEXEC之类的东西 */
    };
    
    
     // This function is supposed to be called by the kernel in paths that do not
     // allow sleeping. In this function we allow the counter to reach the ULLONG_MAX
     // value, and we signal this as overflow condition by returining a POLLERR  to poll(2).
    int eventfd_signal(struct eventfd_ctx *ctx, int n)    //本质上是做一个唤醒
    {
        unsigned long flags;
    
        if (n < 0)
            return -EINVAL;
        spin_lock_irqsave(&ctx->wqh.lock, flags);
        if (ULLONG_MAX - ctx->count < n)
            n = (int) (ULLONG_MAX - ctx->count);
        ctx->count += n;
        if (waitqueue_active(&ctx->wqh))
            wake_up_locked_poll(&ctx->wqh, POLLIN);
        spin_unlock_irqrestore(&ctx->wqh.lock, flags);
    
        return n;
    }

         当内核态想通知用户态时,直接使用eventfd_signal,此时用户态线程需要先把自己放在eventfd_ctx->wqh上,有两种方案,一个是调用read,一个是调用poll。 如果是read,之后会将eventfd_ctx->count清零,下次还能阻塞住。但是如果使用poll,之后count并未清零,导致再次poll时,即使内核态没有eventfd_signal,poll也会即时返回。

    具体实现原理详解见腾讯云社区https://cloud.tencent.com/developer/article/1160842

     

    _ _thread

    是GCC内置的线程局部存储设施,存取效率可以和全局变量相比。_ _thread变量每一个线程有一份独立实体,各个线程的值互不干扰。可以用来修饰那些带有全局性且值可能变,但是又不值得用全局变量保护的变量。通过 _ _thread 修饰的变量,在线程中地址都不一样,__thread变量每一个线程有一份独立实体,各个线程的值互不干扰。

    __thread EventLoop* t_loopInThisThread = 0;
    
    
    if (t_loopInThisThread) {
    	//LOG << "Another EventLoop " << t_loopInThisThread << " exists in this thread " << threadId_;
    }
    else {
    	t_loopInThisThread = this;
    	}

    用来维护每个线程中存在的唯一EventLoop。

     

     

    右值引用与move

    原文:https://blog.csdn.net/p942005405/article/details/84644069 

    在C++11中,标准库在<utility>中提供了一个有用的函数std::move,std::move并不能移动任何东西,它唯一的功能是将一个左值强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义。从实现上讲,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue);

    1. C++ 标准库使用比如vector::push_back 等这类函数时,会对参数的对象进行复制,连数据也会复制.这就会造成对象内存的额外创建, 本来原意是想把参数push_back进去就行了,通过std::move,可以避免不必要的拷贝操作。
    2. std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝所以可以提高利用效率,改善性能.。
    3. 对指针类型的标准库对象并不需要这么做.

    用法:

    原lvalue值被moved from之后值被转移,所以为空字符串. 

    //摘自https://zh.cppreference.com/w/cpp/utility/move
    #include <iostream>
    #include <utility>
    #include <vector>
    #include <string>
    int main()
    {
        std::string str = "Hello";
        std::vector<std::string> v;
        //调用常规的拷贝构造函数,新建字符数组,拷贝数据
        v.push_back(str);
        std::cout << "After copy, str is \"" << str << "\"\n";
        //调用移动构造函数,掏空str,掏空后,最好不要使用str
        v.push_back(std::move(str));
        std::cout << "After move, str is \"" << str << "\"\n";
        std::cout << "The contents of the vector are \"" << v[0]
                                             << "\", \"" << v[1] << "\"\n";
    }
    

    输出:

    After copy, str is "Hello"
    After move, str is ""
    The contents of the vector are "Hello", "Hello"
    

    std::move 的函数原型定义

    template <typename T>
    typename remove_reference<T>::type&& move(T&& t)
    {
    	return static_cast<typename remove_reference<T>::type&&>(t);
    

    还有一些详细说明见原文。

     

    emplace_back

    https://blog.csdn.net/p942005405/article/details/84764104

    c++开发中我们会经常用到插入操作对stl的各种容器进行操作,比如vector,map,set等。在引入右值引用,转移构造函数,转移复制运算符之前,通常使用push_back()向容器中加入一个右值元素(临时对象)时,首先会调用构造函数构造这个临时对象,然后需要调用拷贝构造函数将这个临时对象放入容器中。原来的临时变量释放。这样造成的问题就是临时变量申请资源的浪费。 
    引入了右值引用,转移构造函数后,push_back()右值时就会调用构造函数和转移构造函数,如果可以在插入的时候直接构造,就只需要构造一次即可。这就是c++11 新加的emplace_back。

    emplace_back函数原型:

    template <class... Args>
    
      void emplace_back (Args&&... args);

    在容器尾部添加一个元素,这个元素原地构造,不需要触发拷贝构造和转移构造。而且调用形式更加简洁,直接根据参数初始化临时对象的成员。
    一个很有用的例子:

    #include <vector>  
    #include <string>  
    #include <iostream>  
     
    struct President  
    {  
        std::string name;  
        std::string country;  
        int year;  
     
        President(std::string p_name, std::string p_country, int p_year)  
            : name(std::move(p_name)), country(std::move(p_country)), year(p_year)  
        {  
            std::cout << "I am being constructed.\n";  
        }
        President(const President& other)
            : name(std::move(other.name)), country(std::move(other.country)), year(other.year)
        {
            std::cout << "I am being copy constructed.\n";
        }
        President(President&& other)  
            : name(std::move(other.name)), country(std::move(other.country)), year(other.year)  
        {  
            std::cout << "I am being moved.\n";  
        }  
        President& operator=(const President& other);  
    };  
     
    int main()  
    {  
        std::vector<President> elections;  
        std::cout << "emplace_back:\n";  
        elections.emplace_back("Nelson Mandela", "South Africa", 1994); //没有类的创建  
     
        std::vector<President> reElections;  
        std::cout << "\npush_back:\n";  
        reElections.push_back(President("Franklin Delano Roosevelt", "the USA", 1936));  
     
        std::cout << "\nContents:\n";  
        for (President const& president: elections) {  
           std::cout << president.name << " was elected president of "  
                << president.country << " in " << president.year << ".\n";  
        }  
        for (President const& president: reElections) {  
            std::cout << president.name << " was re-elected president of "  
                << president.country << " in " << president.year << ".\n";  
        }
     
    }
    

    输出:

    emplace_back:
    I am being constructed.
     
    push_back:
    I am being constructed.
    I am being moved.
     
    Contents:
    Nelson Mandela was elected president of South Africa in 1994.
    

    可以看到,emplace_back只调用了一次构造函数,而push_back先调用构造函数创建一个临时变量,再调用move复制到容器中,最后还要析构临时变量。

    在这里不得不说说移动构造函数和拷贝构造函数的区别:

    https://blog.csdn.net/sinat_25394043/article/details/78728504

    移动构造是C++11标准中提供的一种新的构造方法。

    在现实中有很多这样的例子,我们将钱从一个账号转移到另一个账号,将手机SIM卡转移到另一台手机,将文件从一个位置剪切到另一个位置……

        移动构造可以减少不必要的复制,带来性能上的提升。

    有些复制构造是必要的,我们确实需要另外一个副本;而有些复制构造是不必要的,我们可能只是希望这个对象换个地方,移动一下而已。

    在C++11之前,如果要将源对象的状态转移到目标对象只能通过复制。

    而现在在某些情况下,我们没有必要复制对象——只需要移动它们。

    C++11引入移动语义: 源对象资源的控制权全部交给目标对象

    对比一下复制构造和移动构造:

    复制构造是这样的:

    在对象被复制后临时对象和复制构造的对象各自占有不同的同样大小的堆内存,就是一个副本。

    移动构造是这样的:

    就是让这个临时对象它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。

    复制构造和移动构造的差别:

        这种情况下,我们觉得这个临时对象完成了复制构造后,就不需要它了,我们就没有必要去首先产生一个副本,然后析构这个临时对象,这样费两遍事,又占用内存空间,所幸将临时对象它的原本的资源直接转给构造的对象即可了。

        当临时对象在被复制后,就不再被利用了。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制构造。

    什么时候该触发移动构造呢?

        如果临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候我们就可以触发移动构造。

     

    移动构造是需要通过移动构造函数来完成的。

    移动构造函数定义形式:

    class_name(class_name && )

    例:函数返回含有指针成员的对象

    有两种版本:

    版本一:使用深层复制构造函数

        ~ 返回时构造临时对象,动态分配临时对象返回到主调函数,然后删除临时对象。

    版本二:使用移动构造函数

        ~ 将要返回的局部对象转移到主调函数,省去了构造和删除临时对象的过过程。

    实例程序:

    https://blog.csdn.net/carbon06/article/details/81222759

    #include <vector>
    #include <cstring>
    #include <iostream>
    #include<string>
    class A
    {
    public:
    	A(const int size) : size(size)
    	{
    		if (size)
    		{
    			data = new char[size];
    		}
    		std::cout << "I'm constructor.\n";
    	}
    
    	A(const A& other)
    	{
    		size = other.size;
    		data = new char[size];
    		memcpy(data, other.data, size * sizeof(char));
    		std::cout << "I'm copy constructor.\n";
    	}
    
    	A(A&& other)
    	{
    		size = other.size;
    		data = other.data;
    
    		other.size = 0;
    		other.data = nullptr;
    		std::cout << "I'm move constructor.\n";
    	}
    
    private:
    	int size;
    	char* data = nullptr;
    };
    
    int main()
    {
    	std::vector<A> vec;
    	vec.reserve(1024);
    
    	A tmp(5);
    	std::cout << "push_back a left value.\n";
    	vec.push_back(tmp);
    
    	std::cout << "push_back a right value with std::move.\n";
    	vec.push_back(std::move(tmp));
    
    	std::cout << "emplace_back a left value.\n";
    	vec.emplace_back(tmp);
    
    	std::cout << "emplace_back a right value with std::move.\n";
    	vec.emplace_back(std::move(tmp));
    
    	std::cout << "emplace_back in place.\n";
    	vec.emplace_back(5);
    
    	std::cout << "=========================================\n";
    	std::cout << "test with buildin string move and emplace_back\n";
    	std::cout << "=========================================\n";
    
    	std::vector<std::string> str_vec;
    	str_vec.reserve(1024);
    
    	std::string str = "I'd like to be inserted to a container";
    
    	std::cout << "before emplace_back to vec, str is:\n";
    	std::cout << str << std::endl;
    	std::cout << "c_str address is " << (void*)str.c_str() << std::endl;
    
    	str_vec.emplace_back(std::move(str));
    
    	std::cout << "after emplace_back to vec, str is:\n";
    	std::cout << str << std::endl;
    	std::cout << "c_str address is " << (void*)str.c_str() << std::endl;
    	std::cout << "c_str address of the string in container is "
    		<< (void*)str_vec.front().c_str() << std::endl;
    	system("pause");
    	return 0;
    }

    输出结果:

    从执行结果中,我们可以得出以下结论 
    1. push_back 可以接收左值也可以接受右值,接收左值时使用拷贝构造,接收右值时使用移动构造 
    2. emplace_back 接收右值时调用类的移动构造 
    3. emplace_back 接收左值时,实际上的执行效果是先对传入的参数进行拷贝构造,然后使用拷贝构造后的副本,也就是说,emplace_back在接收一个左值的时候其效果和push_back一致!所以在使用emplace_back 时需要确保传入的参数是一个右值引用,如果不是,请使用std::move()进行转换 
    4. emplace_back 接收多个参数时,可以调用匹配的构造函数实现在容器内的原地构造 
    5. 使用string 类验证了移动构造函数式对类成员所有权的传递,从上图中看到string 在插入前c_str的地址和使用emplace_back 移动到容器后的c_str的地址一致。并且移动后字符串c_str 的地址指向其他位置。
     

     

    SIGPIPE

    SIGPIPE的默认行为是终止进程,在命令行程序中这是合理的,但是在网络编程中,这意味着如果对方断开连接而本地继续写入的话,会造成服务进程意外退出(先收到RST再收到SIGPIPE)。

    假如服务器进程繁忙,没有及时处理对方断开连接的事件,就有可能出现在连接断开之后继续发送数据的情况。

    解决的办法很简单,在程序开始时就忽略SIFPIPE即可

    具体是在创建服务器对象

    Server myHTTPServer(&mainLoop, threadNum, port);

    时,在其构造函数中调用 handle_for_sigpipe();函数:

    void handle_for_sigpipe() {
    	struct sigaction sa;
    	memset(&sa, '\0', sizeof(sa));
    	sa.sa_handler = SIG_IGN;
    	sa.sa_flags = 0;
    	if (sigaction(SIGPIPE, &sa, NULL))
    		return;
    }

     

    限制服务器的最大并发连接数

    我们这里讨论的“并发连接数”是指一个服务端程序能同时支持的客户端连接数,连接由客户端主动发起,服务端被动接受连接(accept).

    为什么要限制并发连接数?

    一方面,我们不希望服务程序超载,另一方面更因为file descriptor是稀缺资源,如果出现fd耗尽的情况,比较棘手。就跟“malloc()失败抛出bad_alloc差不多棘手”。

    假如有这样一种场景:

    我们创建了一个listenfd挂在epoll上(水平触发形式),当调用epoll_wait获得新连接事件时,用accept去处理listenfd上的事件,但是此时本进程的文件描述符满了,accpet返回EMFILE,无法为新连接创建socket文件描述符。 但是,既然没有socket描述符来表示这个连接,我们也没法close他,程序就继续运行。当再一次epoll_wait时会立即返回(因为新连接还等待处理,listenfd还是可读的),这样的话程序就会陷入busy loop,CPU占用率接近100%。这既影响了同一event loop上的连接,也影响了同一机器上的其他服务。

    该如何解决呢?书上写了几种做法:

    1.调高进程的文件描述符数目(治标不治本)

    2.死等(鸵鸟算法)

    3.退出程序(小题大作)

    4.关闭listenfd(但是什么时候重新打开呢?)

    5.改用edge trigger(如果漏掉了一次accpet,程序就再也不会受到新连接    why?)

    6.准备一个空的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个fd名额,再accept拿到新的socket fd(可读写的),随后理科close他,这样就优雅地断开了客户端的连接,最后再重新打开一个空闲文件,把“坑”占住,以备再次出现这种情况时使用。

    另外还有一种比较简单的做法,fd是hard limit,我们可以自己设置一个稍低一点的soft limit,如果超过 soft limit就主动关闭新连接。比方说当前进程的max fd 是1024,我们可以在连接数达到1000时就进入“拒绝新连接”的状态,这样就可以留给我们足够的腾挪空间。

     

    tcp半关闭

    在TCP服务端和客户端建立连接之后服务端和客户端会分别有两个独立的输入流和输出流,而且相互对应。服务端的输出流对应于客户端的输入流,服务端的输入流对应于客户端的输出流。这是在建立连接之后的状态。

      当我们调用close()函数时,系统会同时把双方的输入流和输出流全部关闭,但是有时候我们仍需要在一方断开连接之后只进行接受数据或者传输数据其中一项操作。这时就需要我们只断开输入或者输出,保留另一个流的正常运转,也就引入了TCP的半关闭状态。

    基本操作:

    之前我们传输完数据之后便直接调用了close()函数,我们可以使用系统提供的shutdown()函数方便的完成TCP的半关闭。

    shutdown(int socket , int type):半关闭套接字中的输入或者输出流

    • socket(套接字描述符):需要断开的套接字描述符
    • type(断开类型):套接字的断开方式

      SHUT_RD——断开输入流,并清空输入缓冲中的数据

      SHUT_WR——断开输出流,并将输出缓冲中的数据输出

      SHUT_RDWR——同时断开输入输出流,分两次调用shutdown()函数

      成功时返回0,失败时返回-1

    例子:

      /**
        在数据输出完成之后,对输出流进行流半关闭
        这种状态下服务读不能向客户端写入数据,但是可以接受来自客户端的数据
        **/
        shutdown(client_sock,SHUT_WR);
        //接受来自客户端的消息
        while(0 == read(client_sock,buff,BUFF_SIZE))
        {
            continue;
        }
    
    //最后完全关闭
        close(client_sock);

     

    这里提一句RST

    导致“Connection reset”的原因是服务器端因为某种原因关闭了Connection,而客户端依然在读写数据,此时服务器会返回复位标志“RST”,然后此时客户端就会提示“Connection reset”。

    可能有同学对复位标志“RST”还不太了解,这里简单解释一下:

      TCP建立连接时需要三次握手,在释放连接需要四次挥手;例如三次握手的过程如下:

        第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SENT状态,等待服务器确认;

        第二次握手:服务器收到syn包,并会确认客户的SYN(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;

        第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。

      可以看到握手时会在客户端和服务器之间传递一些TCP头信息,比如ACK标志、SYN标志以及挥手时的FIN标志等。

      除了以上这些常见的标志头信息,还有另外一些标志头信息,比如推标志PSH、复位标志RST等。其中复位标志RST的作用就是“复位相应的TCP连接”。

    莫要神话RST,其实他就是tcp报文段首部里面的一个标志位而已,共有如下几个:

    URG,ACK,PSH,RST,SYN,FIN

     

     

    还有一种比较常见的错误“Connection reset by peer”,该错误和“Connection reset”是有区别的:

    服务器返回了“RST”时,如果此时客户端正在从Socket套接字的输出流中读数据则会提示Connection reset”;

    服务器返回了“RST”时,如果此时客户端正在往Socket套接字的输入流中写数据则会提示“Connection reset by peer”。

     

     

    有几种情况会收到RST:

    1.

        客户端、服务器端TCP连接一切正常,TCP连接由于没有数据传输而出于空闲(Idle)状态。突然服务器掉电,当服务器重新启动完毕,与客户端的TCP连接状态由于掉电而完全消失。之后,客户端发给服务器任何消息,都会触发服务器发RST作为回应。

    服务器之所以发RST,是因为连接不存在,通过Reset状态位,间接告诉客户端异常情况的存在。

    a. Reset顺利到达客户端

    客户端意识到异常发生了,会立马释放该TCP连接所占用的内存资源(状态、数据)、以及端口号。客户端TCP会通知数据的主人(应用程序),由于TCP连接被对方Reset,数据发送失败。客户端无需超时等待,立即使用原有端口号,重新发起一个TCP连接,双方状态再一次同步,进入正常通信状态。

    b.

    Reset没有到达客户端

    客户端的状态依然为“established”状态,反正双方的状态已经不同步了,如果客户端有数据、或keepalive要发送,会继续触发服务器发送Reset。这种情况是由于外界因素影响,使得双方状态不同步,一方为“established”,另一方为“closed”, 重置(Reset)连接状态是最好的方法!有读者会问,如果客户端一直没有消息发给服务器端,那双方状态的不同步是否会保持到天长地久?

    会的!

    但是考虑到当前的网络状况,这种可能性是比较小的,因为目前的网络NAT无处不在,为了克服NAT表项没有流量刷新而删除NAT表项,进而影响客户端、服务器端通信,如今的TCP实现会在几十秒发送一次Keepalive,这样即使没有用户流量,Keepalive也会刷新NAT表项,从而避免NAT设备删表操作。所以双方通信不同步状态,在当今的TCP实现上会很快监测到、并予以纠正。

    2.

        当客户端发起一个TCP连接请求,途径公司防火墙时,防火墙查询自己的安全策略,这是一个不被允许的连接请求,于是防火墙以服务器IP的名义,返还给用户一个Reset状态位,用户以为是服务器发的,其实服务器压根不知道,是防火墙作为中间人发的。

     

    优雅关闭连接

    TCP连接断开的时候调用close socket函数,已经讨论过有优雅的断开和强制断开,那么如何设置断开连接的方式呢?是通过设置socket描述符一个linger结构体属性。

    linger结构体数据结构如下:

    struct linger

    {
         int l_onoff;// 表示是否立即关闭连接,0表示不立即关闭,即优雅方式;非零表示立即关闭连接,即强制关闭

         int l_linger;//表示优雅方式关闭连接的等待时间

    };

    有三种组合方式:

    第一种

        l_onoff = 0;

        l_linger忽略

        这种方式下,就是在closesocket的时候立刻返回,底层会将未发送完的数据发送完成后再释放资源,也就

    是优雅的退出。但是这里有一个副作用就是socket的底层资源会被保留直到TCP连接关闭,这个时间用户应用程序是无法控制的。


    第二种

        l_onoff非零

        l_linger = 0;

        这种方式下,在调用closesocket的时候同样会立刻返回,但不会发送未发送完成的数据,而是通过一个RST包强制的关闭socket描述符,也就是强制的退出。


    第三种

        l_onoff非零

        l_linger > 0

        这种方式下,在调用closesocket的时候不会立刻返回,内核会延迟一段时间,这个时间就由l_linger的值来决定。如果超时时间到达之前,发送完未发送的数据(包括FIN包)并得到另一端的确认,closesocket会返回正确,socket描述符优雅性退出。否则,closesocket会直接返回错误值,未发送数据丢失,socket描述符被强制性退出。需要注意的是,如果socket描述符被设置为非堵塞型,则closesocket会直接返回。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK(EAGAIN)错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。

    程序中:

    void setSocketNoLinger(int fd) 
    {
        struct linger linger_;
        linger_.l_onoff = 1;
        linger_.l_linger = 30;
        setsockopt(fd, SOL_SOCKET, SO_LINGER,(const char *) &linger_, sizeof(linger_));
    }

    注意SO_LINGER和SO_DONTLINGER选项只影响closesocket的行为,而与shutdown函数无关,shutdown总是会立即返回的。

     

    关闭tcp nagle算法

        Nagle算法是以他的发明人John Nagle的名字命名的,它用于自动连接许多的小缓冲器消息;这一过程(称为nagling)通过减少必须发送包的个数来增加网络软件系统的效率。Nagle算法于1984年定义为福特航空和通信公司IP/TCP拥塞控制方法,这是福特经营的最早的专用TCP/IP 网络减少拥塞控制,从那以后这一方法得到了广泛应用。Nagle的文档里定义了处理他所谓的小包问题的方法,这种问题指的是应用程序一次产生一字节数据, 这样会导致网络由于太多的包而过载(一个常见的情况是发送端的"愚蠢窗口综合症")。从键盘输入的一个字符,占用一个字节,可能在传输上造成41字节的 包,其中包括1字节的有用信息和40字节的标题数据。这种情况转变成了4000%的消耗,这样的情况对于轻负载的网络来说还是可以接受的,但是重负载的福 特网络就受不了了,它没有必要在经过节点和网关的时候重发,导致包丢失和妨碍传输速度。吞吐量可能会妨碍甚至在一定程度上会导致连接失败。Nagle的算 法通常会在TCP程 序里添加两行代码,在未确认数据发送的时候让发送器把数据送到缓存里。任何数据随后继续直到得到明显的数据确认或者直到攒到了一定数量的数据了再发包。尽管Nagle的算法解决的问题只是局限于福特网络,然而同样的问题也可能出现在ARPANet。这种方法在包括因特网在内的整个网络里得到了推广,成为了 默认的执行方式,尽管在高互动环境下有些时候是不必要的,例如在客户/服务器情形下。在这种情况下,nagling可以通过使用TCP_NODELAY 插座选项关闭。 

     1. nagle算法主要目的是减少网络流量,当你发送的数据包太小时,TCP并不立即发送该数据包,而是缓存起来直到数据包
       到达一定大小后才发送。

     2. 当应用程序每次发送的数据很小,特别是只发送1个字节,加上TCP和IP头的封装,TCP头占20个字节,IP头也占20个字      节,这时候发一个包是41个字节,效率太低。而nagle算法允许计算机缓冲数据,当数据缓存到一定长度后,如果之前发送     的数据得到了ACK确认且接收方有足够空间容纳数据 (当然也要考虑MSS),就发送这些数据,否则继续等待。

     3.  TCP socket提供了关闭nagle算法的接口,可以通过TCP_NODELAY选项决定是否开启该算法。

    程序中:

    void setSocketNodelay(int fd) {
    	int enable = 1;
    	setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (void*)&enable, sizeof(enable));
    }

     

    开启端口复用:

        // 消除bind时"Address already in use"错误
        int optval = 1;
        if(setsockopt(listen_fd, SOL_SOCKET,  SO_REUSEADDR, &optval, sizeof(optval)) == -1)
            return -1;

    INADDR_ANY:

    INADDR_ANY就是指定地址为0.0.0.0的地址,这个地址事实上表示不确定地址,或“所有地址”、“任意地址”。 一般来说,在各个系统中均定义成为0值。

    一般情况下,如果你要建立网络服务器应用程序,则你要通知服务器操作系统:请在某地址 xxx.xxx.xxx.xxx上的某端口 yyyy上进行侦听,并且把侦听到的数据包发送给我。这个过程,你是通过bind()系统调用完成的。——也就是说,你的程序要绑定服务器的某地址,或者说:把服务器的某地址上的某端口占为已用。服务器操作系统可以给你这个指定的地址,也可以不给你。

    如果你的服务器有多个网卡(每个网卡上有不同的IP地址),而你的服务(不管是在udp端口上侦听,还是在tcp端口上侦听),出于某种原因:可能是你的服务器操作系统可能随时增减IP地址,也有可能是为了省去确定服务器上有什么网络端口(网卡)的麻烦 —— 可以要在调用bind()的时候,告诉操作系统:“我需要在 yyyy 端口上侦听,所有发送到服务器的这个端口,不管是哪个网卡/哪个IP地址接收到的数据,都是我处理的。”这时候,服务器程序则在0.0.0.0这个地址上进行侦听。

    	// 设置服务器IP和Port,和监听描述符绑定
    	struct sockaddr_in server_addr;
    	bzero((char*)&server_addr, sizeof(server_addr));
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    	server_addr.sin_port = htons((unsigned short)port);
    	if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
    		return -1;

     

    将线程锁封装为一个类

    class MutexLockGuard : noncopyable
    {
    public:
    	explicit MutexLockGuard(MutexLock &_mutex) :
    		mutex(_mutex)
    	{
    		mutex.lock();
    	}
    	~MutexLockGuard()
    	{
    		mutex.unlock();
    	}
    private:
    	MutexLock &mutex;
    };

    之前一直不理解muduo为啥要这样做。。。还有函数中为啥还能有第二个{}对

    今天在写这个成员函数时突然想通了,不知道理解得对不对:

    EventLoop* EventLoopThread::startLoop() {
    	assert(!thread_.started());
    	thread_.start();
    
    	{
    		MutexLockGuard lock(mutex_);		//将锁搞成一个类,在函数段内创建临时锁对象并加锁,当退出函数段时自动调用对象析构函数进行解锁
    		// 一直等到threadFun在Thread里真正跑起来
    		while (loop_ == NULL)
    			cond_.wait();		//等在条件变量上
    	}
    	return loop_;
    }

    函数中的第二对{}规定了一个小的生命周期,在此{}开始处创建临时锁对象lock并加锁,当退出此{}时,会析构这个临时锁对象,并在他的析构函数中去调用mutex.unlock() 进行解锁。666,我之前还一直奇怪只看到加锁没看到解锁呀。。。

     

    explicit       ->    C++提供了关键字explicit,可以阻止不应该允许的经过转换构造函数进行的隐式转换的发生。声明为explicit的构造函数不能在隐式转换中使用。

    例:

    class Test1
    {
    public:
        Test1(int n)
        {
            num=n;
        }//普通构造函数
    private:
        int num;
    };
    class Test2
    {
    public:
        explicit Test2(int n)
        {
            num=n;
        }//explicit(显式)构造函数
    private:
        int num;
    };
    int main()
    {
        Test1 t1=12;//隐式调用其构造函数,成功
        Test2 t2=12;//编译错误,不能隐式调用其构造函数
        Test2 t2(12);//显式调用成功
        return 0;
    }

    Test1的构造函数带一个int型的参数,代码23行会隐式转换成调用Test1的这个构造函数。而Test2的构造函数被声明为explicit(显式),这表示不能通过隐式转换来调用这个构造函数,因此代码24行会出现编译错误。

    普通构造函数能够被隐式调用。而explicit构造函数只能被显式调用。

     

     

    C++11 中std::function和std::bind的用法

    https://blog.csdn.net/liukang325/article/details/53668046

    关于std::function 的用法: 
    其实就可以理解成函数指针 

    • 保存自由函数
    void printA(int a)
    {
        cout<<a<<endl;
    }
    
    std::function<void(int a)> func;
    func = printA;
    func(2);
    • 保存lambda表达式
    std::function<void()> func_1 = [](){cout<<"hello world"<<endl;};
    func_1();
    • 保存成员函数
    struct Foo {
        Foo(int num) : num_(num) {}
        void print_add(int i) const { cout << num_+i << '\n'; }
        int num_;
    };
    
    // 保存成员函数
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    Foo foo(2);
    f_add_display(foo, 1);

    在实际使用中都用 auto 关键字来代替std::function… 这一长串了。

    关于std::bind 的用法: 
    看一系列的文字,不如看一段代码理解的快

    #include <iostream>
    #include<functional>
    using namespace std;
    class A
    {
    public:
        void fun_3(int k,int m)
        {
            cout<<k<<" "<<m<<endl;
        }
    };
    
    void fun(int x,int y,int z)
    {
        cout<<x<<"  "<<y<<"  "<<z<<endl;
    }
    
    void fun_2(int &a,int &b)
    {
        a++;
        b++;
        cout<<a<<"  "<<b<<endl;
    }
    
    int main(int argc, const char * argv[])
    {
        auto f1 = std::bind(fun,1,2,3); //表示绑定函数 fun 的第一,二,三个参数值为: 1 2 3
        f1(); //print:1  2  3
    
        auto f2 = std::bind(fun, placeholders::_1,placeholders::_2,3);
        //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别有调用 f2 的第一,二个参数指定
        f2(1,2);//print:1  2  3
    
        auto f3 = std::bind(fun,placeholders::_2,placeholders::_1,3);
        //表示绑定函数 fun 的第三个参数为 3,而fun 的第一,二个参数分别有调用 f3 的第二,一个参数指定
        //注意: f2  和  f3 的区别。
        f3(1,2);//print:2  1  3
    
    
        int n = 2;
        int m = 3;
    
        auto f4 = std::bind(fun_2, n,placeholders::_1);
        f4(m); //print:3  4
    
        cout<<m<<endl;//print:4  说明:bind对于不事先绑定的参数,通过std::placeholders传递的参数是通过引用传递的
        cout<<n<<endl;//print:2  说明:bind对于预先绑定的函数参数是通过值传递的
    
    
        A a;
        auto f5 = std::bind(&A::fun_3, a,placeholders::_1,placeholders::_2);
        f5(10,20);//print:10 20
    
        std::function<void(int,int)> fc = std::bind(&A::fun_3, a,std::placeholders::_1,std::placeholders::_2);
        fc(10,20);//print:10 20
    
        return 0;
    }

    具体到我们写的程序中呢,就是用这两个工具给新建的循环线程绑定运行函数。程序中不使用虚函数这种多态机制,具体分析见陈硕大佬的《linux多线程服务端编程》p.447

    bind的设计思想;

    高内聚,低耦合,使被调用的函数和调用者完全隔离开来.调用者可以根据需要任意设计接口,和传参,而被调用函数通过bind可以不经修改接口就可以兼容各种需求的变化.

    区别于静态绑定,动态绑定,这属于程序员自动绑定.

     

     

    线程的栈

    在很多现代操作系统中,一个进程的(虚)地址空间大小为4G,分为系统(内核?)空间和用户空间两部分,系统空间为所有进程共享,而用户空间是独立的,一般linux进程的用户空间为3G。

    进程简说:

    进程就是程序的一次执行。

    进程是为了在CPU上实现多道编程而发明的一个概念。

    事实上我们说线程是进程里面的一个执行上下文,或者执行序列,显然一个进程可以同时拥有多个执行序列,更加详细的描述是,舞台上有多个演员同时出场,而这些演员和舞台就构成了一出戏,类比进程和线程,每个演员是一个线程,舞台是地址空间,这个同一个地址空间里面的所有线程就构成了进程。

    比如当我们打开一个word程序,其实已经同时开启了多个线程,这些线程一个负责显示,一个接受输入,一个定时进行存盘,这些线程一起运转让我们感到我们的输入和屏幕显示同时发生,而不用键入一些字符等好长时间才能显示到屏幕上。

    我们知道,线程共享着一个进程内的堆、全局变量、静态变量(bss和data段)、.text段、文件描述符表等,但是线程拥有自己的pcb(线程id等)、栈、寄存器、程序计数器等。

    我们可以看到,每个线程拥有自己的栈,栈中存放着他调用的函数开辟的栈帧,每个栈帧中存放着一些局部变量和临时值。其中临时值存放了我调用下一个函数开辟栈帧之前的ebp和esp指向的位置,在函数调用完毕后,才知道要从哪里继续执行。

    这里有一个线程号的概念--

    线程号是内核用来分配时间轮片时要用的

    线程id是进程内部区分线程时用到的。

    linux下查看一个进程开辟了多少线程以及他们的线程号:

    ps -Lf 3500(pid)

    其中LWP列就是线程号

    这里特别说一下pthread_join的返回值:

    
    int pthread_join(pthread_t thread, void **rval_ptr);

    用主线程去等待子线程pthread_exit时用到这个函数,thread是目标线程标识符,rval_ptr指向目标线程返回时的退出信息(因为pthread_exit退出的返回值是void*型的,所以要用void**型变量去接受它),该函数会一直阻塞,直到被回收的线程结束为止。

    返回的退出信息可以说普通变量也可以是我们自定义的变量:

    1.返回普通变量:

    2.返回自定义变量如结构体:

    其中,传进线程函数的arg就是我们在创建线程时传进的结构体,见下面程序:

    pthread_detach:

    当在一个线程中通过调用pthread_join()来回收资源时,调用者就会被阻塞,如果需要回收的线程数目过多时,效率就大大下降。比如在一个Web 服务器中, 主线程为每一个请求创建一个线程去响应动作,我们并不希望主线程也为了回收资源而被阻塞,因为可能在阻塞的同时有新的请求,我们可以再使用下面的方法,让线程办完事情后自动回收资源。

    1 ). 在子线程中调用pthread_detach( pthread_self() )。 
    2 ).在主线程中调用pthread_detach( tid )。

    可以将线程状态设为分离。运行结束后会自动释放所有资源。

    注意,在子线程detach之后,别的线程是不能去join他的,会一直返回error=22这个错误。

    #include <stdio.h>
    #include <pthread.h>
    
    
    void* run(void * arg)
    {
        pthread_detach( pthread_self());
        printf("I will detach .. \n");
        return NULL;
    }
    
    int main()
    {
        pthread_t tid1;
        pthread_create(&tid1, NULL, run, NULL);
    //或者:
    //pthread_detach(tid1);
    
        sleep(1); // 因为主线程不会挂起等待,为了保证子线程先执行完分离,让主线程先等待1s
        int ret = 0;
        ret =  pthread_join(tid1, NULL);
        if( ret == 0)
        {
            printf(" join sucess. \n");
        }
        else
        {
            printf(" join failed. \n");
        }
        return 0;
    }

     

     

    今天碰到了一个问题:

    虚表是属于类的还是属于对象的

    https://blog.csdn.net/lihao21/article/details/50688337 

    先说结论:虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。 
    为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

    注意:虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。 
    虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。
        当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

     

    C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。因此可以有下面实验代码:

    https://blog.csdn.net/qq_33657884/article/details/81745009

    class classA {
    
        virtual void function() {
    
        }
    };
    
    int main()
    {
        classA *a = new classA();
        printf("%x\n", *(int*)(void*)a);
        for (int i = 0; i < 10000;i++) {
            classA *b = new classA();
            if (*(int*)(void*)a == *(int*)(void*)b) {
                printf("一样的虚函数表地址\n");
            }
            else {
                printf("不一样的虚函数表地址\n");
                break;
            }
            delete b;
        }
        return 0;
    }
    

    打印结果是满屏的一样的虚函数表地址,所以结论是虚函数表是属于一类的

     

    多线程编程之锁和条件变量

    之前学习这部分内容的时候只是快速过一遍,等真正写程序要用的时候发现全忘了。。。

    果然没有自己动手写过的东西不是真正的掌握呀。。。

    互斥锁mutex:


    互斥锁是通过锁的机制来实现线程间的同步问题。互斥锁的基本流程为:

    1.初始化一个互斥锁:pthread_mutex_init()函数
    2.加锁:pthread_mutex_lock()函数或者pthread_mutex_trylock()函数
    3.对共享资源的操作
    4.解锁:pthread_mutex_unlock()函数
    5.注销互斥锁:pthread_mutex_destory()函数
    其中,在加锁过程中,pthread_mutex_lock()函数和pthread_mutex_trylock()函数的过程略有不同:

    当使用pthread_mutex_lock()函数进行加锁时,若此时已经被锁,则尝试加锁的线程会被阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock()函数有返回值时,说明加锁成功;
    而使用pthread_mutex_trylock()函数进行加锁时,若此时已经被锁,则会返回EBUSY的错误码(轮询方式进行加锁)。
    同时,解锁的过程中,也需要满足两个条件:

    解锁前,互斥锁必须处于锁定状态;
    必须由加锁的线程进行解锁。
    当互斥锁使用完成后,必须进行清除。
    api如下:成功返回0,失败返回错误号

    #include <pthread.h>
    #include <time.h>
    // 初始化一个互斥锁。
    int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
    
    // 对互斥锁上锁,若互斥锁已经上锁,则调用者一直阻塞,
    // 直到互斥锁解锁后再上锁。
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    
    // 调用该函数时,若互斥锁未加锁,则上锁,返回 0;
    // 若互斥锁已加锁,则函数直接返回失败,即 EBUSY。
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    
    // 当线程试图获取一个已加锁的互斥量时,pthread_mutex_timedlock 互斥量
    // 原语允许绑定线程阻塞时间。即非阻塞加锁互斥量。
    int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
                   const struct timespec *restrict abs_timeout);
    
    // 对指定的互斥锁解锁。
    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    
    // 销毁指定的一个互斥锁。互斥锁在使用完毕后,
    // 必须要对互斥锁进行销毁,以释放资源。
    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    

    使用例子:

    可以看到,先在全局区定义一个mutex,保证这个锁是能被主线程和子线程共享的。然后在主线程中先初始化锁(mutex=1)再创建子线程,接着就让主线程和子线程都循环,去争夺cpu和锁。

    注意,在线程中访问完临界区后,应该立即释放锁(此处临界区为stdout,就是往屏幕输出),这样才能尽可能地将锁的粒度减小。

    死锁现象:

    1.线程对同一互斥量加锁两次:第一次加锁成功,第二次欲加锁时,发现有人锁上了于是阻塞等待那个人解锁,但是加锁的人又是他自己,所以造成了死锁(我等我自己)。

    2.线程1拥有A锁,请求获得B锁,而线程2拥有B锁,请求获得A锁(使用try_lock,与不能获得所有的锁时,主动放弃已占有的锁去成全别人)

     

    条件变量

    原文:https://blog.csdn.net/daaikuaichuan/article/details/82950711 

    与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常条件变量和互斥锁同时使用。
      条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:

    1. 一个线程等待"条件变量的条件成立"而挂起;
    2. 另一个线程使 “条件成立”(给出条件成立信号)。

    原理】:

      条件的检测是在互

    斥锁的保护下进行的。线程在改变条件状态之前必须首先锁住互斥量。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量 可以被用来实现这两进程间的线程同步。

    【条件变量的操作流程如下】:

    1. 初始化:init()或者pthread_cond_tcond=PTHREAD_COND_INITIALIER;属性置为NULL;

    2. 等待条件成立:pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真 timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait);

    3. 激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)

    4. 清除条件变量:destroy;无线程等待,否则返回EBUSY清除条件变量:destroy;无线程等待,否则返回EBUSY

    api:

    #include <pthread.h>
    // 初始化条件变量
    int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);
    
    // 阻塞等待
    int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
    
    // 超时等待
    int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
    
    // 解除所有线程的阻塞
    int pthread_cond_destroy(pthread_cond_t *cond);
    
    // 唤醒至少一个等待该条件的线程
    int pthread_cond_signal(pthread_cond_t *cond);
    
    // 唤醒等待该条件的所有线程
    int pthread_cond_broadcast(pthread_cond_t *cond);  
    

    条件变量实现生产者消费者模型:

    在调用pthread_cond_wait之前,应该提前做好

    1.创建锁,初始化锁

    2.创建条件变量,初始化条件变量

    3.线程加上锁,然后调用pthread_cond_wait。

    例子:

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <pthread.h>
    
    //链表作为共享数据,需要被互斥锁保护
    struct msg {
    	struct msg* next;
    	int num;
    };
    
    struct msg* head;
    struct msg* mp;
    
    //静态初始化条件变量和互斥锁
    pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
    
    void* consumer(void* p) {
    	for (;;) {
    		pthread_mutex_lock(&lock);
    		while (head == NULL) {	//头指针为空,说明没有节点
    			pthread_cond_wait(&has_product, &lock);
    		}
    		mp = head;
    		head = mp->next;	//模拟消费掉一个产品
    		pthread_mutex_unlock(&lock);
    
    		printf("consume ---- %d\n", mp->num);
    		free(mp);
    		_sleep(rand() % 5);
    	}
    }
    
    void* producer(void* p) {
    	for (;;) {
    		mp = (struct msg*) malloc(sizeof(struct msg));
    		mp->num = rand() % 1000 + 1;//模拟生产一个产品
    		printf("produce ---%d\n", mp->num);
    
    		pthread_mutex_lock(&lock);
    		mp->next = head;
    		head = mp;
    		pthread_mutex_unlock(&lock);
    
    		pthread_cond_signal(&has_product);//将等待在该条件变量上的一个线程唤醒
    		_sleep(rand() % 5);
    	}
    }
    
    int main(int argc, char* argv[]) {
    	pthread_t pid, cid;
    	srand(time(NULL));
    
    	pthread_create(&pid, NULL, producer, NULL);
    	pthread_create(&cid, NULL, consumer, NULL);
    
    	pthread_join(pid, NULL);
    	pthread_join(cid, NULL);
    
    	return 0;
    }

    其中,pthread_cond_wait做了三件事:1.解锁  2.让线程等在条件变量上(等pthread_cond_signal)3.等到时将锁再加上

     

    这个while比较关键,因为往往有许多消费者线程等在条件变量上,而一次来signal时只有一个线程能抢到锁(即wait的第3步),那么这个抢到的线程跳出了while循环去做完业务之后,条件变量又会变为原样(如head==NULL)。那么其他被唤醒的线程在抢到锁后(第3步),就还要判断一次条件变量,否则可能误跳出while,去做业务了(此处造成的后果可能是访问不存在的链表节点)。

     

     

    在创建线程时,给指定的线程函数传入参数:

    1.传入单个值:

    2.传入多个值,应该要创建一个结构体,再把结构体传进去

     

     

     

    noncopyable类

    在程序中定义了一个共有父类叫做noncopyable:

    class noncopyable
    {
    protected:
    	noncopyable() = default;
    	~noncopyable() = default;
    private:
    	noncopyable(const noncopyable&);
    	const noncopyable& operator=(const noncopyable&);
    };

    注意其拷贝构造函数和重载等号运算符是私有的,这保证了他的子类无法等号赋值和拷贝构造:

    #include "noncopyable.h"
    class non :noncopyable
    {
    public:
    	non() {
    
    	}
    	~non() {
    
    	}
    };
    
    int main(){
        non a;
    	non b(a);
    	non c;
    	c = a;
    
    
        return 0;
    }

     

     

    epoll_create1(int flag)

    epoll的接口非常简单,一共就3/4个函数:

    int epoll_create(int size);
    int epoll_create1(int flags);
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    

     对于epoll_create1 的flag参数: 可以设置为0 或EPOLL_CLOEXEC,为0时函数表现与epoll_create()一致, EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭打开的文件描述符,即在另一个进程获得cpu时,自动将本描述符关闭,以免发生误操作(需要注意的是,epoll_create与epoll_create1当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/<pid>/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽)。
     

     

    最初的epoll_create实现中,size参数告诉内核,调用者期望添加到epoll实例中的文件描述符数量。内核使用该信息作为初始分配内部数据结构空间大小的“提示”(如果调用者的使用超过了size,则内核会在必要的情况下分配更多的空间)。目前,这种“提示”已经不再需要了,内核动态的改变数据结构的大小,但是为了保证向后兼容性(新的epoll应用运行于旧的内核上),size参数还是要大于0。
     

    这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求

    pthread_once

    https://blog.csdn.net/zhangxiao93/article/details/51910043 

    #include <pthread.h>
    int  pthread_once(pthread_once_t  *once_control,  void  (*init_routine) (void));

    使用前要先在外部静态初始化一个控制变量 once_control 如

    pthread_once_t once = PTHREAD_ONCE_INIT

    在多线程环境中,有些事仅需要执行一次。通常当初始化应用程序时,可以比较容易地将其放在main函数中。但当你写一个库时,就不能在main里面初始化了,你可以用静态初始化,但使用一次初始化(pthread_once)会比较容易些。

    在多线程编程环境下,尽管pthread_once()调用会出现在多个线程中,init_routine()函数仅执行一次,究竟在哪个线程中执行是不定的,是由内核调度来决定。

    每次线程欲调用init_routine()时,都会先检查once_control变量是否为初始值,若不为初始值(已被执行过)就简单返回。
    一个测试:

    #include<iostream>  
    #include<pthread.h>  
    #include <unistd.h>
    using namespace std;  
    
    pthread_once_t once = PTHREAD_ONCE_INIT;  
    
    void once_run(void)  
    {  
        cout<<"once_run in thread "<<(unsigned int )pthread_self()<<endl;  
    }  
    
    void * child1(void * arg)  
    {  
        pthread_t tid =pthread_self();  
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;  
        pthread_once(&once,once_run);  
        cout<<"thread "<<tid<<" return"<<endl;  
    }  
    
    
    void * child2(void * arg)  
    {  
        pthread_t tid =pthread_self();  
        cout<<"thread "<<(unsigned int )tid<<" enter"<<endl;  
        pthread_once(&once,once_run);  
        cout<<"thread "<<tid<<" return"<<endl;  
    }  
    
    int main(void)  
    {  
        pthread_t tid1,tid2;  
        cout<<"hello"<<endl;  
        pthread_create(&tid1,NULL,child1,NULL);  
        pthread_create(&tid2,NULL,child2,NULL);  
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        cout<<"main thread exit"<<endl;  
        return 0;  
    }  

    程序执行后,线程中指定函数部分在两个子线程中出现,不过只执行一次。

    hello
    thread 3086535584 enter
    once_run in thread 3086535584
    thread 3086535584 return
    thread 3076045728 enter
    thread 3076045728 return
    main thread exit
    

    线程安全的单例模式

    使用pthread_once来保护对象申请,来保证“单例”,及时在多线程的情况下,这是muduo库作者的思路。 
    代码如下:

    //muduo/base/Singleton.h
    
    template<typename T>
    class Singleton : boost::noncopyable
    {
     public:
      static T& instance()
      {
        pthread_once(&ponce_, &Singleton::init);
        assert(value_ != NULL);
        return *value_;
      }
    
     private:
      Singleton();
      ~Singleton();
    
      static void init()
      {
        value_ = new T();
        if (!detail::has_no_destroy<T>::value)
        {
          ::atexit(destroy);
        }
      }
    
      static void destroy()
      {
        typedef char T_must_be_complete_type[sizeof(T) == 0 ? -1 : 1];
        T_must_be_complete_type dummy; (void) dummy;
    
        delete value_;
        value_ = NULL;
      }
    
     private:
      static pthread_once_t ponce_;
      static T*             value_;
    };

     

    enable_shared_from_this

    https://blog.csdn.net/caoshangpa/article/details/79392878 

      enable_shared_from_this是一个模板类,定义于头文件<memory>,其原型为:

    template< class T > class enable_shared_from_this;
    

    std::enable_shared_from_this 能让一个对象(假设其名为 t ,且已被一个 std::shared_ptr 对象 pt 管理)安全地生成其他额外的 std::shared_ptr 实例(假设名为 pt1, pt2, ... ) ,它们与 pt 共享对象 t 的所有权。
           若一个类 T 继承 std::enable_shared_from_this<T> ,则会为该类 T 提供成员函数: shared_from_this 。 当 T 类型对象 t 被一个为名为 pt 的 std::shared_ptr<T> 类对象管理时,调用 T::shared_from_this 成员函数,将会返回一个新的 std::shared_ptr<T> 对象,它与 pt 共享 t 的所有权。
    使用场景:https://www.cnblogs.com/mkdym/p/4947296.html

    熟悉异步编程的同学可能会对boost::shared_from_this有所了解。我们在传入回调的时候,通常会想要其带上当前类对象的上下文,或者回调本身就是类成员函数,那这个工作自然非this指针莫属了,像这样:

    void sock_sender::post_request_no_lock()
    {
        Request &req = requests_.front();
        boost::asio::async_write(*sock_ptr_,
            boost::asio::buffer(req.buf_ptr->get_content()),
            boost::bind(&sock_sender::self_handler, this, _1, _2));
    }

    然而回调执行的时候并不一定对象还存在。为了确保对象的生命周期大于回调,我们可以使类继承自boost::enable_shared_from_this,然后回调的时候使用boost::bind传入shared

    _from_this()返回的智能指针。由于boost::bind保存的是参数的副本,bind构造的函数对象会一直持有一个当前类对象的智能指针而使得其引用计数不为0,这就确保了对象的生存周期大于回调中构造的函数对象的生命周期,像这样:

    class sock_sender
        : public boost::enable_shared_from_this<sock_sender>
    {
        //...
    };
    
    void sock_sender::post_request_no_lock()
    {
        Request &req = requests_.front();
        boost::asio::async_write(*sock_ptr_,
            boost::asio::buffer(req.buf_ptr->get_content()),
            boost::bind(&sock_sender::self_handler, shared_from_this(), _1, _2));
    }

    我们知道,当类继承自boost::enable_shared_from_this后,类便不能再创建栈上对象了,必须new。然而,代码却并没有阻止我们创建栈上对象,使用这个类的人若不清楚这点,很可能就会搞错,导致运行时程序崩溃。(这里待研究)

    这里https://www.cnblogs.com/codingmengmeng/p/9123874.html讲了具体原理

    实践中使用场景:

    class HttpData : public std::enable_shared_from_this<HttpData>			
    {
    public:
            ...
    }
    
    
    loop_->runInLoop(bind(&HttpData::handleClose, shared_from_this()));

     

    手工处理HTTP请求头:

    自己手工处理一次来知道原来是这么回事

    URIState HttpData::parseURI() {
    	string& str = inBuffer_;		//引用
    	string cop = str;
    	// 读到完整的请求行再开始解析请求
    	size_t pos = str.find('\r', nowReadPos_);	//'\r' 回车,回到当前行的行首,而不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖;
    						//windows下'\n' 换行,换到当前位置的下一行,而不会回到行首;
    	if (pos < 0) {
    		return PARSE_URI_AGAIN;
    	}
    	// 去掉请求行所占的空间,节省空间
    	string request_line = str.substr(0, pos);		//request_line保存请求内容
    	if (str.size() > pos + 1)
    		str = str.substr(pos + 1);		//截去请求部分
    	else
    		str.clear();
    	//Method			//判断请求的方法
    	int posGet = request_line.find("GET");
    	int posPost = request_line.find("POST");
    	int posHead = request_line.find("HEAD");
    
    	if (posGet >= 0) {
    		pos = posGet;
    		method_ = METHOD_GET;
    	}
    	else if (posPost >= 0)
    	{
    		pos = posPost;
    		method_ = METHOD_POST;
    	}
    	else if (posHead >= 0)
    	{
    		pos = posHead;
    		method_ = METHOD_HEAD;
    	}
    	else
    	{
    		return PARSE_URI_ERROR;
    	}
    
    	// filename
    	pos = request_line.find("/", pos);
    	if (pos < 0) {
    		fileName_ = "index_html";
    		HTTPVersion_ = HTTP_11;
    		return PARSE_URI_SUCCESS;
    	}
    	else {
    		size_t _pos = request_line.find(' ', pos);
    		if(_pos < 0)
    			return PARSE_URI_ERROR;
    		else {
    			if (_pos - pos > 1) {	//在pos之后找到了空格
    				fileName_ = request_line.substr(pos + 1, _pos - pos - 1);//手工截取fileName_
    				size_t __pos = fileName_.find('?');
    				if (__pos >= 0)				//有问号的话,后面部分舍去
    				{
    					fileName_ = fileName_.substr(0, __pos);
    				}
    			}
    
    			else
    				fileName_ = "index.html";
    		}
    		pos = _pos;		//能走到这说明_pos>0,是有效值
    	}
    
    	//cout << "fileName_: " << fileName_ << endl;
    	// HTTP 版本号
    	pos = request_line.find("/", pos);
    	if (pos < 0)
    		return PARSE_URI_ERROR;
    	else {
    		if (request_line.size() - pos <= 3)		//不够版本号长度
    			return PARSE_URI_ERROR;
    		else {
    			string ver = request_line.substr(pos + 1, 3);
    			if(ver=="1.0")
    				HTTPVersion_ = HTTP_10;
    			else if (ver == "1.1")
    				HTTPVersion_ = HTTP_11;
    			else
    				return PARSE_URI_ERROR;
    		}
    	}
    	return PARSE_URI_SUCCESS;
    }

     

    日志模块:

    看linya大哥的github:https://github.com/linyacool/WebServer/blob/master/WebServer/base/Log%E7%9A%84%E8%AE%BE%E8%AE%A1.txt

    Log的设计仿照了muduo库的设计,但我写的没那么复杂
    https://github.com/chenshuo/muduo
    
    与Log相关的类包括FileUtil、LogFile、AsyncLogging、LogStream、Logging。
    其中前4个类每一个类都含有一个append函数,Log的设计也是主要围绕这个append函数展开的。
    
    FileUtil是最底层的文件类,封装了Log文件的打开、写入并在类析构的时候关闭文件,底层使用了标准IO,该append函数直接向文件写。
    LogFile进一步封装了FileUtil,并设置了一个循环次数,没过这么多次就flush一次。
    AsyncLogging是核心,它负责启动一个log线程,专门用来将log写入LogFile,应用了“双缓冲技术”,其实有4个以上的缓冲区,但思想是一样的。
    AsyncLogging负责(定时到或被填满时)将缓冲区中的数据写入LogFile中。
    LogStream主要用来格式化输出,重载了<<运算符,同时也有自己的一块缓冲区,这里缓冲区的存在是为了缓存一行,把多个<<的结果连成一块。
    Logging是对外接口,Logging类内涵一个LogStream对象,主要是为了每次打log的时候在log之前和之后加上固定的格式化的信息,比如打log的行、
    文件名等信息。

     

     

    stat

    表头文件:    #include <sys/stat.h>
                #include <unistd.h>
    定义函数:    int stat(const char *file_name, struct stat *buf);
    函数说明:    通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
    返回值:     执行成功则返回0,失败返回-1,错误代码存于errno

    错误代码:
        ENOENT         参数file_name指定的文件不存在
        ENOTDIR        路径中的目录存在但却非真正的目录
        ELOOP          欲打开的文件有过多符号连接问题,上限为16符号连接
        EFAULT         参数buf为无效指针,指向无法存在的内存空间
        EACCESS        存取文件时被拒绝
        ENOMEM         核心内存不足
        ENAMETOOLONG   参数file_name的路径名称太长
    例子:

    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    
    int main() {
        struct stat buf;
        stat("/etc/hosts", &buf);
        printf("/etc/hosts file size = %d/n", buf.st_size);
    }

    其中

    struct stat {
        dev_t         st_dev;       //文件的设备编号
        ino_t         st_ino;       //节点
        mode_t        st_mode;      //文件的类型和存取的权限
        nlink_t       st_nlink;     //连到该文件的硬连接数目,刚建立的文件值为1
        uid_t         st_uid;       //用户ID
        gid_t         st_gid;       //组ID
        dev_t         st_rdev;      //(设备类型)若此文件为设备文件,则为其设备编号
        off_t         st_size;      //文件字节数(文件大小)
        unsigned long st_blksize;   //块大小(文件系统的I/O 缓冲区大小)
        unsigned long st_blocks;    //块数
        time_t        st_atime;     //最后一次访问时间
        time_t        st_mtime;     //最后一次修改时间
        time_t        st_ctime;     //最后一次改变时间(指属性)
    };

     

    inode相关知识参见:讲的非常好

    https://www.cnblogs.com/xiexj/p/7214502.html

    FCB(file control block)文件控制块,是文件系统的一部分。在磁盘上通常会创建一个文件系统,文件系统中包括文件夹信息。以及文件的FCB信息。FCB一半包括文件的读写模式。全部者,时间戳,数据块指针等信息。unix的FCB称为inode。其结构例如以下图所看到的

    文件打开的步骤例如以下图所看到的(从右往左看)

    首先,操作系统依据文件名称a,在系统文件打开表中查找

    第一种情况:

    假设文件a已经打开。则在进程文件打开表中为文件a分配一个表项,然后将该表项的指针指向系统文件打开表中和文件a相应的一项;

    然后在PCB中为文件分配一个文件描写叙述符fd,作为进程文件打开表项的指针,文件打开完毕。

    另外一种情况:

    假设文件a没有打开。查看含有文件a信息的文件夹项是否在内存中。假设不在,将文件夹表(目录表)装入到内存中,作为cache。

    依据文件夹表(目录表)中文件a相应项找到FCB(inode)在磁盘中的位置。

    将文件a的FCB装入到内存中的Active inode中。

    然后在系统文件打开表中为文件a添加新的一个表项,将表项的指针指向Active Inode中文件a的FCB;

    然后在进程的文件打开表中分配新的一项,将该表项的指针指向系统文件打开表中文件a相应的表项。

    然后在PCB中,为文件a分配一个文件描写叙述符fd,作为进程文件打开表项的指针,文件打开完毕。

     

    sprintf和read、write

    sprintf

        指的是字符串格式化命令,主要功能是把格式化的数据写入某个字符串中。sprintf 是个变参函数。使用sprintf 对于写入buffer的字符数是没有限制的,这就存在了buffer溢出的可能性。解决这个问题,可以考虑使用 snprintf函数,该函数可对写入字符数做出限制。

    函数功能:把格式化的数据写入某个字符串
    函数原型:int sprintf( char *buffer, const char *format [, argument] … );
    返回值:字符串长度(strlen)

    例子:
     

    char s[50];
    char* who = "I";
    char* whom = "CSDN";
    sprintf(s, "%s love %s.", who, whom); //产生:"I love CSDN. " 这字符串写到s中
    
    sprintf(s,"%s%d%c","test",1,'2');         //会覆盖,应该s+12

     

     

    read

    ssize_t read [1]  (int fd, void *buf, size_t count);

    read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。若参数count为0,则read为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。

    如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。

     

    错误代码

    EINTR 此调用被信号所中断。

    EAGAIN 或者EWOULDBLOCK,当使用非阻塞I/O 时(O_NONBLOCK == 非阻塞),若读缓冲区为空则返回此值。

        非阻塞socket直接忽略;如果是阻塞的socket,一般是读写操作超时了,还未返回。这个超时是指socket的SO_RCVTIMEO与SO_SNDTIMEO两个属性。所以在使用阻塞socket时,不要将超时时间设置的过小。不然返回了-1,你也不知道是socket连接是真的断开了,还是正常的网络抖动。一般情况下,阻塞的socket返回了-1,都需要关闭重新连接

    EBADF 参数fd 非有效的文件描述词,或该文件已关闭。

    ECONNRESET
    1、在客户端服务器程序中,客户端异常退出,并没有回收关闭相关的资源,服务器端再写会先收到ECONNRESET错误,然后收到EPIPE错误。

       或者是linger选项中设置了l_onoff=>0且linger=0,则调用socket时本端会强制关闭socket,丢弃缓冲区内数据并且向对方发生RST分节,则对方将read返回-1且错误码为ECONNRESET。
    2、连接被远程主机关闭。有以下几种原因:远程主机停止服务,重新启动;当在执行某些操作时遇到失败,因为设置了“keep alive”选项,连接被关闭,一般与ENETRESET一起出现。
    3、远程端执行了一个“hard”或者“abortive”的关闭。应用程序应该关闭socket,因为它不再可用。当执行在一个UDP socket上时,这个错误表明前一个send操作返回一个ICMP“port unreachable”信息。
    4、如果client关闭连接,server端的select并不出错(不返回-1,使用select对唯一一个socket进行non- blocking检测),但是写该socket就会出错,用的是send.错误号:ECONNRESET.读(recv)socket并没有返回错误。
    5、该错误被描述为“connection reset by peer”,即“对方复位连接”,这种情况一般发生在服务进程较客户进程提前终止。当服务进程终止时会向客户 TCP 发送 FIN 分节,客户 TCP 回应 ACK,服务 TCP 将转入 FIN_WAIT2 状态。此时如果客户进程没有处理该 FIN (如阻塞在其它调用上而没有关闭 Socket 时),则客户 TCP 将处于 CLOSE_WAIT 状态。当客户进程再次向 FIN_WAIT2 状态的服务 TCP 发送数据时,则服务 TCP 将立刻响应 RST。一般来说,这种情况还可以会引发另外的应用程序异常,客户进程在发送完数据后,往往会等待从网络IO接收数据,很典型的如 read 或 readline 调用,此时由于执行时序的原因,如果该调用发生在 RST 分节收到前执行的话,那么结果是客户进程会得到一个非预期的 EOF 错误。此时一般会输出“server terminated prematurely”-“服务器过早终止”错误。
    ENETRESET
    网络重置时丢失连接。
    由于设置了"keep-alive"选项,探测到一个错误,连接被中断。在一个已经失败的连接上试图使用setsockopt操作,也会返回这个错误。

     

     

    write

    ssize_t write (int fd,const void * buf,size_t count);

    write()会把指针buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。

    返回值

    如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。

    错误代码

    EINTR 此调用被信号所中断。

    EAGAIN 当使用非阻塞I/O 时(O_NONBLOCK),若写缓冲区已满则返回此值。

    EBADF 参数fd非有效的文件描述词,或该文件已关闭。

    EPIPE
    1、Socket 关闭,但是socket号并没有置-1。继续在此socket上进行send和recv,就会返回这种错误。这个错误会引发SIGPIPE信号,系统会将产生此EPIPE错误的进程杀死。所以,一般在网络程序中,首先屏蔽此消息,以免发生不及时设置socket进程被杀死的情况。
    2、write(..) on a socket that has been closed at the other end will cause a SIGPIPE.
    3、错误被描述为“broken pipe”,即“管道破裂”,这种情况一般发生在客户进程不理会(或未及时处理)Socket 错误,继续向服务 TCP 写入更多数据时,内核将向客户进程发送 SIGPIPE 信号,该信号默认会使进程终止(此时该前台进程未进行 core dump)。结合上边的 ECONNRESET 错误可知,向一个 FIN_WAIT2 状态的服务 TCP(已 ACK 响应 FIN 分节)写入数据不成问题,但是写一个已接收了 RST 的 Socket 则是一个错误

    (对端已经关闭,再向他写则收到对端的rst分节,再写则收到本端内核的sigpipe信号)
     

     

    mmap(一种内存映射文件的方法)

    (进程间通信的共享内存)

    mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。mmap在用户空间映射调用系统中作用很大。

    //头文件
    #include<sys/mman.h>
    //函数原型
    void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
    int munmap(void* start,size_t length);

    条件:mmap() [1]  必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。

    参数说明

    start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。

    length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理

    prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起

        PROT_EXEC //页内容可以被执行

        PROT_READ //页内容可以被读取

        PROT_WRITE //页可以被写入

        PROT_NONE //页不可访问

    flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体

        MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。

        MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。

        MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。

        MAP_DENYWRITE //这个标志被忽略。

        MAP_EXECUTABLE //同上

        MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。

        MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。

        MAP_GROWSDOWN //用于堆栈,告诉内核VM系统,映射区可以向下扩展。

        MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。

        MAP_ANON //MAP_ANONYMOUS的别称,不再被使用。

        MAP_FILE //兼容标志,被忽略。

        MAP_32BIT //将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。

        MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。

        MAP_NONBLOCK //仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

    fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。

    off_toffset:被映射对象内容的起点。

     

     

    返回值:

    成功执行时,mmap()返回被映射区的指针munmap()返回0。失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。errno被设为以下的某个值

    EACCES:访问出错

    EAGAIN:文件已被锁定,或者太多的内存已被锁定

    EBADF:fd不是有效的文件描述符

    EINVAL:一个或者多个参数无效

    ENFILE:已达到系统对打开文件的限制

    ENODEV:指定文件所在的文件系统不支持内存映射

    ENOMEM:内存不足,或者进程已超出最大内存映射数量

    EPERM:权能不足,操作不允许

    ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志

    SIGSEGV:试着向只读区写入

    SIGBUS:试着访问不属于进程的内存区

    • 系统调用mmap()用于共享内存的两种方式: 

      (1)使用普通文件提供的内存映射:适用于任何进程之间;此时,需要打开或创建一个文件,然后再调用mmap();典型调用代码如下: 
      fd=open(name, flag, mode); 
      if(fd<0) 
      ... 
      ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,我们将在范例中进行具体说明。 
      (2)使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。 
      对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可

    • 系统调用munmap() 

      int munmap( void * addr, size_t len ) 
      该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小。当映射关系解除后,对原来映射地址的访问将导致段错误发生。 

    • 系统调用msync() 

      int msync ( void * addr , size_t len, int flags) 
      一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

     

    实际使用:用来打开服务器上的文件,并读取出内容,再发送给请求的客户端:

            int src_fd = open(file_name.c_str(), O_RDONLY, 0);		//打开请求资源文件
            char *src_addr = static_cast<char*>(mmap(NULL, sbuf.st_size, PROT_READ, MAP_PRIVATE, src_fd, 0));
            close(src_fd);
        
            // 发送文件并校验完整性
            send_len = writen(fd, src_addr, sbuf.st_size);
            if(send_len != sbuf.st_size)
            {
                perror("Send file failed");
                return ANALYSIS_ERROR;
            }
            munmap(src_addr, sbuf.st_size);			//解除一个映射关系
            return ANALYSIS_SUCCESS;

     

    后台开发工程师技术能力体系图:

     

    展开全文
  • 目录 nc: tcp连接的正确关闭方式: 服务器tcp端口设置三连: 为什么IO多路复用要搭配非阻塞IO 为什么non-blocking网络编程中应用层buffer是必须的 ...linux最大打开文件描述符数目: ...ncat 也会顺...

    目录

    nc:

    tcp连接的正确关闭方式:

    服务器tcp端口设置三连:

    为什么IO多路复用要搭配非阻塞IO

    为什么non-blocking网络编程中应用层buffer是必须的

    linux最大打开文件描述符数目:

    此版本与先前版本的区别之处:

    写脚本监控进程的内存占用、cpu占用

    tcp 的keepalive


    nc:

    netcat 简称 nc,安全界叫它瑞士军刀。ncat 也会顺便介绍,弥补了 nc 的不足,被叫做 21 世纪的瑞士军刀。nc 的基本功能如下:

    telnet / 获取系统 banner 信息
    传输文本信息
    传输文件和目录
    加密传输文件
    端口扫描
    远程控制 / 正方向 shell
    流媒体服务器
    远程克隆硬盘

    参数如下

    参数

    说明

    -C

    类似-L选项,一直不断连接[1.13版本新加的功能]

    -d

    后台执行

    -e prog

    程序重定向,一旦连接,就执行 [危险!!]

    -g gateway

    源路由跳数,最大值为8(source-routing hop point[s],up to 8)

    -G num

    源路由指针:4,8,12,... (source-routing pointer: 4,8,12,...)

    -h

    帮助信息

    -i secs

    延时的间隔

    -l

    监听模式,用于入站连接

    -n

    指定数字的IP地址,不能用hostname

    -o file

    记录16进制的传输

    -p port

    本地端口号

    -r

    任意指定本地及远程端口

    -s addr

    本地源地址

    -u

    UDP模式,[netcat-1.15可以:远程nc -ulp port -e cmd.exe,本地nc -u ip port连接,得到一个shell.]

    -v

    详细输出——用两个-v可得到更详细的内容

    -w secs

    指定超时的时间

    -z

    将输入输出关掉——用于扫描时

    用法例子:

    https://www.oschina.net/translate/linux-netcat-command

    1.端口扫描

    2.Chat Server

    3.文件传输

    4.目录传输

    5.加密你通过网络发送的数据

    6.流视频

    7.克隆一个设备

    8.打开一个shell

     

     

     

     

    tcp连接的正确关闭方式:

    tcp数据发送不完整的情况:

    当服务端send()发送完数据就立即close fd,若输入缓冲区内有数据,则close()会触发协议发送RST给客户端,导致客户端过早地断开连接,从而客户端接收到的数据不完整(服务端开一个while,每次读一个文件8k再send给客户端,最后发送的若干8k数据在客户端关闭后就送不到了)

    第一次发送文件时,客户端不往服务端写数据,则客户端收到的文件大小是正常的1235399k字节

    第二次发送文件时,客户端往服务端写1234567890这串数字,导致了客户端收到RST,提早关闭了连接,只收到1212416k

    字节的数据,差了22983k字节。

     

    正确的做法:

    服务端send完数据后只关闭写端,保留读端(半关闭状态)->客户端读完了数据(read=0)且无数据要发送 就close -> 服务端read=0 从而close。

     

    注意:若出现客户端恶意或者有bug  一直不close连接,那么服务端可能会一直阻塞或者返回ERRNO=EAGAIN,不满足read=0的情况,就不会close,因此一般要加上一个超时时间,在关闭写端后若干秒内没有收到read=0也强行关闭连接。

     

    对于TCP non-blocking socket, recv返回值== -1,但是errno == EAGAIN, 此时表示在执行recv时相应的socket buffer中没有数据,应该继续recv

    见另一篇文章的 优雅关闭连接

    优雅关闭连接(linger):服务器close连接时,会先将缓冲区内未发送完的数据发出去再关闭连接(可设置超时)。

    半关闭:服务端send完数据后只关闭写端,保留读端(半关闭状态)->客户端读完了数据(read=0)且无数据要发送 就close -> 服务端read=0 从而close。若出现客户端恶意或者有bug  一直不close连接,那么服务端可能会一直阻塞或者返回ERRNO=EAGAIN,不满足read=0的情况,就不会close,因此一般要加上一个超时时间,在关闭写端后若干秒内没有收到read=0也强行关闭连接。

     

    TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

     

    服务器tcp端口设置三连:

    tcp NoDelay :关闭tcp的 Nagle算法

    ignore sigpipe: 防止某个客户端意外断开连接时,服务器还在往他写数据,会收到RST和sigpipe,导致服务器进程退出的bug

                              当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。

    socket reuseaddr :打开端口复用:目的是在服务器挂掉后能够立即重启,监听跟刚才同一个端口,否则会显示端口正在被监听,一般要等2分钟才能再次监听(time_wait) 。 情况2:accept新连接后创建了一个子进程来处理客户端请求,此时监听进程停止了,重启时,由于子进程正在处理客户端请求(这个连接占用了端口),若没开启SO_REUSEADDR将会导致bind失败,监听进程无法启动。

     

    linger:优雅关闭连接

     

     

    为什么IO多路复用要搭配非阻塞IO

     

    • select、poll_wait、epoll_wait返回可读≠read去读的时候能读到(因为select和read是两个独立的系统调用)。如果不用非阻塞,程序会永远卡在read上。以上情况可能出现在多进程同时监听一个socket,只有一个进程可以accept,别的都会block(惊群效应)。
    • 假如socket的读缓冲区已经有足够多的数据,需要read多次才能读完,如果是非阻塞可以在循环里读取,不用担心阻塞在read上,等到errno被置为EWOULDBLOCK的时候break,安全返回select。但如果是阻塞IO,只敢读取一次,因为如果读取没有数据的fd,read会阻塞,无法返回select,这样就只能期待着多次从select返回,每次只读一次,效率低下。而且,如果是ET模式,还会造成数据无人处理,导致饥饿。
       

    为什么non-blocking网络编程中应用层buffer是必须的


    首先,multiplex的核心思想是——用一个线程去同时对多个socket连接服务,(传统的模式是一个thread/process只对一个connection服务),而想要做到这一点,thread/process就不能阻塞在某一个socket的read或write上,就要用到非阻塞IO,原因见上。(应该阻塞在epoll_wait的调用上)

    那么现在假设你要向一个socket发送100kb的数据,但是write调用中,操作系统只接受了80kb的数据,原因可能是受制于TCP的流量控制等等,现在你有两个选择

    1.   等——你可以while这个write调用,但你不知道要等多久,这取决于对方什么时候收到之前的报文并且滑动窗口,而且这样也浪费了处理别的socket的时间。

    2.    把剩下的20kb存起来,下次再发,具体一点就是把这20kb保存在这个TCPconnection的output buffer里,并且注册POLLOUT事件,这样select下次返回的时候就还会来发送这20kb的数据,也不会影响别的socket的监听。

    • 若20kb发送之前,又有数据要write,则应该append到缓冲区尾部,否则可能造成乱码
    • POLLOUT可写事件到来是由操作系统的发送缓冲区有空触发的,同理EPOLLIN事件是操作系统接收缓冲区有数据触发的
    • 若缓冲区为empty,则应该停止关心POLLOUT事件,否则可能会busy loop(但是epoll的ET模式下不会再次提醒,就没有这个问题)

    至于为什么需要inputbuffer, 那是因为TCP是一个没有边界的字节流协议,不可能一个数据报就是一个请求。

     

     

    非阻塞IO这里涉及到一个问题,考虑这样一个简单场景:

    1. 有一个echo服务器,他的任务就是简单的将客户端发来的数据存到机子的缓冲区里,等到read=0的时候就将缓冲区的数据write回客户端。   现在有一个恶意客户端,往服务器不停地写写写,这样服务器的缓冲区就会一直变大(无限增大或者到达上限后开始溢出丢包),导致服务器程序占用的内存不断增大。这时候应该在程序中设置一个缓冲区大小上限,达到上限时就停止收包,赶紧清空缓冲区。
     

    2. 有个代理服务器,他和服务器之间的带宽是1Gb, 客户端通过普通家用宽带(假设8Mb)像代理服务器请求一个大文件,那么代理服务器很快就从服务器里拿到了整个文件,但是他发给客户端的速度太慢了,出现这种情况:

    应该要设置一个高水位和低水位,高于高水位时停止发送,低于低水位时快速发送

     

     

    同步IO和异步IO:

    同步IO 指的是,必须等待IO 操作完成后,控制权才返回给用户进程。异步IO 指的是,无须等待IO 操作完成,就将控制权返回给用户进程。

     

    阻塞IO和非阻塞IO:

    阻塞和非阻塞的概念描述的是用户线程调用内核IO 操作的方式:阻塞是指IO 操作需要彻底完成后才返回到用户空间;而非阻塞是指IO 操作被调用后立即返回给用户一个状态值,不需要等到IO 操作彻底完成。

    在非阻塞状态下, recv()接口在被调用后立即返回,返回值代表了不同的含义,如下
    所述。
    ( 1 ) recv()返回值大于0 ,表示接收数据完毕,返回值即是接收到的字节数。
    ( 2) recv()返回0 ,表示连接已经正常断开。
    ( 3) recv()返回- 1 ,且ermo 等于EAGAIN ,表示recv 操作还没执行完成(内核中数据还没准备好)
    ( 4) recv()返回斗,且ermo 不等于EAGAIN ,表示recv 操作遇到系统错误ermo 。

        可以看到服务器线程可以通过循环调用recv()接口,可以在单个线程内实现对所有连接的数据接收工作。但是上述模型绝不被推荐,因为循环调用recv()将大幅度占用CPU 使用率; 此外, 在这个方案中recv()更多的是起到检测“操作是否完成”的作用,实际操作系统提供了更为高效的检测“操作是否完成”作用的接口,例如select() 多路复用模式,可以一次检测多个连接是存活跃。

     

    异步IO 模型的流程如图7-6 所示。
    用户进程发起read 操作之后,立刻就可以开始去做其他的事;而另一方面,从内核的角度,当它收到一个异步的read 请求操作之后,首先会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存中,当这一切都完成之后,内核会给用户进程发送一个信号,返回read 操作已完成的信息。

    调用阻塞IO 会一直阻塞住对应的进程直到操作完成,而非阻塞IO 在内核还在准备数据的情况下会立刻返回。

    同步IO与异步IO的区别就在于同步IO 进行IO 操作时会阻塞进程。按照这个定义,之前所述的阻塞IO 、非阻塞IO 及多路IO 复用都属于同步IO 。实际上,真实的IO操作,就是例子中的recvfrom 这个系统调用。非阻塞IO 在执行recvfrom 这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是当内核中数据准备好时,recvfrom 会将数据从内核拷贝到用户内存中,这个时候进程则被阻塞。而异步IO 则不一样,当进程发起IO 操作之后就直接返回,直到内核发送一个信号,告诉进程IO 已完成,则在这整个过程中,进程完全没有被阻塞。

    各个IO 模型的比较如图:

     

     

     

     

    linux最大打开文件描述符数目:

    系统最大打开文件描述符数:/proc/sys/fs/file-max

    1.    查看

    $ cat /proc/sys/fs/file-max
    
    186405

     

    2. 设置

    a.    临时性

     echo 1000000 > /proc/sys/fs/file-max

    b.    永久性:在/etc/sysctl.conf中设置

    fs.file-max = 1000000

     

    2.    进程最大打开文件描述符数:user limit中nofile的soft limit

    1.    查看

    $ ulimit -n
    
    1700000

    2. 设置

    a.    临时性:通过ulimit -Sn设置最大打开文件描述符数的soft limit,注意soft limit不能大于hard limit(ulimit -Hn可查看hard limit),另外ulimit -n默认查看的是soft limit,但是ulimit -n 1800000则是同时设置soft limit和hard limit。对于非root用户只能设置比原来小的hard limit。

    查看hard limit:

    $ ulimit -Hn
    
    1700000

    设置soft limit,必须小于hard limit:

    $ ulimit -Sn 1600000

    b.    永久性:上面的方法只是临时性的,注销重新登录就失效了,而且不能增大hard limit,只能在hard limit范围内修改soft limit。若要使修改永久有效,则需要在/etc/security/limits.conf中进行设置(需要root权限),可添加如下两行,表示用户chanon最大打开文件描述符数的soft limit为1800000,hard limit为2000000。以下设置需要注销之后重新登录才能生效:

    chanon           soft    nofile          1800000
    
    chanon           hard   nofile          2000000

    设置nofile的hard limit还有一点要注意的就是hard limit不能大于/proc/sys/fs/nr_open,假如hard limit大于nr_open,注销后无法正常登录。可以修改nr_open的值:

     echo 2000000 > /proc/sys/fs/nr_open

     

    3.    查看当前系统使用的打开文件描述符数

    [root@localhost bin]# cat /proc/sys/fs/file-nr
    
    5664        0        186405

    其中第一个数表示当前系统已分配使用的打开文件描述符数,第二个数为分配后已释放的(目前已不再使用),第三个数等于file-max。

     

    4.    总结:

    a.    所有进程打开的文件描述符数不能超过/proc/sys/fs/file-max

    b.    单个进程打开的文件描述符数不能超过user limit中nofile的soft limit

    c.    nofile的soft limit不能超过其hard limit

    d.    nofile的hard limit不能超过/proc/sys/fs/nr_open
     

     

    此版本与先前版本的区别之处:

    1. RAII机制管理fd、锁等对象,减少了内存泄漏的可能。例如:每个fd都是由一个httpdata对象进行管理的,主线程accept产生读写fd后,就创建一个httpdata对象来管理他,在httpdata对象析构时close他。不需要我们手动close,这也减少了服务端由于无法close而堆积大量close-wait状态连接的现象。还有shared_from_this(),确保对象的生存周期大于回调中构造的函数对象的生命周期,使用场景是处理httpdata对象关闭fd时。
    2. 固定数量线程循环可充分发挥多核cpu的性能。每个eventloop都有自己的任务队列,由主线程accept新连接后以round robin形式放入新任务。固定线程数量同时是为了使一个fd固定由一个线程处理。
    3. keep-alive选项:若无则一个httpdata的timeout为2000ms,若有,则为300000ms。
    4. 线程池与one-loop-per-thread:使用线程池时,可能同一个客户端发来的先后两个请求会被池中不同的两线程处理(指的是IO操作全在主线程,而线程池只负责计算的情况,若线程池计算完直接IO则不会),若后一次请求的复杂度较低,可能客户端会先收到后一次响应,造成乱序(因为线程池是新来一个请求就插入到线程池的queue尾部,然后notify一个线程来处理),应通过响应id来判断。若用one-loop-per-thread,则一个fd从始至终只由一个线程管理,不会有乱序现象。

     

    写脚本监控进程的内存占用、cpu占用

    https://www.cnblogs.com/saryli/p/9924544.html

    https://blog.csdn.net/bbwangj/article/details/81320896

    核心命令:

    cat /proc/pid/status

    其中PID是具体的进程号,这个命令打印出/proc/特定进程/status文件的内容,信息比较多,包含了物理内存/虚拟内存的使用状况,监控进程是否有内存泄露的问题,一般查看进程占用物理内存的情况:

    VmRSS: xxxkB

    可以采用grep命令过滤出我们需要的信息:

    cat /proc/$PID/status | grep RSS >> "$LOG" #过滤包含RSS的行,并且重定向到参数LOG表示的文件
    

     

    由于PID号需要通过进程名获取,同样使用grep命令过滤出我们指定进程的进程号:

    ps | grep $PROCESS | grep -v 'grep' | awk '{print $1;}'#$PROCESS表示进程名字

     

    脚本示例1:

    #!/bin/bash
    pid=$1  #获取进程pid
    echo $pid
    interval=1  #设置采集间隔
    while true
    do
        echo $(date +"%y-%m-%d %H:%M:%S") >> ./proc_memlog.txt
        cat  /proc/$pid/status|grep -e VmRSS >> ./proc_memlog.txt    #获取内存占用
        cpu=`ps -p $1 -o pcpu |grep -v CPU | awk '{print $1}' | awk -F. '{print $1}'`    #获取cpu占用
        echo "Cpu: " $cpu >> ./proc_memlog.txt
        echo $blank >> ./proc_memlog.txt
        sleep $interval
    done

    调用方式:

    $ sh shellName.sh [pid]
    #exp:
    sh monitor.sh 1234

    运行效果:proc_memlog.txt文件中:

     

    tcp 的keepalive

    • TCP Keepalive的起源

    TCP协议中有长连接和短连接之分。短连接环境下,数据交互完毕后,主动释放连接;

    长连接的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断电、死机、崩溃、重启,还是中间路由网络无故断开,这些TCP连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。所以服务器端要做到快速感知失败,减少无效链接操作,这就有了TCP的Keepalive(保活探测)机制。

     

    • TCP Keepalive工作原理

    当一个 TCP 连接建立之后,启用 TCP Keepalive 的一端便会启动一个计时器,当这个计时器数值到达 0 之后(也就是经过tcp_keep-alive_time时间后,这个参数之后会讲到),一个 TCP 探测包便会被发出。这个 TCP 探测包是一个纯 ACK 包(规范建议,不应该包含任何数据,但也可以包含1个无意义的字节,比如0x0。),其 Seq号 与上一个包是重复的,所以其实探测保活报文不在窗口控制范围内。

    如果一个给定的连接在两小时内(默认时长)没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:

    1.     客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。

    2.     客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接

    3.     客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。

    4.     客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探测的响应。

    对于linux内核来说,应用程序若想使用TCP Keepalive,需要设置SO_KEEPALIVE套接字选项才能生效。

    有三个重要的参数:

    1.     tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。

    2.     tcp_keepalive_probes 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)

    3.     tcp_keepalive_intvl,在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。

    其他编程语言有相应的设置方法,这里只谈linux内核参数的配置。例如C语言中的setsockopt()函数,java的Netty服务器框架中也提供了相关接口。
     

    • TCP Keepalive可能导致的问题

    Keepalive 技术只是 TCP 技术中的一个可选项。因为不当的配置可能会引起一些问题,所以默认是关闭的。

    可能导致下列问题:

    1.     在短暂的故障期间,Keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接

    2.     需要消耗额外的宽带和流量

    3.     在以流量计费的互联网环境中增加了费用开销

    • TCP Keepalive HTTP Keep-Alive 的关系

    HTTP协议的Keep-Alive意图在于TCP连接复用,同一个连接上串行方式传递请求-响应数据;TCP的Keepalive机制意图在于探测连接的对端是否存活。

     

    展开全文
  • 目录 model http请求解析: 1.开启线程池和事件循环 2.主线程处理连接事件 3.IO线程处理读写事件 4.timerNode和HttpData 5.日志 6.其他 一些问题: model http请求解析: ...EventLoop mai...

    目录

    model

    http请求解析:

    1.开启线程池和事件循环

    2.主线程处理连接事件

    3.IO线程处理读写事件

    4.timerNode和HttpData

    5.日志

    6.其他

    一些问题:


    model

     

    http请求解析:

     

     

    这个层层调用还是有些复杂,要好好捋一捋

    1.开启线程池和事件循环

    • Main函数开始运行,首先创建
    EventLoop mainLoop;
    Server myHTTPServer(&mainLoop, threadNum, port);		//在此构造函数中new了一个EventLoopThreadPool
    • Main函数中开启主事件循环(reactor线程)运行
    	myHTTPServer.start();
    	mainLoop.loop();
    • 在 myHTTPServer构造函数中,创建了线程池eventLoopThreadPool_(new EventLoopThreadPool(loop_,threadNum)),其线程数量由threadNum指定
    • 在myHTTPServer.start()中,启动了线程池:eventLoopThreadPool_->start();

    好,在启动线程池之后,为线程池添加事件循环线程:

    for (int i = 0; i < numThreads_; ++i) {
    		std::shared_ptr<EventLoopThread> t(new EventLoopThread());	//根据传入的线程数创建指向loop线程的智能指针
    		threads_.push_back(t);		//将管理循环线程的指针填入已创建线程的数组,便于统一管理
    		loops_.push_back(t->startLoop());	//开启新建线程的循环,并放入管理循环的数组中
    	}

    在new EventLoopThread()中,我们一层一层往下看:

    在EventLoopThread的构造函数中,有

    thread_(bind(&EventLoopThread::threadFunc, this), "EventLoopThread"),  //绑定线程的函数
    
    //其中
    void EventLoopThread::threadFunc() {
    	EventLoop loop;
    	{
    		MutexLockGuard lock(mutex_);
    		loop_ = &loop;		//引用	,到这里loop_就不为NULL了,即条件变量等到了
    		cond_.notify();
    	}
    
    	loop.loop();
    	//assert(exiting_);
    	loop_ = NULL;			//就是起到一个EventLoopThread创建EventLoop并启动事件循环的作用
    }

    其中thread_变量是一个Thread类,相当于我们调用了他的构造函数并绑定好了创建loop的函数,如下:

    Thread::Thread(const ThreadFunc &func, const string &n)
    	:started_(false),
    	joined_(false),
    	pthreadId_(0),
    	tid_(0),
    	func_(func),
    	name_(n),
    	latch_(1)
    {
    	setDefaultName();
    }

    接着我们看loops_.push_back(t->startLoop()); 这个函数的目的是开启线程的loop,并push进loops_数组中,便于管理。

    我们看看他往下到底做了啥:

    EventLoop* EventLoopThread::startLoop() {		//由EventLoopThreadPool::start()调用
    	assert(!thread_.started());
    	thread_.start();
    
    	{
    		MutexLockGuard lock(mutex_);		//将锁搞成一个类,在函数段内创建临时锁对象并加锁,当退出函数段时自动调用对象析构函数进行解锁
    		// 一直等到threadFun在Thread里真正跑起来
    		while (loop_ == NULL)
    			cond_.wait();		//等在条件变量上 ,	条件变量也封装为一个条件类,并且this类中包含了cond的对象
    	}
    	return loop_;
    }

    调用了thread_.start(); 追踪一波:

    void Thread::start() {
    	assert(!started_);
    	started_ = true;
    	ThreadData* data = new ThreadData(func_, name_,&tid_, &latch_);
    	if (pthread_create(&pthreadId_, NULL, &startThread, data)) {
    		started_ = false;
    		delete data;
    	}
    	else {							//注意,pthread_create创建成功是返回0的
    		latch_.wait();
    		assert(tid_ > 0);
    	}
    }

    好了,实际上start就是pthread_create创建线程并绑定好我们之前给他的函数。

    创建好的线程会运行threadFunc函数,创建loop并唤醒主线程,好返回自己的loop并填进loops_中。

    至此完成IO线程的loop创建步骤。

    • 然后,在创建完线程之后,myHTTPServer紧接着配置他的channel:
    void Server::start() {
    	eventLoopThreadPool_->start();			//开启线程池
    	//acceptChannel_->setEvents(EPOLLIN | EPOLLET | EPOLLONESHOT);
    	acceptChannel_->setEvents(EPOLLIN | EPOLLET);
    	acceptChannel_->setReadHandler(bind(&Server::handNewConn, this));    //处理新连接事件
    	acceptChannel_->setConnHandler(bind(&Server::handThisConn, this));    //处理读写事件
    	loop_->addToPoller(acceptChannel_, 0);	//将负责处理接受连接的channel 挂到epoll去
    	started_ = true;
    }

    这里不得不仔细看看myHTTPServer的构造函数主要做了哪些工作:

    Server::Server(EventLoop *loop, int threadNum, int port)
    	: loop_(loop),
    	threadNum_(threadNum),
    	eventLoopThreadPool_(new EventLoopThreadPool(loop_,threadNum)),		//新建事件循环线程池
    	started_(false),
    	acceptChannel_(new Channel(loop_)),		//新建channel
    	port_(port),
    	listenFd_(socket_bind_listen(port_))	//绑定端口,初始化listenfd
    {
    	acceptChannel_->setFd(listenFd_);
    	handle_for_sigpipe();			//在util中实现
    	if (setSocketNonBlocking(listenFd_) < 0) {	//设置非阻塞监听
    		perror("set socket non block failed");
    		abort();
    	}
    }

    原来他已经创建好了acceptchannel、listenfd并且绑定了端口、设置了非阻塞监听。

    回到上一个程序段,一系列的set操作将acceptchannel 对新连接的处理函数进行了配置,

    并且loop_->addToPoller(acceptChannel_, 0);    //将负责处理接受连接的channel 挂到epoll红黑树上去。

    自此,主线程已经可以开始监听新连接事件了。

     

     

     

     

    2.主线程处理连接事件

    • 准备工作完成后,主线程就开始loop了:mainLoop.loop();

    好,在主线程loop中有:

    void EventLoop::loop() {
    	assert(!looping_);		//确保线程中的eventloop未启动
    	assert(isInLoopThread());	//确保eventloop已绑定线程
    	looping_ = true;
    	quit_ = false;
    	//LOG_TRACE << "EventLoop " << this << " start looping";
    	std::vector<SP_Channel> ret;		//SP即shared_ptr
    	while (!quit_) {					//不断循环取出eventloop中epoll获得的事件
    		//cout << "doing" << endl;
    		ret.clear();
    		ret = poller_->poll();		//获得带有事件信息的channel指针数组
    		eventHandling_ = true;
    		for (auto &it : ret)
    			it->handleEvents();	//线程逐个处理,在poll中把epoll_wait拿到的事件都放在ret数组中channel对象的revent里,这里逐个拿出来处理
    		eventHandling_ = false;
    		doPendingFunctors();			
    		poller_->handleExpired();  //主线程处理过期连接(定时器大根堆)
    	}
    	looping_ = false;
    }

    在while循环中,调用  ret = poller_->poll();        //将channel事件拷贝过来存进vector中,然后逐个处理。

    • 我们细看他是怎么拿到事件的:

    ret = poller_->poll();   下一层是:    

    std::vector<SP_Channel> Epoll::poll() {	
    	while (true) {				//不停取,直到取到事件就退出
    		int event_count = epoll_wait(epollFd_, &*events_.begin(), events_.size(), EPOLLWAIT_TIME);		//取到事件并放入events_数组中
    		if (event_count < 0)
    			perror("epoll wait error");
    		std::vector<SP_Channel> req_data = getEventsRequest(event_count);
    		if (req_data.size() > 0)
    			return req_data;
    	}
    }
    
    //其中
    // 分发处理函数
    std::vector<SP_Channel> Epoll::getEventsRequest(int events_num) {
    	std::vector<SP_Channel> req_data;
    	for (int i = 0; i < events_num; ++i) {
    		// 获取有事件产生的描述符
    		int fd = events_[i].data.fd;
    		SP_Channel cur_req = fd2chan_[fd];		//从数组中拿到当前channel
    
    		if (cur_req) {
    			cur_req->setRevents(events_[i].events);	//revents   输出 ,  此处将活跃事件赋值给channel对象的revents_变量
    			cur_req->setEvents(0);
    			// 加入线程池之前将Timer和request分离
    			//cur_req->seperateTimer();
    			req_data.push_back(cur_req);
    		}
    		else {
    			LOG << "SP cur_req is invalid";
    		}
    	}
    	return req_data;
    }

    首先epoll_wait拿到活跃事件,并放入数组events_中,(对于主线程,所有事件都是对listenfd的连接事件)然后调用getEventsRequest 拿到带有每个活跃事件信息的channel数组(数组中每个channel带着一个活跃事件)。

    • 好,拿到活跃事件(装在channel)后,逐个处理
    for (auto &it : ret)
    	it->handleEvents();		
    //线程逐个处理,在poll中把epoll_wait拿到的事件都放在ret数组中channel对象的revent里,这里逐个拿出来处理

    看看channel类具体是如何处理事件的:

    void handleEvents()
    	{
    		events_ = 0;
    		if ((revents_ & EPOLLHUP) && !(revents_ & EPOLLIN))
    		{
    			events_ = 0;
    			return;
    		}
    		if (revents_ & EPOLLERR)
    		{
    			if (errorHandler_) errorHandler_();
    			events_ = 0;
    			return;
    		}
    		if (revents_ & (EPOLLIN | EPOLLPRI | EPOLLRDHUP))
    		{
    			handleRead();        //最终会调用我们set的回调函数
    		}
    		if (revents_ & EPOLLOUT)
    		{
    			handleWrite();
    		}
    		handleConn();        //不是处理输入输出的话就是处理连接的情况,包括清零定时器、设为发完剩余数据、或者关闭连接
    	}

    注意:

    主线程在处理新连接时,只调用回调函数void Server::handNewConn(),注意第8行,round robin法分配accept获得的读写socket描述符给IO线程。

        可以这样理解:四个子线程各有一个事件循环(eventloop),并且主线程中有一个数组来管理四个事件循环,分配时,主线程取出一个事件循环并往他的任务队列中放入新到来的客户端连接(以函数+参数形式,类似传统线程池,且此过程存在主线程与子线程的mutex锁争夺),放入之后主线程往子线程监听的eventfd写一个8字节的数据,就唤醒了子线程。

    此处子线程是兼任计算线程和IO线程的,这是由于

    若采用这样的架构:子线程只负责计算,IO全部交给主线程,那么会产生请求乱序的情况

        如一个客户端先后发了两个请求,一个计算量大,一个计算量小,那么主线程会分发给两个计算线程,这样计算量小的请求会先回复给客户端,就产生了乱序现象。

    而兼任的话就不会,因为对一个客户端的计算和IO都是由一个子线程负责的。

    void Server::handNewConn() {
    	struct sockaddr_in client_addr;
    	memset(&client_addr, 0, sizeof(struct socketaddr_in));
    	socklen_t client_addr_len = sizeof(client_addr);
    	int accept_fd = 0;
    	//接受新连接直到没有了
    	while ((accept_fd = accept(listenFd_, (struct sockaddr*)&client_addr, &client_addr) > 0) {
    		EventLoop* loop = eventLoopThreadPool_->getNextLoop();				//所谓的robin round分配法
    		LOG << "New connection from " << inet_ntoa(client_addr.sin_addr) << ":" << ntohs(client_addr.sin_port);
    		// 限制服务器的最大并发连接数
    		if (accept_fd >= MAXFDS) {
    			close(accept_fd);
    			continue;
    		}
    		// 设为非阻塞模式
    		if (setSocketNonBlocking(accept_fd)<0)
    		{
    			LOG << "Set non block failed!";
    			//perror("Set non block failed!");
    			return;
    		}
    		setSocketNodelay(accept_fd);
    		//setSocketNoLinger(accept_fd);
    		shared_ptr<HttpData> req_info(new HttpData(loop, accept_fd));	//HttpData来处理读写描述符的请求内容
    		req_info->getChannel()->setHolder(req_info);
    		loop->queueInLoop(std::bind(&HttpData::newEvent, req_info));  //放入子线程的事件循环任务队列中
    	}
    	acceptChannel_->setEvents(EPOLLIN | EPOLLET);
    }
    
    //其中
    std::bind(&HttpData::newEvent, req_info)
    的第一个参数是往epoll挂fd的,第二个是包含fd的httpdata对象

     

     

    3.IO线程处理读写事件

    好,我们从第2节知道,主线程会源源不断地把新连接分发到IO线程去,而IO线程要么正在处理事件,要么阻塞在epoll上(更多),那么,怎么得知新连接的到来呢?这里使用了eventfd进行异步唤醒,线程会从epoll_wait中醒来,得到活跃事件,进行处理。如下:

    wakeupFd_(creatEventfd()),//这里新建事件唤醒的文件描述符
    pwakeupChannel_(new Channel(this,wakeupFd_))
    poller_->epoll_add(pwakeupChannel_, 0);
    
    
    //其中
    int creatEventfd() {
    	int evtfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    	if (evtfd < 0)
    	{
    		LOG << "Failed in eventfd";
    		abort();
    	}
    	return evtfd;
    }

    在每个eventloop对象的构造函数中,会做上面的三件事,从而在每个IO线程的epoll树上挂了eventfd,当主线程写了8字节过来时,IO线程就会被唤醒了。(这里有个问题,主线程何时写这8字节呢??是在每次处理完一批epoll事件后吗)(是每次有新任务分发给IO线程的时候)

    主线程在分发新连接事件时,拿出一个loop(nextEventLoop),并调用queueInloop放进去,这时候,调用此queueInloop的不是io线程,就会启动wakeup,往eventfd里面写8字节

    void EventLoop::queueInLoop(Functor&& cb) {
    	{
    		MutexLockGuard lock(mutex_);	//主线程与IO线程争用IO线程的eventloop的锁,保证放进任务队列的时候IO线程不会正在处理任务
    		pendingFunctors_.emplace_back(std::move(cb));
    	}
    
    	if (!isInLoopThread() || callingPendingFunctors_)	//在向IO线程分发未决事件时,还是跑一下程序看看
    		wakeup();
    }
    
    
    //其中 是一个函数数组
    typedef std::function<void()> Functor;
    std::vector<Functor> pendingFunctors_;
    

     

    正常情况下,IO线程阻塞在调用epoll_wait上,当主线程给eventfd写8字节后,他就被唤醒,然后执行doPendingFunctors();,去处理任务队列中的任务,即将读写fd挂到epoll红黑树上去。(注意此时用到channel-fd-httpdata的联系)

     

    IO线程阻塞在epoll上,主线程给他分发的事件会由loop->queueInLoop(std::bind(&HttpData::newEvent, req_info)); 暂存进 pendingFunctors_   数组中(每个eventLoop对象都有),所以在IO线程的

    for (auto &it : ret)
    	it->handleEvents();

    执行完毕之后,会执行  doPendingFunctors()处理未决事件。(都是将分发得到的读写fd挂到红黑树上面,底层调用epoll_ctl)。

     

    每个IO线程中socket文件描述符fd对应的是事件存放在HttpData对象,他绑定的channel处理函数也是HttpData的那几个处理函数,见下面代码:

    shared_ptr<HttpData> req_info(new HttpData(loop, accept_fd));	//HttpData来处理读写描述符的请求内容
    req_info->getChannel()->setHolder(req_info);
    loop->queueInLoop(std::bind(&HttpData::newEvent, req_info));
    
    //其中:
    HttpData::HttpData(EventLoop* loop, int connfd)
    	:loop_(loop),
    	channel_(new Channel(loop, connfd)),
    	fd_(connfd),
    	error_(false),
    	connectionState_(H_CONNECTED),
    	method_(METHOD_GET),
    	HTTPVersion_(HTTP_11),
    	nowReadPos_(0),
    	state_(STATE_PARSE_URI),
    	hState_(H_START),
    	keepAlive_(false) 
    {
    	//loop_->queueInLoop(bind(&HttpData::setHandlers, this));
    	channel_->setReadHandler(bind(&HttpData::handleRead, this));
    	channel_->setWriteHandler(bind(&HttpData::handleWrite, this));
    	channel_->setConnHandler(bind(&HttpData::handleConn, this));
    }

    也就是说,在IO线程中调用

    for (auto &it : ret)
    	it->handleEvents();

    时,是将IO线程的epoll上面一个个读写事件拿出来,最终调用 HttpData::handleRead()或者HttpData::handleWrite或HttpData::handleConn绑定好的回调函数来进行请求处理的。

    其中,值得注意的是   handleConn();        //不是处理输入输出的话就是处理连接的情况,包括清零定时器、设为发完剩余数据、或者关闭连接

     

     

    4.timerNode和HttpData

    epoll类中有两个指针数组,分别维护了 fd->channel  以及  fd->HttpData   的映射关系

    std::shared_ptr<Channel> fd2chan_[MAXFDS];//既然是以fd作为存取索引,为何不用map?
    std::shared_ptr<HttpData> fd2http_[MAXFDS];		//通过channel  -  fd   - HttpData   建立了 运载器-文件描述符-http请求业务的映射关系
    TimerManager timerManager_;

    到这里我们能明白,网络IO线程上的事件实际上是一个个的HttpData对象的请求业务,而每个业务都会配有一个计时器TimerNode,其用途是维护处理超时请求和长时间不活跃的连接。

     

    • timerNodeQueue,每个子acceptor维护一个存放若干TimerNode的最大堆

    自定义比较仿函数保证堆顶元素是那个已连接时间最长且不活跃的那个timerNode:

    struct TimerCmp
    {
    	bool operator()(std::shared_ptr<TimerNode> &a, std::shared_ptr<TimerNode> &b) const
    	{
    		return a->getExpTime() < b->getExpTime();			//大根堆
    	}
    };
    
    
    typedef std::shared_ptr<TimerNode> SPTimerNode;
    std::priority_queue<SPTimerNode, std::deque<SPTimerNode>, TimerCmp> timerNodeQueue;

    每次HttpData处理连接HttpData::handleConn()后,都会update一下他的TimerNode,起到一个清零计时的效果

    • 处理完HttpData的输出后(outbuffer.size()==0)

    会调用HttpData::reset() 将定时器置为deleted,待删除。其中outbuffer中的内容是在AnalysisState HttpData::analysisRequest()函数中填入的,就是针对某种请求内容的响应报文。

    timerNode超时节点的处理逻辑是这样的:
    因为

    (1) 优先队列不支持随机访问
    (2) 即使支持,随机删除某节点后破坏了堆的结构,需要重新更新堆结构。
    所以对于被置为deleted的时间节点,会延迟到它(1)超时 或 (2)它前面的节点都被删除时,它才会被删除。
    一个点被置为deleted,它最迟会在TIMER_TIME_OUT时间后被删除。
    这样做有两个好处:
    (1) 第一个好处是不需要遍历优先队列,省时。
    (2) 第二个好处是给超时时间一个容忍的时间,就是设定的超时时间是删除的下限(并不是一到超时时间就立即删除),如果监听的请求在超时后的下一次请求中又一次出现了,就不用再重新申请RequestData节点了,这样可以继续重复利用前面的RequestData,减少了一次delete和一次new的时间。

     

    • 处理http请求:

    我们知道,一个http请求由这几部分构成:请求行、请求头部、空行、请求body

    如:

    /*例子:用telnet 127.0.0.1 80  写请求如下:
    GET /0606/01.php HTTP/1.1		请求行
    Host: localhost					请求头部
    								//空行分割
    								请求body为空
    */
    /*post方法例子:
    POST /0606/02.php HTTP/1.1
    Host: localhost
    Content-type: application/x-www-form-urlencoded
    Contenr-length: 23				//必须有,告诉服务器我要给你发的请求主体有多少字节
    
    username=zhangsan&age=9			//此处服务器中的资源会被写入两行分别是zhangsan 和9 ,并在响应中返回
    
    */

    那么,我们在对IO线程上epoll监听的fd读取到内容时,应该对读到的内容逐个分析,确定他请求的是啥。

    详细步骤如下:

    void HttpData::handleRead() {
    	__uint32_t &events_ = channel_->getEvents();
    	do {
    		bool zero = false;
    		int read_num = readn(fd_, inBuffer_, zero);		//读完放入应用层缓冲区内
    		LOG << "Request: " << inBuffer_;
    		if (connectionState_ == H_DISCONNECTING) {
    			inBuffer_.clear();			//一个string
    			break;
    		}
    		//cout << inBuffer_ << endl;
    		if (read_num < 0) {
    			perror("1");			//参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
    			error_ = true;
    			handleError(fd_, 400, "Bad Request");
    			break;
    		}
    		else if (zero) {		//读到0字节的话zero会被置为true
    			// 有请求出现但是读不到数据,可能是Request Aborted,或者来自网络的数据没有达到等原因
    			// 最可能是对端已经关闭了,统一按照对端已经关闭处理
    			connectionState_ = H_DISCONNECTING;
    			if (read_num == 0)
    			{
    				//error_ = true;
    				break;
    			}
    			//cout << "readnum == 0" << endl;
    		}
    
    		//走到这里说明是有读到数据的
    		if (state_ == STATE_PARSE_URI) {		//state一开始初值就是STATE_PARSE_URI
    			URIState flag = this->parseURI();	//此函数返回一个URIState,这里进行URI合法性判断
    			if (flag == PARSE_URI_AGAIN)
    				break;
    			else if (flag == PARSE_URI_ERROR) {
    				perror("2");
    				LOG << "FD = " << fd_ << "," << inBuffer_ << "******";
    				inBuffer_.clear();
    				error_ = true;
    				handleError(fd_, 400, "Bad Request");
    			}
    			else
    				state_ = STATE_PARSE_HEADERS;		    //若走到这,说明是读到有用的,将state设为报文头状态STATE_PARSE_HEADERS
    		}
    
    		if (state_ == STATE_PARSE_HEADERS) {		//进行下一步head合法性判断
    			HeaderState flag = this->parseHeaders();
    			if (flag == PARSE_HEADER_AGAIN)
    				break;
    			else if (flag == PARSE_HEADER_ERROR) {
    				perror("3");
    				error_ = true;
    				handleError(fd_, 400, "Bad Request");
    				break;
    			}
    
    			if (method_ = METHOD_POST) {		//进行下一步,请求方法判断
    				// POST方法准备
    				state_ = STATE_RECV_BODY;
    			}
    			else {
    				state_ = STATE_ANALYSIS;
    			}
    		}
    
    		if (state_ == STATE_RECV_BODY) {		//请求方式为post情况
    			int content_length = -1;
    			if (headers_.find("Content-length") != headers_.end()) {		//一个std::map<std::string, std::string> headers_,在别的处理函数中赋值
    				content_length = stoi(headers_["Content-length"]);
    			}
    			else {
    				//cout << "(state_ == STATE_RECV_BODY)" << endl;
    				error_ = true;
    				handleError(fd_, 400, "Bad Request: Lack of argument (Content-length)");
    				break;
    			}
    			if (static_cast<int>(inBuffer_.size()) < content_length)
    				break;
    			state_ = STATE_ANALYSIS;			//也就是说post方法的话要多一步判断content-length
    		}
    
    		if (state_ == STATE_ANALYSIS) {			//进行下一步
    			AnalysisState flag = this->analysisRequest();    //这里对请求的内容进行分析,将响应报文写到outbuffer里去
    			if (flag == ANALYSIS_SUCCESS) {
    				state_ = STATE_FINISH;		//完成工作了
    				break;
    			}
    			else {
    				//cout << "state_ == STATE_ANALYSIS" << endl;
    				error_ = true;
    				break;
    			}
    		}
    	} while (false);
    	//cout << "state_=" << state_ << endl;
    	if (!error_) {
    		if (outBuffer_.size() > 0) {		//输出buf里有内容
    			handleWrite();
    			//events_ |= EPOLLOUT;
    		}
    		// error_ may change
    		if (!error_ && state_ == STATE_FINISH) {		//处理完输出之后
    			this->reset();
    			if (inBuffer_.size() > 0) {					//如果输入buf中还有内容,再调用一次本函数
    				if (connectionState_ != H_DISCONNECTING)
    					handleRead();
    			}
    			// if ((keepAlive_ || inBuffer_.size() > 0) && connectionState_ == H_CONNECTED)
    			// {
    			//     this->reset();
    			//     events_ |= EPOLLIN;
    			// }
    		}
    		else if (!error_ && connectionState_ != H_DISCONNECTED)		//走到这个if说明输入buf中无内容
    			events_ |= EPOLLIN;
    	}
    }

    其中,

    void HttpData::handleWrite() {
    	if (!error_ && connectionState_ != H_DISCONNECTED) {
    		__uint32_t &events_ = channel_->getEvents();
    		if (writen(fd_, outBuffer_) < 0) {		//写,将buf中内容往fd_中写
    			perror("writen");
    			events_ = 0;
    			error_ = true;
    		}
    		if (outBuffer_.size() > 0)		//输出buf中还有内容
    			events_ |= EPOLLOUT;
    	}
    }

     

     

     

    5.日志

    Log的实现了学习了muduo,Log的实现分为前端和后端,前端往后端写,后端往磁盘写。为什么要这样区分前端和后端呢?因为只要涉及到IO,无论是网络IO还是磁盘IO,肯定是慢的,慢就会影响其它操作,必须让它快才行。(其实所谓前后端缓冲区就是AsyncLogging对象的四个缓冲区)

    这里的Log前端是前面所述的IO线程,负责产生log,后端是Log线程,设计了多个缓冲区,负责收集前端产生的log,集中往磁盘写。这样,Log写到后端是没有障碍的,把慢的动作交给后端去做好了。

    后端主要是由多个缓冲区构成的,集满了或者时间到了就向文件写一次。采用了muduo介绍了“双缓冲区”的思想,实际采用4个多的缓冲区(为什么说多呢?为什么4个可能不够用啊,要有备无患)。4个缓冲区分两组,每组的两个一个主要的,另一个防止第一个写满了没地方写,写满或者时间到了就和另外两个交换指针,然后把满的往文件里写。

    与Log相关的类包括FileUtil、LogFile、AsyncLogging、LogStream、Logging。 其中前4个类每一个类都含有一个append函数,Log的设计也是主要围绕这个append函数展开的。

     

    日志的封装从低到高依次为

    FileUtil是最底层的文件类,封装了Log文件的打开、写入并在类析构的时候关闭文件,底层使用了标准IO,该append函数直接向文件写。
    LogFile进一步封装了FileUtil,并设置了一个循环次数,每过这么多次就flush一次。
    AsyncLogging是核心,它负责启动一个log线程,专门用来将log写入LogFile,应用了“双缓冲技术”,其实有4个以上的缓冲区,但思想是一样的。AsyncLogging负责(定时到或被填满时)将缓冲区中的数据写入LogFile中。
    LogStream主要用来格式化输出,重载了 << 运算符,同时也有自己的一块缓冲区,这里缓冲区的存在是为了缓存一行,把多个 << 的结果连成一块。
    Logging是对外接口,Logging类内涵一个LogStream对象,主要是为了每次打log的时候在log之前和之后加上固定的格式化的信息,比如打log的行、文件名等信息。

     

     

    首先在主线程中,在一切工作开始之前有这样一句话:

    Logger::setLogFileName(logPath);

    他做了啥工作呢?设置日志文件的路径。

    好,我们在IO线程中写日志的语句为:

    LOG << "FD = " << fd_ << "," << inBuffer_ << "******";

    我们可以逐层往下看到底是如何写进日志文件的:

    1.程序中用LOG打日志

    logging类中首先有

     #define LOG Logger(__FILE__, __LINE__).stream()

    其中,

    __FILE__,__LINE__,__DATA__,__TIME__ ———— 编译器
    
        C / C++编译器会内置几个宏,这些宏定义可以帮助我们完成跨平台的源码编写,也可以输出有用的调试信息。
    
    ANSI C标准中有几个标准预定义宏(也是常用的):
    __DATE__:在源文件中插入当前的编译日期
    __TIME__:在源文件中插入当前编译时间;
    __FILE__:在源文件中插入当前源文件路径及文件名;
    __LINE__:在源代码中插入当前源代码行号;
    __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
    __cplusplus:当编写C++程序时该标识符被定义。
    
    C里就已经有这些宏,可以用于记录log,测试如下:
    
    cout << "log in "<< __FILE__ << " : " << __FUNCTION__ <<" line: " << __LINE__ << endl;
    // 输出: log in d:\tmp\test\test_sizeof.cpp : test line: 10
    

    好,实际上我们写LOG就是构造了一个Logger对象并调用他的stream()成员函数,这个成员函数做了啥呢?

    2.往前端缓冲区写

    写到下一层去了:

    中间过程不看,有

    struct timeval tv;
    time_t time;
    char str_t[26] = { 0 };
    gettimeofday(&tv, NULL);		//获取当前时间
    time = tv.tv_sec;
    struct tm* p_time = localtime(&time);
    strftime(str_t, 26, "%Y-%m-%d %H:%M:%S\n", p_time);
    stream_ << str_t;				//输出到stream_,一个LogStream对象。(重载了一大堆<<的那个类)

    先把当前时间输出到一个LogStream对象,那么接着看这LogStream对象会做啥:

    他重载了一大堆<<号,还有将任意类型变量转换为string类型的函数 void formatInteger(T);

    	LogStream& operator<<(bool v)
    	{
    		buffer_.append(v ? "1" : "0", 1);
    		return *this;
    	}
    
    	LogStream& operator<<(short);
    	LogStream& operator<<(unsigned short);
    	LogStream& operator<<(int);
    	LogStream& operator<<(unsigned int);
    	LogStream& operator<<(long);
    	LogStream& operator<<(unsigned long);
    	LogStream& operator<<(long long);
    	LogStream& operator<<(unsigned long long);
    
    	LogStream& operator<<(const void*);

    每次我们写LOG的时候,他都会创建一个Logger对象,创建完之后呢会先给他写一些格式化的信息,如时间、调用LOG命令的文件名、在第几行调用的等等,然后追加我们实际想写入的信息如  LOG << "FD = " << fd_ << "," << inBuffer_ << "******";

    然后在此对象析构时往后端缓冲区中写日志:

    3.通过AsyncLogger_往后端缓冲区写

    不禁令人有些疑问,为啥每次创建销毁Logger他都能写到后端的缓冲区里面呢?原来每次append到的目标AsyncLogger_对象被声明为一个静态的全局对象啊。

    static pthread_once_t once_control_ = PTHREAD_ONCE_INIT;
    static AsyncLogging* AsyncLogger_;
    void once_init() {
    	AsyncLogger_ = new AsyncLogging(Logger::getLogFileName());
    	AsyncLogger_->start();			//初始化日志文件名时,开启日志(running_=true)(创建日志线程并开启日志线程)
    }
    
    void output(const char* msg, int len) {
    	pthread_once(&once_control_, once_init);  //保证只做一次once_init,得到欲保存的日志文件地址
    	AsyncLogger_->append(msg, len);    //这个append函数加了锁,保证线程安全性
    }
    
    //构造函数
    Logger::Logger(const char *fileName, int line)
    	: impl_(fileName, line)
    { }
    
    //析构函数
    Logger::~Logger()
    {
    	impl_.stream_ << " -- " << impl_.basename_ << ':' << impl_.line_ << '\n';
    	const LogStream::Buffer& buf(stream().buffer());				//构造Buffer对象
    	output(buf.data(), buf.length());			//往同步对象AsyncLogger_的前端缓冲区写
    }
    

    并且这个对象独占了一个线程,AsyncLogger_->start()就是开启这个前后端同步对象的线程。我们可以看看此线程跑的函数是啥:

    void AsyncLogging::threadFunc() {
    	assert(running_ == true);
    	latch_.countDown();
    	LogFile output(basename_);
    	BufferPtr newBuffer1(new Buffer);			//指向FixedBuffer<4000000>对象的智能指针,其底层是一个char data_[4000000],都是new在堆上的
    	BufferPtr newBuffer2(new Buffer);
    	newBuffer1->bzero();
    	newBuffer2->bzero();
    	BufferVector buffersToWrite;
    	buffersToWrite.reserve(16);
    	while (running_)						//在哪里会改变这个running值?			一旦log线程创建完毕,他就会一直循环(后端往文件里写)
    	{												//初始化日志文件名时,开启日志(running_=true)			(至于前端往后端写是在各个IO线程里)
    		assert(newBuffer1 && newBuffer1->length() == 0);
    		assert(newBuffer2 && newBuffer2->length() == 0);
    		assert(buffersToWrite.empty());
    
    		{
    			MutexLockGuard lock(mutex_);
    			if (buffers_.empty()) // unusual usage!			buffers_一个BufferVector的vector,	
    			{
    				cond_.waitForSeconds(flushInterval_);			//日志为空的话等待几秒钟,此处的条件变量就是锁
    			}
    			buffers_.push_back(currentBuffer_);
    			currentBuffer_.reset();
    
    			currentBuffer_ = std::move(newBuffer1);				//当前前端缓冲区push进后端的待写vector中,并清空
    			buffersToWrite.swap(buffers_);							//拿到后端待写vector
    			if (!nextBuffer_) {
    				nextBuffer_ = std::move(newBuffer2);		//移动拷贝给备用buffer赋值
    			}
    		}
    
    		assert(!buffersToWrite.empty());
    
    		if (buffersToWrite.size() > 25) {
    
    		}
    
    		for (size_t i = 0; i < buffersToWrite.size(); ++i) {
    			// FIXME: use unbuffered stdio FILE ? or use ::writev ?
    			output.append(buffersToWrite[i]->data(), buffersToWrite[i]->length());	//一个LogFile对象
    		}
    
    		if (buffersToWrite.size() > 2)
    		{
    			// drop non-bzero-ed buffers, avoid trashing
    			buffersToWrite.resize(2);
    		}
    
    		if (!newBuffer1) {
    			assert(!buffersToWrite.empty());
    			newBuffer1 = buffersToWrite.back();
    			buffersToWrite.pop_back();
    			newBuffer1.reset();		//reset是将此对象的引用计数-1  ,若引用计数为0,则调用其析构函数
    		}
    
    		if (!newBuffer2)
    		{
    			assert(!buffersToWrite.empty());
    			newBuffer2 = buffersToWrite.back();
    			buffersToWrite.pop_back();
    			newBuffer2->reset();
    		}
    
    		buffersToWrite.clear();
    		output.flush();
    	}
    	output.flush();		//往日志中写,实际上LogFile类包含一个AppendFile对象,此对象的成员函数才是真正往文件中写的。
    }

    我们可以看到,同步对象AsyncLogger_准备了前端缓冲区(用于接收Logger的析构函数写来的日志)  和  后端缓冲区管理数组buffersToWrite(用于append到LogFile对象去),在每此循环中,将前后端的缓冲区通过vector的swap交换。

    for (size_t i = 0; i < buffersToWrite.size(); ++i) {
    	output.append(buffersToWrite[i]->data(), buffersToWrite[i]->length());	//一个LogFile对象
    }

    4.通过LogFile对象往日志文件中write:

    append函数实际上调用顺序: 

    LogFile的append -> LogFile的append_unlocked -> AppendFile的append 
    -> AppendFile的write -> AppendFile的fwrite_unlocked 最终写到缓冲区里面去

    上面那个for循环向LogFile对象append完之后,清空buffersToWrite 并 output.flush();

    即调用了LogFile的flush:

    void LogFile::flush() {
    	MutexLockGuard lock(*mutex_);
    	file_->flush();
    }

    其中,file_ 是他的一个成员对象

    std::unique_ptr<AppendFile> file_;

    即,继续往下调用了AppendFile类的flush():将缓冲区内的数据写到fp_打开的文件中去。

    void AppendFile::flush() {
    	fflush(fp_);	//fflush()会强迫将缓冲区内的数据写回参数stream 指定的文件中. 如果参数stream 为NULL,fflush()会将所有打开的文件数据更新.
    }

    至于这个缓冲区到底是哪个呢?是在AppendFile的构造函数中指定的buffer_。其实就是一个char buffer_[64 * 1024];

    AppendFile::AppendFile(string filename)		//打开传入的文件并指定文件流缓冲区(用户)
    	:fp_(fopen(filename.c_str(), "ae"))
    {
    	// 用户提供缓冲区
    	//在打开文件流后,读取内容之前,调用setbuffer()可用来设置文件流的缓冲区。
    	//参数stream为指定的文件流,参数buf指向自定的缓冲区起始地址,参数size为缓冲区大小。
    	setbuffer(fp_, buffer_, sizeof buffer_);
    }

    ok,至此就完成了一次打印log的调用全过程,其中还有一些细节在这里略去不表。

     

    6.其他

    在wakeup()函数中加上一句打印当前调用此函数的线程id,可以看到:一个有趣的现象是这四个子线程是循环轮流调用wakeup的,这也就是round robin的体现吧。但是这个东西好像有问题,待定

    好,之所以会出现这种情况呢,是因为我当时cout的是当前eventloop的threadId_,而不是currentthread的id,这就造成了调用wakeup的线程是4个IO线程的错觉,其实是主线程轮流wakeup4个IO线程才对。

    thread  6093 calling wake up and write 8 bytes
    thread  6094 calling wake up and write 8 bytes
    thread  6095 calling wake up and write 8 bytes
    thread  6096 calling wake up and write 8 bytes
    thread  6093 calling wake up and write 8 bytes
    thread  6094 calling wake up and write 8 bytes
    thread  6095 calling wake up and write 8 bytes
    thread  6096 calling wake up and write 8 bytes
    thread  6093 calling wake up and write 8 bytes
    thread  6094 calling wake up and write 8 bytes
    thread  6095 calling wake up and write 8 bytes
    thread  6096 calling wake up and write 8 bytes
    thread  6093 calling wake up and write 8 bytes
    thread  6094 calling wake up and write 8 bytes
    
    
    //再用ps -Lf 6092 查看此进程包含的线程,有:
    UID         PID   PPID    LWP  C NLWP STIME TTY      STAT   TIME CMD
    root       6092   6091   6092  0    6 20:21 pts/2    Sl+    0:01 ./WebServer -t 4 -l /home/user/build/release/We
    root       6092   6091   6093  0    6 20:21 pts/2    Sl+    0:00 ./WebServer -t 4 -l /home/user/build/release/We
    root       6092   6091   6094  0    6 20:21 pts/2    Sl+    0:00 ./WebServer -t 4 -l /home/user/build/release/We
    root       6092   6091   6095  0    6 20:21 pts/2    Sl+    0:00 ./WebServer -t 4 -l /home/user/build/release/We
    root       6092   6091   6096  0    6 20:21 pts/2    Sl+    0:00 ./WebServer -t 4 -l /home/user/build/release/We
    root       6092   6091   6102  0    6 20:21 pts/2    Sl+    0:00 ./WebServer -t 4 -l /home/user/build/release/We

     

    一些问题:

    1.为什么多线程与IO情况下我们要选择这种模式:每个fd只由一个线程操作(create1()),同理每个epoll fd的add、del、mod、wait也只由一个线程操作?

    因为多个线程操作同一个socket fd可能出现以下情况:

    a.线程A正阻塞地read()或accept()某个fd,而线程B  close()了此fd

    b.线程AB同时read同一个fd,两个线程几乎同时各自收到一部分数据,如何将数据拼成完整的消息?

    c.线程AB同时read同一个fd,每个线程都只发出去半条消息,那么接收方该如何处理?

    d.线程A正阻塞地wait在epoll上,线程B往此epoll fd添加一个新的fd会发生什么?新fd上的事件会不会在此次wait调用中返回?

     

    2.用RAII包装文件描述符

    linux的文件描述符fd是小整数,在程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这时候如果我们打开一个新的文件,他的fd会是3,因为POSIX标准要求每次打开新文件(包括socket)的时候必须使用当前最小可用的文件描述符号。

        这种分配fd的方式稍不注意就会造成串话,如:一个线程从fd=8收到了比较耗时的请求,他开始处理这个请求,并记住要把响应结果发生给fd=8。但是在处理过程中,fd=8断开连接,被关闭了,又有新的连接到来,碰巧使用了相同的fd=8.当线程完成相应计算,把结果发给fd=8时,接收方已经物是人非,结果难以预料。

        在单线程程序中,或许可以通过某种全局表来避免串话,在多线程程序中,这种做法并不高效(通常意味着每次读写都要对全局表加锁)

        在c++里解决这个问题的办法很简单:RAII。用socket对象包装fd,所有对此fd的读写操作都通过此对象进行,在对象析构函数里关闭fd。这样一来,只要socket对象还活着,就不会有其他的socket对象跟他有一样的fd,也就不可能串话。剩下的问题就是做好多线程中的对象生命周期管理。(在程序中,我们简化了muduo的包装,直接用的业务类HttpData来包装fd,并在析构函数中关闭fd)

     

    3.定时器的作用

    muduo里面的定时器是用来踢掉空闲连接的(超过8s没收到消息),并且使用time wheel实现的。而我们这里,使用一个大根堆管理所有的timernode,每次从堆顶开始删除所有isdeleted(在httpdata处理完read和响应之后)isvalid==false(经历时间超过设好的expiredTime_)的timernode。关闭fd是在httpdata对象的析构函数里面做的,好像跟定时器没啥关系。定时器的作用似乎只是管理了某个httpdata连接时间的长短。。。

    正解:我们看到:在timernode的类中有一个指向httpdata对象的shared_ptr

    当指向httpdatatimernode被析构后(在大根堆里面被一个shared_ptr指了,完成一次while的时候会析构),httpdata的引用计数为0,因此会调用析构函数关闭fd

    小实验:

    	httpdata request;
    	shared_ptr<httpdata> t = make_shared<httpdata>(request);
    	timernode tim(t);
    	cout << "一开始t的引用计数t.use_count=" << t.use_count() << endl;
    
    	t.reset();
    
    	cout << "t.reset(类似临时变量被销毁)之后hp的引用计数tim.hp.use_count=" << tim.hp.use_count() << endl;
    
    	cout << "此时t的引用计数t.use_count=" << t.use_count() << endl;
    	cout << "调用timernode的析构函数时会发生:" << endl;
    	tim.~timernode();

    运行结果:

    4.线程唤醒操作

      如何唤醒的?在将fd填入任务队列时,若发现当前线程currentthread::tid()  != 此eventloop所在的线程的tid时,就往此eventloop的epoll写写一个eventfd,达到唤醒的效果

    还有什么办法能够唤醒线程吗?

        条件变量:但是使用条件变量就不能保证按照round robin的方式来分配新连接了(因为他唤醒有随机性,且至少唤醒一个)

        pipe:传统的办法是用pipe,IO线程始终监视此管道的readable事件,在需要唤醒的时候,其他线程往管道里写一个字节,这                样IO线程就从IO复用阻塞调用中返回(原理类似HTTP long polling)

                  即把管道的读取端fd放入selector,那么在wait的时候这个读取端管道fd也会一起参与wait,那么ui线程往队列里塞完任                     务后,马上往管道的写端写入一个字节,就可以把网络线程唤醒了。

    5.为什么要用非阻塞io

        我们采用了IO multiplexing和non-blocking IO这种模式,原因如下:

    1. 没有人真的会用轮询(busy-pooling)来检查某个非阻塞的IO操作是否完成,这样太浪费CPU cycles。

    2.IO复用机制一般不能和阻塞IO用在一起,因为阻塞IO中read / write / accept / connect都有可能阻塞当前线程,这样线程就没办法处理其他socket上的IO事件了。

     

    6.为什么要有应用层缓冲区input buffer和output buffer

     

    7.为什么要限制最大并发连接数?

        我们这里讨论的“并发连接数”是指一个服务端程序能同时支持的客户端连接数,连接由客户端主动发起,服务端被动接受连接(accept).

    为什么要限制并发连接数?
    一方面,我们不希望服务程序超载,另一方面更因为file descriptor是稀缺资源,如果出现fd耗尽的情况,比较棘手。就跟“malloc()失败抛出bad_alloc差不多棘手”。

    假如有这样一种场景:

    我们创建了一个listenfd挂在epoll上(水平触发形式),当调用epoll_wait获得新连接事件时,用accept去处理listenfd上的事件,但是此时本进程的文件描述符满了,accpet返回EMFILE,无法为新连接创建socket文件描述符。 但是,既然没有socket描述符来表示这个连接,我们也没法close他,程序就继续运行。当再一次epoll_wait时会立即返回(因为新连接还等待处理,listenfd还是可读的),这样的话程序就会陷入busy loop,CPU占用率接近100%。这既影响了同一event loop上的连接,也影响了同一机器上的其他服务。

    该如何解决呢?书上写了几种做法:

    1.调高进程的文件描述符数目(治标不治本)

    2.死等(鸵鸟算法)

    3.退出程序(小题大作)

    4.关闭listenfd(但是什么时候重新打开呢?)

    5.改用edge trigger(如果漏掉了一次accpet,程序就再也不会受到新连接    why?)

    6.准备一个空的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个fd名额,再accept拿到新的socket fd(可读写的),随后理科close他,这样就优雅地断开了客户端的连接,最后再重新打开一个空闲文件,把“坑”占住,以备再次出现这种情况时使用。

    另外还有一种比较简单的做法,fd是hard limit,我们可以自己设置一个稍低一点的soft limit,如果超过 soft limit就主动关闭新连接。比方说当前进程的max fd 是1024,我们可以在连接数达到1000时就进入“拒绝新连接”的状态,这样就可以留给我们足够的腾挪空间。
     

     

     

     

     

     

     

     

     

     

     

    运行:在WebServer目录下 ./build.sh 即可自动编译生成可执行文件,  然后去上级目录中build文件夹内找到可执行文件WebServer即可开启服务器。

    注意,一定要加上sudo,否则是不能使用80端口的,会报Bad file descriptor错误,原因就是创建的listenfd绑定端口错误。

    sudo ./WebServer -t 4 -p 80 -l /home/user/build/release/WebServer/weblog.log
    

     

    测试:使用webbench, 用这个命令即可:webbench -c 2 -t 3 http://127.0.0.1/     如:

    user@ubuntu:~/webbench-1.5$ webbench -c 10 -t 5 http://127.0.0.1/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1/
    10 clients, running 5 sec.
    
    Speed=349056 pages/min, 1384565 bytes/sec.
    Requests: 29088 susceed, 0 failed.
    
    user@ubuntu:~$ webbench -c 1000 -t 30 http://127.0.0.1/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1/
    1000 clients, running 30 sec.
    
    Speed=2475898 pages/min, 9821051 bytes/sec.
    Requests: 1237949 susceed, 0 failed.
    

     

     

    展开全文
  • webserver的压测 之前 最近 总结: 之前写的那个简单聊天室真的太简陋了,毫无技术含量呀。。 写一个epoll+线程池实现的webserver,加入了对http请求的处理,还有定时器,用来处理长时间不活跃的连接。 代码...

    目录

    makefile的编写

    gdb程序调试记录

    调试记录1:

    调试记录2:

    调试记录3:

    webserver的压测

    之前

    最近

    总结:


    之前写的那个简单聊天室真的太简陋了,毫无技术含量呀。。

    写一个epoll+线程池实现的webserver,加入了对http请求的处理,还有定时器,用来处理长时间不活跃的连接。

    代码不好放,还是github好管理一些,这里就写一点心得吧。。。

    两个版本的代码如下:

    https://github.com/cja416/simple_server

    https://github.com/cja416/imitate_muduo_server

    makefile的编写

    :如果要用gdb调试,一定要加上-g

    一开始的最简单版本

    CFLAGS :=-std=c++11 -Wall -O3
    CXXFLAGS :=$(CFLAGS) 
    cc := g++
    webserver: epoll.o requestData.o threadpool.o util.o main.o
    	g++ $(CXXFLAGS) epoll.o requestData.o threadpool.o util.o main.o -o webserver -lpthread
    
    epoll.o:epoll.cpp
    	g++ -c $(CXXFLAGS) epoll.cpp -o epoll.o
    
    requestData.o:requestData.cpp
    	g++ -c $(CXXFLAGS) requestData.cpp -o requestData.o
            
    threadpool.o:threadpool.cpp
    	g++ -c $(CXXFLAGS) threadpool.cpp -o threadpool.o -lpthread
    
    util.o:util.cpp
    	g++ -c $(CXXFLAGS) util.cpp -o util.o
    
    main.o:main.cpp
    	g++ -c $(CXXFLAGS) main.cpp -o main.o
    
    .PHONY:
    clean:
    	rm -rf epoll.o requestData.o threadpool.o util.o main.o
    cleanall: clean
    	rm -rf webserver
    
    

    简化一下:

    CFLAGS :=-std=c++11 -Wall -O3
    CXXFLAGS :=$(CFLAGS)
    cc := g++
    TARGET := webserver
    OBJ := epoll.o requestData.o threadpool.o util.o main.o
    
    $(TARGET): $(OBJ)
            $(cc) $(CXXFLAGS) $(OBJ) -o $(TARGET) -lpthread
    
    #%.o: %.cpp                     这样写找不到%.cpp文件,直接让他隐含规则从.cpp推导出.o文件
    #       $(cc) $(CXXFLAGS) -c %.cpp -o %.o     
    
    .PHONY:
    clean:
            rm -rf *.o
    cleanall: clean
            rm -rf $(TARGET)
    

    再简化一下:

    CFLAGS :=-std=c++11 -Wall -O3
    CXXFLAGS :=$(CFLAGS)
    cc := g++
    
    SOURCE :=$(wildcard *.cpp)
    OBJ := $(patsubst %.cpp,%.o, $(SOURCE))
    
    TARGET := webserver
    #OBJ := epoll.o requestData.o threadpool.o util.o main.o
    
    $(TARGET): $(OBJ)
            $(cc) $(CXXFLAGS) $(OBJ) -o $(TARGET) -lpthread
    
    
    .PHONY:
    clean:
            rm -rf *.o
    cleanall: clean
            rm -rf $(TARGET)

     

    gdb程序调试记录

    调试记录1:

    • 问题:漏打一个  “  !” 引发的服务器无法连接现象

     

    这个是epoll+线程池版本的调试记录,不是仿照muduo的版本

    调试的时候一直有这个问题:

    telnet 127.0.0.1 80  然后 ctrl+],再
    
    GET /home/user/old_version1/index.html HTTP/1.1
    
    一直会
    Connection closed by foreign host.

    同时服务器端:

    1
    error event
    ~requestData()
    ~mytimer

    同理,用curl去连也会出现这个问题

    curl "http://127.0.0.1:80/home/user/old_version1/index.html"
    curl: (56) Recv failure: 连接被对方重设
    

    然后我又用了webbench:连接全是failed。wtf?

    webbench -c 3 -t 5 http://127.0.0.1/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1/
    3 clients, running 5 sec.
    
    Speed=222240 pages/min, 0 bytes/sec.
    Requests: 0 susceed, 18520 failed.
    

    在服务端程序里面,是这样写的:

    if(fd == listen_fd){
    			 //cout << "This is listen_fd" << endl;
    			 acceptConnection(listen_fd,epoll_fd,path);
    }
    else{
    	// 排除错误事件
    	if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP)
    		|| (events[i].events & EPOLLIN))
    	{
    		printf("error event\n");
            delete request;
            continue;
    	}
    			 
        // 将请求任务加入到线程池中
        // 加入线程池之前将Timer和request分离
    	request->seperateTimer();
    }

    现在就是出现了这个情况,可是为啥来的读写事件会是错误的呢?明明在acceptConnection里面已经将accept_fd的事件设置为__uint32_t _epo_event = EPOLLIN | EPOLLET | EPOLLONESHOT;如下

    accept_fd=accept(listen_fd,(struct sockaddr*)&client_addr,&client_addr_len)
    __uint32_t _epo_event = EPOLLIN | EPOLLET | EPOLLONESHOT;
    epoll_add(epoll_fd,accept_fd,static_cast<void*>(req_info),_epo_event);
    
    
    //其中
    int epoll_add(int epoll_fd, int fd, void *request, __uint32_t events){
    	struct epoll_event event;
    	event.data.ptr=request;			//指针用->,实体用.
    	event.events=events;
    	if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,fd,&event)<0){
    		perror("epoll_add error");
    		return -1;
    	}
    	return 0;
    }

    想到一个调试的方法:我查到了events各种事件的位标识,可以在程序中看看来读写请求的时候这个event[i].events有哪几位被置1了,就可以判断他到底触发是啥事件(gdb打个断点)

    注意:gdb要attach一个正在运行的进程,需要用sudo来启动gdb :   sudo gdb    ,  查看进程号:ps -elf | grep 进程名

    如果出现No source file named 或者 没有符号表被读取。请使用 "file" 命令,说明是编译时没有加上-g选项,要再Makefile里面加上,并且make clean ,再重新编译一下,会发现可执行文件变大了许多。

    (gdb) attach 2350                  #绑定正在运行的进程
    Attaching to process 2350
    [New LWP 2351]
    [New LWP 2352]
    [New LWP 2353]
    [New LWP 2354]
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    0x00007ff25efb6a13 in epoll_wait () at ../sysdeps/unix/syscall-template.S:84
    84	../sysdeps/unix/syscall-template.S: 没有那个文件或目录.
    (gdb) l
    79	in ../sysdeps/unix/syscall-template.S
    (gdb) b main.cpp:139            #在main.cpp函数第139行打一个断点
    Breakpoint 1 at 0x40254a: main.cpp:139. (2 locations)
    (gdb) c                        #在另一个会话telnet后,继续运行
    Continuing.
    
    Thread 1 "webserver" hit Breakpoint 1, handle_events (path="/", tp=0x13348f0, events_num=<optimized out>, 
        events=<optimized out>, listen_fd=4, epoll_fd=3) at main.cpp:139
    139			 int fd=request->getFd();
    (gdb) l                #显示源代码
    134	 void handle_events(int epoll_fd, int listen_fd, struct epoll_event* events, int events_num, const string &path, threadpool_t* tp)
    135	 {
    136		 //获取有事件产生的描述符
    137		 for(int i=0;i<events_num;++i){
    138			 requestData* request = (requestData*)(events[i].data.ptr);		//装有活跃事件的数组
    139			 int fd=request->getFd();
    140			 
    141			 
    142			 // 有事件发生的描述符为监听描述符
    143			 if(fd == listen_fd){
    

    上面那个是接受新连接的,接下来处理收到请求:

    (gdb) b main.cpp:147            #再在下面打一个断点
    Breakpoint 2 at 0x402557: main.cpp:147. (2 locations)
    (gdb) c
    Continuing.                #telnet发送具体请求
    
    Thread 1 "webserver" hit Breakpoint 1, handle_events (path="/", tp=0x13348f0, events_num=<optimized out>, 
        events=<optimized out>, listen_fd=4, epoll_fd=3) at main.cpp:139
    139			 int fd=request->getFd();
    (gdb) s                #单步往下走
    requestData::getFd (this=this@entry=0x1335350) at requestData.cpp:117
    117	    return fd;
    (gdb) s
    118	}
    (gdb) 
    handle_events (path="/", tp=0x13348f0, events_num=<optimized out>, events=<optimized out>, listen_fd=4, 
        epoll_fd=3) at main.cpp:143
    143			 if(fd == listen_fd){
    (gdb)                         #重点来了
    
    Thread 1 "webserver" hit Breakpoint 2, handle_events (path="/", tp=0x13348f0, events_num=<optimized out>, 
        events=<optimized out>, listen_fd=4, epoll_fd=3) at main.cpp:150
    150					 || (events[i].events & EPOLLIN))
    
    (gdb) l
    145				 acceptConnection(listen_fd,epoll_fd,path);
    146			 }
    147			 else{
    148				 // 排除错误事件
    149				 if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP)
    150					 || (events[i].events & EPOLLIN))
    151				 {
    152					printf("error event\n");
    153	                delete request;
    154	                continue;
    (gdb) print events[i].events
    value has been optimized out            #  wtf???????????????????    
    

    在使用gdb过程中出现value optimized out,上述情况是由于gcc在编译过程中默认使用-O2优化选项。

    以上情况在循环语句中经常出现,对于希望进行单步跟踪调试时,应使用-O0选项。

    当场爆炸,把优化级别改成O0 重新编译 再来一次:

    (gdb) b main.cpp:147
    Breakpoint 1 at 0x40cd4e: file main.cpp, line 147.
    (gdb) c
    Continuing.
    
    Thread 1 "webserver" hit Breakpoint 1, handle_events (epoll_fd=3, listen_fd=4, events=0x11e0e80, events_num=1, 
        path="/", tp=0x11ef8f0) at main.cpp:149
    149				 if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP)
    (gdb) l
    144				 //cout << "This is listen_fd" << endl;
    145				 acceptConnection(listen_fd,epoll_fd,path);
    146			 }
    147			 else{
    148				 // 排除错误事件
    149				 if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP)
    150					 || (events[i].events & EPOLLIN))
    151				 {
    152					printf("error event\n");
    153	                delete request;
    (gdb) print events[i].events
    $1 = 1                        #check it out bro
    

    我们看看:

    EPOLLIN = 0x001,
     
    EPOLLPRI = 0x002,
     
    EPOLLOUT = 0x004,
    
    EPOLLHUP = 0x010,

    这样来说应该是EPOLLIN啊

    emmm,代码少敲了一个!号

    应该是   !(events[i].events & EPOLLIN)

    看看代码里面:

    $1 = 1
    (gdb) s
    150					 || (events[i].events & EPOLLIN))
    (gdb) s
    152					printf("error event\n");
    

    蓝瘦。

    vim main.cpp
    147G
    加上一个!
    :wq
    make   #只会对有改变的文件进行重新编译链接

    这样一来就对了:

    客户端

    user@ubuntu:~$ telnet 127.0.0.1 80
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    ^]
    telnet> 
    GET /home/user/old_version1/index.html HTTP/1.1
    Accept-Encoding: gzip, deflate, br
    
    HTTP/1.1 404 Not Found!        #响应成功了
    Content-type: text/html
    Connection: close
    Content-length: 116
    
    <html><title>TKeed Error</title><body bgcolor="ffffff">404 Not Found!<hr><em> Web Server</em>
    </body></html>Connection closed by foreign host.
    

    服务端也正确地打出了我加上的调试打印信息:

    this time i read: GET /home/user/old_version1/index.html HTTP/1.1
    Accept-Encoding: gzip, deflate, br
    
    
    parse_uri finished
    try to analysisRequest
    ~requestData()
    

    ok。其实也要感谢粗心的自己,才能多多积攒调试经验。。。

    现在关机都用 sudo init 0  简单快捷

     

    emmm,在muduo版本的webserver上面,用如下请求成功了:

    user@ubuntu:~/build/release/WebServer$ telnet 127.0.0.1 80
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    ^]
    telnet> 
    GET /hello HTTP/1.1
    Accept-Encoding: gzip, deflate, br
    
    HTTP/1.1 200 OK                     #响应报文
    Content-type: text/plain
    
    Hello World                #对请求进行处理的程序中写了,遇到请求文件名为hello的话,就返回这个
    

     

    调试记录2:

    • 问题:状态机解析http请求的逻辑漏洞

     

    telnet时常有错误,也不显示请求失败,直接就Connection closed by foreign host.

    sudo gdb ./webserver

    因为是处理http请求有问题,直接看处理代码(展示部分):

           if (state == STATE_PARSE_URI)
            {
                int flag = this->parse_URI();
    
                if (flag == PARSE_URI_AGAIN)
                {
                    break;
                }
                else if (flag == PARSE_URI_ERROR)
                {
                    perror("2");
                    isError = true;
                    break;
                }
            }
            if (state == STATE_PARSE_HEADERS)
            {
                cout<<"parse_uri finished"<<endl;         //197行
     
                int flag = this->parse_Headers();		//获得一对对的key: value ,装在headers这个map里面
                if (flag == PARSE_HEADER_AGAIN)
                {  
                    break;
                }
                else if (flag == PARSE_HEADER_ERROR)
                {
                    perror("3");
                    isError = true;
                    break;
                }
                if(method == METHOD_POST)
                {
                    state = STATE_RECV_BODY;
                }
                else 
                {
                    state = STATE_ANALYSIS;
                }
            }
            if (state == STATE_RECV_BODY)		//处理post请求,要多处理Content-length
            {
                cout<<"this is a POST request, and  parse_headers finished"<<endl;
    
                int content_length = -1;
                if (headers.find("Content-length") != headers.end())
                {
                    content_length = stoi(headers["Content-length"]);
                }
                else
                {
                    isError = true;
                    break;
                }
                if (content.size() < content_length)		//还没读完本次请求
                    continue;
                state = STATE_ANALYSIS;
            }

    首部已经验证没问题了,所以直接在这里打一个断点。

    b requestData.cpp:197

    然后在另一个窗口telnet一下:

    dl123@hust:~$ telnet 127.0.0.1 80
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    ^]
    telnet> 
    GET /data/chenjinan/old_version/index.html HTTP/1.1

    接着在本窗口继续运行程序(c或者r):就停在断点上了

    (gdb) b requestData.cpp:197
    Breakpoint 2 at 0x404c34: file requestData.cpp, line 197.
    (gdb) r
    The program being debugged has been started already.
    Start it from the beginning? (y or n) y
    Starting program: /data/chenjinan/old_version/webserver 
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    [New Thread 0x7ffff6f4e700 (LWP 24580)]
    [New Thread 0x7ffff674d700 (LWP 24581)]
    [New Thread 0x7ffff5f4c700 (LWP 24582)]
    [New Thread 0x7ffff574b700 (LWP 24583)]
    requestData constructed !
    1
    1
    ~mytimer
    this time i read: GET /data/chenjinan/old_version/index.html HTTP/1.1
    
    request_line find, method = 2
    file_name: data/chenjinan/old_version/index.html
    http version: 2
    [Switching to Thread 0x7ffff6f4e700 (LWP 24580)]
    
    Thread 2 "webserver" hit Breakpoint 2, requestData::handleRequest (this=0x635350) at requestData.cpp:197
    197                 cout<<"parse_uri finished"<<endl;

    用s(执行一步并进入函数)或者n(执行一步但不进入函数)来跟踪执行,过程中用p + 变量名 查看变量值,发现问题

    在解析headers的时候,部分逻辑是这样的:

     for (int i = 0; i < str.size() && notFinish; ++i)
        {
            switch(h_state)
            {
                case h_start:
                {
                    if (str[i] == '\n' || str[i] == '\r')
                        break;                //这个地方直接break了
                    h_state = h_key;
                    key_start = i;
                    now_read_line_begin = i;
                    break;
                }
                case h_key:
                {
                    if (str[i] == ':')
                    {
                        key_end = i;
                        if (key_end - key_start <= 0)
                            return PARSE_HEADER_ERROR;
                        h_state = h_colon;
                    }
                    else if (str[i] == '\n' || str[i] == '\r')
                        return PARSE_HEADER_ERROR;
                    break;  
                }

    由于我用telnet请求的时候一直没有发送header部分(为空行),只有请求行部分,因此进入requestData::parse_Headers()函数的时候h_state 始终为 h_start ,这就导致了结束部分的返回逻辑:

      if (h_state == h_end_LF)        //只有正常走到h_state == h_end_LF才会返回成功
        {
            str = str.substr(now_read_line_begin);
            return PARSE_HEADER_SUCCESS;
        }
        str = str.substr(now_read_line_begin);
        return PARSE_HEADER_AGAIN;

    只有当h_state == h_end_LF时才会 return PARSE_HEADER_SUCCESS; 否则一直 return PARSE_HEADER_AGAIN;,所以请求一直处理不了,这就是解析的逻辑问题了,改为这样:

            case h_start:
    404             {
    405                 if (str[i] == '\n' || str[i] == '\r'){
    406                     h_state = h_end_LF;        //请求headers为空行的情况也能继续
    407                     break;
    408                 }
    409                 h_state = h_key;
    410                 key_start = i;
    411                 now_read_line_begin = i;
    412                 break;
    413             }

    就ok。

    但是仍有问题:

    telnet>                 #注意一定要先敲一个回车才能输入请求,否则会报命令无效
    GET /data/chenjinan/old_version/index.html HTTP/1.1
    HTTP/1.1 404 Not Found!
    Content-type: text/html
    Connection: close
    Content-length: 114
    
    <html><title>TKeed Error</title><body bgcolor="ffffff">404 Not Found!<hr><em> cja's Web Server</em>
    </body></html>

    一直找不到index.html文件。一样,打断点看看是啥问题

    (gdb) b requestData.cpp:563
    Breakpoint 3 at 0x4063f1: file requestData.cpp, line 563.
    (gdb) c
    Continuing.
    。。。省略。。。
    [Switching to Thread 0x7ffff5f4c700 (LWP 24582)]
    
    Thread 4 "webserver" hit Breakpoint 3, requestData::analysisRequest (this=0x635350) at requestData.cpp:563
    563             int dot_pos = file_name.find('.');
    
    #报这样的错误
    572                 perror("stat file fail.");
    (gdb) 
    epoll wait error: Interrupted system call
    -1
    stat file fail.: No such file or directory

    把index.html拷贝到 /目录下,再执行 GET /index.html HTTP/1.1就可以了

    dl123@hust:~$ telnet 127.0.0.1 80 
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    ^]
    telnet> 
    GET /index.html HTTP/1.1
    HTTP/1.1 200 OK        #正确的响应报文
    Content-type: 
    Content-length: 13
    
    Hello World !

    那么问题来了,为啥放在/data/chenjinan/old_version/index.html 里面的就 stat 不到?是权限问题吗

    查看错误码的位置:

      首先在自己的程序中#include<errno.h>
      添加打印errno的语句 printf("errno is: %d\n",errno);
      根据errno的值查错。
      errno的不同值的含义:
      在linux 2.4.20-18的内核代码中的/usr/include/asm/errno.h

          

     

     

    调试记录3:

    • 问题:智能指针的reset和其指向对象的reset,到底是调用哪个?

     

    跑程序总是卡在这个assert上:其中newBuffer1 是指向buffer对象的shared_ptr

     assert(newBuffer1 && newBuffer1->length() == 0);
     assert(newBuffer2 && newBuffer2->length() == 0);

    表现如下

    2: Resource temporarily unavailable
    HTTP/1.1 400 Bad Request
    Content-Type: text/html
    Connection: Close
    Content-Length: 107
    Server: My Web Server
    
    <html><title>title><body bgcolor="ffffff">400 Bad Request<hr><em> My Web Server</em></body></html>
    webserver: AsyncLogging.cpp:54: void AsyncLogging::threadFunc(): Assertion `newBuffer1 && newBuffer1->length() == 0' failed.
    
    Thread 6 "Logging" received signal SIGABRT, Aborted.

    不是很科学,每次都把newBuffer1的内容move走了啊:为啥还会length==0报错

    {
         MutexLockGuard lock(mutex_);
         if (buffers_.empty())  
          {
             cond_.waitForSeconds(flushInterval_);       
          }
           buffers_.push_back(currentBuffer_);
           currentBuffer_.reset();
    
           currentBuffer_ = std::move(newBuffer1);         //这里
           buffersToWrite.swap(buffers_);   
           if (!nextBuffer_) {
                 nextBuffer_ = std::move(newBuffer2);   
               }
    }
    

     

    ------------------------------------------------------------------------

    插一段:std::move之后指针会变成啥:

    	A a;
    	shared_ptr<A>s1 = make_shared<A>(a);
    	cout << "s1: " << s1 << ", use_count: "<<  s1.use_count() << endl;
    	shared_ptr<A>s2 = move(s1);
    	cout << "s1: " << s1 << ", use_count: " << s1.use_count() << endl;
    	cout << "s2: " << s2 << ", use_count: " << s2.use_count() << endl;

    可以看到,s1就变成空指针了,指向NULL

    -------------------------------------------------------------------------

     

    看看:经过一轮写日志之后,有这样一个操作

    对他变空指针的情况做了处理:

     if (!newBuffer1)
            {
                assert(!buffersToWrite.empty());
                newBuffer1 = buffersToWrite.back();
                buffersToWrite.pop_back();
                newBuffer1->reset();
            }
    99   newBuffer1 = buffersToWrite.back();
    (gdb) n
    
    100    buffersToWrite.pop_back();
    (gdb) p newBuffer1
    $3 = std::shared_ptr<FixedBuffer<4000000>> (use count 2, weak count 0) = {get() = <optimized out>}
    (gdb) n
    
    101    newBuffer1.reset();             //reset-1  ,0
    (gdb) p newBuffer1
    $4 = std::shared_ptr<FixedBuffer<4000000>> (empty) = {get() = 0x0}

    newBuffer1.reset()之前是有的,reset之后就变为empty了。

    其中

    void reset() { cur_ = data_; }
    
    char data_[SIZE];
    char* cur_;
    
    //在LogStream.h中

     

    第二次进入循环来到这里,发现newBuffer1和newBuffer2不一样了,newBuffer1指针是空的

        assert(newBuffer1 && newBuffer1->length() == 0);
        assert(newBuffer2 && newBuffer2->length() == 0);
        assert(buffersToWrite.empty());
    (gdb) p newBuffer1
    $6 = std::shared_ptr<FixedBuffer<4000000>> (empty) = {get() = 0x0}
    (gdb) p newBuffer1->length()
    Attempt to take address of value not located in memory.
    (gdb) p newBuffer2
    $7 = std::shared_ptr<FixedBuffer<4000000>> (use count 1, weak count 0) = {get() = <optimized out>}
    (gdb) p newBuffer1
    $8 = std::shared_ptr<FixedBuffer<4000000>> (empty) = {get() = 0x0}

    此时再assert的话:发现问题出在哪了。

    webserver: AsyncLogging.cpp:54: void AsyncLogging::threadFunc(): Assertion `newBuffer1 && newBuffer1->length() == 0' failed.
    
    Thread 6 "Logging" received signal SIGABRT, Aborted.

     

    猜测问题:
    上面说到newBuffer1.reset()之前是有的,reset之后就变为empty了。

    但是我自己写的reset是:(buffer类中函数)

    void reset() { cur_ = data_; }

    会不会是调成了shared_ptr的reset了(标准库函数):

    void reset() noexcept
    {	// release resource and convert to empty shared_ptr object
    	shared_ptr().swap(*this);
    }

    导致reset之后并不是起到将newBuffer1->length设置为0,而是将newBuffer1指针置NULL。导致每次重进循环的时候都abort。

    --------------------------------------------------------

    插播小实验: 用智能指针调用reset会发生啥

    	void reset() {        //A类中新增一个reset函数
    		cout << "A's reset" << endl;
    	}
    
            A a;
    	shared_ptr<A>s1 = make_shared<A>(a);
    	cout << "s1: " << s1 << ", use_count: "<<  s1.use_count() << endl;
    	shared_ptr<A>s2 = move(s1);
    	cout << "s1: " << s1 << ", use_count: " << s1.use_count() << endl;
    	cout << "s2: " << s2 << ", use_count: " << s2.use_count() << endl;
            s2->reset();
    	cout << "s2: " << s2 << ", use_count: " << s2.use_count() << endl;
    	s2.reset();
    	cout << "s2: " << s2 << ", use_count: " << s2.use_count() << endl;
    

    结果: 走的是shared_ptr的reset。

    找到问题后:

    --------------------------------------------------------

    我发现问题了:仔细看

    		if (!newBuffer1) {
    			assert(!buffersToWrite.empty());
    			newBuffer1 = buffersToWrite.back();
    			buffersToWrite.pop_back();
    			newBuffer1.reset();		//reset是将此对象的引用计数-1  ,若引用计数为0,则调用其析构函数
    		}
    
    		if (!newBuffer2)
    		{
    			assert(!buffersToWrite.empty());
    			newBuffer2 = buffersToWrite.back();
    			buffersToWrite.pop_back();
    			newBuffer2->reset();
    		}

    之所以每次出错的都是newBuffer1,是因为newBuffer1调用reset是用.reset()形式,这样调用的是智能指针自己的reset,而newBuffer2是用的->reset()来调用的,这样调用的是buffer对象的reset,才是正确的。

    ok

     

    客户端向服务器发送请求得不到响应的排查步骤:

         消息接收分为几个步骤,即客户端是否发送请求、服务器是否发送消息、客户端是否收到消息、客户端是否分发了消息、是否有消息处理函数以及其是否被正确调用。

    ping一下网络、查看代码中返回值和是否抛出异常、抓包、查看端口数是否用完、trace route排查路由、是否进行端口映射等

     

     

    webserver的压测

    之前

    类muduo版本的压测:

    top -Hp pid 可以查看指定进程的线程

     

    用webbench开1000客户端,跑30s

    user@ubuntu:~$ webbench -c 1000 -t 30 http://127.0.0.1:80/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    1000 clients, running 30 sec.
    
    Speed=1180672 pages/min, 4683328 bytes/sec.
    Requests: 590336 susceed, 0 failed.
    

    request全部成功。我们看到,居然是log占用最多的cpu。

    其实一开始是这样的

    只是在一段时间后,业务处理完了,就都开始往后端写日志,才会出现log占用大量cpu的情况。

     

    2.客户端加到2000居然也还没崩

    3.有内存泄露

    USER        PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
    root       1064  0.0  0.0  17676  1644 tty1     Ss+  19:33   0:00 /sbin/agetty --noclear tty1 linux
    root       1080  0.0  1.8 237028 40092 tty7     Ss+  19:33   0:00 /usr/lib/xorg/Xorg -core :0 -seat seat0 -auth 
    user       1772  0.0  0.2  24576  5740 pts/8    Ss   19:34   0:00 -bash
    user       3066  0.0  0.2  24252  5288 pts/9    Ss   19:46   0:00 -bash
    user       3166  0.0  0.2  24252  5276 pts/10   Ss   19:55   0:00 -bash
    root       3254  0.0  0.1  56608  4152 pts/8    S+   20:27   0:00 sudo ./WebServer -l /home/user/build/release/l
    root       3255 24.7  2.4 405716 53892 pts/8    Sl+  20:27   6:14 ./WebServer -l /home/user/build/release/log.lo
    user       4504  0.0  0.1  43408  3732 pts/10   S+   20:41   0:00 top -Hp 3255
    user      26886  0.0  0.1  39104  3512 pts/9    R+   20:52   0:00 ps -au
    

    而且现在不管怎么webbench他,都不动了

    失败的情况:

    user@ubuntu:~$ webbench -c 50 -t 40 http://127.0.0.1:80/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    50 clients, running 40 sec.
    
    Speed=0 pages/min, 0 bytes/sec.
    Requests: 0 susceed, 0 failed.
    

    包括telnet也没反应了。。。应该是文件描述符被用光了,或者是timewait端口太多了?或者是webbench创建进程数达到上限了?要好好查一查啊

    就很气,一直是这样

    dl123@hust:~$ telnet 127.0.0.1 80 
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    GET /data/index.html HTTP/1.1        //往里面放一层就不行了
    HTTP/1.1 404 Not Found!
    Content-type: text/html
    Connection: close
    Content-length: 114
    
    <html><title>TKeed Error</title><body bgcolor="ffffff">404 Not Found!<hr><em> cja's Web Server</em>
    </body></html>Connection closed by foreign host.
    
    
    dl123@hust:~$ telnet 127.0.0.1 80 
    Trying 127.0.0.1...
    Connected to 127.0.0.1.
    Escape character is '^]'.
    GET /index.html HTTP/1.1            //根目录就可以
    HTTP/1.1 200 OK
    Content-type: 
    Content-length: 13
    
    Hello World !

     

     

     

     

    上方是很早之前在普通机器上跑的时候做的测试。

    下方是在服务器机器上跑的。

    最近

    查看当前系统使用的打开文件符数

       $ cat /proc/sys/fs/file-nr

       5664   0   186405

       其中第一个数表示当前系统已分配使用的打开文件描述符数,第二个数为分配后已释放的(目前已不再使用),第三个数等于    file-max 

    查看已连接数量:

    netstat -nat|grep ESTABLISHED|wc -l

     

    开始:

    webbench命令:

    dl123@hust:/data/chenjinan/webbench-1.5$ webbench -c 10 -t 30 http://127.0.0.1:80/  
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    10 clients, running 30 sec.
    
    Speed=4022696 pages/min, 15956690 bytes/sec.
    Requests: 2011348 susceed, 0 failed.

    cpu使用情况:

    这个时候,有很多time-wait状态的连接。过了2MSL就会消失的,即大概1.5min。

    我们知道,time-wait状态是tcp四次挥手中主动断开连接的一方才会有的状态,由于我跑webbench也是在本机,所以才会有,否则是在客户端机器上才会有的。

    dl123@hust:~$ netstat -nat|grep TIME | wc -l
    14112

     

    逐渐增大压力:

    dl123@hust:/data/chenjinan/webbench-1.5$ webbench -c 100 -t 30 http://127.0.0.1:80/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    100 clients, running 30 sec.
    
    Speed=4272254 pages/min, 16946612 bytes/sec.
    Requests: 2136127 susceed, 0 failed.

    还是没有失败的请求。

     

    再多一点:

    可以看到,已经开始有失败的出现了

    dl123@hust:/data/chenjinan/webbench-1.5$ webbench -c 1000 -t 30 http://127.0.0.1:80/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    1000 clients, running 30 sec.
    
    Speed=4153054 pages/min, 16473741 bytes/sec.
    Requests: 2076525 susceed, 2 failed.

    cpu使用情况:

    timewait状态:

    dl123@hust:~$ netstat -nat|grep TIME | wc -l 
    13199

    这个时候,日志文件已经很大了:达到了2G,因为每次新连接和http请求都会打日志,导致日志打的真的猛,其实应该只打一些错误信息的哦

    -rw-r--r-- 1 root  root  2118862159 8月   4 11:11 WebServer.log

     

    这次,把时间调大一些:有7个请求fail了

    dl123@hust:/data/chenjinan/webbench-1.5$ webbench -c 1000 -t 60 http://127.0.0.1:80/   
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    1000 clients, running 60 sec.
    
    Speed=4158905 pages/min, 16496914 bytes/sec.
    Requests: 4158898 susceed, 7 failed.

    cpu使用情况都差不多

    最后跑一个:

    dl123@hust:/data/chenjinan/webbench-1.5$ webbench -c 2000 -t 60 http://127.0.0.1:80/ 
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    2000 clients, running 60 sec.
    
    Speed=4002167 pages/min, 15875150 bytes/sec.
    Requests: 4002157 susceed, 10 failed.

    有10个请求failed

     

    为了搞清楚请求fail的原因,今天又做了一次实验(在另外一台机器上,性能可能不同但是不影响)

    ubuntu@VM-0-12-ubuntu:~$ webbench -t 60 -c 1000 http://127.0.0.1:80/  
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    1000 clients, running 60 sec.
    
    Speed=540189 pages/min, 2142495 bytes/sec.
    Requests: 540126 susceed, 63 failed.

    复现了失败的场景,结果日志里没有打错误的信息。。。

    当我加了好多各种情况下的错误信息并重新编译运行测试,却怎么也复现不了上面那个场景了。。。各种参数都试过了

    日志都打了2G还是没有复现场景

    -rw-r--r-- 1 root root 2.1G Oct 13 11:01 log.log

    ubuntu@VM-0-12-ubuntu:~$ webbench -c 1000 -t 60 http://127.0.0.1:80/
    Webbench - Simple Web Benchmark 1.5
    Copyright (c) Radim Kolar 1997-2004, GPL Open Source Software.
    
    Benchmarking: GET http://127.0.0.1:80/
    1000 clients, running 60 sec.
    
    Speed=534417 pages/min, 1941711 bytes/sec.
    Requests: 534417 susceed, 0 failed.

    在webbench期间,查看Webserver打开的文件数如下(启动1000个客户端的情况,若是启动1500个,大概是1515。因为空载时打开的文件数就是15):

    ubuntu@VM-0-12-ubuntu:~$ sudo ls -l /proc/31923/fd | wc -l
    1013

    而我设置的进程打开最大文件数是65535,所以问题应该不在文件描述符:

    ubuntu@VM-0-12-ubuntu:~$ ulimit -a
    core file size          (blocks, -c) unlimited
    data seg size           (kbytes, -d) unlimited
    scheduling priority             (-e) 0
    file size               (blocks, -f) unlimited
    pending signals                 (-i) 7080
    max locked memory       (kbytes, -l) 16384
    max memory size         (kbytes, -m) unlimited
    open files                      (-n) 65535
    pipe size            (512 bytes, -p) 8
    POSIX message queues     (bytes, -q) 819200
    real-time priority              (-r) 0
    stack size              (kbytes, -s) 8192
    cpu time               (seconds, -t) unlimited
    max user processes              (-u) 7080
    virtual memory          (kbytes, -v) unlimited
    file locks                      (-x) unlimited

    此过程中tcp各种状态:

    ubuntu@VM-0-12-ubuntu:~$ netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
    LISTEN 4
    ESTABLISHED 3905
    TIME_WAIT 8310

    TIME-WAIT始终保持在8000出头的数量,应该也不会占光端口数65535(之所以会占端口数,是因为我客户端也是在本机跑的,创建客户端socket时要随机分配一个端口号给他)(tcp udp的端口号是用short表示的,因此就是65535个)

    这里就引出一个小问题:6万多个端口的服务器怎么承接百万级别的客户端连接呢?我们知道服务器与每个客户端连接是会新建一个可读写的socket的,会占用一个端口

    解决方法:

        我们知道一个tcp连接socket对可用4元组表示:{源IP地址,源端口,目的IP地址,目的端口}

        目的ip地址、目的端口我们无法改变(客户端侧),那么我们只能从源侧下手。而端口号不足正是我们要解决的问题,所以只能从源IP下手:

    增加IP地址,一般假设本机网卡名称为 eth0,那么手动再添加几个虚拟的IP

    ifconfig eth0:1 192.168.190.151 
    ifconfig eth0:2 192.168.190.152 ......

    假设系统可以使用的端口范围为(1024~65535),那么可以使用的大致端口为64000个,系统添加了10个IP地址,那么可以对外发出的数量为 64000 * 10 = 640000,数量很可观。

     

     

     

     

    总结:

    整体来说性能还是很不错的:

    • 在2000个客户端,60s请求的情况下,成功的请求数4M,失败请求数10, 15M字节每秒的传输速率
    • cpu使用率也较为平衡,1个accept线程+4个工作线程占用率都在90%左右,有点高了
    • 日志系统cpu占用率不高,只有2%左右
    • 内存占用总共0.3%左右,应该没有内存泄漏的情况发生

    之前在普通机器上测过一次,发现连续两次 1000 client + 30s  就再也不能接受新的请求了,现在想想应该是忘记调用户的最大打开文件描述符个数了,默认1024个,这次调为65535个,再没出现那种情况。端口用尽?1024~65535

    过程中用到的命令:

    #启动server
    sudo ./WebServer -t 4 -p 80 -l /data/chenjinan/WebServer/build/release/WebServer/weblog.log
    
    #使用webbench
    webbench -c 5000 -t 60 http://127.0.0.1:80/ 
    
    #查看进程的情况
    ps -elf | grep Web
    top -H -p 7460
    
    #查看连接的情况
    netstat -nat|grep TIME | wc -l 
    sudo netstat -anpt
    sudo netstat -pltn | grep pid
    
    #查找文件
    find -name log
    
    #查看当前系统使用的打开文件符数
    cat /proc/sys/fs/file-nr
    
    #查看系统各个限制
    ulimit -a
    
    #查看进程打开的文件数
    sudo ls -l /proc/30035/fd | wc -l    #其中30035是pid
    
    #或者用lsof
    lsof -c pname
    lspf -u username
    
    #抓包
    tcpdump -i eth0 ip地址
    
    #查看各种tcp连接状态数量
    netstat -ant|awk '/^tcp/ {++S[$NF]} END {for(a in S) print (a,S[a])}'
    
    #监控机器
    vmstat dstat

     

     

     

    git管理代码

    首先将本机的公钥放到github的账户上

    ssh-keygen -t rsa -C "youremail@example.com"

    你需要把邮件地址换成你自己的邮件地址,然后一路回车,使用默认值即可,由于这个Key也不是用于军事目的,所以也无需设置密码。

    如果一切顺利的话,可以在用户主目录里找到.ssh目录,里面有id_rsaid_rsa.pub两个文件,这两个就是SSH Key的秘钥对,id_rsa是私钥,不能泄露出去,id_rsa.pub是公钥,可以放心地告诉任何人。

    第2步:登陆GitHub,打开“Account settings”,“SSH Keys”页面:

    然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容

     

     

    一开始在本地机器创建文件夹simple_server

    cd 到文件夹里,git init 将他设为git仓库

    接着, 将文件夹下的文件都 git add 到仓库里

    git commit -m “描述” 可以提交到分支

    git remote add origin git@server-name:path/repo-name.git(自己的github的一个仓库地址) 将本地仓库与远程仓库绑定

    git push origin master 提交到远程仓库去,要输入帐号和密码的

    还有,

    git checkout 分支名 可以切换到分支

    git checkout -b 分支名  新建分支并切换到分支

    git branch 查看分支情况

    git merge dev 将分支dev合并到当前分支

     

    git clone git@github.com:michaelliao/gitskills.git 从远程库克隆

     

    使用命令修改git的用户名和提交的邮箱

             如果你要修改当前全局的用户名和邮箱时,需要在上面的两条命令中添加一个参数,--global,代表的是全局。

             命令分别为:git config  --global user.name 你的目标用户名;

                                  git config  --global user.email 你的目标邮箱名;

     

    当push时

    提示:更新被拒绝,因为远程版本库包含您本地尚不存在的提交。这通常是因为另外提示:一个版本库已向该引用进行了推送。再次推送前,您可能需要先整合远程变更提示:(如 'git pull ...')。提示:详见 'Git push --help' 中的 'Note about fast-forwards' 小节。

    原因可能是之前上传时创建的.git文件被删除或更改,或者其他人在github上提交过代码.
    解决方案如下:1.强行上传   git push -u origin +master
           2. 尽量先同步github上的代码到本地,在上面更改之后再上传

    展开全文
  • MFB项目收获

    2014-03-20 14:39:43
    redmine是一个基于web项目管理软件。可以实时mail项目进度 2、使用svn进行代码管理 3、apache服务器+php+postgresql(zend) 4、javascript+css jQuery Mobile 是一个为触控优化的框架,用于创建移动 web ...
  • 第一次做web项目购物网站项目总结

    千次阅读 2018-11-13 22:46:22
    项目背景:学习完java基础后的第一次做项目,前期老师带着做,...第一次做web项目,看似很简单的流程,却做了好长时间,哪怕前期有老师带着 项目收获 通过做项目,对于web的开发有了一定的了解,也了解到了,以前浏...
  • Web应用开发技术经验收获 jsp程序的部署 1、在项目文件夹下创建WEB-INF文件夹,在WEB-INF创建classes、libs文件夹和web.xml文件,classes文件夹中存放编译好的.class文件,libs中存放用到的jar包,jsp文件和html...
  • 每个学习过前端的学习者来说,都接触过web项目的实战,业内最真实的的,应该是自己独立编写一个网页项目,当然如果你是大佬的话,这只是小菜一碟,而如果你是小白或正在学习该部分知识的人,这一小节你可以融会贯通...
  • 非常好的rest前后端分离项目 企业开发目前主流所选的模式 如果能看懂并整合自己的项目会有巨大的收获
  • 最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整Java Web项目的同学一个...
  • 最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整Java Web项目的同学一个...
  • Python+Django创建web项目详细步骤

    千次阅读 2018-09-02 22:44:24
    本文基于学习《Python编程:从入门到精通》这本书,总结一下Django创建web项目的步骤。 软件环境:win10+Anaconda 1.安装virtualenv 打开Anaconda prompt,输入命令:pip virtualenv 2.创建虚拟环境 在桌面创建...
  • 一个完整的Java Web项目需要掌握哪些技术

    万次阅读 多人点赞 2017-12-19 07:57:18
    最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整java Web项目的同学一个...
  • 刚刚学习网站的可以试试哦,真的是一个让人很容易理解的web项目,新手上手快也希望你能够有所收获
  • 这两天一直在研究java web项目的发布,总是遇到各种各样的bug,也在不断的解决这些问题,现在就来总结一下这两天的收获。 开发环境:IDEA 2018.2.2 流程: 首先创建web 项目: 按照图示勾选即可 自定义项目...
  • 最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整Java Web项目的同学一个...
  • 续:PHP做Web项目的优缺点

    千次阅读 2005-11-04 17:02:00
    偶写的《PHP做Web项目的优缺点document.title="PHP做Web项目的优缺点 - "+document.title 》一文被放到了CSDN的首页上,我也不知道怎么回事,当然,也收获了很多批评很很多比较有见解性的意见,比如Easy写的《不一样...
  • Java-web项目经验小结

    千次阅读 热门讨论 2018-03-30 21:29:29
    作为组长总结一下本次项目收获。 前后端分离的项目,后端使用打包之后放在Tomcat容器中就可以了。但是前端由于是第一次使用Angular新框架,使用Nginx代理,这可谓是真正的做到了前后端分离。1、后端部署 1)执行...
  • 有时候,承认自己笨并不是坏事,可怕的是,明知自己不懂还自暴自弃 1、 2、 3、 4 5 6 正常不会有错,如果有错的话,百度应该可以解决 ...越是经历挫折,越是不能放弃,努力就一定有收获,加油 ...
  • 谈谈第一次Web项目的感想(上)

    千次阅读 2017-01-01 06:51:31
     这次也仅仅是谈一下,自己对于整个Web项目的感想,收获,和不足。  需求分析阶段,由于没有客户,所以所谓的需求分析会议并没有开起来,需求分析是由小组几个开发者自己讨论,需求记录暂且不谈,整个阶段最大...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 643
精华内容 257
关键字:

web项目收获