多路复用
在客户端/服务器模型中,服务器端需要同时处理多个客户端的连接请求,此时就需要使用多路复用。
实现多路复用最简单的方法是采用非阻塞的方式套接字,服务器端不断的查询每一个套接字的状态,如果有数据到达则读出数据,如果没有数据到达则查看下一个套接字。这种方法虽然简单,但是轮询过程中浪费了大量的CPU时间,效率非常的低。
另一种方法是服务器进程并不主动的询问套接字的状态,而是向系统登记希望坚实的套接字,然后阻塞。当套接字上有事件发生时(如有数据到达),系统通知服务器进程告知那个套接字上发生了什么事件,服务器进程查询对应的套接字,并进行处理。在这种工作方式下,套接字上没有事件发生时,服务器进程不会去查询套接字的状态,从而不会浪费CPU的时间,提高了效率。
使用函数select可以实现第二种多路复用。
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *expectfds, struct timeval *timeout);
参数n是要监视的文件描述符数,要监视的文件描述符值为0~n-1。
参数readfds制定需要监视的可读文件描述符集合,当这个集合中的一个描述符上有数据到达的时候,系统将通知调用select函数的程序。
参数writefds指定需要监视的可写文件描述符集合,当这个集合中的一个描述符可以发送数据时,程序将得到通知。
参数expectfds指定需要监视的异常文件描述符集合,当有一个描述符发生异常的时候,程序将得到通知。
参数timeout设置阻塞的时间。
系统为文件描述符集合提供了一系列的操作宏:
- FD_CLR(int fd,fd_set *set) 将文件描述符fd从文件描述符集合set中删除
- FD_ISSET(int fd,fd_set *set) 测试fd是否在set中
- FD_SET(int fd,fd_set *set) 在文件描述符集合set中增加文件描述符fd
- FD_ZERO(fd_set *set)
服务器端
int main() { int sock_fd,conn_fd; int optval; int ret; int name_num; pid_t pid; socklen_t cli_len; struct sockaddr_in cli_addr, serv_addr; char recv_buf[128]; //initialization a socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); if(sock_fd < 0) { my_err("socket",_LINE_); } //initialization server address memset(&serv_addr, 0, sizeof(struct sockaddr_in)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons(SERV_PORT); serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); //bind if(bind(sock_fd, (struct sockaddr*)&serv_addr), sizeof(struct sockaddr_in) < 0) { my_err("bind"); } //listen if(listen(sock_fd, LISTENQ) < 0) { my_err("listen"); } cli_len = sizeof(struct sockaddr_in); while(1) { conn_fd = accept(sock_fd, (struct sockaddr *)&cli_addr, &cli_len); if(conn_fd < 0) { my_err("accept",_LINE_); } printf("Accept a new client,ip:%s \n", inet_ntoa(cli_addr.sin_addr)); if((pid = fork()) == 0) { // child process while(1) { if((ret = recv(conn_fd, recv_buf, sizeof(recv_buf), 0)) < 0) { perror("recv"); exit(1); } recv_buf[ret-1] = '\0'; if(flag_recv == USERNAME) { name_num = find_name(recv_buf); switch(name_num) { case -1: send_data(conn_fd, "n\n"); break; case -2: exit(1); break; default: send_data(conn_fd, "y\n"); flag_recv = PASSWORD; break; } } else if(flag_recv == PASSWORD) { //check password right or wrong } } close(sock_fd); close(conn_fd); exit(0);// child process end } else { //parent process close(conn_fd); } } return 0; }
服务器程序用fork函数来处理多个客户端,但是在数据库应用程序中,这可能不是最佳的解决办法。因为服务器将程序可能会相当大,而且在数据库访问方面还存在着多个服务器副本的协调问题。
所以我们需要一个办法支持让单个服务器进程在不阻塞,不等待客户请求带大的前提下处理多个客户。所以选择select。程序如下
1 int main() 2 { 3 int server_sockfd, client_sockfd; 4 int server_len, client_len; 5 struct sockaddr_in server_address; 6 struct sockaddr_in client_address; 7 int result; 8 fd_set readfds, testfds; 9 10 //create a socket on the server 11 server_sockfd = socket(AF_INET, SOCK_STREAM, 0); 12 server_address.sin_family = AF_INET; 13 server_address.sin_addr.s_addr = htonl(INADDR_ANY); 14 server_address.sin_port = htons(9734); 15 server_len = sizeof(server_address); 16 17 bind(server_sockfd, (struct sockaddr*)&server_address, server_len); 18 19 //create a listen queue 20 listen(server_sockfd, 5); 21 22 FD_ZERO(&readfds); 23 FD_SET(server sockfd, &readfds); 24 25 while(1) 26 { 27 char ch; 28 int fd; 29 int nread; 30 31 testfds = readfds; 32 33 printf("server waiting \n"); 34 result = select(FD_SETSIZE, &testfds, (fd_set *)0, (fd_set *)0, (struct timeval *)0); 35 if(result < 1) 36 { 37 perror("server"); 38 exit(1); 39 } 40 41 for(fd = 0; fd < FD_SETSIZE; fd++) 42 { 43 if(FD_ISSET(fd, &testfds)) 44 { 45 if(fd == server_sockfd) 46 { 47 client_len = sizeof(client_address); 48 client_sockfd = accept(server_sockfd, (struct sockaddr*)&client_address, &client_len); 49 FD_SET(client_sockfd, &readfds); 50 } 51 else 52 { 53 //如果活动不是发生在服务器套接字上,那就一定是客户端的活动 54 ioctl(fd, FIONREAD, &nread); 55 56 if(nread == 0) 57 { 58 close(fd); 59 FD_CLR(fd, &readfds); 60 printf("removing client on fd %d\n", fd); 61 } 62 else 63 { 64 read(fd, &ch, 1); 65 sleep(5); 66 printf("serving client on fd %d\n", fd); 67 ch++; 68 write(fd, &ch, 1); 69 } 70 } 71 } 72 } 73 } 74 }