简单的用C语言版本实现的webserver
摘要
本项目是参考黑马Linux网络编程教学的C语言版本webserver服务器。本项目是BS模式,实现了浏览器发来对服务端上某个资源的请求 (http报文),服务端(即我们实现的webserver)返回给浏览器响应报文以及所请求的资源(普通文件或者目录文件)。
技术要点
- 多路IO复用技术 epoll
- http请求与应答协议
- TCP协议:三次握手四次挥手,连接建立完成后进行数据传输
- web服务器:解析浏览器发来的请求数据,得到请求文件名
- 目录访问功能
- 访问中文目录的问题
BS模式示意图(功能)
程序的执行流程图
具体实现
创建socket并绑定端口号
这里我们将创建监听文件描述符lfd和绑定端口号和ip的操作封装到一个函数中去
int tcp4bind(short port,const char *IP)
{
struct sockaddr_in serv_addr;
int lfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&serv_addr,sizeof(serv_addr));
if(IP == NULL){
serv_addr.sin_addr.s_addr = INADDR_ANY;
}else{
if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
perror(IP);
exit(1);
}
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
return lfd;
}
调用该函数,端口号设置为9999,IP为NULL,则使用INADDR_ANY ,服务器监听0.0.0.0创建socket,无论使用127.0.0.1或本机ip都可以建立tcp连接
int lfd = tcp4bind(9999, NULL);
设置监听
我们也将listen 封装为一个函数,内部带有错误判断
Listen(lfd, 128);
创建epoll树
int epfd = epoll_create(1024);
if(epfd < 0)
{
perror("epoll create error");
close(lfd);
return -1;
}
将监听文件描述符lfd上epoll树
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
在循环中调用epoll_wait 循环等待事件发生
有事件发生,内核会将数据写入epoll_event 数组中,并且根据对应描述符类型来进行相应的处理:
int i = 0;
int nready;
int cfd;
int sockfd;
struct epoll_event events[1024];
while(1)
{
nready = epoll_wait(epfd, events, 1024, -1);
if(nready < 0)
{
if (errno == EINTR)
{
continue;
}
break;
}
for(i = 0;i < nready; ++i)
{
sockfd = events[i].data.fd;
if(sockfd == lfd)
{
cfd = Accept(lfd, NULL, NULL);
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);
}
}
}
处理浏览器发来的请求http报文(httprequest函数)
前置知识:http报文
思路分析
处理浏览器发来的请求报文可分为如下几步:
- 读取请求行数据,解析出请求的资源文件名
- 循环读取完剩余数据
- 判断文件是否存在
- 不存在:返回http响应消息+错误页内容
- 存在:
- 普通文件
- 目录文件
读取请求数据并解析
int n;
char buf[1024];
memset(buf, 0x00, sizeof(buf));
n = Readline(cfd, buf, sizeof(buf));
if(n <= 0)
{
close(cfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
return -1;
}
printf("buf = [%s]\n", buf);
char reqType[16] = {0};
char fileName[255] = {0};
char protocol[16] = {0};
sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocol)
char *pFile = fileName;
if(strlen(fileName) <= 1)
{
strcpy(pFile, "./");
}
else
{
pFile = fileName + 1;
}
strdecode(pFile, pFile);
循环读完剩余数据
目的是为了防止粘包影响到下一个客户端发来的请求的数据解读,由于内部调用了read,会产生阻塞,故要将cfd设置为非阻塞
while((n = Readline(cfd, buf, sizeof(buf))) > 0);
发送响应报文状态行和头部
int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
char buf[1024] = {0};
sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg);
sprintf(buf + strlen(buf), "Content-Type:%s\r\n", fileType);
if(len > 0)
{
sprintf(buf + strlen(buf), "Content-Length:%d\r\n", len);
}
strcat(buf, "\r\n");
Write(cfd, buf, strlen(buf));
return 0;
}
发送响应报文消息正文
int send_file(int cfd, char *fileName)
{
int fd = open(fileName, O_RDONLY);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024];
int n;
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if(n <= 0)
{
break;
}
else
{
Write(cfd, buf, n);
}
}
}
判断文件是否存在
struct stat st;
stat(pFile, &st);
当文件不存在时处理
组织应答消息:http响应消息+错误页:
printf("file not exist\n");
send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
send_file(cfd, "error.html");
当文件存在且为普通文件
为普通文件的时候,下方get_mime_type() 通过文件名得到文件类型,结尾会给出实现:
if(S_ISREG(st.st_mode))
{
printf("file exist\n");
send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
send_file(cfd, pFile);
}
文件存在且为目录文件
如果是目录文件我们除了要返回状态行和消息报头,我们返回的响应主体应该是一个html页面显示目录下的文件(如下图),所以要进行拼接生成一个html文件,上面是html头,中间是目录的内容(需要程序实现,没法写死)然后加上html的尾部。
发送html上半部分dir_header.html
<html><head><title>Index of ./</title></head>
<body bgcolor="#99cc99"><h4>Index of ./</h4>
<ul type=circle>
发送html中我们程序实现的目录显示的部分,注意**如果是目录的话href值的结尾要加上/,**下面的代码中要重点留意,如果不加的话访问内层文件的时候会出错。
<li><a href=苦瓜.txt> 苦瓜.txt </a></li>
<li><a href=aa/> aa </a></li>
发送html的结尾部分dir_tail.html
</ul>
<address><a href="http://www.baidu.com/">xhttpd</a></address>
</body></html>
代码的实现如下:
else if(S_ISDIR(st.st_mode))
{
printf("目录文件\n");
send_header(cfd, "200", "OK", get_mime_type(".html"), 0);
send_file(cfd, "html/dir_header.html");
struct dirent **namelist;
int num;
char buffer[1024];
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("%s\n", namelist[num]->d_name);
memset(buffer, 0x00,sizeof(buf));
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);
}
send_file(cfd, "html/dir_tail.html");
}
杂项
-
更改进程当前工作目录,让程序在找文件的时候到指定的目录下搜寻: char path[255] = {0};
sprintf(path, "%s/%s", getenv("HOME"), "webpath");
chdir(path);
-
无法访问中文文件的问题解决 当我们点击一个名字为中文的文件的时所接受到的文件名是UTF8编码,如点击苦瓜.txt 时,我们接受到的请求报文中解析出的文件名是%隔开的utf8编码的字符串形式: 苦瓜的utf8编码(十六进制)为 :苦:e8 8b a6 ;瓜:e7 93 9c
注:中文的utf8编码占3个字节
解决这个问题的办法是:将每个中文的utf8编码(字符串格式存储的)转换成十进制存放到三个char变量(一个char变量占1B足够两位十六进制数)中: 代码实现如下: void strdecode(char *to, char *from)
{
for ( ; *from != '\0'; ++to, ++from) {
if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
*to = hexit(from[1])*16 + hexit(from[2]);
from += 2;
} else
*to = *from;
}
*to = '\0';
}
int hexit(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return 0;
}
-
SIGPIPE信号问题(接收端断开而发送端仍发送数据–》管道破裂)
问题描述:**当服务端正在给客户端发送数据时,数据未发送完时,浏览器(客户端)就断开连接,**此时相当于管道读端关闭,故服务端会收到SIGPIPE 信号
测试方法:发送目录文件时,**发送html尾部时sleep(10),主动关掉浏览器,**检查服务端程序发现程序已结束。
解决方法:由于这种情况即便浪费了资源,但我们不希望随便就使服务端程序停止,所以我们采用忽略信号的方法,在main函数开始注册信号处理函数:
int main()
{
.....
struct sigaction act;
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGPIPE, &act, NULL);
.....
}
注意事项
- 当浏览器返回上一级的时候,实际上关闭了原来客户端的cfd,然后建立新的连接得到cfd来发送请求数据。
附录
webserver.c
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include "pub.h"
#include "wrap.h"
int http_request(int cfd, int epfd);
int send_header(int cfd, char *code, char *msg, char *fileType, int len);
int send_file(int cfd, char *fileName);
int main()
{
struct sigaction act;
act.sa_handler = SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGPIPE, &act, NULL);
char path[255] = {0};
sprintf(path, "%s/%s", getenv("HOME"), "webpath");
chdir(path);
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;
ev.data.fd = lfd;
ev.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
int i = 0;
int nready;
int cfd;
int sockfd;
struct epoll_event events[1024];
while(1)
{
nready = epoll_wait(epfd, events, 1024, -1);
if(nready < 0)
{
if (errno == EINTR)
{
continue;
}
break;
}
for(i = 0;i < nready; ++i)
{
sockfd = events[i].data.fd;
if(sockfd == lfd)
{
cfd = Accept(lfd, NULL, NULL);
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);
}
}
}
}
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)
{
close(cfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
return -1;
}
printf("buf = [%s]\n", buf);
char reqType[16] = {0};
char fileName[255] = {0};
char protocol[16] = {0};
sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocol);
printf("[%s]\n", fileName);
char *pFile = fileName;
if(strlen(fileName) <= 1)
{
strcpy(pFile, "./");
}
else
{
pFile = fileName + 1;
}
strdecode(pFile, pFile);
while((n = Readline(cfd, buf, sizeof(buf))) > 0);
struct stat st;
if(stat(pFile, &st) < 0)
{
printf("file not exist\n");
send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
send_file(cfd, "error.html");
}
else
{
if(S_ISREG(st.st_mode))
{
printf("file exist\n");
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");
send_header(cfd, "200", "OK", get_mime_type(".html"), 0);
send_file(cfd, "html/dir_header.html");
struct dirent **namelist;
int num;
char buffer[1024];
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("%s\n", namelist[num]->d_name);
memset(buffer, 0x00,sizeof(buf));
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);
}
send_file(cfd, "html/dir_tail.html");
}
}
}
int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
char buf[1024] = {0};
sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg);
sprintf(buf + strlen(buf), "Content-Type:%s\r\n", fileType);
if(len > 0)
{
sprintf(buf + strlen(buf), "Content-Length:%d\r\n", len);
}
strcat(buf, "\r\n");
Write(cfd, buf, strlen(buf));
return 0;
}
int send_file(int cfd, char *fileName)
{
int fd = open(fileName, O_RDONLY);
if(fd < 0)
{
perror("open error");
return -1;
}
char buf[1024];
int n;
while(1)
{
memset(buf, 0x00, sizeof(buf));
n = read(fd, buf, sizeof(buf));
if(n <= 0)
{
break;
}
else
{
Write(cfd, buf, n);
}
}
}
pub.c
#include "pub.h"
char *get_mime_type(char *name)
{
char* dot;
dot = strrchr(name, '.');
if (dot == (char*)0)
return "text/plain; charset=utf-8";
if (strcmp(dot, ".html") == 0 || strcmp(dot, ".htm") == 0)
return "text/html; charset=utf-8";
if (strcmp(dot, ".jpg") == 0 || strcmp(dot, ".jpeg") == 0)
return "image/jpeg";
if (strcmp(dot, ".gif") == 0)
return "image/gif";
if (strcmp(dot, ".png") == 0)
return "image/png";
if (strcmp(dot, ".css") == 0)
return "text/css";
if (strcmp(dot, ".au") == 0)
return "audio/basic";
if (strcmp( dot, ".wav") == 0)
return "audio/wav";
if (strcmp(dot, ".avi") == 0)
return "video/x-msvideo";
if (strcmp(dot, ".mov") == 0 || strcmp(dot, ".qt") == 0)
return "video/quicktime";
if (strcmp(dot, ".mpeg") == 0 || strcmp(dot, ".mpe") == 0)
return "video/mpeg";
if (strcmp(dot, ".vrml") == 0 || strcmp(dot, ".wrl") == 0)
return "model/vrml";
if (strcmp(dot, ".midi") == 0 || strcmp(dot, ".mid") == 0)
return "audio/midi";
if (strcmp(dot, ".mp3") == 0)
return "audio/mpeg";
if (strcmp(dot, ".ogg") == 0)
return "application/ogg";
if (strcmp(dot, ".pac") == 0)
return "application/x-ns-proxy-autoconfig";
return "text/plain; charset=utf-8";
}
int get_line(int sock, char *buf, int size)
{
int i = 0;
char c = '\0';
int n;
while ((i < size - 1) && (c != '\n'))
{
n = recv(sock, &c, 1, 0);
if (n > 0)
{
if (c == '\r')
{
n = recv(sock, &c, 1, MSG_PEEK);
if ((n > 0) && (c == '\n'))
recv(sock, &c, 1, 0);
else
c = '\n';
}
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0';
return(i);
}
void strdecode(char *to, char *from)
{
for ( ; *from != '\0'; ++to, ++from) {
if (from[0] == '%' && isxdigit(from[1]) && isxdigit(from[2])) {
*to = hexit(from[1])*16 + hexit(from[2]);
from += 2;
} else
*to = *from;
}
*to = '\0';
}
int hexit(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return 0;
}
void strencode(char* to, size_t tosize, const char* from)
{
int tolen;
for (tolen = 0; *from != '\0' && tolen + 4 < tosize; ++from) {
if (isalnum(*from) || strchr("/_.-~", *from) != (char*)0) {
*to = *from;
++to;
++tolen;
} else {
sprintf(to, "%%%02x", (int) *from & 0xff);
to += 3;
tolen += 3;
}
}
*to = '\0';
}
pub.h
#ifndef _PUB_H
#define _PUB_H
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <ctype.h>
char *get_mime_type(char *name);
int get_line(int sock, char *buf, int size);
int hexit(char c);
void strencode(char* to, size_t tosize, const char* from);
void strdecode(char *to, char *from);
#endif
wrap.h
#ifndef __WRAP_H_
#define __WRAP_H_
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s);
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr);
int Bind(int fd, const struct sockaddr *sa, socklen_t salen);
int Connect(int fd, const struct sockaddr *sa, socklen_t salen);
int Listen(int fd, int backlog);
int Socket(int family, int type, int protocol);
ssize_t Read(int fd, void *ptr, size_t nbytes);
ssize_t Write(int fd, const void *ptr, size_t nbytes);
int Close(int fd);
ssize_t Readn(int fd, void *vptr, size_t n);
ssize_t Writen(int fd, const void *vptr, size_t n);
ssize_t my_read(int fd, char *ptr);
ssize_t Readline(int fd, void *vptr, size_t maxlen);
int tcp4bind(short port,const char *IP);
#endif
wrap.c
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
void perr_exit(const char *s)
{
perror(s);
exit(-1);
}
int Accept(int fd, struct sockaddr *sa, socklen_t *salenptr)
{
int n;
again:
if ((n = accept(fd, sa, salenptr)) < 0) {
if ((errno == ECONNABORTED) || (errno == EINTR))
goto again;
else
perr_exit("accept error");
}
return n;
}
int Bind(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = bind(fd, sa, salen)) < 0)
perr_exit("bind error");
return n;
}
int Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
int n;
if ((n = connect(fd, sa, salen)) < 0)
perr_exit("connect error");
return n;
}
int Listen(int fd, int backlog)
{
int n;
if ((n = listen(fd, backlog)) < 0)
perr_exit("listen error");
return n;
}
int Socket(int family, int type, int protocol)
{
int n;
if ((n = socket(family, type, protocol)) < 0)
perr_exit("socket error");
return n;
}
ssize_t Read(int fd, void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = read(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
ssize_t Write(int fd, const void *ptr, size_t nbytes)
{
ssize_t n;
again:
if ( (n = write(fd, ptr, nbytes)) == -1) {
if (errno == EINTR)
goto again;
else
return -1;
}
return n;
}
int Close(int fd)
{
int n;
if ((n = close(fd)) == -1)
perr_exit("close error");
return n;
}
ssize_t Readn(int fd, void *vptr, size_t n)
{
size_t nleft;
ssize_t nread;
char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ((nread = read(fd, ptr, nleft)) < 0) {
if (errno == EINTR)
nread = 0;
else
return -1;
} else if (nread == 0)
break;
nleft -= nread;
ptr += nread;
}
return n - nleft;
}
ssize_t Writen(int fd, const void *vptr, size_t n)
{
size_t nleft;
ssize_t nwritten;
const char *ptr;
ptr = vptr;
nleft = n;
while (nleft > 0) {
if ( (nwritten = write(fd, ptr, nleft)) <= 0) {
if (nwritten < 0 && errno == EINTR)
nwritten = 0;
else
return -1;
}
nleft -= nwritten;
ptr += nwritten;
}
return n;
}
static ssize_t my_read(int fd, char *ptr)
{
static int read_cnt;
static char *read_ptr;
static char read_buf[100];
if (read_cnt <= 0) {
again:
if ( (read_cnt = read(fd, read_buf, sizeof(read_buf))) < 0) {
if (errno == EINTR)
goto again;
return -1;
} else if (read_cnt == 0)
return 0;
read_ptr = read_buf;
}
read_cnt--;
*ptr = *read_ptr++;
return 1;
}
ssize_t Readline(int fd, void *vptr, size_t maxlen)
{
ssize_t n, rc;
char c, *ptr;
ptr = vptr;
for (n = 1; n < maxlen; n++) {
if ( (rc = my_read(fd, &c)) == 1) {
*ptr++ = c;
if (c == '\n')
break;
} else if (rc == 0) {
*ptr = 0;
return n - 1;
} else
return -1;
}
*ptr = 0;
return n;
}
int tcp4bind(short port,const char *IP)
{
struct sockaddr_in serv_addr;
int lfd = Socket(AF_INET,SOCK_STREAM,0);
bzero(&serv_addr,sizeof(serv_addr));
if(IP == NULL){
serv_addr.sin_addr.s_addr = INADDR_ANY;
}else{
if(inet_pton(AF_INET,IP,&serv_addr.sin_addr.s_addr) <= 0){
perror(IP);
exit(1);
}
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(port);
int opt = 1;
setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
Bind(lfd,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
return lfd;
}
|