一个小型的Web多线程服务器开发笔记

Posted by Scalpel on May 24, 2017

  首先说一下服务器支持的功能:支持POST和GET方法,目录浏览,可以提供静态与动态两种服务,采用了预线程化的并发技术,项目主页
  服务器采用生产者-消费者模型,主线程不断地接受来自客户端的连接请求,并将连接描述符放到一个缓冲区中,每一个线程池中的线程反复地从这个缓冲区中取出可用的描述符,为客户端服务,具体代码如下:

sbuf_t sbuf; //缓冲区

int main(int argc, char* argv[]) 
{
    //判断参数
    if (argc != 2) 
    {
        perror("usage: webserver <port>");
        exit(1);
    }
    //分配套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) 
    {
        perror("Can't allocate sockfd");
        exit(1);
    }
    //设置服务器套接字地址
    struct sockaddr_in clientaddr, serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(atoi(argv[1]));
    //绑定并监听
    if (bind(sockfd, (const struct sockaddr *) &serveraddr, sizeof(serveraddr)) == -1)
    {
        perror("Bind Error");
        exit(1);
    }
    if (listen(sockfd, 4096) == -1)
    {
        perror("Listen Error");
        exit(1);
    }
    sbuf_init(&sbuf, SBUFSIZE); //初始化缓冲区
    pthread_t tid;
    //创建线程
    for (int i = 0; i < NTHREADS; ++i)
        if (pthread_create(&tid, NULL, thread, NULL) != 0) 
        {
            perror("Create pthread error");
            exit(1);
        }
    while (true) 
    {
        socklen_t clientlen = sizeof(clientaddr);
        //建立连接
        int connfd = accept(sockfd, (struct sockaddr *) &clientaddr, &clientlen);
        if (connfd == -1)
        {
            perror("Connect Error");
            exit(1);
        }
        char addr[INET_ADDRSTRLEN];
        printf("Accepted connection from (%s:%s)\n", inet_ntop(AF_INET, &clientaddr.sin_addr, addr, INET_ADDRSTRLEN), argv[1]);
        sbuf_insert(&sbuf, connfd); //将当前已连接套接字添加到缓存中
    }
    sbuf_destroy(&sbuf);
}

void* thread(void *vargp)
{
    if (thread_detach(pthread_self()) != 0)  //分离当前线程
    {
        perror("Detach pthread error");
        exit(1);
    }
    while (true)
    {
        int connfd = sbuf_remove(&sbuf);  //从缓存中取可连接的套接字
        //处理连接请求
        work(connfd);
        close(connfd);
    }
    return NULL;
}

  sbuf_t的结构如下:

typedef struct
{
    int *buf; //缓冲区
    int cnt;  //缓冲区可以保存的项目大小
    int begin;  //首项目
    int end;  //尾项目
    sem_t mutex;  //缓冲区互斥量
    sem_t slots;  //有多少空余位置
    sem_t items;  //缓冲区已保存多少项目
} sbuf_t;

  sbuf_init初始化缓冲区,sbuf_destroy销毁缓冲区sbuf_insert向缓冲区添加一个项目,具体过程为:等待一个空位置可以,加锁,添加,解锁,sbuf_remove是从缓存区移除一个项目,具体代码如下:

void P(sem_t *sem) //封装的PV操作
{
    if (sem_wait(sem) == -1)
        perror("P error");
}

void V(sem_t *sem) 
{
    if (sem_post(sem) == -1)
        perror("V error");
}

void sbuf_init(sbuf_t *sp, int cnt)
{
    sp->buf = calloc(cnt, sizeof(int)); 
    if (sp->buf == NULL)
    {
        perror("Calloc error");
        exit(1);
    }
    sp->cnt = cnt;                      
    sp->begin = sp->end = 0;       
    if (sem_init(&sp->mutex, 0, 1) == -1)      
    {
        perror("Sem_init error");
        exit(1);
    }
    if (sem_init(&sp->slots, 0, cnt) == -1)     
    {
        perror("Sem_init error");
        exit(1);
    }
    if (sem_init(&sp->items, 0, 0) == -1)    
    {
        perror("Sem_init error");
        exit(1);
    }
}

void sbuf_destory(sbuf_t *sp)
{
    free(sp->buf);
}

void sbuf_insert(sbuf_t *sp, int item)
{
    P(&sp->slots);                          
    P(&sp->mutex);                         
    sp->buf[(++sp->end) % (sp->cnt)] = item;   
    V(&sp->mutex);                         
    V(&sp->items);                         
}

int sbuf_remove(sbuf_t *sp)
{
    int item;
    P(&sp->items);                          
    P(&sp->mutex);                          
    item = sp->buf[(++sp->begin) % (sp->cnt)];  
    V(&sp->mutex);                         
    V(&sp->slots);                         
    return item;
}

  work函数处理一个HTTP事务,首先读取并且处理请求行,判断它的请求方法是GET还是POST,根据请求方法来处理请求报头,然后将URI解析为文件名和参数串,根据解析结果判断请求是静态还是动态内容并提供,如果是目录则显示目录内容,具体代码如下:

void work(int fd) 
{
    char buf[MAXLINE] = {0};
    rio_t rio;
    rio_readinitb(&rio, fd);
    //读取并解析请求行
    if (rio_readlineb(&rio, buf, MAXLINE) == -1) 
    {
        perror("Read error");
        exit(1);
    }
    puts("Request headers:");
    puts(buf);
    char method[MAXLINE], uri[MAXLINE], version[MAXLINE];
    sscanf(buf, "%s %s %s", method, uri, version);
    //判断请求方式
    bool isGET = !strcasecmp(method, "GET"), isPOST = !strcasecmp(method, "POST");
    if (!isGET && !isPOST) 
    {
        print_error(fd, method, "501", "Not implemented", "The server does not implement this method");
        return;
    }
    //读取请求报头
    char content[MAXLINE] = {0};
    if (isGET)
        read_get_requesthdrs(&rio);
    else
        read_post_requesthdrs(&rio, content);
    //将URI解析为文件名和参数串并判断提供何种内容
    char filename[MAXLINE] = {0}, cgiargs[MAXLINE] = {0};
    bool is_static = analyse_uri(uri, filename, cgiargs);
    struct stat sbuf;
    if (stat(filename, &sbuf) < 0) 
    {
        print_error(fd, filename, "404", "Not found", "The server could not find this file");
        return;
    }
    if(S_ISDIR(sbuf.st_mode))
        serve_dir(fd, filename); //显示目录内容
    else if (is_static) 
    {
        if (isPOST)
        {
            print_error(fd, filename, "405", "Method Not Allowed", "Request method POST is not allowed for the URL");
            return;
        }
        if (!(S_ISREG(sbuf.st_mode)) && !(S_IRUSR & sbuf.st_mode)) 
        {
            print_error(fd, filename, "403", "Forbidden", "The server could not read this file");
            return;
        }
        static_serve(fd, filename, sbuf.st_size);
    }
    else //动态内容
    {
        if (!(S_ISREG(sbuf.st_mode)) && !(S_IXUSR & sbuf.st_mode)) 
        {
            print_error(fd, filename, "403", "Forbidden", "The server could not run the CGI program");
            return;
        }
        if (isPOST)
            strcpy(cgiargs, content);
        dynamic_serve(fd, filename, cgiargs);
    }
}

  如果是GET方法,则忽略报头,如果是POST,则根据Content-Length读取报文:

oid read_get_requesthdrs(rio_t *rp) 
{
    char buf[MAXLINE];
    if (rio_readlineb(rp, buf, MAXLINE) == -1) 
    {
        perror("Read error");
        exit(1);
    }
    while (strcmp(buf, "\r\n")) 
    {
        if (rio_readlineb(rp, buf, MAXLINE) == -1) 
        {
            perror("Read error");
            exit(1);
        }
        printf("%s", buf);
    }
}

