Linux服务器开发——epoll模式web服务器代码

2023-11-05

1. main.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include "epoll_server.h"
 
int main(int argc, const char* argv[]){
    if(argc < 3){
        printf("eg: ./a.out port path\n");
        exit(1);
    }
 
    // 端口 字符串转整数
    int port = atoi(argv[1]);
 
    // 修改进程的工作目录, 方便后续操作
    int ret = chdir(argv[2]);
    if(ret == -1){
        perror("chdir error");
        exit(1);
    }
    // 启动epoll模型 
    epoll_run(port);
    return 0;
}

2. epoll_server.c

2.1 初始化epoll树,初始化监听的描述符

#define MAXSIZE 2000
 
void epoll_run(int port){
 
    // 创建一个epoll树的根节点
    int epfd = epoll_create(MAXSIZE);
    if(epfd == -1){
        perror("epoll_create error");
        exit(1);
    }
 
    // 添加要监听的节点
    // 先添加监听lfd
    int lfd = init_listen_fd(port, epfd);
 
    // 委托内核检测添加到树上的节点
    struct epoll_event all[MAXSIZE];
    while(1){
        //epfd是epoll树的根节点,如果有树上某个文件描述符对应的缓冲区发生了变换,则会被拷贝到结构体数组all中
        // MAXSIZE是数组的大小, -1是一直阻塞,直到有节点发生变化的时候再发生返回,返回值ret是发生变化的文件描述符个数
        int ret = epoll_wait(epfd, all, MAXSIZE, -1);
        if(ret == -1){
            perror("epoll_wait error");
            exit(1);
        }
 
        // 遍历发生变化的节点
        for(int i=0; i<ret; ++i){
            // 只处理读事件, 其他事件默认不处理
            struct epoll_event *pev = &all[i];
            if(!(pev->events & EPOLLIN)){
                // 不是读事件
                continue;
            }
 
            if(pev->data.fd == lfd){
                // 接受连接请求
                do_accept(lfd, epfd);
            }
            else{
                // 读数据
                do_read(pev->data.fd, epfd);
            }
        }
    }
}
 
 
//初始化监听,传入端口号和epoll书的根节点
int init_listen_fd(int port, int epfd) {
 
    // 创建监听的套接字
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lfd == -1) {
        perror("socket error");
        exit(1);
    }
 
    // lfd绑定本地IP和port
    struct sockaddr_in serv;
    memset(&serv, 0, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(port);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
 
    // 端口复用
    int flag = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
    int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv));    //绑定
    if(ret == -1){
        perror("bind error");
        exit(1);
    }
 
    // 设置监听,最大是128,这里写个64是随意写的
    ret = listen(lfd, 64);
    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;
}

2.2 接受新的连接请求并上树

// 接受新连接处理
void do_accept(int lfd, int epfd){
    struct sockaddr_in client;
    socklen_t len = sizeof(client);
    int cfd = accept(lfd, (struct sockaddr*)&client, &len);
    if(cfd == -1){
        perror("accept error");
        exit(1);
    }
 
    // 打印客户端信息
    char ip[64] = {0};
    //inet_ntop大端整型转淀粉十进制ip地址
    printf("New Client IP: %s, Port: %d, cfd = %d\n",
           inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
           ntohs(client.sin_port), cfd);
 
    // 设置cfd为非阻塞(默认是阻塞的)
    int flag = fcntl(cfd, F_GETFL);
    flag |= O_NONBLOCK; //非阻塞
    fcntl(cfd, F_SETFL, flag);
 
    // 得到的新节点挂到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);
    }
}

2.3 已经在树上的进行读数据

  1. 整体主函数
// 读数据
void do_read(int cfd, int epfd) {
 
	// 将浏览器发过来的数据, 读到buf中 
	char line[1024] = { 0 };
	// 读请求行
	int len = get_line(cfd, line, sizeof(line));
 
	//get_line调用的是recv,recv返回值为0则说明客户端断开了链接
	if (len == 0) {
		printf("客户端断开了连接...\n");
		// 关闭套接字, cfd从epoll上删除
		disconnect(cfd, epfd);
	}
	//recv失败了
	else if (len == -1) {
		perror("recv error");
		exit(1);
	}
	else {
		printf("请求行数据: %s", line);
		printf("============= 请求头 ============\n");
		// 还有数据没读完,继续读
		while (len) {
			char buf[1024] = { 0 };
			len = get_line(cfd, buf, sizeof(buf));
			printf("-----: %s", buf);
		}
		printf("============= The End ============\n");
	}
 
	// 请求行: get /xxx http/1.1
	// 判断是不是get请求  strncasecmp函数用于比较两个字符串,不区分大小写的比较前n个字符
	if (strncasecmp("get", line, 3) == 0) {
		// 处理http请求
		http_request(line, cfd);
		// 关闭套接字, cfd从epoll上del
		disconnect(cfd, epfd);
	}
}

用到的库函数:

strncasecmp("get", line, 3)

strncasecmp函数用于比较两个字符串,不区分大小写的比较前n个字符

  1. 解析http请求消息的每一行内容
