【八】http服务器开发--实现一个http服务器

一、整体概述

本节主要实现一个http服务器的示例;该http服务器的主要功能是,在浏览器端访问服务器ip+html文件名,结果为如果服务端如果存在我们想要的html文件,则将其展示在浏览器端。

整体流程下图所示:
在这里插入图片描述
主要步骤就三个,接收http请求,解析http请求,相应http请求。

二、接收http请求

这部分主要负责接收http请求,并对请求信息进行解析。

从上一节中我们知道,客户端发来的请求信息,主要包括请求行,请求头部,请求内容等组成,
在这里插入图片描述
通过解析请求行我们可以得到请求方法和URL等,然后解析请求头部信息,那怎么判断请求头部信息结束了呢?由图中可以看出,当连续解析到两个回车符时,就是请求头部结束了,剩下的就是请求数据。

首先是解析请求行,解析请求行的函数如下:

//读取请求行:返回值: -1 表示读取出错, 等于0表示读到一个空行, 大于0 表示成功读取一行
int get_line(int sock, char *buf, int size)
{
	int count = 0;//计数器,用来对当前已经读取的字符数进行计数
	char ch = '\0';//字符串结束符
	int len = 0;
	
	//当前读取的字符数小于传入的字符数size-1,且ch不是换行符时
	while( (count<size - 1) && ch!='\n'){
		len = read(sock, &ch, 1);//将sock里的数据读到ch中,每次读1个字符;当返回值len为1时,说明返回的是1,即读取成功
		
		if(len == 1)
        {
			if(ch == '\r')//如果读到的是回车符,则continue,进行下一次循环
            {
				continue;
			}else if(ch == '\n')//如果读到的是换行符,说明这一行读完了,break跳出循环
            {
				//buf[count] = '\0';
				break;
			}
			
			//这里处理一般的字符
			buf[count] = ch;
			count++;
			
		}
        else if( len == -1 )
        {//读取出错
			perror("read failed");
			count = -1;
			break;
		}
        else 
        {// read 返回0,客户端关闭sock 连接.
			fprintf(stderr, "client close.\n");
			count = -1;
			break;
		}
	}
	
	if(count >= 0) 
        buf[count] = '\0';//为读取的这一行添加字符串结束符
	
	return count;
}

解析完请求行后,我们需要拿出请求行中的URL,然后拿到URL中包含的html文件名,并在我们本地的html文件夹中搜索是否存在该URL对应的html文件。
编写了一个名为do_http_request的函数去解析收到的请求信息,功能为解析请求拿到URL,并在本地路径中查找该html的功能,代码过程如下:

