Linux c下 基于poll方法实现一个简单的server
1.poll
1.1 简单介绍
poll实际上实在select的基础上进行了改进,但是本质上还是要将维护的集合copy到内核进行线性遍历的计算,然后将整个集合copy返回给用户。
不过poll维护的集合更加直观,对一个结构体进行维护,官方介绍如下:
//The set of file descriptors to be monitored is specified in the fds argument, which is an array of //structures of the following form:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
可以看到整个结构体有三个变量。
fd表示我们需要监测的文件描述符events表示我们需要检测的事件revents表示经过内核检测这个文件描述符后返回的事件
下面我们简单介绍下poll函数。
NAME
poll, ppoll - wait for some event on a file descriptor
SYNOPSIS
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
RETURN VALUE
On success, poll() returns a nonnegative value which is the number of elements in the pollfds whose revents fields have been set to a nonzero
value (indicating an event or an error). A return value of zero indicates that the system call timed out before any file descriptors became read.
On error, -1 is returned, and errno is set to indicate the cause of the error.
从官方文档我们可以看到,poll的第一个参数为我们我们维护的集合(结构体变量)、nfds表示需要检测的文件描述符个数、timeout表示阻塞等待事件。
返回值:
成功:返回就绪的fd数量
失败:返回-1
1.2 代码解读
1.2.1 网络编程基础
//1.创建一个用于监听请求的文件文件描述符(fd),命名为listen_fd
int listen_fd = socket(AF_INET,SOCK_STREAM,0);
if( listen_fd < 0){
perror("socket:");
exit(0);
}
//2.用sockaddr_in类型的结构体来存储本地server的信息
//包括地址族协议,端口号,IP地址
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) < 0){
perror("bind:");
exit(0);
}
//3.将设置好的listen_fd状态设置为监听,这个listen_fd会在内核中一直监听,不会主动关闭
//一次最多可以监听5个请求,然后将这些请求放到内核的缓冲区中,并用队列进行存储
printf("server is listening..\n");
if(listen(listen_fd,5) < 0){
perror("listen:");
exit(0);
}
1.2.2 初始化fd集合
//4.初始化需要维护并监听的结构体数组
struct pollfd myfd[MAX_POLLFD] = {0};
for(int i = 0;i < MAX_POLLFD;i++){
myfd[i].fd = -1;
}
//首先加入用于监听连接的文件描述符listen_fd
myfd[0].fd = listen_fd;
myfd[0].events = POLLIN;
int nfds = 1; //监听的文件描述符数量
###1.2.3 循环监听并处理
再阅读代码之前需要先了解下位运算&的含义,先举个例子。
int x = 0b1010; // 10,0b是二进制前缀
int mask = 0b0010;
if(x & mask){
printf("1");
}
x&mask的计算方式如下:
1010 //x
0010 //mask
----
0010 != 0 →判断了x从右往左的第二位不是0,x包含了mask事件
那么我们再实际编程中这个&运算有什么意义呢,请看如下代码:
//5.2判断用于监听新的请求的listen_fd是否就绪
if(myfd[0].revents & POLLIN){
//略
}
我第一次看这个代码的时候不知道这是再干嘛,一脸懵,别急我们一步步拆解。
POLLIN代表读取数据的事件,来看看POLLIN的实际值是多少:
/* Event types that can be polled for. These bits may be set in `events'
to indicate the interesting event types; they will appear in `revents'
to indicate the status of the file descriptor. */
#define POLLIN 0x001 /* There is data to read. */
#define POLLPRI 0x002 /* There is urgent data to read. */
#define POLLOUT 0x004 /* Writing now will not block. */
可以看到这是一个16进制的数字(0x表示16进制的前缀),POLLIN相关事件可以二进制详细表示如下:
#define POLLIN 0x001 // 0000 0001
#define POLLPRI 0x002 // 0000 0010
#define POLLOUT 0x004 // 0000 0100
因为这里的事件只有一个bit位为1,其他的位置为0,所以使用&可以直接判断其他事件的包含关系,怎么做到的呢?
假设myfd[0].revents = POLLIN|POLLOUT,即包含读取和写入事件,那么myfd[0].revents & POLLIN的计算如下:
0000 0101 //myfd[0].revents
0000 0001 //POLLIN
----
0000 0001 != 0 →说明判断事件revents从右往左的第1位不是0,则myfd[0].revents包含了POLLIN,进入if判断语句
接下来我们就可以进入while循环进行事件的监听和事件类型的判断,然后对其进行相关操作。
//5.循环监听并处理事件
while(1){
//5.1使用poll函数来监听准备好的fd,返回就绪的fd数量,-1表示阻塞知道有事件就绪
int nready = poll(myfd,nfds,-1);
//5.2判断用于监听新的请求的listen_fd是否就绪
if(myfd[0].revents & POLLIN){
struct sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
int client_fd = accept(listen_fd,(struct sockaddr *)&clientAddr,&len);
if(client_fd < 0){
perror("accept:");
continue;
}
printf("Connect client(%d)\n",client_fd);
myfd[nfds].fd = client_fd;
myfd[nfds].events = POLLIN;
nfds++;
}
//5.3线性遍历结构体数组中的所有文件描述符,从1开始是因为存储listenfd信息的下标是0
for(int i = 1;i <=nfds ;i++){
int client_fd = myfd[i].fd;
char buffer[BUFFER_MAXSIZE] = {0};
//判断是否可以读取数据
if(myfd[i].revents & POLLIN){
int recv_len = recv(myfd[i].fd,buffer,BUFFER_MAXSIZE,0);
if(recv_len == 0){
printf("Client(%d) lose connection\n",client_fd);
myfd[i].fd = -1;
close(client_fd);
continue;
}
printf("Client(%d) sends data(%dbytes):%s\n",client_fd,recv_len,buffer);
int send_len = send(client_fd,buffer,recv_len,0);
if(send_len <= 0){
perror("send:");
continue;
}
}
}
}
1.2.3 完整代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <poll.h>
#define MAX_POLLFD 1024
#define BUFFER_MAXSIZE 1024
#define PORT 8080
int main(){
//1.创建一个用于监听请求的文件文件描述符(fd),命名为listen_fd
int listen_fd = socket(AF_INET,SOCK_STREAM,0);
if( listen_fd < 0){
perror("socket:");
exit(0);
}
//2.用sockaddr_in类型的结构体来存储本地server的信息
//包括地址族协议,端口号,IP地址
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(PORT);
serverAddr.sin_addr.s_addr = INADDR_ANY;
if(bind(listen_fd,(struct sockaddr *)&serverAddr,sizeof(serverAddr)) < 0){
perror("bind:");
exit(0);
}
//3.将设置好的listen_fd状态设置为监听,这个listen_fd会在内核中一直监听,不会主动关闭
//一次最多可以监听5个请求,然后将这些请求放到内核的缓冲区中,并用队列进行存储
printf("server is listening..\n");
if(listen(listen_fd,5) < 0){
perror("listen:");
exit(0);
}
//4.初始化需要维护并监听的结构体数组
struct pollfd myfd[MAX_POLLFD] = {0};
for(int i = 0;i < MAX_POLLFD;i++){
myfd[i].fd = -1;
}
//首先加入用于监听连接的文件描述符listen_fd
myfd[0].fd = listen_fd;
myfd[0].events = POLLIN;
int nfds = 1;
//5.循环监听并处理事件
while(1){
//5.1使用poll函数来监听准备好的fd,返回就绪的fd数量,-1表示阻塞知道有事件就绪
int nready = poll(myfd,nfds,-1);
//5.2判断用于监听新的请求的listen_fd是否就绪
if(myfd[0].revents & POLLIN){
struct sockaddr_in clientAddr;
socklen_t len = sizeof(clientAddr);
int client_fd = accept(listen_fd,(struct sockaddr *)&clientAddr,&len);
if(client_fd < 0){
perror("accept:");
continue;
}
printf("Connect client(%d)\n",client_fd);
myfd[nfds].fd = client_fd;
myfd[nfds].events = POLLIN;
nfds++;
}
//5.3线性遍历结构体数组中的所有文件描述符,从1开始是因为存储listenfd信息的下标是0
for(int i = 1;i <=nfds ;i++){
int client_fd = myfd[i].fd;
char buffer[BUFFER_MAXSIZE] = {0};
//判断是否可以读取数据
if(myfd[i].revents & POLLIN){
int recv_len = recv(myfd[i].fd,buffer,BUFFER_MAXSIZE,0);
if(recv_len == 0){
printf("Client(%d) lose connection\n",client_fd);
myfd[i].fd = -1;
close(client_fd);
continue;
}
printf("Client(%d) sends data(%dbytes):%s\n",client_fd,recv_len,buffer);
int send_len = send(client_fd,buffer,recv_len,0);
if(send_len <= 0){
perror("send:");
continue;
}
}
}
}
return 0;
}
1.3 poll缺点简单分析
poll是在select的基础上进行改进的,所以二者使用的逻辑基本是一样的,但是poll的集合维护更加直观,而且poll监听的fd数量没有限制,select监听fd的·最大数量为1024。poll本质上还是会在用户和内核之间反复copy 并线性循环的计算,效率上没有太大的提高。
更多推荐




所有评论(0)