poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
① struct pollfd *fds:一个结构体,初始化定义为数组,用来存放文件描述符与其相关信息,他的结构如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件 常用有三种(其余可以自己去man查看):
POLLIN:有数据可读
POLLPRI:有紧急数据需要读取
POLLOUT: 文件可写*/
short revents; /* 返回的事件 与evevt相同*/ };
② nfds_t nfds:fds的数量,也就是定义的结构体数组中有多少个fd。
③ int timeout:设置阻塞的时间(毫秒) ,0为非阻塞 负数表示永久阻塞。
返回值:成功返回一个正数,失败返回 -1 ,返回0表示在规定阻塞时间内没有准备好的文件描述符。
在实现多路复用时直接使用定义的数组里的fd即可,具体看代码,这里我们只实现服务器端代码,使用 nc 命令向主机发送数据:
#include "net.h"//封装好的方法库,在文章最后展现
#include<poll.h>
int main(int argc, char *argv[])
{
struct pollfd fds[MAX_LENGTH] = {};//定义一个结构体类型的数组来存放文件描述符
nfds_t nfds = 1;//初始化为1,也可以为0,更改后续的一些代码即可
Addr_in addr;
socklen_t addrlen = sizeof(Addr_in);//提前定义为了能打印出是哪个地址连接或发送或退出
int ret,i,j;
/*检查参数,小于3个 直接退出进程,方法库实现*/
Argment(argc, argv);
/*创建已设置监听模式的套接字,方法库实现*/
int fd = CreateSocket(argv);
fds[0].fd = fd;//让内核帮助我们关注fds[0]号文件描述符上的事件
fds[0].events = POLLIN;//关注事件类型为读事件
while(1){
ret = poll(fds,nfds,-1);//通过返回值判断是否有文件描述符上的读事件就绪
// -1 表示永久阻塞等待
if(ret < 0)
ErrExit("poll");
for(i = 0;i < nfds;i++){//遍历fds数组
//找到准备好的文件描述符并判断他是否有读事件就绪
if(fds[i].fd == fd && fds[i].revents == POLLIN){
int newfd = accept(fd, (Addr *)&addr, &addrlen);
printf("[%s,%d]has
connect\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
if(newfd < 0)
perror("accept");
fds[nfds].fd = newfd;
fds[nfds].events = POLLIN;
nfds++;
}else if(i > 0 && fds[i].revents == POLLIN){
/*处理客户端数据*/
if(DataHandle(fds[i].fd) <= 0){
if(( getpeername(fds[i].fd,(Addr *)&addr,&addrlen)) < 0)
ErrExit("getpeername");
close(fds[i].fd);
printf("[%s,%d]has
exit\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
for(j = i;j<MAX_LENGTH-1;j++){
fds[j] = fds[j+1];
}
i--;
nfds--;
}
}
}
}
return 0;
}
epoll函数族
比较常用尽量熟练!!
/*创建epoll句柄*/
int epoll_create(int size); //size参数实际上已经被弃用
/*epoll句柄的控制接口*/
int epoll_ctl(int epfd, int op, int fd,struct epoll_event *event);
参数:
epfd: epoll 专用的文件描述符,epoll_create()的返回值
op: 表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从 epfd 中删除一个 fd;
fd: 需要监听的文件描述符
event: 告诉内核要监听什么事件,他的类型是epoll_event结构体,定义如下:
struct epoll_event
{ __uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */ };
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32; __uint64_t u64;
} epoll_data_t;
/*等待 epoll 文件描述符上的 I/O 事件*/
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
注意这里的第二个参数是结构体的数组,而 ctl 函数里的最后一个参数则是epoll_event的结构体
参数:
epfd: epoll 专用的文件描述符,epoll_create()的返回值
events: 分配好的 epoll_event 结构体数组,epoll_wait 将会把发生被ctl监听的事件对应的文件描述符拷贝到events 数组中
maxevents: events 数组的元素个数
timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞
代码展示:
#include "net.h"
#include<poll.h>
#include <sys/epoll.h>
int main(int argc, char *argv[])
{
int eplfd,ret,i;
struct epoll_event epls,epl[MAX_LENGTH] = {};
//定义一个epoll_event类型结构体和一个结构体数组
Addr_in addr;
socklen_t addrlen = sizeof(Addr_in);
/*检查参数,小于3个 直接退出进程*/
Argment(argc, argv);
/*创建已设置监听模式的套接字*/
int fd = CreateSocket(argv);
eplfd = epoll_create(1);//创建epoll专用文件描述符
if(eplfd < 0)
ErrExit("eplfd");
epls.data.fd = fd;//选择需要监视的文件描述符和监视的类型
epls.events = EPOLLIN;
//让内核监视epls结构体中events成员类型的事件
if((epoll_ctl(eplfd,EPOLL_CTL_ADD,fd,&epls)) < 0 )
ErrExit("epoll_ctl");
/*接收客户端连接,并生成新的文件描述符*/
while(1){
//把内核中发生了我们所监听的事件拷贝到epl数组中并返回事件的个数
ret = epoll_wait(eplfd,epl,MAX_LENGTH,-1);
if(ret < 0)
ErrExit("epoll_wait");
//遍历返回的epl数组判断发送的是连接信号还是传输信息的信号
//因为连接过后会使用新的文件描述符去传递信息
//所以当epl数组中的某个值与开始时创建客户端的套接字相等代表客户端的连接还没有被接受
//此时开始进行接受客户端的连接请求,并且把accept函数返回的新的文件描述符也变成监听对象
//然后重新循环调用wait函数继续监听
//当其判断数组中的文件描述符等于accept返回的文件描述符时,代表客户端发送了信息,
//则进行对应的客户端处理的操作
//若还是相等则表示有新的客户端发送了连接请求
for(i = 0;i<ret;i++){
if(epl[i].data.fd == fd){
int newfd = accept(fd, (Addr *)&addr, &addrlen);
if(newfd < 0)
ErrExit("newfd");
//打印一个连接提醒,下面的打印也都是提醒,告知用户是哪个客户端进行操作
printf("[%s,%d]has connect\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
epls.data.fd = newfd;
epls.events = EPOLLIN;
if((epoll_ctl(eplfd,EPOLL_CTL_ADD,newfd,&epls)) < 0 )
ErrExit("epoll_ctl");
}else{
/*处理客户端数据*/
//此函数是自创的函数,就是实现接收和打印客户端发送的信息
if(DataHandle(epl[i].data.fd) <= 0){
//小于0表示客户端退出了连接,此时需要删除epl数组中对应的文件描述符
if((epoll_ctl(eplfd,EPOLL_CTL_DEL,epl[i].data.fd,NULL)) )
ErrExit("epoll_ctl");
if(( getpeername(epl[i].data.fd,(Addr *)&addr,&addrlen)))
ErrExit("getpeername");
printf("[%s,%d]has exit\n",inet_ntoa(addr.sin_addr),ntohs(addr.sin_port));
//记得将打开的文件描述符关闭,在程序退出之前也需要关闭所有的文件描述符
close(epl[i].data.fd);
}
}
}
}
close(eplfd);
close(fd);
return 0;
}
net.h文件:
#ifndef _NET_H_
#define _NET_H_
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>
#include <errno.h>
typedef struct sockaddr Addr;
typedef struct sockaddr_in Addr_in;
#define BACKLOG 5
//打印错误信息的宏
#define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)
#define MAX_LENGTH 1024
void Argment(int argc, char *argv[]);
//创建客户端套接字的方法
int CreateSocket(char *argv[]);
int DataHandle(int fd);
#endif
服务器端一些函数的实现:
#include "net.h"
//使用命令行传参并判断数量是否正确
void Argment(int argc, char *argv[]){
if(argc < 3){
fprintf(stderr, "%s<addr><port>\n", argv[0]);
exit(0);
}
}
//创建套接字的方法,使用socket简单实现
int CreateSocket(char *argv[]){
/*创建套接字*/
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
ErrExit("socket");
/*允许地址快速重用*/
int flag = 1;
if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
perror("setsockopt");
/*设置通信结构体*/
Addr_in addr;
bzero(&addr, sizeof(addr) );
addr.sin_family = AF_INET;
addr.sin_port = htons( atoi(argv[2]) );
/*绑定通信结构体*/
if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )
ErrExit("bind");
/*设置套接字为监听模式*/
if( listen(fd, BACKLOG) )
ErrExit("listen");
return fd;
}
//接受客户端信息的函数
int DataHandle(int fd){
char buf[BUFSIZ] = {};
Addr_in addr;
socklen_t addrlen = sizeof(Addr_in);
getpeername(fd,(Addr *)&addr,&addrlen);
int ret = recv(fd, buf, BUFSIZ, 0);
if(ret < 0)
perror("recv");
if(ret > 0)
printf("[%s,%d]: %s", inet_ntoa(addr.sin_addr),ntohs(addr.sin_port),buf);
return ret;
}