void do_http_request(int client_sock)
{
	int len = 0;
	char buf[256];
	char method[64];//请求的方法,是get还是post
	char url[256];//请求的URL
	char path[256];
	
	struct stat  st;
	
	
	/*读取客户端发送的http 请求*/
	
	
	//1.读取请求行
	len = get_line(client_sock, buf, sizeof(buf));//请求行只要一行,因此只需要读一次即可
    printf("--line 96,do_http_request --,result for get_line is [%s]\n",buf);
	
	if(len > 0)
    {   //读到了请求行,首先获取请求的方法
		int i=0, j=0;
		while(!isspace(buf[j]) && (i<sizeof(method)-1))//如果不是空格,且没超过method的容量最大值;遇到空格说明请求方法结束了
        {
			method[i] = buf[j];//用来存放请求方法,是get还是post
			i++;
			j++;
		}
		
		method[i] = '\0';
		if(debug) printf("request method: %s\n", method);
		
		if(strncasecmp(method, "GET", i)==0)
        { //只处理get请求
	        if(debug) printf("method = GET\n");
		
		    //获取url
		    while(isspace(buf[j++]));//跳过白空格
		    i = 0;
		
		    while(!isspace(buf[j]) && (i<sizeof(url)-1))//碰到空格则跳出循环,即读到了完整的URL
            {
			    url[i] = buf[j];//用来存放URL
			    i++;
			    j++;
		    }
		
		    url[i] = '\0';
		
		    if(debug) printf("-----url is: [%s]\n", url);
		
			//继续读取http 头部
		    do
            {
			    len = get_line(client_sock, buf, sizeof(buf));
			    if(debug) printf("read: %s\n", buf);
			
		    }while(len>0);
			
			//***定位服务器本地的html文件***
			
			//处理url 中的问号?,因为有的url是带着问号的,我们这里只取问号前面的作为URL
			{
				char *pos = strchr(url, '?');//查找问号第一次出现的位置
				if(pos)
                {
					*pos = '\0';//将问号的地方换成\0,即字符串结束符,表示此URL到此结束
					printf("-----real url: [%s]\n", url);
				}
			}
			
			sprintf(path, "./html_docs/%s", url);//./html_docs是我们存放html文件的文件夹路径
			if(debug) printf("----path is: [%s]\n", path);
			
			//执行http 响应
			//判断文件是否存在,如果存在就响应200 OK,同时发送相应的html 文件,如果不存在,就响应 404 NOT FOUND.
			if(stat(path, &st)==-1)
            {//文件不存在或是出错
				fprintf(stderr, "stat %s failed. reason: %s\n", path, strerror(errno));
			    not_found(client_sock);
			}
            else 
            {//文件存在
			
			    if(S_ISDIR(st.st_mode)){
					strcat(path, "/index.html");
				}
				
				do_http_response(client_sock, path);
				
			}
				
	
	    }
        else 
        {//非get请求, 读取http 头部,并响应客户端 501 	Method Not Implemented
			fprintf(stderr, "warning! other request [%s]\n", method);
			do{
			    len = get_line(client_sock, buf, sizeof(buf));
			    if(debug) printf("read: %s\n", buf);
			
		    }while(len>0);
			
			//unimplemented(client_sock);   //在响应时再实现
			
		}
		
			
		
	}else {//请求格式有问题,出错处理
		//bad_request(client_sock);   //在响应时再实现
	}

	
}

三、响应http请求

接收并解析客户端的请求后,如果找到客户端所要求请的内容,则需要对其进行响应。

响应的格式如下所示:
主要包括响应头部(状态行+消息报头)、响应正文两部分。

在这里插入图片描述
组装响应头部的实现代码如下所示:

/****************************
 *返回关于响应文件信息的http 头部
 *输入: 
 *     client_sock - 客服端socket 句柄
 *     resource    - 文件的句柄 
 *返回值: 成功返回0 ,失败返回-1
******************************/
int  headers(int client_sock, FILE *resource)
{
	struct stat st;
	int fileid = 0;
	char tmp[64];
	char buf[1024]={0};
	strcpy(buf, "HTTP/1.0 200 OK\r\n");
	strcat(buf, "Server: anchenliang test Server\r\n");
	strcat(buf, "Content-Type: text/html\r\n");
	strcat(buf, "Connection: Close\r\n");
	
	fileid = fileno(resource);//获取该文件的file id
	
	if(fstat(fileid, &st)== -1)//同stat函数类似,查看st文件是否存在,返回-1则说明文件不存在的
    {
		inner_error(client_sock);//发送500的错误码,表示服务器内部出错
		return -1;
	}
	
	snprintf(tmp, 64, "Content-Length: %ld\r\n\r\n", st.st_size);//连续两个回车符和换行符表示这是消息头部的结束了,st.st_size表示的是st文件的长度
	strcat(buf, tmp);
	
	if(debug) fprintf(stdout, "header: %s\n", buf);
	
	if(send(client_sock, buf, strlen(buf), 0) < 0)//send函数表示向socket函数发送消息,返回值小于0表示发送失败了
    {
		fprintf(stderr, "send failed. data: %s, reason: %s\n", buf, strerror(errno));
		return -1;
	}
	
	return 0;
}

组装响应内容的函数如下所示:

/****************************
 *说明:实现将html文件的内容按行
        读取并送给客户端
 ****************************/
void cat(int client_sock, FILE *resource)
{
	char buf[1024];
	
    fgets(buf, sizeof(buf), resource);//将resource文件中的内容读到buf中,fgets函数每次读取一行
	
	while(!feof(resource))//feof用来判断resource文件是否读到尾部了,文件结束返回非0,文件未结束返回0
    {
		int len = write(client_sock, buf, strlen(buf));//将buf的内容写进socket,即发送这一行内容
		
		if(len<0){//发送body 的过程中出现问题,怎么办?1.重试? 2.
			fprintf(stderr, "send body error. reason: %s\n", strerror(errno));
			break;
		}
		
		if(debug) fprintf(stdout, "%s", buf);
		fgets(buf, sizeof(buf), resource);
		
	}
}

四、完整代码即效果

实现一个http服务器,功能为输入想要访问的html文件的URL,得到正确的响应html文件。下面附上完整代码以及实现过程;

4.1 完整代码:

 #include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>




#define SERVER_PORT 80

static int debug = 1;

int get_line(int sock, char *buf, int size);
void do_http_request(int client_sock);
void do_http_response(int client_sock, const char *path);
int  headers(int client_sock, FILE *resource);
void cat(int client_sock, FILE *resource);

void not_found(int client_sock);
void inner_error(int client_sock);
void do_http_response1(int client_sock);

int main(void){

    int sock;//代表信箱
    struct sockaddr_in server_addr;


    //1.美女创建信箱
    sock = socket(AF_INET, SOCK_STREAM, 0);

    //2.清空标签,写上地址和端口号
    bzero(&server_addr, sizeof(server_addr));

    server_addr.sin_family = AF_INET;//选择协议族IPV4
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//监听本地所有IP地址
    server_addr.sin_port = htons(SERVER_PORT);//绑定端口号

    //实现标签贴到收信得信箱上
    bind(sock, (struct sockaddr *)&server_addr,  sizeof(server_addr));

    //把信箱挂置到传达室,这样,就可以接收信件了
    listen(sock, 128);

    //万事俱备,只等来信
    printf("等待客户端的连接\n");


    int done =1;

    while(done){
        struct sockaddr_in client;
        int client_sock, len, i;
        char client_ip[64];
        char buf[256];

        socklen_t  client_addr_len;
        client_addr_len = sizeof(client);
        client_sock = accept(sock, (struct sockaddr *)&client, &client_addr_len);

        //打印客服端IP地址和端口号
        printf("client ip: %s\t port : %d\n",
                 inet_ntop(AF_INET, &client.sin_addr.s_addr,client_ip,sizeof(client_ip)),
                 ntohs(client.sin_port));
        /*处理http 请求,读取客户端发送的数据*/
        do_http_request(client_sock);
		close(client_sock);

    }
    close(sock);
    return 0;
}


void do_http_request(int client_sock)
{
	int len = 0;
	char buf[256];
	char method[64];//请求的方法,是get还是post
	char url[256];//请求的URL
	char path[256];
	
	struct stat  st;//用于stat函数执行后,保存文件状态
	
	
	/*读取客户端发送的http 请求*/
	
	
	//1.读取请求行
	len = get_line(client_sock, buf, sizeof(buf));//请求行只要一行,因此只需要读一次即可
    printf("--line 96,do_http_request --,result for get_line is [%s]\n",buf);
	
	if(len > 0)
    {   //读到了请求行,首先获取请求的方法
		int i=0, j=0;
		while(!isspace(buf[j]) && (i<sizeof(method)-1))//如果不是空格,且没超过method的容量最大值;遇到空格说明请求方法结束了
        {
			method[i] = buf[j];//用来存放请求方法,是get还是post
			i++;
			j++;
		}
		
		method[i] = '\0';
		if(debug) printf("request method: %s\n", method);
		
		if(strncasecmp(method, "GET", i)==0)
        { //只处理get请求
	        if(debug) printf("method = GET\n");
		
		    //获取url
		    while(isspace(buf[j++]));//跳过白空格
		    i = 0;
		
		    while(!isspace(buf[j]) && (i<sizeof(url)-1))//碰到空格则跳出循环,即读到了完整的URL
            {
			    url[i] = buf[j];//用来存放URL
			    i++;
			    j++;
		    }
		
		    url[i] = '\0';
		
		    if(debug) printf("-----url is: [%s]\n", url);
		
			//继续读取http 头部
		    do
            {
			    len = get_line(client_sock, buf, sizeof(buf));
			    if(debug) printf("read: %s\n", buf);
			
		    }while(len>0);
			
			//***定位服务器本地的html文件***
			
			//处理url 中的问号?,因为有的url是带着问号的,我们这里只取问号前面的作为URL
			{
				char *pos = strchr(url, '?');//查找问号第一次出现的位置
				if(pos)
                {
					*pos = '\0';//将问号的地方换成\0,即字符串结束符,表示此URL到此结束
					printf("-----real url: [%s]\n", url);
				}
			}
			
			sprintf(path, "./html_docs/%s", url);//./html_docs是我们存放html文件的文件夹路径
			if(debug) printf("----path is: [%s]\n", path);
            //do_http_response1(client_sock);
			
			//执行http 响应
			//判断文件是否存在,如果存在就响应200 OK,同时发送相应的html 文件,如果不存在,就响应 404 NOT FOUND.
			if(stat(path, &st)==-1)
            {//文件不存在或是出错
				fprintf(stderr, "stat %s failed. reason: %s\n", path, strerror(errno));
			    not_found(client_sock);
			}
            else 
            {   //文件存在
			
			    if(S_ISDIR(st.st_mode))//S_ISDIR用来判断st是否是一个目录
                {
					strcat(path, "/index.html");//在path的字符串后面加上/index.html
				}
				
				do_http_response(client_sock, path);
				
			}
				
	
	    }
        else 
        {//非get请求, 读取http 头部,并响应客户端 501 	Method Not Implemented
			fprintf(stderr, "warning! other request [%s]\n", method);
			do{
			    len = get_line(client_sock, buf, sizeof(buf));
			    if(debug) printf("read: %s\n", buf);
			
		    }while(len>0);
			
			//unimplemented(client_sock);   //在响应时再实现
			
		}
		
			
		
	}else {//请求格式有问题,出错处理
		//bad_request(client_sock);   //在响应时再实现
	}
	
	
	
	
	
}

/*响应http的函数*/
void do_http_response(int client_sock, const char *path)
{
	int ret = 0;
	FILE *resource = NULL;
	
	resource = fopen(path, "r");
	
	if(resource == NULL){
		not_found(client_sock);
		return ;
	}
	
	//1.发送http 头部
	ret = headers(client_sock, resource);
	
	//2.发送http body .
	if(!ret){
	    cat(client_sock, resource);
	}
	
	fclose(resource);
}

/****************************
 *返回关于响应文件信息的http 头部
 *输入: 
 *     client_sock - 客服端socket 句柄
 *     resource    - 文件的句柄 
 *返回值: 成功返回0 ,失败返回-1
******************************/
int  headers(int client_sock, FILE *resource)
{
	struct stat st;
	int fileid = 0;
	char tmp[64];
	char buf[1024]={0};
	strcpy(buf, "HTTP/1.0 200 OK\r\n");
	strcat(buf, "Server: anchenliang test Server\r\n");
	strcat(buf, "Content-Type: text/html\r\n");
	strcat(buf, "Connection: Close\r\n");
	
	fileid = fileno(resource);//获取该文件的file id
	
	if(fstat(fileid, &st)== -1)//同stat函数类似,查看st文件是否存在,返回-1则说明文件不存在的
    {
		inner_error(client_sock);//发送500的错误码,表示服务器内部出错
		return -1;
	}
	
	snprintf(tmp, 64, "Content-Length: %ld\r\n\r\n", st.st_size);//连续两个回车符和换行符表示这是消息头部的结束了,st.st_size表示的是st文件的长度
	strcat(buf, tmp);
	
	if(debug) fprintf(stdout, "header: %s\n", buf);
	
	if(send(client_sock, buf, strlen(buf), 0) < 0)//send函数表示向socket函数发送消息,返回值小于0表示发送失败了
    {
		fprintf(stderr, "send failed. data: %s, reason: %s\n", buf, strerror(errno));
		return -1;
	}
	
	return 0;
}

/****************************
 *说明:实现将html文件的内容按行
        读取并送给客户端
 ****************************/
void cat(int client_sock, FILE *resource)
{
	char buf[1024];
	
    fgets(buf, sizeof(buf), resource);//将resource文件中的内容读到buf中,fgets函数每次读取一行
	
	while(!feof(resource))//feof用来判断resource文件是否读到尾部了,文件结束返回非0,文件未结束返回0
    {
		int len = write(client_sock, buf, strlen(buf));//将buf的内容写进socket,即发送这一行内容
		
		if(len<0){//发送body 的过程中出现问题,怎么办?1.重试? 2.
			fprintf(stderr, "send body error. reason: %s\n", strerror(errno));
			break;
		}
		
		if(debug) fprintf(stdout, "%s", buf);
		fgets(buf, sizeof(buf), resource);
		
	}
}


void do_http_response1(int client_sock)
{
    const char *main_header = "HTTP/1.0 200 OK\r\nServer: http Server\r\nContent-Type: text/html\r\nConnection: Close\r\n";


    const char * welcome_content = "\
<html lang=\"zh-CN\">\n\
<head>\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\n\
<title>This is a test</title>\n\
</head>\n\
<body>\n\
<div align=center height=\"500px\" >\n\
<br/><br/><br/>\n\
<h2>大家好,欢迎学习网络编程</h2><br/><br/>\n\
<form action=\"commit\" method=\"post\">\n\
尊姓大名: <input type=\"text\" name=\"name\" />\n\
<br/>芳龄几何: <input type=\"password\" name=\"age\" />\n\
<br/><br/><br/><input type=\"submit\" value=\"提交\" />\n\
<input type=\"reset\" value=\"重置\" />\n\
</form>\n\
</div>\n\
</body>\n\
</html>";

	//1. 发送main_header
	int len = write(client_sock, main_header, strlen(main_header));
	
	if(debug) fprintf(stdout, "... do_http_response...\n");
    if(debug) fprintf(stdout, "write[%d]: %s", len, main_header);
	
	//2. 生成Content-Length 
	char send_buf[64];
	int wc_len = strlen(welcome_content);
	len = snprintf(send_buf, 64, "Content-Length: %d\r\n\r\n", wc_len);//两个回车换行符,表示结束
	len = write(client_sock, send_buf, len);//发送Content-Length
	
	if(debug) fprintf(stdout, "write[%d]: %s", len, send_buf);
	
	len = write(client_sock, welcome_content, wc_len);
	if(debug) fprintf(stdout, "write[%d]: %s", len, welcome_content);


}



