代码改变世界

Linux TCP server系列(4)-浅谈listen与大并发TCP连接

2011-09-18 18:43  Aga.J  阅读(4209)  评论(0编辑  收藏  举报

背景:

   服务器在调用listenaccept后,就会阻塞在accept函数上,accpet函数返回后循环调用accept函数等待客户的TCP连接。如果这时候又大量的用户并发发起connect连接,那么在listen有队列上限(最大可接受TCP的连接数)的情况下,有多少个connect会成功了。试验证明,当连接数远远高于listen的可连接数上限时,客户端的大部分TCP请求会被抛弃,只有当listen监听队列空闲或者放弃某个连接时,才可以接收新的连接,那么我们应该如何来避免这种情况出现?

 

分析:

(一)客户端

客户端运行初期完成所设定的一定量的socket创建和相应的处理线程的创建,然后使用条件变量来完成线程同步,直到最后一个线程创建完成,才向所有线程发出广播通知,让所有线程并发调用connect,连接成功则关闭连接,失败则返回,如下代码所示。

socket创建和线程创建:

        int testCount=300;        //并发用户数

/*

每个进程需要自己独立的栈空间,linux下默认栈大小是10M,在32位的机子上一个进程需要4G的内存空间,去掉自己的栈空间全局程序段空间,一般只有3G内存可以用,创建线程时就需要从这3G的空间中分配10M出来,所以最多可以分配300个线程。当然这里还可以使用多个进程,每个进程300个线程的方式来进一步扩大并发量。

*/

        int sockfd[testCount];

        pthread_t ntid[testCount];

 

        bzero(&servaddr,sizeof(servaddr));

        servaddr.sin_family=AF_INET;

        servaddr.sin_port=htons(SERVER_PORT);

        inet_pton(AF_INET,argv[1],&servaddr.sin_addr);

 

     int testCaseIndex=0;

     for(testCaseIndex=0;testCaseIndex<testCount;testCaseIndex++)

     {

        sockfd[testCaseIndex]=socket(AF_INET,SOCK_STREAM,0);

                     //为每个并发客户端创建一个socket

        if(sockfd[testCaseIndex]==-1)

        {

           printf("socket established error: %s\n",(char*)strerror(errno));

           return -1;

        }

        if( pthread_create(&ntid[testCaseIndex],NULL,handleFun,&sockfd[testCaseIndex])!=0)

 {

                printf("create thread error :%s\n",strerror(errno));

                return -1;

 }

//为每个并发客户端创建一个线程来执行connect

}

 

     printf("%d client has initiated\n",testCaseIndex);

 

   并发客户端的线程实现:线程阻塞在条件变量上(只有条件满足了并且发起唤醒动作,线程才开始执行)。

        int sockfd=*((int*)arg);

        {

                pthread_cond_wait(&cond,&mut);

                                     //在条件变量上等待条件满足!

//阻塞返回后立即解锁,防止互斥量加锁带来的阻塞

pthread_mutex_unlock(&mut);

int conRes=0;

                conRes=connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));

                                     //线程执行connect连接,每个线程在接到唤醒信号后,才可以执行该语句,来模拟多个线程的并发调用。

                if(conRes==-1)

                {

                  printf("connect error: %s\n",strerror(errno));

                  return 0;

                }

        }

 

   当条件满足时,唤醒阻塞在条件变量上的线程:

   while(1)

{

sleep(2);

pthread_cond_broadcast(&cond);  //在所有线程创建完成后才进行唤醒。

}

 

综上,客户端模拟并发过程中没有存在不同步的情况导致上述性能问题。(注意,在广播的时候,会出现广播丢失的情况,所以需要多次执行广播操作才会使得所有线程执行任务,所以某种程度上这里并不能模拟完完全全的并发)

 

   (二)通信中介

客户端和服务器之间的连接是在同一台机器上,使用Socket方式通信的话会经过127.0.0.1的回环线路,不会有网卡等硬件资源的访问性能消耗,所以不存在网络通信时延等问题。

 

   (三)服务器 

性能问题主要发生在服务器,可能是以下几部分造成:

1)服务器的监听队列 listen(listenfd,xxx),参数2指定队列内所能容纳的元素的最大值,当来不及从队列中移除元素时(调用accept移除或者TCP自动放弃)就会造成队列满而使得一些请求丢失。

解决办法:

a)增大队列容量是一种办法,但是注意等待队列太会带来效率的性能缺陷,而且listen函数对最大队列容量有一个上限,大小为SOMAXCONN,当然必要时刻我们可以修改这个常量的大小。
  b
)直接修改listen及相关函数的实现(比较麻烦,不建议),可以将listen所维护的队列修改为linklist,支持队列的动态增长。

 

2accept处理速度太慢,导致阻塞过长时间,使得队列无法及时清空已经完成3次连接的socket。也就是任意两个accept之间的时间间隔关键因素(这里实验了将accept后面的也删了,那么10个可以达到153的数目),如下面代码所示(listen之后在循环调用accept来将已完成3次握手的连接从listen所维护的队列中移除。)

listen(listenfd,10);

