使用方法:
编译
例子:./httpserver 9999 ../ htmltest/
可执行文件 +端口 +要访问的目录下的
例子:http://192.168.88.130:9999/luffy.html
前提概要
http协议 :应用层协议,用于网络通信,封装要传输的数据,通过http协议组织的数据最终会是一个数据块多行数据,换行需要 \r\n
通信流程:
客户端:通过使用http传输数据发送给服务器通过http协议组织数据→得到一个字符串→发送给服务器接受数据→根据http协议解析→得到原始数据→处理服务器端:接受数据→通过http协议解析→得到原始数据→处理回复数据→通过http协议组织数据→得到一个字符串→发送给客户端
http协议分成两部分:
http请求:
客户端发送给服务器的一种数据格式
http响应:
服务器端回复客户端的一种格式
http请求
客户端给服务器发送的一种数据格式,可以分为四部分
1.请求行 指定提交数据的方式有两种提交的方式:**get**:简单 **post**:复杂2. 请求头 多个键值对客户端给服务器发送的身份的描述符3. 空行 4.请求的数据向服务器提交的数据
这是网页用GET 发过来的请求
第一行:请求行用的GET
第一部分:GET :提交的数据的方式
第二部分:中间的橙色的字符
/ :访问的服务器资源目录,/ →代表资源根目录? :后面的内容:客户端向服务器端提交的数据key=value
第三部分 :HTTP/1.1 →http协议的版本
第二-第八行: 请求头
若干个键值对,每一个键值对占一行,使用\r\n换行
第九行是:空行
用post请求
第一行:请求行
post:提交数据的方式
/:作为客户端访问了服务器的什么目录,资源的根目录
http 、1.1http协议的版本
第二行-12请求头
第13行:空行
第14 行:客户端向服务器提交的数据
GET与POST的区别
功能上:
get
作为客户端向服务器申请访问静态资源(网页,图片,文件)
post :
向服务器提交动态数据用户登录信息上传下载文件
从操作的数据量来说:
get:
比较少,使用get向服务器提交的数据在请求行的第二部分在请求第二部分的时候需要显示到浏览器的地址栏中浏览器的地址栏的缓存很小,谷歌默认7k左右,数据量小
post :
可以操作大数据文件上传(大文件)post 提交数据放到了请求协议的第四部分
安全性:
get :
提交的数据会显示到浏览器的地址栏中,容易泄露
post :
不会泄露,提交数据不再浏览器的地址栏中
http响应
服务器给客户端回复数据
http响应的组成部分→4个部分
状态行
响应头(包头)
n个键值对里面的信息是服务器发送给客户端
空行
响应的数据,根据客户端请求给客户端回复的数据
第一行 :状态行
HTTP 、1.1 http协议版本
200:状态码
ok :对应状态码的描述
第二-九行 :响应头
content-type :服务器给客户端的数据快的格式==http协议的第四块的数据格式
text、plain→纯文本charset =iso-8859-1→数据的字符编码iso-8859-1→不支持中文utf 支持中文
content-length :服务器给客户端的数据快的长度==http协议的第四块的数据块的长度,总字节数;不知道写-1;
http状态码:
3.web服务器实现
客户端:浏览器
通过浏览器访问服务器: -访问方式:
服务器的IP地址:端口 应用层协议使用:http,数据需要在浏览器端使用该协议进行包装响应消息的处理也是浏览器完成的 => 程序猿不需要管-客户端通过ur1访问服务器资源
-客户端访问的路径:http://192.168.1.100:8989/或者http://192.168.1.100:8989
**[访问服务器提供的资源目录的根目录](http://192.168.1.100:8989/或者http://192.168.1.100:8989访问服务器提供的资源目录的根目录)**并不是服务器的 / 目录
#### 服务器端:
提供服务器,让客户端访问
支持多客户端访问
-使用I0多路转接=>epo11
客户端发送给的请求消息是基于http的 -需要能够解析http请求 服务器回复客户端数据,使用http协议封装回复的数据=>http响应
服务器端需要提供一个资源目录,目录中的文件可以供客户端访问
客户端访问的文件没有在资源目录中,就不能访问了
假设服务器端提供的目录:/home/robin/luffy
代码展示
main()函数
/*************************************************************************> File Name: main.cpp> Author:Wux1aoyu> > Created Time: Fri 17 May 2024 05:02:16 AM PDT************************************************************************/#include"sever.h"
using namespace std;
// 原则上 main 函数只是逻辑函数调用,具体的内容不会写在这里面
//代码量少
int main(int argc,char *argv[]){//启动服务器->epollif(argc<3){cout<<"./a.out port path\n"<<endl;exit(0);}//argv[2]是path的路径 //将进程进入到当前的目录相当于cdchdir(argv[2]);//启动服务器 -》基于epoll ET 非阻塞unsigned short port=atoi(argv[1]);// ./后面的参数epollrun(port);
}
头文件
#ifndef SERVER_H
#define SERVER_H#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <pthread.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <strings.h>
#include<dirent.h>using namespace std;#ifdef __cplusplus
extern "C" {
#endif// 初始化监听的文件描述符
int initlistenFd(unsigned short port);// 启动 epoll 模型
int epollrun(unsigned short port);// 建立新连接
int acceptConn(int lfd, int epfd);// 接收 HTTP 请求
int recvHttprequest(int cfd, int epfd);// 解析请求行
int parserequestline(const char *requline, int cfd);// 发送头信息
int sendHeadmsg(int cfd, int status, const char *descr, const char *type, int length);//发送目录
int senddir(int cfd,char*dirname);// 发送文件
int sendFile(int cfd, const char *file);// 断开连接
int disconnect(int cfd, int epfd);#ifdef __cplusplus
}
#endif#endif // SERVER_H
服务器端: sever.cpp
#include"sever.h"
//初始化监听套接字
int initlistenFd(unsigned short port)
{//1.创建监听的套接字int lfd=socket(AF_INET,SOCK_STREAM,0);if(lfd==-1){perror("socket");return -1;}//2. 端口复用//如果服务器主动断开链接,那么将会进入TIME_WAIT 状态,等待2msl,这个时间太长了,所以就设置端口复用,继续使用端口复用,使客户端用这个端口链接,但是上一个仍处于TIME_WAIT int opt=1;int ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));if(ret==-1){perror("ret");}//3.绑定//设置文件描述符的地址ip端口struct sockaddr_in addr;addr.sin_family=AF_INET;//IPV4addr.sin_port=htons(port);addr.sin_addr.s_addr=INADDR_ANY; //0地址ret=bind(lfd,(sockaddr*)&addr,sizeof(addr));if(ret==-1){perror("bind");return -1;}//4.设置监听ret=listen(lfd,128);if(ret==-1){perror("listen");return -1;}//5.返回可用的监听的套接字return lfd;}//启动epoll模型
int epollrun(unsigned short port){//初始化epoll模型int epfd=epoll_create(1000);//创建epoll树if(epfd==-1){perror("create");return -1;}//初始化epoll树,将监听lfd添加上树int lfd=initlistenFd(port);struct epoll_event ev;//事件结构体ev.events=EPOLLIN;//检查读事件ev.data.fd=lfd;//将lfd添加属性中//添加上树int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);if(ret==-1){perror("epoll_ctl-add");return -1;}//检测,循环检测,边沿ET模式,epoll非阻塞struct epoll_event evs[1024];int size=sizeof(evs)/sizeof(int);int flag =0;while (1){if(flag==1){break;}int num=epoll_wait(epfd,evs,size,0);//非阻塞进行//遍历发生可读事件的变化的数组for (int i = 0; i < num; i++){int curfd=evs[i].data.fd;//临时变量找到变化的文件描述符if(curfd==lfd)//如果使监听套接字发生变化,一定是客户端请求链接{//建立链接int ret= acceptConn(curfd,epfd);if(ret==-1){//建立链接失败直接终止程序flag=1;break;}}else{//通信//接受http请求recvHttprequest(curfd,epfd);}}}return 0;
}//和客户端建立新连接,并且将通信文件描述符设置成非阻塞属性
int acceptConn(int lfd,int epfd){//建立链接int cfd=accept(lfd,NULL,NULL);if(cfd==-1){perror("accept");return -1;}//设置通信文案描述属性为非阻塞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(epfd,EPOLL_CTL_ADD,cfd,&ev);if(ret==-1){perror("epoll_ctl");return -1;}}//和客户端断开新链接
int disconnect(int cfd,int epfd){//将节点从epoll模型删除int ret =epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);//删除操作最后一个制空if(ret==-1){perror("epoll_ctl_del");return -1;}//关闭通信套接字close(cfd);return 0;
}
//接受客户端http的请求消息
int recvHttprequest(int cfd,int epfd){//因为是边沿非阻塞模型,所以要一次性循环读char tmp[1024];//每次读1k数据char buf[4096];//每次把读的数据存到这个缓冲区里面//循环读数据int len,total=0;//total 是当前的buf的数据//客户端申请的都是静态资源,请求的资源内容,在请求行的第二部分//只需将请求完整的保存下来就可以//不需要解析请求头的数据,因此接受到之后不储存也是没问题的while((len=recv(cfd,tmp,sizeof(tmp),0))>0){if(len+total<sizeof(buf))//说明接受的和当前的还没超过缓冲区的大小{//有空间储存数据memcpy(buf+total,tmp,len);//从当前的数据往后加}total+=len;//当前的缓冲区的容量;buf[total] = '\0';}//循环结束了,说明读完了//非阻塞,缓存没有数据,返回-1,返回错误号if(len==-1&&errno==EAGAIN){//将请求行从接收的数据中拿出来 (http协议中他分了很多行,我们要拿第一行)//找到 \r\n就可以找到第一行char*pt= strstr(buf,"\r\n");//找到了\r\n之前的请求行int reqlen=pt-buf;//\r\n 的位置-首地址的位置//保留请求行buf[reqlen]='\0';//截断了//此时buf里面存在的是http的请求行的内容//解析请求行parserequestline(buf,cfd);}else if(len==0){cout<<"客户端断开连接了....."<<endl;//服务器和客户端也断开,cfd,从epoll删除文件描述符disconnect(cfd,epfd);}else{perror("recv");return -1;}return 0;}//解析请求行
int parserequestline(const char *requline,int cfd){//请求行分为三部分//GET /HELLO/WORLD/HTTP/1.1//1.拆分请求行,有用的是前两部分//提交数据的方式//客户端向服务器请求的文件名//拆分用正则表达式 sscanfchar method[5]; //POST GET char path[1024]; //存储的是目录文件地址sscanf(requline,"%[^ ] %[^ ]",method,path);//2. 判断请求的方式是不是get' ,不是get 直接忽略if(strcasecmp(method,"get")!=0){cout<<"用户提交不是get请求"<<endl;return -1;}//3. 判断用户访问的是文件还是目录// /HELLO/WORLD/ ,判断是不是 用statchar *file=NULL;if(strcmp(path,"/")==0){ //就是比较是不是/file="./";}else{file=path+1; //"./" +1 就是从h开始的// hello/a.txt == ./hello/a.txt 这个目录等价 加.比较麻烦,如果什么都不加,就是从根目录找了}//属性判断 是不是文件或者目录struct stat st;//传出参数int ret=stat(file,&st);if(ret==-1){//判断失败//无文件发送404给客户端sendHeadmsg(cfd,404,"not found","text/html",-1);sendFile(cfd,"404.html");}if(S_ISDIR(st.st_mode)){//如果是目录的话将目录内容发送给客户端}else{//如果是普通文件,发送文件,把头信息发出去sendHeadmsg(cfd,200,"ok","text/html",st.st_size); //这里我们默认传输html文件sendFile(cfd,file);}return 0;
}//发送头信息
int sendHeadmsg(int cfd,int status,const char *descr,const char*type,int length){//状态行 +消息包头 +空行char buf[4096];//http/1.1 200 oksprintf(buf,"http/1.1 %d %s\r\n",status,descr);//消息包头 ->这里只需两个键值对//content-type /content-length https://tool.oschina.net/commons去这里查sprintf(buf + strlen(buf), "Content-Type: %s\r\n", type);sprintf(buf + strlen(buf), "Content-Length: %d\r\n\r\n", length);// 空行//拼接完成之后发送send(cfd,buf,strlen(buf),0);//非阻塞return 0;}int sendFile(int cfd,const char *file){//读文件,发送给客户端//在发送内容之前应该有状态+消息包头,+空行+文件内容//这四部分数据组织好之后再发送数据吗?//不是 为什么,因为传输层默认人是tcp的//面向连接的流式传输协议-》只有最后全部发送完就可以int fd=open(file,O_RDONLY);//只读while (1){char buf[1024];int len=read(fd,buf,sizeof(buf));if(len>0){//发送读出的数据send(cfd,buf,len,0);}else if(len==0){//文件读完了break;}else{perror("read");return -1;}}return 0;
}