//读取请求行:返回值: -1 表示读取出错, 等于0表示读到一个空行, 大于0 表示成功读取一行
int get_line(int sock, char *buf, int size)
{
	int count = 0;//计数器,用来对当前已经读取的字符数进行计数
	char ch = '\0';//字符串结束符
	int len = 0;
	
	//当前读取的字符数小于传入的字符数size-1,且ch不是换行符时
	while( (count<size - 1) && ch!='\n'){
		len = read(sock, &ch, 1);//将sock里的数据读到ch中,每次读1个字符;当返回值len为1时,说明返回的是1,即读取成功
		
		if(len == 1)
        {
			if(ch == '\r')//如果读到的是回车符,则continue,进行下一次循环
            {
				continue;
			}else if(ch == '\n')//如果读到的是换行符,说明这一行读完了,break跳出循环
            {
				//buf[count] = '\0';
				break;
			}
			
			//这里处理一般的字符
			buf[count] = ch;
			count++;
			
		}
        else if( len == -1 )
        {//读取出错
			perror("read failed");
			count = -1;
			break;
		}
        else 
        {// read 返回0,客户端关闭sock 连接.
			fprintf(stderr, "client close.\n");
			count = -1;
			break;
		}
	}
	
	if(count >= 0) 
        buf[count] = '\0';//为读取的这一行添加字符串结束符
	
	return count;
}


void not_found(int client_sock)
{
	const char * reply = "HTTP/1.0 404 NOT FOUND\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML lang=\"zh-CN\">\r\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\r\n\
<HEAD>\r\n\
<TITLE>NOT FOUND</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
	<P>文件不存在!\r\n\
    <P>The server could not fulfill your request because the resource specified is unavailable or nonexistent.\r\n\
</BODY>\r\n\
</HTML>";

	int len = write(client_sock, reply, strlen(reply));
	if(debug) fprintf(stdout, reply);
	
	if(len <=0){
		fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
	}
	
	
}


void inner_error(int client_sock)
{
	const char * reply = "HTTP/1.0 500 Internal Sever Error\r\n\
Content-Type: text/html\r\n\
\r\n\
<HTML lang=\"zh-CN\">\r\n\
<meta content=\"text/html; charset=utf-8\" http-equiv=\"Content-Type\">\r\n\
<HEAD>\r\n\
<TITLE>Inner Error</TITLE>\r\n\
</HEAD>\r\n\
<BODY>\r\n\
    <P>服务器内部出错.\r\n\
</BODY>\r\n\
</HTML>";

	int len = write(client_sock, reply, strlen(reply));
	if(debug) fprintf(stdout, reply);
	
	if(len <=0){
		fprintf(stderr, "send reply failed. reason: %s\n", strerror(errno));
	}
}

4.2 实现过程

第一步:将以上代码复制进minihttp.c文件中,编译:gcc minihttp.c -o minihttp
第二步:新建一个html_dock文件夹,里面放置一个test.html文件。文件内容如下所示:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>test</title>
	<style>
		body{
			text-align:center;
			padding:50px;
		}
	</style>
</head>
<body>
	<p>恭喜,http服务器示例测试成功</p>
</body>
</html>	


第三步:运行minihttp文件,./minihttp
第四步:在浏览器访问你的ip(或者域名)+test.html,示例:http://anchenliang.com/test.html,结果如下:
在这里插入图片描述

附(stat函数)

作用:
返回文件的状态信息

头文件

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

函数原型

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

path:
文件的路径
buf:
传入的保存文件状态的指针,用于保存文件的状态
返回值
成功返回0,失败返回-1,设置errno

struct stat {
               dev_t     st_dev;     /* ID of device containing file */
               ino_t     st_ino;     /* inode number */
               mode_t    st_mode;    /* S_ISREG(st_mode)  是一个普通文件  S_ISDIR(st_mode)  是一个目录*/
               
               nlink_t   st_nlink;   /* number of hard links */
               uid_t     st_uid;     /* user ID of owner */
               gid_t     st_gid;     /* group ID of owner */
               dev_t     st_rdev;    /* device ID (if special file) */
               off_t     st_size;    /* total size, in bytes */
               blksize_t st_blksize; /* blocksize for filesystem I/O */
               blkcnt_t  st_blocks;  /* number of 512B blocks allocated */
               time_t    st_atime;   /* time of last access */
               time_t    st_mtime;   /* time of last modification */
               time_t    st_ctime;   /* time of last status change */
           };
  • 7
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乘凉~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值