Linux以其源代碼公開聞名于世,并以其穩(wěn)定性和可靠性雄霸操作系統(tǒng)領(lǐng)域,在網(wǎng)絡(luò)應(yīng)用技術(shù)方面使用得更加廣泛。很久以來它就是Windows的重要對手之一。隨著網(wǎng)絡(luò)時代的來臨,Linux的這種優(yōu)勢已變得更加突出。本文將論述如何在Linux環(huán)境下利用Socket實現(xiàn)客戶機/服務(wù)器通信。
隨著網(wǎng)絡(luò)技術(shù)的發(fā)展,網(wǎng)絡(luò)結(jié)構(gòu)已從過去的主機/終端型、對等型發(fā)展到現(xiàn)在廣為使用的客戶機/服務(wù)器型。客戶機/服務(wù)器模型應(yīng)用十分廣泛,在Internet上WWW,E-mail,F(xiàn)TP等都是基于這種模型的。在面向連接的通信模式下,服務(wù)器打開監(jiān)聽端口,監(jiān)聽網(wǎng)絡(luò)上其它客戶機向該服務(wù)器發(fā)出的連接請求,當(dāng)收到一個請求信號時與該客戶機建立一個連接,之后兩者進行交互式的通信。具體步驟可這樣組織:
服務(wù)器:
1.打開一個已知的監(jiān)聽端口,如smtp為25、pop3為110、ftp為21、telnet為23等。
2.在監(jiān)聽端口上監(jiān)聽客戶機的連接請求,如果有客戶機請求連接則建立一個連接線路。
3.在連接線路上與客戶機通信。
4.通信完畢后關(guān)閉連接線路并繼續(xù)監(jiān)聽客戶機的連接請求。
客戶機:
1.向指定的服務(wù)器主機及端口發(fā)出連接請求。
2.當(dāng)服務(wù)器建立連接線路后與服務(wù)器進行通信。
3.通信完畢后關(guān)閉連接線路。
Linux的許多特性都非常有助于網(wǎng)絡(luò)程序設(shè)計:首先Linux擁有POSIX.1標(biāo)準(zhǔn)庫函數(shù),socket()、bind()、listen()這幾個庫函數(shù)可以非常方便地實現(xiàn)服務(wù)器/客戶機模型,有關(guān)這幾個庫函數(shù)的使用說明將在后邊介紹。其次Linux的進程管理也非常符合服務(wù)器的工作原理,所謂進程就是程序在內(nèi)存中運行時的狀態(tài),可以說進程是動態(tài)的程序。在運行著Linux操作系統(tǒng)的計算機中,每一個進程都有一個創(chuàng)建它的父進程,而且它也能創(chuàng)建多個子進程。在服務(wù)器端我們可以用父進程去監(jiān)聽客戶機的連接請求,當(dāng)有客戶機的連接請求時父進程創(chuàng)建一個子進程去建立連接線路并與客戶機通信,而它本身可繼續(xù)監(jiān)聽其它客戶機的連接請求,這樣就可避免當(dāng)有一個客戶機與服務(wù)器建立連接后服務(wù)器就不能再與其它客戶機通信的問題。Linux的另一個特性是它秉承了UNIX設(shè)備無關(guān)性這一優(yōu)秀特征,即它通過文件描述符實現(xiàn)了統(tǒng)一的設(shè)備接口,磁盤、顯示終端、音頻設(shè)備、打印設(shè)備甚至網(wǎng)絡(luò)通信都使用統(tǒng)一的I/O調(diào)用。這三個特性將使Linux下的網(wǎng)絡(luò)程序設(shè)計變得易如反掌。上述三個特性的綜合利用將是這篇文章所要講述的真諦所在。下邊的客戶機/服務(wù)器實現(xiàn)過程可以說明一二,注意與上文所述步驟的不同。
服務(wù)器:
1.打開一個已知的監(jiān)聽端口。
2.在監(jiān)聽端口上監(jiān)聽客戶機的連接請求,當(dāng)有一客戶機請求連接時建立連接線路并返回通信文件描述符。
4.父進程創(chuàng)建一子進程,父進程關(guān)閉通信文件描述符并繼續(xù)監(jiān)聽端口上的客戶機連接請求。
3.子進程通過通信文件描述符與客戶機進行通信,通信結(jié)束后終止子進程并關(guān)閉通信文件描述符。
客戶機:
1.向指定的服務(wù)器主機及端口發(fā)出連接請求,請求成功將返回通信文件描述符。
2.通過通信文件描述符與服務(wù)器進行通信。
3.通信完畢后關(guān)閉通信文件描述符。
Linux的以下幾個庫函數(shù)是網(wǎng)絡(luò)程序設(shè)計的核心部分,它們分別是:
(1)socket
調(diào)用方式:
#include
#include
int socket(int domain,int type,int protocol);
簡要說明:
此函數(shù)為通信創(chuàng)建一個端口,正常調(diào)用將返回一個文件描述符,錯誤調(diào)用將返回-1。domain參數(shù)有兩種選擇:AF_UNIX與AF_INET,其中AF_INET為Internet通信協(xié)議。type參數(shù)也有兩種選擇:SOCK_STREAM用于TCP,SOCK_DGRAM用于UDP。protocol參數(shù)通常為0。可通過下列代碼為基于TCP協(xié)議的Internet通信建立套接口傳輸端口:
#include
#include
#include
int sock;
if((sock=socket(AF_INET,SOCK_STREAM,0))==-1)
perror("Could not create socket");
(2)bind
調(diào)用方式:
#include
#include
int bind(int s,const struct sockaddr *address,size_t address_len);
簡要說明:
bind英文含意是關(guān)聯(lián),捆綁。其目的就是把socket返回的套接口端口與網(wǎng)絡(luò)上的物理位置相關(guān)聯(lián)。
bind正常調(diào)用返回0,出錯返回-1。此函數(shù)有三個參數(shù):其中s為socket調(diào)用返回的文件描述符,*address設(shè)置了與網(wǎng)絡(luò)上的物理位置相關(guān)的信息,它的類型是struct sockaddr,但在Internet上它是struct sockaddr_in。在socket.h中struct sockaddr_in定義為:
struct sockaddr_in{
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
sin_family一般為AF_INET,sin_port為端口號,由于使用不同字節(jié)順序的機器必須作轉(zhuǎn)換,故應(yīng)使用宏命令htons(host to network short)來轉(zhuǎn)換端口號,sin_addr將置為INADDR_ANY。這三個值設(shè)置完成后*address參數(shù)才有意義。在編寫代碼時,應(yīng)先設(shè)置*address參數(shù)內(nèi)部各成員變量的值,再調(diào)用bind。
(3)listen
調(diào)用方式:
#include
#include
int listen(int s,int backlog);
簡要說明:
本函數(shù)使socket端口能夠接受從客戶機來的連接請求,正常調(diào)用返回0,出錯返回-1。
s參數(shù)為socket產(chǎn)生的文件描述符,backlog為所能接受客戶機的最大數(shù)目。
socket,bind,listen 三個函數(shù)的綜合調(diào)用最終在服務(wù)器上產(chǎn)生一個能接受客戶機請求的監(jiān)聽文件描述符s。
(4)accept
調(diào)用方式:
#include
#include
int accept(int s,struct sockaddr *address,int *address_len);
簡要說明:
當(dāng)有客戶機發(fā)出連接請求時,此函數(shù)初始化這個連接。正常調(diào)用返回與客戶機通信的通信文件描述符,出錯返回-1。參數(shù)s為socket調(diào)用返回的文件描述符,address將用來存儲客戶機的信息,此信息由accept填入,當(dāng)與客戶機連接時,客戶機的地址與端口將填到此處。address_len是客戶機地址長度的字節(jié)數(shù),也由accept填入。
(5)connect
調(diào)用方式:
#include
#include
int connect(int s,struct sockaddr *address,size_t address_len);
簡要說明:
客戶機調(diào)用socket建立傳輸端口后,調(diào)用connect來建立與遠程服務(wù)器相連的連接線路。
此函數(shù)的參數(shù)調(diào)用同bind。
(6)inet_addr
調(diào)用方式:
#include
#include
#include
in_addr_t inet_addr(const char *addstring);
簡要說明:
此函數(shù)將字符串a(chǎn)ddstring表示的網(wǎng)絡(luò)地址(如192.168.0.1)轉(zhuǎn)換成32位的網(wǎng)絡(luò)字節(jié)序二進制值,若成功返回32位二進制的網(wǎng)絡(luò)字節(jié)序地址,若出錯返回 INADDR_NONE。INADDR_NONE是32位均為1的值(即255.255.255.255,它是Internet的有限廣播地址),故如果要轉(zhuǎn)換的addstring是255.255.255.255,函數(shù)調(diào)用將失敗。
(7)fork
調(diào)用方式:
#include
#include
pid_t fork(void);
簡要說明:
fork的作用是拷貝父進程的內(nèi)存映象來創(chuàng)建子進程,兩個進程將接著fork后的指令繼續(xù)執(zhí)行。 事實上它返回兩個進程控制號,對于父進程它返回子進程的進程ID,對于子進程它返回0。
可用下邊的代碼調(diào)用fork:
pid_t childpid;
if((childpid=fork())=-1){
perror("The fork failed");
exit(1);
}
else if(child==0){
調(diào)用子進程;
}
else if(child>0){
調(diào)用父進程;
}
以上介紹了網(wǎng)絡(luò)編程的有關(guān)庫函數(shù)的調(diào)用方法,下面舉一個客戶機/服務(wù)器程序的小例子具體說明如何設(shè)計網(wǎng)絡(luò)程序。本例介紹如何查看服務(wù)器上的時間和日期,由于daytime服務(wù)器的通用端口為13,客戶機程序?qū)⑼ㄟ^調(diào)用13號端口對服務(wù)器上的時間和日期進行操作。

