珠海建设局网站首页,中山seo,如何给网站添加统计代码,北京西城网站建设公司1. IO 多路转接之select
1.1 select概述
select 是系统提供的一个多路转接接口#xff0c;其核心工作在于等待。它能够让程序同时监视多个文件描述符上的事件是否就绪#xff0c;只有当被监视的多个文件描述符中有一个或多个事件就绪时#xff0c;select 才会成功返回其核心工作在于等待。它能够让程序同时监视多个文件描述符上的事件是否就绪只有当被监视的多个文件描述符中有一个或多个事件就绪时select 才会成功返回并将对应文件描述符的就绪事件告知调用者。
1.2 select函数 函数原型int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);参数说明 nfds需要监视的文件描述符中最大的文件描述符值 1。readfds输入输出型参数。调用时用户告知内核需监视哪些文件描述符的读事件是否就绪返回时内核告知用户哪些文件描述符的读事件已就绪。writefds输入输出型参数。调用时告知内核需监视哪些文件描述符的写事件是否就绪返回时告知哪些文件描述符的写事件已就绪。exceptfds输入输出型参数。调用时告知内核需监视哪些文件描述符的异常事件是否就绪返回时告知哪些文件描述符的异常事件已就绪。timeout输入输出型参数。调用时由用户设置 select 的等待时间返回时表示 timeout 的剩余时间。其取值有以下几种情况 NULL/nullptrselect 调用后进行阻塞等待直至被监视的某个文件描述符上的某个事件就绪。0select 调用后进行非阻塞等待无论被监视的文件描述符上的事件是否就绪select 检测后都会立即返回。特定的时间值select 调用后在指定时间内进行阻塞等待若被监视的文件描述符上一直无事件就绪则在该时间后 select 进行超时返回。 返回值说明 若函数调用成功则返回有事件就绪的文件描述符个数。若 timeout 时间耗尽则返回0。若函数调用失败则返回 -1同时错误码会被设置可能的错误码有 EBADF文件描述符为无效的或该文件已关闭。EINTR此调用被信号所中断。EINVAL参数 nfds 为负值。ENOMEM核心内存不足。 1.3 fd_set结构
fd_set 结构与 sigset_t 结构类似本质是一个位图通过位图中对应的位来表示要监视的文件描述符。在调用 select 函数之前需用 fd_set 结构定义出对应的文件描述符集然后将需监视的文件描述符添加到该集合中。
/* fd_set for select and pselect. */
typedef struct
{/* XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */#ifdef _USE_XOPEN__fd_mask fds_bits[__FD_SETSIZE / __NDBITS];#define _FDS_BITS(set) ((set)-fds_bits)#else__fd_mask _fds_bits[__FD_SETSIZE / __NDBITS];#define _FDS_BITS(set) ((set)-_fds_bits)#endif
} fd_set;
typedef long int _fd_mask;这个添加过程虽本质是位操作但系统提供了一组专门接口来操作 fd_set 类型的位图如下
void FD_CLR (int fd, fd_set *set); // 用来清除描述词组 set 中相关 fd 的位
int FD_ISSET (int fd, fd_set *set); // 用来测试描述词组 set 中相关 fd 的位是否为真
void FD_SET (int fd, fd_set *set); // 用来设置描述词组 set 中相关 fd 的位
void FD_ZERO (fd_set *set); // 用来清除描述词组 set 的全部位1.4 timeval结构
传入 select 函数的最后一个参数 timeout是一个指向 timeval 结构的指针。timeval 结构用于描述一段时间长度该结构包含两个成员其中 tv_sec 表示秒tv_usec 表示微秒。
struct timeval {__kernel_time_t tv_sec; /* seconds */__kernel_suseconds_t tv_usec; /* microseconds */
};总的来说select 机制为程序同时处理多个文件描述符的事件就绪情况提供了一种有效的方式通过合理设置其参数及利用相关结构的操作接口能较好地实现对多个文件描述符的监控与处理不过在使用过程中也需要注意处理可能出现的各种返回情况及错误码。
1.5 socket 就绪条件
1.5.1 读事件就绪条件 接收缓冲区字节数足够 当 socket 内核中接收缓冲区的字节数大于等于低水位标记 SO_RCVLOWAT 时可以无阻塞地读取该文件描述符且读取返回值大于0。 对端关闭连接 在 socket TCP 通信中如果对端关闭连接那么对该 socket 进行读操作时会返回0。 监听socket有新连接请求 对于监听的socket当有新的连接请求到来时该socket处于读就绪状态。这是服务器端socket常见的就绪情况用于接受新的客户端连接。 socket有未处理错误 当socket上存在未处理的错误时它也处于读就绪状态。这种情况需要及时处理错误以确保socket的正常运行。 1.5.2 写事件就绪条件 发送缓冲区有足够空间 当 socket 内核中发送缓冲区的可用字节数大于等于低水位标记 SO_SNDLOWAT 时可以无阻塞地进行写操作且写操作返回值大于0。 写操作被关闭 当 socket 的写操作被关闭例如通过 close 或 shutdown 函数后对这个写操作被关闭的 socket 进行写操作会触发 SIGPIPE 信号。 非阻塞 connect 操作完成成功或失败 当 socket 使用非阻塞 connect 连接操作完成无论是连接成功还是失败后该 socket 处于写就绪状态。 socket 有未读取错误 当 socket 上存在未读取的错误时它处于写就绪状态。 1.5.3 异常事件就绪 收到带外数据 当 socket 收到带外数据时处于异常就绪状态。带外数据与TCP的紧急模式相关通过TCP报头中的 URG 标志位和16位紧急指针搭配使用来发送和接收带外数据。 2. 服务端代码
然后我们就可以编写一个基于 select多路转接的 TCP 服务端
#pragma once
#include iostream
#include string
#include unistd.h
#include cstring
#include sys/types.h
#include sys/stat.h
#include sys/socket.h
#include arpa/inet.h
#include netinet/in.h// 定义可能出现的错误码
enum
{SocketErr 1,BindErr,ListenErr
};// 定义最大连接数
const int backlog 10;class Sock
{
public:Sock() {}public:// 创建套接字void Socket(){// 使用IPv4协议族流式套接字TCP创建套接字_sockfd socket(AF_INET, SOCK_STREAM, 0);if (_sockfd 0){// 如果创建套接字失败输出错误信息并退出程序std::cerr socket error... std::endl;exit(SocketErr);}int opt 1;// 设置套接字选项允许地址重用setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, opt, sizeof(opt));}// 绑定套接字到指定端口void Bind(uint16_t port){struct sockaddr_in local;// 初始化结构体memset(local, 0, sizeof(local));local.sin_family AF_INET;// 将端口转换为网络字节序local.sin_port htons(port);// 绑定任意本地IP地址local.sin_addr.s_addr INADDR_ANY;if (bind(_sockfd, (const struct sockaddr *)local, sizeof(local)) 0){// 如果绑定失败输出错误信息并退出程序std::cerr bind error... std::endl;exit(BindErr);}}// 监听套接字void Listen(){if (listen(_sockfd, backlog) 0){// 如果监听失败输出错误信息并退出程序std::cerr listen error... std::endl;exit(ListenErr);}}// 接受客户端连接int Accept(std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len sizeof(peer);// 接受客户端连接返回新的套接字描述符int newfd accept(_sockfd, (struct sockaddr *)peer, len);if (newfd 0){std::cout accept error... std::endl;return -1;}char ipstr[64];// 将网络字节序的IP地址转换为点分十进制字符串inet_ntop(AF_INET, peer.sin_addr, ipstr, sizeof(ipstr));*clientip ipstr;// 将网络字节序的端口转换为主机字节序*clientport ntohs(peer.sin_port);return newfd;}// 连接到指定IP和端口bool Connect(const std::string ip, const uint16_t port){struct sockaddr_in peer;memset(peer, 0, sizeof(peer));peer.sin_family AF_INET;peer.sin_port htons(port);// 将点分十进制IP字符串转换为网络字节序inet_pton(AF_INET, ip.c_str(), peer.sin_addr);int n connect(_sockfd, (const struct sockaddr *)peer, sizeof(peer));if (n -1){// 如果连接失败输出错误信息并返回falsestd::cerr connect to ip : port error std::endl;return false;}return true;}// 关闭套接字void Close(){close(_sockfd);}// 获取套接字描述符int Fd(){return _sockfd;}private:int _sockfd;
};#pragma once
#include Sock.hpp
#include sys/select.h// 定义默认文件描述符值
#define DFL_FD -1
// 定义文件描述符数组的大小
#define NUM 128class SelectServer
{
public:// 构造函数初始化服务器监听端口SelectServer(int port): _port(port){}// 初始化服务器相关设置包括创建、绑定和监听套接字void InitSelectServer(){// 创建套接字_listensock.Socket();// 将套接字绑定到指定端口_listensock.Bind(_port);// 开始监听套接字_listensock.Listen();}// 运行服务器处理客户端连接和数据读取等操作void Run(){fd_set readfds;int fd_array[NUM];// 初始化文件描述符数组将所有元素设为默认值for (int i 0; i NUM; i){fd_array[i] DFL_FD;}// 将监听套接字的文件描述符放入数组的第一个位置fd_array[0] _listensock.Fd();while (true){// 清空读文件描述符集合FD_ZERO(readfds);int maxfd DFL_FD;// 遍历文件描述符数组将有效的文件描述符添加到读文件描述符集合中并更新最大文件描述符值for (int i 0; i NUM; i){if (fd_array[i] DFL_FD)continue;FD_SET(fd_array[i], readfds);if (fd_array[i] maxfd){maxfd fd_array[i];}}// 调用select函数等待事件发生// struct timeval timeout {2, 0};switch (select(maxfd 1, readfds, nullptr, nullptr, nullptr)){case 0:// 如果select返回0表示超时// std::cout time out... std::endl;break;case -1:// 如果select返回 -1表示发生错误输出错误信息std::cerr select error std::endl;break;default:// 如果select正常返回调用HandlerEvent处理就绪事件HandlerEvent(readfds, fd_array, NUM);break;}}}// 析构函数关闭监听套接字~SelectServer(){if (_listensock.Fd() 0){_listensock.Close();}}private:// 处理就绪事件的函数void HandlerEvent(const fd_set readfds, int fd_array[], int num){for (int i 0; i num; i){if (fd_array[i] DFL_FD)continue;// 如果是监听套接字且有可读事件表示有新的客户端连接if (fd_array[i] _listensock.Fd() FD_ISSET(fd_array[i], readfds)){struct sockaddr_in peer;socklen_t len sizeof(peer);memset(peer, 0, len);std::string clientip;uint16_t clientport;// 接受新的客户端连接获取客户端的套接字描述符、IP地址和端口号int sock _listensock.Accept(clientip, clientport);std::cout get a new link[ clientip : clientport ] std::endl;// 将新连接的套接字描述符放入文件描述符数组中如果数组已满则关闭该套接字并输出提示信息if (!SetFdArray(fd_array, num, sock)){close(sock);std::cout select server is fullclose fd sock std::endl;}}// 如果不是监听套接字且有可读事件表示有数据可读进行数据读取和处理else if (FD_ISSET(fd_array[i], readfds)){char buffer[1024];ssize_t n read(fd_array[i], buffer, sizeof(buffer) - 1);if (n 0){// 如果读取到数据添加字符串结束符并输出数据内容buffer[n] 0;std::cout echo# buffer std::endl;}else if (n 0){// 如果读取到的字节数为0表示客户端已断开连接关闭对应的套接字并将数组元素设为默认值std::cout client quit... std::endl;close(fd_array[i]);fd_array[i] DFL_FD;}else{// 如果读取发生错误输出错误信息关闭对应的套接字并将数组元素设为默认值std::cerr read error std::endl;close(fd_array[i]);fd_array[i] DFL_FD;}}}}// 将新的套接字描述符放入文件描述符数组中的函数bool SetFdArray(int fd_array[], int num, int fd){for (int i 0; i num; i){if (fd_array[i] DFL_FD){fd_array[i] fd;return true;}}return false;}private:Sock _listensock;int _port;
};服务器当前调用select函数时将timeout参数设置为nullptr这使得select函数调用后会进入阻塞等待状态。
起初服务器第一次调用select函数时仅让其监视监听套接字的读事件。如此一来在服务器运行后若没有客户端发送连接请求监听套接字的读事件就不会变为就绪状态那么服务器就会一直在这第一次调用的select函数中持续阻塞等待下去。 当我们利用telnet工具向该select服务器发起连接请求时情况就会发生变化。此时select函数能够立刻检测到监听套接字的读事件已经就绪进而select函数会成功返回。并且执行相应的事件处理。 3. select 的缺陷
虽然 select可以实现多路转接提升 IO 效率。但是我们在实际应用中很少会用到 select因为 每次调用 select 时都需要手动设置fd集合从接口使用的便捷性角度来看这种操作方式较为繁琐给开发者带来了不便。每次调用 select都要把 fd 集合从用户态拷贝到内核态。当需要监控的文件描述符数量很多时这种数据拷贝操作所产生的开销会变得很大影响系统性能。每次调用 select内核都需要遍历传递进来的所有 fd。同样在 fd 数量众多的情况下这个遍历过程所消耗的系统资源也会很大进一步降低系统的运行效率。 并且 select 可监控的文件描述描述符数量取决于 fd_set 类型的比特位个数。一般情况下 select 可监控的文件描述符个数通常为1024个。这在实际应用中是一个较大的局限例如在实现 select 服务器时除去一个监听套接字最多只能连接1023个客户端对于一些需要处理大量并发连接的场景这个数量可能远远不够。