// 解析http请求消息的每一行内容
int get_line(int sock, char *buf, int size){
    int i = 0;
    char c = '\0';
    int n;
    //每次读一个字节,判断合理就放入缓冲区中
    while ((i < size - 1) && (c != '\n')){
        n = recv(sock, &c, 1, 0);
        if (n > 0){
            if (c == '\r'){
                //MSG_PEEK 使得recv以拷贝的方式从缓冲区中读取数据(否则读取之后,缓冲区中的数据就没了)
                //试探性的获取缓冲区中的数据量
                n = recv(sock, &c, 1, MSG_PEEK);
                //缓冲区中有数据并且结尾是 \n  ,则读取数据
                if ((n > 0) && (c == '\n')){
                    recv(sock, &c, 1, 0);
                } else {
                    c = '\n';
                }
            }
            buf[i] = c;
            i++;
        } else{
            c = '\n';
        }
    }
    buf[i] = '\0';
 
    //recv失败
    if (n == -1) {
        i = -1;
    }
 
    return i;
}

用到的库函数:

recv(sock, &c, 1, MSG_PEEK);

MSG_PEEK 使得recv以拷贝的方式从缓冲区中读取数据(否则读取之后,缓冲区中的数据就没了)

2.4 http请求解析并处理

// http请求处理,传入request和
void http_request(const char* request, int cfd) {
 
	// 拆分http请求行
	//    get        /xxx        http/1.1
	char method[12], path[1024], protocol[12];
	//%[^ ]匹配遇到空格为止
	sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
 
	printf("method = %s, path = %s, protocol = %s\n", method, path, protocol);
 
	// 转码 将不能识别的中文乱码 - > 中文
	// 解码 %23 %34 %5f
	decode_str(path, path);
	// 处理path  /xx(有可能是目录,也有可能是文件)
	// 去掉path中的/
	char* file = path + 1;
	// 如果没有指定访问的资源, 默认显示资源目录中的内容
	if (strcmp(path, "/") == 0) {
		// file的值, 资源目录的当前位置
		file = "./";
	}
 
	// 获取文件属性
	struct stat st;
	int ret = stat(file, &st);
	if (ret == -1) {
		// show 404
		send_respond_head(cfd, 404, "File Not Found", ".html", -1);
		send_file(cfd, "404.html");
	}
 
	// 判断是目录还是文件 path  /xx(有可能是目录,也有可能是文件)
	// 如果是目录
	if (S_ISDIR(st.st_mode)) {
		// 发送头信息
		send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
		// 发送目录信息
		send_dir(cfd, file);
	}
	else if (S_ISREG(st.st_mode))
	{
		// 文件
		// 发送消息报头
		send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
		// 发送文件内容
		send_file(cfd, file);
	}
}

2.5 发送http响应头

// 发送响应头  cfd(服务器和浏览器通讯的文件描述符),no(状态码)
//desp(对状态码的描述),type(Content-Type),len(发送的数据长度)
void send_respond_head(int cfd, int no, const char* desp, const char* type, long len) {
	char buf[1024] = { 0 };
 
	// 状态行 http不区分大小写
	sprintf(buf, "http/1.1 %d %s\r\n", no, desp);
	//防止buf溢出,先把里面的数据发送出去
	send(cfd, buf, strlen(buf), 0);
 
	// 消息报头
	sprintf(buf, "Content-Type:%s\r\n", type);
	sprintf(buf + strlen(buf), "Content-Length:%ld\r\n", len);
	send(cfd, buf, strlen(buf), 0);
 
	// 空行
	send(cfd, "\r\n", 2, 0);
}

2.6 发送普通文件

// 发送文件
void send_file(int cfd, const char* filename){
	// 打开文件
	int fd = open(filename, O_RDONLY);
	if (fd == -1){
		// show 404
		return;
	}
 
	// 循环读文件
	char buf[4096] = { 0 };
	int len = 0;
	while ((len = read(fd, buf, sizeof(buf))) > 0){
		// 发送读出的数据
		send(cfd, buf, len, 0);
	}
	if (len == -1){
		perror("read file error");
		exit(1);
	}
 
	close(fd);
}

2.7 读目录

第一种方式读目录——使用readdir读目录

    // 打开目录
	DIR* dir = opendir(dirname);
	if (dir == NULL){
		perror("opendir error");
		exit(1);
	}
 
	// 读目录
	struct dirent* ptr = NULL;
	while ((ptr = readdir(dir)) != NULL){
		char* name = ptr->d_name;
	}
	closedir(dir);

更好用的方式——使用函数scandir

#include <dirent.h>
 
int scandir(const char *dirp,                   
     struct dirent ***namelist,           
     int (*filter)(const struct dirent *), 
     int (*compar)(const struct dirent **, 
     const struct dirent **));

参数:
	dirp		当前要扫描的目录
	namelist	在下面讲
	filter		函数指针,搜索的时候,希望过滤掉某些目录,可以通过该回调函数来指定过滤的规则。不指定函数,则传入NULL
	compar		文件名显示的时候, 指定排序规则
				库里面的两个用于排序的函数
				    alphasort——按首字母排序
    				versionsort——按版本号排序

参数namelist

  指向一块地址,这块地址是scandir内部开辟的存储空间。namelist指针指向一个数组,数组中存储目录项指针,目录项指针都指向一块内存,内存中存储了文件对应的信息。

