實驗目的
掌握在 ARM 開發板實現一個簡單 WEB 伺服器的過程。
學習在 ARM 開發板上的 SOCKET 網路程式設計。
學習 Linux 下的 signal()函式的使用。
實驗內容
學習使用 socket 進行通訊程式設計的過程,瞭解一個實際的網路通訊應用程式整體設計, 閱讀 HTTP 協議的相關內容,學習幾個重要的網路函式的使用方法。 讀懂 HTTPD.C 原始碼。在此基礎上增加一些其他功能。在 PC 計算機上使用瀏覽器測試 嵌入式 WEB 伺服器的功能。
連線arm開發板
將 arm 開發板電源線接好,保持開發板開關處於閉合狀態。再分別將串列埠線、並口線和網線與 pc 機連線好。
建立超級終端
執行WindowsXP中:開始→所有程式→附件→通訊→超級終端(HyperTerminal)。
新建一個通訊終端。要求輸入區號、電話號碼等資訊請隨意輸入,為所建超級終端取名為 arm,隨意為其選一個圖示。
在屬性對話方塊中,將波特率設為 115200,資料位設為 8,無奇偶校驗,停止位為 1,無資料流控制。將其另存為在桌面。
開啟超級終端
開啟超級終端,開啟 arm 機電源開關。等待一分鐘,arm 機的資訊會顯示在超級終端的視窗中。輸入“boot”後會引導kernel,啟動linux系統。在應用程式目錄下,可以通過“ls”檢視。
實驗步驟
1、閱讀理解原始碼
進入/arm2410cl/exp/basic/07_httpd目錄,使用 vi 編輯器或其他編輯器閱讀理解原始碼
2、編譯應用程式
執行 make 產生可執行檔案httpd
但是當我們執行make時發現出現問題,無法執行成功(忘記截圖),於是查詢上一屆的部落格發現問題是沒有修改makefile:
make編譯成功。
3、下載除錯
使用 NFS 服務方式將 HTTPD 下載到開發板上,並拷貝測試用的網頁進行除錯。
4、本機測試
在桌上型電腦的瀏覽器中輸入 http://192.168.0.121,觀察在客戶機的瀏覽器中的連線請求結果和在開發板上的伺服器的列印資訊。
實驗程式碼分析:
/ * httpd.c: A very simple http server
* Copyfight (C) 2003 Zou jian guo <ah_zou@163.com>
* Copyright (C) 2000 Lineo, Inc. (www.lineo.com)
* Copyright (c) 1997-1999 D. Jeff Dionne <jeff@lineo.ca>
* Copyright (c) 1998 Kenneth Albanowski <kjahds@kjahds.com>
* Copyright (c) 1999 Nick Brok <nick@nbrok.iaehv.nl>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <sys/stat.h>
#include <dirent.h>
#include <signal.h>
#include <unistd.h>
#include <ctype.h>
#include "pthread.h"
#define DEBUG
int KEY_QUIT=0;
int TIMEOUT=30;
#define O_BINARY
#define O_BINARY 0
#endif
char referrer[128];
int content_length;
#define SERVER_PORT 80
/*傳送HTTP協議資料頭,伺服器迴應http協議資料頭,傳送純文字檔案資訊、gif格式圖片、gpeg格式圖片資訊、html資訊、伺服器版本資訊、檔案永不過期資訊。*/
int PrintHeader(FILE *f, int content_type)
{
alarm(TIMEOUT);
fprintf(f,"HTTP/1.0 200 OKn");
switch (content_type)
{
case 't':
fprintf(f,"Content-type: text/plainn");
break;
case 'g':
fprintf(f,"Content-type: image/gifn");
break;
case 'j':
fprintf(f,"Content-type: image/jpegn");
break;
case 'h':
fprintf(f,"Content-type: text/htmln");
break;
}
fprintf(f,"Server: uClinux-httpd 0.2.2n");
fprintf(f,"Expires: 0n");
fprintf(f,"n");
alarm(0);
return(0);
}
//對jpeg格式的檔案進行處理;
int DoJpeg(FILE *f, char *name)
{
char *buf;
FILE * infile;
int count;
if (!(infile = fopen(name, "r"))) {
fprintf(stderr, "Unable to open JPEG file %s, %dn", name, errno);
fflush(f);
alarm(0);
return -1;
}
PrintHeader(f,'j');
copy(infile,f); /* prints the page */
alarm(TIMEOUT);
fclose(infile);
alarm(0);
return 0;
}
//處理gif格式檔案;
int DoGif(FILE *f, char *name)
{
char *buf;
FILE * infile;
int count;
if (!(infile = fopen(name, "r"))) {
alarm(TIMEOUT);
fprintf(stderr, "Unable to open GIF file %s, %dn", name, errno);
fflush(f);
alarm(0);
return -1;
}
PrintHeader(f,'g');
copy(infile,f); /* prints the page */
alarm(TIMEOUT);
fclose(infile);
alarm(0);
return 0;
}
//處理目錄;
int DoDir(FILE *f, char *name)
{
char *buf;
DIR * dir;
struct dirent * dirent; //此處dirent不僅僅指向目錄,還指向目錄中的具體檔案,dirent結構體儲存的關於檔案的資訊很少,所以dirent起著一個索引的作用
if ((dir = opendir(name))== 0) {
fprintf(stderr, "Unable to open directory %s, %dn", name, errno);
fflush(f);
return -1;
}
PrintHeader(f,'h');
alarm(TIMEOUT);
fprintf(f, "<H1>Index of %s</H1>nn",name);
alarm(0);
if (name[strlen(name)-1] != '/') {
strcat(name, "/");
}
while(dirent = readdir(dir)) {
alarm(TIMEOUT);
fprintf(f, "<p><a href="/%s%s">%s</p>n", name, dirent->d_name, dirent->d_name);
alarm(0); //傳送目錄資訊;
}
closedir(dir);
return 0;
}
int DoHTML(FILE *f, char *name)
{
char *buf;
FILE *infile; //定義檔案流指標
int count;
char * dir = 0;
if (!(infile = fopen(name,"r"))) { //通過檔名開啟一個檔案,只讀屬性;
alarm(TIMEOUT);
fprintf(stderr, "Unable to open HTML file %s, %dn", name, errno); //列印開啟檔案失敗資訊;
fflush(f);
alarm(0);
return -1;
}
PrintHeader(f,'h'); //傳送http協議資料包;f表示客戶連線的檔案流指標用於寫入http協議資料頭資訊;
copy(infile,f); /* prints the page */ //將開啟的檔案內容通過傳送回客戶端;
alarm(TIMEOUT);
fclose(infile);
alarm(0);
return 0;
}
int DoText(FILE *f, char *name) //純文字檔案的處理;
{
char *buf;
FILE *infile; //定義檔案流指標;
int count;
if (!(infile = fopen(name,"r"))) { //通過檔名開啟一個檔案,只讀屬性
alarm(TIMEOUT);
fprintf(stderr, "Unable to open text file %s, %dn", name, errno);
fflush(f);
alarm(0);
return -1;
}
PrintHeader(f,'t'); //傳送t型別的http協議資料頭資訊;
copy(infile,f); /* prints the page */
alarm(TIMEOUT);
fclose(infile);
alarm(0);
return 0;
}
int ParseReq(FILE *f, char *r)
{
char *bp; //定義指標bp;
struct stat stbuf;
char * arg; //引數指標;
char * c;
int e;
int raw;
#ifdef DEBUG
printf("req is '%s'n", r); //列印請求命令;例如:GET /img/baidu_sylogo1.gif HTTP/1.1rn
#endif
while(*(++r) != ' '); /*skip non-white space*/ //判斷buf中的內容是否為空跳過非空白;
while(isspace(*r)) //判斷r所在位置的字元是否為空格若為空格則r指向下一個字元;
r++;
while (*r == '/') //判斷r所在位置的字元是否為/若為空格則r指向下一個字元;
r++;
bp = r; //將r所指向的內容賦值給bp bp指向/之後的內容;img/baidu_sylogo1.gif HTTP/1.1rn
while(*r && (*(r) != ' ') && (*(r) != '?'))
r++;//當r不為空,並求 r不為?時r指向下一個字元
#ifdef DEBUG
printf("bp='%s' %x, r='%s' n", bp, *bp,r); //列印 r和bp的值;
#endif
if (*r == '?') //判斷 r是否為 ?若為?則執行以下語句;
{
char * e; //定義指標變數;
*r = 0; //將r所在位置處的字元設為; 的ASCII碼值是0
arg = r+1; //arg指向下一個引數;
if (e = strchr(arg,' '))
{
*e = ''; //如果arg為空則將arg所在位置置為複製給e;
}
} else
{ // 如果當前r指向字元不為 '?', 將r指向字元置為 '',
arg = 0;
*r = 0; // r處設為;
}
c = bp;//將bp賦值給c;
/*zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz*/
if (c[0] == 0x20){ //判斷c中的字元內容是否為空格;若為空格
c[0]='.'; //將.和放入c陣列中;
c[1]='';
}
/*zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz*/
if(c[0] == '') strcat(c,"."); //若 c中為則將.連結在c後;
if (c && !stat(c, &stbuf)) //通過檔名c獲取檔案資訊,並儲存在stbuf中
//返回值: 執行成功則返回0,失敗返回-1,錯誤程式碼存於errno
{
if (S_ISDIR(stbuf.st_mode))//判斷結果是否為特定的值
{
char * end = c + strlen(c); //end指向c的末尾;
strcat(c, "/index.html"); //將/index.html加到c後,後面追加;
if (!stat(c, &stbuf)) //通過檔名c獲取檔案資訊,並儲存在stbuf中 ;成功返回0;
{
DoHTML(f, c); //對html檔案進行處理;
}
else
{
*end = ''; //將end指向;
DoDir(f,c); //若c中沒有"/index.html" 則跳到目錄處理目錄程式碼處去執行;
}
}
else if (!strcmp(r - 4, ".gif")) //判斷r中的後四個字元,即判斷檔案型別;
DoGif(f,c); //若是 gif格式的檔案則跳轉到DoGif對其進行處理;
else if (!strcmp(r - 4, ".jpg") || !strcmp(r - 5, ".jpeg"))
DoJpeg(f,c); //若是 jpg或jpeg格式的檔案則跳轉到DoJpeg對其進行處理;
else if (!strcmp(r - 4, ".htm") || !strcmp(r - 5, ".html"))
DoHTML(f,c); //若是 htm格式的檔案則跳轉到DoHTML處對其進行處理;
else
DoText(f,c);//若是 純文字格式的檔案則跳轉到DoText對其進行處理
}
else{
PrintHeader(f,'h'); //傳送h型別的http協議資料頭
alarm(TIMEOUT);
fprintf(f, "<html><head><title>404 File Not Found</title></head>n"); //列印出錯資訊
fprintf(f, "<body>The requested URL was not found on this server</body></html>n");
alarm(0);
}
return 0;
}
void sigalrm(int signo) //定時器終止時傳送給程式的訊號;
{
/* got an alarm, exit & recycle */
exit(0);
}
int HandleConnect(int fd)
{
FILE *f;//定義檔案流FILE結構體指標用來表示與客戶連線的檔案流指標;
char buf[160]; //定義緩衝區buf用來存放客戶端的請求命令;
char buf1[160]; //定義緩衝區buf用來存放客戶端的各欄位資訊;
f = fdopen(fd,"a+"); //以檔案描述符的形式開啟檔案; a+ 以附加方式開啟可讀寫的檔案。若檔案不存在,則會建立該檔案,如果檔案存在,寫入的資料會被加到檔案尾後,即檔案原先的內容會被保留。
if (!f) {//若檔案開啟失敗則列印出錯資訊;
fprintf(stderr, "httpd: Unable to open httpd input fd, error %dn", errno);
alarm(TIMEOUT); // 鬧鐘函式成功則返回上一個鬧鐘時間的剩餘時間,否則返回0。 出錯返回-1
close(fd);//關閉檔案描述符;
alarm(0); //將鬧鐘時間清0;
return 0;
}
setbuf(f, 0); //將關閉緩衝區;
alarm(TIMEOUT); //啟用鬧鐘;
if (!fgets(buf, 150, f)) { //直接通過f讀取150個字元放入以buf為起始地址中,不成功時返回0則列印出錯資訊;否則fgets成功返回函式指標列印buf的內容;
fprintf(stderr, "httpd: Error reading connection, error %dn", errno);
fclose(f); //關閉檔案描述符;
alarm(0);
return 0;
}
#ifdef DEBUG
printf("buf = '%s'n", buf); //列印客戶機發出的請求命令;
#endif
alarm(0); //將鬧鐘時間清0;
referrer[0] = '';//初始化referrer陣列;
content_length = -1; //將資訊長度初始化為-1;
alarm(TIMEOUT); //設定定時器;
//read other line to parse Rrferrer and content_length infomation
while (fgets(buf1, 150, f) && (strlen(buf1) > 2)) { //直接通過f讀取150個字元放入以buf1為起始地址的空間中;
alarm(TIMEOUT);
#ifdef DEBUG
printf("Got buf1 '%s'n", buf1); //列印buf1中的資訊;
#endif
if (!strncasecmp(buf1, "Referer:", 8)) { //將buf1中的前八個字元與字串Referer:若相等則將將指標指向buf1中的Referer:之後;
char * c = buf1+8;
while (isspace(*c)) //判斷c處是否為空格若為空格則c指向下一個字元;
c++;
strcpy(referrer, c); //將c所指的記憶體單元的內容複製到referrer陣列中;
}
else if (!strncasecmp(buf1, "Referrer:", 9)) { //將buf1中的前九個字元與字串Referrer:若相等則將將指標指向buf1中的Referrer:之後;
char * c = buf1+8;
char * c = buf1+9;
while (isspace(*c)) //判斷c處是否���空格若為空格則c指向下一個字元;
c++;
strcpy(referrer, c); //將c所指的記憶體單元的內容複製到referrer陣列中;
}
else if (!strncasecmp(buf1, "Content-length:", 15)) { )) { //將buf1中的前15個字元與字串Content-length:若相等則將將指標指向buf1中的Content-length:之後;
content_length = atoi(buf1+15); //atoi型別轉換將buf1中的內容轉換為整型賦值給content_length;
}
}
alarm(0);
if (ferror(f)) { //錯誤資訊輸出;
fprintf(stderr, "http: Error continuing reading connection, error %dn", errno);
fclose(f);
return 0;
}
ParseReq(f, buf); //解析客戶請求函式;
alarm(TIMEOUT); //開啟計時器;
fflush(f); //重新整理流;
fclose(f); //關閉檔案流;
alarm(0);
return 1;
}
void* key(void* data)
{
int c;
for(;;){
c=getchar(); //從鍵盤輸入一個字元
if(c == 'q' || c == 'Q'){
KEY_QUIT=1;
exit(10); //若輸入q則退出程式;
break;
}
}
}
int main(int argc, char *argv[])
{
int fd, s; //定義套接字檔案描述符作為客戶機和伺服器之間的通道;
int len;
volatile int true = 1; //定義volatile型別的變數用來作為指向緩衝區的指標變數;
struct sockaddr_in ec;
struct sockaddr_in server_sockaddr; //定義結構體變數;
pthread_t th_key;//定義執行緒號;
void * retval; //用來儲存被等待執行緒的返回值。
signal(SIGCHLD, SIG_IGN); //忽略訊號量;
signal(SIGPIPE, SIG_IGN);
signal(SIGALRM, sigalrm); //設定時鐘訊號的對應動作;
chroot(HTTPD_DOCUMENT_ROOT); //改變根目錄;在makefile檔案中指定;
printf("starting httpd...n"); //列印啟用伺服器程式資訊;
printf("press q to quit.n");
// chdir("/");
if (argc > 1 && !strcmp(argv[1], "-i")) {// 若argv【1】等於-i strcmp返回0 並且 argc大於1 執行if下的語句快即關閉檔案描述符;
/* I'm running from inetd, handle the request on stdin */
fclose(stderr);
HandleConnect(0); //向HandleConnect函式傳入0檔案描述符即標準輸入;
exit(0);
}
if((s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1) { //若獲取套接字出錯則將錯誤資訊輸出到標準裝置;
perror("Unable to obtain network");
exit(1);
}
if((setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (void *)&true, //此函式用於設定套介面,若成功返回0,否則返回錯誤
sizeof(true))) == -1) {
perror("setsockopt failed"); //輸出錯誤資訊;
exit(1);
}
server_sockaddr.sin_family = AF_INET; //設定ip地址型別;
server_sockaddr.sin_port = htons(SERVER_PORT); //設定網路埠;
server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY表示本地任意ip;
if(bind(s, (struct sockaddr *)&server_sockaddr, //將所監聽的埠號與伺服器的地址、埠繫結;
sizeof(server_sockaddr)) == -1) {
perror("Unable to bind socket");//若繫結失敗則列印出錯資訊;
exit(1);
}
if(listen(s, 8*3) == -1) { //listen()宣告伺服器處於監聽狀態,並且最多允許有24個客戶端處於連線待狀態;
perror("Unable to listen");
exit(4);
}
pthread_create(&th_key, NULL, key, 0); //建立執行緒;
/* Wait until producer and consumer finish. */
printf("wait for connection.n"); //列印伺服器等待連結資訊;
while (1) {
len = sizeof(ec);//ec結構體變數的長度;
if((fd = accept(s, (void *)&ec, &len)) == -1) { //接受客戶機的請求,與客戶機建立連結;
exit(5);
close(s);
}
HandleConnect(fd); //處理連結函式呼叫fd 為客戶連線檔案描述符;;
}
pthread_join(th_key, &retval); //以阻塞的方式等待thread指定的執行緒結束。當函式返回時,被等待執行緒的資源被收回。如果程式已經結束,那麼該函式會立即返回。成功返回0;該語句不會執行到;
}