一、用select實現的併發服務器,能達到的併發數,受兩方面限制
1、一個進程能打開的最大文件描述符限制。這可以通過調整內核參數。可以通過ulimit -n來調整或者使用setrlimit函數設置, 但一個系統所能打開的最大數也是有限的,跟內存大小有關,可以通過cat /proc/sys/fs/file-max 查看2、select中的fd_set集合容量的限制(FD_SETSIZE,一般爲1024) ,這需要重新編譯內核。
下面爲測試程序,只建立連接,看看最多能夠建立多少個連接,客戶端程序如下:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void)
{
int count = 0;
while(1)
{
int sock;
if ((sock = socket(AF_INET, SOCK_STREAM,0)) < 0)
{
sleep(4);
ERR_EXIT("socket");
}
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
if (connect(sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("connect");
struct sockaddr_in localaddr;
socklen_t addrlen = sizeof(localaddr);
if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0)
ERR_EXIT("getsockname");
printf("ip=%s port=%d\n", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
printf("count = %d\n", ++count);
}
return 0;
}
啓動select 的服務器端程序,再啓動客戶端測試程序:
從中可以看出對於客戶端,最多隻能開啓1021個連接套接字,因爲總共是1024個,還得除去0,1,2。而服務器端只能accept 返回1020個已連接套接字,因爲除了012之外還有一個監聽套接字,客戶端某一個套接字(不一定是最後一個)雖然已經建立了連接,在已完成連接隊列中,但accept 返回時達到最大描述符限制,返回錯誤,打印提示信息。
也許有人會注意到上面有一行 sleep(4); 當客戶端調用socket準備創建第1022個套接字時,如上所示也會提示錯誤,此時socket函數返回-1出錯,如果沒有睡眠4s後再退出進程會有什麼問題呢?如果直接退出進程,會將客戶端所打開的所有套接字關閉掉,即向服務器端發送了很多FIN段,而此時也許服務器端還一直在accept ,即還在從已連接隊列中返回已連接套接字,此時服務器端除了關心監聽套接字的可讀事件,也開始關心前面已建立連接的套接字的可讀事件,read 返回0,所以會有很多 client close 字段 參雜在條目的輸出中,還有個問題就是,因爲read 返回0,服務器端會將自身的已連接套接字關閉掉,那麼也許剛纔說的客戶端某一個連接會被accept 返回,即測試不出服務器端真正的併發容量。同時可以看到最後接收到的一個連接端口是52234,即不一定是客戶端的最後一個連接。
二、poll 函數應用舉例
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數1:結構體數組指針,struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
參數2:結構體數組的成員個數,即文件描述符個數。
參數3:即超時時間,若爲-1,表示永不超時。
服務端程序修改如下所示:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<signal.h>
#include<sys/wait.h>
#include<poll.h>//poll函數
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while (0)
int main(void)
{
int count = 0;
signal(SIGPIPE, SIG_IGN);
int listenfd; //被動套接字(文件描述符),即只可以accept, 監聽套接字
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
ERR_EXIT("socket error");
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
/* servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); */
/* inet_aton("127.0.0.1", &servaddr.sin_addr); */
int on = 1;
if (setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
ERR_EXIT("setsockopt error");
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
ERR_EXIT("bind error");
if (listen(listenfd, SOMAXCONN) < 0) //listen應在socket和bind之後,而在accept之前
ERR_EXIT("listen error");
struct sockaddr_in peeraddr; //傳出參數
socklen_t peerlen = sizeof(peeraddr); //傳入傳出參數,必須有初始值
int conn; // 已連接套接字(變爲主動套接字,即可以主動connect)
int i;
struct pollfd client[2048];//pollfd數組
int maxi = 0; //client[i]最大不空閒位置的下標
for (i = 0; i < 2048; i++)
client[i].fd = -1;
int nready;
client[0].fd = listenfd;//監聽套接字加入client數組
client[0].events = POLLIN;//events即感興趣事件,POLLIN表示有數據到來,文件描述符可讀
while (1)
{
/* poll檢測[0, maxi + 1) */
nready = poll(client, maxi + 1, -1);//poll會檢測所有存在的文件描述符,maxi爲文件描述符所在數組的最大下標
if (nready == -1)
{
if (errno == EINTR)
continue;
ERR_EXIT("poll error");
}
if (nready == 0)
continue;
if (client[0].revents & POLLIN)//client[0]表示的監聽套接字,將POLLIN事件加入返回事件
{
conn = accept(listenfd, (struct sockaddr *)&peeraddr, &peerlen); //accept不再阻塞
if (conn == -1)
ERR_EXIT("accept error");
for (i = 1; i < 2048; i++)
{
if (client[i].fd < 0)
{
client[i].fd = conn;//連接套接字加入client數組
if (i > maxi)
maxi = i;//修改最大下標
break;
}
}
if (i == 2048)
{
fprintf(stderr, "too many clients\n");
exit(EXIT_FAILURE);
}
printf("count = %d\n", ++count);
printf("recv connect ip=%s port=%d\n", inet_ntoa(peeraddr.sin_addr),
ntohs(peeraddr.sin_port));
client[i].events = POLLIN;//連接進來的套接字的事件改爲有數據到來即可讀
if (--nready <= 0)//第一次調用的時候nready爲1
continue;
}
for (i = 1; i <= maxi; i++)//i從1開始表示除去第一個監聽套接字。
{
conn = client[i].fd;
if (conn == -1)
continue;
if (client[i].revents & POLLIN)
{
char recvbuf[1024] = {0};
int ret = read(conn, recvbuf, 1024);
if (ret == -1)
ERR_EXIT("read error");
else if (ret == 0) //客戶端關閉
{
printf("client close \n");
client[i].fd = -1;
close(conn);
}
fputs(recvbuf, stdout);
write(conn, recvbuf, strlen(recvbuf));
if (--nready <= 0)
break;
}
}
}
return 0;
}
/* poll 只受一個進程所能打開的最大文件描述符限制,這個可以使用ulimit -n調整 */
來看一下輸出:服務器端:
客戶端:
可以看到現在最大的連接數已經是2045個了,雖然服務器端有某個連接沒有accept 返回。即poll 比 select 能夠承受更多的併發連接,只受一個進程所能打開的最大文件描述符個數限制。可以通過ulimit -n 修改,但一個系統所能打開的文件描述符個數也是有限的,這跟系統的內存大小有關係,所以說也不是可以無限地並發,可以查看一下本機的容量:
本機是虛擬機,內存大約1G,能夠打開的文件描述符個數大約在10w個左右。