IO多路复用之select
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
第一个入参nfds为所传入的最大文件描述符+1
第二个入参readfds为可读事件对应的描述符集合
第三个入参writefds为可写事件对应的描述符集合
第四个入参exceptfds为异常事件对应的描述符集合
第五个入参timeout超时时间,如果设置为NULL永远阻塞;如果timeval结构的两个字段都为零,则select立即返回
struct timeval结构如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
void FD_ZERO(fd_set *set); // 清空监控集合
void FD_SET(int fd, fd_set *set); // 添加要监控的描述符到集合中
int FD_ISSET(int fd, fd_set *set); // 判断要监控的描述符是否在集合中存在
void FD_CLR(int fd, fd_set *set); // 集合中去除指定描述符
注意:
①linux中每个程序能够打开的最多文件描述符是有限制的。默认是1024,Linux上执行ulimit -n可查看,
select第一个参数也受此影响
②select第二到第四个参数是一个固定大小的缓冲区,是有边界值的即FD_SETSIZE
下面是man select的原话
An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.
用select实现的TCP通信例子:
server.cpp
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <map>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define CHECK_RETURN_EXIT(ret) \
if (ret != 0) { \
exit(-1); \
}
class TCPServer {
public:
TCPServer() : sockFd(-1) {}
int Socket()
{
sockFd = socket(AF_INET, SOCK_STREAM, 0);
if (sockFd == -1) {
perror("socket");
return -1;
}
return 0;
}
int Bind()
{
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
// 或者另一种写法 addr.sin_port = htons("127.0.0.1");
addr.sin_port = inet_addr("127.0.0.1"); // 将ip地址转换成网络字节序
addr.sin_port = htons(8081); // 将端口转换成网络字节序
if (bind(sockFd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
perror("bind");
return -1;
}
return 0;
}
int Listen()
{
if (listen(sockFd, SOMAXCONN) != 0) {
perror("listen");
return -1;
}
return 0;
}
std::pair<int, struct sockaddr_in> Accept()
{
struct sockaddr_in clientAddr;
int len = sizeof(clientAddr);
// int newSocket = accept(sockFd, NULL, NULL); 不需要 clientAddr可以填NULL
int newSocket = accept(sockFd, (struct sockaddr*)&clientAddr, (socklen_t*)&len);
if (newSocket == -1) {
perror("accept");
}
return std::make_pair(newSocket, clientAddr);
}
int GetSockFd() { return sockFd; }
// 设置被绑定的端口在断连后可以立即重用
void SetSocketReuseAddr()
{
bool bReuseaddr = true;
setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, (const char*)&bReuseaddr, sizeof(bool));
}
private:
int sockFd;
};
int main() {
TCPServer tcpServer;
CHECK_RETURN_EXIT(tcpServer.Socket());
tcpServer.SetSocketReuseAddr();
CHECK_RETURN_EXIT(tcpServer.Bind());
CHECK_RETURN_EXIT(tcpServer.Listen());
int sockFd = tcpServer.GetSockFd();
fd_set fdSet;
std::map<int, struct sockaddr_in> socketMap{};
char buf[1024] = {};
for (;;) {
FD_ZERO(&fdSet);
// 通过select监控sock描述符和输入缓冲区
FD_SET(STDIN_FILENO, &fdSet);
FD_SET(sockFd, &fdSet);
for (auto &cit : socketMap) {
FD_SET(cit.first, &fdSet);
}
int ret = select(1023, &fdSet, NULL, NULL, NULL);
// select轮训到集合中至少有一个描述符可读
if (ret > 0) {
// 新连接到来
if (FD_ISSET(sockFd, &fdSet)) {
std::pair<int, struct sockaddr_in> tmpNewSockPair(tcpServer.Accept());
if (tmpNewSockPair.first != -1) {
socketMap.insert(tmpNewSockPair);
continue;
}
}
// 老连接可读
for (auto itr = socketMap.begin(); itr != socketMap.end(); ++itr) {
if (FD_ISSET(itr->first, &fdSet)) {
memset(buf, 0, sizeof(buf));
ret = recv(itr->first, buf, sizeof(buf), 0);
if (ret == -1) {
std::cout << "recv from client ip:" << inet_ntoa(itr->second.sin_addr) << " port:" << ntohs(itr->second.sin_port) << " failed" << std::endl;
close(sockFd);
for (auto &cit : socketMap) {
close(cit.first);
}
return -1;
} else if (ret == 0) {
std::cout << "recv from client ip:" << inet_ntoa(itr->second.sin_addr) << " port:" << ntohs(itr->second.sin_port) << " disconnect" << std::endl;
socketMap.erase(itr);
close(itr->first);
continue;
}
std::cout << "recv from client ip:" << inet_ntoa(itr->second.sin_addr) << " port:" << ntohs(itr->second.sin_port) << " message: " << buf;
}
}
// 标准输入可读,将输入的信息发送给对端
if (FD_ISSET(STDIN_FILENO, &fdSet)) {
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
for (auto &cit : socketMap) {
ret = send(cit.first, buf, sizeof(buf) - 1, 0);
}
}
}
}
close(sockFd);
for (auto &cit : socketMap) {
close(cit.first);
}
return 0;
}
client.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#define ERR_EXIT(m) \
do { \
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int do_service(int sockfd)
{
fd_set fdSet;
char buf[1024] = {};
for (;;) {
FD_ZERO(&fdSet);
FD_SET(STDIN_FILENO, &fdSet);
FD_SET(sockfd, &fdSet);
int ret = select(sockfd + 1, &fdSet, NULL, NULL, NULL);
// select轮训到集合中至少有一个描述符可读
if (ret > 0) {
// newSocket可读
if (FD_ISSET(sockfd, &fdSet)) {
memset(buf, 0, sizeof(buf));
ret = recv(sockfd, buf, sizeof(buf), 0);
if (ret == -1) {
std::cout << "recv from server failed" << std::endl;
return -1;
} else if (ret == 0) {
std::cout << "recv from server disconnect" << std::endl;
return -1;
}
std::cout << "recv from server message: " << buf;
}
// 标准输入可读,将输入的信息发送给对端
if (FD_ISSET(STDIN_FILENO, &fdSet)) {
memset(buf, 0, sizeof(buf));
read(STDIN_FILENO, buf, sizeof(buf));
ret = send(sockfd, buf, sizeof(buf) - 1, 0);
}
}
}
}
int main(int argc, const char *argv[])
{
int peerfd = socket(PF_INET, SOCK_STREAM, 0);
if(peerfd == -1)
ERR_EXIT("socket");
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8081);
socklen_t len = sizeof addr;
if(connect(peerfd, (struct sockaddr*)&addr, len) == -1)
ERR_EXIT("Connect");
do_service(peerfd);
close(peerfd);
return 0;
}