通用的HTTP服务框架

未完成测试,注释多,供参考

 

//该文件包含了整个HTTP服务器的实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<fcntl.h>
#include<sys/wait.h>

typedef struct sockaddr sockaddr;
typedef struct socket_in socket_in;
//定义一个结构体来表示缓冲区

#define SIZE (1024*4)
typedef struct HttpRequest{
    char* first_line;
    char* method;
    char* url;
    char* url_path;
    char* query_string
    int content_length;
}HttpRequest;

//从socket中读取一行数据
//HTTP请求中换行符\n\r \n\r都可以兼容处理
//核心思路:将未知问题转换为已知问题.将\r\n 都转化为\n
int ReadLine(int sock, char output[],ssize_t max_size)
{
    //1 一个字符一个字符的从socket中读取数据
    char c = '\0';
    ssize_t i = 0;//记录了output缓冲区中当前已经写了多少个字符
    while (i < max_size)
    {
        ssize_t read_size = recv(sock, &c, 1, 0);
        if (read_size < 0)
        {
            //此处希望读到完整一行,如果还没读到换行就读到EOF就认为出错
            return -1;
        }

        //2 判断当前字符是不是\r
        if (c == '\r')
        {
            recv(sock, &c, 1, MSG_PEEK);//MSG_PEEK提前看缓冲区的内容,但是不读取
            //3 如果当前字符是\r,尝试读取下一个字符
            // a)如果下一个字符是\n
            if (c == '\n')
            {
                recv(sock, &c, 1, 0);
            }//读取字符将\r\n转化为\n
            // b)如果下一个字符不是\n
            else
            {
                c = '\n';//将\r转化为\n
            }
            //这两种情况都把\r转化成\n
        }
        //此时所有类型的分割符都被转化为\n
        //4 如果当前字符是\n,已将这行读完,结束函数
        if (c == '\n')
        {
            break;
        }
        //5 如果当前字符是一个普通字符,直接追加到输出结果中
        output[i++] = c;
    }
    output[i] = '\0';
    return i;
}

//解析首行,获取到其中的method和url:字符串切分ftok
//首行格式:GET /index.html?a=10&b=20 HTTP/1.1
int ParseFirstLine(char first_line[], char** p_method, char** p_url)
{
    char* tok[10] = { 0 };
    //使用Split函数对字符串进行切分,n表示切分结果有几个部分
    int n=Split(first_line, " ",tok);
    if (n != 3)
    {
        //如果不是3,不符合HTTP协议
        printf("Split failed!n=%d\n");
        return -1;
    }
    //此处可以进行更加复杂的校验
    *p_method = tok[0];
    *p_url = tok[1];
    return 0;
}

//进行字符串切分
//strtok内部使用static变量来保存字符串切分的情况,如果有大量的客户端去创建进程,就会
//使static变为共享资源,线程不安全
//使用strtok_r是线程安全版本,内部没有静态变量,需要用户在栈上手动定义变量作为缓冲区去存储.
int Split(char input[], const char*split_char, char* output[])
{
    char* tmp = NULL;//如果在tmp前面加static,那么线程就会共用tmp,就不会线程安全
    int output_index = 0;
    char* p = strtok_r(input,split_char,&tmp);
    while (p != NULL)
    {
        output[output_index++] = p;
        p = strtok_r(NULL, split_char,&tmp);
    }
    return output_index;
}

//url形如: /index.html?a=10&b=20
//            http:/.www.baidu.com/index.html?a=10&b=20(不考虑)
int  ParseUrl(char url[], char** p_url_path, char** p_query_string)
{
    *p_url_path = url;
    //查找问号所在位置
    char* p = url;
    for (; *p != '\0'; ++p)
    {
        if (*p == '?')
        {
            //找到了?,将之替换成\0
            *p = '\0';
            *p_query_string = p + 1;
            return 0;
        }
    }
    *p_query_string=NULL;//没有找到
    return -1;
}

int ParseHeader(int new_sock, int* content_length)
{
    char buf[SIZE] = { 0 };
    while (1)
    {
        ssize_t read_size = ReadLine(new_sock, buf, sizeof(buf)-1);
        if (raed_size <= 0)
        {
            return -1;
        }
        if (strcmp(buf, "\n") == 0)
        {
            //读到空行 
            return 0;
        }
        //Content_length:100\n
        const char* key = "Conten - Length: ";
        if (strncmp(buf, key, strlen(key)) == 0)
        {
            *content_length = atoi(buf + strlen(key));
            //break;使用break会出现粘包问题
        }
    }
    return 0;
}

void Handler404(int new_sock)
{
    const char* first_line = "HTTP/1.1 404 Not Found!\n";
    //此处可以不加header
    //Content-Type可以让浏览器自动识别
    //Content-Length可以通过关闭socket的 方式告知浏览器已经读完
    //body部分是 html的页面
    const char* body = "<head><meta http-equiv=\"Content - Type\" "
        "content=\"text / html; charset = utf - 8\">"
        "</head><h1>404!!!页面被吃了</h1>";
}

int IsDir(const char* file_path)
{
    struct stat st;
    int ret = stat(file_path, &st);
    if (ret < 0)
    {
        //此处不是目录
        return 0;
    }
    if (S_ISDIR(st.st_mode))
    {
        return 1;
    }
    return 0;
}

void HandlerFilePath(const char* url_path, char file_path[])
{
    //url_path是以/开头的,所以不需要wwwroot之后显示指明/
    sprintf(file_path, "./wwwroot%s", url_path);
    //如果url_path指向目录,就在目录后面拼装index.html作为默认访问的文件
    //识别url_path指向的是普通文件还是目录
    // a)url_path以/结尾,例如:/image/,一定是目录
    if (file_path[strlen(file_path) - 1] == '/')
    {
        strcat(file_path, "index.html");
    }
    else
    {
        // b)url_path没有以/ 结尾,此时需要根据文件属性来判定是否是目录:stat函数获取文件属性
        if (IsDir(file_path))
        {
            strcat(file_path, "./index.html");
        }
    }
}

ssize_t GetFileSize(const char* file_path)//int=2G,太小
{
    struct stat st;
    int ret = stat(file(file_path, &st));
    if (ret < 0)
    {
        return 0;
    }
    return st.st_size;
}

int WriteStaticFile(int new_sock, const char* file_path)
{
    //1.打开文件,如果打开失败,就返回404
    int fd = open(file_path,O_RDONLY)
    if (fd < 0)
    {
        perror("open");
        return 404;
    }
    //2.构造HTTP响应报文
    const char* first_line = "HTTP/1.1 200 OK\n";
    send(new_sock, first_line, strlen(first_line), 0);
    //此处如果更严谨,就需要加一些header
    //因为浏览器能够自动识别Content-Type,就没写
    //没写conten_length是因为后面立刻关闭了socket
    //浏览器能识别数据在哪里结束
    const char* blank_line = '\n';
    send(new_sock, first_line, strlen(blank_line), 0);
    //3.读文件并且写入socket中
    //更高效:sendfile把一个文件中的数据读出来,写到另一个中.可以从中间开始
    /*char c = '\0';
    while (read(new_sock, &c, 1) > 0)
    {
        send(new_sock, &c, 1, 0);
    }*/
    ssize_t file_size = GetFileSize(file_path);
    sendfile(new_sock, fd, NULL, file_size);
    //4.关闭文件
    colse(fd);
    return 200;
}

int HandlerStaticFile()
{
    //1.根据url_path获取到文件的真实目录
    //例如,hTTP服务器根目录叫 ./wwwroot
    //此时有一个文件叫做 ./wwwroot/image/101.jpg
    //在url中写一个path就叫做 /image/101.jpg
    char file_path[SIZE] = { 0 };
    //根据下面的函数将 /image/101.jpg转化为
    //磁盘上的 ./wwwroot/image/101.jpg
    HandlerFilePath(req->url_path, file_path);
    //2.打开文件,读取文件内容,把文件内容写到socket中
    int err_code = WriteStaticFile(new_sock, file_path);
    return err_code;
}

int HandlerCGIFather(int new_sock,int father_read,int father_write,const HttpRequest* req)
{
    // a)如果是POST请求,把body部分的数据读出来写到管道里,
    //剩下的动态生成页面的过程都交给子进程来完成
    if (strcasecmp(req->method, "POST"))
    {
        //根据body的长度决定读取多少个字节
        char c = '\0';
        int i = 0;
        //使用循环防止 read被打断,不加循环即使缓冲区够长,
        for (; i < req->content_length; ++i)
        {
            read(new_sock, &c, 1);
            write(father_write, &c, 1);
        }
    }
    // b)构造HTTP响应
    const char* first_line = "HTTP/1.1 200 OK\n";
    send(new_sock, blank_line, strlen(blank_line),0);
    // c)从管道中读取数据(子进程动态生成的页面),把这个页面也写到socket当中,
    //此处不方便用sendfile,,主要是数据的长度不容易确定
    char c = '\0';
    while (raed(father_read, &c, 1) > 0)
    {
        write(new_sock, &c, 1);
    }
    // d)进程等待,回收子进程的资源.
    //此处如果要进程等待,最好使用waitpid,保证当前回收的子进程就是当年
    waitpid(NULL);
    return 200;
}


int HandlerCGIChild(int child_read,int child_write,int new_sock, int father_read, int father_write, const HttpRequest* req)
{
    //注意:环境变量写在父进程中,虽然子进程能够继承父进程的环境变量,由于同一时刻会有很多个请求,每个请求
    //都在请求修改环境变量,会产生类似于线程安全的问题.导致子进程不能正确的获取到这些信息
    // a)设置环境变量(METHOD,CONTENT_LENGTH,QUERY_STRING)
    //   如果把上面的这几个信息通过管道来告知替换之后的程序,把这个数据也写到socket当中也是可行的,但是此处要遵守CGI标准,
    //所以必须使用环境变量传递以上信息.
    char method_env[SIZE] = { 0 };
    //REQUERY_METHOD=GET
    sprintf(method_env, "REQUEST_METHOD=%s", req->method);
    putenv(method_env);
    if (strcasecmp(req->method,"GET")==0)
    {//设置REQUERY_STRING
        char query_string_env[SIZE] = { 0 };
        sprintf(method_env, "QUERY_STRING=%s", req->query_string);
        putenv(query_string_env);
    }
    else
    {
        //设置CONTENT_LENGTH
        char conten_length_env[SIZE] = { 0 };
        sprint(conten_length_env, "CONTENT_LENGTH=%s", req->content_length);
        putenv(conten_length_env);
    }

    // b)把标准输入和标准输出重定向到管道中.此时,CGI读写标准输入输出就相当于读写管道.
    dup2(child_read,0);//把后面重定向到前面
    dup2(child_write, 1)
        // c)子进程进行程序替换(需要先找到是哪个CGI可执行程序,然后再使用exec函数进行替换)
        //   替换成功之后,动态页面完全交给CGI程序来计算生成.
        char file_path[SIZE] = { 0 };
    HandlerFilePath(req->url_path, file_path);
    //l lp le
    //v vp ve
    //第一个参数是可执行程序的路径
    //第二个参数,argv[0]
    //第三个参数NULL,表示命令行参数结束了
    execl(file_path,file_path,NULL)
    // d)替换失败的错误处理,子进程就是为了替换而存在的
    //如果替换失败,就没有存在的必要了
}

int HandlerCGI(int new_sock)
{
    //1.创建一对匿名管道
    int fd[1], fd[2];
    pipe(fd1);
    pipe(fd2);
    int father_read = fd[0];
    int child_write = fd[1];
    int father_write = fd[1];
    int child_read = fd[0];
    //2.创建子进程fork
    pid_t ret = fork();
    if (ret > 0)
    {
        //father
        close(child_read);
        close(child_write);
        HandlerCGIFather();
    }
    else if (ret == 0)
    {
        //child;
        close(father_read);
        close(father_write);
        HandlerCGIChild();
    }
    else
    {
        perror("fork");
        goto END;
    }
    //3.父进程核心流程
    //father
    //此处先把不必要的文件描述符关闭掉
    //
    close(child_write);
    close();
    //4.子进程的核心流程
    
//收尾工作
END:
    close();
    close();
    close();
}

//完成具体的请求处理过程
void HandlerRequest(int64_t new_sock)
{
    int err_code = 200;//定义一个默认的错误码200,出错置为400
    HttpRequest req;
    memset(&req, 0, sizeof(req));
    //1.解析请求
    // a)从socket中读取首行
    char first_line[SIZE] = { 0 };
    if (ReadLine(new_sock,req.first_line,sizeof(req.first_line )-1) < 0)
    {
        //todo 错误处理,此处一旦触发逻辑,就简单处理,无脑返回404数据报.正常应该根据不同的错误原因返回不同的数据报
        err_code = 404;
        goto END;
    }
    printf("first_line:%s\n", req.first_line);
    // b)解析首行,获取到url和method
    if (ParseFirstLine(req.first_line,&req.method,&req.url)<0)
    {
        //错误处理
        err_code = 404;
        goto END;
    }
    // c)解析url,获取到url_path和query_string
    if (PerseUrl(req.url, &req.url_path, &req.query_string) < 0)
    {
        //错误处理
        err_code = 404;
        goto END;
    }
    // d)解析header,丢弃大部分header,只保留content-Length
    if (ParseHeader(new_sock, &req.content_length))
    {
        //错误处理
        err_code = 404;
        goto END;
    }
    //2.根据请求计算响应并能写回客户端
    if (strcasecmp(req.method, "GET")==0&&req.query_string==NULL)//strcasecmp忽略大小写的比较
    {
        // a)处理静态页面
        err_code=HandlerStaticFile(new_sock,&req);
    }
    else if (strcasecmp(req.method, "GET") == 0 && req.query_string != NULL)
    {
        // b)处理动态页面
        err_code=HandlerCGI();
    }
    else if (strcasecmp(req.method, "POST") == 0) {
        // b)处理动态页面
        err_code=HandlerCGI();
    }
    else
    {
        //错误处理
        err_code = 404;
        goto END;
    }

    END:
    //收尾工作,主动关闭socket,会进入TIME_WAUT
    if (err_code != 200){
        Handler404(new_sock);
    }
    close(new_sock);
}


void* ThreadEntry(void *arg)
{
    int64_t new_sock = (int64_t)arg;
    HeadlerRequest(new_sock);
    return NULL;
}

void HttpServerStart(const char* ip, short port)
{
    //1.基本的初始化,基于TCP
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_sock < 0)
    {
        perror("socket");
        return;
    }
    //设置一个选项 REUSEADDR:地址重用
    int opt = 1;
    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(OPT))

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);
    int ret = bind(listen_sock, (sockaddr*)&addr, sizeof(addr));//一个进程可以绑定多个端口号,通常一个端口号不能被多个进程绑定
    if (ret < 0)
    {
        perror("bind");
        return;
    }
    ret = listen(listen_sock, 5);
    if (ret < 0)
    {
        perror("listen");
        return ;
    }
    printf("HttpServer start OK\n");
    
    //2.进入事件循环
    while (1)
    {
        //此处实现一个多线程版本的服务器
        //每个请求都创建一个新的线程处理具体请求
        sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int64-t new_sock = accept(listen_sock, (sockaddr*)&peer, &len);//不能用static,因为N个线程不能共用同一个new_sock
        if (new_sock < 0)
        {
            perror("accept");
            continue;
        }
        pthread_t tid;
        pthread_create(&tid, NULL, THreadEntry, (void*)new_sock);
        //new_sock怎么传递给线程入口函数,不能写取地址.同一时刻服务器可能会有很多连接线程,每个线程处理一个请求,所有的
        //请求共用同一份new_sock一定会出错.正确的处理应该把new_sock按照值的方式传递到线程入口函数中.
        pthread_detach(tid);//线程分离
    }
}
int main(int argc,char* argv[])
{
    if (argc != 3)
    {
        printf("Usage ./http_server [ip] [port]\n");
        return 1;
    }
    signal(SIGCHLD, SIG_IGN);
    HttpServerStart(argv[1], atoi(argv[2]));
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
简介: 本框架是在Netroid的基础之上进行了封装,节省了其中配置的过程和一些不必要的操作 主要进行文本请求和图片请求,图片请求都进行了缓存(内存缓存和sd卡缓存)的封装,sd卡缓存时间可自行更改. 文本请求可传入解析的泛型clazz,即可返回解析后的clazz对象进行数据 操作,如果不需要进行数据解析,可通过另一种方式获取原生的string; 单图请求,单图请求可执行对本地asset文件夹,sd卡,http三种请求模式.只需传入相应的路径即可; 多图请求,多图请求主要是针对listview这种图文混排模式而生,能快速加载图片并实现缓存,不需要考虑 图片错位问题.只需传入相应的url即可完成全部功能. 使用说明: 1:在新创建的Manifest.xml中application中申明: <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" android:name="com.aqs.application.UApplication" > 并初始化Const.init();此处的初始化主要是对内存缓存,SD卡缓存大小,缓存时间等进行设置,如果不初始化,则按使用默认配置; 2:依赖HttpAqs-library或者jar包 3:通过公有方法进行网络请求,示例如下: >文本请求: >解析后的文本请求: HttpRequest.reqquest(int,String,Parse,Class){....}; >原生string文本请求: HttpRequest.getString(String,AqsString){...} >单张图片请求: HttpRequest.setImage(ImageView,String,int,int){...} >多张图片请求: 可使用AQSImageView控件来加载图片;特别是针对listview图文混排 实现方法: >在布局中添加 >在代码中 av.setImageUrl(url);

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值