用法:

  1. struct dirent** ptr; ——先定义一个二级指针变量(dirent**相当于一个数组)
  2. struct dirent* ptr[];——另一个定义一个二级指针变量的方式
  3. &ptr;——然后对二级指针变量取地址

使用示例:

// 目录项二级指针
struct dirent** ptr;
int num = scandir(dirname, &ptr, NULL, alphasort);

2.8 发送目录

// 发送目录内容,拼写一个html表格
void send_dir(int cfd, const char* dirname) {
 
	// 拼一个html页面<table></table> table可以显示多行多列,便于显示文件信息
	char buf[4096] = { 0 };
 
	//目录名
	sprintf(buf, "<html><head><title>目录名: %s</title></head>", dirname);
	//当前目录
	sprintf(buf + strlen(buf), "<body><h1>当前目录: %s</h1><table>", dirname);
 
	/*-------------------------------获取当前目录中的所有内容---------------------------*/
	char enstr[1024] = { 0 };
	char path[1024] = { 0 };
 
	// 目录项二级指针
	struct dirent** ptr;
	int num = scandir(dirname, &ptr, NULL, alphasort);
 
	// 遍历
	for (int i = 0; i < num; ++i){
		//获取文件名
		char* name = ptr[i]->d_name;
 
		// 拼接文件的完整路径
		sprintf(path, "%s/%s", dirname, name);
		printf("path = %s ===================\n", path);
		struct stat st;
		stat(path, &st);
 
		encode_str(enstr, sizeof(enstr), name);
		// 如果是文件
		if (S_ISREG(st.st_mode)){
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
				enstr, name, (long)st.st_size);
		}
		// 如果是目录
		else if (S_ISDIR(st.st_mode)){
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s/\">%s/</a></td><td>%ld</td></tr>",
				enstr, name, (long)st.st_size);
		}
		//每循环一次就发送异常,清空buf,防止buf溢出
		send(cfd, buf, strlen(buf), 0);
		memset(buf, 0, sizeof(buf));
		// 字符串拼接
	}
 
	sprintf(buf + strlen(buf), "</table></body></html>");
	send(cfd, buf, strlen(buf), 0);
 
	printf("dir message send OK!!!!\n");
 
}

2.9 断开连接

// 断开连接的函数
void disconnect(int cfd, int epfd){
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
    if(ret == -1){
        perror("epoll_ctl del cfd error");
        exit(1);
    }
    close(cfd);
}

2.10 编码和解码

原因:
http协议中会对中文进行编码。中文如果是Unicode类型,则占三个字节。url在数据传输过程中不支持中文,需要转码。
在这里插入图片描述
特殊字符

要处理可见字符
从space开始 - 32
前0-31个不可见

不需要转换的特殊字符
.
_
*
/
~
0-9
a-z
A-Z

需要转换的字符使用其16进制的值前加%表示

实现代码:

// 16进制数转化为10进制
int hexit(char c) {
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;
 
	return 0;
}
 
//字符转成16进制数
void encode_str(char* to, int tosize, const char* from) {
	int tolen;
 
	for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) {
		//isalnum判断字符是不是数字(判断字符是否需要解码)
		//http协议中, /_.~这四个不需要转
		if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) {
			//不需要转,则原封不动的拷贝一份
			*to = *from;
			++to;
			++tolen;
		}
		else {
			//字符转成十六进制数—— *from取字符值,然后与十六进制数字做按位与
			sprintf(to, "%%%02x", (int)*from & 0xff);
			//to永远指向字符从末尾
			to += 3;
			tolen += 3;
		}
	}
	*to = '\0';
}
 
//编码,用作回写浏览器的时候,将除字母、数字以及 /_.~以外的字符转义后回写
//将16进制数转换成字符
void decode_str(char *to, char *from) {
	for (; *from != '\0'; ++to, ++from) {
		//十六进制转十进制
		if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
			//依次判断from中 %20 三个字符
			*to = hexit(from[1]) * 16 + hexit(from[2]);
			//移除已经处理的两个字符
			from += 2;
		}else {
			*to = *from;
		}
	}
	*to = '\0';
}

2.11通过文件名获取文件的类型

// 通过文件名获取文件的类型
const char *get_file_type(const char *name){
	char* dot;
 
	// 自右向左查找‘.’字符, 如不存在返回NULL
	dot = strrchr(name, '.');
	if (dot == NULL)
		return "text/plain; charset=utf-8";
	if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
		return "text/html; charset=utf-8";
	if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
		return "image/jpeg";
	if (strcmp(dot, ".gif") == 0)
		return "image/gif";
	if (strcmp(dot, ".png") == 0)
		return "image/png";
	if (strcmp(dot, ".css") == 0)
		return "text/css";
	if (strcmp(dot, ".au") == 0)
		return "audio/basic";
	if (strcmp(dot, ".wav") == 0)
		return "audio/wav";
	if (strcmp(dot, ".avi") == 0)
		return "video/x-msvideo";
	if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
		return "video/quicktime";
	if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
		return "video/mpeg";
	if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
		return "model/vrml";
	if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
		return "audio/midi";
	if (strcmp(dot, ".mp3") == 0)
		return "audio/mpeg";
	if (strcmp(dot, ".ogg") == 0)
		return "application/ogg";
	if (strcmp(dot, ".pac") == 0)
		return "application/x-ns-proxy-autoconfig";
 
	return "text/plain; charset=utf-8";
}

