HTTP GET/POST 请求时,空格应该编码为 %20 还是 +?

「灵异」的空格

1.%20 还是 + ?

这个是个史诗级的大坑,我曾经被这个协议冲突坑了一天。

开始讲解前先看个小测试,在浏览器里输入 blank test( blank 和 test 间有个空格),我们看看浏览器如何处理的:

从动图可以看出浏览器把空格解析为一个加号「+」。

是不是感觉有些奇怪?我们再做个测试,用浏览器提供的几个函数试一下:

encodeURI("q=blank test")        // "q=blank%20test"
new URLSearchParams("q=blank test").toString() // "q=blank+test"

代码是不会说谎的,其实上面的结果都是正确的,encode 结果不一样,是因为 URI 规范和 W3C 规范冲突了,才会搞出这种让人疑惑的乌龙事件。

2.冲突的协议

我们首先看看 URI 中的保留字,这些保留字不参与编码。保留字符一共有两大类:

gen-delims:: / ? # [ ] @
sub-delims:! $ & ' ( ) * + , ; =

URI 的编码规则也很简单,先把非限定范围的字符转为 16 进制,然后前面加百分号。

空格这种不安全字符转为十六进制就是 0x20,前面再加上百分号 % 就是 %20:

所以这时候再看 encodeURIComponent 和 encodeURI 的编码结果,就是完全正确的。

既然空格转为%20 是正确的,那转为 + 是怎么回事?这时候我们就要了解一下 HTML form 表单的历史。

早期的网页没有 AJAX 的时候,提交数据都是通过 HTML 的 form 表单。form 表单的提交方法可以用 GET 也可以用 POST,大家可以在 MDN form 词条上测试:

经过测试我们可以看出表单提交的内容中,空格都是转为加号的,这种编码类型就是 application/x-www-form-urlencoded,在 WHATWG 规范里是这样定义的:

到这里基本上就破案了,URLSearchParams 做 encode 的时候,就按这个规范来的。我找到了 URLSearchParams 的 Polyfill 代码,里面就做了 %20 到 + 的映射:

replace = {
    '!': '%21',
    "'": '%27',
    '(': '%28',
    ')': '%29',
    '~': '%7E',
    '%20': '+', // <= 就是这个
    '%00': '\x00'
}

规范里对这个编码类型还有解释说明:

The application/x-www-form-urlencoded format is in many ways an aberrant monstrosity, the result of many years of implementation accidents and compromises leading to a set of requirements necessary for interoperability, but in no way representing good design practices. In particular, readers are cautioned to pay close attention to the twisted details involving repeated (and in some cases nested) conversions between character encodings and byte sequences. Unfortunately the format is in widespread use due to the prevalence of HTML forms.

这种编码方式就不是个好的设计,不幸的是随着 HTML form 表单的普及,这种格式已经推广开了

其实上面一大段句话就是一个意思:这玩意儿设计的就是

