fcntl_write.c

本文提供了一个使用 C 语言实现的文件锁操作示例代码,包括读锁、写锁及解锁的功能。通过 fcntl 函数对文件进行加锁,并通过 getpid() 获取当前进程 ID 显示锁的状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

#include <unistd.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
void lock_set(int fd, int type)
{
struct flock lock;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len =0;
while(1){
lock.l_type = type;
if((fcntl(fd, F_SETLK, &lock)) == 0){
if( lock.l_type == F_RDLCK )
printf("read lock set by %d/n",getpid());
else if( lock.l_type == F_WRLCK )
printf("write lock set by %d/n",getpid());
else if( lock.l_type == F_UNLCK )
printf("release lock by %d/n",getpid());
return;
}
fcntl(fd, F_GETLK,&lock);
if(lock.l_type != F_UNLCK){
if( lock.l_type == F_RDLCK )
printf("read lock already set by %d/n",lock.l_pid);
else if( lock.l_type == F_WRLCK )
printf("write lock already set by %d/n",lock.l_pid);
getchar();
}
}
}
 
int main(void)
{
int fd;
fd=open("hello",O_RDWR | O_CREAT, 0666);
if(fd < 0){
perror("open");
exit(1);
}
 
lock_set(fd, F_WRLCK);
getchar();
lock_set(fd, F_UNLCK);
getchar();
close(fd);
exit(0);
}