3. 整体epoll_server.c代码

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/stat.h>
#include <ctype.h>
#include "epoll_server.h"
 
#define MAXSIZE 2000
 
void epoll_run(int port) {
 
	// 创建一个epoll树的根节点
	int epfd = epoll_create(MAXSIZE);
	if (epfd == -1) {
		perror("epoll_create error");
		exit(1);
	}
 
	// 添加要监听的节点
	// 先添加监听lfd
	int lfd = init_listen_fd(port, epfd);
 
	// 委托内核检测添加到树上的节点
	struct epoll_event all[MAXSIZE];
	while (1) {
		//epfd是epoll树的根节点,如果有树上某个文件描述符对应的缓冲区发生了变换
        //则会被拷贝到结构体数组all中
		// MAXSIZE是数组的大小, -1是一直阻塞,直到有节点发生变化的时候再发生返回
        //返回值ret是发生变化的文件描述符个数
		int ret = epoll_wait(epfd, all, MAXSIZE, -1);
		if (ret == -1) {
			perror("epoll_wait error");
			exit(1);
		}
 
		// 遍历发生变化的节点
		for (int i = 0; i < ret; ++i) {
			// 只处理读事件, 其他事件默认不处理
			struct epoll_event *pev = &all[i];
			if (!(pev->events & EPOLLIN)) {
				// 不是读事件
				continue;
			}
 
			if (pev->data.fd == lfd) {
				// 接受连接请求
				do_accept(lfd, epfd);
			}
			else {
				// 读数据
				do_read(pev->data.fd, epfd);
			}
		}
	}
}
 
// 读数据
void do_read(int cfd, int epfd) {
 
	// 将浏览器发过来的数据, 读到buf中 
	char line[1024] = { 0 };
	// 读请求行
	int len = get_line(cfd, line, sizeof(line));
 
	//get_line调用的是recv,recv返回值为0则说明客户端断开了链接
	if (len == 0) {
		printf("客户端断开了连接...\n");
		// 关闭套接字, cfd从epoll上删除
		disconnect(cfd, epfd);
	}
	//recv失败了
	else if (len == -1) {
		perror("recv error");
		exit(1);
	}
	else {
		printf("请求行数据: %s", line);
		printf("============= 请求头 ============\n");
		// 还有数据没读完,继续读
		while (len) {
			char buf[1024] = { 0 };
			len = get_line(cfd, buf, sizeof(buf));
			printf("-----: %s", buf);
		}
		printf("============= The End ============\n");
	}
 
	// 请求行: get /xxx http/1.1
	// 判断是不是get请求  strncasecmp函数用于比较两个字符串,不区分大小写的比较前n个字符
	if (strncasecmp("get", line, 3) == 0) {
		// 处理http请求
		http_request(line, cfd);
		// 关闭套接字, cfd从epoll上del
		disconnect(cfd, epfd);
	}
}
 
// 断开连接的函数
void disconnect(int cfd, int epfd) {
	int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
	if (ret == -1) {
		perror("epoll_ctl del cfd error");
		exit(1);
	}
	close(cfd);
}
 
