新手入门socket编程——io多路复用
这是一篇服务器socket通信的文章
socket是什么
socket的定义
socket,也就是套接字,它是一种计算机之间进行通信的约定或方式
在我的理解里,它是一个封装了TCP/IP以及其他网络协议族中许多函数的接口,例如:listen,accept,recv,send等函数。它通过绑定一个端口和IP地址来进行网络通信。
本文我们以在服务端生成一个回声网络通信的代码实现来了解socket网络通信。
io多路复用
基本概念
在多并发的情况下,io多路复用是通过同时监视多个文件描述符来高效管理。相比于传统的io模型,它提高了系统对于多并发问题的处理能力。
常用的io多路复用机制
select
最早的io多路复用机制之一,通过select函数传递文件描述符集合给内核,从而实现对多个文件描述符状态进行监视。select在可监视的文件数量和效率上性能不足。
poll
poll是在select的基础上产生的,将select函数中的三个参数:read,write,error改为一个结构体数组参数,使传递更加方便。poll 使用 pollfd 结构数组来保存需要监视的文件描述符以及事件,并将数组传递给内核。它没有文件描述符数量的限制,但在文件描述符较多时,效率会比较低。
epoll
epoll 是在 select 和 poll 基础上的进一步改进。它提供更高的性能和可伸缩性,适用于大量文件描述符的高并发场景。使用 epoll_create 创建一个 epoll 实例,通过 epoll_ctl 向其中添加或删除文件描述符,最后通过 epoll_wait 获取就绪的文件描述符。
server端io多路复用代码
main函数中
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
创建一个套接字,并分配一个文件描述符,参数分别表示为使用IPv4地址,使用流式套接字,自动选择合适协议。
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
定义一个服务器地址的结构体变量并进行初始化。
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2047);
对该服务器的参数进行设置,网络地址类型为IPv4,接受任意接口的连接,端口号为2047.
if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr == -1))) {
perror("bind error");
return -1;
}
listen(sockfd, 10);
将套接字的文字描述符与服务器地址绑定,然后开始监听,最多允许10个连接处理。
select函数
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select函数
fd_set rfd,rset;
FD_ZERO(&rfd);
FD_SET(sockfd,&rfd);
int maxfd=sockfd;
设置两个文件描述符集合,将其中的rfd集合初始化,并将之前监听的sockfd加入,设置集合最大文件描述符为sockfd。
while(1){
rset=rfd;
int nREADY=select(maxfd+1,&rset,NULL,NULL,NULL);
if(FD_ISSET(sockfd, &rset)){
struct sockaddr_in clientaddr;
socklen_t len =sizeof(clientaddr);
int clientfd=accept(sockfd(structsockaddr*)&clientaddr,&len);
printf("clientfd:%d\n",clientfd);
FD_SET(clientfd,&rfd);
maxfd=clientfd;
}
进入循环,将rfd复制给rset。select每次循环会将rset集合从用户空间复制到内核空间进行选择(这会改变rset集合),然后将接收到的clientfd记录到rfd中,之后在下次循环又将新的rfd集合复制给rset。每次接收一个clientfd就将其写到rfd中,也就变成了对多个文件描述符形成的集合进行监听。
int i=0;
for(i=sockfd+1;i<=maxfd;i++){
printf("maxfd\n");
if(FD_ISSET(i, &rset)){
char buffer[128]={0};
int count=recv(clientfd,buffer,128,0);
if(count==0){
break;}
send(i,buffer,count,0);
printf("clientfd: %d,count:%d,buffer:%s\n",i,,count,buffer);
}
}
这个循环用于对clientfd,也就是客户端发过来的数据进行接收和回声发送。
poll函数
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
将select中的中间三个参数读、写、异常装在poll的结构体数组中
struct pollfd fds[1024]={0};
fds[sockfd].fd=sockfd;
fds[sockfd].events=POLLIN;
int maxfd=sockfd;
创建结构体数组,存储文件描述符和事件
while(1)
{
int nready=poll(fds,maxfd+1,-1);
if(fds[sockfd].revents & POLLIN){
struct sockaddr_in clientaddr;
socklen_t len =sizeof(clientaddr);
int clientfd=accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("clientfd:%d\n",clientfd);
fds[sockfd].fd=clientfd;
fds[sockfd].events=POLLIN;
maxfd=clientfd;
}
进入无限循环,使用poll函数监视文件描述符事件,返回就绪状态的文件描述符数量。如果是就绪状态,获得请求后创建clientfd并更新fds和maxfd。
int i=0;
for(i=sockfd+1;i<=maxfd;i++){
if(fds[i].revents & POLLIN){
char buffer[128]={0};
int count=recv(i,buffer,128,0);
if(count==0){
printf("disconnect\n");
fds[i].fd= -1;
fds[i].events=0;
close(i);
continue;}
send(i,buffer,count,0);
printf("clientfd: %d,count:%d,buffer:%s\n",i,count,buffer);
}
}
}
遍历在就绪集合中的文件描述符,完成接收和发送工作。并在接受完数据后,将结构体数组中的变量去除,避免重复检测、收发。
epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll有三个函数,分别是创建epoll实例,对epoll实例中的文件描述符里的事件进行操作,等待事件以及返回就绪事件。
int epfd=epoll_create(1);
struct epoll_event ev;
ev.events=EPOLLIN;
ev.data.fd=sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);
struct epoll_event events[1024]={0};
创建epoll实例epfd,时间结构体ev,设置监听sockfd并加入到epfd中。
while(1){
int nready= epoll_wait(epfd,events,1024,-1);
进入循环,获取就绪事件数量
int i=0;
for(i=0;i<nready;i++){
int connfd=events[i].data.fd;
if(sockfd==connfd){
struct sockaddr_in clientaddr;
socklen_t len =sizeof(clientaddr);
int clientfd=accept(sockfd,(struct sockaddr*)&clientaddr,&len);
ev.events=EPOLLIN |EPOLLET;
ev.data.fd=clientfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
printf("clientfd:%d\n",clientfd);
}
遍历就绪事件,将i的文件描述符设为connfd,将请求的客户端文件描述符clientfd中的事件状态设为可读,并将clientfd加入到epfd中
else if(events[i].events & EPOLLIN){
char buffer[10]={0};
int count=recv(connfd,buffer,10,0);
if(count==0){
printf("disconnect\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(i);
continue;}
send(connfd,buffer,count,0);
printf("clientfd: %d,count:%d,buffer:%s\n",connfd,count,buffer);
}
}
}
如果显示这是这是一个就绪事件并且是可读的,则接收数据并在epfd中将connfd删除,最后输出数据。