在Unix/C中,Socket相关的函数操作都是阻塞式的,在单线程下服务端只能处理一个客户端请求。采用多线程处理客户端请求,虽然能充分发挥多核CPU能力,但是在客户端连接过多,并发度维持相对较高水平时,多线程引起线程的上下文切换将导致系统效率低下。Select多路复用模型的核心思想,在单线程下处理多客户端连接,Unix/C提供了相应函数库,具体如下:
int select (int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,
const struct timeval* timeout);
struct timeval {
long tv_sec; //秒
long tv_usec; //毫秒
};
fd_set是一个SOCKET链表,以下宏可以对该队列进行操作:
FD_CLR( s, *set) 从队列set删除句柄s;
FD_ISSET( s, *set) 检查句柄s是否存在与队列set中;
FD_SET( s, *set )把句柄s添加到队列set中;
FD_ZERO( *set ) 把set队列初始化成空队列.
Select执行流程:
1:用FD_ZERO宏来初始化我们感兴趣的fd_set。
2:用FD_SET宏来将套接字句柄分配给相应的fd_set。
3:调用select函数
4:用FD_ISSET对套接字句柄进行检查。
服务端代码如下:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/select.h>
#include <unistd.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#define PORT 8080
#define BACKLOG 1024
#define BUFSIZE 64
int client_count;
int main(){
int server_socket = -1;
struct sockaddr_in socket_addr;
memset(&socket_addr, 0, sizeof(socket_addr));
socket_addr.sin_addr.s_addr = htonl(INADDR_ANY);
socket_addr.sin_port = htons(PORT);
socket_addr.sin_family = AF_INET;
if((server_socket = create_socket(socket_addr, BACKLOG))<0)
return -1;
struct timeval tik;
memset(&tik, 0, sizeof(tik));
fd_set socket_fd;
int fds[BACKLOG];
memset(fds, 0, sizeof(fds));
int max_fd;
while(1){
FD_ZERO(&socket_fd); //初始化fd_set
FD_SET(server_socket, &socket_fd);//服务端socket添加至fd_set
tik.tv_sec = 10;
tik.tv_usec = 0;
max_fd = server_socket;
int index;
for(index = 0; index < client_count; index++){
FD_SET(fds[index], &socket_fd);//添加客户端Socket至fd_set
if(fds[index] > max_fd)
max_fd = fds[index];
}
int select_ret = -1;
if((select_ret = select(max_fd+1, &socket_fd, NULL, NULL, &tik)) < 0){
perror("select error");
return -1;
}
if(select_ret == 0)
continue;
for(index = 0;index < client_count;index++)
if(FD_ISSET(fds[index], &socket_fd))
read_handler(&fds[index]);//处理IO事件
if(FD_ISSET(server_socket, &socket_fd))
connect_handler(server_socket, fds + client_count);//处理连接
}
return 0;
}
char *get_client_info(int client_fd){
if(client_fd <= 0)
return NULL;
char client_info[16];
memset(client_info, 0, 16);
struct sockaddr_in peeraddr;
socklen_t len = sizeof(peeraddr);
memset(&peeraddr, 0, len);
getpeername(client_fd, &peeraddr, &len);
sprintf(client_info, "[%s:%d]", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
return client_info;
}
void read_handler(int *socket_fd){
if(socket_fd == NULL)
return;
int len;
char buf[BUFSIZE];
memset(buf, 0, sizeof(buf));
if((len = read(*socket_fd, buf, sizeof(buf)))<0){
perror("read error");
return;
}else if(len == 0 ){
perror("client close");
*socket_fd = -1;
}else{
printf("%s%s", get_client_info(*socket_fd), buf);
write(*socket_fd, buf, sizeof(buf));
}
}
void connect_handler(int server_socket, int *fd){
if(server_socket == NULL || fd == NULL)
return;
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
memset(&client_addr, 0, len);
int accept_ret = -1;
if((accept_ret = accept(server_socket, &client_addr, &len)) < 0){
perror("client error");
return;
}else{
*fd = accept_ret;
client_count++;
printf("[%s:%d] connected\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
}
int create_socket(struct sockaddr_in socket_addr, int backlog){
int server_socket = -1;
if((server_socket = socket(AF_INET, SOCK_STREAM, 0))<0){
perror("socket error");
return -1;
}
int on = 1;
setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int bind_ret = -1;
if((bind_ret = bind(server_socket, &socket_addr, sizeof(socket_addr)))<0){
perror("bind error");
return -1;
}
int listen_ret = -1;
if((listen_ret = listen(server_socket, backlog))<0){
perror("listen error");
return -1;
}
return server_socket;
}
客户端代码:
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 8080
int main(){
struct sockaddr_in client_addr;
memset(&client_addr, 0, sizeof(client_addr));
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = htonl(INADDR_ANY);
client_addr.sin_port = htons(PORT);
int socket_fd = -1;
if((socket_fd = socket(AF_INET, SOCK_STREAM, 0))<0){
perror("socket error");
return -1;
}
connect(socket_fd, &client_addr, sizeof(client_addr));
char send_buf[64],recv_buf[64];
memset(send_buf, 0, sizeof(send_buf));
memset(recv_buf, 0, sizeof(recv_buf));
while(fgets(send_buf,sizeof(send_buf),stdin)!=NULL){
write(socket_fd, send_buf, strlen(send_buf));
read(socket_fd, recv_buf, sizeof(recv_buf));
fputs(recv_buf, stdout);
memset(send_buf, 0, sizeof(send_buf));
memset(recv_buf, 0, sizeof(recv_buf));
}
close(socket_fd);
return 0;
}
运行效果如下图:
小结:Select模型解决了单线程下,无法处理多客户端请求问题。但由于其采用轮询策略依次去处理所有socket句柄,而通常大多数连接处理不活跃状态,这导致轮询效率低下;实验表明,在大部分客户端不活跃而维持长连接状态时,select多路复用模型的效率并不比多线程分别处理客户端效率高。此外,由于Select基于文件描述符实现多路复用,其最大并发处理的并发数也受到操作系统相应的限制(linux默认1024)。