// http请求处理,传入request和
void http_request(const char* request, int cfd) {
 
	// 拆分http请求行
	//    get        /xxx        http/1.1
	char method[12], path[1024], protocol[12];
	//%[^ ]匹配遇到空格为止
	sscanf(request, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
 
	printf("method = %s, path = %s, protocol = %s\n", method, path, protocol);
 
	// 转码 将不能识别的中文乱码 - > 中文
	// 解码 %23 %34 %5f
	decode_str(path, path);
	// 处理path  /xx(有可能是目录,也有可能是文件)
	// 去掉path中的/
	char* file = path + 1;
	// 如果没有指定访问的资源, 默认显示资源目录中的内容
	if (strcmp(path, "/") == 0) {
		// file的值, 资源目录的当前位置
		file = "./";
	}
 
	// 获取文件属性
	struct stat st;
	int ret = stat(file, &st);
	if (ret == -1) {
		// show 404
		send_respond_head(cfd, 404, "File Not Found", ".html", -1);
		send_file(cfd, "404.html");
	}
 
	// 判断是目录还是文件
	// 如果是目录
	if (S_ISDIR(st.st_mode)) {
		// 发送头信息
		send_respond_head(cfd, 200, "OK", get_file_type(".html"), -1);
		// 发送目录信息
		send_dir(cfd, file);
	}
	else if (S_ISREG(st.st_mode))
	{
		// 文件
		// 发送消息报头
		send_respond_head(cfd, 200, "OK", get_file_type(file), st.st_size);
		// 发送文件内容
		send_file(cfd, file);
	}
}
 
// 发送目录内容,拼写一个html表格
void send_dir(int cfd, const char* dirname) {
 
	// 拼一个html页面<table></table> table可以显示多行多列,便于显示文件信息
	char buf[4096] = { 0 };
 
	//目录名
	sprintf(buf, "<html><head><title>目录名: %s</title></head>", dirname);
	//当前目录
	sprintf(buf + strlen(buf), "<body><h1>当前目录: %s</h1><table>", dirname);
 
	/*-------------------------------获取当前目录中的所有内容---------------------------*/
	char enstr[1024] = { 0 };
	char path[1024] = { 0 };
 
	// 目录项二级指针
	struct dirent** ptr;
	int num = scandir(dirname, &ptr, NULL, alphasort);
 
	// 遍历
	for (int i = 0; i < num; ++i){
		//获取文件名
		char* name = ptr[i]->d_name;
 
		// 拼接文件的完整路径
		sprintf(path, "%s/%s", dirname, name);
		printf("path = %s ===================\n", path);
		struct stat st;
		stat(path, &st);
 
		encode_str(enstr, sizeof(enstr), name);
		// 如果是文件
		if (S_ISREG(st.st_mode)){
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s\">%s</a></td><td>%ld</td></tr>",
				enstr, name, (long)st.st_size);
		}
		// 如果是目录
		else if (S_ISDIR(st.st_mode)){
			sprintf(buf + strlen(buf),
				"<tr><td><a href=\"%s/\">%s/</a></td><td>%ld</td></tr>",
				enstr, name, (long)st.st_size);
		}
		//每循环一次就发送异常,清空buf,防止buf溢出
		send(cfd, buf, strlen(buf), 0);
		memset(buf, 0, sizeof(buf));
		// 字符串拼接
	}
 
	sprintf(buf + strlen(buf), "</table></body></html>");
	send(cfd, buf, strlen(buf), 0);
 
	printf("dir message send OK!!!!\n");
 
#if 0
	// 打开目录
	DIR* dir = opendir(dirname);
	if (dir == NULL){
		perror("opendir error");
		exit(1);
	}
 
	// 读目录
	struct dirent* ptr = NULL;
	while ((ptr = readdir(dir)) != NULL){
		char* name = ptr->d_name;
	}
	closedir(dir);
#endif
}
 
// 发送响应头  cfd(服务器和浏览器通讯的文件描述符),no(状态码)
//desp(对状态码的描述),type(Content-Type),len(发送的数据长度)
void send_respond_head(int cfd, int no, const char* desp, const char* type, long len) {
	char buf[1024] = { 0 };
 
	// 状态行 http不区分大小写
	sprintf(buf, "http/1.1 %d %s\r\n", no, desp);
	//防止buf溢出,先把里面的数据发送出去
	send(cfd, buf, strlen(buf), 0);
 
	// 消息报头
	sprintf(buf, "Content-Type:%s\r\n", type);
	sprintf(buf + strlen(buf), "Content-Length:%ld\r\n", len);
	send(cfd, buf, strlen(buf), 0);
 
	// 空行
	send(cfd, "\r\n", 2, 0);
}
 
// 发送文件
void send_file(int cfd, const char* filename){
	// 打开文件
	int fd = open(filename, O_RDONLY);
	if (fd == -1){
		// show 404
		return;
	}
 
	// 循环读文件
	char buf[4096] = { 0 };
	int len = 0;
	while ((len = read(fd, buf, sizeof(buf))) > 0){
		// 发送读出的数据
		send(cfd, buf, len, 0);
	}
	if (len == -1){
		perror("read file error");
		exit(1);
	}
 
	close(fd);
}
 
// 解析http请求消息的每一行内容
int get_line(int sock, char *buf, int size) {
	int i = 0;
	char c = '\0';
	int n;
	//每次读一个字节,判断合理就放入缓冲区中
	while ((i < size - 1) && (c != '\n')) {
		n = recv(sock, &c, 1, 0);
		if (n > 0) {
			if (c == '\r') {
				//MSG_PEEK 使得recv以拷贝的方式从缓冲区中读取数据(否则读取之后,缓冲区中的数据就没了)
				//试探性的获取缓冲区中的数据量
				n = recv(sock, &c, 1, MSG_PEEK);
				//缓冲区中有数据并且结尾是 \n  ,则读取数据
				if ((n > 0) && (c == '\n')) {
					recv(sock, &c, 1, 0);
				}
				else {
					c = '\n';
				}
			}
			buf[i] = c;
			i++;
		}
		else {
			c = '\n';
		}
	}
	buf[i] = '\0';
 
	//recv失败
	if (n == -1) {
		i = -1;
	}
 
	return i;
}
 
// 接受新连接处理
void do_accept(int lfd, int epfd) {
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
	int cfd = accept(lfd, (struct sockaddr*)&client, &len);
	if (cfd == -1) {
		perror("accept error");
		exit(1);
	}
 
	// 打印客户端信息
	char ip[64] = { 0 };
	//inet_ntop大端整型转淀粉十进制ip地址
	printf("New Client IP: %s, Port: %d, cfd = %d\n",
		inet_ntop(AF_INET, &client.sin_addr.s_addr, ip, sizeof(ip)),
		ntohs(client.sin_port), cfd);
 
	// 设置cfd为非阻塞(默认是阻塞的)
	int flag = fcntl(cfd, F_GETFL);
	flag |= O_NONBLOCK; //非阻塞
	fcntl(cfd, F_SETFL, flag);
 
	// 得到的新节点挂到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);
	}
}
 
