IO多路复用就是用来处理并发(如果服务器端的程序只有一个进程或线程,不能通过多进程或多线程来实现并发)
为什么使用IO多路复用
我们在上一篇文章:并发网络通信-套接字通信中提到,网络通信会发生阻塞等待(accept、read、write),我们用的是多线程的方法进行处理的,现在利用IO多路复用,把本来该我们做的事(检测读写缓冲区是否可用)交给内核,内核同时检测多个文件描述符的读写缓冲区,检测完成后把可以进行操作的文件描述符告诉我们,我们就可以建立连接,发送,接收等操作。
三者的差别:
select可以跨平台,poll与epoll只能在linux下使用
epoll底层使用的是红黑树,select与poll底层使用的是线性表,epoll效率更高
select连接的上限数为1024,poll与epoll没有限制
select
使用select这种IO多路转接方式需要调用一个同名函数 select,这个函数是跨平台的,Linux、Mac、windows 都是支持的。通过调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态。
select函数原型
#include <sys/select.h>
struct timeval {
time t tv_sec;
suseconds_t tv_usec;
};
/*
必须初始化,一个秒,一个微妙
*/
int select(int Infds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval * timeout);
/*
函数参数:
nfds:委托内核检测的这三个集合中最大的文件描述符+1
内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
在Window中这个参数是无效的,指定为-1即可
readfds:文件描述符的集合,内核只检测这个集合中文件描述符对应的读缓冲区
传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据
writefds:文件描述符的集合,内核只检测这个集合中文件描述符对应的写缓冲区
传入传出参数,如果不需要使用这个参数可以指定为NULL
exceptfds:文件描述符的集合,内核检测集合中文件描述符是否有异常状态
传入传出参数,如果不需要使用这个参数可以指定为NULL
timeout:检测时间,设置阻塞时间,当没有文件描述符空闲时,就开始阻塞,阻塞时间为(秒+微妙)
不等待:函数不会阻塞,直接将该参数对应的结构体初始化为0即可。
函数返回值:大于0:成功,返回集合中已就绪的文件描述符的总个数
*/
fd_set类型:
下图中的fd_set 中存储了要委托内核检测读缓冲区的文件描述符集合。

如果集合中的标志位为0 代表不检测 这个文件描述符状态
如果集合中的标志位为 1 代表 检测 这个文件描述符状态
内核在遍历这个读集合的过程中,如果被检测的文件描述符对应的读缓冲区中没有数据,内核将修改这个文件描述符在读集合 fd_set 中对应的标志位,改为0,如果有数据那么这个标志位的值不变,还是1。
//将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0
void FD_CLR(imt fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int FD_ISSET(int fd, fd_set *set);
//将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
//将set集合中,所有文件文件描述符对应的标志位设置为0,集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
并发处理

1. 创建监听的套接字 Ifd=socket ();
2. 将监听的套接字和本地的IP和端口绑定 bind ()
3. 给监听的套接字设置监听 listen()
4. 创建一个文件描述符集合fd_set,用于存储需要检测读事件的所有的文件描述符
通过FD_ZERO()初始化,通过FD_SET()将监听的文件描述符放入检测的读集合中
5. 循环调用select 0,周期性的对所有的文件描述符进行检测
6. select ()解除阻塞返回,得到内核传出的满足条件的就绪的文件描述符集合
通过FD_ISSET ()判断集合中的标志位是否为1
如果这个文件描述符是监听的文件描述符,调用accept ()和客户端建立连接,将得到的新的通信的文件描述符,通过FD_SET ()放入到检测集合中
如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信
如果客户端和服务器断开了连接,使用FD_CLR ()将这个文件描述符从检测集合中删除
7. 重复第6步
示例代码
简单select
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
int main() {
// 1、创建监听套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
std::cerr << "socket error" << std::endl;
return -1;
}
// 2。将监听套接字与本地ip端口绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0 自动绑定本地的ip地址
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret < 0) {
std::cerr << "bind error" << std::endl;
return -1;
}
// 3.监听
ret = listen(fd, 128);
if (ret < 0) {
std::cerr << "listen error" << std::endl;
return -1;
}
fd_set redset;
FD_ZERO(&redset);
FD_SET(fd, &redset);
int max_fd = fd;
while (1) {
fd_set temp = redset;
int ret = select(max_fd + 1, &temp, nullptr, nullptr, nullptr);
// 判断是不是监听的fd
if (FD_ISSET(fd, &temp)) {
// 接收客户端连接
int cfd = accept(fd, nullptr, nullptr);
FD_SET(cfd, &redset);
if (cfd > max_fd) {
max_fd = cfd;
}
}
for (int i = 0; i < max_fd + 1; ++i) {
if (i != fd && FD_ISSET(i, &temp)) {
// 接收数据
char buff[1024]; // 接收数据
int len = recv(i, buff, sizeof(buff), 0);
if (len > 0) {
std::cout << "客户端发送:" << buff << std::endl;
send(i, buff, len, 0); // 服务器发送
} else if (len == 0) {
FD_CLR(i, &redset);
close(i);
std::cout << "客户端断开连接" << std::endl;
break;
} else {
// 连接失败
std::cerr << "recv error" << std::endl;
break;
}
}
}
}
close(fd);
return 0;
}
运行结果:

