Web服務器端程序的實現

 

Web服務器端程序主要是兩個部分,一部分是主函數,一部門是命令處理函數。命令處理函數比較好理解就是針對客戶端不一樣的命令進行處理,與客戶端進行通訊。主函數也有兩個主要的功能,第一是要對程序進行初始化,其中包括建立監聽套接字而且綁定到地址和端口上,第二是建立子進程處理對應的鏈接請求。服務器

一、主函數socket

Web服務器的主函數中第一是初始化程序,第二就是建立子進程。父進程一直監聽,子進程進行鏈接處理,提升服務器端的處理能力,提升效率,流程圖以下:函數

 

 

主函數中的代碼以下所示:學習

#include "common.h"
int main(void)
{
    struct sockaddr_in sin; /* 服務器端地址結構 */
    struct sockaddr_in cin; /* 客戶端地址結構 */
    int lfd, cfd;
    socklen_t len = sizeof(struct sockaddr_in);
                            /* 十進制點分格式地址的長度,包括字符串結束符 */
    char buf[MAX_LINE];      /* 命令緩衝區,存儲發送給客戶端的信息 */
    char str[ADDR_LEN];      /* 十進制點分地址緩衝區 */
    int sock_opt = 1;    /* 套接字選項 */
    int n;
    pid_t pid;
    if(init(&sin, &lfd, sock_opt) == -1)
                            /* 初始化程序,獲得地址結構和監聽套接字描述符 */
        exit(1);
    printf("waiting connections ...\n"); /* 打印提示信息 */
    while(1){ /* 死循環,處理客戶端的請求 */
        if( (cfd = accept(listen_fd, (struct sockaddr *)&cin, &len)) ==           -1){ /* 接收請求 */
            perror("fail to accept");
            exit(1);
        }
        if( (pid = fork()) < 0){ /* 建立子進程*/
            perror("fail to fork");
            exit(1);
        }else if(pid == 0){   /* 子進程處理鏈接請求,父進程繼續監聽 */
            close(lfd);      /* 關閉監聽套接字 */
            while(1){ /* 本程序的客戶端是一個交互式程序,服務器端和客戶端也是交互的 */
                if(my_read(cfd, buf, MAX_LINE) == -1) /* 讀取客戶端的命令 */
                    exit(1);
                /* 在用戶發送的命令串中尋找合法的命令,找到則處理 */
                if(strstr(buf, "GET") == buf) /* get命令 */
                    if(do_put(cfd, &buf[4]) == -1) /* 調用do_put函數進行處理 */
                        printf("error occours while putting\n");
                else if(strstr(buf, "PUT") == buf) /* put命令 */
                    if(do_cd(cfd, &buf[4]) == -1) /* 調用do_put函數進行處理 */
                        printf("error occours while getting\n");
                else if(strstr(buf, "CD") == buf) /* cd命令 */
                    if(do_ls(cfd &buf[4]) == -1)
                        printf("error occours while changing directory\n");
                else if(strstr(buf, "LS") == buf) /* ls命令 */
                    if(do_ls(&buf[4]) == -1)
                        printf("error occours while listing\n");
               else if(strstr(buf, "BYE") == buf) /* bye命令 */
                    break; /* 不調用相關函數處理,直接跳出循環 */
                else{ /* 未知命令,出錯 */
                    printf("wrong command\n");
                    exit(1);
                }
            }
            close(cfd); /* 跳出循環後關閉鏈接套接字描述符,通訊結束 */
            exit(0); /* 子進程退出 */
        }else
           close(cfd); /* 父進程關閉鏈接套接字,繼續監聽 */
    }
      return 0;            /* 服務器程序不多有退出的時候 */
}

在主函數中也能夠看出來父進程用來監聽,子進程用來處理鏈接請求。一開始進行LinuxC學習的時候學到了進程的相關的知識,這是一個多進程的程序實例,原理很簡單,可是須要注意的是子進程與父進程資源共享,所以要在父進程進行監聽的時候要關閉鏈接套接字,在子進程中要關閉監聽套接字,防止父子進程中相互干擾。主函數中進程初始化調用的init函數,函數代碼以下:spa

