其他分享
首页 > 其他分享> > 基于epoll实现的简单httpserver

基于epoll实现的简单httpserver

作者:互联网

基于epoll实现的httpserver端

流程:

1 main函数: 传入端口号, 传入 想要访问的目录。

通过atoi 函数将获取到的port转成integer , 通过chdir将当前进程的工作路径转为要访问的目录。 然后将端口传入到自定义的epoll_run函数中。

2 epoll_run 函数: 传入端口号

实现epoll的三个主要 操作 1、新建epoll红黑树 2、将事件挂到树根上 3、 监听事件

3 init_listen_fd :传入端口号和在run中生成的epoll树根 返回值为listen文件描述符lfd

4 do_accept()传入lfd ,红黑树根

5 do_read () 传入cfd 红黑树根 处理http协议

读取第一行http协议头 拆分成三部分: GET 文件名 协议号 例如GET /hello.c HTTP/1.1

如果第一行为空,则表示为客户端要与服务器端口连接。执行dis_connect 函数

否则用三个数组依次接收上面的三部分。 将发送过来的请求不断读到缓冲区。

通过c中函数strcasecmp(method, "GET", 3)比对前三个字符是否为GET ,如果是就获取到想要访问的文件名然后通过http_request 处理get请求,处理完之后断开连接。

6 http_request()传入cfd , 文件名

通过stat函数判断文件是否存在,并且把文件放到缓冲区 。

如果是一个普通文件就要给客户端发送HTTP应答和所要请求的数据。 发送应答通过send_respond 实现 发送文件用 send_file实现

7 send_respond 传入客户端端的fd, 错误号,错误描述,回发文件类型, 文件长度

通过

sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);

send(cfd, buf, strlen(buf), 0);

等函数把需要的东西放到buf中 再通过send 函数发送到cfd

8 send_file 传入cfd 文件名

用open 函数打开的服务器本地文件。 --- cfd 能访问客户端的 socket

open成功后通过read函数读到buf中, 再通过send函数发送出去

在send函数的返回值中有两个小细节。 当返回值为-1 时 表示发送失败。但是这个时候我们不能直接退出进程。 因为在错误号中有两个错误我们要注意: EAGAIN 和EINTR

其中发送缓冲区是指 :send()仅仅是把应用层buffer的数据拷贝进socket的内核发送buffer中 而内核buffer就是发送缓冲区。发送是TCP的事情,和send其实没有太大关系。

如:以 O_NONBLOCK的标志打开文件/socket/FIFO,如果连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。

所以针对这种ERROR,我们不能直接退出进程,要continue 使其重发。

以上两种的其他错误情况就要退出进程。

点击查看代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>

#define MAXSIZE 2048


// 获取一行 \r\n 结尾的数据 
int get_line(int cfd, char *buf, int size)
{
    int i = 0;
    char c = '\0';
    int n;
    while ((i < size-1) && (c != '\n')) {  
        n = recv(cfd, &c, 1, 0);
        if (n > 0) {     
            if (c == '\r') {            
                n = recv(cfd, &c, 1, MSG_PEEK);
                if ((n > 0) && (c == '\n')) {              
                    recv(cfd, &c, 1, 0);
                } else {                       
                    c = '\n';
                }
            }
            buf[i] = c;
            i++;
        } else {      
            c = '\n';
        }
    }
    buf[i] = '\0';
    
    if (-1 == n)
    	i = n;

    return i;
}

int init_listen_fd(int port, int epfd)
{
    // 创建监听的套接字 lfd
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1) {    
        perror("socket error");
        exit(1);
    }
    // 创建服务器地址结构 IP+port
    struct sockaddr_in srv_addr;
    
    bzero(&srv_addr, sizeof(srv_addr)); //清空地址结构
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(port);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // 端口复用
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    
    // 给 lfd 绑定地址结构
    int ret = bind(lfd, (struct sockaddr*)&srv_addr, sizeof(srv_addr));
    if (ret == -1) {   
        perror("bind error");
        exit(1);
    }
    // 设置监听上限
    ret = listen(lfd, 128);
    if (ret == -1) { 
        perror("listen error");
        exit(1);
    }
    
    // lfd 添加到 epoll 树上
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;
    
    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if (ret == -1) { 
        perror("epoll_ctl add lfd error");
        exit(1);
    }

    return lfd;
}