原文链接.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个基于 Linux C 多线程的 HTTP 服务器示例,支持 GET、POST 方法,并能够处理表单提交: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h> #include <sys/socket.h> #include <netinet/in.h> #include <ctype.h> #define PORT 8080 #define MAX_CONNECTIONS 10 #define BUFFER_SIZE 1024 #define MAX_REQUEST_SIZE 8192 #define MAX_QUERY_PARAMS 16 #define MAX_HEADERS 32 #define MAX_POST_PARAMS 16 typedef struct { int conn_fd; char method[8]; char uri[MAX_REQUEST_SIZE]; char query_params[MAX_QUERY_PARAMS][MAX_REQUEST_SIZE]; int num_query_params; char headers[MAX_HEADERS][MAX_REQUEST_SIZE]; int num_headers; char post_params[MAX_POST_PARAMS][MAX_REQUEST_SIZE]; int num_post_params; } http_request; void parse_request_line(char *line, http_request *request) { char *method, *uri, *query; method = strtok(line, " "); uri = strtok(NULL, " "); query = strchr(uri, '?'); // parse method if (method) { strncpy(request->method, method, sizeof(request->method)); } else { strncpy(request->method, "GET", sizeof(request->method)); } // parse URI and query parameters if (uri) { if (query) { strncpy(request->uri, uri, query-uri); query++; while (*query) { char *next = strchr(query, '&'); if (next) { strncpy(request->query_params[request->num_query_params], query, next-query); request->query_params[request->num_query_params][next-query-query] = '\0'; request->num_query_params++; query = next + 1; } else { strncpy(request->query_params[request->num_query_params], query, MAX_REQUEST_SIZE); request->num_query_params++; break; } } } else { strncpy(request->uri, uri, sizeof(request->uri)); } } } void parse_header_line(char *line, http_request *request) { char *name, *value; name = strtok(line, ": "); value = strtok(NULL, "\r\n"); if (name && value) { strncpy(request->headers[request->num_headers], line, sizeof(request->headers[0])); request->num_headers++; } } void parse_post_params(char *data, http_request *request) { while (*data) { char *next = strchr(data, '&'); if (next) { strncpy(request->post_params[request->num_post_params], data, next-data); request->post_params[request->num_post_params][next-data-data] = '\0'; request->num_post_params++; data = next + 1; } else { strncpy(request->post_params[request->num_post_params], data, MAX_REQUEST_SIZE); request->num_post_params++; break; } } } void *handle_connection(void *arg) { http_request *request = (http_request*)arg; char buffer[BUFFER_SIZE]; ssize_t n; // read request message memset(buffer, 0, sizeof(buffer)); n = read(request->conn_fd, buffer, sizeof(buffer)-1); if (n <= 0) { close(request->conn_fd); free(request); pthread_exit(NULL); } // parse request message char *line = strtok(buffer, "\r\n"); if (line) { parse_request_line(line, request); } while ((line = strtok(NULL, "\r\n"))) { parse_header_line(line, request); } // handle request method if (strcasecmp(request->method, "GET") == 0) { // handle GET request char *response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\nHello, world!"; write(request->conn_fd, response, strlen(response)); } else if (strcasecmp(request->method, "POST") == 0) { // handle POST request char *content_length_header = NULL; for (int i = 0; i < request->num_headers; i++) { if (strncasecmp(request->headers[i], "Content-Length:", 15) == 0) { content_length_header = request->headers[i]; break; } } if (content_length_header) { int content_length = atoi(content_length_header+15); char *data = buffer + strlen(buffer) + 2; n = read(request->conn_fd, data, content_length); if (n > 0) { data[n] = '\0'; parse_post_params(data, request); } } char response[BUFFER_SIZE]; snprintf(response, sizeof(response), "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" "<html><body><h1>POST parameters:</h1><ul>"); for (int i = 0; i < request->num_post_params; i++) { char *param = request->post_params[i]; for (int j = 0; j < strlen(param); j++) { if (param[j] == '+') { param[j] = ' '; } } char *name = strtok(param, "="); char *value = strtok(NULL, "="); snprintf(response+strlen(response), sizeof(response)-strlen(response), "<li>%s: %s</li>", name, value); } strncat(response, "</ul></body></html>", sizeof(response)-strlen(response)-1); write(request->conn_fd, response, strlen(response)); } close(request->conn_fd); free(request); pthread_exit(NULL); } int main() { int sock_fd, conn_fd; struct sockaddr_in serv_addr, client_addr; socklen_t client_len; pthread_t threads[MAX_CONNECTIONS]; // create socket sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("Failed to create socket"); exit(EXIT_FAILURE); } // set server address memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); // bind socket to server address if (bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { perror("Failed to bind socket"); exit(EXIT_FAILURE); } // listen for connections if (listen(sock_fd, MAX_CONNECTIONS) < 0) { perror("Failed to listen for connections"); exit(EXIT_FAILURE); } printf("HTTP server is listening on port %d\n", PORT); // handle connections in separate threads while (1) { client_len = sizeof(client_addr); conn_fd = accept(sock_fd, (struct sockaddr*)&client_addr, &client_len); if (conn_fd < 0) { perror("Failed to accept connection"); continue; } http_request *request = (http_request*)malloc(sizeof(http_request)); memset(request, 0, sizeof(http_request)); request->conn_fd = conn_fd; pthread_t thread; if (pthread_create(&thread, NULL, handle_connection, request) < 0) { perror("Failed to create thread"); close(conn_fd); free(request); continue; } pthread_detach(thread); } return 0; } ``` 上述代码创建了一个监听在 8080 端口的 HTTP 服务器,可以处理多个连接。在每个连接到来,都会创建一个新的线程来处理请求。在处理请求的线程中,首先读取请求消息,然后解析请求行、请求头和请求体中的参数,根据请求方法的不同返回不同的响应消息。 需要注意的是,在处理 POST 请求,需要首先读取请求头中的 Content-Length 字段,然后读取相应长度的请求体数据,最后解析 POST 参数。在本例中,使用了 URL 编码格式,因此需要将 POST 参数中的 + 号替换为空格

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值