int init(struct sockaddr_in *sin, int *lfd, int sock_opt)
{
    bzero(sin, sizeof(struct sockaddr_in));
    sin->sin_family = AF_INET;
    sin->sin_addr.s_addr = INADDR_ANY;
    sin->sin_port = htons(PORT);

    if( (tfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { /* 建立監聽套接字 */
        perror("fail to creat socket");
        return -1;
    }
    setsockopt(tfd, SOL_SOCKET, SO_REUSEADDR, &sock_opt, sizeof(int)); /* 設置套接字選項 */
    /* 綁定客戶端地址,具體地址沒有限制 */
    if( (bind(tfd, (struct sockaddr *)sin, sizeof(struct sockaddr_in))) ==     -1){
        perror("fail to bind");
        return -1;
    }
    if( (listen(tfd, 20)) == -1){
        perror("fail to listen");
    return -1;
    }
    *lfd = tfd;
    return 0;
}
 

二、命令處理模塊code

根據以前的客戶端程序能夠知道服務器端有4個函數,分別是處理GET、PUT、CD、LS這四個命令。server

do_put函數處理GET命令,函數成功返回0,失敗返回-1。客戶端發過來的GET命令的格式爲:GET arg1。blog

參數說明:進程

cfd : 鏈接套接字的描述符ci

file : 客戶端請求的文件的路徑

int do_put(int cfd, char *file)
{
    struct stat statbuf; /* 文件狀態緩衝區 */
    int n, fd;
    int res = -1;      /* 返回值 */
    if( (fd = open(file, O_RDONLY)) == -1){ /* 打開客戶端請求的本地文件 */
        /* 打開失敗則向客戶端輸出出錯信息並將應答碼設置爲ERR
        * 出錯信息格式爲:應答碼 出錯信息
        */
        my_write(cfd, "ERR open server file\n", strlen("ERR open server    file\n")); 
        return res;        /* 返回-1 */
    }
    if( (fstat(fd, &statbuf)) == -1){ /* 獲得文件狀態 */
        my_write(cfd, "ERR stat server file\n", strlen("ERR stat server  file\n")      /* 出錯則發送錯誤信息 */
        goto end;
    }
    if(!S_ISREG(statbuf.st_mode)){ /* 若是被請求文件不是普通文件,則出錯 */
        if(my_write(cfd, "ERR server path should be a regular file\n", strlen("ERR server path should be a regular file\n")) == -1) /*           向客戶端發送出錯信息 */
            goto end;
        res = 0; /* 成功發送後do_put函數返回0,雖然出現了錯誤,但仍是返回0 */
        goto end;
    }
    sprintf(buf, "OK %d", statbuf.st_size); /* 一切正常,發送應答信息,格式爲:OK 發送文件的長度 */
    if(my_write(cfd, buf, strlen(buf)) == -1) /* 發送應答信息 */
        goto end;
    if ( (my_read(cfd, buf, MAX_LINE)) <= 0) /* 等待客戶端的應答信息,應答碼是RDY */
        goto end;
    while(1){ /* 開始傳送文件內容 */
        n = read(fd, buf, MAX_LINE); /* 循環讀取文件內容,直到文件結束 */
        if(n > 0)
            if(my_write(cfd, buf, n) == -1) /* 將讀取的文件內容發送給客戶端 */
                goto end;
        else if(n == 0) {  /* 文件已經到達結尾 */
            printf("OK\n");  /* 輸出提示信息 */
            break;
        }else   { /* 若是讀取的字節數小於0則說明出錯 */
            perror("fail to read");
            goto end;  
        }
    }
    res = 0; /* 執行至此一切正常 */
end:
    close(fd); /* 關閉文件,注意不是關閉套接字 */
    return res;
}

 do_get函數處理PUT命令,成功返回0,失敗返回-1。客戶端發過來的 PUT命令的格式爲:PUT arg1 arg2

該命令將客戶端的文件傳送至服務器,arg1爲文件的大小,arg2爲傳送文件在服務器端的文件路徑。

參數說明:

cfd : 鏈接套接字的描述符

file : 傳送過來的文件在服務器端的存儲路徑

int do_get(int cfd, char *file)
{
    struct stat statbuf; /* 文件狀態緩衝區 */
    int n, fd, len;
int res = -1;
if( (fd = open(file, O_WRONLY|O_CREAT|O_TRUNC, 0644)) == -1){ /* 打開文件 */
    /* 文件的打開方式是覆蓋寫,也就是說若是該文件已存在則覆蓋。
    * 可是若是有一個同名的目錄則沒法覆蓋,下面分支處理這種狀況
    */
    if(errno == EISDIR){
        if(my_write(cfd, "ERR server has a dir with the same name\n",
            strlen("ERR server has a dir with the same name\n")) == -1) /*
            輸出錯誤信息 */
            goto end;
        res = 0; /* 這種錯誤屬於用戶的不正確請求 */
        goto end;
    }else{ /* 其餘的緣由不能打開文件則是服務器程序的錯誤 */
        my_write(cfd, "ERR open server file\n", strlen("ERR open server  file\n")); /* 輸出錯誤信息 */
        goto end;
        }
}
if( (fstat(fd, &statbuf)) == -1){ /* 獲得文件狀態 */
    my_write(cfd, "ERR stat server file\n", strlen("ERR stat server file\n"));                       /* 輸出錯誤信息 */
    goto end;
    }
len = statbuf.st_size;
if(!S_ISREG(statbuf.st_mode)){    /* 若是該文件不是普通文件則出錯 */
    if(my_write(cfd, "ERR server path should be a regular file\n", strlen("ERR server path should be a regular file\n")) == -1)         /* 輸出錯誤信息 */
        goto end;
    res = 0;
    goto end;
    }
if(my_write(cfd, "OK", 2) == -1)   /* 發送正確的應答碼 */
    goto end;
    while(1){                        /* 循環讀傳送過來的文件內容 */
    n = my_read(cfd, buf, MAX_LINE);
    if(n > 0){
        write(fd, buf, n);            /* 寫到指定的文件中 */
        len -= n;                    /* 文件長度減小 */
    }else if(len == 0) {
        /* 文件沒有剩餘,表示傳輸完畢 */
        printf("OK\n");
        break;
    }else                           /* 讀取的字節數小於0,出錯 */
        goto end;
    }
    res = 0;                         /* 正常返回 */
end:
    close(fd);
    return res;
}

do_cd函數處理CD命令,成功返回0,失敗返回-1,客戶端發送過來的CD命令的格式爲:PUT arg1

該命令進入指定的服務器端目錄,arg1爲指定目錄的路徑

參數說明:

path:指定目錄的路徑

int do_cd(char *path)
{
    if(chdir(path) == -1){ /* 進入指定的目錄 */
        perror("fail to change directory\n");
        /* 出錯則向客戶端發送錯誤應答碼和出錯緣由 */
        my_write(cfd, "ERR can't change directory\n", strlen("ERR can't change directory\n"));
        return -1;
    }
    my_write(cfd, "OK\n");
    return 0;
}

 do_ls函數處理LS命令,成功返回0,失敗返回-1。客戶端發送過來的LS命令的格式爲:LS arg1。

該命令列出當前服務器端目錄下的文件,arg1爲指定目錄的路徑

參數說明:

path:指定目錄的路徑

int do_ls(char *path)
{
    char cmd[128];
    char buf[NAME_LEN];
    struct stat statbuf; /* 文件狀態緩衝區 */
    int n, fd;
    int res = -1;      /* 返回值 */
    /* 拼接命令「ls path > temp.txt」,將文件列表寫在temp.txt文件中 */
    sprintf(cmd, "ls %s > temp.txt ",path);
    system(cmd);       /* 執行該命令 */
    if( (fd = open(「temp.txt」, O_RDONLY)) == -1){ /* 打開客戶端請求的本地文件    */
        /* 打開失敗則向客戶端輸出出錯信息並將應答碼設置爲ERR。
        * 出錯信息格式爲:應答碼 出錯信息
        */
        my_write(cfd, "ERR ls server file\n", strlen("ERR ls server  file\n")); 
        return res;        /* 返回-1 */
    }
    if( (fstat(fd, &statbuf)) == -1){ /* 獲得文件狀態 */
        my_write(cfd, "ERR stat server file\n", strlen("ERR stat server   file\n") /* 出錯則發送錯誤信息 */
        goto end;
    }
    if(!S_ISREG(statbuf.st_mode)){ /* 若是被請求文件不是普通文件,出錯 */
        if(my_write(cfd, "ERR server path should be a regular file\n",  strlen("ERR server path should be a regular file\n")) == -1)          /* 向客戶端發送出錯信息 */
            goto end;
        res = 0; /* 成功發送後do_put函數返回0,雖然出現了錯誤,但仍是返回0 */
        goto end;
    }
    sprintf(buf, "OK %d", statbuf.st_size); /* 一切正常發送應答信息,格式爲: OK 發送文件的長度 */
    if(my_write(cfd, buf, strlen(buf)) == -1) /* 發送應答信息 */
        goto end;
    if ( (my_read(cfd, buf, MAX_LINE)) <= 0) /* 等待客戶端的應答信息,應答碼是RDY */
        goto end;
    while(1){ /* 開始傳送文件內容 */
        n = read(fd, buf, MAX_LINE);      /* 循環讀取文件內容,直到文件結束 */
        if(n > 0)
            if(my_write(cfd, buf, n) == -1)/* 將讀取的文件內容發送給客戶端 */
                goto end;
        else if(n == 0) {                 /* 文件已經到達結尾 */
            printf("OK\n");              /* 輸出提示信息 */
            break;
        }else   { /* 若是讀取的字節數小於0則說明出錯 */
            perror("fail to read");
            goto end;  
        }
    }
    res = 0;     /* 執行至此一切正常 */
end:
    close(fd); /* 關閉文件,注意不是關閉套接字 */
    return res;
}
相關文章
相關標籤/搜索