实现一个简单的web服务器 myhttpd。能够给浏览器提供服务,供用户借助浏览器访问主机中的文件。
1 超文本标记语言 HTML
超文本标记语言时构成网页文档的主要语言。可以说明文字,图形,动画,声音,表格,链接等。在计算机中以 .html
, .htm
作为扩展名,可以被浏览器访问。
示例:
<!doctype html>
<html>
<head>
<title>404 not fount</title>
</head>
<body>
<p id="top"></p>
<a href="https://www.w3school.com.cn/" title="HTML学习网站" target="_blank"> HTML学习网站 </HTML></a>
<div align=center>
<h1>404 not fount</h1>
</div>
<hr size="3" />
<div align=center>
<font>ngix 12.14</font>
</div>
<ul type="circle">
<li> 选项1 </li>
<li> 选项2 </li>
<li> 选项3 </li>
<li> 选项4 </li>
<li> 选项5 </li>
</ul>
<ol type="I">
<li> 选项1 </li>
<li> 选项2 </li>
<li> 选项3 </li>
<li> 选项4 </li>
<li> 选项5 </li>
</ol>
<dl>
<dt>小标题</dt>
<dd>解释标题</dd>
</dl>
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<img src="13.png" alt="图片加载失败" title="壁纸" width="300" height="200" />
<a href="https://www.bilibili.com/" title="bilibili" target="_blank"> B站 </a>
<a href="https://www.bilibili.com/" title="bilibili" target="_blank">
<img src="13.png" alt="图片加载失败" width="300" height="200" />
</a>
<a href="https://www.w3school.com.cn/" title="HTML学习网站" target="_blank"> HTML学习网站 </HTML></a>
<a href="#top"> 回到顶部 </a>
</body>
</html>
2.HTTP 协议
HTTP:超文本传输协议。互联网最为广泛的一种网络应用层协议。它可以减少网络传输,使浏览器更加高效。
通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。
2.1 请求消息
浏览器----发给----服务器。主旨内容包含 4 部分:
- 请求行:说明请求类型,要访问的资源,以及使用的http版本
- 请求头:说明服务器要使用的附加信息
- 空行:必须!即使没有请求数据
- 请求数据:也叫主体,可以添加任意的其他数据。
2.2 响应消息
服务器----发给----浏览器,主旨包括 4 部分:
- 状态行:包括http协议版本号,状态码,状态信息
- 消息报头:说明客户端要使用一些附加信息
- 空行:必须!
- 响应正文:服务器返回给客户端的文本信息
2.3 HTTP请求方法
- GET: 请求指定的页面信息,并返回实体主体
- POST: 向指定资源提交数据进行处理请求。数据包含在请求体中。post请求可能会导致新的资源建立和/或已有资源的修改。
- HEAD: 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
- PUT: 从客户端向服务器传送数取代指定的文档内容
2.4 HTTP常用状态码
状态码有三位数字组成,第一个数字代表响应类别,共分五种类别:
- 1xx:指示信息–表示请求已连接,继续处理
- 2xx:成功–表示请求连接成功
- 3xx:重定向–要完成请求必须进行更进一步的操作
- 4xx:客户端错误–请求有语法错误或者请求无法实现
- 5xx:服务器端错误–服务器未能实现合法请求
3. 示例—web服务器
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <error.h>
#include <errno.h>
#include <dirent.h>
#include <ctype.h>
//获取一行 \r\n 结尾的数据
int get_line(int cfd, char *buf, int size) {
int i = 0;
char c = '\0';
int n;
while ((i < size - 1) && (c != '\n'))
{
n = recv(cfd, &c, 1, 0);
if(n > 0) {
if(c == '\r') {
n = recv(cfd, &c, 1, MSG_PEEK);
if((n > 0) && (c == '\n')) {
recv(cfd, &c, 1, 0);
}
else {
c = '\r';
}
}
buf[i] = c;
i++;
}
else {
c = '\n';
}
}
buf[i] = '\0';
if(n == -1) {
i = n;
}
return i;
}
//创建、初始化 socket
int init_listen_fd(int port, int epfd) {
int ret;
//创建socket
int fd = socket(AF_INET, SOCK_STREAM, 0);
//设置端口复用
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in serv;
memset(&serv, 0, sizeof(serv));
serv.sin_family = AF_INET;
serv.sin_port = htons(port);
serv.sin_addr.s_addr = htonl(INADDR_ANY);
ret = bind(fd, (struct sockaddr*)&serv, sizeof(serv));
if(ret < 0) {
perror("bind error");
exit(1);
}
ret = listen(fd, 128);
if(ret < 0) {
perror("listen error");
exit(1);
}
//设置文件描述符非阻塞 ET
// int flag = fcntl(fd, F_GETFL);
// flag |= O_NONBLOCK;
// fcntl(fd, F_SETFL, flag);
//创建epoll事件
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
if(ret < 0) {
perror("epoll_ctl error");
exit(1);
}
return fd;
}
//接受新连接
void do_accept(int fd, int epfd) {
int ret;
struct sockaddr_in cli;
socklen_t len = sizeof(cli);
int cfd = accept(fd, (struct sockaddr*)&cli, &len);
if(cfd < 0) {
perror("accept error");
exit(1);
}
printf("client[%d] connected.\n", cfd);
//设置文件描述符非阻塞 ET
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
struct epoll_event ev;
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(ret < 0) {
perror("epoll_ctl error");
exit(1);
}
return;
}
//断开连接
void disconnect(int cfd, int epfd) {
printf("服务器检测到客户端关闭...\n");
int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
if(ret < 0) {
perror("epoll_ctl error");
exit(1);
}
close(cfd);
}
//回发应答协议给浏览器
/*
void send_respond(int cfd, int no, char *disp, char *type, int len)
cfd: 客户端cfd,用于回发数据
no: 状态码
disp: 状态描述
type: 回发文件类型
len: 文件大小
*/
void send_respond(int cfd, int no, char *disp, const char *type, int len) {
char buf[1024] = {0};
//发送状态行
sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);
//发送消息报头、空行、响应正文
sprintf(buf + strlen(buf), "Content-Type: %s\r\n", type);
sprintf(buf + strlen(buf), "Content-Length: %d\r\n", len);
sprintf(buf + strlen(buf), "\r\n");
send(cfd, buf, strlen(buf), 0);
}
//发送404错误页面
void send_error(int cfd, int no, char *disp, char *text) {
//void send_error(int cfd, char *text) {
char buf[4096] = {0};
int n;
//发送状态行
sprintf(buf, "HTTP/1.1 %d %s\r\n", no, disp);
//发送消息报头、空行、响应正文
sprintf(buf + strlen(buf), "Content-Type: %s\r\n", "text/html");
sprintf(buf + strlen(buf), "Content-Length: %d\r\n", -1);
sprintf(buf + strlen(buf), "Connection: close\r\n");
sprintf(buf + strlen(buf), "\r\n");
send(cfd, buf, strlen(buf), 0);
memset(buf, 0, sizeof(buf));
int fd = open(text, O_RDONLY);
if(fd < 0) {
perror("open error");
exit(1);
}
while((n = read(fd, buf, sizeof(buf))) > 0) {
send(cfd, buf, strlen(buf), 0);
}
// sprintf(buf, "<html><head><title> %d %s <title></head></html>\n", no, disp);
// sprintf(buf + strlen(buf), "<body bgcolor=\"#cc99cc\"><h4 align=\"center\"> %d %s </h4>\n", no, disp);
// sprintf(buf + strlen(buf), "%s\n", text);
// sprintf(buf + strlen(buf), "<hr>\n</body>\n</html>\n");
// send(cfd, buf, strlen(buf), 0);
return;
}
//发送文件给浏览器
void send_file(int cfd, const char *filename) {
int n, ret;
char buf[4096] = {0};
int fd = open(filename, O_RDONLY);
if(fd < 0) {
//发送404错误页面
printf("open error on send_file\n");
//send_error(cfd, 404, "no found", "打开文件失败!");
send_error(cfd, 404, "no found", "/home/yang/network/web/404.html");
close(fd);
return;
}
while((n = read(fd, buf, sizeof(buf))) > 0) {
ret = send(cfd, buf, n, 0);
if(ret < 0) {
printf("errno = %d\n", errno);
if(errno == EAGAIN) {
printf("--------------------EAGAIN.\n");
continue;
}
else if(errno == EINTR) {
printf("---------------------EINTR.\n");
}
else {
perror("send error on send_file.");
exit(1);
}
}
}
if(n < 0) {
perror("read error on send_file");
exit(1);
}
close(fd);
}
//通过文件名判断是哪种文件类型
const char* get_file_type(const char *filename) {
char *dot = NULL;
dot = strrchr(filename, '.');
if(dot == NULL) 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 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 encode_str(char *to, int 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';
}
//解码
void decode_str(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';
}
//遍历目录
void send_dir(int cfd, const char *dirname) {
int i, ret;
//拼一个html页面
char buf[4096] = {0};
sprintf(buf, "<html>\n\t<head>\n\t\t<title>目录名:%s</title>\n\t</head>\n", dirname);
sprintf(buf + strlen(buf), "<body><h1>当前目录:%s</h1><table>\n", dirname);
char enstr[1024] = {0};
char path[1024] = {0};
struct dirent **ptr;
int num = scandir(dirname, &ptr, NULL, alphasort);
for(i = 0; i < num; i++) {
char *name = ptr[i]->d_name;
//拼接完整路径
sprintf(path, "%s/%s", dirname, name);
printf("path = %s -------------------------------------\n", path);
struct stat st;
stat(path, &st);
//编码
encode_str(enstr, sizeof(enstr), name);
if(S_ISDIR(st.st_mode)) { //如果是目录
sprintf(buf + strlen(buf), "<tr><td><a href=\"%s/\"> %s/ </a></td><td>%ld</td></tr>\n", enstr, name, (long)st.st_size);
}
else if(S_ISREG(st.st_mode)) { //如果是文件
sprintf(buf + strlen(buf), "<tr><td><a href=\"%s\"> %s </a></td><td>%ld</td></tr>\n", enstr, name, (long)st.st_size);
}
ret = send(cfd, buf, strlen(buf), 0);
if(ret < 0) {
printf("errno = %d\n", errno);
if(errno == EAGAIN) {
printf("--------------------EAGAIN.\n");
continue;
}
else if(errno == EINTR) {
printf("---------------------EINTR.\n");
}
else {
perror("send error on send_file.");
exit(1);
}
}
memset(buf, 0, sizeof(buf));
}
sprintf(buf + strlen(buf), "</table></body></html>\n");
send(cfd, buf, strlen(buf), 0);
printf("dir message send OK!!!\n");
}
//处理客户端http请求--文件操作,判断文件是否存在 回发
void http_request(int cfd, const char *buf) {
//拆分请求行
//将http请求的首行进行拆分 get /xxx http/1.1
//int sscanf(const char *str, const char *format, ...);
char method[16], path[256], protocol[16];
sscanf(buf, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
printf("method = [%s]\t path = [%s] \t protocol = [%s]\n", method, path, protocol);
//解码 将不能识别的中文乱码-> 中文
decode_str(path, path);
char *filename = path + 1; //因为path为 /xxx 所以文件名要地址加 1
if(strcmp(path, "/") == 0) { //如果未指定访问资源
filename = "./"; //filename的值为当前目录
}
struct stat sbuf;
//判断文件是否存在
//int stat(const char *pathname, struct stat *buf);
int ret = stat(filename, &sbuf);
if(ret < 0) {
//不存在 回发浏览器 404 错误页面
printf("-----It's not a file.-----\n");
//send_error(cfd, 404, "no found", "文件不存在!");
send_error(cfd, 404, "no found", "/home/yang/network/web/404.html");
return;
}
//判断是什么文件类型
//S_ISREG(m)
if(S_ISREG(sbuf.st_mode)) { //普通文件
printf("-----It's a file.-----\n");
//回发http协议应答
//void send_respond(int cfd, int no, char *disp, char *type, int len)
send_respond(cfd, 200, "OK", get_file_type(filename), sbuf.st_size);
//发送文件信息
send_file(cfd, filename);
}
else if(S_ISDIR(sbuf.st_mode)) { //目录
//回发http协议应答
//void send_respond(int cfd, int no, char *disp, char *type, int len)
send_respond(cfd, 200, "OK", get_file_type(".html"), -1);
//发送目录信息
send_dir(cfd, filename);
}
}
//接收数据
void do_read(int cfd, int epfd) {
//读取一行http请求,拆分,获取get 文件名 协议号
char buf[1024] = {0};
int n;
n = get_line(cfd, buf, 1024);
if(n < 0) {
perror("get_line error");
exit(1);
}
else if(n == 0) { //客户端关闭连接
disconnect(cfd, epfd);
}
else {
//将http请求的首行进行拆分 get /xxx http/1.1
//int sscanf(const char *str, const char *format, ...);
//char method[16], path[256], protocol[16];
//sscanf(buf, "%[^ ] %[^ ] %[^ ]", method, path, protocol);
//printf("method = [%s]\t path = [%s] \t protocol = [%s]\n", method, path, protocol);
printf("---------------请求头-------------\n");
printf("请求行数据: %s", buf);
//读完缓冲区还剩余的数据
while(1) {
char sbuf[1024];
n = get_line(cfd, sbuf, sizeof(sbuf));
if(n < 0) {
break;
}
else if(n == 1 && sbuf[0] == '\n') {
break;
}
//printf("%d-----%s", n, sbuf);
}
printf("----------------------------------\n");
}
//判断是否为get请求
//int strncasecmp(const char *s1, const char *s2, size_t n); 比较 s1 s2 前n个字符是否相等
if(strncasecmp(buf, "GET", 3) == 0) { //相同-是get请求
//char *filename = path + 1; //因为path为 /xxx 所以文件名要地址加 1
http_request(cfd, buf);
//关闭套接字,将cfd从树上摘下
disconnect(cfd, epfd);
}
}
//启动epoll
void epoll_run(int port) {
int i;
struct epoll_event evs[1024];
int epfd = epoll_create(1024);
if(epfd < 0) {
perror("epoll_create error\n");
exit(1);
}
int lfd = init_listen_fd(port, epfd);
//循环监听事件
while(1) {
int ret = epoll_wait(epfd, evs, 1024, 0);
if(ret < 0) {
perror("epoll_wait error");
exit(1);
}
for(i = 0; i < ret; i++) {
struct epoll_event *ev = &evs[i];
if(!(ev->events & EPOLLIN)) {
continue;
}
if(ev->data.fd == lfd) { //接受连接请求
do_accept(lfd, epfd);
}
else { //接收数据
do_read(ev->data.fd, epfd);
}
}
}
}
int main(int argc, char *argv[]) {
if(argc < 3) {
printf("./Server port path");
}
//获取输入的端口
int port = atoi(argv[1]);
//改变进程工作目录
int ret = chdir(argv[2]);
if(ret < 0) {
perror("chdir error\n");
exit(1);
}
//启动监听
epoll_run(port);
return 0;
}