網絡編程,一定離不開套接口;那什么是套接口呢?在Linux下,所有的I/O操作都是通過讀寫文件描述符而產生的,文件描述符是一個
和打開的文件相關聯的整數,這個文件并不只包括真正存儲在磁盤上的文件,還包括一個網絡連接、一個命名管道、一個終端等,而套接口就是系統進程和文件描述
符通信的一種方法。目前最常用的套接口是字:字節流套接口(基于TCP)和數據報套接口(基于UDP),當然還有原始套接口(原始套接口提供TCP套接口
和UDP套接口所不提供的功能,如構造自己的TCP或UDP分組)等,我們這里主要介紹字節流套接口和數據報套接口。
要學習網絡編程,一定離不開網絡庫的函數,在Linux系統下,可以用"man 函數名"來得到這個函數的幫助,不過為了照顧E文不大好的朋友,下面就將常用的網絡函數和用法列出來供大家參考:
1、socket函數:為了執行網絡輸入輸出,一個進程必須做的第一件事就是調用socket函數獲得一個文件描述符。
-----------------------------------------------------------------
#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回:非負描述字---成功 -1---失敗
-----------------------------------------------------------------
|
第一個參數指明了協議簇,目前支持5種協議簇,最常用的有AF_INET(IPv4協議)和AF_INET6(IPv6協議);第二個
參數指明套接口類型,有三種類型可選:SOCK_STREAM(字節流套接口)、SOCK_DGRAM(數據報套接口)和SOCK_RAW(原始套接
口);如果套接口類型不是原始套接口,那么第三個參數就為0。
2、connect函數:當用socket建立了套接口后,可以調用connect為這個套接字指明遠程端的地址;如果是字節流套接口,connect就使用三次握手建立一個連接;如果是數據報套接口,connect僅指明遠程端地址,而不向它發送任何數據。
-----------------------------------------------------------------
#include <sys/socket.h>
int connect(int sockfd,const struct sockaddr * servaddr,socklen_t
addrlen);
返回:0---成功 -1---失敗
-----------------------------------------------------------------
|
第一個參數是socket函數返回的套接口描述字;第二和第三個參數分別是一個指向套接口地址結構的指針和該結構的大小。
這些地址結構的名字均已“sockaddr_”開頭,并以對應每個協議族的唯一后綴結束。以IPv4套接口地址結構為例,它以“sockaddr_in”命名,定義在頭文件<netinet/in.h>;以下是結構體的內容:
------------------------------------------------------------------
struct in_addr {
in_addr_t s_addr; /* IPv4地址 */
};
struct sockaddr_in {
uint8_t sin_len; /* 無符號的8位整數 */
sa_family_t sin_family;
/* 套接口地址結構的地址簇,這里為AF_INET */
in_port_t sin_port; /* TCP或UDP端口 */
struct in_addr sin_addr;
char sin_zero[8];
};
-------------------------------------------------------------------
|
3、bind函數:為套接口分配一個本地IP和協議端口,對于網際協議,協議地址是32位IPv4地址或128位IPv6地址與
16位的TCP或UDP端口號的組合;如指定端口為0,調用bind時內核將選擇一個臨時端口,如果指定一個通配IP地址,則要等到建立連接后內核才選擇
一個本地IP地址。
-------------------------------------------------------------------
#include <sys/socket.h>
int bind(int sockfd,const struct sockaddr * myaddr,socklen_t
addrlen);
返回:0---成功 -1---失敗
-------------------------------------------------------------------
|
第一個參數是socket函數返回的套接口描述字;第二和第第三個參數分別是一個指向特定于協議的地址結構的指針和該地址結構的長度。
4、listen函數:listen函數僅被TCP服務器調用,它的作用是將用sock創建的主動套接口轉換成被動套接口,并等待來自客戶端的連接請求。
-------------------------------------------------------------------
#include <sys/socket.h>
int listen(int sockfd,int backlog);
返回:0---成功 -1---失敗
-------------------------------------------------------------------
|
第一個參數是socket函數返回的套接口描述字;第二個參數規定了內核為此套接口排隊的最大連接個數。由于listen函數第二個參
數的原因,內核要維護兩個隊列:以完成連接隊列和未完成連接隊列。未完成隊列中存放的是TCP連接的三路握手為完成的連接,accept函數是從以連接隊
列中取連接返回給進程;當以連接隊列為空時,進程將進入睡眠狀態。
5、accept函數:accept函數由TCP服務器調用,從已完成連接隊列頭返回一個已完成連接,如果完成連接隊列為空,則進程進入睡眠狀態。
-------------------------------------------------------------------
#include <sys/socket.h>
int accept(int sockfd,struct sockaddr *
cliaddr,socklen_t * addrlen);
回:非負描述字---成功 -1---失敗
-------------------------------------------------------------------
|
第一個參數是socket函數返回的套接口描述字;第二個和第三個參數分別是一個指向連接方的套接口地址結構和該地址結構的長度;該函數返回的是一個全新的套接口描述字;如果對客戶段的信息不感興趣,可以將第二和第三個參數置為空。
6、inet_pton函數:將點分十進制串轉換成網絡字節序二進制值,此函數對IPv4地址和IPv6地址都能處理。
-------------------------------------------------------------------
#include <arpa/inet.h>
int inet_pton(int family,const char * strptr,void * addrptr);
返回:1---成功 0---輸入不是有效的表達格式 -1---失敗
-------------------------------------------------------------------
|
第一個參數可以是AF_INET或AF_INET6:第二個參數是一個指向點分十進制串的指針:第三個參數是一個指向轉換后的網絡字節序的二進制值的指針。
7、inet_ntop函數:和inet_pton函數正好相反,inet_ntop函數是將網絡字節序二進制值轉換成點分十進制串。
-------------------------------------------------------------------
#include <arpa/inet.h>
const char * inet_ntop(int family,const void *
addrptr,char * strptr,size_t len);
返回:指向結果的指針---成功 NULL---失敗
-------------------------------------------------------------------
|
第一個參數可以是AF_INET或AF_INET6:第二個參數是一個指向網絡字節序的二進制值的指針;第三個參數是一個指向轉換后的點分十進制串的指針;第四個參數是目標的大小,以免函數溢出其調用者的緩沖區。
8、fock函數:在網絡服務器中,一個服務端口可以允許一定數量的客戶端同時連接,這時單進程是不可能實現的,而fock就分配一個子進程和客戶端會話,當然,這只是fock的一個典型應用。
-------------------------------------------------------------------
#include <unistd.h>
pid_t fock(void);
返回:在子進程中為0,在父進程中為子進程ID -1---失敗
-------------------------------------------------------------------
|
fock函數調用后返回兩次,父進程返回子進程ID,子進程返回0。
有了上面的基礎知識,我們就可以進一步了解TCP套接口和UDP套接口
1、TCP套接口
TCP套接口使用TCP建立連接,建立一個TCP連接需要三次握手,基本過程是服務器先建立一個套接口并等待客戶端的連接請求;當客戶
端調用connect進行主動連接請求時,客戶端TCP發送一個SYN,告訴服務器客戶端將在連接中發送的數據的初始序列號;當服務器收到這個SYN后也
給客戶端發一個SYN,里面包含了服務器將在同一連接中發送的數據的初始序列號;最后客戶在確認服務器發的SYN。到此為止,一個TCP連接被建立。
下面就用一個例子來說明服務器和客戶是怎么連接的
-------------------------------------------------------------------
/* client.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main(int argc,char *argv[]) {
int sockfd,numbytes;
char buf[100];
struct hostent *he;
struct sockaddr_in their_addr;
int i = 0;
//將基本名字和地址轉換
he = gethostbyname(argv[1]);
//建立一個TCP套接口
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) {
perror("socket");
exit(1);
}
//初始化結構體,連接到服務器的2323端口
their_addr.sin_family = AF_INET;
their_addr.sin_port = htons(2323);
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
bzero(&(their_addr.sin_zero),8);
//和服務器建立連接
if(connect(sockfd,(struct sockaddr *)&their_addr,
sizeof(struct sockaddr))
==-1){
perror("connect");
exit(1);
}
//向服務器發送字符串"hello!"
if(send(sockfd,"hello!",6,0)==-1) {
perror("send");
exit(1);
}
//接受從服務器返回的信息
if((numbytes = recv(sockfd,buf,100,0))==-1) {
perror("recv");
exit(1);
}
buf[numbytes] = '';
printf("result:%s",buf);
close(sockfd);
return 0;
}
--------------------------------------------------------------------
/* server.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
main() {
int sockfd,new_fd;
struct sockaddr_in my_addr;
struct sockaddr_in their_addr;
int sin_size;
//建立TCP套接口
if((sockfd = socket(AF_INET,SOCK_STREAM,0))==-1) {
perror("socket");
exit(1);
}
//初始化結構體,并綁定2323端口
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(2323);
my_addr.sin_addr.s_addr = INADDR_ANY;
bzero(&(my_addr.sin_zero),8);
//綁定套接口
if(bind(sockfd,(struct sockaddr *)&my_addr,sizeof(struct
sockaddr))==-1)
{
perror("bind");
exit(1);
}
//創建監聽套接口
if(listen(sockfd,10)==-1) {
perror("listen");
exit(1);
}
//等待連接
while(1) {
sin_size = sizeof(struct sockaddr_in);
perror("server is run");
//如果建立連接,將產生一個全新的套接字
if((new_fd = accept(sockfd,(struct sockaddr *)
&their_addr,&sin_size))==-1)
{
perror("accept");
exit(1);
}
//生成一個子進程來完成和客戶端的會話,父進程繼續監聽
if(!fork()) {
//讀取客戶端發來的信息
if((numbytes = recv(new_fd,buff,strlen(buff),0))==-1)
{
perror("recv");
exit(1);
}
printf("%s",buff);
//將從客戶端接收到的信息再發回客戶端
if(send(new_fd,buff,strlen(buff),0)==-1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd);
}
close(sockfd);
}
------------------------------------------------------------------
|
現在讓我們來編譯這兩個程序:
root@linuxaid#gcc -o server server.c
root@linuxaid#gcc -o client client.c
|
然后在一臺計算機上先運行服務器程序,再在另一個終端上運行客戶端就會看到結果;如果不運行服務器程序而先運行客戶程序將立即提示"Connect:
Connection refused",這就是TCP套接口的好處,如果是UDP套接口將會有一個延時才會得到錯誤信息(UDP套接口后面有介紹)。
建立一個TCP連接需要三次握手,而斷開一個TCP則需要四個分節。當某個應用進程調用close(主動端)后(可以是服務器端,也可
以是客戶端),這一端的TCP發送一個FIN,表示數據發送完畢;另一端(被動端)發送一個確認,當被動端待處理的應用進程都處理完畢后,發送一個FIN
到主動端,并關閉套接口,主動端接收到這個FIN后再發送一個確認,到此為止這個TCP連接被斷開。
2、UDP套接口
UDP套接口是無連接的、不可靠的數據報協議;既然他不可靠為什么還要用呢?其一:當應用程序使用廣播或多播是只能使用UDP協議;其
二:由于他是無連接的,所以速度快。因為UDP套接口是無連接的,如果一方的數據報丟失,那另一方將無限等待,解決辦法是設置一個超時。
在編寫UDP套接口程序時,有幾點要注意:建立套接口時socket函數的第二個參數應該是SOCK_DGRAM,說明是建立一個
UDP套接口;由于UDP是無連接的,所以服務器端并不需要listen或accept函數;當UDP套接口調用connect函數時,內核只記錄連接放
的IP地址和端口,并立即返回給調用進程,正因為這個特性,UDP服務器程序中并不使用fock函數,用單進程就能完成所有客戶的請求。
另一個構造socket庫,使用對象構造模式。如果您看過wawa老大的動網EXPLOIT與isno大哥的WEBDAVX,您就會發現這些EXPLOITS都是使用這個庫做的,所以如果您想寫EXPLOITS的話,不妨看看此文。
以前我寫的是傳統的C語言‘遺留’下了的SOCKET庫它使用了部分C庫的二進制格式,導致PERL無法完全使用它。而這篇文所介紹的IO::Socket庫是IO::Handle的子類,完全對象編程,一切就會‘自由’很多了......
使用格式與常用方法(父類IO::Handle與IO::File的通用方法就不在下文中說明了):
=============================================================================
-----------------------------------------------------------------------------
導入IO::Socket包:
use IO::Socket;
講解:
IO::Socket下又有兩個子類IO::Socket::INET與IO::Socket::UNIX,我們現在用的當然是IO::Socket::INET了。
-----------------------------------------------------------------------------
new()方法:
SOCKET對象變量=IO::Socket::INET->new(SOCKET變量值);
實例:
$sock=IO::Socket::INET->new('192.168.1.2:23');
講解:
所有的PERL對象編程都把對象‘形象化’為某個變量,這里的SOCKET句柄對象也不例外,調用此方法的返回值便為SOCKET對象變量
了。這里使用參數為簡單參數模式,在雙引號或但引號內的socket地址結構為'主機IP或域名:端口號或服務名稱',也可以是'主機IP或域名:服務名
稱(端口號)'。
除了最簡單的單參數調用外,new方法還有很多參數可以選擇性調用的,下面就對這些參數作出一個簡單的概括吧:
***********************************************************************
參數 描述 值類型
±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
PeerAddr 遠程主機的地址 主機地址[:端口或服務]
PeerHost 與PeerAddr相同
PeerPort 遠程端口或服務 端口或服務
LocalAddr 本地地址 主機地址[:端口或服務]
LocalHost 與LocalAddr相同
LocalPort 本地端口 端口或服務
Proto 所使用的協議 協議名或協議號
Type 套接字類型 SOCK_STREAM/SOCK_DGRAM...
Listen 監聽的隊列長度 整形數
Reuse 用于避免重啟時BIND時間間隙 布爾值
Timeout 超時值 整形數
MultiHomed 用于連接多IP地址 布爾值
***********************************************************************
參數PeerAddr(遠程主機地址)與PeerHost(遠程主機名)基本相同,調用方式也相同,其值格式除了標準的格式外,還可以加':'號后再加端口或服務,這樣的的話,后面的參數PeerPort(遠程主機端口或服務)的值就無效了。
參數PeerPort(遠程主機端口或服務),其值的格式可以是端口,還可以是服務名,更可以是‘組合’,如:"telnet(23)";當PeerAddr(遠程主機地址)或
PeerHost(遠程主機名)的值格式中指明了端口,再調用此參數時,此參數的值無效。
參數LocalAddr(本地主機地址)、LocalHost(本地主機名)、LocalPort(本地主機端口或服務)之間的關系與調用方
式與上面介紹的三個參數PeerAddr(遠程主機地址)、PeerHost(遠程主機名)、PeerPort(遠程主機端口或服務)相當。
還有一種情況,就是如果只定義了LocalPort(本地主機端口或服務),而沒有定義LocalAddr(本地主機地址)或
LocalHost(本地主機名),那IO::Socket會將本地機器的地址的值默認為INADDR_ANY通配符,也就是不定義本地主機的地址值的話
就定義為允許所有接口。
Proto(協議類型)的值可以用兩種方式表示。一種是直接的字符串表示方式,如:
proto=>"tcp"
表示該協議類型為TCP。第二種方式就是直接使用協議號了,EGP---8、HMP---20、ICMP---1、RAW---255、RDP-
--27、RVD---66、TCP---6、UDP---17、XNS-IDP---22、其他---22、ALL---0;也可以使用
getprotobyname函數加協議名為參數調用獲的該值,如:
proto=>getprotobyname('tcp')
該形式也表示該協議的類型為TCP。建議還是使用第一種方式比較方便。
Type(套接字類型)的值通常為SOCK_STREAM(流套接字)、SOCK_DGRAM(數據報套接字)、SOCK_RAW(原始套接字)等,不用說大家都知道,TCP用的是流套接字,UDP用的是數據報套接字,構造IP包用的是原始套接字。
如果上面的參數Proto(協議類型)與Type(套接字類型)的值都不定義的話,IO::Socket::INET就會通過程序中上下‘文’部分猜估它們的值,猜估不到的話就會默認為'tcp'。
參數Listen(監聽隊列的長度)的值是一個整形數。它代表能接受的連接主機數量。如果您要構造服務端的話,Listen這個步驟是必不可少的。
調用Reuse(在綁定前設置SO_REUSEADDR)可以免去服務器在終止到重啟之間的所停留的時間。
Timeout(超時值)以秒計算,用于連接中的connect與accept這兩個步驟,調用目的是為了在連接遠程主機不可到達時限制連接的掛起時間。
MultiHomed(用于連接多IP地址)的值是一個布爾值,當其值為真時,如果要連接的主機擁有多個IP地址,則本機的new方法調用gethostbyname()窮舉其所有IP地址,直到能成功調用為止。
從樓上的列表中可以看到IO::Socket與傳統C庫的Socket API接口在調用上有什么不同了:
1)控制范圍不同。C庫提供的接口在生成SOCKET句柄時只能控制的只有域、套接字類型、協議這幾個參數。而IO::Socket接口的創建語句(調用new方法)幾乎能決定這個套接字的所有參數。
2)調用所使用的‘協議’定義部分不同。IO::Socket接口調用new方法中的參數'Proto'的值可以直接定義為'tcp',這比傳統C庫的Socket定義更為簡便。
3)IO::Socket在定義時能直接定義本地主機地址、本地端口與遠程主機地址、遠程端口在一個Socket中,如果是這種情況的服務端
就無需調用accept了,在I/O讀寫部分可以直接向這個Socket進行讀寫操作,而無需再定義遠程客戶端的Socket了。
-----------------------------------------------------------------------------
accept()方法:
遠程連接套接字對象變量=服務端套接字對象變量->accept();
實例:
$remote_sock=$sock->accept();
講解:
此方法的調用環境與傳統C中SOCKET庫調用原理一樣,用于服務端的等待監聽過程。無參數,返回值為遠程連接的套接字對象變量。調用此方法
也是一個生成套接字的過程,只不過此套接字為遠程連接的套接字而已,它以對象變量方式存在,據有與本地套接字變量相同的屬性與方法。
accept()方法在IO::Socket包里還提供另一種雙返回值的調用方法:
(遠程連接套接字對象變量,遠程主機壓縮地址變量)=服務端對象變量->accept();
實例:
($remote_sock,$remote_addr)=$sock->accept();
講解:
與樓上一個返回值的調用方式基本相同,只是返回值中多了一個變量而已,返回值中多了個變量------遠程主機壓縮地址變量。
-----------------------------------------------------------------------------
bind()方法:
返回值變量=服務端套接字對象變量->bind(本地端口號,本地主機網絡地址);
實例:
$result=$sock->bind(80,'127.0.0.1');
講解:
bind方法用于在服務器端綁定主機的地址與端口。它使用的兩個參數都為未壓縮值,第一個為端口,第二個為主機的網絡適配器接口地址(可以使
用默認的保留字INADDR_ANY,此保留字包括了主機的所有網絡適配器接口地址,調用它時,它會以窮舉的方法窮舉所有的網絡適配器接口地址,直到找到
為止);返回值為布爾值,用于檢測這次調用是否成功。
-----------------------------------------------------------------------------
connect()方法:
返回值變量=套接字對象變量->connect(壓縮地址變量);
實例:
$result=$sock->connect($pack_addr);
講解:
常用于TCP連接(也可用于UDP,不過不常用),調用將向遠程主機發送連接請求。參數‘壓縮地址變量’為sockaddr_in形式值,返
回值為布爾值。若調用此方法則建立IO::Socket::INET對象時不能賦予參數'PeerAddr'或'PeerHost'、
'PeerPort',否則就會出現程序邏輯錯誤。
connect()方法也有雙參數調用方式,使用起來更簡單:
返回值變量=套接字對象變量->connect(遠程端口號,遠程主機地址);
實例:
$result=$sock->connect($remote_port,$remote_host);
講解:
調用的目的與樓上單參數的調用方式相當。第一個參數為遠程需要連接的主機的端口(等于new方法的參數'PeerPort'),第二個參數為需要連接的主機地址(等于new方法的參數'PeerAddr'或'PeerHost'),返回值為布爾值。
-----------------------------------------------------------------------------
listen()方法:
返回值變量=套接字對象變量->listen(請求隊列的最大長度值);
實例:
$result=$sock->listen(20);
講解:
TCP服務端不可缺少的方法。單參數,參數為此服務端接受遠端請求隊列的最大長度值,返回值為布爾值。調用此方法等同于在建立IO::
Socket::INET對象時定義參數'Listen'的值,所以若在new方法中定義了參數'Listen'再調用此方法的話就會出現‘程序定義沖突
’這樣的邏輯錯誤了。
-----------------------------------------------------------------------------
shutdown()方法:
返回值變量=套接字對象變量->shutdown(控制參數);
實例:
$result=$sock->shutdown(2);
講解:
此方法是除了close外的另一個關閉套接字對象的方法。單參數,參數值為外加參數定義,下為此方法的外加參數列表:
***********************************************************************
參數值 描述
±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
0 關閉對象套接字的讀操作
1 關閉對象套接字的寫操作
2 關閉對象套接字的所有操作
***********************************************************************
其返回值為布爾值。
-----------------------------------------------------------------------------
send()方法:
成功發送的數據值變量=套接字對象變量->send(發送數據,標志值,目標地址值);
實例:
$succ_bytes=$sock->send('hihi\n',0,$pack_host);
講解:
send方法是專門為SOCKET發送數據的特殊方法,調用格式與參數格式也基本與C庫的SOCKET
API中的send函數相同。第一個參數是需要發送的數據;第二參數是標志值,不添的話默認為0;第三個參數通常只用于UDP連接,是需要連接的
sockaddr_in格式地址值(注意:當第三個參數有必要一定要寫時,第二個參數也一定要加上);返回值為成功發送的數據值大小(以byte為單
位)。
-----------------------------------------------------------------------------
recv()方法:
壓縮遠程地址地址=套接字對象變量->recv(接收數據變量,接收數據值長度,標志值);
實例:
$remote_pack_address=$sock->recv($mem,100,0);
講解:
recv方法是專門為SOCKET接收數據的特殊方法,調用格式與參數格式也與C庫的SOCKET API基本一樣。第一個參數是存放接收后的數據的變量值;第二個參數是接收的數據的長度值;第三個參數是標志值,默認為0就可以了(省略此值不填,系統默認也為0)。
-----------------------------------------------------------------------------
===================================================================================
IO::Socket接口的常用方法就介紹完了,不過還有一個問題是需要注意的:
作為一個簡單的客戶端,它的步驟只需要先調用new方法,然后立刻就可以進行基本I/O操作(使用print與getline等基本I/O方法)了,最后
只需調用close方法結束會話,那么整個SOCKET會話就算完成了。
典型使用例子:
wawa's dvbbs exploit:
http://haowawa.8866.org/wawa/new/tech/dvbbs.pl
isno's webdavx exploit:
http://www.xfocus.net/tools/200304/webdavx3.pl
究竟C庫的傳統SOCKET接口與本文介紹的IO::Socket接口哪個比較好用呢???我只能回答你:"蘿卜青菜,各有所愛"......:P