一個簡易B/S系統——Http Server和精簡的瀏覽器
——在實戰中開始C語言網絡編程
寫在前面:
這篇博文的題目同時也是我們這學期 C語言 課程中綜合實驗的題目。本菜鳥自然也是第一次干這事兒。在網上找了很多文章來學習,現在也寫一篇自己的博文來回饋大家!
下面先簡單介紹一下我們這篇博文要說些什么。
其實看題目就已經很明白了:就是要做一個Http Server和一個精簡的瀏覽器嘛。而為了實現這兩個東西,我們大概要用到套接字(socket)和http協議相關的知識,其中包括URL的知識。
最后,我們要有一個browser程序,一個server程序。此外還有一個test.html文件作為傳送的目標。
打開server讓它運作;然后打開browser,輸入URL,回車,請求就會生成報文(http協議)並發送給server。server接收到報文后,解析報文,並返回相應的應答報文。
不同於大多數文章上來就開始羅列相關函數(因為我是個菜鳥),我們不妨先通過一個簡單的對比例子來了解一下我們整個B/S系統的通信過程。
比方說有一天你發短信給你的朋友,想問他/她索要一張近照。你的朋友收到短信后給你回復了一張前幾天去海邊玩的照片。那么在這個過程中,你和你的朋友就算是構成了一個B/S系統(瀏覽器/服務器系統)。在這個過程中你和朋友是如何實現通信的呢?
想想上面的例子,在展開討論它之前,我們再來回憶一下平時用瀏覽器訪問網頁時的情景:
比如你要上百度,你會在瀏覽器的導航工具欄中輸入:www.baidu.com,然后回車,瀏覽器就會把百度首頁顯示給你。什么?你直接去收藏欄里開百度?好吧,那只是瀏覽器自動幫你完成了填寫網址的操作罷了。
事實上瀏覽器還幫你做了很多別的事,比如默認幫你補全前面的“http://”。
當我們要取得網絡上的某個資源時,一般是通過各種途徑給出了該資源的URL(統一資源定位符)。這是網絡上每個資源所獨有的地址。完整的格式如下:
傳送協議://服務器:端口號/路徑?查詢
詳細的介紹大家可以去文后的鏈接中找,本菜鳥就不贅述了。
網頁的訪問一般是通過http協議進行的。這種協議規定瀏覽器和服務器間通過規定格式的報文進行通信。通過協議,通信雙方可以完全不用考慮對方通信的具體內部實現,只要能讀懂報文(報文的格式是全球統一的)就可以通信了。也許你還沒有理解這里的意思。沒關系,這里我們先保留一個小驚喜,到后面你就會明白了。
再回過頭看看我們找朋友要照片的例子。大概就是說,你要請求一個照片(資源),於是你填寫了一封你們都可以理解的(遵守協議的)短信(報文),然后發送(send)給你的朋友;而另一邊,你的朋友總是時刻在注意聽手機有沒有響(listen),當聽到短信提示音后,便接收(accept)了你的短信並進行了閱讀(read)。在明白了你的需求后,你的朋友寫了一封附帶照片的你們可以讀懂的短信(應答報文)並回復給了你(send);然后你接收(recv)到了這個短信。這就是整個通信過程。當然在這些短信中,除了你們編寫的文字外還附加了諸如發送時間、手機號碼等附加屬性。
我們瀏覽器和服務器之間的通信也是類似的。事實上,上面括號中給出的就是我們的程序中要用到的具體函數!
當然,與我們發短信還是有一些不同的地方,比如說——你可能已經聽到過這個名字了——套接字。套接字在度娘那里是這樣描述的:
多個TCP連接或多個應用程序進程可能需要通過同一個 TCP協議端口傳輸數據。為了區別不同的應用程序進程和連接,許多計算機操作系統為應用程序與TCP/IP協議交互提供了稱為套接字(Socket)的接口。
更具體的定義和用法我在文后同樣給出了鏈接。
到此為止,相信你已經對整個B/S系統有一定的認識了。那么接下來附上我的代碼。根據我的注釋,大家應該基本可以看懂了!
代碼1:
/*
browser.c
一個精簡的瀏覽器。
可以接收用戶輸入的URL(ip形式),並進行訪問
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define MAXSIZE 2048
//URL解析器
int analysis_URL(char *buf, char *ip, char *port, char *html);
//報文生成器
int packet_builder(char *Header, char *html, char *ip, char *port);
int main (void)
{
int i; //用於計數
char buf[MAXSIZE]; //存放URL
char Header[MAXSIZE] = {'\0'}; //填寫報文
char s_ip[16]; //存放ip
char s_p[5]; //存放端口號 字符串
unsigned short s_port = 0; //存放端口號 整形
char s_html[30]; //存放路徑
int sockfd, recvbytes;
struct sockaddr_in serv_addr;
while(1)
{
//獲取URL
printf("URL:\n");
scanf("%s",buf);
//解析URL
if(!analysis_URL(buf, s_ip, s_p, s_html))
{
i = 0;
s_port = 0;
while(s_p[i] != '\0')
s_port = s_port*10+(s_p[i++]-'0');
}
else
{
printf("不支持的協議!");
continue;
}
//創建套接字
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket error!");
exit(1);
}
//根據URL填寫服務器端套接字網絡地址信息
bzero(&serv_addr,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(s_port);
serv_addr.sin_addr.s_addr= inet_addr(s_ip);
//與服務器端建立連接
if (connect(sockfd, (struct sockaddr *)&serv_addr,sizeof(struct sockaddr)) == -1)
{
perror("connect error!");
exit(1);
}
//填寫報文
memset(Header, '\0', MAXSIZE); //先清空原來的報文
packet_builder(Header, s_html, s_ip, s_p);
//發送請求報文
send(sockfd, Header, strlen(Header),0);
//接收應答文
if ((recvbytes = recv(sockfd, buf, MAXSIZE,0)) == -1)
{
perror("recv error!");
exit(1);
}
buf[recvbytes] = '\0';
//打印接收到的響應
printf("\n響應報文已接收!\n%s\n",buf);
//關閉套接字
close(sockfd);
close(recvbytes);
}
return 0;
}
代碼2:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <time.h>
#define SERVPORT 3333 //用戶接入端口
#define BACKLOG 10 //允許等待連接數
#define MAXSIZE 2048
//報文解析器
int analysis_packet();
//應答報文頭生成器
int packet_builder_s();
int main(void) {
int sockfd,client_fd;
struct sockaddr_in my_addr;
struct sockaddr_in remote_addr;
char msg[MAXSIZE] = {'\0'};
struct tm *ptr;
time_t it;
it=time(NULL);
//創建套接字
if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("socket create failed!");
exit(1);
}
//綁定端口地址
my_addr.sin_family = AF_INET;//通信類型
my_addr.sin_port = htons(SERVPORT);//端口
my_addr.sin_addr.s_addr = INADDR_ANY;//使用自己的ip地址
bzero(&(my_addr.sin_zero),8);
if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(struct sockaddr)) == -1)
{
perror("bind error!");
exit(1);
}
//監聽端口
if (listen(sockfd, BACKLOG) == -1)
{
perror("listen error");
exit(1);
}
while (1)
{
int sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr*)&remote_addr,&sin_size)) == -1)
{
perror("accept error!");
continue;
}
printf("收到來自%s的連接!\n", (char*)inet_ntoa(remote_addr.sin_addr));
//子進程段
if (!fork())
{
//接受client發送的請示信息
int rval;
char buf[MAXSIZE];
char file[50] = {'\0'};
FILE *fp;
char text[MAXSIZE];
char fname[100] = "/home/arcane/C_learning"; //服務器文件的路徑
int size;
if ((rval = read(client_fd, buf, MAXSIZE)) < 0)
{
perror("reading stream error!");
continue;
}
printf("%s\n",buf);
//解析報文
if(!analysis_packet(buf, file)) //解析成功
{
//生成本地文件路徑
strcat(fname, file);
if((fp=fopen(fname,"r")) != NULL) //找到文件
{
fseek(fp, 0, SEEK_END);
size = ftell(fp);
fseek(fp, 0, SEEK_SET);
//填寫響應報文頭
packet_builder_s(msg, 200, size);
while(fgets(text,2048,fp)!=NULL)
{
strcat(msg,text);
};
fclose(fp);
//向client發送應答報文
if (send(client_fd, msg, strlen(msg), 0) == -1) perror("send error!");
close(client_fd);
}
else
{
packet_builder_s(msg, 404, 0);
if (send(client_fd, msg, strlen(msg), 0) == -1) perror("send error!");
close(client_fd);
}
}
else
printf("報文解析失敗!\n");
exit(0);
}
close(client_fd);
}
return 0;
}
結合之前的介紹,上面的代碼是很好理解的。唯一的難點可能就在結構體 sockaddr_in 上面。這個結構體展開來是這樣的:
struct sockaddr_in
{
short int sin_family; //通信類型
unsigned short int sin_port; // 端口
struct in_addr sin_addr; // Internet 地址
unsigned char sin_zero[8]; // 與sockaddr結構的長度相同
};
關於它的更多描述在文后鏈接中有提到,有興趣深究的朋友可以看看。
當然,上面的代碼你還不能直接使用。除了要把服務器程序中的資源路徑改成你的資源路徑外,我們還有4個函數只給出了聲明而沒有給出實現。它們分別是:browser的URL解析器和報文生成器、server的報文解析器和應答報文頭生成器。而要實現這4個函數,我們就要深入了解http協議——因為我們的報文就是http協議規定的嘛!
報文的生成其實就是按照規則生成一個字符串,而報文解析就是對這個報文字符串按照同樣的規則拆分的過程。報文中包含了諸如主機地址、請求資源、發送時間、接受狀態等信息。
具體的格式我同樣給出了鏈接。大家按照同樣的規則寫好的報文程序應該都是可以解析出來的。
下面我給出我的實現。像前面說的,我在這里只是實現了一小部分功能,比如browser只能生成GET類型的報文(這是最簡單的類型,還有其他類型的可以在鏈接中找到)。但是它們確實是嚴格按照規則來的。
代碼1(添加到browser.c底部):
/*
解析URL
URL一般格式:scheme://host:port/path?query#fragment
這是一個只能解析http協議的URL解析器
*/
int analysis_URL(char *buf, char *ip, char *port, char *html)
{
char *p;
int i;
if((p=strtok(buf, ":")) != NULL)
{
//確認為 http 協議
if(!strcmp(p, "http"))
{
//確認 http 后的 "://"
p=strtok(NULL,":");
if(*p=='/' && *(p+1)=='/')
{
strcpy(ip, p+2); //填寫 ip
//確認有端口號
if((p=strtok(NULL,":")) != NULL)
{
if((i=strcspn(p,"/")) < strlen(p))
{
strncpy(port, p, i); //填寫 port
//端口號之后的 "/"
strcpy(html, p+i); //填寫具體文件
return 0;
}
}
else
{
printf("缺少端口號!");
return 1;
}
}
}
else
{
printf("不支持的協議!");
return 1;
}
}
printf("格式錯誤!");
return 1;
}
/*
報文生成器
這是一個只能生成GET類報文的簡單生成器
報文首部只填寫部分屬性
*/
int packet_builder(char *Header, char *html, char *ip, char *port)
{
//請求行
strcat(Header,"GET ");
strcat(Header,html);
strcat(Header," HTTP/1.1\r\n");
//首部
strcat(Header,"Accept: */*\r\n");
strcat(Header,"Accept-Language: zh-cn\r\n");
strcat(Header,"User-Agent: Qbrowser/0.0\r\n");
strcat(Header,"Host: ");
strcat(Header,ip);
strcat(Header,":");
strcat(Header,port);
strcat(Header,"\r\nConnection: Keep-Alive\r\n");
//空行
strcat(Header,"\r\n");
//請求主體
return 0;
}
代碼2(添加到server.c底部):
/*
請求報文解析器
這是一個極簡的解析器
事實上只解析了請求行
*/
int analysis_packet(char *packet, char *f)
{
char *p;
char *delims={ " \r" };
if((p=strtok(packet, delims)) == NULL)
{
return 1;
}
if(!strcmp(p,"GET")) //確認請求為 GET
{
if((p=strtok(NULL, delims)) == NULL)
{
return 1;
}
strcpy(f, p); //獲取文件路徑
if((p=strtok(NULL, delims)) == NULL)
{
return 1;
}
if(!strcmp(p, "HTTP/1.1")) //確認協議為 HTTP/1.1
return 0;
else
printf("報文解析:未知的協議!\n");
}
else
printf("報文解析:未知的請求!\n");
return 1;
}
/*
應答報文頭生成器
只支持200和404狀態
填寫了部分屬性
*/
int packet_builder_s(char *msg, int status, int s)
{
char num[5];
char *wday[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
char *wmon[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
struct tm *ptr;
time_t it;
time(&it);
ptr=localtime(&it); //取得本地時間
switch(status)
{
case 200:
//狀態行
strcat(msg,"HTTP/1.1 200 OK\r\n");
//首部
strcat(msg,"Date: ");
strcat(msg, wday[ptr->tm_wday]);
strcat(msg, ", ");
sprintf(num,"%d",ptr->tm_mday);
strcat(msg,num);
strcat(msg," ");
strcat(msg, wmon[ptr->tm_mon]);
strcat(msg," ");
sprintf(num,"%d",1900+ptr->tm_year);
strcat(msg,num);
strcat(msg," ");
sprintf(num,"%d",ptr->tm_hour);
strcat(msg,num);
strcat(msg,":");
sprintf(num,"%d",ptr->tm_min);
strcat(msg,num);
strcat(msg,":");
sprintf(num,"%d",ptr->tm_sec);
strcat(msg,num);
strcat(msg," GMT\r\n");
strcat(msg,"Content-Type: text/html;charset=gb2312\r\n");
strcat(msg,"Content-Length: ");
sprintf(num, "%d", s);
strcat(msg, num);
strcat(msg,"\r\n\r\n");
break;
case 404:
//狀態行
strcat(msg,"HTTP/1.1 404 Not Found\r\n");
//首部
strcat(msg,"Date: ");
strcat(msg, wday[ptr->tm_wday]);
strcat(msg, ", ");
sprintf(num,"%d",ptr->tm_mday);
strcat(msg,num);
strcat(msg," ");
strcat(msg, wmon[ptr->tm_mon]);
strcat(msg," ");
sprintf(num,"%d",1900+ptr->tm_year);
strcat(msg,num);
strcat(msg," ");
sprintf(num,"%d",ptr->tm_hour);
strcat(msg,num);
strcat(msg,":");
sprintf(num,"%d",ptr->tm_min);
strcat(msg,num);
strcat(msg,":");
sprintf(num,"%d",ptr->tm_sec);
strcat(msg,num);
strcat(msg," GMT\r\n\r\n");
break;
}
return 0;
}
上面的代碼只是一個小小的示范。相信在你充分理解了http協議和一些string操作的方法之后,你可以寫出更強大、更穩定的函數!
記得之前說過要把server.c中的服務器資源路徑改成你的路徑嗎?我們在這個路徑下放一個用於測試的test.html文件。不然空空如也的服務器象什么話嘛!這個文件是用html語言寫的,不用管它,復制粘貼下面的內容就好了!
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<meta http-equiv="Content-Language" content="zh-cn" />
</head>
<body bgcolor="yellow">
<h2>Hello World</h2>
</body>
</html>
這樣服務器就有資源了。服務器每次受到請求,都會在服務器資源路徑下查找,如果找到了,就返回200報文,並附上文件內容;如果沒找到,則返回404報文。
下面來看看我們的成果吧!
把我們的程序編譯好。
運行server,它就開始監聽連接了。保持它的運行。
運行browser,輸入:
http://127.0.0.1:3333/test.html
回車
你就會看到服務器和瀏覽器都收到了對方發來的報文!
在browser中輸入:
由於我們沒有在資源路徑下放置 no.html 這個文件,於是瀏覽器會接收到一個404報文。
雖然很簡單,但是以上就是我們的精簡B/S系統的全部了!
哦,等等!還有一個說好的小驚喜呢!
之前我們有說過的,由於我們是嚴格按照http協議來編寫的程序,所以它可以和任何其他同樣遵循此協議的程序實現通信而無需考慮其內部實現!什么意思呢?
現在,保持你的server運行。
打開你的瀏覽器。注意,不是那個browser,而是你的系統瀏覽器,比如火狐。
在你的導航欄里——不要懷疑——輸入:
http://127.0.0.1:3333/test.html
回車
你會發現你的瀏覽器真的為你打開了一個網頁!這個網頁是黃色的頁面,上面寫這一行字:“Hello World!”
你也許會很奇怪,你完全不知道火狐是如何編寫的,但是它卻可以訪問你的服務器!其實也不必大驚小怪,所謂協議就是用來干這個的呀!
鏈接:
這篇文章提供了詳細的c語言socket函數講解,很多定義和用法都可以找到:
http://blog.csdn.net/shisqf/article/details/6563942
這篇文章比較直觀地給出了http協議的格式和例子:
http://www.cnblogs.com/shaoge/archive/2009/08/14/1546019.html
這篇文章更詳細地介紹了http的相關概念,包括URL的介紹:
http://www.360doc.com/content/13/0217/11/9318309_266094744.shtml
這篇文章是一個介紹操作string的各種函數的好文章,在報文的生成和解析中可能用得到:
http://blog.csdn.net/sunnylgz/article/details/6677103