Chinaunix首页 | 论坛 | 博客
  • 博客访问: 429643
  • 博文数量: 58
  • 博客积分: 587
  • 博客等级: 中士
  • 技术积分: 710
  • 用 户 组: 普通用户
  • 注册时间: 2012-04-25 11:37
文章分类

全部博文(58)

文章存档

2014年(4)

2013年(32)

2012年(22)

分类: 网络与安全

2013-01-18 19:51:24

1、首先需要一个内存池,目的在于:
·减少频繁的分配和释放,提高性能的同时,还能避免内存碎片的问题;
·能够存储变长的数据,不要很傻瓜地只能预分配一个最大长度;
·基于SLAB算法实现内存池是一个好的思路:分配不同大小的多个块,请求时返回大于请求长度的最小块即可,对于容器而言,处理固定块的分配和回收,相当容易实现。当然,还要记得需要设计成线程安全的,自旋锁比较好,使用读写自旋锁就更好了。
· 分配内容的增长管理是一个问题,比如第一次需要1KB空间,随着数据源源不断的写入,第二次就需要4KB空间了。扩充空间容易实现,可是扩充的时候必然涉 及数据拷贝。甚至,扩充的需求很大,上百兆的数据,这样就不好办了。暂时没更好的想法,可以像STL一样,指数级增长的分配策略,拷贝数据虽不可避免,但 是起码重分配的几率越来越小了。
·上面提到的,如果是上百兆的数据扩展需要,采用内存映射文件来管理是一个好的办法:映射文件后,虽然占了很大的虚拟内存,但是物理内存仅在写入的时候才会被分配,加上madvice()来加上顺序写的优化建议后,物理内存的消耗也会变小。
·用string或者vector去管理内存并不明智,虽然很简单,但服务器软件开发中不适合使用STL,特别是对稳定性和性能要求很高的情况下。

2、第二个需要考虑的是对象池,与内存池类似:
·减少对象的分配和释放。其实C++对象也就是struct,把构造和析构脱离出来手动初始化和清理,保持对同一个缓冲区的循环利用,也就不难了。
·可以设计为一个对象池只能存放一种对象,则对象池的实现实际就是固定内存块的池化管理,非常简单。毕竟,对象的数量非常有限。

3、第三个需要的是队列:
·如果可以预料到极限的处理能力,采用固定大小的环形队列来作为缓冲区是比较不错的。一个生产者一个消费者是常见的应用场景,环形队列有其经典的“锁无关”算法,在一个线程读一个线程写的场景下,实现简单,性能还高,还不涉及资源的分配和释放。好啊,实在是好!
·涉及多个生产者消费者的时候,tbb::concurent_queue是不错的选择,线程安全,并发性也好,就是不知道资源的分配释放是否也管理得足够好。

4、第四个需要的是映射表,或者说hash表:
·因为epoll是事件触发的,而一系列的流程可能是分散在多个事件中的,因此,必须保留下中间状态,使得下一个事件触发的时候,能够接着上次处理的位置继续处理。要简单的话,STL的hash_map还行,不过得自己处理锁的问题,多线程环境下使用起来很麻烦。
·多线程环境下的hash表,最好的还是tbb::concurent_hash_map。

5、核心的线程是事件线程:
·事件线程是调用epoll_wait()等待事件的线程。例子代码里面,一个线程干了所有的事情,而需要开发一个高性能的服务器的时候,事件线程应该专注于事件本身的处理,将触发事件的socket句柄放到对应的处理队列中去,由具体的处理线程负责具体的工作。

6、accept()单独一个线程:
·服务端的socket句柄(就是调用bind()和listen()的这个)最好在单独的一个线程里面做accept(),阻塞还是非阻塞都无所谓,相比整个服务器的通讯,用户接入的动作只是很小一部分。而且,accept()不放在事件线程的循环里面,减少了判断。

7、接收线程单独一个:
·接收线程从发生EPOLLIN事件的队列中取出socket句柄,然后在这个句柄上调用recv接收数据,直到缓冲区没有数据为止。接收到的数据写入以socket为键的hash表中,hash表中有一个自增长的缓冲区,保存了客户端发过来的数据。
·这样的处理方式适合于客户端发来的数据很小的应用,比如HTTP服务器之类;假设是文件上传的服务器,则接受线程会一直处理某个连接的海量数据,其他客户端的数据处理产生了饥饿。所以,如果是文件上传服务器一类的场景,就不能这样设计。

