linux下http服務器開發
2)工作原理
1)客戶端
一台客戶機與服務器建立連接后,會發送一個請求給服務器,請求方式的格式為:統一資源定位符(URL)、協議版本號,后邊是MIME信息,包括請求修飾符、客戶機信息和可能的內容。
2)服務器端
1)服務器接收到客戶機的請求后,首先解析請求信息,根據不同的請求模式給予相應的響應信息。HTTP中規定了6種請求格式,但最常用到的是GET和POST請求
2)任何服務器除了包括HTML文件以外,還有一個HTTP駐留程序,用於響應用戶的請求。
3)服務器端駐留程序接收到請求后,在進行必要的操作后回送所要求的文件,在這一過程中,在網絡上發送和接收的數據已經被分成一個或多個數據包(Packet),每個數據包包括:要傳送的數據;控制信息,即告訴網絡怎樣處理數據包。
3)在HTTP請求格式中,最重要的信息有兩個,一個是請求的內容,另一個是請求的方法。
4)數據對是由字段組成,其格式為:valuename=value;數據對與數據對之間由&連接,這是HTTP中定義的請求規則。
5)在通常的WEB應用中,大數據量的提交一般選用POST方法,小數據量的提交,如查詢操作,一般采用GET方法。
6)請求實體由實體名和實體值組成,它的格式為:Entity:value
2.HTTP服務器設計
1)實現功能1)在HTTP服務器中,經常要處理的請求是GET,MesteryServer中將要實現針對GET請求的處理
服務器並不支持CGI功能,所以針對GET中的CGI部分的請求處理,在本版本中不予支持。
2)利用Gtk編寫可視化的界面,三個按鈕用於啟動、關閉和暫停服務,一個標簽用於顯示服務狀態。
3)文件傳輸功能是本 服務器最基本的功能,負責可靠傳送用戶請求的資源文件。
4)具有日志功能,會將客戶的請求信息存儲到本地的日志文件里,以XML格式進行存儲。
2)業務功能
1)針對GET請求功能,在這里只支持資源文件的下載,並且可以斷點下載。
2)如果是請求資源,則予以響應;如果涉及CGI的請求,則不予以響應。
3)系統只支持GET請求,對於POST、HEAD、TRACE等請求,都予以忽略。
4)對於HTTP服務器,它的運行模式是基於請求、響應機制,而下面的文件傳輸功能,其實是請求的具體執行過程,當服務器端成功解析請求命令后,會針對請求的類型,生成相應的響應碼,並傳送相應的資源文件。
5)當請求工作執行完畢時,會形成一條日志記錄,並插入到日志文件中。
6)考慮到服務的關聯性,服務器將為每一個請求開啟一個單獨的線程進行服務,該線程實現客戶端請求接受、請求分析、響應碼生成與傳輸、響應文件查找與傳輸、客戶套接字關閉、日志記錄寫入等功能
3)可視化界面
其設計與業務的邏輯采用松耦合,雙方只是通過消息的方式,傳遞控制命令與狀態信息。
4)主服務功能
提供端口綁定(HTTP默認端口是80)、服務偵聽、客戶端套接字維護、業務線程創建等。
5)界面模塊
1)由兩個子模塊組成:界面顯示子模塊->繪出程序的運程界面;按鈕事件處理子模塊
2)界面模塊與主服務模塊之間的消息傳遞采用全局變量共享的方式。
6)主服務模塊
1)以線程身份存在。
2)不斷輪詢全局變量gServerStatus,並通過這個狀態來動態調整真實服務狀態。
7)業務處理模塊
1)程序核心部分。
2)由請求分析子模塊、響應處理子模塊、文件傳輸子模塊、日志添加子模塊等幾個子模塊組成。
3.測試效果
1)本地測試
3)下載數據預覽
4.疑問解答
前幾天群里有人在討論TCP和UDP,然后又引出了HTTP服務器,於是我就給他們推薦了這篇文章,但是大家看過之后還是有很多疑問,這里我根據自己的理解簡單描述下。
❶主服務模塊的設計原理
可以看見,程序界面是用gtk寫的,當點擊“開始”按鈕的時候,會動態創建該線程,其線程回調函數原型為
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
void
* server_process(
void
*p)
{
int
serverSocket;
struct
sockaddr_in server_addr;
struct
sockaddr_in clientAddr;
int
addr_len =
sizeof
(clientAddr);
if
((serverSocket = socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror
(
"error: create server socket!!!"
);
exit
(1);
}
bzero(&server_addr,
sizeof
(server_addr));
server_addr.sin_family =AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if
(bind(serverSocket,(
struct
sockaddr *)&server_addr,
sizeof
(server_addr)) < 0)
{
perror
(
"error: bind address !!!!"
);
exit
(1);
}
if
(listen(serverSocket,5)<0)
{
perror
(
"error: listen !!!!"
);
exit
(1);
}
gIsRun = 1;
printf
(
"MesteryServer is running.....\n"
);
while
(gIsRun)
{
int
clientsocket;
clientsocket = accept(serverSocket,(
struct
sockaddr *)&clientAddr,(socklen_t*)&addr_len);
if
(clientsocket < 0)
{
perror
(
"error: accept client socket !!!"
);
continue
;
}
if
(gServerStatus == 0)
{
close(clientsocket);
}
else
if
(gServerStatus == 1)
{
pthread_t threadid;
int
temp;
temp = pthread_create(&threadid, NULL, processthread, (
void
*)&clientsocket);
/*if(threadid !=0)
{
pthread_join(threadid,NULL);
}*/
}
}
close(serverSocket);
}
|
從程序中可以看見,當綁定本地服務地址和端口后,便調用listen()函數進行偵聽,while(gIsRun)表示主服務模塊已經啟動;然后采用阻塞式等待用戶連接的到來,在連接到來的時候,還需要判斷gServerStatus的值,即系統是否允許提供服務,如果允許,則創建服務線程。
pthread_create(&threadid, NULL, processthread, (void *)&clientsocket);該線程的回調函數為processthread(),具體如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
void
* processthread(
void
*para)
{
int
clientsocket;
char
buffer[1024];
int
iDataNum =0;
int
recvnum=0;
clientsocket = *((
int
*)para);
printf
(
"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<BEGIN [%d]>>>>>>>>>>>>>>>>>>>>>>>\n"
,clientsocket);
struct
HttpRequest httprequest;
httprequest.content = NULL;
httprequest.path = NULL;
httprequest.path = (
char
*)
malloc
(1024);
httprequest.rangeflag = 0;
httprequest.rangestart = 0;
while
(1)
{
iDataNum = recv(clientsocket,buffer+recvnum,
sizeof
(buffer)-recvnum-1,0);
if
(iDataNum <= 0)
{
close(clientsocket);
pthread_exit(NULL);
return
0;
}
recvnum += iDataNum;
buffer[recvnum]=
'\0'
;
if
(
strstr
(buffer,
"\r\n\r\n"
)!=NULL ||
strstr
(buffer,
"\n\n"
)!=NULL)
break
;
}
printf
(
"request: %s\n"
,buffer);
//解析請求信息並處理請求信息
switch
(getrequest(buffer,&httprequest))
{
case
GET_COMMON:
processgetcommon(clientsocket,&httprequest);
break
;
case
GET_CGI:
processgetcgi(clientsocket,&httprequest);
break
;
case
POST:
processpost(clientsocket,&httprequest);
break
;
case
HEAD:
processhead(clientsocket,&httprequest);
break
;
default
:
break
;
}
insertlognode(pfilelog,&httprequest);
if
(httprequest.path != NULL)
free
(httprequest.path);
if
(httprequest.content != NULL)
free
(httprequest.content);
close(clientsocket);
printf
(
"<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<END [%d]>>>>>>>>>>>>>>>>>>>>>>>\n"
,clientsocket);
pthread_exit(NULL);
}
|
可以看見,在這個線程里面,便開始對請求進行業務分析了。
❷協議解析
這個比較簡單,因為HTTP協議的格式是固定的,所以只用對其按照HTTP的格式進行逐步解析就可以了。
❸文件傳輸
文件傳輸是歸在GET_COMMON類的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
//解析請求信息並處理請求信息
switch
(getrequest(buffer,&httprequest))
{
case
GET_COMMON:
processgetcommon(clientsocket,&httprequest);
break
;
case
GET_CGI:
processgetcgi(clientsocket,&httprequest);
break
;
case
POST:
processpost(clientsocket,&httprequest);
break
;
case
HEAD:
processhead(clientsocket,&httprequest);
break
;
default
:
break
;
}
|
processgetcommon()函數實現如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
void
processgetcommon(
int
s,
struct
HttpRequest *prequest)
{
//先判斷文件是否存在
FILE
*fp = isexistfile(prequest->path);
printf
(
"%s\n"
,prequest->path);
struct
stat finfo;
if
(fp == NULL)
{
responsecode(s,404,prequest);
}
else
{
if
(prequest->rangeflag == 0)
{
stat(prequest->path,&finfo);
prequest->rangetotal = finfo.st_size;
}
responsecode(s,200,prequest);
transferfile(s,fp,prequest->rangeflag,prequest->rangestart,prequest->rangetotal);
fclose
(fp);
}
}
|
它先會判斷有沒有這個文件,如果沒有,就生成404響應碼,如果有,就返回200響應碼,然后首先對prequest->rangeflag進行一個判斷,看是否是斷點續傳,然后便開始傳輸文件,傳輸文件函數transferfile()如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
int
transferfile(
int
s,
FILE
*fp,
int
type,
int
rangstart,
int
totallength)
{
if
(type == 1)
{
//為1,則表示當前從指定的位置傳送文件
fseek
(fp,rangstart,0);
}
int
sendnum = 0;
int
segment = 1024;
while
(!
feof
(fp)&&sendnum < totallength)
{
char
buf[segment];
memset
(buf,0,1024);
int
i = 0;
while
(!
feof
(fp) && i < segment && sendnum+i < totallength)
{
buf[i++] =
fgetc
(fp);
}
if
(sendsegment(s,buf,i) == 0)
return
0;
sendnum += i;
}
return
1;
}
|
可以看見,具體的傳輸文件,是調用sendsegment()函數來實現的。
1
2
3
4
5
6
7
8
9
10
|
int
sendsegment(
int
s,
char
*buffer,
int
length)
{
if
(length <= 0)
return
0;
printf
(
"%s\n"
,buffer);
int
result = send(s,buffer,length,0);
if
(result < 0)
return
0;
return
1;
}
|
而在sendsegment()函數里面,就是用的socket里面的send()函數來實現的。
❹其它功能
對於其它的功能,比如日志操作的,就是屬於文件類的了;響應碼則是屬於對返回信息的一個格式處理,只要按照HTTP協議來就可以了;界面則是用gtk繪制就行了,這個空間比較大,只有綁定相應按鈕和處理函數就行了。
5.源代碼
請到我原博客附件下載:http://infohacker.blog.51cto.com/6751239/1155176
【驅動】linux設備驅動·入門
linux設備驅動
驅動程序英文全稱Device Driver,也稱作設備驅動程序。驅動程序是用於計算機和外部設備通信的特殊程序,相當於軟件和硬件的接口,通常只有操作系統能使用驅動程序。
在現代計算機體系結構中,操作系統並不直接於硬件打交道,而是通過驅動程序於硬件通信。
設備驅動介紹
驅動程序是附加到操作系統的一段程序,通常用於硬件通信。
每種硬件都有自己的驅動程序,其中包含了硬件設備的信息。操作系統通過驅動程序提供的硬件信息與硬件設備通信。由於驅動設備的重要性,在安裝操作系統后需要安裝驅動程序,外部設備才能正常工作。
Linux內核自帶了相當多的設備驅動程序,幾乎可以驅動目前主流的各種硬件設備。
在同一台計算機上,盡管設備是相同的,但是由於操作系統不同,驅動程序是有很大差別的。但是,無論什么系統驅動程序的功能都是相似的,可以歸納為下面三點:
-
初始化硬件設備。
這是驅動程序最基本的功能,初始化通過總線識別設備,訪問設備寄存器,按照需求配置設備地端口,設置中斷等。
-
向操作系統提供統一的軟件接口。
設備驅動程序向操作系統提供了一類設備通用的軟件接口,如硬盤設備向操作系統提供了讀寫磁盤塊、尋址等接口,無論是哪種品牌的硬盤驅動向操作系統提供的接口都是一致的。
-
提供輔助功能。
現代計算機的處理能力越來越強,操作系統有一類虛擬設備驅動,可以模擬真實設備的操作,如虛擬打印機驅動向操作系統提供了打印機的接口,在系統沒有打印機制情況下仍然可以執行打印操作。
Linux內核模塊
Linux內核模塊是一種可以被內核動態加載和卸載的可執行程序。
通過內核模塊可以擴展內核的功能,通常內核模塊被用於設備驅動、文件系統等。如果沒有內核模塊,需要向內核添加功能就需要修改代碼、重新編譯內核、安裝新內核等步驟,不僅繁瑣而且容易保出錯,不易於調試。
內核模塊簡介
Linux內核是一個整體結構,可以把內核想象成一個巨大的程序,各種功能結合在一起。當修改和添加新功能的時候,需要重新生成內核,效率較低。
為了彌補整體式內核的缺點,Linux內核的開發者設計了內核模塊機制。
從代碼的角度看,內核模塊是一組可以完成某種功能的函數集合。
從執行的角度看,內核模塊可以看做是一個已經編譯但是沒有連接的程序。
內核模塊是一個應用程序,但是與普通應用程序有所不同,區別在於:
-
運行環境不同。
內核模塊運行在內核空間,可以訪問系統的幾乎所有的軟硬件資源;普通應用程序運行在用戶空間,可以訪問的資源受到限制。這也是內核模塊與普通應用程序最主要的區別。由於內核模塊可以獲得與操作系統內核相同的權限,因此在編程的時候應該格外注意,可能在用戶空間看到的一點小錯誤在內核空間就會導致系統崩潰。
-
功能定位不同。
普通應用程序為了完成某個特定的目標,功能定位明確;內核模塊是為其他的內核模塊以及應用程序服務的,通常提供的是通用的功能。
-
函數調用方式不同。
內核模塊只能調用內核提供的函數,訪問其他的函數會導致運行異常;普通應用程序可能調用自身以外的函數,只要能正確連接就有運行。
內核模塊的結構
內核編程與用戶空間編程最大的區別就是程序的並發性。
在用戶空間,除多線程應用程序外,大部分應用程序的運行是順序執行的,在程序執行過程中不必擔心被其他程序改變執行的環境。而內核的程序執行環境要復雜的多,即時最簡單的內核模塊也要考慮到並發執行的問題。
設計內核模塊的數據結構要十分小心。由於代碼的可重入特性,必須考慮到數據結構在多線程環境下不被其他線程破壞,對於共享數據更是應該采用加鎖的方法保護。驅動程序員的通常錯誤是假定某段代碼不會出現並發,導致數據被破壞而很難於調試。
linux內核模塊使用物理內存,這點與應用程序不同。應用程序使用虛擬內存,有一個巨大的地址空間,在應用程序中可以分配大塊的內存。內核模塊可以供使用的內存非常小,最小可能小到一個內存頁面(4096字節)。在編寫內核模塊代碼的時候要注意內存的分配和使用。
內核模塊至少支持加載和卸載兩種操作。因此,一個內核模塊至少包括加載和卸載兩個函數。在linux 2.6系列內核中,通過module_init()宏可以在加載內核模塊的時候調用內核模塊的初始化函數,module_exit()宏可以在卸載內核模塊的時候調用內核模塊的卸載函數。
內核模塊的初始化和卸載函數是有固定格式的。
1
2
|
static
int
__init init_func(
void
);
//初始化函數
static
void
__exit exit_func(
void
);
//清除函數
|
這兩個函數的名稱可以由用戶自己定義,但是必須使用規定的返回值和參數格式。
-
static修飾符的作用是函數僅在當前文件有效,外部不可見;
-
__init關鍵字告訴編譯器,該函數代碼在初始化完畢后被忽略;
-
__exit關鍵字告訴編譯器,該代碼僅在卸載模塊的時候被調用;
內核模塊的加載
linux內核提供了一個kmod的模塊用來管理內核模塊。
kmod模塊與用戶態的kmodule模塊通信,獲取內核模塊的信息。
通過insmod命令和modprobe命令都可以加載一個內核模塊。
-
insmod命令加載內核模塊的時候不檢查內核模塊的符號是否已經在內核中定義。
-
modprobe不僅檢查內核模塊符號表,而且還會檢查模塊的依賴關系。
另外,linux內核可以在需要加載某個模塊的時候,通過kmod機制通知用戶態的modprobe加載模塊。
使用insmod加載內核模塊的時候,首先使用特權級系統調用查找內核輸出的符號。通常,內核輸出符號被保存在內核模塊列表第一個模塊結構里。insmod命令把內核模塊加載到虛擬內存,利用內核輸出符號表來修改被加載模塊中沒有解析的內核函數的資源地址。
修改完內核模塊中的函數和資源地址后,insmod使用特權指令申請存放內核模塊的空間。因為內核模塊是工作在內核態的,訪問用戶態的資源需要做地址轉換。申請好空間后,insmod把內核模塊復制到新空間,然后把模塊加入到內核模塊列表的尾部,並且設置模塊標志為UNINTIALIZED,表示模塊還沒有被引用。insmod使用特權指令告訴內核新增加的模塊初始化和清除函數的地址,供內核調用。
內核模塊的卸載
卸載的過程相對於加載要簡單,主要問題是對模塊引用計數的判斷。
一個內核模塊被其他模塊引用的時候,自身的引用計數器會增加1.當卸載模塊的時候,需要判斷模塊引用計數器值是否為0,如果為0才能卸載模塊,否則只能把模塊計數減1.
超級用戶使用rmmod命令可以卸載指定的模塊。
此外,內核kmod機制會定期檢查每個模塊的引用計數器,如果某個模塊的引用計數器值為0,kmod會卸載該模塊。
編寫一個基本的內核模塊
還是以最經典的"Hello World !"為例子吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
/* 內核模塊: ModuleHelloWorld.c */
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE(
"GPL"
);
MODULE_AUTHOR(
"Mystety"
);
/* init function */
static
int
__init hello_init(
void
)
{
printk(KERN_ALERT
"(init)Hello,World!\n"
);
return
0;
}
/* exit function */
static
void
__exit hello_exit(
void
)
{
printk(KERN_ALERT
"(exit)Bye-bye,Mystery!\n"
);
}
module_init(hello_init);
module_exit(hello_exit);
|
編譯內核模塊
編譯內核模塊需要建立一個Makefile,主要目的是使用內核頭文件,因為內核模塊對內核版本有很強的依賴關系。
❶我用的系統是Ubuntu的,首先在系統命令行shell下安裝當前版本的linux內核源代碼
1
|
sudo
apt-get
install
linux-
source
|
編譯內核模塊不需要重新編譯內核代碼,但前提是需要使用當前內核版本相同的代碼。
❷安裝內核代碼完畢后,在ModuleHelloWorld.c同一目錄下編寫Makefile
1
2
3
4
5
6
7
8
|
ifneq ($(KERNELRELEASE),)
obj-m := ModuleHelloWorld.o
else
KERNELDIR :=
/lib/modules/
$(shell
uname
-r)
/build
PWD := $(shell
pwd
)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
|
程序第1行檢查是否定義了KERNELRELEASE環境變量,如果定義則表示該模塊是內核代碼的一部分,直接把模塊名稱添加到 obj-m環境變量即可;
如果未定義環境變量,表示在內核代碼以外編譯,通過設置KERNELDIR和PWD環境變量,然后通過內核腳本編譯當前文件,生成內核模塊文件。
❸Makefile建立完畢后,在shell下輸入"make"回車編譯內核模塊。
❹編譯結束后,生成ModuleHelloWorld.ko內核模塊,通過modprobe或者insmod加載內核模塊。
在加載過程中可以看到hello_init()函數的輸出信息。
❺加載內核模塊成功后,可以使用rmmod命令卸載內核模塊。
卸載模塊的時候,內核會調用內核的卸載函數,輸出hello_exit()函數的內容。
模塊卸載以后,使用lsmod命令查看模塊列表,如果沒有任何輸出,表示HelloWorld內核模塊已經被成功卸載。
1
|
lsmod |
grep
ModuleHelloWorld
|
為內核模塊添加參數
驅動程序常需要在加載的時候提供一個或者多個參數,內模塊提供了設置參數的能力。
通過module_param()宏可以為內核模塊設置一個參數。
定義如下:module_param(參數名稱,類型,屬性)
其中,參數名稱是加載內核模塊時使用的參數名稱,在內核模塊中需要有一個同名的變量與之對應;類型是參數的類型,內核支持C語言常用的基本類型;屬性是參數的訪問權限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE(
"GPL"
);
MODULE_AUTHOR(
"Mystety"
);
static
int
initValue = 0;
//模塊參數 initValue = <int value>
static
char
*initName = NULL;
//模塊參數 initName = <char*>
module_param(initValue,
int
, S_IRUGO);
module_param(initName, charp, S_IRUGO);
/* init function */
static
int
__init hello_init(
void
)
{
printk(KERN_ALERT
"initValue = %d initName = %s \n"
,initValue,initName);
//打印參數值
printk(KERN_ALERT
"(init)Hello,World!\n"
);
return
0;
}
/* exit function */
static
void
__exit hello_exit(
void
)
{
printk(KERN_ALERT
"(exit)Bye-bye,Mystery!\n"
);
}
module_init(hello_init);
module_exit(hello_exit);
|
在原來的代碼中,增加了兩個變量initValue和initName,分別是int類型和char*類型;然后在第8行設置initValue為int類型的參數,第9行設置initName為char*類型的參數。重新編譯,帶參數加載模塊。
從輸出結果可以看出,內核模塊的參數被正確傳遞到了程序中。
總結
驅動其實也沒有傳說中的難,關鍵是需要動手去實踐,相信自己,什么都可以!
本文出自 “成鵬致遠” 博客,請務必保留此出處http://infohacker.blog.51cto.com/6751239/1218461