多线程select示例
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <thread>
#include <sys/select.h>
#include <mutex>
std::mutex mutex_;
struct FdInfo {
int fd;
int* maxfd;
fd_set* rdset;
};
void* accept_connect(FdInfo* fd_info) {
std::cout << "子线程id:" << std::this_thread::get_id() << std::endl;;
int cfd = accept(fd_info->fd, nullptr, nullptr);
mutex_.lock();
FD_SET(cfd, fd_info->rdset);
if (cfd > *fd_info->maxfd) {
*fd_info->maxfd = cfd;
}
mutex_.unlock();
delete fd_info;
return nullptr;
}
void* communicate(FdInfo* fd_info) {
char buff[1024]; // 接收数据
int len = recv(fd_info->fd, buff, sizeof(buff), 0);
if (len > 0) {
std::cout << "客户端发送:" << buff << std::endl;
send(fd_info->fd, buff, len, 0); // 服务器发送
delete fd_info;
return nullptr;
} else if (len == 0) {
mutex_.lock();
FD_CLR(fd_info->fd, fd_info->rdset);
mutex_.unlock();
close(fd_info->fd);
std::cout << "客户端断开连接" << std::endl;
delete fd_info;
return nullptr;
} else {
// 连接失败
std::cerr << "recv error" << std::endl;
delete fd_info;
return nullptr;
}
}
int main() {
// 1、创建监听套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) {
std::cerr << "socket error" << std::endl;
return -1;
}
// 2。将监听套接字与本地ip端口绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0 自动绑定本地的ip地址
int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret < 0) {
std::cerr << "bind error" << std::endl;
return -1;
}
// 3.监听
ret = listen(fd, 128);
if (ret < 0) {
std::cerr << "listen error" << std::endl;
return -1;
}
fd_set redset;
FD_ZERO(&redset);
FD_SET(fd, &redset);
int max_fd = fd;
while (1) {
mutex_.lock();
fd_set temp = redset;
mutex_.unlock();
int ret = select(max_fd + 1, &temp, nullptr, nullptr, nullptr);
// 判断是不是监听的fd
if (FD_ISSET(fd, &temp)) {
// 接收客户端连接,创建子线程
FdInfo* info = new FdInfo();
info->fd = fd;
info->maxfd = &max_fd;
info->rdset = &redset;
std::thread t1(accept_connect, info);
t1.detach();
}
for (int i = 0; i < max_fd + 1; ++i) {
if (i != fd && FD_ISSET(i, &temp)) {
// 接收数据,创建子线程
FdInfo* info = new FdInfo();
info->fd = i;
info->rdset = &redset;
std::thread t2(communicate, info);
t2.detach();
}
}
}
close(fd);
return 0;
}
运行结果:

