近代TCP服务器-IO复用之select
所谓IO复用,复用的是线程,无论是accept的新链接事件,还是recv的可读事件,都可以在同一个线程中完成,不用引入新的独立线程。
IO复用随着历史发展经历了3个主要阶段,即select(),poll(),epoll().
这节课内容先介绍select方式的IO复用模型
一 服务端代码编写
我们的TCP服务器业务比较简单,代码架构简化模型如下:
服务器架构中只有一个主线程,主线程中有一个while死循环,while循环里面嵌套了一个有限的for循环。
//server5.cpp
#include<unistd.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<iostream>
#include<vector>
#include<cstring>
//有数据到来,触发此socket事件,才会进行recv,所以recv一般情况下不会死等在那里,recv必有数据来到。
//所以这个io模型相比多线程模型,已经很高效了。
const uint16_t port = 8880;
const uint16_t buf_len = 1024;
int main(){
int max_fd;
int rc,on=1;
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd ){
std::cout << "create socket error!" <<std::endl;
return -1;
}
struct sockaddr_in s_addr;
s_addr.sin_family = AF_INET;
s_addr.sin_addr.s_addr = htonl(INADDR_ANY);
s_addr.sin_port = htons(port);
int ret = bind(fd,(struct sockaddr*)&s_addr,sizeof(s_addr));
if(ret == -1){
std::cout << "bind addr error!" <<std::endl;
return -1;
}
max_fd = fd;
ret = listen(fd,1024);
if(-1 == ret){
std::cout << "listen error!" << std::endl;
return -1;
}
//声明两个描述符集合。
fd_set main_fds,read_fds;
//初始化集合。
FD_ZERO(&main_fds);
FD_ZERO(&read_fds);
//添加fd到main_fds集合
FD_SET(fd,&main_fds);
while (1){
//每次调用select后,被监听的集合会被破坏,所以需要复制未被破坏的集合(main_fds)到read_fds
memcpy(&read_fds, &main_fds, sizeof(main_fds));
//只监听可读事件的集合,其他无关参数设置为NULL.
int result = select(max_fd+1, &read_fds,NULL,NULL,NULL);
if(result < 1){
std::cout << "select error!" <<std::endl;
return -1;
}
std::cout << "select sockets = " << result << std::endl;
std::string send_buf = R"({"radio": {"proto": "11axg","country": "Britain","bandwidth": "HT20","usrlimit": "32","chanid": "auto","bswitch": "1"}})";
std::string recv_buff = "";
recv_buff.resize(buf_len);
//所有的描述符索引值一定在0~max_fd之间。
for (int peer_fd = 0; peer_fd <= max_fd; peer_fd++){
if(FD_ISSET(peer_fd,&read_fds)){
//如果当前的描述符为主描述符,表示当前是一个新链接的事件。
if(peer_fd == fd){
struct sockaddr_in c_addr;
int client_fd = accept(fd,(struct sockaddr*)NULL,NULL);
//接收新链接描述符
if(client_fd > 0){
//新链接的描述符添加到原始集合中。
FD_SET(client_fd,&main_fds);
//更新max_fd,保持为最大值。
if(client_fd > max_fd){
max_fd = client_fd;
}
std::cout << "accept a new client connection!" <<std::endl;
}else{
std::cout << "error client connection!" <<std::endl;
continue;
}
}else{
//不是主描述符,就是一个可读事件,或者可写事件
memset(&recv_buff.at(0),0,recv_buff.size());
int c = recv(peer_fd,&recv_buff.at(0),recv_buff.size(),0);
if(c < 0){
std::cout << "conn " << peer_fd << " recv error!" << std::endl;
close(peer_fd);
//如果读写的连接发生致命错误,把当前描述符从集合去除。
FD_CLR(peer_fd,&main_fds);
std::cout << "close peer " << peer_fd << std::endl;
continue;
}else if(c == 0){
std::cout <<"peer " << peer_fd << " connection closed" << std::endl;
close(peer_fd);
FD_CLR(peer_fd,&main_fds);
std::cout << "close peer " << peer_fd << std::endl;
continue;
}else{
std::cout << "recv client peer " << peer_fd << " data: " << recv_buff << std::endl;
}
int ret = send(peer_fd,&send_buf.at(0),send_buf.size(),0);
if(ret < 0){
std::cout << "send data error!" << std::endl;
}
}
}else{
// std::cout << "find fd!" << std::endl;
}
}
}
//释放连接资源
for (size_t i = 0; i < max_fd; i++){
if(FD_ISSET(i,&main_fds)){
close(i);
}
}
}
二 编译运行
2.1 服务端
编译
g++ -std=c++11 server5.cpp -lpthread -o server5
运行
./server5
2.2 客户端
编译客户端代码:
g++ client1.cpp -o client1
启动客户端:
./client1 192.168.199.160
client1 的第一个参数是服务端的地址(192.168.199.160)
服务端输出
可以同时启动多个client,验证一下服务器此时的并发数。
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
./client1 192.168.199.160
三 系统API 详解
3.1 select
#include <sys/select.h>
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict timeout);
- nfds
socket文件描述符,动态保持为三个集合中最大值得描述符+1
,表示一个遍历搜索的边界值。 - readfds
需要监听可读事件
的描述符集合首地址。select() 返回后,readfds
将被清除所有文件描述符,除了那些有可读事件触发的描述符。 - writefds
需要监听可写事件
的描述符集合首地址。select() 返回后,writefds
将被清除所有文件描述符,除了那些有可写事件触发的描述符。 - exceptfds
需要监听异常事件
的描述符集合首地址。select() 返回后,exceptfds
将被清除所有文件描述符,除了那些有异常事件触发的描述符。 - timeout
等待可读可写事件的超时时间。 - 返回值
如果执行成功,返回有事件发生的描述符总数,这些描述符散落在readfds、writefds,exceptfds这三个集合中。如果超时过期,返回值可能为零。
如果执行出错,返回-1,设置errno表示错误;
[!NOTE]
fd_set
是一个固定大小的缓冲区。表示一个有限集合。
3.2 辅助
void FD_ZERO(int fd, fd_set *set);//清除集合宏(删除集合中所有文件描述符)。被用作初始化一个文件描述符集。
void FD_SET(int fd, fd_set *set);//添加一个描述符到集合中。
void FD_CLR(fd_set *set);//从集合中删除一个描述符
int FD_ISSET(int fd, fd_set *set);
//调用 select() 后,FD_ISSET() 宏可用于测试文件描述符是否仍然存在于集合中。
//如果 FD_ISSET() 返回非零值,则文件描述符 fd 存在于集合中。如果返回0,则不在集合中。
四 总结
可以看到select可以既监听新连接事件,也可以监听读写事件,用一个主线程就完成了多种事件的监听,这就是IO复用,复用了线程。linux下线程和进程是同等的,也可以说是复用了进程。
select比起之前的多线程模型效率大大提升,但是也存在致命缺陷,不能实现百万并发的目标。
缺点以下:
- 最大连接数为1024
- 查找集合中的描述符为线性的。查找效率不高。
下一节介绍更高效的IO复用接口poll
三种IO复用的优缺点总结: