使用方法:
編譯
例子:./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;
}