HTTP协议
HTTP即超文本传输协议(HyperText Transfer Protocol),是一种用于分布式、协作式和超媒体信息系统的应用层协议。HTTP是万维网的数据通信的基础。
2. 请求信息——Request
客户端发送一个HTTP请求到服务器,请求消息格式为如下四部分组成的一个字符串。
请求行(request line)
请求头(header)
空行
请求体
第一行请求行指定<方法>、<URL>、<协议版本>以空格分隔,如下例
GET /hello HTTP/1.1
1
第二行起为请求头部,Host指出请求的目的地(主机域名);User-Agent是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送,如下实例
GET /hello HTTP/1.1
第二行起为请求头部,Host
指出请求的目的地(主机域名);User-Agent
是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送,如下实例
Host: www.google.com
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
第三行为空行,放入\r\n
字符
第四行为请求体,可以添加任意的其它数据,一般POST请求使用
3. 响应信息——Response
服务器收到客户端的请求后,就会有一个HTTP的响应消息,HTTP响应也由四部分组成
状态行
状态行由协议版本号、状态码、状态消息组成
响应头
响应头是客户端可以使用的一些信息,如:Date(生成响应的日期)、Content-Type(MIME类型及编码格式)、Connection(默认是长连接)等等
空行
第三行是空行
响应体
响应正文,服务器返回给客户端的文本信息
HTTP/1.1 200 OK
Date: Fri, 22 May 2009 06:07:21 GMT
Content-Type: text/html; charset=UTF-8
<html><head>Hello</head> <body>Hello,world </body> </html>
4. 请求方法
即请求行中指定的请求方法。HTTP/1.1协议中共定义了八种方法(也叫“动作”)来以不同方式操作指定的资源。最常用的请求方法是GET和POST
请求方法 简述
GET 请求指定的页面信息,并返回实体主体,由于服务器的限制,对get提交通常有长度限制
POST 用来传输实体的主体。虽然get也可以,但是一般是用post传输。在REST架构标准的网站中,post被用来创建资源
PUT 用来传输文件。就像FTP协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求URI指定的位置中。在REST架构标准的网站中,会开放此方法用来更新资源。
DELETE 用来删除文件,是与put相反的方法。REST架构标准的网站中,会开放此方法用来作为删除资源。
HEAD 和get方法一样,只是不返回报文主体部分。用于确认uri的有效性及资源更新的时间等
TRACE 回显服务器收到的请求,主要用于测试或诊断
CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。通常用于SSL加密服务器的链接
OPTIONS 可使服务器传回该资源所支持的所有HTTP请求方法。用’*'来代替资源名称,向Web服务器发送OPTIONS请求,可以测试服务器功能是否正常运作
4.1 GET和POST的区别
GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditPosts.aspx?name=test1&id=123456。 POST方法是把提交的数据放在HTTP包的Body中。因此GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变
GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制。
GET方式需要使用Request.QueryString来取得变量的值,而POST方式通过Request.Form来获取变量的值。
GET方式提交数据,会带来安全问题,比如一个登录页面,通过GET方式提交数据时,用户名和密码将出现在URL上,如果页面可以被缓存或者其他人可以访问这台机器,就可以从历史记录获得该用户的账号和密码。
5. 状态码
在响应信息的状态行中。状态码以3位数字组成,状态代码的第一个数字代表当前响应的类型
1xx消息——请求已被服务器接收,继续处理
2xx成功——请求已成功被服务器接收、理解、并接受
3xx重定向——需要后续操作才能完成这一请求
4xx请求错误——请求含有词法错误或者无法被执行
5xx服务器错误——服务器在处理某个正确请求时发生错误
200 OK //客户端请求成功
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的URL
500 Internal Server Error //服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
使用C实现简单HTTP服务器
可以看到,数据并不是一起发送,而是一条一条的发送。
void not_found(Client client){ /* 返回404 */
char buf[1024];
sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "your request because the resource specified\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "is unavailable or nonexistent.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
void bad_request(Client client){ /* 发送400 */
char buf[1024];
sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "Content-type: text/html\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "<P>Your browser sent a bad request, ");
send(client, buf, sizeof(buf), 0);
sprintf(buf, "such as a POST without a Content-Length.\r\n");
send(client, buf, sizeof(buf), 0);
}
void headers(Client client, const char *filename){ /* 200发送HTTP头 */
char buf[1024];
(void)filename; /* could use filename to determine file type */
strcpy(buf, "HTTP/1.0 200 OK\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
strcpy(buf, "\r\n");
send(client, buf, strlen(buf), 0);
}
501 方法
void unimplemented(Client client){ /* 501 相应方法未实现 */
char buf[1024];
sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, SERVER_STRING);
send(client, buf, strlen(buf), 0);
sprintf(buf, "Content-Type: text/html\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</TITLE></HEAD>\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
send(client, buf, strlen(buf), 0);
sprintf(buf, "</BODY></HTML>\r\n");
send(client, buf, strlen(buf), 0);
}
打开一个文件发送
void cat(Client client, FILE *resource){
char buf[1024];
fgets(buf, sizeof(buf), resource); /* 读取文件到buf中 */
while (!feof(resource)){ /* 判断文件是否读取到末尾 */
send(client, buf, strlen(buf), 0); /* 读取并发送文件内容 */
fgets(buf, sizeof(buf), resource);
}
}
读取http请求行,http请求行以\r\n结尾,所以必须用特殊的方法读取,并去掉结尾的\r\n
int get_line(Client client, char *buf, int size){
int i = 0;
char c = '\0';
while ((i < size - 1) && (c != '\n')){
if (recv(client, &c, 1, 0) > 0) {
if (c == '\r') {
if ((recv(client, &c, 1, MSG_PEEK) > 0) && (c == '\n'))
recv(client, &c, 1, 0);
else c = '\n';
}
buf[i] = c;
i++;
}else c = '\n';
}
buf[i] = '\0';
return(i);
}
接收并处理http请求
读取HTTP的请求行,根据
/* 接收客户端的连接,读取请求数据 */
void *accept_request(void* args){
Client client = (Client)args;
char *method = NULL;
char *url = NULL;
char buf[BUF_SIZE];
int numchars;
char path[512];
struct stat st;
int cgi = 0;
char *query_string = NULL;
memset(buf, 0, sizeof(buf));
/* 获取一行HTTP报文数据 */
if ((numchars = get_line(client, buf, sizeof(buf))) == 0) return NULL;
/* 获取Http请求行字段,格式为<method> <request-URL> <version> 每个字段以空白字符相连 */
method = strtok(buf, " "); /* 从请求行中分割出method字段 */
/* 本Demo仅实现GET请求和POST请求 */
if (StrCaseCmp(method, "GET") && StrCaseCmp(method, "POST")){
unimplemented(client);
return NULL;
}
/* 如果请求方法为POST,cgi标志位置1,开启cgi解析 */
if (StrCaseCmp(method, "POST") == 0) cgi = 1;
url = strtok(NULL, " "); /* 从请求行中分割出request-URL字段 */
if (StrCaseCmp(method, "GET") == 0){ /* GET请求,url可能带有"?",有查询参数 */
query_string = url;
while ((*query_string != '?') && (*query_string != '\0'))
query_string++;
if (*query_string == '?'){
cgi = 1; /* 如果带有查询参数,执行cgi解析参数,设置标志位为1 */
*query_string = '\0'; /* 将解析参数截取下来 */
query_string++;
}
}
/* url中的路径格式化到 path */
sprintf(path, "static%s", url);
/* 如果path只是一个目录,默认设置为首页 */
if (path[strlen(path) - 1] == '/') strcat(path, "index.html");
if (stat(path, &st) == -1) { /* 访问的网页不存在,则读取剩下的请求头信息并丢弃 */
while ((numchars > 0) && strcmp("\n", buf))
numchars = get_line(client, buf, sizeof(buf));
not_found(client);
}else{
/* 网页存在。路径如果是目录,显示主页 (S_IFDIR代表目录) */
if ((st.st_mode & S_IFMT) == S_IFDIR) strcat(path, "/index.html");
#ifdef _ZJ_WIN32
char *suffix = &path[strlen(path) - 4];
if (StrCaseCmp(suffix, ".cgi") == 0) cgi = 1;
#else
if ((st.st_mode & S_IXUSR) || (st.st_mode & S_IXGRP) || (st.st_mode & S_IXOTH))
cgi = 1;
#endif // _ZJ_WIN32
if (!cgi) serve_file(client, path); /* 将静态文件返回 */
else execute_cgi(client, path, method, query_string); /* 执行cgi动态解析 */
}
CloseSocket(client); /* 关闭套接字 */
return NULL;
}
读取请求行,常常涉及split的操作,对于C语言库函数strtok函数,这个函数用法比较特殊
头文件:<string.h>
函数原型:char * strtok(char *s, const char *delim);
参数s 指向将要分割的字符串,参数delim 为分割符,即以之做为分割的标志。当函数在参数s 的字符串中发现参数delim 符时则会将该字符改为\0 字符。在第一次调用时,strtok必需给予参数s 字符串,往后的调用则将参数s 设置成NULL。每次调用成功则返回下一个分割后的字符串指针。
返回值:返回下一个分割后的字符串指针,如果已无则返回NULL
#include <stdio.h>
#include <string.h>
int main(){
char str[] = "python Golang C++ Java JavaScript";
/* 以空格作为分割符分割字符串 */
char *p = strtok(str, " ");
printf("%s\n", p);
while((p = strtok(NULL, " ")))
printf("%s\n",p);
return 0;
}
Web基础
1. CGI 协议
CGI规范允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器,CGI将Web的一组简单的静态超媒体文档变成一个完整的新的交互式媒体。通俗的讲CGI就像是一座桥,把网页和WEB服务器中的执行程序连接起来,它把HTML接收的指令传递给服务器的执行程序,再把服务器执行程序的结果返还给HTML页。CGI 的跨平台性能极佳,几乎可以在任何操作系统上实现。
实际上有多种方式可以执行CGI程序,但对http的请求方法来说,只有get和post两种方法允许执行CGI脚本(实际上post方法的内部本质还是get方法,只不过在发送http请求时,get和post方法对url中的参数处理方式不一样而已)。
常用于编写CGI的语言有perl、php、python等,实际上任何一种语言都能编写CGI,java也一样能写,但java的servlet完全能实现CGI的功能,且更优化、更利于开发。
1.1 特点
CGI方式在遇到连接请求(用户请求)时先要创建CGI的子进程,激活一个CGI进程,然后处理请求,处理完后结束这个子进程。所以用CGI方式的服务器有多少连接请求就会有多少CGI子进程,子进程反复加载是CGI性能低下的主要原因。当用户请求数量非常多时,会大量挤占系统的资源如内存,CPU时间等,造成效能低下。
1.2 CGI脚本工作流程
浏览器通过HTML表单或超链接请求指向一个CGI应用程序的URL
服务器收发到请求
服务器执行所指定的CGI应用程序
CGI应用程序执行所需要的操作,通常是基于浏览者输入的内容
CGI应用程序把结果格式化为网络服务器和浏览器能够理解的文档(通常为HTML网页)
网络服务器把结果返回到浏览器中
1.3 实现原理
一般情况下,服务器和CGI程序之间是通过标准输入输出来进行数据传递的,而这个过程需要环境变量的协作方可实现。每个CGI程序只能处理一个用户请求,所以在激活一个CGI程序进程时也创建了属于该进程的环境变量。
1.服务器将URL指向一个CGI应用程序
2.服务器为应用程序执行做准备
3.应用程序执行,读取标准输入和有关环境变量
4.应用程序进行标准输出
1.3.1 CGI 接口标准
接口标准 简述
标准输入 CGI程序像其他可执行程序一样,可通过标准输入(stdin)从Web服务器得到输入信息,如Form中的数据,这就是所谓的向CGI程序传递数据的POST方法。这意味着在操作系统命令行状态可执行CGI程序,对CGI程序进行调试。POST方法是常用的方法。
环境变量 操作系统提供了许多环境变量,它们定义了程序的执行环境,应用程序可以存取它们。Web服务器和CGI接口又另外设置了自己的一些环境变量,用来向CGI程序传递一些重要的参数。CGI的GET方法还通过环境变量QUERY_STRING向CGI程序传递Form中的数据。
标准输出 CGI程序通过标准输出(stdout)将输出信息传送给Web服务器。传送给Web服务器的信息可以用多种格式,通常是以纯文本或者HTML文本的形式,这样我们就可以在命令行状态调试CGI程序,并且得到它们的输出。
对于CGI程序来说,它继承了系统的环境变量。CGI的环境变量在CGI程序启动时初始化,在结束时销毁。当一个CGI程序不是被HTTP服务器调用时,它的环境变量几乎是系统环境变量的复制,而当这个CGI程序被HTTP服务器调用时,它的环境变量就会多出以下关于HTTP服务器、客户端、CGI传输过程等内容
GET 通过在URL中嵌入的形式传递参数。对CGI程序而言,在GET请求中传递的参数要通过环境变量“QUERY_STRING”来接收。 1、参数的内容作为URL信息,用户可以看到;2、有大小的限制。
POST CGI程序从标准输入接收参数。与GET方法不同的是,参数的内容从URL信息中不能获得,对于大小也没有限制。 与GET方法问题1、2完全相反
实现一个cgi程序
//sayhi.c
#include <stdio.h>
int main(){
printf("Content-Type: text/html\n");
printf("\n");
printf("<html>");
printf("<head>");
printf("<title>CGI</title>");
printf("</head>");
printf("<body>");
printf("I am a CGI program!\n");
printf("</body>");
printf("</html>\n");
return 0;
}