Linux C/C++服务器
网络io与select、epoll
网络io是什么?是网络连接的input与output,也就是socket,更进一步说就是fd
- 阻塞与非阻塞
- io多路复用,检测io是否有事件(可读、可写)
- 同步与异步
头文件与listenfd的监听模块
不管select还是epoll还是reactor,前面的listenfd监听模块都是一样的
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#define BUFFER_LENGTH 128
int main(){
//fd创建的时候默认是block(阻塞的)
int listenfd = socket(AF_INET, SOCK_STREAM, 0); //listenfd 是从3开始一次往后累加分配的,0,1,2分别是stdin,stdout,stderr
//socket(AF_INET协议地址族, SOCK_STREAM, 0) 接口的固定写法,由于历史遗留原因,除了第二个参数会修改外,其他两个参数基本都不会修改, fd就是一个链接的句柄,即标识号
if(listenfd == -1) return -1; //unix api 成功的返回值一般都为0
struct sockaddr_in servaddr; //网络通信的地址设置,协议,IP,端口
servaddr.sin_family = AF_INET;
//htonl,host to network long, host(点分十进制)转换为 long网络字节序
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY是"0.0.0.0" 宏定义,addr一般指ipv4的地址
servaddr.sin_port = htons(9999); //端口
if(-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
return -2;
}
#if 0 //nonblock(非阻塞fd设置)
int flag = fcntl(listenfd, F_GETFL, 0); //先get出属性
flag |= O_NONBLOCK; //或,位运算,加上新属性,标准写法,不会覆盖之前的属性
fcntl(listenfd, F_SETFL, flag); //set进去
#endif
listen(listenfd, 10);
/*
不同的网络io处理模块
accept()、recv()、send()、close()
...
...
*/
return 0;
}
单一网络io模块
最简单的网络io,只允许单一客户端连接
... //头文件
int main(){
... //listenfd监听模块
//单一网络连接、网络io
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
//这里的listenfd是阻塞的fd,没有数据连接,进程一直阻塞住,等客户端的连接过来
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //accpet接收成功会生成一个新的socketfd当作客户端的fd
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if(ret == 0){ //recv返回0,代表客户端连接断开
close(clientfd);
}
printf("buffer : %s, ret : %d\n", buffer, ret);
ret = send(clientfd, buffer, ret, 0);
//printf("clientfd : %d\n", clientfd);
}
多线程网络io模块
支持多客户端同时接入,一线程一请求,逻辑简单;缺点:最多支持1024个客户端接入,多线程
... //头文件
#include <pthread.h>
void *routine(void* arg){ //线程回调函数
int clientfd = *(int *)arg;
while(1){
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
printf("buffer : %s, ret : %d\n", buffer, ret);
if(ret == 0){
close(clientfd);
break;
}
ret = send(clientfd, buffer, ret, 0);
}
return 0;
}
int main(){
... //listenfd监听模块
while(1){
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //只能用于阻塞的fd
pthread_t threadid;
pthread_create(&threadid, NULL, routine, &clientfd);
}
}
select网络 io多路复用 模块
io多路复用组件,单线程 解决多个客户端连接,低于1024并发会比epoll更快一些,只有一个接口:select(maxfd, rset, wset, err, timeout)
select的核心是检测 rset, wset(io可读、可写事件,bit位数据结构)--> for遍历所有的fd(close的fd也会遍历),for(i=listenfd+1; i<=maxfd; i++)
... //头文件
int mian(){
... //listenfd监听模块
fd_set rfds, wfds, rset, wset; //io可读、可写事件,bit位数据结构,这就是select的核心
FD_ZERO(&rfds); //清空fd设置,比特位清空
FD_SET(listenfd, &rfds);
FD_ZERO(&wfds);
int maxfd = listenfd; //最大的fd值
unsigned char buffer[BUFFER_LENGTH] = {0};
int ret=0;
while(1){
//我们自己操作可读、可写事件用fds,检测可读、可写事件用set
rset = rfds;
wset = wfds;
//监听listenfd,并创建clientfd
//select实现是个for遍历,maxfd就是循环的最大值,返回值位可读可写的事件总数
int nready = select(maxfd+1, &rset, &wset, NULL, NULL); //rset可读集合、wset可写集合,如果没有可读可写事件会阻塞住
if(FD_ISSET(listenfd, &rset)){ //判断listenfd在可读事件里有没有,位运算判断
printf("listenfd--> \n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len); //新增clentfd,accept会把listenfd可读可写事件清空
FD_SET(clientfd, &rfds); //设置clientfd可读事件
if(clientfd > maxfd) maxfd = clientfd;
}
//处理clientfd内的数据
int i=0;
for(i=listenfd+1; i<=maxfd; i++){ //clientfd都是在listenfd(=3)上累加的,这里的i就是clientfd
if(FD_ISSET(i, &rset)){ //如果clientfd有读事件
ret = recv(i, buffer, BUFFER_LENGTH, 0);
if(ret == 0){
close(i);
FD_CLR(i, &rfds); //清空可读事件i
}else if(ret > 0){
printf("buffer : %s, ret : %d\n", buffer, ret);
FD_SET(i, &wfds); //设置可写事件i
}
}else if(FD_ISSET(i, &wset)){
ret = send(i, buffer, ret, 0);
FD_CLR(i, &wfds);
FD_SET(i, &rfds);
}
}
}
return 0;
}
epoll 网络io多路复用模块
epoll即event poll,基于事件驱动的 io多路复用,单线程 解决多个客户端连接,epoll有三个接口epoll_create(int)、epoll_ctl(epfd, add_del, fd, events)、epoll_wait(epfd, events, evlength, timeout)
优点:1.不需要循环遍历所有fd 2.每一次取就绪集合,在固定位置 3.就绪和应用层处理可异步解耦 4.并发接入量>10k时,比select快很多
... //头文件
#include <sys/epoll.h>
#include <string.h>
#define EVENTS_LENGTH 128
char rbuffer[BUFFER_LENGTH] = {0};
char wbuffer[BUFFER_LENGTH] = {0};
int main(){
... //listenfd监听模块
//epoll
int epfd = epoll_create(1); //参数只要大于0即可,1和100没有区别,看源码
struct epoll_event ev, events[EVENTS_LENGTH];
//将listenfd添加进epoll
ev.events = EPOLLIN; //设置为读事件
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while(1){
//timeout:epoll_wait的第四个参数,-1为阻塞等数据,0为立即返回,1000:每1s返回一次
int nready = epoll_wait(epfd, events, EVENTS_LENGTH, -1); //如果检测到可读可写事件,nready为可读写事件的总数,事件存储在events内
printf("nready---->: %d\n", nready);
int i=0;
for(i=0; i<nready; i++){
int evfd = events[i].data.fd; //evfd事件有两种一个是lisentfd=3,另一种是clientfd
if(evfd == listenfd){ //listenf可读事件触发,即客户端请求连接服务端:connect()->accept()
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &len);
printf("accpet:%d\n", clientfd);
//将clientfd添加进epoll
ev.events = EPOLLIN; //EPOLLIN表示读事件,默认水平触发LT(只要buffer有数据就一直触发,只需要写一个recv即可)
//ev.events = EPOLLIN | EPOLLET; //边沿触发ET,触发一次读一次,需要循环recv "while(recv(...)){}" 直到buffer内无数据可读
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev); //将clientfd加到epoll检测队列中去
}else if(events[i].events & EPOLLIN){ //clientfd可读事件触发,recv()
//char rbuffer[BUFFER_LENGTH] = {0};
int n = recv(evfd, rbuffer, BUFFER_LENGTH, 0); //水平触发LT,buffer里有数据就一直触发
if(n > 0){
rbuffer[n]='\0';
printf("recv: %s\n", rbuffer);
memcpy(wbuffer, rbuffer, BUFFER_LENGTH);
ev.events = EPOLLOUT;
ev.data.fd = evfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, evfd, &ev); //修改evfd(clientfd)为写事件
}else if(n == 0){
close(evfd);
}
/*
while(recv(evfd, buffer, BUFFER_LENGTH, 0)){ //边沿触发ET,边沿触发使用较多
printf("recv: %s\n", buffer);
//send(evfd, buffer, sizeof(buffer), 0);
memset(buffer,0,sizeof(buffer));
}*/
}else if(events[i].events & EPOLLOUT){ //clientfd可写事件触发,send()
int sent = send(evfd, wbuffer, BUFFER_LENGTH, 0);
//printf("sent:%d\n", sent);
ev.events = EPOLLIN;
ev.data.fd = evfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, evfd, &ev); //修改evfd(clientfd)为写事件
}
}
}
return 0;
}
以上代码均已通过测试,以上代码只能在demo测试使用,实际使用中会存在共用一个数据缓冲区问题,为了让每一个fd都有自己独立的缓冲区,于是就有了reactor
1.listenfd用来监听客户连接请求,clienfd用来处理具体数据读写,那它们的sock_item有没有区别?
2.100万的sock_item如何快速查找?
//reactor简单的结构
struct sock_item{ //conn_item
int fd; //clientfd
char *rbuffer;
int rlength;
char *wbuffer;
int wlength;
int event;
//callback
void (*recv_cb)(int fd, char *buffer, int length);
void (*send_cb)(int fd, char *buffer, int length);
void (*accept_cb)(int fd, char *buffer, int length);
}
struct reactor{
int epfd;
...
}