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; }