void read_post_requesthdrs(rio_t *rp, char *content)
{
    char buf[MAXLINE];
    int contentlength = 0;
    if (rio_readlineb(rp, buf, MAXLINE) == -1) 
    {
        perror("Read error");
        exit(1);
    }
    while (strcmp(buf, "\r\n")) 
    {
        if (rio_readlineb(rp, buf, MAXLINE) == -1) 
        {
            perror("Read error");
            exit(1);
        }
        printf("%s", buf);
        if (strstr(buf, "Content-Length: ") == buf)
            contentlength = atoi(buf + strlen("Content-Length: ")); //获得长度
    }
    //读入报文体
    int n;
    if ((n = rio_readnb(rp, content, contentlength)) != contentlength)
    {
        perror("Read POST content error");
        if (n == -1)
            exit(1);
        contentlength = n;
    }
    content[contentlength] = '\0';
    puts(content);
}

  print_error函数用来发送相关的错误消息给客户端,并显示为HTML页面,analyse_uri则将URI解析为文件名和参数串,并根据是否工作在cgi目录下判断是否为动态服务:

void print_error(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) 
{
    char buf[MAXLINE] = {0}, body[MAXLINE] = {0};
    //输出HTTP响应
    if (rio_writen(fd, buf, strlen(buf)) == -1) 
    {
        perror("Write error");
        exit(-1);
    }
    sprintf(buf, "Content-Type: text/html\r\n");
    if (rio_writen(fd, buf, strlen(buf)) == -1) 
    {
        perror("Write error");
        exit(-1);
    }
    sprintf(buf, "Content-Length: %lu\r\n\r\n", strlen(body));
    if (rio_writen(fd, buf, strlen(buf)) == -1) 
    {
        perror("Write error");
        exit(-1);
    }
    //建立并输出HTTP响应体
    sprintf(body, "<html><title>Server Error</title>");
    sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
    sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
    sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
    sprintf(body, "%s<hr><em>The Web Server</em></body></html>\r\n", body);
    sprintf(buf, "HTTP/1.0 %s %s \r\n", errnum, shortmsg);
    if (rio_writen(fd, body, strlen(body)) == -1) 
    {
        perror("Write error");
        exit(-1);
    }
}
bool analyse_uri(char *uri, char *filename, char *cgiargs) 
{
    //默认可执行文件主目录为cgi
    if (!strstr(uri, "cgi")) //静态内容
    {
        strcpy(cgiargs, ""); //清空参数字符串
        strcpy(filename, ".");
        strcat(filename, uri); //将uri转为相对路径名
        if (strlen(uri) == 0 || (strlen(uri) == 1 && uri[0] == '/')) //如果uri以/结尾,则将默认文件名加在后面
            strcat(filename, "index.html");
        return true;
    }
    else 
    {
        char *ptr = index(uri, '?'); //找到文件名与参数字符串分隔符
        if (ptr) 
        {
            strcpy(cgiargs, ptr + 1); //提前参数字符串
            *ptr = '\0';
        }
        else
            strcpy(cgiargs, "");
        strcpy(filename, ".");
        strcat(filename, uri);	//将uri剩下的部分转为相对路径名
        return false;
    }
}

  如果是目录,则根据文件名提取目录名,并且遍历这个目录,显示所有的文件链接,大小和修改时间,供用户浏览和打开:

