wb服务器
- 想法:用浏览器访问某个目录里面的内容,比如图片,音乐,文本…
效果展示:
这是服务器要接收的http协议请求,下面那个是服务器要发送给客户端的http请求格式。
代码:
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<error.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<string.h>
#include <arpa/inet.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<errno.h>
#include<dirent.h>
using std::cout;
using std::endl;
void eopll_run(int port);
int init_listenfd(int port, int efd); //初始化连接
void do_accept(int lfd, int efd); //进行连接
void do_read(int cfd, int efd); //读数据
int get_line(int cfd, char *buf, int size); //读取http协议头的一行的部分,是以\r\n结尾的
void http_file(int cfd ,char *file); //处理http请求,检查文件是否存在
void send_http(int cfd, int flag, const char *dircp, const char *type, int len); //回发http协议头
void send_data(int cfd, char *file); //回发请求的数据
const char *get_file_type(char *filename); //获取文件类型
void send_error(int cfd, int flag, const char *dircp, const char *text); //发送错误html界面
void send_Catalogue(int cfd, char *file); //发送目录信息
int hex2dec(char c);
char dec2hex(short int c);
void urlencode(char *url); //编码
void urldecode(char* url); //解码
int main(int argc, char **argv)
{
if(argc < 3)
{
cout << "需要传递三个三个参数,例如:\n ./server port path" << endl;
}
int port = atoi(argv[1]); //将端口号转换成int型
//改变当前的工作目录,服务器所要共享文件的目录,
//例如:/home/web_server/ 这个目录里面的东西是你想要共享的
int ret = chdir(argv[2]);
if(ret != 0)
{
perror("chdir error");
exit(1);
}
//启动eopll监听
eopll_run(port);
return 0;
}
void eopll_run(int port)
{
struct epoll_event all_events[2048]; //用于epoll_wait传出
//创建监听树
int efd = epoll_create(2048);
if(efd == -1)
{
perror("epoll_creat error");
exit(1);
}
cout << "等待客户端的连接..." <<endl;
//创建lfd连接监听,并添加到监听树
int lfd = init_listenfd(port, efd );
while(1)
{
//监听节点对应的事件
int ret = epoll_wait(efd, all_events, 2048, -1);
if(ret == -1)
{
perror("epoll_wait error");
exit(1);
}
for(int i = 0; i < ret; i++)
{//只处理读事件
struct epoll_event *pev = &all_events[i];
//不是读事件就返回重新继续
if(pev->events != EPOLLIN)
continue;
if(pev->data.fd == lfd){ //说明有客户端要进行连接
do_accept(lfd, efd);
}
else{ //否则就说明是读事件,进行读数据
do_read(pev->data.fd, efd);
}
}
}
}
int init_listenfd(int port, int efd)
{
//创建监听套接字lfd
int lfd = socket(AF_INET,SOCK_STREAM, 0 );
if(lfd == -1)
{
perror("socket error");
exit(1);
}
//创建服务器的地址结构 IP和端口
struct sockaddr_in ser;
//初始化地址结构
bzero(&ser, sizeof(ser)); //将结构体置为0
ser.sin_family = AF_INET;
ser.sin_port = htons(port); //端口赋值
ser.sin_addr.s_addr = htonl(INADDR_ANY); //IP设为任一值
//端口复用
int opt = 1;
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//给lfd 绑定地址结构
int ret = bind(lfd, (struct sockaddr *)&ser, sizeof(ser));
if(ret == -1)
{
perror("bind error");
exit(1);
}
//设置监听上限
ret = listen(lfd , 1024);
if(ret == -1)
{
perror("listen error");
exit(1);
}
//借助结构体将lfd 添加到epoll树上
struct epoll_event ev;
ev.data.fd = lfd;
ev.events = EPOLLIN;
ret = epoll_ctl(efd, EPOLL_CTL_ADD, lfd, &ev);
if(ret == -1)
{
perror("epoll_ctl add lfd error");
exit(1);
}
return lfd;
}
void do_accept(int lfd, int efd)
{
//创建客户端的地址结构
struct sockaddr_in c_addr;
socklen_t socklen = sizeof(c_addr);
//进行服务器与客户端的连接
int cfd = accept(lfd, (struct sockaddr *)&c_addr, &socklen);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
//打印客户端的IP和端口
char client_ip[64];
cout << "新连接的客户端的IP:" << inet_ntop(AF_INET, &c_addr.sin_addr.s_addr, client_ip, sizeof(client_ip))
<< ", 端口:" << ntohs(c_addr.sin_port) << endl;
//设置cfd 非阻塞
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
//将新节点挂到 epoll 监听树上
struct epoll_event ev;
ev.data.fd = cfd;
//边沿非阻塞模式
ev.events = EPOLLIN | EPOLLET;
int ret = epoll_ctl(efd, EPOLL_CTL_ADD, cfd, &ev);
if(ret == -1)
{
perror("epoll_ctl add cfd error");
exit(1);
}
}
void do_read(int cfd, int efd)
{
// 读取http协议, 获取GET 文件名 协议号
char line[4096] = {0};
int len = get_line(cfd, line, sizeof(line)); //读取http协议最主要的开头行
if(len == 0)
{
cout << "服务器检测到客户端已经关闭..." << endl;
epoll_ctl(efd, EPOLL_CTL_DEL, cfd, 0); //从监听树上摘除
close(cfd);
}
else
{
char mothod[16], path[128], httpnum[16];
sscanf(line, "%[^ ] %[^ ] %[^ ]", mothod, path, httpnum); //用正则表达式将协议头拆分成三部分
urldecode(path);
cout << "请求:" << mothod << ", 路径名:" << path << ",协议版本号:"<< httpnum << endl;
//读取出来的路径名可能是中文,这样的话就会导致乱码,所以要将它进行解码操作
while(1) //接着循环读取协议其他内容
{
char buf[1024] = {0};
len = get_line(cfd, buf, sizeof(buf));
if(len == '\n'){
break;
}else if(len == -1) {
break;
}
}
if(strncasecmp(mothod, "GET", 3) == 0) //检查是否是GET请求
{
http_file(cfd, path);
int ret = epoll_ctl(efd, EPOLL_CTL_DEL, cfd, NULL); //完成一次通信,服务器就会断开连接
//从监听树上摘下,关闭文件描述符
if(ret == -1)
{
perror("epoll_ctl del cfd error");
exit(1);
}
close(cfd);
}
}
}
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); //MSG_PEEK 读取下一个字符,但不会把上一个字符替换掉,相当于只是查看
if((n > 0) && (c == '\n'))
{
recv(cfd, &c, 1, 0); //这里才是正式读取‘\n’
}
else
c = '\n';
}
buf[i] = c;
i++;
}
else
c = '\n';
}
buf[i] = '\0';
if(-1 == n)
i = n;
return i;
}
//处理http请求,检查文件是否存在
void http_file(int cfd,char *path)
{
//将路径中的 / 去掉,获取文件名
char *file = path + 1;
if(strcmp(path, "/") == 0){ //如果是目录,就将文件名赋值为当前目录
file = "./";
}
struct stat sbuf; //这是一个判断文件是否存在所用到的结构体
//开始判断
int ret = stat(file, &sbuf);
if(ret != 0) //不存在
{
send_error(cfd, 404, "Not Found", "The file does not exist");
return;
// 回发浏览器 404 html界面
}
if(S_ISDIR(sbuf.st_mode)) //是否是目录
{
//发送头文件信息,显示html网页,点击超链接进入这个目录
send_http(cfd, 200, "OK", get_file_type(".html"), -1);
//发送目录信息
send_Catalogue(cfd, file);
}
else if(S_ISREG(sbuf.st_mode)) //是否是普通文件
{
//回发http协议应答
//send_http(cfd, 200, "OK", " Content-Type: text/plain; charset=iso-8859-1", sbuf.st_size);
send_http(cfd, 200, "OK", get_file_type(file), sbuf.st_size);
send_data(cfd, file);
}
}
//回发http协议头
// cfd 客户端cfd flag 错误号 dircp 错误描述 type 回发文件类型 len 文件长度
void send_http(int cfd, int flag, const char *dircp,const char *type, int len)
{
//对http协议进行封装发送
char buf[1024] = {0};
sprintf(buf, "HTTP/1.1 %d %s\r\n", flag, dircp);
sprintf(buf + strlen(buf), "Content-Type: %s\r\n", type);
sprintf(buf + strlen(buf), "Content-Length:%d\r\n", len);
send(cfd, buf, strlen(buf), 0);
send(cfd, "\r\n", 2, 0);
}
void send_data(int cfd, char *file)
{
int n = 0;
char buf[4000000];
cout << file << endl;
//打开服务器本地文件
int fd = open(file, O_RDONLY);
if(fd == -1)
{
send_error(cfd, 404, "Not Found", "File opening failure"); //打开失败
return;
//404错误界面
}
cout << "打开了" << file<< endl;
while((n = read(fd, buf, sizeof(buf)))> 0) //读取打开文件的数据
{
int ret = send(cfd, buf, n, 0); //发送读取出来的数据给客户端
if(ret == -1)
{
if(errno == EAGAIN)
continue;
else if(errno == EINTR) //对错误类型进行判断,如果是这种类型就返回重新发送
continue;
else {
perror("send error");
exit(1);
}
}
if (ret < 4000000)
cout << "---send ret : " << ret << endl;
}
close(fd);
}
const char* get_file_type(char *filename)
{
char *sign;
//从左到右查找字符'.',返回该字符出现的位置,用指针接收,就可以找到文件的后缀名,也就是文件类型了
sign = strrchr(filename, '.');
if(sign == NULL)
return "text/plain; charset=utf-8"; //不知道的类型就全部设为文本类型
if(strcmp(sign, ".html") == 0 || strcmp(sign, ".htx") == 0 || strcmp(sign, ".htm") == 0)
return "text/html; charset=utf-8"; //HTML格式
if(strcmp(sign, ".jpg") == 0 || strcmp(sign, ".jpeg") == 0 )
return "image/jpeg"; //jpg图片格式
if(strcmp(sign, ".gif") == 0 )
return "image/gif"; //gif图片格式
if(strcmp(sign, ".png") == 0 )
return "image/png"; //png 图片格式
if(strcmp(sign, ".ico") == 0 )
return "image/x-icon"; //图标
if(strcmp(sign, ".mp3") == 0 )
return "audio/mp3"; //mp3音频格式
return "text/plain; charset=utf-8"; //如果都不是,就返回纯文本格式
//还有很多格式,大家可以自己去扩展一下
}
// 客户端描述符 错误码 错误描述 HTML文本描述
void send_error(int cfd, int flag,const char *dircp, const char *text)
{
send_http(cfd, flag, dircp, "text/html", -1); //调用上面的函数发送协议头
//html返回错误网页文本发送
char buf[4096] = {0};
sprintf(buf, "<html><head><title>%d %s</title></head>\n",flag, dircp);
sprintf(buf + strlen(buf),"<body><div align=center><h1>%d %s</h1></div>\n", flag, dircp);
sprintf(buf + strlen(buf), "<hr>\n<div align=center><font>%s</font></div>\n", text);
sprintf(buf+ strlen(buf), "</body></html>");
send(cfd, buf, strlen(buf), 0);
}
//发送目录信息
void send_Catalogue(int cfd, char *dirname)
{
char buf[4096];
//先写一个html界面
sprintf(buf, "<html><head><title>目录名:%s</title></head>", dirname);
sprintf(buf + strlen(buf), "<body><h1>当前目录: %s</h1><table>", dirname);
char str[1024] = {0};
char path[1024] = {0};
//目录项的二级指针,就是一个二维数组
struct dirent **ptr;
int num = scandir(dirname, &ptr, NULL, alphasort); //alphasort进行排列
//这个函数就是将当前目录的内容储存在ptr这个二维数组中,返回值是该目录下有多少个文件或者目录
//遍历得到每一个文件或者目录
for(int i = 0; i < num; i++)
{
char * name = ptr[i]->d_name; //文件名
//拼接完成的文件路径
sprintf(path, "%s/%s", dirname, name);
//再次判断是文件还是目录
struct stat st;
stat(path, &st);
strcpy(str, name);
//对得到的目录或者文件进行编码
//因为浏览器只识别URL编码
urlencode(name);
//如果是文件
if(S_ISREG(st.st_mode))
sprintf(buf + strlen(buf),"<tr><td><a href=\"%s\">%s</a></td></tr>", name, str);//html超链接
else if(S_ISDIR(st.st_mode)) //如果是目录
sprintf(buf + strlen(buf), "<tr><td><a href=\"%s/\">%s/</a></td></tr>", name, str);
int ret = send(cfd, buf, strlen(buf), 0);
if(ret == -1)
{
if(errno == EAGAIN)
continue;
else if(errno == EINTR) //对错误类型进行判断,如果是这种类型就返回重新发送
continue;
else {
perror("send error");
exit(1);
}
}
memset(buf, 0, sizeof(buf));
}
sprintf(buf + strlen(buf), "</table></body></html>");
send(cfd, buf, sizeof(buf),0);
cout << "send OK!"<<endl;
}
//======编码和解码操作===============
int hex2dec(char c)
{
if ('0' <= c && c <= '9')
{
return c - '0';
}
else if ('a' <= c && c <= 'f')
{
return c - 'a' + 10;
}
else if ('A' <= c && c <= 'F')
{
return c - 'A' + 10;
}
else
{
return -1;
}
}
char dec2hex(short int c)
{
if (0 <= c && c <= 9)
{
return c + '0';
}
else if (10 <= c && c <= 15)
{
return c + 'A' - 10;
}
else
{
return -1;
}
}
//编码一个url
void urlencode(char *url)
{
int i = 0;
int len = strlen(url);
int res_len = 0;
char res[2048];
for (i = 0; i < len; ++i)
{
char c = url[i];
if ( ('0' <= c && c <= '9') ||
('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
c == '/' || c == '.')
{
res[res_len++] = c;
}
else
{
int j = (short int)c;
if (j < 0)
j += 256;
int i1, i0;
i1 = j / 16;
i0 = j - i1 * 16;
res[res_len++] = '%';
res[res_len++] = dec2hex(i1);
res[res_len++] = dec2hex(i0);
}
}
res[res_len] = '\0';
strcpy(url, res);
}
// 解码url
void urldecode(char *url)
{
int i = 0;
int len = strlen(url);
int res_len = 0;
char res[2048];
for (i = 0; i < len; ++i)
{
char c = url[i];
if (c != '%')
{
res[res_len++] = c;
}
else
{
char c1 = url[++i];
char c0 = url[++i];
int num = 0;
num = hex2dec(c1) * 16 + hex2dec(c0);
res[res_len++] = num;
}
}
res[res_len] = '\0';
strcpy(url, res);
}