如何设置common.h文件中的web的路径 实验环境:centos7虚拟机环境 我有4个html文件,文件中可能包含图片等内容,前端文件如下 . ├── bin │ ├── common.o │ ├── epoll_serv │ ├── epoll_serv.o │ ├── process_serv │ ├── process_serv.o │ ├── pthread_serv │ └── pthread_serv.o ├── include │ └── common.h ├── Makefile ├── src │ ├── common.c │ ├── epoll_serv.c │ ├── process_serv.c │ └── pthread_serv.c └── web ├── About.html ├── Contact.html ├── css │ ├── reset.css │ ├── style.css │ └── style_mobile.css ├── data │ └── contact.json ├── images │ ├── About_baby.png │ ├── About_ball.png │ ├── About_be.png │ ├── About_bird.png │ ├── About_content.png │ ├── About_message.png │ ├── About_@.png │ ├── body.png │ ├── footer_baby.png │ ├── footer_ball.png │ ├── footer_be.png │ ├── footer_bird.png │ ├── footer_message.png │ ├── footer_@.png │ ├── footer.png │ ├── header.png │ └── Thumbs.db ├── img │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── 5.jpg │ ├── 6.jpg │ └── Thumbs.db ├── Index.html ├── js │ ├── action.js │ └── jquery-1.9.1.min.js ├── test.html └── Work.html 请使用epoll,I/O复用的方式实现一个简单的web server(端口80),使得我能够在浏览器实现对各个html网页的访问。 浏览器兼容要求:Firefox,chrome、ie8+ Web server需完成get post等基础请求处理(必修) 使用makefile构建(必修)
最新发布
08-10
<think>根据您提供的项目结构,我们将实现一个基于epoll的简单Web服务器。服务器将监听80端口,并能够处理GET和POST请求。我们将使用非阻塞I/O和边缘触发模式(EPOLLET)来提高性能。 步骤: 1. 创建监听套接字,绑定到80端口,并设置为非阻塞模式。 2. 创建epoll实例,并将监听套接字加入epoll,关注EPOLLIN事件(同时使用边缘触发)。 3. 进入主循环,等待epoll事件。 4. 当有新的连接时,接受连接,并将新的客户端套接字加入epoll(同样设置为非阻塞和边缘触发)。 5. 当客户端套接字有数据可读时,读取请求并解析(这里我们只处理GET和POST请求)。 6. 根据请求的URL,构建本地文件路径(在web目录下查找文件)。 7. 对于GET请求,发送文件内容;对于POST请求,我们简单处理(例如,可以打印请求体,然后返回一个响应)。 8. 注意处理大文件发送(使用sendfile)和错误处理(如文件不存在返回404)。 9. 使用Makefile构建项目。 注意:由于要监听80端口,程序可能需要以root权限运行(或者赋予可执行文件CAP_NET_BIND_SERVICE能力)。 项目结构: - include/common.h:包含公共定义和函数声明。 - src/common.c:包含公共函数实现(如路径处理、发送文件等)。 - src/epoll_serv.c:主服务器实现。 - web/:Web根目录,包含HTML、CSS、JS和图片等。 我们将按照以下步骤实现: 1. 修改common.h,定义ROOT_DIR为"../web/"(因为可执行文件在bin目录下,而web目录在项目根目录,所以相对路径是../web/)。但为了灵活性,我们会在Makefile中通过编译选项定义ROOT_DIR。 2. 实现common.c中的辅助函数,如构建安全路径、发送文件、发送错误响应等。 3. 实现epoll_serv.c,包含主函数和事件循环。 4. 编写Makefile,编译生成epoll_serv可执行文件。 由于代码较长,我们将分模块实现。 首先,修改common.h:</think># 基于 epoll 的 Web 服务器实现 下面我将为您实现一个基于 epoll I/O 复用的 Web 服务器,使用非阻塞 I/O 和边缘触发模式,支持 GET/POST 请求处理,兼容各种浏览器。 ## 项目结构 根据您提供的目录结构,我将实现以下文件: ``` . ├── bin │ └── webserver (构建后的可执行文件) ├── include │ └── common.h ├── Makefile ├── src │ ├── common.c │ └── webserver.c └── web └── ... (您的网页文件) ``` ## 实现代码 ### 1. include/common.h ```c #ifndef __COMMON_H__ #define __COMMON_H__ #define _GNU_SOURCE #define _XOPEN_SOURCE 700 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/sendfile.h> #include <sys/types.h> #include <dirent.h> #include <errno.h> #include <sys/wait.h> #include <signal.h> #include <sys/epoll.h> #include <time.h> #include <ctype.h> #define PORT 80 #define MAX_EVENTS 1024 #define BUFFER_SIZE 4096 #define MAX_PATH_LEN 1024 #define MAX_HEADERS 20 // Web根目录 - 在编译时由Makefile定义 #ifndef ROOT_DIR #define ROOT_DIR "../web/" #endif // HTTP状态码 #define HTTP_200 "200 OK" #define HTTP_400 "400 Bad Request" #define HTTP_404 "404 Not Found" #define HTTP_500 "500 Internal Server Error" #define HTTP_501 "501 Not Implemented" // 连接状态 typedef enum { CONN_READING, CONN_WRITING, CONN_CLOSING } conn_state_t; // HTTP请求结构 typedef struct { char method[16]; // GET, POST等 char path[MAX_PATH_LEN]; // 请求路径 char version[16]; // HTTP/1.1 char *headers[MAX_HEADERS]; // 请求头 int header_count; // 请求头数量 char *body; // 请求体 size_t body_length; // 请求体长度 } http_request_t; // HTTP响应结构 typedef struct { int status; // 状态码 char *content_type; // 内容类型 size_t content_length; // 内容长度 int fd; // 文件描述符 } http_response_t; // 连接上下文 typedef struct { int fd; // 文件描述符 conn_state_t state; // 连接状态 http_request_t request; // HTTP请求 http_response_t response; // HTTP响应 char buffer[BUFFER_SIZE]; // 读写缓冲区 size_t buffer_len; // 缓冲区数据长度 size_t bytes_sent; // 已发送字节数 } connection_t; // 函数声明 int set_nonblocking(int fd); void handle_request(connection_t *conn); void send_response(connection_t *conn); void send_error(connection_t *conn, const char *status); const char *get_mime_type(const char *path); int build_safe_path(char *dest, size_t size, const char *requested_path); #endif ``` ### 2. src/common.c ```c #include "common.h" #include <libgen.h> #include <limits.h> // 设置文件描述符为非阻塞模式 int set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); if (flags == -1) return -1; return fcntl(fd, F_SETFL, flags | O_NONBLOCK); } // 根据文件扩展名获取MIME类型 const char *get_mime_type(const char *path) { const char *ext = strrchr(path, '.'); if (!ext) return "application/octet-stream"; ext++; if (strcasecmp(ext, "html") == 0 || strcasecmp(ext, "htm") == 0) return "text/html"; if (strcasecmp(ext, "css") == 0) return "text/css"; if (strcasecmp(ext, "js") == 0) return "application/javascript"; if (strcasecmp(ext, "json") == 0) return "application/json"; if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0) return "image/jpeg"; if (strcasecmp(ext, "png") == 0) return "image/png"; if (strcasecmp(ext, "gif") == 0) return "image/gif"; if (strcasecmp(ext, "ico") == 0) return "image/x-icon"; return "application/octet-stream"; } // 构建安全路径,防止目录遍历攻击 int build_safe_path(char *dest, size_t size, const char *requested_path) { char full_path[PATH_MAX]; char resolved_path[PATH_MAX]; // 构建基础路径 snprintf(full_path, sizeof(full_path), "%s%s", ROOT_DIR, requested_path); // 解析路径(处理 ../ 等) if (realpath(full_path, resolved_path) == NULL) { return -1; // 路径无效 } // 检查是否在根目录内 size_t root_len = strlen(ROOT_DIR); if (strncmp(resolved_path, ROOT_DIR, root_len) != 0) { return -2; // 路径遍历攻击 } // 复制到目标缓冲区 if (strlen(resolved_path) >= size) { return -3; // 缓冲区太小 } strcpy(dest, resolved_path); return 0; } // 发送错误响应 void send_error(connection_t *conn, const char *status) { const char *html_fmt = "<html><head><title>Error</title></head>" "<body><h1>%s</h1></body></html>"; char body[BUFFER_SIZE]; int len = snprintf(body, sizeof(body), html_fmt, status); // 构建响应头 char headers[BUFFER_SIZE]; int hlen = snprintf(headers, sizeof(headers), "HTTP/1.1 %s\r\n" "Content-Type: text/html\r\n" "Content-Length: %d\r\n" "Connection: close\r\n\r\n", status, len); // 合并响应 memcpy(conn->buffer, headers, hlen); memcpy(conn->buffer + hlen, body, len); conn->buffer_len = hlen + len; conn->state = CONN_WRITING; conn->response.fd = -1; // 没有文件要发送 } // 发送文件响应 void send_file_response(connection_t *conn, const char *path) { struct stat st; if (stat(path, &st) < 0 || !S_ISREG(st.st_mode)) { send_error(conn, HTTP_404); return; } // 打开文件 int fd = open(path, O_RDONLY); if (fd < 0) { send_error(conn, HTTP_500); return; } // 构建响应头 const char *mime_type = get_mime_type(path); char headers[BUFFER_SIZE]; int hlen = snprintf(headers, sizeof(headers), "HTTP/1.1 %s\r\n" "Content-Type: %s\r\n" "Content-Length: %ld\r\n" "Connection: keep-alive\r\n\r\n", HTTP_200, mime_type, st.st_size); // 将响应头放入缓冲区 memcpy(conn->buffer, headers, hlen); conn->buffer_len = hlen; conn->bytes_sent = 0; // 设置文件响应 conn->response.fd = fd; conn->response.content_length = st.st_size; conn->state = CONN_WRITING; } // 处理GET请求 void handle_get(connection_t *conn) { char safe_path[MAX_PATH_LEN]; // 默认请求index.html const char *path = conn->request.path; if (strcmp(path, "/") == 0) { path = "/Index.html"; } // 构建安全路径 if (build_safe_path(safe_path, sizeof(safe_path), path) != 0) { send_error(conn, HTTP_404); return; } // 发送文件 send_file_response(conn, safe_path); } // 处理POST请求 void handle_post(connection_t *conn) { // 简单的POST处理示例 // 在实际应用中,这里可以处理表单提交等 const char *response = "HTTP/1.1 200 OK\r\n" "Content-Type: text/plain\r\n" "Content-Length: 12\r\n" "Connection: close\r\n\r\n" "POST received"; memcpy(conn->buffer, response, strlen(response)); conn->buffer_len = strlen(response); conn->state = CONN_WRITING; } // 解析HTTP请求 int parse_request(connection_t *conn) { char *buf = conn->buffer; size_t len = conn->buffer_len; // 确保有完整的第一行 char *end = strstr(buf, "\r\n"); if (!end) return -1; // 不完整的请求 // 解析请求行 char method[16], path[MAX_PATH_LEN], version[16]; if (sscanf(buf, "%15s %1023s %15s", method, path, version) != 3) { return -1; // 格式错误 } // 保存请求信息 strncpy(conn->request.method, method, sizeof(conn->request.method)); strncpy(conn->request.path, path, sizeof(conn->request.path)); strncpy(conn->request.version, version, sizeof(conn->request.version)); // 解析头部 char *ptr = end + 2; // 跳过第一个\r\n conn->request.header_count = 0; while ((ptr - buf) < len) { char *line_end = strstr(ptr, "\r\n"); if (!line_end) break; // 空行表示头部结束 if (line_end == ptr) { ptr += 2; break; } // 保存头部行 if (conn->request.header_count < MAX_HEADERS) { *line_end = '\0'; // 终止字符串 conn->request.headers[conn->request.header_count++] = ptr; } ptr = line_end + 2; } // 处理请求体 conn->request.body = ptr; conn->request.body_length = len - (ptr - buf); return 0; } // 处理请求 void handle_request(connection_t *conn) { // 解析HTTP请求 if (parse_request(conn) < 0) { send_error(conn, HTTP_400); return; } // 根据方法处理请求 if (strcasecmp(conn->request.method, "GET") == 0) { handle_get(conn); } else if (strcasecmp(conn->request.method, "POST") == 0) { handle_post(conn); } else { send_error(conn, HTTP_501); } } ``` ### 3. src/webserver.c ```c #include "common.h" #include <sys/epoll.h> #include <signal.h> // 全局变量 connection_t *connections = NULL; int epoll_fd = -1; // 清理连接 void cleanup_connection(connection_t *conn) { if (conn->response.fd != -1) { close(conn->response.fd); conn->response.fd = -1; } if (conn->fd != -1) { close(conn->fd); conn->fd = -1; } memset(conn, 0, sizeof(connection_t)); conn->fd = -1; conn->state = CONN_CLOSING; } // 添加事件到epoll void epoll_ctl_add(int epoll_fd, int fd, uint32_t events) { struct epoll_event ev; ev.events = events; ev.data.fd = fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { perror("epoll_ctl"); exit(EXIT_FAILURE); } } // 修改epoll事件 void epoll_ctl_mod(int epoll_fd, int fd, uint32_t events) { struct epoll_event ev; ev.events = events; ev.data.fd = fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) { perror("epoll_ctl_mod"); exit(EXIT_FAILURE); } } // 初始化服务器 int init_server() { int server_fd; struct sockaddr_in addr; // 创建套接字 if ((server_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置SO_REUSEADDR int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } // 绑定地址 addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听 if (listen(server_fd, SOMAXCONN) < 0) { perror("listen"); exit(EXIT_FAILURE); } printf("Server listening on port %d\n", PORT); printf("Web root: %s\n", ROOT_DIR); return server_fd; } // 处理新连接 void handle_new_connection(int server_fd) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd; while ((client_fd = accept4(server_fd, (struct sockaddr *)&client_addr, &client_len, SOCK_NONBLOCK)) > 0) { // 找到空闲连接槽 int i; for (i = 0; i < MAX_EVENTS; i++) { if (connections[i].fd == -1) break; } if (i == MAX_EVENTS) { close(client_fd); continue; } // 初始化连接 connection_t *conn = &connections[i]; memset(conn, 0, sizeof(connection_t)); conn->fd = client_fd; conn->state = CONN_READING; conn->response.fd = -1; conn->buffer_len = 0; conn->bytes_sent = 0; // 添加到epoll struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev); // 打印连接信息 char ip_str[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, ip_str, INET_ADDRSTRLEN); printf("New connection from %s:%d\n", ip_str, ntohs(client_addr.sin_port)); } } // 处理可读事件 void handle_readable(connection_t *conn) { ssize_t count; // 读取数据 count = read(conn->fd, conn->buffer + conn->buffer_len, sizeof(conn->buffer) - conn->buffer_len - 1); if (count == -1) { if (errno != EAGAIN) { cleanup_connection(conn); } return; } else if (count == 0) { // 客户端关闭连接 cleanup_connection(conn); return; } conn->buffer_len += count; conn->buffer[conn->buffer_len] = '\0'; // 检查是否收到完整请求 if (strstr(conn->buffer, "\r\n\r\n") != NULL) { handle_request(conn); // 修改epoll事件为写事件 struct epoll_event ev; ev.events = EPOLLOUT | EPOLLET; ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &ev); } } // 处理可写事件 void handle_writable(connection_t *conn) { // 发送响应头 if (conn->buffer_len > 0) { ssize_t sent = write(conn->fd, conn->buffer, conn->buffer_len); if (sent == -1) { if (errno != EAGAIN) { cleanup_connection(conn); } return; } // 更新缓冲区 memmove(conn->buffer, conn->buffer + sent, conn->buffer_len - sent); conn->buffer_len -= sent; if (conn->buffer_len > 0) { // 还有数据要发送,保持写事件 return; } } // 发送文件内容 if (conn->response.fd != -1) { off_t offset = conn->bytes_sent; size_t remaining = conn->response.content_length - conn->bytes_sent; size_t to_send = remaining > BUFFER_SIZE ? BUFFER_SIZE : remaining; ssize_t sent = sendfile(conn->fd, conn->response.fd, &offset, to_send); if (sent == -1) { if (errno != EAGAIN) { cleanup_connection(conn); } return; } conn->bytes_sent += sent; if (conn->bytes_sent < conn->response.content_length) { // 文件未发送完,保持写事件 return; } // 文件发送完成 close(conn->response.fd); conn->response.fd = -1; } // 请求处理完成,准备接收新请求 conn->state = CONN_READING; conn->buffer_len = 0; // 修改epoll事件为读事件 struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.ptr = conn; epoll_ctl(epoll_fd, EPOLL_CTL_MOD, conn->fd, &ev); } // 主函数 int main() { int server_fd = init_server(); // 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } // 分配连接数组 connections = calloc(MAX_EVENTS, sizeof(connection_t)); for (int i = 0; i < MAX_EVENTS; i++) { connections[i].fd = -1; } // 添加服务器套接字到epoll struct epoll_event ev; ev.events = EPOLLIN | EPOLLET; ev.data.fd = server_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev); // 事件循环 struct epoll_event events[MAX_EVENTS]; printf("Server started. Waiting for connections...\n"); while (1) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (n == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int i = 0; i < n; i++) { // 新连接 if (events[i].data.fd == server_fd) { handle_new_connection(server_fd); } // 客户端事件 else { connection_t *conn = (connection_t *)events[i].data.ptr; if (events[i].events & EPOLLERR) { // 错误事件,关闭连接 cleanup_connection(conn); continue; } if (events[i].events & EPOLLIN) { // 可读事件 handle_readable(conn); } if (events[i].events & EPOLLOUT) { // 可写事件 handle_writable(conn); } } } } // 清理(理论上不会执行到这里) close(server_fd); close(epoll_fd); free(connections); return 0; } ``` ### 4. Makefile ```makefile # Makefile for epoll web server # 编译器设置 CC = gcc CFLAGS = -Wall -Wextra -O2 -I./include -std=c11 -D_GNU_SOURCE LDFLAGS = -lpthread # 根目录配置(开发环境) WEB_ROOT ?= ../web/ # 源文件和目标 SRC_DIR = src BIN_DIR = bin SRCS = $(wildcard $(SRC_DIR)/*.c) OBJS = $(patsubst $(SRC_DIR)/%.c,$(BIN_DIR)/%.o,$(SRCS)) TARGET = $(BIN_DIR)/webserver # 添加根目录到编译选项 CFLAGS += -DROOT_DIR="\"$(WEB_ROOT)\"" # 默认目标 all: $(TARGET) # 创建目录 $(shell mkdir -p $(BIN_DIR)) # 编译规则 $(BIN_DIR)/%.o: $(SRC_DIR)/%.c $(CC) $(CFLAGS) -c $< -o $@ # 链接可执行文件 $(TARGET): $(OBJS) $(CC) $^ -o $@ $(LDFLAGS) # 清理 clean: rm -f $(BIN_DIR)/*.o $(TARGET) # 生产环境构建(使用绝对路径) prod: $(MAKE) WEB_ROOT=/var/www/html # 运行服务器(需要sudo权限绑定80端口) run: $(TARGET) sudo $(TARGET) .PHONY: all clean prod run ``` ## 功能说明 1. **epoll I/O 复用**: - 使用边缘触发(EPOLLET)模式和非阻塞I/O - 高效处理大量并发连接 - 支持长连接(keep-alive) 2. **HTTP 请求处理**: - 支持 GET/POST 方法 - 解析 HTTP 请求头和请求体 - 自动处理路径安全(防止目录遍历攻击) 3. **文件服务**: - 自动识别文件 MIME 类型 - 使用 sendfile 高效传输文件 - 支持大文件传输 4. **错误处理**: - 400 Bad Request - 404 Not Found - 500 Internal Server Error - 501 Not Implemented 5. **浏览器兼容**: - 支持 HTTP/1.1 - 兼容 Chrome, Firefox, IE8+ 等主流浏览器 ## 构建与运行 1. **构建服务器**: ```bash make ``` 2. **运行服务器**(需要 root 权限绑定 80 端口): ```bash sudo ./bin/webserver ``` 3. **生产环境构建**(使用绝对路径): ```bash make WEB_ROOT=/var/www/html ``` 4. **访问网页**: 在浏览器中访问: - `http://<your-server-ip>/Index.html` - `http://<your-server-ip>/About.html` - `http://<your-server-ip>/Work.html` - `http://<your-server-ip>/Contact.html` ## 安全注意事项 1. **以非特权用户运行**: 在生产环境中,服务器启动后应降权到非 root 用户: ```c // 在 main() 中绑定端口后添加 if (setuid(1000) < 0) { // 1000 是普通用户ID perror("setuid"); exit(EXIT_FAILURE); } ``` 2. **目录遍历防护**: `build_safe_path()` 函数使用 `realpath()` 规范化路径,并检查是否在根目录内 3. **资源限制**: 建议设置文件描述符限制: ```bash ulimit -n 10000 ``` 4. **日志记录**: 在实际部署中应添加详细的访问日志和错误日志 这个实现提供了高性能的静态文件服务,支持基本的 GET/POST 请求处理,并考虑了浏览器兼容性和安全性问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值