/**//*timeserve.c*/?

/**//*服務(wù)器程序偽代碼如下:?

打開daytime監(jiān)聽端口;?
while(客戶機與服務(wù)器成功連接——成功返回通信文件描述符)?
{?
fork()?
子進程:?
{?
讀出當(dāng)前時間;?
將當(dāng)前時間寫入通信文件描述符;?
關(guān)閉通信文件描述符;?
}?
父進程:?
關(guān)閉通信文件描述符;?
}?
*/?

#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?

int?main(int?argc,char?*argv[])?


{?
int?listenfd,communfd;?
struct?sockaddr_in?servaddr;?
pid_t?childpid;?
time_t?tick;?
char?buf[1024];?

if((listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)?


{?
perror("Could?not?create?socket");?
exit(1);?
}?

servaddr.sin_family=AF_INET;?
servaddr.sin_addr.s_addr=INADDR_ANY;?
servaddr.sin_port=htons(13);?
if(bind(listenfd,(struct?sockaddr?*)&servaddr,sizeof(servaddr))==-1)?


{?
perror("bind?error");?
exit(1);?
}?
if(listen(listenfd,254)==-1)?


{?
perror("listen?error");?
exit(1);?
}?
while(communfd=accept(listenfd,(struct?sockaddr*)NULL,NULL))?


{?
if((childpid=fork())==-1)?


{?
perror("fork?error");?
exit(1);?
}?
else?if(childpid==0)?


{?
tick=time(NULL);?
snprintf(buf,sizeof(buf),"%.24s\r\n",ctime(&tick));?
write(communfd,buf,strlen(buf));?
close(communfd);?
}?
else?if(childpid>0)?
close(communfd);?

}?
exit(0);?
}?



/**//*timeclient.h*/?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?
#include?

int?main(int?argc,char?*argv[])?


{?
int?communfd,n;?
struct?sockaddr_in?servaddr;?
char?recieve[1024],buf[1024];?

if(argc!=2)?


{?
perror("Usage:?client?");?
exit(1);?
}?
if((communfd=socket(AF_INET,SOCK_STREAM,0))==-1)?


{?
perror("socket?error");?
exit(1);?
}?
servaddr.sin_family=AF_INET;?
servaddr.sin_port=htons(13);?
if((servaddr.sin_addr.s_addr=inet_addr(argv[1]))==INADDR_NONE)?


{?
perror("inet_addr?error");?
exit(1);?
}?
if(connect(communfd,(struct?sockaddr*)&servaddr,sizeof(servaddr))==-1)?


{?
perror("connect?error");?
exit(1);?
}?
while((n=read(communfd,recieve,1024))>0)?


{?
recieve[n]=0;?
if(fputs(recieve,stdout)==EOF)?
perror("fputs?error");?
}?
close(communfd);?
exit(0);?
}?

用gcc編譯兩個源程序分別取名為server和client,以根用戶身份運行服務(wù)器程序(設(shè)服務(wù)器網(wǎng)絡(luò)地址為192.168.0.1):
server &
然后運行客戶機程序(設(shè)服務(wù)器網(wǎng)絡(luò)地址為192.168.0.1):
client 192.168.0.1
在客戶機上就會反映出服務(wù)器上當(dāng)前的時間如(Tue Feb 29 21:46:19 2000)。
以上程序代碼在redhat 6.0上試驗通過。在程序代碼中有關(guān)庫函數(shù)snprintf、fputs、read、write、close的用法就不在這里說明了,如想了解這些庫函數(shù)的調(diào)用方法可到我的網(wǎng)頁http://lzdx.yeah. net/pro_unix.html去查找。在我的網(wǎng)頁http://lzdx.yeah.net/pro_uici.html中有關(guān)于通用Internet接口(UICI)專用庫的介紹,通用Internet接口(UICI)利用Socket庫函數(shù)提供了一個簡化的獨立于傳輸?shù)慕涌冢鼜恼w上簡化了網(wǎng)絡(luò)程序設(shè)計過程。有興趣的人可到那里去看看。最后祝愿我們每個人都能編寫出自己的網(wǎng)絡(luò)程序。