在 HTTP学习笔记——报文格式
一文中,我们已经可以了解到HTTP的报文格式,有了报文格式,我们就可以实现简单的HTTP服务器了。
一个简单的HTTP服务器会包含一下几个部分:
+ 接收HTTP请求
+ 解析HTTP请求
+ 构造HTTP响应
+ 发送HTTP响应。
我们看一下如何用C语言实现简单的HTTP服务器。
在下面代码中会用到几个自定义的重要的结构体,这里先列出来:
typedef struct request_startup{
char method[64];
char req_url[1024];
char version[64];
} RequestS;
typedef struct URL{
char file_path[64];
char query_string[512];
} URL;
我们先看一下main函数
int main(int argc, char** argv)
{
int sockfd, clientfd;
short port;
struct sockaddr_in serv_addr, client_addr;
socklen_t serv_len, client_len;
port = htons(PORT);
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = port;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET, SERV_ADDR, &serv_addr.sin_addr);
Bind(sockfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr));
Listen(sockfd, 6);
while (1)
{
client_len = sizeof (client_addr);
clientfd = Accept(sockfd, (struct sockaddr*)&client_addr, &client_len);
handle_request(clientfd);
close(clientfd);
}
close(sockfd);
return 0;
}
接收请求
首先我们要单间一个socket套接字的服务端程序,然后阻塞在Accept
等待HTTP客户端的请求。当接收到客户端的连接时,我们定义了函数handle_request(clientfd);
来处理客户端的请求。在handle_request()
函数中,我们根据HTTP请求报文的格式,解析HTTP报文
解析HTTP报文:
int handle_request(int clientfd)
{
char buf[1024];
char method[32];
char req_url[1024];
char version[32];
int i, j;
int numchars;
printf("Handle_request\n");
numchars = get_line(clientfd, buf, sizeof(buf)); // Get start line
printf("get line: %s\n", buf);
i = 0;
j = 0;
/* get method from start_line */
while ( !isspace(buf[j]) && i < (sizeof (method) - 1))
{
method[i] = buf[j];
++i;
++j;
}
method[i] = '\0';
/* Get Request URL from start_line */
i = 0;
while( isspace(buf[j]) )
++j;
while ( !isspace(buf[j]) && i < (sizeof (req_url) -1 ))
{
req_url[i] = buf[j];
++i;
++j;
}
req_url[i] = '\0';
printf("Get URL:%s\n", req_url);
/* Get Version from start_line */
i = 0;
while( isspace(buf[j]) )
++j;
while ( buf[j] != '\0' && i < (sizeof (version) -1 ))
{
version[i] = buf[j];
++i;
++j;
}
version[i] = '\0';
if(!strcasecmp(method, "GET"))
{
printf("%s\n", req_url);
response_GET(clientfd, req_url);
printf("GET finished%s\n", req_url);
}
else
{
NotSupportMethod();
}
return 0;
}
首先通过get_line()
读取HTTP报文的起始行,然后解析出HTTP请求报文的方法、请求的URL。
目前我们只支持GET方法,其他方法进入NotSupportMethod()
处理。
当HTTP报文请求的方法是GET时,我们进入response_GET()
处理。
void response_GET(int clientfd, char *url_str)
{
printf("Response GET...\n");
URL url;
prase_url(url_str, &url);
serve_file(clientfd, url.file_path);
}
该函数有两个调用,prase_url()
用来解析客户端请求的url, 然后将他转换为服务器上响应资源的地址。 比如客户端的输入的网址为http://127.0.0.1:8080/index.html
,则报文中的URL为/index.html
该报文请求的是服务器上相对路径为./index.html
的资源。
解析了URL之后,我们就需要想客户端做出响应了。对于GET
请求而言,就是把报文请求的资源返回给客户端,这一部分通过serve_file
实现。
构造并发送HTTP响应
void serve_file(int clientfd, const char* file_path)
{
printf("Serve file...\n");
FILE *resource;
char buf[1024];
char filename[1024];
int numchars = 1;
int file_len = 0;
filename[0] = '.';
filename[1] = '\0';
strcat(filename, file_path);
printf("respond file: %s\n", filename);
resource = fopen(filename, "r");
fseek(resource,0,2); //指针:移动到文件尾部
file_len = ftell(resource); //返回指针偏离文件头的位置(即文件中字符个数)
fseek(resource,0,0); //指针:移动到文件尾部
printf("====file_len:%d=======\n", file_len);
if (resource == NULL)
{
printf("File not found!\n");
//not_found(client);
}
else
{
response_header(clientfd, filename);
fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(clientfd, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
}
fclose(resource);
}
HTTP响应报文我们主要分为两部分: 非实体部分(包括起始行和首部)、实体部分(资源的内容)。
对于一个简单的GET
请求,其实体部分就是资源./index.html
的内容。 实体部分(资源的内容)的构造与发送其实就是读取资源内容,然后发送给客户端:
fgets(buf, sizeof(buf), resource);
while (!feof(resource))
{
send(clientfd, buf, strlen(buf), 0);
fgets(buf, sizeof(buf), resource);
}
非实体部分(起始行和首部)的构造与发送在函数response_header(clientfd, filename);
中实现。
void response_header(int client, const char* filename)
{
char buf[1024];
(void)filename; /* could use filename to determine file type */
ssize_t n;
strcpy(buf, "HTTP/1.0 200 OK\r\nContent-Type: text/html\r\nContent-Length: 125\r\n\r\n");
send(client, buf, strlen(buf), 0);
}
这里要提一下Content-Length: 125
这一个首部,这里我写了固定值125(其实它是资源内容的大小,这里为了简单说明,在代码中写了固定值)。Content-Length
这个首部可以说是必须的,它能帮助客户端识别HTTP响应报文的结束。正常情况下,我们必须给这个首部赋上准确的值,否则一些客户端可能无法正确的识别HTTP报文,发生连接错误、浏览器不进行缓存。(如会发现浏览器已经请求到了index.html页面,但是闪一下就不见了。)。