void serve_dir(int fd, char *dirpath)
{  
    char *p = strrchr(dirpath, '/');
    ++p;
    char dir[MAXLINE] = {0};
    strcpy(dir, p); //复制目录名
    strcat(dir, "/");
    DIR *dp;
    if((dp = opendir(dirpath)) == NULL)
    {
        perror("Cann't open the dir");
        exit(1);
    }
    char fbuf[MAXLINE] = {0};
    sprintf(fbuf, "<html><title>Display directory content</title>");
    sprintf(fbuf, "%s<body bgcolor=""ffffff"" font-family=Consolas><table cellspacing=""10"">\r\n", fbuf);
    struct dirent *dirp;
    while((dirp = readdir(dp)) != NULL) //遍历目录
    {
        if(!strcmp(dirp->d_name, ".") || !strcmp(dirp->d_name, ".."))
            continue;
        char filepath[MAXLINE] = {0};
        sprintf(filepath, "%s/%s", dirpath, dirp->d_name);
        struct stat sbuf;
        stat(filepath, &sbuf);
        sprintf(fbuf,"%s<tr><td><a href=%s%s>%s</a></td><td>%ld</td><td>%s</td></tr>\r\n",
                fbuf, dir, dirp->d_name, dirp->d_name, sbuf.st_size, ctime_r(&sbuf.st_mtime));
    }
    closedir(dp);
    sprintf(fbuf,"%s</table></body></html>\r\n", fbuf);
    char buf[MAXLINE] = {0};
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    sprintf(buf, "%sServer: The Web Server\r\n", buf);
    sprintf(buf, "%sContent-Tength: %lu\r\n", buf, strlen(buf));
    sprintf(buf, "%sContent-Type: %s\r\n\r\n", buf, "text/html");
    rio_writen(fd, buf, strlen(buf));
    rio_writen(fd, fbuf, strlen(fbuf));
}

  服务器可以提供以下几种类型的静态内容:HTML文件,JPG,PNG,GIF格式的图片和无格式的文本:

void static_serve(int fd, char *filename, int filesize) 
{
    char buf[MAXLINE] = {0}, filetype[MAXLINE] = {0};
    get_filetype(filename, filetype); //获得文件类型
    //给客户端发送HTTP响应头
    sprintf(buf, "HTTP/1.0 200  OK\r\n");
    sprintf(buf, "%sServer: The Web Server\r\n", buf);
    sprintf(buf, "%sConnection: close\r\n", buf);
    sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
    sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
    if (rio_writen(fd, buf, strlen(buf)) == -1) 
    {
        perror("Write error");
        exit(1);
    }
    puts("Response headers:");
    printf("%s", buf);
    //打开请求文件
    int srcfd = open(filename, O_RDONLY, 0);
    if (srcfd < 0 ) 
    {
        perror("Can't open the file");
        exit(1);
    }
    //将请求文件内容映射到一个虚拟内存空间
    char *srcp = mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
    if (srcp == (void *)-1)
    {
    	perror("mmap error");
    	exit(1);
    }
    close(srcfd);
    //将内容发送到客户端
    if (rio_writen(fd, srcp, filesize) == -1) 
    {
        perror("Write error");
        exit(1);
    }
    //释放虚拟内存
    munmap(srcp, filesize);
}

//获取文件类型
void get_filetype(char *filename, char *filetype) 
{
    if (strstr(filename, ".html"))
        strcpy(filetype, "text/html");
    else if (strstr(filename, ".gif"))
        strcpy(filetype, "image/gif");
    else if (strstr(filename, ".png"))
        strcpy(filetype, "image/png");
    else if (strstr(filename, ".jpg"))
        strcpy(filetype, "image/jpg");
    else
        strcpy(filetype, "text/plain");
}

  动态服务则通过用URI的CGI参数来初始化QUERY_STRING环境变量,然后运行CGI程序:

void dynamic_serve(int fd, char *filename, char *cgiargs) 
{
    char buf[MAXLINE] = {0}, *emptylist[] = {NULL};
    //向客户端发送响应行
    sprintf(buf, "HTTP/1.0 200 OK\r\n");
    if (rio_writen(fd, buf, strlen(buf)) == -1) 
    {
        perror("Write error");
        exit(1);
    }
    sprintf(buf, "Server: The Web Server\r\n");
    if (rio_writen(fd, buf, strlen(buf)) == -1) 
    {
        perror("Write error");
        exit(1);
    }
    if (fork() == 0) //Child
    {
        setenv("QUERY_STRING", cgiargs, 1); //用CGI参数初始化环境变量
        dup2(fd, STDOUT_FILENO);
        execve(filename, emptylist, environ); //执行CGI程序
    }
    wait(NULL); //父进程阻塞
}