一个http服务器的实现

1. 个人看法

这段时间学习和了解了一些前端的知识,再加之之前学习的网络编程,遂写了一个最简单的http服务器,其实准确来说是为了实现一个网站而提供的后台服务。个人认为,后台都是最基本的原理加上复杂的业务场景,就变成了丰富的网络。今天实现的这个http后台服务,就是最简单的,浏览器访问我们的一个html文档(资源),我们就把这个文档推送给浏览器,浏览器的工作原理会使得它打开此html然后渲染,不过这不是我们所关心的.

2.工程结构

总共五个文件:
server.c:后台服务的实现。
common.h:使用linux套接字接口创建服务,会需要包含很多头文件,所以将它们统一放在头文件中,后续比如实现一些其他的服务器如ftp等,直接引用该头文件即可。
passge.h:由于c语言很多的写法,比如分配内存后要检查是否分配成功、函数使用指针作为参数传递,在使用前需要判断是否为空,这些都是很模块化的写法,直接定义封装了一些宏,使得代码简化。
filelogin.html:这个就是浏览器请求的资源,由几行简单的html组成,是我学习html时跟着书上写的,大家可以换成自己的代码。
其实这个代码可以扩展为一个博客系统的实现,而且是不依附于任何的框架,只不过需要根据需求来拓展。如在后台服务中,增加对更多的请求报文的解析和响应,前端也根据需要,写好页面,做好关联。
start.sh 编译脚本

3.代码如下

common.h

#ifndef __COMMON_H__
#define __COMMON_H__

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

#define HTTP_PROTO 80
#define LISTEN_BACKLOG 50
#define INDEXXML_PATH	"./filelogin.html"
#define BUFFER_WRITE 1024
#define SERV_ADDR ("192.168.227.128")//ip地址,要运行代码的需要替换为自己的linux环境的ip地址

#endif 

passage.h

#ifndef __PASSAGE__H__
#define __PASSAGE__H__

#define ERROR	0
#define OK	1
//对memset的封装1
#define MM_SET0(var,type)	\
		do{memset(var,0,sizeof(type));}while(0);
//对memset的封装2
#define MM_SET_VALUE(var,len,value)	\
		do{memset(var,value,len);}while(0);
		
//对指针做参数的判空逻辑的封装
#define NULL_JUDGE_INT(string)	\
		do{if (string == NULL){printf("parma is NULL!\r\n");return ERROR;}}while(0);

//对free()的封装
#define SAFE_FREE(var) \
		do{if (var != NULL){free(var);}}while(0);		

//对malloc()的封装
#define SAFE_MALLOC(var,len) \
		do{if (var != NULL){printf("the var isn't null!r\n");\
			return ERROR;}else{var = malloc(len);}}while(0);
			
#endif

server.c

#include "common.h"
#include "passage.h"

//根据子串切割主串(此处是为了由http协议的报文格式,切割出http报文)
int msplit(char * const destring,const char *substr,char ***ret,int *segnum)
{
	char *pos = NULL;
	char *start = NULL;
	char **retarray = NULL;
	int sublen = 0,tmp = 0;
	NULL_JUDGE_INT(destring);
	NULL_JUDGE_INT(substr);
	NULL_JUDGE_INT(ret);
	NULL_JUDGE_INT(segnum);
	
	sublen  = strlen(substr);
	start = destring;
	while (NULL != (pos = strstr(start,substr)))
	{
		tmp = strlen(start) - strlen(pos) + sublen;
		retarray = realloc(retarray,sizeof(char* )*((*segnum) + 1));//使用realloc是为了动态递增指针数组空间
		retarray[*segnum] = malloc(tmp);//给该指针数组retarray中的成员*segnum分配内存空间。
		snprintf(retarray[*segnum],tmp,"%s",start);
		
		(*segnum) += 1;//指针成员数+1
		start += tmp;
		pos = NULL;
		tmp = 0;
	}
	
	*ret = retarray;
	return OK;
}

//释放由msplit分配的指针数组空间及其每个指针成员指向的内存
int free_msplit(char **retarry,int segnum)
{
	int i = 0;
	NULL_JUDGE_INT(retarry);
	
	for (i = 0; i < segnum;i++)
	{
		SAFE_FREE(retarry[i]);
	}
	SAFE_FREE(retarry);
	return OK;
}

//根据请求的资源名filepath,构造响应报文
int responsehttp(int connfd,const char *filepath)
{
	FILE *fp = NULL;
	char *buffer = NULL;
	int filesize = 0;
	int left = 0,wrleft = 0;
	int rdlen = 0,wrlen = 0 ;
	struct stat fstat;
	
	//判断所请求的资源文件是否存在
	if (stat(filepath,&fstat) != 0)
	{
		printf("stat file: %s error!\r\n",filepath);
		fclose(fp);
		return errno;
	}
	
	if (!S_ISREG(fstat.st_mode))//先判断文件是否存在
	{
		printf("file:%s invalid!\r\n",filepath);
		fclose(fp);
		return errno;
	}
	
	filesize = fstat.st_size;//获取要读取的文件的大小
	SAFE_MALLOC(buffer,filesize+500);//由于我们一开始不知道文件有多大,所以我们先得到文件大小,再分配用于存放http报文的内存,后续记得要手动释放。+500是因为http报文的起始行和首部空间.
	
	//1.构造起始行+首部
	sprintf(buffer+strlen(buffer),"%s","HTTP/1.1 200 OK\r\n");
	sprintf(buffer+strlen(buffer),"%s","Content-type: text/html\r\n");
	sprintf(buffer+strlen(buffer),"%s%d%s","Content-lenth: ",filesize,"\r\n\r\n");//这里是两个\r\n,最后一个\r\n是http报文的body和头部之间的空行
	left = filesize;
	
	//打开文件,读取文件内容到http报文的body
	fp = fopen(filepath,"r");
	if (fp == NULL)
	{
		printf("open file:%s error!\r\n",filepath);
		return errno;
	}
	
	//2.构造响应报文的body部分。
	fread(buffer+strlen(buffer),filesize,1,fp);
	
	//发送报文
	write(connfd,buffer,strlen(buffer));
	
	SAFE_FREE(buffer);
	fclose(fp);
	return 0;
}

