线程过多会占用很多的资源:
在空间方面,线程是有内存开销的,1000个线程就要512M或2G内存;
在时间方面,线程切换是有CPU开销的,大量线程会让时间花在上下文切换。
1. IO多路复用
IO多路复用: 多个网络连接复用一个IO线程
使用一个线程来检查I/O流(Socket)的就绪状态。通过记录跟踪每个I/O流(Socket)的状态,来同时管理多个I/O流 。
多个Socket复用功能是在内核驱动实现的。
2. 分类
2.1 Select模式
结构体:
fd_set
:描述符集合( long类型数组 )
宏定义:
参数 | 含义 |
---|---|
FD_ZERO(fd_set *fdset) | 清空文件描述符集 |
FD_SET(int fd,fd_set *fdset) | 设置监听的描述符(把监听的描述符设置为1) |
FD_CLR(int fd,fd_set *fdset) | 清除监听的描述符(把监听的描述符设置为0) |
FD_ISSET(int fd,fd_set *fdset) | 判断描述符是否设置(判断描述符是否设置为1) |
FD_SETSIZE | 256 |
函数:
int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout)
参数 | 含义 |
---|---|
maxfd | 需要监视的最大的文件描述符值+1 |
rdset | 需要检测的可读文件描述符的集合 |
wrset | 需要检测的可写文件描述符的集合 |
exset | 需要检测的异常文件描述符的集合 |
timeout | 超时时间 |
返回值:
返回值 | 含义 |
---|---|
-1 | 出错 |
=0 | 超时 |
>0 | 获取到数据 |
编码过程:
- 定义描述符集
- 清空描述符集
- 设置指定的描述符并获取最大的描述符值+1
- 等待描述符就绪
- 判断已就绪的描述符,并做对应处理。
程序:
服务器(可以对应上编码过程):
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <thread>
#include <list>
#include <sys/select.h>
#include <algorithm>
using namespace std;
int main(int argc,char* argv[]) {
if(3 != argc) {
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd) {
perror("open socket error");
return 1;
}
// 设置属性:端口释放
int flag = 1;
setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&flag,sizeof(flag));
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2]));
int res = bind(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res) {
perror("bind error");
return 1;
}
res = listen(fd,4);
if(-1 == res) {
perror("listen error");
return 1;
}
// IO复用
fd_set fdset;
FD_ZERO(&fdset);
FD_SET(STDIN_FILENO,&fdset);
FD_SET(fd,&fdset);
list<int> fds = {STDIN_FILENO,fd};
int maxfdp1 = *max_element(fds.begin(),fds.end())+1;
while(true) {
// 阻塞等待监听读取数据的fd
if(select(maxfdp1,&fdset,NULL,NULL,NULL) > 0) { // block
if(FD_ISSET(STDIN_FILENO,&fdset)) { // 标准输入
string s;
cin >> s;
for(auto connfd:fds) {
if(connfd == STDIN_FILENO || connfd == fd) continue;
write(connfd,s.c_str(),s.size()+1);
}
}
if(FD_ISSET(fd,&fdset)) { // 新的连接
sockaddr_in remote_addr;
socklen_t len = sizeof(remote_addr);
int connfd = accept(fd,(sockaddr*)&remote_addr,&len); // block
cout << inet_ntoa(remote_addr.sin_addr) << ":" << ntohs(remote_addr.sin_port) << endl;
if(-1 == connfd) {
perror("accept error");
return 1;
}
FD_SET(connfd,&fdset);
fds.push_back(connfd);
}
for(auto connfd:fds) {
if(connfd == STDIN_FILENO || connfd == fd) continue;
if(FD_ISSET(connfd,&fdset)) { // 接收客户端的数据
char buffer[256] = {0};
int n = read(connfd,buffer,256); // block
if(0 == n) {
printf("client exit\n");
fds.remove(connfd);
FD_CLR(connfd,&fdset);
break;
}
cout << buffer << endl;
}
}
FD_ZERO(&fdset);
maxfdp1 = *max_element(fds.begin(),fds.end())+1;
for(auto f:fds) {
FD_SET(f,&fdset);
}
}
}
for(auto connfd:fds) close(connfd);
close(fd);
}
客户端(和之前一样):
#include <iostream>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h> // AF_INET sockaddr_in
#include <unistd.h> // write() read() close()
#include <thread>
using namespace std;
int main(int argc,char* argv[]) {
if(3 != argc) {
printf("argument error\n");
printf("Usage:%s server_ip server_port\n",argv[0]);
return 1;
}
int fd = socket(AF_INET,SOCK_STREAM,0);
if(-1 == fd) {
perror("open socket error");
return 1;
}
sockaddr_in addr;
addr.sin_family = AF_INET;
inet_aton(argv[1],&addr.sin_addr);
addr.sin_port = htons(atoi(argv[2])); // atoi把字符串转为数字
int res = connect(fd,(sockaddr*)&addr,sizeof(addr));
if(-1 == res) {
perror("connect error");
return 1;
}
// 创建子线程
thread t( [fd]() {
for(;;) {
char buffer[256] = {0};
int n = read(fd,buffer,256); // block
if(0 == n) {
printf("server exit\n");
break;
}
cout << buffer << endl;
}
} );
t.detach();
// 主线程
string s;
while(cin >> s) { // block
write(fd,s.c_str(),s.size()+1);
}
close(fd);
}
结果为:
除了Select模式,还有 Poll模式 和 Epool模式,使用方法可查