网络编程,一定离不开套接口;那什么是套接口呢?在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