//解析浏览器的请求,并根据请求进行响应
int parsehttpAndresonpse(int connfd)
{
	char buffer[BUFFER_WRITE] = {0};
	int len = 0;
	int segnum = 0;
	char **retarray = NULL;
	char *ps,*pe; 
	
	//1.接收客户端(浏览器)的请求报文
	if (0 == (len = read(connfd,buffer+strlen(buffer),BUFFER_WRITE))) 
	{
		return 0;
	}
	
	//2.根据http报文的格式,切割请求报文
	msplit(buffer,"\r\n",&retarray,&segnum);//根据http报文的结尾特点("\r\n"),切割出每一行
	if (segnum <= 0)
	{
		return 0;
	}
	
	//3.解析报文请求的资源,例如请求报文的第一行:GET /test/index.html HTTP/1.1,则我们要解析出资源 /test/index.html
	ps = strstr(retarray[0],"GET /");
	pe = strstr(retarray[0]," HTTP/");
	
	if (NULL == ps && NULL == pe)//避免解析请求的资源错误
	{
		return 0;
	}
	
	MM_SET_VALUE(buffer,BUFFER_WRITE,0);
	snprintf(buffer,strlen(ps + strlen("GET /")) - strlen(pe) + 1,"%s",ps + strlen("GET /"));//得到请求的文件路径
	responsehttp(connfd,buffer);//响应浏览器
	free_msplit(retarray,segnum);//释放内存
	
	return 0;
}

int main()
{
	int fd = -1;
	int connectfd = -1;
	int clientaddrlen = 0;
	pid_t pid = 0;
	int status = 0;
	
	struct sockaddr_in servaddr,clientaddr;
	if (-1 == (fd = socket(AF_INET,SOCK_STREAM,0)))
	{
		printf("create socket error!\r\n");
		return 0;
	}
	
	servaddr.sin_family = AF_INET;
	//servaddr.sin_addr.s_addr = INADDR_ANY;
	inet_aton(SERV_ADDR,&servaddr.sin_addr);
	servaddr.sin_port = htons(HTTP_PROTO);
	
	//设置端口复用标志
	int opt = 1;
	if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,&opt,sizeof(int)) < 0)
	{
		printf("setsockopt error! errno[%d]\r\n",errno);
		return 0;
	}
	
	if (-1 == bind(fd,(struct sockaddr *)&servaddr,sizeof(struct sockaddr_in)))
	{
		printf("bind error! errno[%d]\r\n",errno);
		return 0;
	}
	
	listen(fd,LISTEN_BACKLOG);
	
	while (1)
	{
		MM_SET0(&clientaddr,struct sockaddr_in);
		connectfd = accept(fd,(struct sockaddr*)&clientaddr,(socklen_t *)&clientaddrlen);
		if (-1 == connectfd)
		{
			continue;
		}
		
		printf("connectfd[%d]!client addr: %s\r\n",connectfd,inet_ntoa(clientaddr.sin_addr));
		
		if (0 == (pid = fork()))
		{
			close(fd);
			parsehttpAndresonpse(connectfd);
			//close(connectfd);//close只是将此链接的引用数-1,但是若此链接引用数>1.则可能就达不到我们关闭链接的目的。
			
			shutdown(connectfd,SHUT_RDWR);//真正地关闭连接
			/*这个原理是由于,connect成功,建立了一个以文件系统为模型的插口。
			而fork,子进程继承父进程的资源,所以子进程和父进程各自引用此插口,此时插口文件的引用计数==2,
			所以若子进程使用close,那么父进程也要使用close再关闭一次,才能真正将对套接字的引用减小至0,从而删除,达到关闭链接的目的
			*/
			exit(0);
		}
		else
		{
			waitpid(pid,&status,WNOHANG);//给子进程收尸
		}
	}
	
	return 0;
}

资源文件 filelogin.html

<!DOCTYPE html>
<html>
	<head>
		<title>my csdn</title>
	</head>
	<body>
		<a title="Csdn site" href="https://me.csdn.net/dengwodaer" style="background: grey;color:white;padding:10px">Visit the Csdn site</a>
		</p>
		<form>
			<label>name:<input type="text" name="name" tabindex="1"/></label>
			<p>
			<label>City:<input type="text" name="City" tabindex="-1"/></label>
			</p>
			<label>Country:<input type="text" name="country" tadindex="2"/></label>
			</p>
			<input type="submit" tabindex="3">
			</p>
		</form>
	</body>
</html>

4.编译脚本start.sh

我写了个小脚本来编译,方便一些,直接执行sh start.sh后,执行./server即可运行程序。

#!/bin/bash
rm server
gcc ./server.c -g -o server
exit 0

5.执行效果

1.在浏览器访问http://192.168.227.128/filelogin.html

在这里插入图片描述

6.总结

简单写了整个前后端关联的流程,程序性能这些统统没有考虑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值