青岛模板网站做网站用什么框架

当前位置: 首页 > news >正文

青岛模板网站,做网站用什么框架,盐城网站制作,网店运营推广高级实训教程多路I/O转接服务器是一种高效管理多个I/O操作的技术#xff0c;核心目的是允许单线程或单进程同时监控和处理多个I/O事件。 另外为什么需要I/O多路转接呢#xff1f; 传统阻塞I/O模型中#xff0c;处理多个客户端连接的常见方式是#xff1a;为每个客户端创建一个独立线程…多路I/O转接服务器是一种高效管理多个I/O操作的技术核心目的是允许单线程或单进程同时监控和处理多个I/O事件。 另外为什么需要I/O多路转接呢 传统阻塞I/O模型中处理多个客户端连接的常见方式是为每个客户端创建一个独立线程线程阻塞在read()或write()操作上等待数据到达。这样会大幅度增加资源消耗另外我们如果每个套接字设置为非阻塞然后采用单线程轮询所有连接会造成CPU的浪费如果采用多线程依旧属于治标不治本。 这时候就体现出I/O多路转接的核心价值了用单线程高效管理多连接。不仅能大幅度减少资源消耗还能高效处理连接多活跃少的场景。还简化了代码不用考虑多线程锁的竞争死锁等问题。 select 多路转接核心作用对多个文件描述符进行等待,并通知上层哪些fd已经就绪本质是一种对IO事件就绪的通知机制 #include sys/select.hint select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval timeout);nfds监控的文件描述符集里最大文件描述符加1因此参数会告诉内核前多少个文件描述符的状态。 struct timeval {time_t tv_sec; / seconds /suseconds_t tv_usec; / microseconds */};struct timeval *timeout1.如果设置为NULL表示阻塞等待 等待多个fd至少有一个fd就绪select就会返回  2.如果timeout设置为{0,0}表示非阻塞等待 有多个fd没有一个就绪也立即返回。如果有就绪也是立即返回。3.如果timeout设置为{5,0}表示5s以内阻塞超时后立即返回。         另外timeout为输入输出型 什么是输出当select返回的时候表示还剩余多少事件例子:如果输入timeout为{5,0}超时返回输出为{00}如果在2秒内接受到一个fd那么输出为{3,0} timeout 返回值有3种 1. n 0 : n就绪了多少个fd 2. n(-1) 0: select等待失败了 3.n 0:底层fd没有就绪也没有出错 与 timeout配合使用 fd_set *readfds 读文件描述符集关心读事件-fd是否可读-接受缓冲区是否有数据 fd_set *writefds 写文件描述符集关心写事件-fd是否可写-发送缓冲区是否有空间 fd_set exceptfds 异常文件描述符集关心异常事件-fd是否出现异常-fd错误的fd fd_set本质是一个位图用位图中对应的位来表示要监视的文件描述符。 fd_set是OS给用户提供的一种具体的数据类型 (固定大小)也就是说fd_set能够包含的fd的个数是有上限的可以添加多个文件描述符01234….. / fd_set for select and pselect. / typedef struct{/ XPG4.2 requires this member name. Otherwise avoid the namefrom the global namespace. */ #ifdef USE_XOPENfd_mask fds_bits[__FD_SETSIZE / __NFDBITS];

define __FDS_BITS(set) ((set)-fds_bits)

#else__fd_mask fds_bits[FD_SETSIZE / __NFDBITS];

define FDS_BITS(set) ((set)-fds_bits)