//初始化监听,传入端口号和epoll书的根节点
int init_listen_fd(int port, int epfd) {
 
	// 创建监听的套接字
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	if (lfd == -1) {
		perror("socket error");
		exit(1);
	}
 
	// lfd绑定本地IP和port
	struct sockaddr_in serv;
	memset(&serv, 0, sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(port);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
 
	// 端口复用
	int flag = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
	int ret = bind(lfd, (struct sockaddr*)&serv, sizeof(serv));    //绑定
	if (ret == -1) {
		perror("bind error");
		exit(1);
	}
 
	// 设置监听,最大是128,这里写个64是随意写的
	ret = listen(lfd, 64);
	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;
}
 
// 16进制数转化为10进制
int hexit(char c) {
	if (c >= '0' && c <= '9')
		return c - '0';
	if (c >= 'a' && c <= 'f')
		return c - 'a' + 10;
	if (c >= 'A' && c <= 'F')
		return c - 'A' + 10;
 
	return 0;
}
 
//字符转成16进制数
void encode_str(char* to, int tosize, const char* from) {
	int tolen;
 
	for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) {
		//isalnum判断字符是不是数字(判断字符是否需要解码)
		//http协议中, /_.~这四个不需要转
		if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) {
			//不需要转,则原封不动的拷贝一份
			*to = *from;
			++to;
			++tolen;
		}
		else {
			//字符转成十六进制数—— *from取字符值,然后与十六进制数字做按位与
			sprintf(to, "%%%02x", (int)*from & 0xff);
			//to永远指向字符从末尾
			to += 3;
			tolen += 3;
		}
	}
	*to = '\0';
}
 
//编码,用作回写浏览器的时候,将除字母、数字以及 /_.~以外的字符转义后回写
//将16进制数转换成字符
void decode_str(char *to, char *from) {
	for (; *from != '\0'; ++to, ++from) {
		//十六进制转十进制
		if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
			//依次判断from中 %20 三个字符
			*to = hexit(from[1]) * 16 + hexit(from[2]);
			//移除已经处理的两个字符
			from += 2;
		}else {
			*to = *from;
		}
	}
	*to = '\0';
}
 
// 通过文件名获取文件的类型
const char *get_file_type(const char *name){
	char* dot;
 
	// 自右向左查找‘.’字符, 如不存在返回NULL
	dot = strrchr(name, '.');
	if (dot == NULL)
		return "text/plain; charset=utf-8";
	if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
		return "text/html; charset=utf-8";
	if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
		return "image/jpeg";
	if (strcmp(dot, ".gif") == 0)
		return "image/gif";
	if (strcmp(dot, ".png") == 0)
		return "image/png";
	if (strcmp(dot, ".css") == 0)
		return "text/css";
	if (strcmp(dot, ".au") == 0)
		return "audio/basic";
	if (strcmp(dot, ".wav") == 0)
		return "audio/wav";
	if (strcmp(dot, ".avi") == 0)
		return "video/x-msvideo";
	if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
		return "video/quicktime";
	if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
		return "video/mpeg";
	if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
		return "model/vrml";
	if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
		return "audio/midi";
	if (strcmp(dot, ".mp3") == 0)
		return "audio/mpeg";
	if (strcmp(dot, ".ogg") == 0)
		return "application/ogg";
	if (strcmp(dot, ".pac") == 0)
		return "application/x-ns-proxy-autoconfig";
 
	return "text/plain; charset=utf-8";
}

epoll_server.h

#ifndef _EPOLL_SERVER_H
#define _EPOLL_SERVER_H
 
int init_listen_fd(int port, int epfd);
void epoll_run(int port);
void do_accept(int lfd, int epfd);
void do_read(int cfd, int epfd);
int get_line(int sock, char *buf, int size);
void disconnect(int cfd, int epfd);
void http_request(const char* request, int cfd);
void send_respond_head(int cfd, int no, const char* desp, const char* type, long len);
void send_file(int cfd, const char* filename);
void send_dir(int cfd, const char* dirname);
void encode_str(char* to, int tosize, const char* from);
void decode_str(char *to, char *from);
const char *get_file_type(const char *name);
 
