Linux以其源代碼公開聞名於世,並以其穩定性和可靠性雄霸操作系統領域,在網絡應用技術方面使用得更加廣泛。很久以來它就是Windows的重要對手之一。隨著網絡時代的來臨,Linux的這種優勢已變得更加突出。本文將論述如何在Linux環境下利用Socket實現客戶機/服務器通信。 隨著網絡技術的發展,網絡結構已從過去的主機/終端型、對等型發展到現在廣為使用的客戶機/服務器型。客戶機/服務器模型應用十分廣泛,在Internet上WWW,E-mail,FTP等都是基於這種模型的。在面向連接的通信模式下,服務器打開監聽端口,監聽網絡上其它客戶機向該服務器發出的連接請求,當收到一個請求信號時與該客戶機建立一個連接,之後兩者進行交互式的通信。具體步驟可這樣組織: 服務器: 1.打開一個已知的監聽端口,如smtp為25、pop3為110、ftp為21、telnet為23等。 2.在監聽端口上監聽客戶機的連接請求,如果有客戶機請求連接則建立一個連接線路。 3.在連接線路上與客戶機通信。 4.通信完畢後關閉連接線路並繼續監聽客戶機的連接請求。 客戶機: 1.向指定的服務器主機及端口發出連接請求。 2.當服務器建立連接線路後與服務器進行通信。 3.通信完畢後關閉連接線路。 Linux的許多特性都非常有助於網絡程序設計:首先Linux擁有POSIX.1標准庫函數,socket()、bind()、listen()這幾個庫函數可以非常方便地實現服務器/客戶機模型,有關這幾個庫函數的使用說明將在後邊介紹。其次Linux的進程管理也非常符合服務器的工作原理,所謂進程就是程序在內存中運行時的狀態,可以說進程是動態的程序。在運行著Linux操作系統的計算機中,每一個進程都有一個創建它的父進程,而且它也能創建多個子進程。在服務器端我們可以用父進程去監聽客戶機的連接請求,當有客戶機的連接請求時父進程創建一個子進程去建立連接線路並與客戶機通信,而它本身可繼續監聽其它客戶機的連接請求,這樣就可避免當有一個客戶機與服務器建立連接後服務器就不能再與其它客戶機通信的問題。Linux的另一個特性是它秉承了UNIX設備無關性這一優秀特征,即它通過文件描述符實現了統一的設備接口,磁盤、顯示終端、音頻設備、打印設備甚至網絡通信都使用統一的I/O調用。這三個特性將使Linux下的網絡程序設計變得易如反掌。 上述三個特性的綜合利用將是這篇文章所要講述的真谛所在。下邊的客戶機/服務器實現過程可以說明一二,注意與上文所述步驟的不同。 服務器: 1.打開一個已知的監聽端口。 2.在監聽端口上監聽客戶機的連接請求,當有一客戶機請求連接時建立連接線路並返回通信文件描述符。 4.父進程創建一子進程,父進程關閉通信文件描述符並繼續監聽端口上的客戶機連接請求。 3.子進程通過通信文件描述符與客戶機進行通信,通信結束後終止子進程並關閉通信文件描述符。 客戶機: 1.向指定的服務器主機及端口發出連接請求,請求成功將返回通信文件描述符。 2.通過通信文件描述符與服務器進行通信。 3.通信完畢後關閉通信文件描述符。 Linux的以下幾個庫函數是網絡程序設計的核心部分,它們分別是: (1)socket 調用方式: #include #include int socket(int domain,int type,int protocol); 簡要說明: 此函數為通信創建一個端口,正常調用將返回一個文件描述符,錯誤調用將返回-1。 domain參數有兩種選擇:AF_UNIX與AF_INET,其中AF_INET為Internet通信協議。 type參數也有兩種選擇:SOCK_STREAM用於TCP,SOCK_DGRAM用於UDP。 protocol參數通常為0。 可通過下列代碼為基於TCP協議的Internet通信建立套接口傳輸端口: #include #include #include int sock; if((sock=socket(AF_INET,SOCK_STREAM,0))==-1) perror("Could not create socket"); (2)bind 調用方式: #include #include int bind(int s,const strUCt sockaddr *address,size_t address_len); 簡要說明: bind英文含意是關聯,捆綁。其目的就是把socket返回的套接口端口與網絡上的物理位置相關聯。 bind正常調用返回0,出錯返回-1。此函數有三個參數:其中s為socket調用返回的文件描述符,*address設置了與網絡上的物理位置相關的信息,它的類型是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為端口號,由於使用不同字節順序的機器必須作轉換,故應使用宏命令htons(host to network short)來轉換端口號,sin_addr將置為INADDR_ANY。這三個值設置完成後*address參數才有意義。在編寫代碼時,應先設置*address參數內部各成員變量的值,再調用bind。 (3)listen 調用方式: #include #include int listen(int s,int backlog); 簡要說明: 本函數使socket端口能夠接受從客戶機來的連接請求,正常調用返回0,出錯返回-1。 s參數為socket產生的文件描述符,backlog為所能接受客戶機的最大數目。 socket,bind,listen 三個函數的綜合調用最終在服務器上產生一個能接受客戶機請求的監聽文件描述符s。 (4)accept 調用方式: #include #include int accept(int s,struct sockaddr *address,int *address_len); 簡要說明: 當有客戶機發出連接請求時,此函數初始化這個連接。正常調用返回與客戶機通信的通信文件描述符,出錯返回-1。 參數s為socket調用返回的文件描述符, address將用來存儲客戶機的信息,此信息由accept填入,當與客戶機連接時,客戶機的地址與端口將填到此處。 address_len是客戶機地址長度的字節數,也由accept填入。 (5)connect 調用方式: #include #include int connect(int s,struct sockaddr *address,size_t address_len); 簡要說明: 客戶機調用socket建立傳輸端口後,調用connect來建立與遠程服務器相連的連接線路。 此函數的參數調用同bind。 (6)inet_addr 調用方式: #include #include #include in_addr_t inet_addr(const char *addstring); 簡要說明: 此函數將字符串addstring表示的網絡地址(如192.168.0.1)轉換成32位的網絡字節序二進制值,若成功返回32位二進制的網絡字節序地址,若出錯返回 INADDR_NONE。INADDR_NONE是32位均為1的值(即255.255.255.255,它是Internet的有限廣播地址),故如果要轉換的addstring是255.255.255.255,函數調用將失敗。 (7)fork 調用方式: #include #include pid_t fork(void); 簡要說明: fork的作用是拷貝父進程的內存映象來創建子進程,兩個進程將接著fork後的指令繼續執行。 事實上它返回兩個進程控制號,對於父進程它返回子進程的進程ID,對於子進程它返回0。 可用下邊的代碼調用fork: pid_t childpid; if((childpid=fork())=-1){ perror("The fork failed"); exit(1); } else if(child==0){ 調用子進程; } else if(child>0){ 調用父進程; } 以上介紹了網絡編程的有關庫函數的調用方法,下面舉一個客戶機/服務器程序的小例子具體說明如何設計網絡程序。本例介紹如何查看服務器上的時間和日期,由於daytime服務器的通用端口為13,客戶機程序將通過調用13號端口對服務器上的時間和日期進行操作。 /*timeserve.c*/ /*服務器程序偽代碼如下: 打開daytime監聽端口; while(客戶機與服務器成功連接——成功返回通信文件描述符) { fork() 子進程: { 讀出當前時間; 將當前時間寫入通信文件描述符; 關閉通信文件描述符; } 父進程: 關閉通信文件描述符; } */ #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,以根用戶身份運行服務器程序(設服務器網絡地址為192.168.0.1): server & 然後運行客戶機程序(設服務器網絡地址為192.168.0.1): client 192.168.0.1 在客戶機上就會反映出服務器上當前的時間如(Tue Feb 29 21:46:19 2000)。 以上程序代碼在redhat 6.0上試驗通過。在程序代碼中有關庫函數snprintf、fputs、read、write、close的用法就不在這裡說明了,如想了解這些庫函數的調用方法可到我的網頁http://lzdx.yeah. net/pro_unix.Html去查找。在我的網頁http://lzdx.yeah.net/pro_uici.html中有關於通用Internet接口(UICI)專用庫的介紹,通用Internet接口(UICI)利用Socket庫函數提供了一個簡化的獨立於傳輸的接口,它從整體上簡化了網絡程序設計過程。有興趣的人可到那裡去看看。 最後祝願我們每個人都能編寫出自己的網絡程序。