8、发送线程单独一个:
· 发送线程从发送队列获取需要发送数据的SOCKET句柄,在这些句柄上调用send()将数据发到客户端。队列中指保存了SOCKET句柄,具体的信息还 需要通过socket句柄在hash表中查找,定位到具体的对象。如同上面所讲,客户端信息的对象不但有一个变长的接收数据缓冲区,还有一个变长的发送数 据缓冲区。具体的工作线程发送数据的时候并不直接调用send()函数,而是将数据写到发送数据缓冲区,然后把SOCKET句柄放到发送线程队列。
·SOCKET句柄放到发送线程队列的另一种情况是:事件线程中发生了EPOLLOUT事件,说明TCP的发送缓冲区又有了可用的空间,这个时候可以把SOCKET句柄放到发送线程队列,一边触发send()的调用;
· 需要注意的是:发送线程发送大量数据的时候,当频繁调用send()直到TCP的发送缓冲区满后,便无法再发送了。这个时候如果循环等待,则其他用户的发 送工作受到影响;如果不继续发送,则EPOLL的ET模式可能不会再产生事件。解决这个问题的办法是在发送线程内再建立队列,或者在用户信息对象上设置标 志,等到线程空闲的时候,再去继续发送这些未发送完成的数据。

9、需要一个定时器线程:
·一位将epoll使用的高手说道:“单纯靠epoll来管理描述符不泄露几乎是不可能的。完全解决方案很简单,就是对每个fd设置超时时间,如果超过timeout的时间,这个fd没有活跃过,就close掉”。
·所以,定时器线程定期轮训整个hash表,检查socket是否在规定的时间内未活动。未活动的SOCKET认为是超时,然后服务器主动关闭句柄,回收资源。

10、多个工作线程:
·工作线程由接收线程去触发:每次接收线程收到数据后,将有数据的SOCKET句柄放入一个工作队列中;工作线程再从工作队列获取SOCKET句柄,查询hash表,定位到用户信息对象,处理业务逻辑。
·工作线程如果需要发送数据,先把数据写入用户信息对象的发送缓冲区,然后把SOCKET句柄放到发送线程队列中去。
·对于任务队列,接收线程是生产者,多个工作线程是消费者;对于发送线程队列,多个工作线程是生产者,发送线程是消费者。在这里需要注意锁的问题,如果采用tbb::concurrent_queue,会轻松很多。

11、仅仅只用scoket句柄作为hash表的键,并不够:
· 假设这样一种情况:事件线程刚把某SOCKET因发生EPOLLIN事件放入了接收队列,可是随即客户端异常断开了,事件线程又因为EPOLLERR事件 删除了hash表中的这一项。假设接收队列很长,发生异常的SOCKET还在队列中,等到接收线程处理到这个SOCKET的时候,并不能通过SOCKET 句柄索引到hash表中的对象。
·索引不到的情况也好处理,难点就在于,这个SOCKET句柄立即被另一个客户端使用了,接入线程为这个SCOKET建立了hash表中的某个对象。此时,句柄相同的两个SOCKET,其实已经是不同的两个客户端了。极端情况下,这种情况是可能发生的。
·解决的办法是,使用socket fd + sequence为hash表的键,sequence由接入线程在每次accept()后将一个整型值累加而得到。这样,就算SOCKET句柄被重用,也不会发生问题了。

12、监控,需要考虑:

·框架中最容易出问题的是工作线程:工作线程的处理速度太慢,就会使得各个队列暴涨,最终导致服务器崩溃。因此必须要限制每个队列允许的最大大小,且需要监视每个工作线程的处理时间,超过这个时间就应该采用某个办法结束掉工作线程


13、

当非阻塞socket与epoll的ET模式结合使用进行编程时需要注意的事项:
当客户端与服务端建立连接数据通信完成后,即服务端收到客户端的数据后,再向客户端发送数据,当发送完数据后立马断开连接。客户端epoll_wait和read的处理:

while(1)
{
     nfds = epoll_wait(epfd_rcv, events, EVENTSIZE , -1);// get the sockfd ready
        if (nfds < 0){
                        if(errno == EINTR)
                               continue;
                        else{
                                 fprintf(stderr, "epoll_wait err:%s\n", strerror(errno));
                                 return NULL;
                        }
        }
        for (i=0; i < nfds; i++){
            if(events[i].events & EPOLLIN){
                if ( (sockfd = events[i].data.fd) < 0) {
                    continue;
                }
                num2=1;
                while(num2>0)
                {
                       num2 = read(sockfd, recv_buf, g_receive_package_size - data_buf_r[sockfd]);

                        if (num2 < 0) {        //有对errno的处理
                             if ( (errno != EINTR) && (errno != EAGAIN) ) {
                                  if (close(sockfd) == 0) {
                                     ...........
                                  }
                             }
                      } else if (num2 == 0) {  //无对errno的处理,当num2=0 时系统没有给errno赋新值,而errno是全局变量会保存上一次系统赋给它的值     
                                 if (close(sockfd) == 0) {
                                   .............
                                 }
                     }else{
                            .............
                            ............
                     }
                }
       }
}

因 为是非阻塞socket和ET模式,如果不进行循环read的话,服务器发送完数据后立即close掉连接,那么客户端能read到服务器发送来的数据但 是捕捉不到服务器close连接的事件,而客户端的tcp/ip协议栈收到了服务器发来的FIN包,但由于应用程序中没有捕获到服务器close连接的事 件,也就是没有read返回0,因此没有向服务器发送FIN包,导致客户端大量的CLOSE_WAIT.


阅读(1700) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~