select函数实现主要依托于下列函数
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
参数maxfd是需要监视的最大的文件描述符值+1;rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合。struct timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
需要注意的是select既可监听读端口也可监听写端口以及异常事件,但是每一次调用select之前都必须要要清空端口重新赋值才行。因为当select返回的时候,rset位都将被置0,除了那些有变化的fd位,为了得到每次大循环时fd的变化需要每次重新赋值,这也是导致select不能支持太大量请求的原因。
那么select何时返回呢?我猜测是轮询完所有fd之后返回。不过这个我也不确定,希望有大神能够解释一下。
下面程序为自己了解select时所写。
程序的目的用select函数实现服务器的并发处理,并试验其所支持的最大连接数。
因此,只判断监听套接词是否有事件发生,对于已建立连接的套接词事件并不关心,同时对于后来断开的连接也不关心。以webbench作压力测试,发送1w个请求。注意在此之前应将ulimit -n的值改为10240。
先贴上程序,具体与poll,epoll程序的比较另开帖。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAX_NU 10000
#define MAX_CONN 5000
int main(int argc, char const *argv[])
{
int count = 0;
fd_set fds;
int maxfdp;
int fd;
int yes = 1;
struct sockaddr_in servaddr;
int array[MAX_NU];
char buff[200];
memset(array, '\0', MAX_NU);
memset(buff, '\0', 200);
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0){
perror("[Error]Create socket");
exit(1);
}
printf("Socket success!\n");
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
perror("[Error]setsockopt");
exit(1);
}
memset(&servaddr, '\0', sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(12345);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(fd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
perror("[Error]Bind socket");
exit(1);
}
printf("Bind success!\n");
if (listen(fd, MAX_CONN) < 0){
perror("[Error]Listen socket");
exit(1);
}
printf("Listen success!\n");
array[0] = fd;
while(1){
maxfdp = fd;
FD_ZERO(&fds);
int i;
FD_SET(fd, &fds);
for (i = 1; i < count+1; ++i){
//检测已关闭的套接词
/*ret = read(array[i], &buff , sizeof(buff));
if (ret <= 0 )
{
array[i] = array[count];
count--;
i--;
continue;
}
*/
FD_SET(array[i], &fds);
if (array[i] > maxfdp)
maxfdp = array[i];
}
//printf("maxfdp=%d\n", maxfdp);
//printf("Before select\n");
//为保证每次都等待timeout的时间,需要每次重新赋值
struct timeval timeout={10,0};
switch(select(maxfdp+1, &fds, NULL, NULL, &timeout)){
case -1:
perror("[Error]Select");
exit(1);
case 0:
//printf("Nothing\n");
break;
//说明一定有数据过来了,接下来FD_ISSET是要查出是哪个套接词有数据,有可能是请求建立连接,也有可能是发数据过来了。
default:
//此处fd仅为监听套接词,如果为真则说明有连接请求
//printf("We get something!\n");
if (FD_ISSET(fd, &fds))
{
struct sockaddr_in cltaddr;
int accept_fd;
socklen_t length = sizeof(cltaddr);
if((accept_fd = accept(fd, (struct sockaddr*)&cltaddr, &length)) < 0){
perror("[Error]Accept socket");
exit(1);
}
count++;
array[count] = accept_fd;
printf("recv connect ip=%s port=%d\n", inet_ntoa(cltaddr.sin_addr), ntohs(cltaddr.sin_port));
printf("count = %d\n", count);
}
//此处不管已连接的套接词是否有数据,所以只判断监听套接词是否有数据变动
}//end switch
}//end while
return 0;
}//end main
测试结果如图所示,到5164时crash,无法再支持更多的并发请求。
其实类似于上面这种测试没有太大意义,还是要落实到具体的实例才能看出他的性能优劣。我用之前写的web服务器改了一下,以select实现并发请求。
用ab测试,ab -c 100 -n 1000 http://107.167.184.223:12345/。大约抗了500个请求,然后就不行了。这个程序写得很不精炼,参考意义不大,主要select逻辑与上面程序类似。
/* Open the login page of renren then check
if the client have the right to enter */
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <signal.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define KNOWN_PORT 12345
#define MAX_QUEUE_LENGTH 512
#define MAXLINE 1024
#define MAX_NU 1000
void doit(int fd);
void serve_home(int fd, char *method);
void serve_add(int fd, char *para, char uri[MAXLINE]);
void select_srv(int fd);
void setnoneblocking(int fd);
int main(int argc, char const *argv[])
{
int sockfd;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
//设置socket的地址信息,地址及端口号
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(KNOWN_PORT);
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
//建立socket,作为所设网络接口,并开始监听网络
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
printf("Creat socket error");
exit(1);
}
setnoneblocking(sockfd);
if(bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr))){
printf("Server bind port %d failed\n", KNOWN_PORT);
exit(1);
}
if (listen(sockfd, MAX_QUEUE_LENGTH))
{
printf("Server listen port %d failed\n", KNOWN_PORT);
exit(1);
}
//服务器始终运行
select_srv(sockfd);
return 0;
}
void doit(int fd){
int len, is_get, fp, n_read, n_write;
char buf[MAXLINE], uri[MAXLINE], method[MAXLINE], version[MAXLINE];
char *para;
int i;
//得到method, uri, version
//获取请求头第一行包含method, uri, version的内容
len = recv(fd, buf, MAXLINE, 0);
for (i = 0; buf[i] == '\n'; i++)
buf[i + 1] = '\0';
if (len < 0)
{
printf("Can not get the request!\n");
exit(0);
}
//recv(fd, buf, MAXLINE, 0);
sscanf(buf, "%s %s %s", method, uri, version); //此处uri均为"/"
//request_igrest(fd); //忽略其余的请求
is_get = strcmp(method, "GET");
if (is_get)
{
perror("The web server doesn't implement this method!");
exit(1);
}
para = strchr(uri, '?');
if (!para)
{
fp = open("hu.html", O_RDONLY, "0744");
if (fp < 0)
{
printf("Open Erro!\n");
exit(1);
}
while((n_read = read(fp, buf, MAXLINE)) != 0){
n_write = send(fd, buf, n_read, 0);
if (n_read != n_write)
{
printf("Writing Erro!\n");
exit(1);
}
}
close(fp);
}
else if(!strstr(uri, "cgi"))
serve_home(fd, para);
else serve_add(fd, para, uri);
close(fd);
}
//此函数为用户第一次登录时出现
void serve_home(int fd, char *para){
int fp1, fp2, result, n_read, n_write, is_right, len;
char buf[MAXLINE], username[MAXLINE], password[MAXLINE], bufp[MAXLINE], action[MAXLINE];
char usern[MAXLINE], pwd[MAXLINE]; //用于存储已经注册过的用户名密码
char clientun[MAXLINE], clientpwd[MAXLINE];
//从浏览器中获取username和password
strcpy(bufp, para + 1);
sscanf(bufp, "%[^&]&%[^&]&%[^&]", username, password, action);
//从本地txt中获取username和password
fp1 = open("database.txt", O_RDONLY, "0744");
len = read(fp1, buf, MAXLINE);
buf[len] = '\0';
sscanf(buf, "%s %s", usern, pwd);
strcpy(clientun, "usern=");
strcpy(clientpwd, "pwd=");
strcat(clientun, usern);
strcat(clientpwd, pwd);
close(fp1);
//比较两者是否一样
if(strcmp(clientun, username)){
printf("You haven't registered yet!\n");
exit(0);
}
if (strcmp(clientpwd, password))
{
printf("Wrong password\n");
exit(0);
}
fp2 = open("home.html", O_RDONLY, "0744");
while((n_read = read(fp2, buf, MAXLINE)) != 0){
n_write = send(fd, buf, n_read, 0);
if (n_read != n_write)
{
printf("Writing Erro!\n");
exit(1);
}
}
close(fp2);
//关闭已断开连接的套接词
/*ret = read(array[i], &buff , sizeof(buff));
if (ret <= 0 )
{
array[i] = array[count];
count--;
i--;
continue;
}
*/
}
//为请求提供加法cgi
void serve_add(int fd, char *para, char uri[MAXLINE]){
char cgiargs[MAXLINE];
char filename[MAXLINE];
char *emptylist[]= {NULL};
char *buf;
//char*envp[]={"PATH=/bin",NULL};
//printf("%s\n", uri);
strcpy(cgiargs, para + 1);
*para = '\0';
strcpy(filename, ".");
strcat(filename, uri);
//printf("filename = %s, cgiargs = %s\n", filename, cgiargs);
if (fork() == 0)
{
setenv("QUERY_STRING", cgiargs, 1);
//buf = getenv("QUERY_STRING");
//printf("buf = %s\n", buf);
dup2(fd, STDOUT_FILENO);
execl(filename, "add.cgi");
perror("execve failed!");
}
wait(NULL);
}
//使用select处理并发连接
void select_srv(int fd){
int maxfdp;
int i;
fd_set fds;
int count = 0;
char array[MAX_NU];
while(1){
memset(array, '\0', MAX_NU);
maxfdp = fd;
FD_ZERO(&fds);
FD_SET(fd, &fds);
for (i = 1; i < count+1; ++i){
FD_SET(array[i], &fds);
if (array[i] > maxfdp)
maxfdp = array[i];
}
//printf("maxfdp=%d\n", maxfdp);
//printf("Before select\n");
//为保证每次都等待timeout的时间,需要每次重新赋值
//struct timeval timeout={10,0};
switch(select(maxfdp+1, &fds, NULL, NULL, 0)){
case -1:
perror("[Error]Select");
exit(1);
case 0:
//printf("Nothing\n");
break;
//说明一定有数据过来了,接下来FD_ISSET是要查出是哪个套接词有数据,有可能是请求建立连接,也有可能是发数据过来了。
default:
//此处fd仅为监听套接词,如果为真则说明有连接请求
//printf("We get something!\n");
if (FD_ISSET(fd, &fds))
{
struct sockaddr_in cltaddr;
int accept_fd;
socklen_t length = sizeof(cltaddr);
if((accept_fd = accept(fd, (struct sockaddr*)&cltaddr, &length)) < 0){
perror("[Error]Accept socket");
exit(1);
}
setnoneblocking(accept_fd);
count++;
array[count] = accept_fd;
printf("recv connect ip=%s port=%d\n", inet_ntoa(cltaddr.sin_addr), ntohs(cltaddr.sin_port));
printf("count = %d\n", count);
doit(array[i]);
}
//此处处理已建立连接的套接词
/*
for (i = 1; i < count+1; ++i)
{
if (FD_ISSET(array[i], &fds)){
printf("Serve it\n");
doit(array[i]);
}
}*/
}//end switch
}//end while
}
//将套接词设为no-block
void setnoneblocking(int fd){
int yes = 1;
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) == -1) {
perror("[Error]setsockopt");
exit(1);
}
}