#endif} fd_set; int main() {fd_set fds;std::cout fds: sizeof(fds) * 8 std::endl; // sizeof(fds)表示字节数*8表示位数 } 在我的机器上fd的个数为1024。 再举个readfds的具体例子 1.fd_set在输入的时候用户告诉内核你要帮我关心readfds位图中被设置了的fd上的读事件比如0101 0010 表示关心146(左向右从)文件描述符的读事件。 2.输出的时候内核告诉用户你让我关心的readfds中有哪些fd已经就绪了 0000 0010 表示1号位置的读事件已经就绪了而46位置的没有就绪而就绪的文件描述符就进行读取读取一次的时候一定不会被阻塞因为对应的fd的读事件已经就绪了。 此外每次调用select都要对输入设置参数进行重新设置为什么因为我每次输出从内核告诉用户的时候已经修改了readfds中的位图如上面的例子1号位置已经就绪了难道就不用管46的位图了吗也就是说我们要对历史上所有的fd进行服务器保存起来方便我们多次添加到fd_set中如果还是没有理解可以看看下面的代码 另外对fd_set位图操作 系统提供了对应的封装 void FD_CLR(int fd, fd_set *set);int FD_ISSET(int fd, fd_set *set);void FD_SET(int fd, fd_set *set);void FD_ZERO(fd_set *set);代码  这里先复习一下对应的系统调用中的函数 就是对系统调用相关的函数以及数据结构进行解析了解。想了解select代码可以跳过这一部分 int socket(int domain, int type, int protocol); domaindomain参数指定通信域 这是选择用于通信的协议族。这些族在sys/socket.h中定义。控件当前可以理解的格式Linux内核包括 type 套接字具有指定的类型它指定通信语义。目前定义的类型有  当domain和type组合存在多种协议实现时protocol用于精确指定使用哪种协议不过protocol默认写0 return 返回值 成功返回一个新的文件描述符失败返回-1。 int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); 这个sockfd就是我们所创建socket的返回值。 操作系统在内部定义了一套自己的数据结构就是struct sockaddr先看看下面的图 socket的种类会多一点1.网络socket 本地网络 2.本地socket(unix域间socket) 3.原始socket 这里我侧重说一下12  struct sockaddr_in表示网络通信in-inetstruct sockaddr_un表示本地通信un-unix。 这三者struct sockaddr struct sockaddr_instruct sockaddr_un有什么区别其实可以理解C中的基类与派生类之间的关系struct sockaddr为基类其余那两个都是派生类。 struct sockaddr为了区分是本地通信还是网络通信在struct sockaddr前两个比特位设置了16位地址类型标识是本地还是网络通信其实这前两个比特位就是宏定义。 写一个伪代码判断一下是否是网络通信还是本地通信 if(addr - add_type AF_INET) net else unix socklen_t len 其实就是绑定 struct sockaddr addr字节数长度。  return 返回值 成功返回0失败返回-1并设置全局变量errno以指示错误类型。 这里写一下绑定的过程吧 先了解一下sockaddr_in中的数据结构 / Structure describing an Internet socket address. / struct sockaddr_in{__SOCKADDRCOMMON (sin); //sin_familyin_port_t sin_port; / Port number. /struct in_addr sin_addr; / Internet address. /// 下面的是填充字段/ Pad to size of struct sockaddr. /unsigned char sin_zero[sizeof (struct sockaddr)- __SOCKADDR_COMMON_SIZE- sizeof (in_port_t)- sizeof (struct in_addr)];}; 写一个简单的TCP服务端bind过程     int sockfd socket(AF_INET, SOCK_STREAM, 0);// 创建一个基于 IPv4 地址族(AF_INET)的 TCP 套接字(SOCK_STREAM)// 第三个参数为 0 表示使用默认协议(TCP)struct sockaddr_in local;bzero(local, sizeof(local));// 注意这里只是填充了结构体local.sin_family AF_INET;// 设置地址族为 IPv4必须与 socket() 函数的第一个参数一致// 这告诉系统如何解析后续的地址信息local.sin_port htons(8080); // 要发送到网络中  主机 - 网络local.sin_addr.s_addr INADDR_ANY; // 也可以这么写::inet_addr(127.0.0.1)// 本质就是string ip-4bytes 2.转为网络序列network order // bind设置进入内核中int n ::bind(sockfd, (struct sockaddr)local, sizeof(local)); 下面聊一下大小端 因为主机到网络中需要转成大端字节序列 比如我的机器上就是小端存储  小端存储模式低位字节存于低地址高位字节存于高地址。在0x11223344中0x44是低位字节存储在低地址0x11是高位字节存储在高地址。若从内存地址0x1000开始存储存储顺序为 | 内存地址 | 存储内容 | |0x1000|0x44| |0x1001|0x33| |0x1002|0x22| |0x1003|0x11| 大端存储模式高位字节存于低地址低位字节存于高地址。对于0x11223344 0x11作为高位字节存于低地址0x44作为低位字节存于高地址。假设从内存地址0x1000开始存储存储顺序为 | 内存地址 | 存储内容 | |0x1000|0x11| |0x1001|0x22| |0x1002|0x33| |0x1003|0x44| tcp需要将socket设置成为监听状态  int listen(int sockfd, int backlog); backlog指定全连接队列的最大长度。 当队列已满时新的连接请求已完成三次握手会被拒绝客户端收到 RST 包。 int accept(int sockfd, struct sockaddr *_Nullable restrict addr,socklen_t *_Nullable restrict addrlen);如果没有人连接会阻塞此外fcntl可以将一个fd设置为非阻塞有兴趣可以了解一下 其中这里比较重要的就是sockfd和这个返回值返回的也是sockfd有什么区别 总而言之就是监听套接字是仅用于接受连接请求可通过listen持续监听多个客户端生命周期从socket创建到close关闭。 连接套接字是主动套接字每个客户端对应一个实例负责与特定客户端进行数据交互read/write/recv/send生命周期从accept返回到close关闭释放资源。  int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);这里connect是客户端要用的系统调用创建sockfd 设置 sockaddr_in这个结构体中的字段然后connect进行连接 recvfrom  sendto 是UDP recv send 是TCP相关的函数。这就不做过多赘述 #pragma once#include iostream #include string #include memory#define NUM 1024 #define gdefaultfd -1using namespace LogMudule; using namespace SocketMudule; class SelectServer { public:SelectServer(int port): _port(port), _listen_socket(std::make_uniqueTcpSocket()), _is_running(false){}~SelectServer(){}void Init(){_listen_socket-BuildTcpSocketMethod(_port);for (int i 0; i NUM; i){_fd_array[i] gdefaultfd;}_fd_array[0] _listen_socket-fd();}void Start(){// 读文件描述符集fd_set rfds; _is_running true;while (_is_running){// 清空rfdsFD_ZERO(rfds);struct timeval timeout {10, 0};int maxfd gdefaultfd;for (int i 0; i NUM; i){if (_fd_array[i] gdefaultfd){continue;}// 将合法的fd加入rfdsFD_SET(_fd_array[i], rfds);// 更新maxfdif (_fd_array[i] maxfd){maxfd _fd_array[i];}}// 我们不能让accept来阻塞检测新连接到来而应该让select来负责进行就绪事件的检测int n select(maxfd 1, rfds, nullptr, nullptr, timeout);switch (n){case 0:std::cout select timeout std::endl;break;case -1:perror(select error);break;default:// 有事件就绪// rfds: 内核告诉用户你关心的rfds中的fd有哪些已经就行了std::cout 有事件就绪…., timeout timeout.tv_sec std::endl;HandlerEvent(rfds);break;}}_is_running false;}void HandlerEvent(fd_set rfds){for (int i 0; i NUM; i){if (_fd_array[i] gdefaultfd)continue;// 如果是listen_socketif (_fd_array[i] _listen_socket-fd()){// 判断listensockfd是否在rfdsif (FD_ISSET(_listen_socket-fd(), rfds)){InetAddr client;// listen_socket有新连接到来int newfd _listen_socket-Accepter(client); // 这里不会被阻塞因为我们在select中注册了listen_socketif (newfd 0){std::cout accept error std::endl;return;}else{std::cout Get new client newfd connected std::endl;// 这里能直接recv? 读事件是否就绪我们并不清楚所以要进行托管。让select帮我关心新的sockfd上面的读事件就绪// 如果不能直接recv那么就需要自己维护一个读缓冲区然后在select中注册读缓冲区然后在读事件就绪的时候把读到的内容放到读缓冲区中// 然后在HandlerEvent中从读缓冲区中取出数据然后处理// 如何把newfd托管给select来管理把newfd加入到_fd_array中int pos -1;for (int j 0; j NUM; j){if (_fd_array[j] gdefaultfd){pos j;break;}}if (pos -1){LOG(LogLevel::ERROR) 服务器满载….;close(newfd);}else{_fd_array[pos] newfd;}}}}else{if (FD_ISSET(_fd_array[i], rfds)){// 合法的就绪的普通的fdchar buffer[1024];// 这里的recv对不对呢不完善要把这个写对必须有协议ssize_t n recv(_fd_array[i], buffer, sizeof(buffer) - 1, 0); // select告诉我已经就绪了if (n 0){buffer[n] \0;std::cout client buffer std::endl;// 把读到的信息在回显回去std::string message echo# std::string(buffer);send(_fd_array[i], message.c_str(), message.size(), 0); // bug}else if (n 0){LOG(LogLevel::DEBUG) 客户端退出sockfd _fd_array[i];close(_fd_array[i]);_fd_array[i] gdefaultfd;}else{LOG(LogLevel::DEBUG) 客户端读取退出sockfd _fd_array[i];close(_fd_array[i]);_fd_array[i] gdefaultfd;}}}}}private:uint16_t _port;std::unique_ptrSocket _listen_socket;bool _is_running;int _fd_array[NUM]; }; select的特点 可监控的文件描述符个数取决于 sizeof(fd_set)的值 fd 加入 select 监控集的同时还要再使用一个数据结构 array 保存放到 select监控集中的 fd 注fd_set 的大小可以调整可能涉及到重新编译内核。 select缺点 每次调用 select, 都需要手动设置 fd 集合readfds输入输出fd_set每次都需要修改 每次调用 select都需要把 fd 集合从用户态拷贝到内核态这个开销在 fd 很多时会很大位图修改 同时每次调用 select 都需要在内核遍历传递进来的所有 fd这个开销在 fd 很多时也很大。 select 支持的文件描述符数量太小虽然进程打开fd是有上限的但是不管select中fd有上限的理由