void do_accept(int lfd, int epfd)
{
	struct sockaddr_in clt_addr;
    socklen_t clt_addr_len = sizeof(clt_addr);
    
    int cfd = accept(lfd, (struct sockaddr*)&clt_addr, &clt_addr_len);
    if (cfd == -1) {   
        perror("accept error");
        exit(1);
    }

    // 打印客户端IP+port
    char client_ip[64] = {0};
    printf("New Client IP: %s, Port: %d, cfd = %d\n",
           inet_ntop(AF_INET, &clt_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
           ntohs(clt_addr.sin_port), cfd);

    // 设置 cfd 非阻塞
    int flag = fcntl(cfd, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(cfd, F_SETFL, flag);

    // 将新节点cfd 挂到 epoll 监听树上
    struct epoll_event ev;
    ev.data.fd = cfd;
    
    // 边沿非阻塞模式
    ev.events = EPOLLIN | EPOLLET;
    
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    if (ret == -1)  {
        perror("epoll_ctl add cfd error");
        exit(1);
    }
}

// 断开链接
void disconnect(int cfd, int epfd)
{
	int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
	if (ret != 0) {
		perror("epoll_ctl error");	
		exit(1);
	}
	close(cfd);
}

// 客户端端的fd, 错误号,错误描述,回发文件类型, 文件长度 
void send_respond(int cfd, int no, char *disp, char *type, int len)
{
	char buf[4096] = {0};
	
	sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);
	send(cfd, buf, strlen(buf), 0);
	memset(buf, 0, sizeof(buf));
	sprintf(buf, "Content-Type:%s\r\n", type);
	sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
	send(cfd, buf, strlen(buf), 0);
	
	send(cfd, "\r\n", 2, 0);
}

// 发送服务器本地文件 给浏览器
void send_file(int cfd, const char *file)
{
	int n = 0, ret;
	char buf[4096] = {0};
	
	// 打开的服务器本地文件。  --- cfd 能访问客户端的 socket
	int fd = open(file, O_RDONLY);
	if (fd == -1) {
		// 404 错误页面
		perror("open error");
		exit(1);
	}
	
	while ((n = read(fd, buf, sizeof(buf))) > 0) {		
		ret = send(cfd, buf, n, 0);
		if (ret == -1) { 
			printf("errno = %d\n", errno);
			if (errno == EAGAIN) {
				printf("-----------------EAGAIN\n");
				continue;
			} else if(errno == EINTR) {
				printf("-----------------EINTR\n");
				continue;
			} else { 
				perror("send error");	
				exit(1);
			}	
		}
		if (ret < 4096)		
			printf("-----send ret: %d\n", ret);
	}
	
	close(fd);		
}

// 处理http请求, 判断文件是否存在, 回发
void http_request(int cfd, const char *file)
{
	struct stat sbuf;
	
	// 判断文件是否存在
	int ret = stat(file, &sbuf);
	if (ret != 0) {
		perror("stat");
		exit(1);	
	}
	
	if(S_ISREG(sbuf.st_mode)) {		// 是一个普通文件
		
		send_respond(cfd, 200, "OK", "text/plain; charset=iso-8859-1", -1);
		
		// 回发 给客户端请求数据内容。
		send_file(cfd, file);
	}	
}

void do_read(int cfd, int epfd)
{
	// 读取一行http协议, 拆分, 获取 get 文件名 协议号	
	char line[1024] = {0};
	char method[16], path[256], protocol[16]; 
	
	int len = get_line(cfd, line, sizeof(line)); //读http请求协议首行 GET /hello.c HTTP/1.1
	if (len == 0) {
		printf("客户端关闭....\n");	
		disconnect(cfd, epfd);
	} else {
				
		sscanf(line, "%[^ ] %[^ ] %[^ ]", method, path, protocol);	
		printf("method=%s, path=%s, protocol=%s\n", method, path, protocol);
		
		while (1) {
			char buf[1024] = {0};
			len = get_line(cfd, buf, sizeof(buf));	
			if (buf[0] == '\n') {
				break;	
			} else if (len == -1)
				break;
		}	
		
	}
	
	if (strncasecmp(method, "GET", 3) == 0)
	{
		char *file = path+1;   // 取出 客户端要访问的文件名
		
		http_request(cfd, file);
		
		disconnect(cfd, epfd);
	}
}

void epoll_run(int port)
{
	int i = 0;
    struct epoll_event all_events[MAXSIZE];  //用event数组接收监听事件,为wait的传出做准备

    // 创建一个epoll监听树根
    int epfd = epoll_create(MAXSIZE);
    if (epfd == -1) { 
        perror("epoll_create error");
        exit(1);
    }
    
    // 创建lfd,并添加至监听树
    int lfd = init_listen_fd(port, epfd);  
   
    while (1) {
    	// 监听节点对应事件
        int ret = epoll_wait(epfd, all_events, MAXSIZE, 0);
        if (ret == -1) {      
            perror("epoll_wait error");
            exit(1);
        }

        for (i=0; i<ret; ++i) {
        	    
            // 只处理读事件, 其他事件默认不处理
            struct epoll_event *pev = &all_events[i];
            
            // 不是读事件
            if (!(pev->events & EPOLLIN)) {                     
                continue;
            }
            if (pev->data.fd == lfd) {   	// 接受连接请求   
                
                do_accept(lfd, epfd);
                
            } else {						// 读数据
                
                do_read(pev->data.fd, epfd);
            }
        }
    }
}


int main(int argc, char *argv[])
{ 
    // 命令行参数获取 端口 和 server提供的目录
    if (argc < 3) 
    {
    	printf("./server port path\n");	
    }
    
    // 获取用户输入的端口 
    int port = atoi(argv[1]);
    
    // 改变进程工作目录
    int ret = chdir(argv[2]);
    if (ret != 0) {
    	perror("chdir error");	
    	exit(1);
    }

	// 启动 epoll监听
	epoll_run(port);

    return 0;
}




我们通过telnet命令进行测试

可以成功访问path中的文本文件。

标签:lfd,基于,httpserver,epoll,int,cfd,send,buf
来源: https://www.cnblogs.com/fight-nxlwwy/p/15899955.html