说明:本文所说的相关技术已经很陈旧了,实在不适合用“初探”这个词,但是于我个人而言,却又的确是初探,现总结出来,分享之。本文难免有错漏之处,还请各位高人斧正,别直接拿斧子找我。:)另感谢Neill老大友情支持。
一:connect()
在介绍connect非阻塞模型之前。让我们先来认识一下这个函数——connect()。connect函数将一个流套接字连接到指定IP地址的指定端口上。(cnctloveyu注:connect也可以用于UDP,只是没有3次握手,见http://blog.youkuaiyun.com/cnctloveyu/archive/2009/06/13/4266564.aspx)connect函数的用法:
int connect(SOCKET s,const struct sockaddr FAR* name,int namelen);
参数s指定用于连接的套接字句柄,name参数指向一个sockaddr_in结构,用来指定要连接到的服务器的IP地址和端口,namelen参数则指定sockaddr_in结构的长度。这个参数连接成功的时候,函数返回0,否则返回值是SOCKET_ERROR。connect到目标主机的时候,这个超时是不由我们来设置的。不过正常情况下这个超时都很长,并且connect又是一个阻塞方法,一个主机不能连接,等着connect返回还能忍受,你的程序要是要试图连接多个主机,恐怕遇到多个不能连接的主机的时候,会塞得你受不了的.正常情况下,因为TCP三次握手需要一些时间,也就是说确认连接用的数据包需要一定的往返时间,当连接互联网上的主机时,连接的过程往往需要几秒的时间,如何让connect设置超时时间呢,下面我们就说说select()
二:select()
函数说明:
int select(int n,fd_set * readfds,fd_set * writefds,fd_set * exceptfds,struct timeval * timeout);
select()用来等待文件描述词状态的改变。参数n代表最大的文件描述词加1,参数readfds、writefds 和exceptfds 称为描述词组,是用来回传该描述词的读,写或例外的状况。底下的宏提供了处理这三种描述词组的方式:
FD_CLR(inr fd,fd_set* set);用来清除描述词组set中相关fd 的位
FD_ISSET(int fd,fd_set *set);用来测试描述词组set中相关fd 的位是否为真
FD_SET(int fd,fd_set*set);用来设置描述词组set中相关fd的位
FD_ZERO(fd_set *set); 用来清除描述词组set的全部位
参数:timeout为结构timeval,用来设置select()的等待时间,其结构定义如下
struct timeval
{
time_t tv_sec;
time_t tv_usec;
};
返回值:如果参数timeout设为NULL则表示select()没有timeout。
错误代码:执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
三:代码。
上面简单的说明了connect和select函数的用法以及返回值错误代码之类的,其他的我这里不详细说明了,网上一找一堆,还是直接举个例子吧,下面放一个linux c扫描器的源码,没什么技术含量。只是一个简单的示例而已。源码如下:
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <asm/ioctls.h>
#include <errno.h>
#include <sys/select.h>
#include <fcntl.h>
#define ZERO (fd_set *)0
int main(int argc, char** argv)
{
char *ip;
int startport,endport;
in_addr_t startip;
in_addr_t endip;
int sockfd;
int nCurrentIPval; //,flags;
struct sockaddr_in to; /* 定义to为sockaddr_in类型 */
//int ret;
// unsigned long mode=1;
int flags;
int error;
void msg()
{
system("clear");
printf("scan startip endip startport endport/n");
printf("scan 192.168.1.1 192.168.1.255 20 50/n");
printf("scan 192.168.1.1 20 50/n");
}
if (argc!=4 && argc!=5)
{
msg();
return 0;
}
if( 4 == argc)
{
ip=argv[1];
startport=atoi(argv[2]); /* 转换为长整型 */
endport=atoi(argv[3]);
if (startport<1||endport>65535)
{
printf("port error 1<port<65535/n");
return 0;
}
to.sin_family=AF_INET; /* to结构体中的sin_family定义地址族 */
to.sin_addr.s_addr=inet_addr(ip); /* 填写IP,并转换成in_addr */
struct timeval timeout;
fd_set mask;
int i;
for (i =startport; i <=endport ; i++)
{
to.sin_port=htons(i); /* 将端口转换成网络字节序 */
sockfd=socket(AF_INET,SOCK_STREAM,0);/* 创建套接字 */
if( -1 == sockfd )
{
printf( "Err number: %d/n", errno );
perror( "socket" );
//exit( -1 );
continue;
}
flags=fcntl(sockfd,F_GETFL,0);
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);
int nRet = connect( sockfd, (struct sockaddr *)&to, sizeof(to) );
if ( -1 == nRet )
{
if(errno != EINPROGRESS)
{
perror("connect");
//return -1;
continue;
}
FD_ZERO(&mask);
FD_SET(sockfd,&mask);
timeout.tv_sec=0;
timeout.tv_usec=500;
switch (select(sockfd + 1, NULL, &mask, NULL, &timeout))
{
case -1:
close(sockfd);
printf("select error/n");
perror( "select" );
break;
case 0:
close(sockfd);
break;
default:
error = 0;
socklen_t len = sizeof(int);
if (( 0 == getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len) ))
{
if( 0 == error )
{
printf("%s %d open/n", ip, i);
close(sockfd);
}
}
break;
}
}
else if( 0 == nRet )
{
close(sockfd);
printf("Connected: %s %d open/n", ip, i);
}
}
}
else if( 5 == argc )
{
startip=ntohl(inet_addr(argv[1]));
endip=ntohl(inet_addr(argv[2]));
if( endip < startip )
{
in_addr_t nswap = endip;
endip = startip;
startip = nswap;
}
startport=atoi(argv[3]);
endport=atoi(argv[4]);
if (startport<1||endport>65535)
{
printf("port error 1<port<65535/n");
return 0;
}
to.sin_family=AF_INET;
//to.sin_addr.s_addr=inet_addr(startip);
//to.sin_addr.s_addr=inet_addr(endip);
for (nCurrentIPval =startip; nCurrentIPval<=endip; nCurrentIPval++)
{
to.sin_addr.s_addr = htonl(nCurrentIPval);
printf( "/n---------------------------------/n/n" );
int i;
for (i =startport; i <=endport ; i++)
{
sockfd=socket(AF_INET,SOCK_STREAM,0);
if( -1 == sockfd )
{
printf( "Err number: %d/n", errno );
exit( -1 );
}
to.sin_port=htons(i);
if (connect(sockfd, (struct sockaddr *)&to, sizeof(struct sockaddr)) == 0)
{
printf("%s %d/n",inet_ntoa(to.sin_addr),i);
}
close( sockfd );
}
}
}
return 0;
}
四:Epoll函数
上面的代码在执行大规模并发连接的时候,就会出现个问题,select()函数的 FD_SETSIZE限制。linux系统不能超过1024。也就是说,你同时扫描,不能超过1024个端口。超过就会报错。有人会说用多线程,不就能解决此问题了吗,使用多线程,并发数达到1000时将严重影响系统的性能。此时引出一个函数。EPOLL。epoll的IO效率不随FD数目增加而线性下降,传统的select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。内核实现中epoll是根据每个fd上面的callback函数实现的。只有"活跃"的socket才会主动的去调用callback函数,其他idle状态socket则不会。如果所有的socket基本上都是活跃的---比如一个高速LAN环境,过多使用epoll,效率相比还有稍微的下降。但是一旦使用idleconnections模拟WAN环境,epoll的效率就远在select/poll之上了。
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明:
#define __FD_SETSIZE 1024
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。epoll的接口非常简单,一共就三个函数:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
其他函数说明篇幅原因,也不多说了,具体可以自己去man一下或网上查。直接放出修改过后的源码吧。如下:
#include <stdio.h>
#include <sys/time.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <asm/ioctls.h>
#include <errno.h>
#include <sys/select.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define ZERO (fd_set *)0
#define MAXSOCKFDNUM 10000
#define TIMEOUT (20 * 1000)
struct epoll_datas
{
struct sockaddr servaddr;
int sockfd;
};
struct epoll_datas datas[MAXSOCKFDNUM];
void msg()
{
system("clear");
printf("scan startip endip startport endport/n");
printf("scan 192.168.1.1 192.168.1.255 20 2009/n");
printf("scan 192.168.1.1 20 2009/n");
}
int main(int argc, char** argv)
{
char *ip;
int startport,endport;
in_addr_t startip;
in_addr_t endip;
int sockfd,nRet;
int nCurrentIPval,CurrentPort; //,flags;
struct sockaddr_in to; /* 定义to为sockaddr_in类型 */
int flags;
int error;
int epoll_handle;
socklen_t len;
struct epoll_event ev;
struct epoll_event events[MAXSOCKFDNUM * 2];
if (argc!=4 && argc!=5)
{
msg();
return 0;
}
if( 4 == argc)
{
ip=argv[1];
startport=atoi(argv[2]); /* 转换为整型 */
endport=atoi(argv[3]);
if (startport<1||endport>65535)
{
printf("port error 1<port<65535/n");
return 0;
}
if ((epoll_handle = epoll_create(MAXSOCKFDNUM)) == -1)
{
perror("epoll_create");
return 0;
}/* 创建一个epoll的句柄 */
to.sin_family=AF_INET; /* to结构体中的sin_family定义地址族 */
to.sin_addr.s_addr=inet_addr(ip); /* 填写IP,并转换成in_addr */
int i;
for (CurrentPort =startport; CurrentPort <=endport ; CurrentPort++)
{
to.sin_port=htons(CurrentPort); /* 将端口转换成网络字节序 */
sockfd=socket(AF_INET,SOCK_STREAM,0);/* 创建套接字 */
if( -1 == sockfd )
{
printf( "Err number: %d/n", errno );
perror( "socket" );
}
flags=fcntl(sockfd,F_GETFL,0);
if (flags == -1)
{
perror("fcntl");
return 0;
}
fcntl(sockfd,F_SETFL,flags|O_NONBLOCK);
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
if (epoll_ctl(epoll_handle, EPOLL_CTL_ADD, sockfd, &ev) < 0)
{
perror("epoll_ctl");
return 0;
}
int nRet = connect( sockfd, (struct sockaddr *)&to, sizeof(to) );
if ( -1 == nRet )
{
if(errno != EINPROGRESS)
{
perror("connect");
//return -1;
}
}
if (nRet == 0)
{
ev.events = EPOLLIN | EPOLLOUT | EPOLLET; /* 可读、可写、设为ET模式 */
len = sizeof (struct sockaddr_in);
//ev.data.fd = nRet;
epoll_ctl(epoll_handle,EPOLL_CTL_MOD, sockfd, &ev);
}
if ((nRet = epoll_wait(epoll_handle,events,MAXSOCKFDNUM * 2, TIMEOUT)) == -1)
{
perror("epoll_wait");
}
for (i = 0;i <nRet;i++)
{
if(events[i].events == sockfd)
{
error = 0;
socklen_t len = sizeof(int);
if (( 0 == getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&error,&len) ))
{
if( 0 == error )
{
printf("%s %d open/n", ip, CurrentPort);
}
}
} close(sockfd);
}
}
}}