一、引言
C 的webserver项目是自己在学完网络编程后根据网课的内容做的一个初级的网络编程项目。
这个项目的效果是可以在浏览器通过输入网络IP地址和端口,然后打开对应的文件目录
效果如下:
也可以打开文件夹后点击目录,打开到对应的文件夹中去。
这个就是简单的webserver功能,后期自己也可以修改代码实现更多可能性的玩法,比如做一个简单的前端交互式的界面。
二、代码开发流程
我这个项目主要用到的实现方式,是用epoll,epoll是可以实现网络服务器编程有下面几个优点
1. 高效:epoll使用事件驱动模型,只有当IO事件发生时才会被激活,避免了轮询的开销,提高了服务器的效率。 2. 可扩展:epoll支持较大的并发连接数,可以处理成千上万个连接,而且在连接数量增加时,性能下降较慢。 3. 高可靠性:epoll使用边缘触发模式,只有在数据可读或可写时才会通知应用程序,避免了因为网络拥塞等原因导致的误报,提高了服务器的可靠性。 4. 灵活性:epoll支持多种事件类型,包括读、写、异常等,可以根据不同的需求进行定制。 5. 跨平台:epoll是Linux系统内核提供的机制,可以在不同的Linux系统上使用,实现跨平台开发。
下面是epoll开发webserver项目的流程图(不包括具体函数的实现)
代码语言:javascript复制int main()
{
//若web服务器给浏览器发送数据的时候, 浏览器已经关闭连接,
//则web服务器就会收到SIGPIPE信号
struct sigaction act;
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGPIPE, &act, NULL);
int lfd = tcp4bind(9999,NULL);
Listen(lfd,128);
int epfd = epoll_create(1024);
if(epfd < 0)
{
perror("epoll_create error");
close(lfd);
return -1;
}
struct epoll_event ev;
struct epoll_event events[1024];
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
int nready;
int i;
int cfd;
int sockfd;
while(1)
{
nready = epoll_wait(epfd,events,1024,-1);
if(nready < 0)
{
perror("epoll wait error");
if(nready == EINTR)
{
continue;
}
break;
}
for(i = 0;i < nready;i )
{
sockfd = events[i].data.fd;
if(sockfd == lfd)
{
cfd = Accept(lfd,NULL,NULL);
//设置cfd为非阻塞,防止其在 while((n = Readline(cfd,buf,sizeof(buf))) > 0)处阻塞
//设置cfd为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
ev.data.fd = cfd;
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
}
else {
http_request(sockfd,epfd);
}
}
}
}
上面的tcp4bind是封装好的函数,如果想看具体实现,可以看一下文章后面的全部代码wrap.c
代码。类似的Listen,Accept也是封装好的函数。
三、http_request 函数
这个函数是具体实现,打开文件和打开文件夹的函数。
当客户端发起HTTP请求时,服务器会调用http_request函数来处理请求。函数流程如下:
函数流程如下:
- 读取请求行数据,分析出要请求的资源文件名。
- 判断请求的文件是否存在,若不存在则发送404 NOT FOUND的头部信息和error.html文件内容。
- 若文件存在,判断文件类型,如果是普通文件则发送200 OK的头部信息和文件内容;如果是目录文件则发送200 OK的头部信息和目录文件列表信息的html内容。
- 发送完数据后关闭连接,并将文件描述符从epoll树上删除。
代码
代码语言:javascript复制int http_request(int cfd, int epfd)
{
int n;
char buf[1024];
//读取请求行数据, 分析出要请求的资源文件名
memset(buf, 0x00, sizeof(buf));
n = Readline(cfd, buf, sizeof(buf));
if(n<=0)
{
//printf("read error or client closed, n==[%d]n", n);
//关闭连接
close(cfd);
//将文件描述符从epoll树上删除
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
return -1;
}
printf("buf==[%s]n", buf);
//GET /hanzi.c HTTP/1.1
char reqType[16] = {0};
char fileName[255] = {0};
char protocal[16] = {0};
sscanf(buf, "%[^ ] %[^ ] %[^ rn]", reqType, fileName, protocal);
//printf("[%s]n", reqType);
printf("--[%s]--n", fileName);
//printf("[%s]n", protocal);
char *pFile = fileName;
if(strlen(fileName)<=1)
{
strcpy(pFile, "./");
}
else
{
pFile = fileName 1;
}
//转换汉字编码
strdecode(pFile, pFile);
printf("[%s]n", pFile);
//循环读取完剩余的数据,避免产生粘包
while((n=Readline(cfd, buf, sizeof(buf)))>0);
//判断文件是否存在
struct stat st;
if(stat(pFile, &st)<0)
{
printf("file not existn");
//发送头部信息
send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
//发送文件内容
send_file(cfd, "error.html");
}
else //若文件存在
{
//判断文件类型
//普通文件
if(S_ISREG(st.st_mode)) //man 2 stat查询,S_ISREG表示普通文件
{
printf("file existn");
//发送头部信息
send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
//发送文件内容
send_file(cfd, pFile);
}
//目录文件
else if(S_ISDIR(st.st_mode))
{
printf("目录文件n");
char buffer[1024];
//发送头部信息
send_header(cfd, "200", "OK", get_mime_type(".html"), 0);
//发送html文件头部
send_file(cfd, "html/dir_header.html");
//文件列表信息
struct dirent **namelist;
int num;
num = scandir(pFile, &namelist, NULL, alphasort);
if (num < 0)
{
perror("scandir");
close(cfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
return -1;
}
else
{
while (num--)
{
printf("%sn", namelist[num]->d_name);
memset(buffer, 0x00, sizeof(buffer));
if(namelist[num]->d_type==DT_DIR)
{
sprintf(buffer, "<li><a href=%s/>%s</a></li>", namelist[num]->d_name, namelist[num]->d_name);
}
else
{
sprintf(buffer, "<li><a href=%s>%s</a></li>", namelist[num]->d_name, namelist[num]->d_name);
}
free(namelist[num]);
Write(cfd, buffer, strlen(buffer));
}
free(namelist);
}
//发送html尾部
sleep(10);
send_file(cfd, "html/dir_tail.html");
}
}
return 0;
}
四、细节
1.cfd要设置为非阻塞
代码语言:javascript复制//设置cfd为非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
这段代码的作用是将文件描述符cfd设置为非阻塞模式。
首先,使用fcntl函数和F_GETFL命令获取cfd的文件状态标志。这些标志包括文件的读写模式、是否阻塞等信息。获取后的标志保存在flag变量中。
接着,使用按位或运算符(|)将O_NONBLOCK标志(表示非阻塞模式)添加到flag变量中。这样做是为了将O_NONBLOCK标志添加到文件描述符的状态标志中,表示将该文件描述符设置为非阻塞模式。
最后,使用fcntl函数和F_SETFL命令将修改后的flag标志设置回文件描述符cfd,以实现将cfd设置为非阻塞模式。
因此,这段代码的作用是将文件描述符cfd设置为非阻塞模式,以便在进行I/O操作时,如果没有数据可读或没有足够的空间可写,不会阻塞进程的执行,而是立即返回一个错误或一个特殊的状态,使得进程可以继续执行其他任务。
2.要改变环境工作目录
前提是把webpath设置在家目录下
代码语言:javascript复制char path[255] = {0};
sprintf(path, "%s/%s", getenv("HOME"), "webpath");
chdir(path);
这段代码的作用是构造一个路径并将当前工作目录切换到该路径。
让我们逐步解释这段代码:
char path[255] = {0};
- 定义一个长度为255的字符数组path,并初始化为0。这个数组将用来存储构造的路径。
sprintf(path, "%s/%s", getenv("HOME"), "webpath");
- 使用sprintf函数将路径构造为$HOME/webpath的形式。getenv("HOME")用于获取当前用户的主目录路径,然后将其与"webpath"拼接起来,得到完整的路径。
chdir(path);
- 使用chdir函数将当前工作目录切换到构造的路径。这样,程序的当前工作目录就会变成$HOME/webpath。
综合起来,这段代码的作用是构造一个路径,并将当前工作目录切换到该路径。通常情况下,这样的操作用于确保程序在正确的目录下执行,以便正确地访问和处理文件。
3.fileName 读取位置 1,略过“/“
不然就是下面这样
4.scandir函数
scandir 函数是用于扫描指定目录并返回目录中的文件列表的函数。它返回一个指向 dirent 结构的指针数组,每个结构包含一个目录中的一个条目的信息。
以下是 scandir 函数的原型:
代码语言:javascript复制int scandir(const char *dirp, struct dirent ***namelist,
int (*filter)(const struct dirent *),
int (*compar)(const struct dirent **, const struct dirent **));
dirp:要扫描的目录的路径名。
namelist:指向指针数组的指针,用于存储指向每个目录条目的指针。
filter:一个可选的过滤函数,用于决定哪些目录条目应该被返回。如果不需要过滤,可以将其设置为 NULL。
compar:一个可选的比较函数,用于对返回的目录条目进行排序。如果不需要排序,可以将其设置为 NULL。
以下是一个简单的示例,演示了如何使用 scandir 函数来列出目录中的文件:
代码语言:javascript复制#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
int main() {
struct dirent **namelist;
int n;
n = scandir(".", &namelist, NULL, alphasort);
if (n < 0) {
perror("scandir");
exit(EXIT_FAILURE);
} else {
for (int i = 0; i < n; i ) {
printf("%sn", namelist[i]->d_name);
free(namelist[i]);
}
free(namelist);
}
return 0;
}
在这个示例中,scandir 函数扫描当前目录,并使用 alphasort 函数对返回的文件列表进行排序。然后,它遍历列表并打印每个文件的名称。
5.添加默认路径
比如http://192.168.44.3:9999 可以访问默认的主目录下面的文件夹内容
代码语言:javascript复制char *pFile = fileName;
if(strlen(fileName)<=1) //添加默认为主目录下面
{
strcpy(pFile, "./");
}
else
{
pFile = fileName 1;
}
注意不能将char *pFile fileName = NULL 设置为这样,否则会产生段错误
6.解决遇到汉字的问题
在webserver代码中调用了一个函数
strdecode(pFile, pFile); 这个函数在pub.c中,然后写了一个"编码",用作回写浏览器的时候,将除字母数字及/_.-~以外的字符转义后回写。
代码语言:javascript复制//strencode(encoded_name, sizeof(encoded_name), name);
void strencode(char* to, size_t tosize, const char* from)
{
int tolen;
for (tolen = 0; *from != '