#endif
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux服务器开发——epoll模式web服务器代码 的相关文章

  • 如何在 Android 中从网络浏览器 (Chrome) 打开任何应用程序?我与 A Href 链接有什么关系?

    我想从我的网络浏览器打开第三方应用程序 所以 我没有任何清单文件或任何东西 我有一个网页 我想要一个可以打开第三方应用程序的链接 例如 Twitter 或 Opera 如何构建锚链接来打开该应用程序 谢谢 基于意图的 URI 的基本语法如下
  • 如何定义状态?

    有谁有一个好的定义state在网络应用程序的上下文中 更具体地说 什么是state在 React 的上下文中意味着 这与第一个定义有什么不同吗 我看到 状态 这个术语在 React 开发中被大量使用 但我一直无法找到一个可靠 简洁的定义 两
  • 如何判断网站上是否加载了 Flash?

    我正在对一个非常大的网站进行一些测试 创建了许多不同的开发人员 有没有办法判断网站是否正在使用flash questions tagged flash和 或已加载闪存plugin questions tagged plugin 如果页面使用
  • 多次渲染一个组件 React.js

    这是一个简单计数器的代码 但是 当我渲染视图时 我没有得到任何输出 请告诉我代码有什么问题 按下按钮 计数器就会递增并呈现在屏幕上 var Title React createClass getInitialState function r
  • 在 ASP Web API 中指定无效参数时返回错误

    我正在使用 C 和 ASP NET Web API 创建一个 API 并且希望它在使用无法识别的参数时返回错误 例如 api Events 应该有一个事件列表 api Events startTime startTime 应该返回在特定时间
  • 如何使用 FastAPI 返回 HTMLResponse

    是否可以在端点显示 HTML 文件 例如用户正在访问的主页 是的 FastAPI 可能有HTMLResponse 您可以返回一个HTMLResponse from fastapi import FastAPI from fastapi re
  • 使用 ASP.NET 进行卷曲请求

    我已经阅读了 Stack 上的其他一些帖子 但我无法让它工作 当我在Windows机器上的git中运行curl命令时 它在我的电脑上工作正常 但是当我将其转换为asp net时 它不起作用 private void BeeBoleReque
  • 如何在没有服务器或服务器端脚本的情况下创建动态访问数据库的内联网网站?

    问题是 必须在没有服务器托管的情况下创建 Intranet 网页 并且我一直在尝试使用 HTML 和 javascript 更新 Access 数据库 但这不适用于以下代码 请帮助 当以下代码从 Visual Studio 运行时 出现错误
  • 客户端Web - 如何随时获取当前记录ID

    我正在努力 基于工作流程状态的不同权限 https stackoverflow com questions 10994216 different permissions based on workflow state问题 但我正在努力解决这
  • Javascript - 事件属性的浏览器命名约定让我感到困惑

    我一直想知道为什么不同浏览器的 JavaScript 中事件的 target sourceElement 的属性名称存在差异 event srcElement in Internet Explorer event target in mos
  • 如何使用 mysql 从 php 中的表中返回多行

    我决定为我的家人建立一个梦幻足球网站 但我无法从数据库返回多行 我想要的 进行一次 sql 调用并获取整个玩家列表 以便我可以填充一个对象或对象列表 如果整个桌子都可以归还那就太好了 我的目标是简单地向用户显示待选秀的可用球员列表 目前 通
  • 在 Google Cloud 中设置网站? [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我完全迷失在这之中 我习惯于使用带有 cpanel 的简单 Web 主机来制作网站 只需使用 filezilla 登录并上传我的 ht
  • Ant Design Collapse - 关闭按钮

    我是 Ant Design 的初学者 在使用 Ant Design 库中的 Collapse 和 Form 时遇到了这个问题 我已经设置了我的页面 其中添加新项目的表单位于折叠中 并且折叠下方有一个项目列表
  • Tomcat 是否立即支持 JAX-RS(它是否支持 JAX-RS)?

    从教材 RESTful Java with JAX RS 中我们可以读到 如果我们的应用程序服务器是 JAX RS 感知的 或者换句话说 与 JAX RS 紧密集成 则声明我们的ShoppingApplication作为 servlet 的
  • IIS 7.5:对网站的初始请求永远不会加载

    当我第一次浏览我的网站时 互联网不断旋转 加载 如果我两秒钟后尝试再次加载它 一切都会正常 就好像它 睡着了 一样 我想这一定和 回收 有关 我已将 空闲超时 设置为 0 将回收 定期时间间隔 设置为 0 在浏览我的网站之前 我查看了 工作
  • 通过站点到站点 VPN 将 Azure 网站连接到本地数据库

    我的目标是运行一个天蓝色的网站 该网站可以访问我们本地公司数据库中的数据 我按照网络上的教程设置了一个 Azure 虚拟网络 并通过站点到站点 VPN 将其连接到我们本地公司网络 在天蓝色门户中 我可以看到连接实际上正在工作 并且数据已被接
  • URL中的gs_upl是什么意思?

    在任何谷歌搜索 URL 中 gs upl 是什么意思 例如 那么 gs upl 1045l1663l0l3648l4l4l0l0l0l0l258l682l0 3 1l4l0 在这里意味着什么 从构建的脚本gs upl j 我找到 funct
  • 使用 REST API 进行正确的会话管理

    我已经完成了 RESTful API 的设计 其中我使用作为参数发送的 API 令牌对每个请求进行身份验证 现在我想创建一个客户端界面 我想知道什么是管理每个客户端的会话的正确安全方法browser客户 我想过一个流程来保持服务器端无状态
  • C:epoll和多线程

    我需要创建专门的 HTTP 服务器 为此我计划使用 epoll sycall 但我想利用多个处理器 核心 但我无法提出架构解决方案 ATM我的想法如下 使用自己的epoll描述符创建多个线程 主线程接受连接并将它们分配给线程epoll 但还
  • 在 html 中创建子页面 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 假设我有一个网站http www example com http www example com 如何为此页面创建更多子页面 即 w

随机推荐

  • NVIDIA-smi

    上图是服务器上 GeForce GTX 1080 Ti 的信息 下面一一解读参数 上面的表格中的红框中的信息与下面的四个框的信息是一一对应的 GPU GPU 编号 Name GPU 型号 Persistence M 持续模式的状态 持续模式
  • 剑指offer-10 二进制数字1的个数

    输入一个int型数字 输出它作为二进制的1的个数 如9的二进制为1001 有2个1 则输出2 法一 因为int型有4个字节 一共32位 每次将输入右移1 并和1进行 与 操作 将结果累加 则为1的个数 public class Test10
  • 二维各向同性介质弹性波数值模拟(交错网格有限差分法)

    一 一阶速度 应力弹性波方程 在二维各向同介质xoz平面内 假定体力为0 从上面方程当中 我们为了得到各点的应力和速度值 就需要得到关于对时间t和空间x z的偏导 二 时间上的2M阶差分 由Taylor公式得 三 空间2N阶近似差分 由一阶
  • 【2023】Python安装教程

    一 Python下载 1 进入Python官网 官网地址 https www python org 2 点击 Downloads 展开后点击 Windows 跳转到下载python版本页面 选择 Stable Releases 稳定版本 我
  • 尤其抖音,不如学习

    什么是我们最宝贵的资源 是钱吗 不不不 我们最宝贵的资源是时间 与其把时间耗费在刷抖音 或者其他没多少价值的事情上 不如多学习一些有用的技能 今天 小灰给大家推荐几个有用的公众号 这些公众号有些是IT相关 有些是教我们如何穿衣打扮 有些是讨
  • 超出表空间"users"的空间限额

    这是因为用户被数据库限制了在建表的表空间 执行一下下面的语句后 再执行建表语句 alter user 用户名 quota unlimited on 表空间名字
  • shell中if多重条件

    if a aa a b bb c cc a d dd then echo success fi
  • APP自动化之weditor工具

    由于最近事情颇多 许久未更新文章 大家在做APP自动化测试过程中 可能使用的是Appium官方提供的inspect进行元素定位 但此工具调试不方便 于是今天给大家分享一款更好用的APP定位元素工具 weditor weditor基于web网
  • linux查看可执行文件的编译器版本

    一部分可执行程序 可以使用这个方法快速确认 strings a program name grep i gcc 输出 GCC Debian 4 8 4 1 4 8 4
  • django/CVE-2017-12794XSS漏洞复现

    docker搭建漏洞复现环境 漏洞原理看帮助文档 Django debug page XSS漏洞 CVE 2017 12794 分析 Django发布了新版本1 11 5 修复了500页面中可能存在的一个XSS漏洞 这篇文章说明一下该漏洞的
  • 挖洞经验

    本文分享的是作者在一次众测中的SQL报错型注入漏洞发现过程 有趣之处在于 在后续漏洞利用的构造中 如果在目标服务端数据库逻辑的INSERT查询中使用逗号 Comma 将导致构造的Payload不可用 这种情况下 作者通过综合Time bas
  • 在ubuntu16.04搭建Qt4.8.6嵌入式开发环境

    环境以及软件版本 虚拟机环境 ubuntu16 04 64位 虚拟机环境中gcc的版本 gcc version 5 4 0 20160609 Qt版本 qt everywhere opensource src 4 8 6 tar gz 下载
  • mac安装Python3.7

    https www cnblogs com linxue p 10097785 html
  • 基础笔记(三):网络协议之Tcp、Http

    目录 一 网络协议 二 TCP Transmission Control Protocol 传输控制协议 TCP头格式 TCP协议中的三次握手和四次挥手 TCP报文抓取工具 三 HTTP HyperText Transfer Protoco
  • 关于android webview 的那些坑

    近日做项目的时候使用webview 遇到一些麻烦 第一条 H5页面选择图片时 点击无效 无法选择照片 第二条 当修复完第一条bug时 出现新bug 如果选择照片时 什么都不选返回 会出现无法点击的情况 第三条 如何打开PDF格式的文件 首先
  • ubuntu16.04 cuda8.0 caffe 编译时遇到 caffe fatal error: cublas_v2.h 没有那个文件或目录

    caffe 配置参考 http blog csdn net leo xu06 article details 53010900 make caffe的过程中碰过 include caffe util device alternate hpp
  • WEB---在删改查:

    数据库 SQLyog Ultimate v12 09 64 bit MySQL 5 7 27 log Database myshool 40101 SET NAMES utf8 40101 SET SQL MODE 40014 SET OL
  • 如何在 Java 中进行日志记录

    您可以使用本指南为您的应用程序发现 理解和使用正确的 Java 日志库 例如 Log4j2 Logback 或 java util logging 日志 似乎 是一个非常简单的主题 但在实践中可能相当棘手 并且没有在任何地方进行足够详细的介
  • webpack处理样式资源(css less sass scss)

    Webpack 本身是不能识别样式资源的 所以我们需要借助 Loader 来帮助 Webpack 解析样式资源 一 处理css样式资源 去项目根目录新建css文件夹 在css文件夹下新增index css文件 内容如图 在src main
  • Linux服务器开发——epoll模式web服务器代码

    Linux服务器开发 epoll模式web服务器代码 1 main c 2 epoll server c 2 1 初始化epoll树 初始化监听的描述符 2 2 接受新的连接请求并上树 2 3 已经在树上的进行读数据 2 4 http请求解