------一个网络io与io多路复用的学习笔记#include errno.h #include stdio.h #include sys/socket.h #include netinet/in.h #include string.h #include pthread.h #include unistd.h #include poll.h #include sys/epoll.h以上是需要引的头文件 以下是main函数中具体的代码实现1.第一种#if 0int sockfd socket(AF_INET,SOCK_STREAM,0); //IPv4 地址族。流式套接字tcp 创建一个tcp socket struct sockaddr_in servaddr; //定义服务器地址结构体 告诉操作系统用什么协议族 绑定哪个 IP 绑定哪个端口 memset(servaddr, 0, sizeof(servaddr)); servaddr.sin_family AF_INET; //前面 socket用的是 AF_INET这里也必须使用 servaddr.sin_addr.s_addr htonl(INADDR_ANY); // 设置服务器的 IP 地址。 //sin_addr 是一个 IP 地址结构 s_addr 是里面真正存放 IP 的整数形式 INADDR_ANY 的值本质上表示绑定本机所有可用网卡地址 0.0.0.0 //htonl : 把主机字节序转换成网络字节序。 servaddr.sin_port htons(2000); //设置端口号 0 ~ 1023 是知名端口很多系统服务会用普通用户程序最好别乱占 1024 以上更适合自己写程序测试 if(bind(sockfd,(struct sockaddr*)servaddr,sizeof(struct sockaddr)) -1){ printf(bind failed : %s\n,strerror(errno)); }//bind : 把 socket 和一个具体的地址绑定起来 第二个参数需要强转强转成 struct sockaddr* listen(sockfd,10); //把这个 socket 从“普通 socket”变成“监听 socket” 10:等待队列里最多允许积压多少个还没处理的连接请求 printf(listen finished\n); struct sockaddr_in clientaddr; //创建一个用来存“客户端地址信息”的结构体 socklen_t len sizeof(clientaddr); //接字相关 API 规定地址长度参数一般用socklen_t这段代码整体做的事情其实就是从零开始建立一个小型的TCP服务器先用socket(AF_INET, SOCK_STREAM, 0)创建一个基于 IPv4 TCP 的套接字相当于向操作系统申请了一个“通信端点”接着定义一个struct sockaddr_in servaddr用来描述服务器自己的地址信息包括协议族、IP、端口并用memset清零避免脏数据然后设置sin_family AF_INET指定 IPv4sin_addr.s_addr htonl(INADDR_ANY)表示绑定本机所有网卡也就是 0.0.0.0任何发到这台机器的连接都能接sin_port htons(2000)指定监听端口同时通过htons/htonl把主机字节序转换为网络字节序保证跨平台一致之后调用bind把这个 socket 和刚才配置好的“IP 端口”绑定在一起这一步相当于正式告诉操作系统“我要在这个地址上提供服务”如果失败就打印错误信息再调用listen(sockfd, 10)把这个普通 socket 转成监听 socket并设置一个长度为 10 的“半连接/全连接等待队列”表示最多可以有 10 个还没被accept处理的客户端连接在排队最后定义clientaddr和len是为后续accept做准备用来接收客户端的地址信息谁连上来了服务器完成从“创建 → 绑定 → 监听”的全部准备工作接下来只差accept就可以正式接收客户端连接了。但是这里如果加上accept会有一些问题我们只有一个监听sockfd和一个clientfd也就是我一次只能与一台服务器进行通信 也就是只可以实现单路io下面是while循环来多次创建的情况 也无法实现多路io2.第二种#elif 0 while (1) { printf(accept\n); int clientfd accept(sockfd, (struct sockaddr*)clientaddr, len); printf(accept finshed\n); char buffer[1024] {0}; int count recv(clientfd, buffer, 1024, 0); printf(RECV: %s\n, buffer); count send(clientfd, buffer, count, 0); printf(SEND: %d\n, count); }第一次创建fd 会执行accept 然后在recv之前 这个循环会阻塞在recv我同时再想要去用另一个客户端连接的时候 无法执行accept因为循环阻塞在recv了只有我接受到第一个客户端的消息send回消息本次循环结束之后才能开下一个客户端依然无法实现多路io。我们建立一个函数client_thread每有一个新的客户端 就创建出来一个线程专门负责void *client_thread(void *arg){ //它通常会被 pthread_create() 创建出来专门负责处理一个已经连接上的客户端。 //main中每当有一个新的客户端连进来主线程就会得到一个新的clientfd 再创建一个线程 把这个clientfd 交给这个线程去处理 于是每个客户端都由一个单独线程负责。 int clientfd *(int *)arg; //传进来的是 clientfd 的地址 强转成int再解引用 拿到socket 保存到clientfd里面 while(1){ //while循环 只要这个客户端没断开这个线程就一直给它服务。 char buffer[1024] {0}; int count recv(clientfd,buffer,1024,0); //从这个客户端 socket 里读取数据放进 buffer 里。 if(count 0){ //接收到的字符数为0 就break -1:接收错误也break printf(client disconnect : %d\n,clientfd); close(clientfd); //关闭这个fd break; } // parser :这里可以放一些业务 对收到的数据解析处理。 printf(RECV : %s\n,buffer); //把收到的数据按字符串打印出来。 count send(clientfd,buffer,count,0); //把刚才收到的那 count 个字节原封不动发回给客户端。 printf(SEND : %d\n,count); //打印的是send返回值接受到了多少字符。 } return NULL; } /*这个函数的工作流程从参数里取出客户端socket 反复接收客户端发来的数据 打印收到的数据 再把收到的数据发回去 果客户端断开就关闭 socket结束线程。 每来一个客户端就新开一个线程去跑这个函数。这样多个客户端就能“同时”被处理了。属于多线程并发模型*/这个client_thread函数本质上是一个“客户端处理线程函数”它负责专门服务某一个已经连接上的客户端先从传进来的参数里取出客户端 socket 描述符clientfd然后进入死循环不断调用recv接收客户端发送的数据如果返回值是 0说明客户端已经断开连接于是打印断开信息、关闭 socket并退出循环结束线程如果成功收到数据就先打印出来再调用send把收到的数据原样发回去。在main函数的分支#elif 1 while(1){ printf(accept\n); int clientfd accept(sockfd,(struct sockaddr*)clientaddr,len); //接受一个客户端连接 printf(accept finished:%d\n,clientfd); pthread_t thid; //定义线程 ID pthread_create(thid,NULL,client_thread,clientfd); //新建一个线程让它从 client_thread 这个函数开始执行并把 clientfd 传给它。 }主线程始终运行在死循环中不断调用accept等待新的客户端连接一旦有客户端连进来accept就返回一个新的clientfd这个clientfd不再是监听 socket而是专门用于和这个客户端进行通信的连接 socket随后主线程通过pthread_create创建一个新线程并把这个客户端的clientfd交给线程函数client_thread去处理而主线程自己不会和这个客户端继续通信而是立刻回到accept继续等待下一个客户端因此多个客户端到来时就会有多个线程分别同时处理各自的收发过程在线程函数中会不断对该客户端执行recv接收数据、打印数据、再用send把数据原样发回去直到客户端断开连接此时关闭对应的clientfd并结束线程所以这套代码已经可以实现多客户端并发处理。做到了一请求一线程可以多个客户端一起处理但是不利于并发客户端数量太多内核负担过重占用资源比较多下面引入io多路复用select做法#elif 0 fd_set rfds,rset; //fd_set:一个“集合”里面记录了哪些 fd 需要被监视。 //rfds原始监视集合 rset本次 select 使用的临时集合 因为每一次监视都需要修改 我们不能直接改原始的集合 要用总名单复制出一个临时名单这一轮拿临时名单去检测。 //比如当前我要盯这些 fd3监听 socket 4客户端 A 5客户端 B 8客户端 C 那这个 fd_set 里就记着3, 4, 5, 8 FD_ZERO(rfds); //清空集合 rfds 先把监视名单清空 FD_SET(sockfd,rfds); //把 sockfd 加入集合 服务器第一件事就是要知道有没有新客户端来连接。 int maxfd sockfd; //maxfd:当前所有被监视 fd 中最大的那个 fd 值。 一开始就加入了sockfd所以最大的只能是他 while(1){ rset rfds; //把“总监视名单”复制给“本轮检测名单”。 int nready select(maxfd1,rset,NULL,NULL,NULL);//调用 select 这里后面几个参数是rset监视“可读事件 NULL不监视可写 //NULL不监视异常 NULL 无限阻塞等待 如果没有任何 fd 可读就一直卡在这里。 一旦有 fd 可读就返回。 if(FD_ISSET(sockfd,rset)){ //判断监听 socket 是否就绪 也就是是否有新的客户端进来 socket就绪 就accept int clientfd accept(sockfd,(struct sockaddr*)clientaddr,len); //接受连接生成一个新的客户端 fd。 printf(accept finished: %d\n,clientfd); FD_SET(clientfd,rfds); //把这个新的客户端 socket 放进总监视名单。 if(clientfd maxfd) maxfd clientfd; //更新最大 fd。 } //recv int i 0; for(i sockfd 1;i maxfd;i){ //遍历所有客户端 fd看看谁发数据了 从sockfd1开始的原因是 一般先有sockfd然后其他的clientfd都更大 if(FD_ISSET(i,rset)){ //这个客户端 fd 可读可能有数据也可能断开了。 char buffer[1024] {0}; int count recv(i,buffer,1024,0); //recv 接收 if(count 0){ //count 0 证明断开了 printf(client dosconnect: %d\n,i); close(i); //关闭本地 fd FD_CLR(i,rfds); //从总监视集合中删除 continue; } printf(RECV: %s\n,buffer); //正常收到数据回显回去 count send(i,buffer,count,0); printf(SEND: %d\n,count); } } }这段代码实现的是一个基于select的单线程多路tcp服务器程序首先创建一个fd_set类型的集合rfds作为“总监视集合”初始化时只把监听 socketsockfd加入进去同时用maxfd记录当前集合中最大的文件描述符。进入主循环后每一轮都会先把rfds复制到临时集合rset然后调用select(maxfd1, rset, NULL, NULL, NULL)阻塞等待事件发生。select返回后rset中保留下来的 fd 就是“本轮就绪的 fd”也就是需要处理的对象。接着程序先判断监听 socket 是否在rset中如果在说明有新的客户端连接到来于是调用accept创建新的clientfd并把它加入到rfds中同时更新maxfd这样后续就能一起监视这个客户端。然后程序通过for循环遍历sockfd1到maxfd的所有可能客户端 fd对每个 fd 用FD_ISSET(i, rset)判断是否在本轮就绪集合中。如果在说明这个客户端 socket 可读就调用recv读取数据如果返回值为 0说明客户端断开连接此时关闭 fd 并从rfds中移除如果大于 0说明收到了数据就通过send原样发回实现一个简单的回显echo功能。整个程序的本质就是维护一个“被监视的 fd 集合”每一轮通过select找出“当前有事件的 fd”然后分别处理监听 socket负责接新连接和客户端 socket负责收发数据或断开从而在单线程下实现同时处理多个连接。下面是poll的做法/*先把监听 socket sockfd 加入监视列表 用 poll() 一直等待 有没有新客户端连接 有没有某个客户端发来数据 如果有新连接就 accept 如果有客户端发数据就 recv 收到后再 send 回去形成回显服务器 如果客户端断开就关闭并从监视列表中移除*/ #elif 1 struct pollfd fds[1024] {0}; //定义了一个 pollfd 数组大小是 1024。 fds[sockfd].fd sockfd; //把监听 socket 放进 fds 数组中。 fds[sockfd].events POLLIN; //关心可读事件 响应了就是有新的客户端连接来了可以 accept 了。 int maxfd sockfd; //这个 maxfd 表示当前监视范围内最大的 fd 值 while(1){ int nready poll(fds,maxfd1,-1); //三个参数 1.监视的数组 2.数组前多少项要参与检查。3.-1表示永久阻塞等待 if(fds[sockfd].revents POLLIN){ //看监听 socket 这次返回后实际发生的事件里是否包含 POLLIN。包含了就有新的事件 revents是实际发生的事件 int clientfd accept(sockfd,(struct sockaddr*)clientaddr,len); //accept 新连接 printf(accept finished: %d\n,clientfd); fds[clientfd].fd clientfd; //把新客户端也加入 poll 监视列表 fds[clientfd].events POLLIN; if(clientfd maxfd) maxfd clientfd; // 更新最大fd } int i 0; for(i sockfd 1;i maxfd ;i){ if(fds[i].revents POLLIN){ //判断某个客户端是否可读 char buffer[1024] {0}; //先准备缓冲区 int count recv(i,buffer,1024,0); //真正接收 if(count 0){ //disconnect printf(client disconnect: %d\n,i); close(i); fds[i].fd -1; //这个位置以后不再参与监视了。 fds[i].events 0; //表示不再关心任何事件。 continue; } printf(RECV: %s\n,buffer); count send(i,buffer,count,0); printf(SEND: %d\n,count); //打印数据 回发数据 } } }这段代码是一个基于poll的多路 IO 回显服务器。它先把监听 socketsockfd加入监视列表并设置只关心POLLIN读事件因为监听 socket 一旦可读就说明有新的客户端连接到来可以调用accept。进入死循环后服务器通过poll(fds, maxfd 1, -1)一直阻塞等待事件发生。若监听 socket 的revents中包含POLLIN就说明有新连接此时accept得到新的clientfd再把这个客户端 fd 也加入fds数组继续监视。随后程序遍历所有客户端 fd如果某个客户端的revents中有POLLIN就调用recv接收数据如果recv返回 0说明客户端断开连接此时关闭该 fd并把它在fds数组中的位置置为无效如果成功收到数据就打印出来再通过send原样发回去实现回显功能。整个服务器只用一个线程但可以同时管理多个客户端这就是poll多路复用的基本思想。epoll做法//流程创建epoll对象 把监听socket(sockfd)加入epoll while(1) epoll_wait阻塞等待事件发生 遍历所有就绪事件 如果是sockfd有事件 → 说明有新连接 → accept 把新的clientfd加入epoll 如果是某个clientfd可读 → recv 如果recv返回0 → 客户端断开close 从epoll删除 否则send回去 #else 1 int epfd epoll_create(1); //创建一个 epoll 实例。 struct epoll_event ev; //准备一个事件结构体把sockfd加进去 ev.events EPOLLIN; //关心“可读事件” ev.data.fd sockfd; //这个事件结构体对应的是 sockfd 也就是现在要把监听 socket 注册到 epoll 中 epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,ev); //把 sockfd 加入到 epfd 这个 epoll 实例中并且监听它的 EPOLLIN 事件 //四个参数 1.哪一个epoll示例 也就是传一个epfd2.执行什么操作 这里是添加 3.要操作的fd4.这个fd的具体监听配置 传一个结构体 while(1){ struct epoll_event events[1024] {0}; //这个events不是用来看要监听谁 而是数组用来接收 epoll_wait 返回的“已经就绪的事件” int nready epoll_wait(epfd,events,1024,-1); //阻塞等待直到某些 fd 上发生了你关心的事件。 //第一个参数 epfd:哪个epoll实例 第二个参数events用来接收返回的就绪事件数组 第三个参数 1024 最多返回多少个就绪事件 第四个参数 -1 阻塞等待直到有事件发生 int i 0; for(i 0;i nready;i ){ int connfd events[i].data.fd; //取出当前的fd if(connfd sockfd){ //如果是监听fd int clientfd accept(sockfd,(struct sockaddr*)clientaddr,len); //接收这个新连接得到一个新的客户端socketclientfd printf(accept finished:%d\n,clientfd); ev.events EPOLLIN; ev.data.fd clientfd; epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,ev); //把 clientfd 加入 epoll }else if(events[i].events EPOLLIN){ //如果不是 sockfd并且这个 fd 发生了可读事件 说明某个客户端发数据来了可以 recv char buffer[1024] {0}; //接受客户端数据 int count recv(connfd,buffer,1024,0);//从 connfd 这个客户端 socket 中读取数据到buffer 返回值count表示收到多少字节。 if(count 0){ //count 0说明断开连接了 printf(client disconnect: %d\n,connfd); //打印日志 close(connfd); //关闭 fd epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL); //从 epoll 中删除 continue; } printf(RECV:%s\n,buffer); //如果没断开就说明收到了数据 打印收到的内容。 count send(connfd,buffer,count,0); //发送回去形成回显 } } } #endif getchar(); printf(exit\n); return 0;服务器先创建一个epoll对象把监听 socketsockfd加入进去监听它的可读事件。然后进入死循环不断调用epoll_wait等待事件发生。如果返回的是sockfd说明有新的客户端连接到来此时调用accept得到新的clientfd再把这个clientfd也加入epoll中继续监听。如果返回的是某个客户端clientfd的可读事件说明客户端发送了数据此时调用recv读取。若recv返回 0表示客户端断开连接需要close并从epoll中删除。若读取成功则调用send将数据原样发回去从而实现回显。epoll不用再像select和poll去遍历扫描很多次直接在已经就绪的fd中进行不需要扫描也不需要拷贝资源。实现io多路复用。零声学习资源https://github.com/0voice以上是io及多路复用的学习笔记~