TCP/IP編程實現遠程文件傳輸 ?? ????在TCP/IP網絡結構中,為了保證網絡安全,網絡人員往往需要在路由器上添加防火墻,禁止非法用戶用ftp等安全危害較大的TCP/IP協議訪問主機。而有時系統維護人員需要用ftp將一些文件從中心機房主機傳到前端網點主機上,比如應用程序的替換升級。如果每次傳輸文件時都要打開防火墻,未免顯得有些繁瑣,要是在自己的應用程序中增加一個專門的文件傳輸模塊,那將是十分愉快的事情。 ? UNIX網絡程序設計一般都采用套接字(socket)系統調用。針對目前十分流行的客戶/服務器模式,其程序編寫步驟如下: 1.Socket系統調用 為了進行網絡I/O,服務器和客戶機兩端的UNIX進程要做的第一件事是調用socket()系統調用,建立軟插座,指明合適的通訊協議。格式為: #include<sys/types.h> #include<sys/socket.h> int?socket(int?family,int?type,int?protocol) 其中:(1)family指明套節字族,其值包括: AF_UNIX ?(UNIX內部協議族) AF_INET ?(Iternet協議) AF_NS?(XeroxNs協議,TCP/IP編程取該值) AF_IMPLINK ?(IMP鏈接層) (2)type?指明套接字類型,取值有: SOCK_STREAM? (流套接字) SOCK_DGRAM? (數據報套接字) SOCK_RAW ? (原始套接字) SOCK_SEQPACKET ?(定序分組套接字) 一般情況下,前兩個參數的組合就可以決定所使用的協議,這時第三個參數被置為0,如果第一個參數為AF_INET,第二個參數選SOCK_STREAM,則使用的協議為TCP;第二個參數選SOCK_DGRAM,則使用的協議為UDP;當第二個參數選SOCK_RAW時,使用的協議為IP。值得指出的是并不是所有的族和類型的組合都是合法的,具體請查閱相關資料。該系統調用若成功則返回一個類似文件描述符,成為套節字描述字,可以像文件描述符那樣用read和write對其進行I/O操作。當一個進程使用完該軟插座時,需用close(<描述符> 關閉(具體見后面內容)。 2.服務器端Bind系統調用 軟插座創建時并沒有與任何地址相關聯,必須用bind()系統調用為其建立地址聯系。其格式為: #include<sys/types.h> #include<sys/socket.h> int?bind(int?socketfd,struct?sockaddr_in?*localaddr,sizeof(localaddr)); 其中:(1)第一個參數socketfd是前步socket()系統調用返回的套節字描述符。 (2)第二個參數被捆向本地地址的一種結構,該結構在sys/netinet/in.h中定義: struct?sockaddr_in{ short?sin_family;/*socket()系統調用的協議族如AF_INET*/ u_short?sin_port;/*網絡字節次序形式的端口號碼*/ struct?in_addr?sin_addr;/*網絡字節次序形式的網絡地址*/ char?sin_zero[8]; } 一臺機器上的每個網絡程序使用一個各自獨立的端口號碼,例如:telnet程序使用端口號23,而ftp文件傳輸程序使用端口號21。我們在設計應用程序時,端口號碼可以由getservbyname()函數從/etc/services庫文件中獲取,也可以由htons?(int?portnum)函數將任意正整數轉換為網絡字節次序形式來得到,有些版本的UNIX操作系統則規定1024以下的端口號碼只可被超級用戶使用,普通用戶程序使用的端口號碼只限于1025到32767之間。網絡地址可以由gethostbyname(char*hostname)函數得到(該函數和getservbyname()一樣都以網絡字節次序形式返回所有在他們結構中的數據),參數hostname為/etc/hosts文件中某一網絡地址所對應的機器名。該函數返回一個類型為hostent的結構指針,hostent結構在netdb.h中定義: struct?hostent{ ?char?*h_name; ?char?**h_aliases; ?int?h_addrtype; ?int?h_length; /*地址長度*/ ?char?**h_addr_list; ?#define?h_addr?h_addr_list[0];/*地址*/ } (3)第三個參數為第二個結構參數的長度,如果調用成功,bind返回0,否則將返回-1并設置errno。 3.服務器端系統調用listen,使服務器愿意接受連接 格式:int?listen(int?socketfd,int?backlong) 它通常在socket和bind調用后在accept調用前執行。第二個參數指明在等待服務器執行accept調用時系統可以排隊多少個連接要求。此參數常指定為5,也是目前允許的最大值。 4.服務器調用accept,以等待客戶機調用connect進行連接。格式如下: int?newsocket=(int?socketfd,struct?sockaddr_in?*peer,int*addrlen); 該調用取得隊列上的第一個連接請求并建立一個具有與sockfd相同特性的套節字。如果沒有等待的連接請求,此調用阻塞調用者直到一連接請求到達。連接成功后,該調用將用對端的地址結構和地址長度填充參數peer和addlen,如果對客戶端的地址信息不感興趣,這兩個參數用0代替。 5.客戶端調用connect()與服務器建立連接。格式為: connect(int?socketfd,struct?sockaddr_in?*servsddr,int?addrlen) 客戶端取得套接字描述符后,用該調用建立與服務器的連接,參數socketfd為socket()系統調用返回的套節字描述符,第二和第三個參數是指向目的地址的結構及以字節計量的目的地址的長度(這里目的地址應為服務器地址)。調用成功返回0,否則將返回-1并設置errno。 6.通過軟插座發送數據 一旦建立連接,就可以用系統調用read和write像普通文件那樣向網絡上發送和接受數據。Read接受三個參數:一個是套節字描述符;一個為數據將被填入的緩沖區,還有一個整數指明要讀的字節數,它返回實際讀入的字節數,出錯時返回-1,遇到文件尾則返回0。Write也接受三個參數:一個是套節字描述符;一個為指向需要發送數據的緩沖區,還有一個整數指明要寫入文件的字節個數,它返回實際寫入的字節數,出錯時返回-1。當然,也可以調用send和recv來對套節字進行讀寫,其調用與基本的read和write系統調用相似,只是多了一個發送方式參數。 7.退出程序時,應按正常方式關閉套節字。格式如下: int?close(socketfd) 前面介紹了UNIX客戶/服務器模式網絡編程的基本思路和步驟。值得指出的是socket編程所涉及的系統調用不屬于基本系統調用范圍,其函數原形在libsocket.a文件中,因此,在用cc命令對原程序進行編譯時需要帶-lsocket選項。 現在,我們可以針對文章開頭提出的問題著手進行編程了。在圖示的網絡結構中,為使中心機房的服務器能和網點上的客戶機進行通信,需在服務器端添加通過路由器1112到客戶機的路由,兩臺客戶機也必須添加通過路由器2221到服務器的路由。在服務器的/etc/hosts文件中應該包含下面內容: 1.1.1.1 server 2.2.2.2 cli1 2.2.2.3 cli2 客戶機的/etc/hosts文件中應該有本機地址信息和服務器的地址信息,如cli1客戶機的/etc/hosts文件: 2.2.2.2 cli1 1.1.1.1 server 網絡環境搭建好后,我們可以在服務器端編寫fwq.c程序,負責接受客戶機的連接請求,并將從源文件中讀取的數據發送到客戶機。客戶機程序khj.c向服務器發送連接請求,接收從服務器端發來的數據,并將接收到的數據寫入目標文件。源程序如下:
/*服務器源程序fwq.c*/?
#include<stdio.h>?
#include<sys/types.h>?
#include<sys/fcntl.h>?
#include<sys/socket.h>?
#include<sys/netinet/in.h>?
#include<netdb.h>?
#include<errno.h>?
main()?
  {?
char?c,buf[1024],file[30];?
int?fromlen,source;?
register?int?k,s,ns;?
struct?sockaddr_in?sin;?
struct?hostent?*hp;?
system(″clear″);?
printf(″\n″);?
??
printf(″\n\n\t\t輸入要傳輸的文件名:″);?
scanf(″%s″,file);?
 if?((source=open(file,O_RDONLY))<0) {?
perror(″源文件打開出錯″);?
exit(1);?
}?
printf(″\n\t\t在傳送文件,稍候…″);?
hp=gethostbyname(″server″);?
 if?(hp==NULL) {?
?perror(″返回主機地址信息錯!!!″);?
?exit(2);?
}?
s=socket(AF_INET,SOCK_STREAM,0);?
 if(s<0) {?
?perror(″獲取SOCKET號失敗!!!″);?
?exit(3);?
}?
sin.sin_family=AF_INET;?
sin.sin_port=htons(1500);/*使用端口1500*/?
bcopy(hp->h_addr,&sin.sin_addr,hp->h_length);?
 if(bind(s,&sin,sizeof(sin))<0) {?
perror(″不能將服務器地址捆綁到SOCKET號上!!!″);?
colse(s);?
exit(4);?
}?
 if(listen(s,5)<0 {?
perror(″sever:listen″);?
exit(5);?
}?
 while(1) {?
 if((ns=accept(s,&sin,&fromlen))<0) {?
perror(″sever:accept″);?
exit(6);?
}?
lseek(source,OL,0);/*每次接受客戶機連接,應將用于讀的源文件指針移到文件頭*/?
write(ns,file,sizeof(file));?/*發送文件名*/?
while((k=read(source,buf,sizeof(buf)))>0)?
?write(ns,buf,k);?
printf(″\n\n\t\t傳輸完畢!!!\n″);?
close(ns);?
}?
close(source);?
exit(0);?
/*客戶機源程序khj.c*/?
#include<stdio.h>?
#include<sys/types.h>?
#include<sys/fcntl.h>?
#include<sys/socket.h>?
#include<sys/netinet/in.h>?
#include<netdb.h>?
#include<errno.h>?
#include?<string.h>?
main()?
 {?
char?buf[1024],file[30];?
char?*strs=″\n\n\t\t正在接收文件″;?
int?target;?
register?int?k,s;?
struct?sockaddr_in?sin;?
struct?hostent?*hp;?
system(″clear″);?
printf(″\n″);?
??
hp=gethostbyname(″server″);?
 if(hp==NULL) {?
perror(″返回服務器地址信息錯!!!″);?
? exit(1);?
}?
s=socket(AF_INET,SOCK_STREAM,0);?
 if(s<0) {?
perror(″獲取SOCKET號失敗!!!″);?
exit(2);?
}?
sin.sin_family=AF_INET;?
sin.sin_port=htons(1500);/*端口號需與服務器程序使用的一致*/?
bcopy(hp->h_addr,&sin.sin_addr,hp->h_length);?
printf(″\n\n\t\t正在與服務器連接…″);?
 if(connect(s,&sin,sizeof(sin),0)<0) {?
perror(″不能與服務器連接!!!″);?
exit(3);?
}?
while((k=read(s,file,sizeof(file)))<=0/*接收文件名*/?
 ?if((target=open(file,o_WRONLY|O_CREAT|O_TRUNC,0644))<0) {?
perror(″不能打開目標文件!!″);?
exit(4);?
}?
strcat(strs,file);?
strcat(strs,″,稍候…″);?
write(1,strs,strlen(strs));?
while((k=read(s,buf,sizeof(buf)))>0)?
write(tatget,buf,k);?
printf(″\n\n\t\t接收文件成功!!!\n″);?
close(s);?
close(target);?
}? 上述程序在Sco?Unix?System?v3.2及Sco?TCP/IP?Rumtime環境下調試通過。
|