for(;;)

{

      chilen=sizeof(chiaddr);

connfd=accept(listenfd,(struct sockaddr*)&chiaddr,&chilen);

//其他操作

}

 

解决办法:

a 两个accept之间尽量不要又多余的操作,使得accept返回后可以立刻执行下一个accept经过试验,该方法可以较好的提高性能,减少connect的丢失数。

 

b

   本质上这是一种“生产者-消费者”的模式,listen维护“已连接”和“待连接”的队列,当客户发出连接请求并最终连接成功时,在“已连接”队列中会生产一个“product”,然后这时候希望“消费者”也就是accept函数可以快速的从队列中消费这个“product,这样就不会因为队列满而导致无法继续生产(也就是客户的connect会失效,导致上面队列长度10300个并发connect带来的67个存活的情况),但是在本例情况下,我们无法控制生产者的疯狂生产行为,因为连接是客户发起的,这是不可预知的,所以我们如果想不修改listen函数来提高性能的话,那么就只能让消费者更加快的把产品消耗掉,使得listen队列可以容纳更多的新生产的产品,而第一种加快消费者消耗产品的方法就是a,第二种加快消费者消耗产品的方法是我们可以增加多几个消费者来帮忙消耗,但是这几个消费者间也要好好协调。第三种方法是让消费者把产品先移走为listen的队列腾出空间,再自行处理产品,如d所示。

使用多线程策略,每个线程独立调用accept(花了一个上午的时候正glib的线程池,一直用不了,其他都正常,就是线程不启动,不知道会不会是bug)

下面自己手动使用简单的多线程来测试

线程数                队列容量                   并发用户数              通过数

1                            10                                 300                              65

2                            10                                 300                              142

3                            10                                 300                              122

6                            10                                 300                              120

10                          10                                 300                              196

 20                          10                                 300                              174

可以看到线程间也会出现竞争现象,并不是说一味增大并发线程数就可以提高并发数的。

 

c

修改listenaccept的实现方式,让listen所维护的队列可以智能的判断拥挤情况,从而对accept的调用做出调度,在队列繁忙时,使用多线程的方式让多个accept来移除队列中的元素,在队列空闲时,可以适当的调整accept的处理线程数,这也是一种线程池的实现。

 

 

  d

修改accept的实现方式,在accept中实现一个“消费缓冲区”,为的是及时将listen中的队列元素移动到该缓冲区中,再由其他处理线程或者进程来对缓冲区中的元素进行处理,这个方法尽量listen队列中已连接的socket可以被移除

最后这个方法比较上述方法来说是比较好的一种,但是还是需要修改已有的代码。

 

源码:

server.cpp
  1 #include<sys/types.h>
2 #include<sys/socket.h>
3 #include<strings.h>
4 #include<arpa/inet.h>
5 #include<unistd.h>
6 #include<stdlib.h>
7 #include<stdio.h>
8 #include<string.h>
9 #include<errno.h>
10 #include<signal.h>
11 #include<sys/wait.h>
12 #include<pthread.h>
13
14 #define LISTEN_PORT 84
15 void str_echo(int sockfd) // 服务器收到客户端的消息后的响应
16 {
17 ssize_t n;
18 char line[512];
19
20 printf("ready to read\n");
21
22 while( (n=read(sockfd,line,512))>0 )
23 {
24
25 line[n]='\0';
26 printf("Client: %s\n",line);
27
28 char msgBack[512];
29 snprintf(msgBack,sizeof(msgBack),"recv: %s\n",line);
30 write(sockfd,msgBack,strlen(msgBack));
31 bzero(&line,sizeof(line));
32
33 }
34
35 printf("end read\n");
36
37 }
38
39 void sig_child(int signo) //父进程对子进程结束的信号处理
40 {
41 pid_t pid;
42 int stat;
43
44 while( (pid=waitpid(-1,&stat,WNOHANG))>0)
45 printf("child %d terminated\n",pid);
46
47 return;
48 }
49
50 void* acceptThreadFun(void *arg)
51 {
52 int listenfd=*((int*)arg);
53 struct sockaddr_in chiaddr;
54 socklen_t chilen;
55 int connfd;
56 for(;;)
57 {
58 chilen=sizeof(chiaddr);
59
60 connfd=accept(listenfd, (struct sockaddr*)&chiaddr, &chilen);
61 //accept会总listen所维护的已连接队列中pop一个出来
62 //阻塞在accept,直到三次握手成功了才返回
63 if(connfd==-1)
64 printf("accept client error: %s\n",strerror(errno));
65 else
66 printf("client connected :%d\n",1);
67
68 close(connfd);
69
70 }
71 }
72 int successCount=0;
73 int main(int argc, char **argv)
74 {
75 int listenfd, connfd;
76 pid_t childpid;
77 socklen_t chilen;
78
79 struct sockaddr_in chiaddr,servaddr;
80
81 listenfd=socket(AF_INET,SOCK_STREAM,0);
82 if(listenfd==-1)
83 {
84 printf("socket established error: %s\n",(char*)strerror(errno)); //后面需要采用日志到方式来记录
85 //socket创建失败后可以让用户选择重新连接
86 }
87
88 bzero(&servaddr,sizeof(servaddr));
89 servaddr.sin_family=AF_INET;
90 servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
91 servaddr.sin_port=htons(LISTEN_PORT);
92
93 int bindc=bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
94 if(bindc==-1)
95 {
96 printf("bind error: %s\n",strerror(errno));
97 //绑定失败,错误提示
98 }
99
100 listen(listenfd,10); //limit是SOMAXCONN
101
102 signal(SIGCHLD,sig_child);
103
104 pthread_t acceptThread[20];
105 int threadCount=0;
106 for(threadCount;threadCount<20;threadCount++)
107 {
108 err=pthread_create(&acceptThread[threadCount],NULL,acceptThreadFun,&listenfd); //创建线程来帮忙accept
109 if(err!=0)
110 printf("can not create thread : %s\n",strerror(errno));
111 }
112
113 for(;;)
114 {
115 chilen=sizeof(chiaddr);
116
117 connfd=accept(listenfd,(struct sockaddr*)&chiaddr,&chilen);
118 //accept会总listen所维护的已连接队列中pop一个出来
119 //阻塞在accept,直到三次握手成功了才返回
120 if(connfd==-1)
121 printf("accept client error: %s\n",strerror(errno));
122 else
123 printf("client connected :%d\n",++successCount);
124
125 close(connfd);
126
127 }
128
129 }
client.cpp
 1 #include<sys/types.h>
