一个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.总结
简单写了整个前后端关联的流程,程序性能这些统统没有考虑。