Linux系統能提供強大可靠的網路服務,並有管理程式對服務進行管理。例如我們熟悉的Web、FTP和電子郵件等,它們既可以單獨運行,也可以被守護進程inetd調用,而且運行得都非常好。但我們不能僅停留在讚歎中,下面就給出兩個服務程式程式和一個客戶程式的例子,介紹服務程式和客戶程式之間是如何溝通的。另外還要編輯配置一些檔案,讓服務程式也能接受服務管理程式管理。
這兩個服務程式功能相同,但一個是獨立服務程式,另一個是被inetd調用的服務程式。這是TCP/IP網路服務的兩大類,這裡將兩個程式放在一起是為了比較程式結構和運行方式。兩服務程式都在Red Hat Linux 7.1和TurboLinux 7.0上調試通過。
獨立伺服器
TCP和UDP是兩大TCP/IP資料轉送方式,套介面是建立伺服器客戶機串連的機制,首先介紹它們建立通訊聯絡的過程,然後給出一個TCP服務程式例子。
1.TCP套介面通訊方式
對於TCP伺服器端,服務程式首先調用建立套介面的函數socket(),然後調用綁定服務IP地址和協議連接埠號碼函數bind()。綁定成功後調用被動監聽函數listen()等待客戶串連,還要調用擷取串連請求函數accept(),並一直阻塞到客戶串連請求的到達,這個函數擷取客戶機IP地址和協議連接埠號碼。
對於TCP用戶端,客戶程式啟動後後調用建立套介面函數socket(),然後調用串連函數connect(),此函數與伺服器通過三向交握建立串連。
伺服器和客戶機建立串連後,就可以使用讀函數read()和寫函數write()收發資料了。資料交換完成後便各自調用關閉套介面函數close()刪除套介面。TCP套介面通訊方式見圖1所示。
圖1 TCP套介面通訊方式
2.UDP套介面通訊方式
UDP程式與TCP的區別是無需建立串連。伺服器首先啟動,然後等待使用者請求。客戶機啟動後便直接向伺服器請求服務,伺服器接到請求後給出應答。
對於UDP伺服器端,服務程式首先調用套介面函數socket(),然後調用綁定IP地址和協議連接埠號碼函數bind()。之後調用函數recvfrom()接收客戶資料,調用sendto()向客戶發送資料。
對於UDP用戶端,客戶機程式啟動後調用套介面函數socket(),然後調用sendto()向伺服器發送資料,調用recvfrom()接收伺服器資料。
雙方資料交換成功後,各自調用關閉套介面函數close()關閉套介面。UDP套介面通訊方式見圖2所示。
圖2 UDP套介面通訊方式
下面給出獨立服務程式的例子。這個程式雖然簡單,但是與複雜程式有著相同的結構。
//程式名:server.c
//功能:伺服器從客戶機讀入一個字元,並將排在此字元後面的字元回送客戶機
//伺服器連接埠:9000
#include "sys/types.h"
#include "sys/socket.h"
#include "stdio.h"
#include "netinet/in.h"
#include "arpa/inet.h"
#include "unistd.h"
int main()
{
int pid; //用於存放fork()執行結果
int server_sockfd,client_sockfd; //用於伺服器和客戶機套介面描述符
int bind_flag,listen_flag; //用於存放bind()和listen()執行結果
int server_address_length,client_address_length; //作為伺服器客戶機地址長變數
struct sockaddr_in server_address; //作為伺服器位址結構變數(含地址和連接埠)
struct sockaddr_in client_address; //作為客戶機地址結構變數(含地址和連接埠)
if((pid=fork())!=0) //用fork()產生新進程
exit(0) ;
setsid() ; //以子進程開始下面的程式
函數socket(),建立一個套介面,成功則返回套介面描述符。
server_sockfd=socket(AF_INET,SOCK_STREAM,0);
if(server_sockfd<0)
{
printf(“socket error /n”);
exit(1);
}
server_address.sin_family=AF_INET;
函數htonl()用於將32位主機位元組順序轉換為網路位元組順序,其中參數INADDR_ANY表示任何IP地址。
server_address.sin_addr.s_addr=htonl(INADDR_ANY);
函數htons()用於將16位主機位元組順序轉換為網路位元組順序,其中的參數是綁定的連接埠號碼,讀者可根據環境自行改動,目的是不與其它服務連接埠衝突。
server_address.sin_port=htons(9000);
server_address_length=sizeof(server_address);
函數bind()用於綁定本地地址和服務連接埠號碼,若調用成功傳回值為0。
bind_flag=bind(server_sockfd,/
(struct sockaddr *)&server_address,/
server_address_length);
if(bind_flag<0)
{
printf(“bind error /n”);
exit(1);
}
函數listen(),指明伺服器的隊列長度,被動等待客戶串連,調用成功傳回值為0。
listen_flag=listen(server_sockfd,5);
if(listen_flag<0)
{
printf(“listen error /n”);
exit(1);
}
while(1)
{
char ch;
函數accept()等待和擷取使用者請求,為每個新串連請求建立一個新的套介面,調用成功返回新套介面描述符。
client_sockfd=accept(server_sockfd,/
(struct sockaddr *)&client_address,/
&client_address_length);
函數read()和write()用於在伺服器和客戶機之間傳送資料,調用成功返回讀和寫的位元組數。
函數close(),用於程式使用完一個套介面後關閉套介面,調用成功傳回值0。其中的參數為accept()建立的套介面的描述符client_sockfd。
read(client_sockfd,&ch,1);
printf(“cli_ch=%c”,ch);
ch++;
write(client_sockfd,&ch,1);
close(client_sockfd);
}
}
程式完成後就可以使用命令進行編譯。在命令列中輸入“gcc -o server server.c”,將server.c編譯成可執行程式server,這時便可用客戶程式進行測試。在命令列執行“./server”啟動服務程式,執行“netstat -na”查看有無server的服務連接埠。如果存在,則執行下面編寫的客戶程式“./client”。不過這僅是手工啟動的方法,下面給出用服務管理程式管理server程式的方法。只要在目錄/etc/rc.d/init.d下放入服務程式的指令碼就能被服務程式讀到。在命令列執行“touch server”建立檔案server,並將檔案屬性改成可執行。在管理程式中並不能看到此服務名,指令檔必須有一些結構才能被管理程式認為是服務程式指令碼。
為了減少工作量,拷貝/etc/rc.d/init.d下指令碼httpd,將拷貝指令碼名命名為server,然後對其編輯。
(1)執行“cp httpd server”。
(2)用文字編輯器vi(其它編輯器亦可)將server開啟進入編輯狀態。首先用字串server替換httpd。然後找到daemon server行,如果編寫的程式放在變數PATH目錄中,不需要修改此行;如果把服務程式放在其它目錄中,就要寫服務的全路徑。例如程式在/root的目錄中,就要寫成daemon /root/server,還要刪除“rm -f /var/run/server.pid”這一行。
(3)執行“chmod 755 server”,將server屬性設定為可執行。
此時就可以用chkconfig、ntsysv等工具,在希望的運行級中增加這個新服務程式,然後測試客戶機與伺服器能否通訊。
被xinetd調用的服務程式
在Linux系統中,有很多服務是被xinetd(較早版本使用的是inetd)超級守護伺服器啟動的。其實凡是基於TCP和UDP的服務都可使用超級守護進程啟動,只是在服務量很大影響效率的情況下不被採用。
1.依賴xinetd啟動的服務建立通訊過程
為了與獨立伺服器程式比較,我們看一下依賴xinetd的伺服器是如何啟動的。
(1)xinetd啟動時讀取/etc/xinetd目錄中的檔案(早期版本為/etc/inetd檔案),根據其中的內容給所有允許啟動的服務建立一個指定類型的套介面,並將套介面放入select()中的描述符集合中。
(2)對每個套介面綁定bind(),所用的連接埠號碼和其它參數來自/etc/xinetd目錄下每個服務的設定檔。
(3)如果是TCP套介面就調用函數listen(),等待使用者串連。如果是UDP套介面,就不需調用此函數。
(4)所有套介面建立後,調用函數select()檢查哪些套介面是活動的。
(5)若select()返回TCP套介面,就調用accept()接收這個串連。如果為UDP,就不需調用此函數。
(6)xinetd調用fork()建立子進程,由子進程處理串連請求。
◆ 子進程關閉所有其它描述符,只剩下套介面描述符。這個套介面描述符對於TCP是accept()返回的套介面,對於UDP為最初建立的套介面。然後子進程連續三次dup()函數,將套介面描述符複製到0、1和2,它們分別對應標準輸入、標準輸出和標準錯誤輸出,並關閉套介面描述符。
◆ 子進程查看/etc/xinetd下檔案中的使用者,如果不是root使用者,就用調用命令setuid和setgid將使用者ID和組ID改成檔案中指定的使用者。
(7)對於TCP套介面,與使用者交流結束後父進程需要關閉已串連套介面。父進程重新處於select()狀態,等待下一個可讀的套介面。
最後調用設定檔中指定的外部服務程式,外部程式啟動後就可與使用者進行資訊傳遞了。
2.為xinetd編寫專門的服務程式
除了獨立服務程式能被xinetd啟動外,還可以為xinetd編寫專門的程式。此處的例子程式與上面server.c功能相同。不過兩者的程式區別是很大的,此例的代碼僅相當於上面傳輸資料的部分。我們還將程式名定為server.c,所以不能放在相同目錄中,同名僅是為了和上面程式對照。
#include "unistd.h"
int main()
{
char ch;
read(0,&ch,1);
ch++;
write(1,&ch,1);
}
將程式編譯成可執行檔,並做些設定就可被xinetd啟動。注意不要和上面的獨立服務程式server一起啟動,因為客戶程式寫得比較簡單,訪問的是固定連接埠,伺服器都設成了相同的連接埠號碼。
(1)編輯/etc/services檔案,在行末增加一條記錄:
server 9000/tcp
(2)在目錄/etc/xinetd.d下編寫檔案server,內容為:
service server
{
disable = no
socket_type = stream
protocol = tcp
wait = no
user = root
server = /home/test/server (此處設定成自己程式所在的目錄)
}
如果使用的是較早版本,則需在/etc/inetd.conf檔案中添加下面的行:
server tcp nowait root /path/to/yourdirectory/server
(3)執行/etc/rc.d/initd.d/xinetd restart重新啟動xinetd伺服器。早期版本執行/etc/rc.d/initd.d/inetd restart重新啟動inetd。
(4)執行netstat -an查看有沒有server程式使用的連接埠號碼,如果有就可使用下面客戶機程式進行測試了。
客戶機程式
下面就客戶機函數做一簡單介紹。
//程式名client.c
/*功能:從客戶的控制台輸入一個字元,然後將這個字元送到伺服器,並將伺服器返回的字元顯示出來*/
#include "sys/types.h"
#include "sys/socket.h"
#include "stdio.h"
#include "netinet/in.h"
#include "arpa/inet.h"
#include "unistd.h"
int main()
{
int sockfd;//
int address_len;
int connect_flag;
struct sockaddr_in address;
int connect_result;
char client_ch,server_ch;
函數socket()用於建立一個套介面,建立成功返回套介面描述符。
sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
printf(“sockfd error /n”);
}
address.sin_family=AF_INET;
address.sin_addr.s_addr=inet_addr(“192.168.0.1”);/*讀者根據自己環境改成伺服器位址*/
address.sin_port=htons(9000);
address_len=sizeof(address);
函數connect()用於與伺服器建立一個主動串連,調用成功傳回值為0。
connect_flag=connect(sockfd,(struct sockaddr *)&address,address_len);
if(connect_flag==-1)
{
perror(“client”);
exit(1);
}
printf(“Input a character :”);
函數scanf()用於從控制台輸入一個字元,並將字元存入client_ch的地址。函數write()和read()用於傳輸資料。函數printf()在客戶機螢幕上顯示伺服器傳回的字元。函數close()關閉套介面。
scanf(“%c”,&client_ch);
write(sockfd,&client_ch,1);
read(sockfd,&server_ch,1);
printf(“character from server : %c/n”,server_ch);
close(sockfd);
exit(0);
}
執行命令“gcc -o client client.c”,將client.c編譯成client。執行“./client”,在程式提示下輸入一個字元,就能看到伺服器傳回的字元。
以上介紹的僅是簡單的例子。平時見到的服務程式遠比它複雜,而且很多是多協議服務程式或是多協議多服務程式。多協議服務程式就是在main()中分別建立供服務的TCP和UDP套介面。為每個服務分別寫出相應程式好處是便於控制,但是這樣每個服務都啟動兩個伺服器,而它們的演算法響應是一樣的,就要耗費不必要的資源,並且出了問題排錯也較困難。多服務是將不同的服務整合在一起由一個程式完成,可用一個數組表示服務,數組中的每一項表示某協議某服務的一種,這樣很容易擴充程式的服務功能。