2 #include<stdlib.h>
3 #include<stdio.h>
4 #include<unistd.h>
5 #include<sys/socket.h>
6 #include<strings.h>
7 #include<string.h>
8 #include<arpa/inet.h>
9 #include<errno.h>
10 #include<stdio.h>
11 #include<pthread.h>
12 #include<algorithm>
13
14 #include<exception>
15
16 #define SERVER_PORT 84
17
18 pthread_mutex_t mut=PTHREAD_MUTEX_INITIALIZER; //互斥量
19 pthread_cond_t cond=PTHREAD_COND_INITIALIZER; //条件变量
20
21 int cond_value=1;
22 struct sockaddr_in servaddr;
23
24 void *handleFun(void *arg)
25 {
26 int sockfd=*((int*)arg);
27
28 {
29 pthread_cond_wait(&cond,&mut);
30 pthread_mutex_unlock(&mut);
31 //信号会丢失,使得这里永远醒不了,所以需要重发信号.
32
33 int conRes=0;
34 conRes=connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr));
35
36 printf("%d\n",sockfd); //如果不加connect,那么这里显示正确
37 if(conRes==-1)
38 {
39 printf("connect error: %s\n",strerror(errno));
40 return 0;
41 }
42
43 }
44
45 }
46
47 void *handleFun2(void *arg)
48 {
49 *((int*)arg)=2;
50 pthread_cond_broadcast(&cond);
51 }
52
53 int main(int argc, char **argv)
54 {
55 int testCount=300;
56 int sockfd[testCount];
57 pthread_t ntid[testCount];
58
59 //tcpcli <ipaddress> <data>
60 if(argc!=3)
61 return -1;
62
63 bzero(&servaddr,sizeof(servaddr));
64 servaddr.sin_family=AF_INET;
65 servaddr.sin_port=htons(SERVER_PORT);
66 inet_pton(AF_INET,argv[1],&servaddr.sin_addr);
67
68 int testCaseIndex=0;
69 for(testCaseIndex=0;testCaseIndex<testCount;testCaseIndex++)
70 {
71 sockfd[testCaseIndex]=socket(AF_INET,SOCK_STREAM,0);
72 //为每个客户端线程创建socket
73 if(sockfd[testCaseIndex]==-1)
74 {
75 printf("socket established error: %s\n",(char*)strerror(errno));
76
77 return -1;
78 }
79
80 if(pthread_create(&ntid[testCaseIndex],NULL,handleFun,&sockfd[testCaseIndex])!=0)
81 //客户端线程
82 {
83 printf("create thread error :%s\n",strerror(errno));
84 return -1;
85 }
86 }
87
88 printf("%d client has initiated\n",testCaseIndex);
89
90 cond_value=2;
91 while(1)
92 {
93 sleep(2);
94 pthread_cond_broadcast(&cond); //条件满足后发信号通知所有阻塞在条件变量上的线程!
95 }
96 exit(0);
97 }

  

 

分析:

   网络服务器在大并发环境下出现性能问题的瓶颈主要在于服务器如何快速处理和响应大量的客户实际请求,但是在客户请求之前,客户端向服务器发起的tcp连接也是需要考虑的。

selectpollepoll可以帮助我们复用IO,从而更好的服务于大量客户并发请求。而对于listen在不改动内核源码的情况下似乎找不到并行处理请求的方式,它不像基于socket的实际请求,内核没有提供独立的互不相干的对象来管理请求,本质上它是将请求存放在队列中,所以本质上它需要串行处理。本文针对该问题做了分析,探讨如何提高listen的接收量,从而增大服务器处理客户端并发请求连接的能力。