簡單的Web服務器
實現一個基於HTTP通信協議的web服務器。客戶端向服務器程序發送所需文件的請求,服務器端分析請求並將文件發送個客戶端。
1、整體程序設計
客戶端發送所需文件,服務器返回該文件,通信結束。
服務器程序的任務大致分爲3步:
a、解析客戶端程序發來的內容,找到需求文件的路徑或者名字
b、在服務器主機上查找該文件
c、將該文件內容組織成客戶端程序可以理解的形式,並將其發回給客戶端。
步驟一:解析客戶端發送的內容
Note:客戶端程序使用web瀏覽器,瀏覽器程序是默認使用HTTP通信協議。通常,http默認使用的端口號是80。
打開瀏覽器,在地址欄輸入需要訪問的服務器的ip地址和端口號。例,”httP://127.0.0.1:8080” 表示瀏覽器希望從地址爲127.0.0.1的服務器上使用8080端口的應用程序那裏接收數據,這些數據是遵守http通信協議的。
這時,瀏覽器向服務器程序發送一個HTTP協議的“協議頭”,包含了一些客戶端程序的基本信息,例如客戶機的ip地址、瀏覽器版本、使用協議版本以及請求文件的方式等。協議頭:
GET / HTTP/1.1
Host: 192.168.159.2:8080
User-Agent: Mozilla/5.0 (X11; U; Linux i586; en-US; rv:1.8.1.6)
Gecko/20061201 Firefox/2.0.0.6 (Redhat-feisty)
Accept:
text/xml, application/xml, application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en; q=0.5
Accept-Encoding: gzip, deflate
Accept-Charset: ISO-8859-1, utf-8; q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
附:協議頭最後有一個空行。
和本程序有關的是第一行。該行包括以下3個信息:
a、GET請求文件的方式,默認使用的是GET的方法。這個方法其實只是一個約定,具體的還要看服務器程序的實現。
b、“/”請求服務器程序的根目錄。這個目錄可由服務器程序編寫時自行配置。例,如果服務器程序的根目錄是“/home/admin”,那麼“/”所代表的就是這個目錄。
c、HTTP協議的版本號,本程序中使用的是1.1版本。這個字段對本程序沒有什麼影響。
服務器程序的第1步就是要解析“協議頭”的第1行,文件請求方式和協議版本我們不關心,主要是要找到請求的目錄。如果瀏覽器地址欄輸入“http://192.168.11.6:8080”表示的是服務器程序根目錄“/home/admin”,那麼“http://192.168.11.6:8080/web/test.html”就表示的是“/home/admin/web/test.html”文件。因此,服務器程序的第一個任務就是解析出這個客戶端請求的目錄和該目錄下的文件。
步驟二:尋找客戶端需要的文件
在指定目錄下尋找文件。如果是一個普通文件,且該文件沒有執行權限,則將其打開,把文件返回給客戶端。如果該文件是一個可執行文件,例如二進制可執行文件或者解釋器文件,則執行該文件,將執行的輸出結果返回給客戶端,這種可執行文件成爲CGI程序。
步驟三:將客戶端所請求的信息返回
Note:客戶端使用的是瀏覽器,簡單地返回一個文本文件是不對的,理想的方式是返回一個客戶端可以理解的格式的文件,這裏使用HTML格式的文件。
文件index.html的內容如下所示:
<html>
<head><title>This is a test</title></head>
<body>
<p>successfully communicate</p>
<img src=’test.jpg’>
</body>
</html>
這個頁面只有一行字和一張圖片,圖片的路徑是根目錄下的test.jpg文件。
除了瀏覽器可以解析的正文外,服務器還需要爲返回的信息加上HTTP的協議頭。該協議頭包含2行:第1行是版本信息和應答碼,其後是一個隨便寫的字符串“OK”用於調試目的;第2行是即將發送的文件類型。
版本協議和客戶端發送來的是一致的,連接成功,應答碼爲200;如果失敗,例如指定的文件不存在,應答碼爲404。文件類型爲text/html,是因爲index.html文件中既有文本又有圖片,單純的文本類型爲text/plain,單純的圖片類型爲image/jpg等。完整的返回信息應該如下所示:
HTTP/1.1 200 OK # 協議版本和應答碼
Content-Type: text/html # 文件類型
# 注意協議頭結束後有一個空行
<html>
<head><title>This is a test</title></head>
<body>
<p>successfully communicate</p>
<img src=’test.jpg’>
</body>
</html>
到此,服務器程序的任務就結束了,完成傳輸後,服務器端需要主動將連接斷開,這樣客戶端就知道數據已經傳輸完畢。這是由HTTP協議所規定的。
瀏覽器接收到index.html後,發現裏面需要一張圖片,因此瀏覽器會再次向服務器發出一次請求,要求獲得相應的文件。
經過兩次請求後,客戶端的瀏覽器獲得了index.html所需要的所有文件。這時,瀏覽器就可以正確顯示該頁面了,至於如何顯示則交給瀏覽器去做。
測試:
在web_server程序所在的目錄下準備配置文件config.ini
Port: 8080
Root-path: /home/trb331617/www
該程序使用8080端口,使用/home/trb331617/www作爲根目錄
在/home/trb331617/www目錄下準備index.html文件;準備圖片png_favicon.png和test.jpg
在shell中運行該程序:“./build”
打開web瀏覽器,在地址欄中輸入:http://127.0.0.1:8080,即可得到index.html頁面。
附:
Signal(SIGCHLD, SIG_IGN);
Signal(SIGPIPE, SIG_IGN);
網絡編程
服務端
// 填充服務端地址結構
Bzero(serv_addr, sizeof(struct sockaddr_in));
Serv_addr->sin_family = AF_INET;
Serv_addr->sin_addr.s_addr = htonl(INADDR_ANY);
Serv_addr->sin_port = htons(port);
Listenfd = socket(AF_INET, SOCK_STREAM, 0); // socket創建套接字
Bind(fd, (struct sockaddr *)serv_addr, sizeof(struct sockaddr_in)); // bind 套接字和填充好的地址綁定
Listen(fd, 20); // listen 監聽套接字
Accept(listenfd, (struct sockaddr *)&serv_addr, &len); // accept 接受連接請求
文件操作
Int fd = open(path, O_RDONLY);
FILE *fp = fopen(“./config.ini”, “r”);
Fgets(buf, BUFFSIZE, fp);
Read(connfd, buf, BUFFSIZE);
Write(connfd, buf, strlen(buf));
Struct stat stat_buf; fstat(fd, &stat_buf);
S_ISREG(stat_buf.st_mode); // 判斷是否爲普通文件
Stat_buf.st_mode & S_IXOTH // 判斷是否爲可執行文件
Dup2(connfd, STDOUT_FILENO); // dup2重定向標準輸出至連接套接字
Execl(path, path, NULL); // execl執行
字符串操作
Strstr(buf, “port”); // 返回指定字符子串第一次出現的位置
Strcpy(path, p); // 複製字符串
Strcat(path, “/index.html”); // 連接字符串
Strtok(&buf[4], “ ”); // strtok 分割字符串
源代碼文件彙總:
1 /*
2 * FILE: main.c
3 * DATE: 20180205
4 * ===============
5 */
6
7 #include "common.h"
8
9 int main(void)
10 {
11 struct sockaddr_in serv_addr, cli_addr;
12 struct stat stat_buf;
13 int listenfd, connfd, len, fd, port;
14 pid_t pid;
15 char path[BUFFSIZE];
16
17
18 signal(SIGCHLD, SIG_IGN); // signal 信號處理
19 signal(SIGPIPE, SIG_IGN);
20
21 printf("initializing ...\n");
22 if(init(&serv_addr, &listenfd, &port, path) < 0) // 自定義init初始化
23 {
24 DEBUG("error during initializing\n");
25 exit(-1);
26 }
27 while(1)
28 {
29 DEBUG("waiting connection ...\n");
30 connfd = accept(listenfd, (struct sockaddr *)&serv_addr, &len);
31 if(connfd < 0)
32 {
33 perror("fail to accept");
34 exit(-2);
35 }
36 pid = fork(); // fork 子進程,併發處理請求
37 if(pid < 0)
38 {
39 perror("fail to fork");
40 exit(-3);
41 }
42 if(pid == 0)
43 {
44 close(listenfd); // 關閉監聽套接字
45 if(get_path(connfd, path) < 0) // 分析客戶端發來的信息,得到請求文件路徑
46 {
47 DEBUG("error during geting filepath\n");
48 exit(-4);
49 }
50 DEBUG("%s\n", path);
51 if((fd=open(path, O_RDONLY)) < 0)
52 {
53 error_page(connfd);
54 close(connfd);
55 exit(0);
56 }
57 if(fstat(fd, &stat_buf) < 0)
58 {
59 perror("fail to get file status");
60 exit(-5);
61 }
62 if(!S_ISREG(stat_buf.st_mode)) // S_ISREG 判斷是否爲普通文件
63 {
64 if(error_page(connfd) < 0) // 自定義error_page 輸出出錯頁面
65 {
66 DEBUG("error during writing error-page\n");
67 close(connfd);
68 exit(-6);
69 }
70 close(connfd);
71 exit(0);
72 }
73 if(stat_buf.st_mode & S_IXOTH) // 可執行文件,說明是一個CGI文件
74 {
75 dup2(connfd, STDOUT_FILENO); // dup2重定向標準輸出到連接套接字
76 if(execl(path, path, NULL) < 0) // exec 執行該CGI文件
77 {
78 perror("fail to exec");
79 exit(-7);
80 }
81 }
82 if(write_page(connfd, path, fd) < 0) // 若爲普通文件,則將文件內容發送給客戶端
83 {
84 DEBUG("error during writing page\n");
85 exit(-8);
86 }
87 close(fd); // 關閉文件
88 close(connfd); // 服務器端主動關閉連接套接字,表示數據傳輸完畢
89 exit(0); // 子進程正常退出
90 }
91 else
92 close(connfd); // 父進程關閉連接套接字,繼續監聽
93 }
94 return 0;
95
96 }
97
98
99
100 /* FILE: web_server.c
101 * DATE: 20180205
102 * ===============
103 */
104
105 #include "common.h"
106 /*
107 * 讀取配置文件,對端口號和根目錄進行配置。
108 * 只在本文件內調用,使用static關鍵字進行聲明。
109 * port: 端口號 path: 服務器程序的根目錄
110 */
111 static int configuration(int *port, char *path)
112 {
113 FILE *fp;
114 char buf[BUFFSIZE];
115 char *p;
116 // 打開配置文件。該文件放在服務器程序所在目錄下
117 fp = fopen("./config.ini", "r");
118 if(fp == NULL)
119 {
120 perror("fail to open config.ini");
121 return -1;
122 }
123 while(fgets(buf, BUFFSIZE, fp) != NULL) // fgets 讀取文件每一行的內容
124 {
125 if(buf[strlen(buf)-1] != '\n') // 判斷文件格式
126 {
127 printf("error in config.ini\n");
128 return -1;
129 }
130 else
131 buf[strlen(buf)-1] = '\0'; // 將換行符\n改爲結束符\0
132 // 端口號的配置格式爲 port: 8080,注意冒號:後面有一個空格
133 if(strstr(buf, "port") == buf) // 匹配port關鍵字,讀取端口號
134 {
135 if((p=strchr(buf, ':')) == NULL)
136 {
137 printf("config.ini expect ':'\n");
138 return -1;
139 }
140 *port = atoi(p+2); // 跳過冒號:和空格得到端口號
141 if(*port <= 0)
142 {
143 printf("error port\n");
144 return -1;
145 }
146 }
147 // 根目錄的配置格式爲 root-path: /root,注意冒號:後面有一個空格
148 else if(strstr(buf, "root-path") == buf)
149 {
150 if((p=strchr(buf, ':')) == NULL)
151 {
152 printf("config.ini expect ':'\n");
153 return -1;
154 }
155 p = p + 2; // 跳過冒號和空格,得到根目錄
156 strcpy(path, p);
157 }
158 else
159 {
160 printf("error in config.ini\n");
161 return -1;
162 }
163 }
164 return 0;
165 }
166
167 int init(struct sockaddr_in *serv_addr, int *listenfd, int *port, char *path)
168 {
169 int fd;
170
171 configuration(port, path);
172
173 bzero(serv_addr, sizeof(struct sockaddr_in)); // bzero
174 serv_addr->sin_family = AF_INET; // sin_family AF_INET
175 serv_addr->sin_addr.s_addr = htonl(INADDR_ANY); // sin_addr.s_addr INADDR_ANY
176 serv_addr->sin_port = htons(*port); // sin_port
177
178 if((fd=socket(AF_INET, SOCK_STREAM, 0)) < 0) // socker 創建套接字
179 {
180 perror("fail to creat socket");
181 return -1;
182 }
183 // bind 將已創建的套接字與填充好的服務端地址綁定
184 if(bind(fd, (struct sockaddr *)serv_addr, sizeof(struct sockaddr_in)) < 0)
185 {
186 perror("fail to bind");
187 return -2;
188 }
189 if(listen(fd, 20) < 0) // listen 監聽套接字
190 {
191 perror("fail to listen");
192 return -3;
193 }
194 *listenfd = fd;
195 return 0;
196 }
197
198 /*
199 * 分析http協議頭的第一行,得到請求文件方式和文件路徑
200 * connfd: 連接套接字
201 * path: 服務器程序的根目錄,用於和解析出的文件名拼成完整的文件路徑
202 */
203 int get_path(int connfd, char *path)
204 {
205 char buf[BUFFSIZE];
206
207 read(connfd, buf, BUFFSIZE); // 讀取HTTP協議頭的第一行
208 // HTTP協議頭第一行的格式爲“GET / HTTP/1.1”
209 // 第四個字符爲空格,第五個字符開始是所要求的文件路徑
210 if(strstr(buf, "GET") != buf) // 協議的開始說明取得文件的方式“GET”
211 {
212 DEBUG("wrong request\n");
213 return -1;
214 }
215 // 若沒有指定文件名,則使用默認文件index.html
216 if(buf[4]=='/' && buf[5]==' ')
217 strcat(path, "/index.html");
218 else
219 {
220 strtok(&buf[4], " "); // strtok 分割字符串
221 strcat(path, &buf[4]); // strcat 連接字符串
222 }
223 return 0;
224 }
225
226 int error_page(int sockfd)
227 {
228 char err_str[BUFFSIZE];
229 #ifdef DEBUG_PRINT // 調試時,用於向客戶端輸出出錯信息
230 sprintf(err_str, "HTTP/1.1 404 %s\r\n", strerror(errno));
231 #else // 發佈版,則不輸出具體出錯信息
232 sprintf(err_str, "HTTP/1.1 404 Not Exsit\r\n");
233 #endif
234 // http協議頭第一行
235 write(sockfd, err_str, strlen(err_str));
236 // 協議頭第2行,說明頁面的類型。只輸出出錯信息,所以頁面類型爲文本類型text/html
237 write(sockfd, "Content-Type: text/html\r\n\r\n", strlen("Content-Type: text/html\r\n\r\n"));
238 // 輸出html內容
239 write(sockfd, "<html><body> the file dose not exsit </body></html>",
240 strlen("<html><body> the file not exist </body></html>"));
241 // http協議的每一行以\r\n結尾,整個協議的結尾還有額外的一行\r\n
242 return 0;
243 }
244 // 向客戶端輸出需要的頁面
245 // 將文件內容發送給客戶端,同樣,服務器程序需要爲該文件添加HTTP協議頭
246 // connfd: 連接套接字 path: 文件的完整路徑
247 int write_page(int connfd, char *path, int fd)
248 {
249 int len = strlen(path);
250 char buf[BUFFSIZE];
251 // 協議頭的第一行
252 write(connfd, "HTTP/1.1 200 OK\r\n", strlen("HTTP/1.1 200 OK\r\n"));
253 // 協議頭的第2行,頁面類型。需要根據文件的擴展名來進行判斷
254 write(connfd, "Content-Type: ", strlen("Content-Type: "));
255 // 三種圖片格式
256 if(strcasecmp(&path[len-3], "jpg")==0 || strcasecmp(&path[len-4], "jpeg")==0)
257 write(connfd, "image/jpeg", strlen("image/jpeg"));
258 else if(strcasecmp(&path[len-3], "gif") == 0)
259 write(connfd, "image/gif", strlen("image/gif"));
260 else if(strcasecmp(&path[len]-3, "png") == 0)
261 write(connfd, "image/png", strlen("image/png"));
262 else
263 write(connfd, "text/html", strlen("text/html"));
264 // 添加協議尾,最後需多出一個\r\n空行
265 write(connfd, "\r\n\r\n", 4);
266 // fd = open(path, O_RDONLY);
267 while((len=read(fd, buf, BUFFSIZE)) > 0)
268 write(connfd, buf, len);
269
270 return 0;
271 }
272
273
274
275
276 # FILE: Makefile
277 # DATE: 20180205
278 # ==============
279
280 OBJECTS = common.h web_server.c main.c
281
282 all: build
283
284 build: $(OBJECTS)
285 gcc -o build $(OBJECTS)
286
287 .PHONY: clean
288 clean:
289 rm *.o
290
291
292 # FILE: config.ini
293 # DATE: 20180205
294 # ===============
295
296 port: 8000
297 root-path: /home/admin
298
299
300 # FILE: index.html
301 # DATE: 20180205
302 # ===============
303
304 <html>
305 <head>
306 <title>This is a test</title>
307 <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
308 <link rel="icon" href="png_favicon.png" type="image/png" >
309 </head>
310 <body>
311 <p>successfully communicate</p>
312 <img src='test.jpg'>
313 </body>
314 </html>