特点:
1. 待检测集合(第2、3、4个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
2. 内核对于select传递进来的待检测集合的检测方式是线性的
如果集合内待检测的文件描述符很多,检测效率会比较低
如果集合内待检测的文件描述符相对较少,检测效率会比较高
3. 使用select能够检测的最大文件描述符个数有上限,默认是1024,这是在内核中被写死了的。
poll
poll函数原型
#include <poll.h>
//每个委托po11检测的fd都对应这样一个结构体
struct pollfd {
int fd; /* 委托内核检测的文件描述符 */
short events; /* 委托内核检测文件描述符的什么事件 POLLIN\\POLLOUT */
short revents; /* 文件描述符实际发生的事件->传出 */
};
struct pollfd myfd[100];
int poll(struct polIfd *fds, nfds_t nfds, int timeout);
/*
nfds:这是第一个参数数组中最后一个有效元素的下标+1(也可以指定参数1数组的元素总个数)
timeout:指定poll函数的阻塞时长
-1:一真阻塞,直到检测的集合中有就绪的文件描述符(有事件产生)解除阻塞
0:不阻塞,不管检测集合中有没有已就绪的文件描述符,函数马上返回
大于0:阻塞指定的毫秒(ms)数之后,解除阻塞
函数返回值:失败:返回-1。成功:返回一个大于0的整数,表示检测的集合中已就绪的文件描述符的总个数
*/
epoll
epoll效率高,但只能在linux下使用
对于待检测集合select和poll是基于线性方式处理的,epoll是基于红黑树来管理待检测集合的。
select和pol1每次都会线性扫描整个待检测集合,集合越大速度越慢,epoll使用的是回调机制,效率高,处理效率也不会随着检测集合的变大而下降
select和pol1工作过程中存在内核/用户空间数据的频繁拷贝问题,在epoll中内核和用户区使用的是共享内存(基于mmap内存映射区实现),省去了不必要的内存拷贝。
我们需要对select和pol1返回的集合进行判断才能知道哪些文件描述符是就绪的,通过epol1可以直接得到已就绪的文件描述符集合,无需再次检测
使用epoll没有最大文件描述符的限制,仅受系统中进程能打开的最大文件数目限制
epoll函数原型
#include <sys/epoll.h>
//创建epoll实例,通过一棵红黑树管理待检测集合
// 返回一个文件描述符
// 添加的节点取决于内存大小
int epoll_creat(int size);
// 管理红黑树上的文件描述符(添加、修改、删除)
// 成功0,失败-1
// epoll实例 EPOLL_CTL_ADD\\MOD\\DEL
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events /* Epoll events */ EPOLLIN EPOLLOUT EPOLLERR EPOLLET
epo1l_data_t data; /* User data variable */
}
// 检测epo1l树中是否有就绪的文件描述符,没有就阻塞
// 传出参数 指定数组的最大容量 堵塞时间ms
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
流程
1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0)
2.使用本地的IP与端口和监听的套接字进行绑定
int ret = bind(lfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
3.给监听的套接字设置监听
listen(1fd, 128);
4.创建epoll实例对象
int epfd = epoll_creatd(100);
5.将用于监听的套接字添加到epo11实例中
struct epoll_event ev; //检测lfd读读缓冲区是否有数据
ev.events = EPOLLIN;
ev.data.fd = lfd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
6.检测添加到epol1实例中的文件描述符是否已就绪,并将这些已就绪的文件描述符进行处理
int num = epoll_wait(epfd, evs, size, -1);
如果是监听的文件描述符,和新客户端建立连接,将得到的文件描述符添加到epoll实例中
int cfd = accept(curfd, NULL, NULL);
ev.events = EPOLLIN;
ev.data.fd = cfd;
//新得到的文件描述符添加到epo11模型中,下一轮循环的时候就可以被检测了
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
如果是通信的文件描述符,和对应的客户端通信,如果连接已断开,将该文件描述符从epoll实例中删除
int len = rqcv(curfd, buf, sizeof(buf), 0);
if(len == 0) {
//将这个文件描述符从epo11模型中删除
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
} else if (len > 0) {
send(curfd, buf, len, 0);
}
7.重复执行第6步
工作模式
水平触发模式
如果一个文件描述符就绪了,通知用户次数多,效率低,消耗资源多
水平模式的特点:
读事件:如果文件描述符对应的读缓冲区还有数据,读事件就会被触发,epoll_wait ()解除阻塞
写事件:如果文件描述符对应的写缓冲区可写,写事件就会被触发,epoll_wait 0解除阻塞
边沿触发模式
如果一个文件描述符就绪了,对用户进行一次通知,效率高,消耗资源少
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 设置边沿模式
为了发送接收不堵塞,要修改文件描述符为非堵塞模式
//设置完成之后,读写都变成了非阻塞模式
#include <fcntl.h>
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
边沿触发示例代码
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
int main() {
// 1、创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) {
std::cerr << "socket error" << std::endl;
return -1;
}
// 2。将监听套接字与本地ip端口绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0 自动绑定本地的ip地址
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret < 0) {
std::cerr << "bind error" << std::endl;
return -1;
}
// 3.监听
ret = listen(lfd, 128);
if (ret < 0) {
std::cerr << "listen error" << std::endl;
return -1;
}
// 4.创建epoll实例
int epfd = epoll_create(1);
if (epfd == -1) {
std::cerr << "epoll_create error" << std::endl;
return -1;
}
//5. 把监听描述符添加到epoll中
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边沿触发模式
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
std::cerr << "epoll_ctl error" << std::endl;
return -1;
}
// 检测
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (1) {
int num = epoll_wait(epfd, evs, size, -1);
std::cout << "num :" << num << std::endl;
for (int i = 0; i < num; ++i) {
int fd = evs[i].data.fd;
// 监听文件描述符,则建立连接
if (fd == lfd) {
int cfd = accept(lfd, nullptr, nullptr);
// 修改文件描述符的默认属性为非阻塞的
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 把通信描述符再放入epoll树中
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
} else {
// 通信文件描述符
char buff[5]; // 接收数据
while (1) {
int len = recv(fd, buff, sizeof(buff), 0);
if (len > 0) {
//std::cout << "客户端发送:" << buff << std::endl;
// 因为每次只读5个字节,结尾没有\\0,换一种方式显示在终端
write(STDOUT_FILENO, buff, len);
send(fd, buff, len, 0); // 服务器发送
} else if (len == 0) {
std::cout << "客户端断开连接" << std::endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
break;
} else {
// 连接失败
if (errno == EAGAIN) {
std::cout << "数据接送完毕" << std::endl;
break;
} else {
std::cerr << "recv error" << std::endl;
break;
}
}
}
}
}
}
// 关闭文件描述符
close(lfd);
return 0;
}
示例代码
epoll处理服务器的并发
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/epoll.h>
int main() {
// 1、创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) {
std::cerr << "socket error" << std::endl;
return -1;
}
// 2。将监听套接字与本地ip端口绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0 自动绑定本地的ip地址
int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));
if (ret < 0) {
std::cerr << "bind error" << std::endl;
return -1;
}
// 3.监听
ret = listen(lfd, 128);
if (ret < 0) {
std::cerr << "listen error" << std::endl;
return -1;
}
// 4.创建epoll实例
int epfd = epoll_create(1);
if (epfd == -1) {
std::cerr << "epoll_create error" << std::endl;
return -1;
}
//5. 把监听描述符添加到epoll中
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
std::cerr << "epoll_ctl error" << std::endl;
return -1;
}
// 检测
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (1) {
int num = epoll_wait(epfd, evs, size, -1);
std::cout << "num :" << num << std::endl;
for (int i = 0; i < num; ++i) {
int fd = evs[i].data.fd;
// 监听文件描述符,则建立连接
if (fd == lfd) {
int cfd = accept(lfd, nullptr, nullptr);
// 把通信描述符再放入epoll树中
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
} else {
// 通信文件描述符
char buff[1024]; // 接收数据
int len = recv(fd, buff, sizeof(buff), 0);
if (len > 0) {
std::cout << "客户端发送:" << buff << std::endl;
send(fd, buff, len, 0); // 服务器发送
} else if (len == 0) {
std::cout << "客户端断开连接" << std::endl;
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, nullptr);
close(fd);
break;
} else {
// 连接失败
std::cerr << "recv error" << std::endl;
break;
}
}
}
}
// 关闭文件描述符
close(lfd);
return 0;
}
运行结果

基于多线程的边沿非阻塞处理
运行结果:
#include <iostream>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <cstring>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <thread>
struct SocketInfo
{
int fd;
int epfd;
};
void *acceptConnection(void *arg) {
std::cout << "子线程id:" << std::this_thread::get_id() << std::endl;
SocketInfo* info = (struct SocketInfo*)arg;
int cfd = accept(info->fd, nullptr, nullptr);
// 修改文件描述符的默认属性为非阻塞的
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 把通信描述符再放入epoll树中
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(info->epfd, EPOLL_CTL_ADD, cfd, &ev);
delete info;
return nullptr;
}
void *communication(void *arg) {
SocketInfo* info = (struct SocketInfo*)arg;
char buff[5]; // 接收数据
while (1) {
int len = recv(info->fd, buff, sizeof(buff), 0);
if (len > 0) {
// std::cout << "客户端发送:" << buff << std::endl;
// 因为每次只读5个字节,结尾没有\\0,换一种方式显示在终端
write(STDOUT_FILENO, buff, len);
send(info->fd, buff, len, 0); // 服务器发送
} else if (len == 0) {
std::cout << "客户端断开连接" << std::endl;
epoll_ctl(info->epfd, EPOLL_CTL_DEL, info->fd, nullptr);
close(info->fd);
break;
}else {
// 连接失败
if (errno == EAGAIN) {
std::cout << "数据接送完毕" << std::endl;
break;
} else {
std::cerr << "recv error" << std::endl;
break;
}
}
}
delete info;
return nullptr;
}
int main() {
// 1、创建监听套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if (lfd < 0) {
std::cerr << "socket error" << std::endl;
return -1;
}
// 2。将监听套接字与本地ip端口绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY; // 0 = 0.0.0.0 自动绑定本地的ip地址
int ret = bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
if (ret < 0) {
std::cerr << "bind error" << std::endl;
return -1;
}
// 3.监听
ret = listen(lfd, 128);
if (ret < 0) {
std::cerr << "listen error" << std::endl;
return -1;
}
// 4.创建epoll实例
int epfd = epoll_create(1);
if (epfd == -1) {
std::cerr << "epoll_create error" << std::endl;
return -1;
}
// 5. 把监听描述符添加到epoll中
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; // 边沿触发模式
ev.data.fd = lfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
if (ret == -1) {
std::cerr << "epoll_ctl error" << std::endl;
return -1;
}
// 检测
struct epoll_event evs[1024];
int size = sizeof(evs) / sizeof(evs[0]);
while (1) {
int num = epoll_wait(epfd, evs, size, -1);
std::cout << "num :" << num << std::endl;
for (int i = 0; i < num; ++i) {
int fd = evs[i].data.fd;
struct SocketInfo* info = new SocketInfo();
info->fd = fd;
info->epfd = epfd;
// 监听文件描述符,则建立连接
if (fd == lfd) {
std::thread t1(acceptConnection, info);
t1.detach();
} else {
// 通信文件描述符
std::thread t2(communication, info);
t2.detach();
}
}
}
// 关闭文件描述符
close(lfd);
return 0;
}




