网络扫描技术揭秘读书笔记2--常用的网络编程-TCP/IP协议编程

来源:互联网 发布:php魔方加密在线解密 编辑:程序博客网 时间:2024/05/16 14:19

 网络协议和网络编程例程

常用的网络编程---TCP/IP协议编程

1.几个重要概念:

(1). 端口(Port)和套接口

端口正是我们要扫描的对象,具有“开”和“关”两种状态,利用它的开或关状态就可以初步判断一台主机是否提供了某种服务。和端口所在主机的IP地址结合起来,所形成的一个二元组(IP地址,端口地址)就组成了一个套接口

(2). 地址表示顺序

不同的系统在内存存储多字节数据的方式有所不同,而网络传输中,数据存储顺序不一定和系统存储顺序一样,因此为保证系统正确性和可移植性,需要利用系统的转换函数进行转换。

(3). 服务器和客户机

服务器不一定非和一台物理主机相对应,一台主机可以对应多个服务器,多台主机也可用软件捆绑后安装成一台服务器,服务器的主要识别标志就是一台主机运行了哪些服务器软件

 

2.WindowsSocket结构

(1)in_addr和sockaddr_in

一个IPv4的基本数据结构主要有in_addr和sockaddr_in两个,前者表示32位的IP地址,后者是通用的套接口地址结构,它们的结构如下:

struct in_addr 

    in_addr_t s_addr; 

}; 

structsockaddr_in 

    short sin_family;  //协议族,一般是AF_INET

    u_short sin_port;  //端口地址

     struct in_addr sin_addr;  //一个存储IP的结构。

    char sin_zero[8]; 

};

其中真正存储IP结构的sin_addr变量又是一个结构,该结构如下:

structin_addr 

   union 

   { 

      struct{ 

          unsigned  char  s_b1,s_b2,s_b3, s_b4; 

            } S_un_b; 

      struct{ 

          unsigned  short  s_w1,s_w2; 

            } S_un_w; 

      unsigned long S_addr; 

} S_un; 

#define s_addr S_un.S_addr

};

其中S_addr是一个ULONG型的变量,如果赋值的时候,使用的是字符串类型,需要通过调用inet_addr函数将字符串类型转换成网络存储格式的ULONG

有的服务器有多个网卡,此时会有多个IP地址,或是一个网卡配置多个IP地址,而当前的程序并不想只绑定某一个IP地址,这时可以设置S_addrhtonl(INADDR_ANY)

(2)hostent结构

hostent结构用于存储给定主机的信息,例如主机名、IP地址等属性。

hostent结构的定义为:

struct hostent { 

 char  *h_name;  //主机名

 char  * *h_aliases;  //主机的别名

 short  h_addrtype;  //地址的类型

 short  h_length;  //每个地址的长度,以字节为单位

 char * *h_addr_list; // 地址列表

};

h_addr_list地址列表中,每一个地址是以网络存储顺序保存的IP地址的ULONG表示格式

该双重指针其实可以看成一个指针数组,其中h_addr_list[0]表示第一个IP地址,如果有多个IP地址,则h_addr_list[1]表示第二个IP地址,依次类推。其中h_addr_list[0]可以用宏h_addr来表示。

3servent结构

servent结构用于保存或返回给定服务名的名称和服务参数

struct servent { 

  char *  s_name;  //服务名

  char * *  s_aliases;  //服务的别名

  short   s_port;  //服务的端口

  char  *  s_proto; //协议的名称

};

 

3.Windows socket转换类函数

htons函数将计算机存储的USHORT格式转换为网络存储的USHORT格式。

u_short htons(u_short   hostshort);

ntohs函数将网络存储的USHORT格式转换为计算机存储的USHORT格式。

u_short ntohs(u_short   netshort);

htonl函数将计算机存储的ULONG格式转换为网络存储ULONG格式。

u_long htonl(u_long  hostlong); 

ntohl函数将网络存储的ULONG格式转换为计算机存储的ULONG格式。

u_long ntohl(u_long netlong); 

 

inet_ntoa函数将由in_addr结构所表示的网络地址,转换成由字符串表示的IP地址。

char * inet_ntoa(struct in_addr  in); 

inet_addr函数将字符串组成的IP地址串转换成一个ULONG的整数,该整数可用于in_addr结构中,是按网络格式存储的。

unsigned long inet_addr(const char *cp);

 

gethostbyname函数根据主机名读取主机的信息(主要是IP地址)。

struct hostent *gethostbyname(const char*name);

其它函数:

struct HOSTENT * gethostbyaddr(constchar   *addr,int  len, int type);

int gethostname(char *name,intnamelen); 

 

getservbyname函数根据服务名和协议读取服务信息

struct servent  * getservbyname( const char  *name, const char *proto); 

getservbyport函数根据端口和协议读取服务信息

 

4.WindowsSocket通信类函数

(1). WSAStartup函数

#include <Winsock2.h>

#pragramcomment(lib,"Ws2_32.lib")    

int WSAStartup( 

 WORD wVersionRequested, 

 LPWSADATA lpWSAData 

);

WSAStartup函数首先查询当前操作系统是否支持所要求的版本号,完成对Windows Sockets2的初始化工作。要使用Socket2通信,必须首先使用该函数,因此该函数应在逻辑上,处于Socket2所有函数中的第一位。

Eg:
         //
初始化WinSock

         //MAKEWORD(2,2)表示使用WINSOCK2版本.wsaData用来存储系统传回的关于WINSOCK的资料.

         WORDwVersionRequested = MAKEWORD( 2, 2 );

         WSADATAwsaData;

         //WSA(WindowsSocKNDs AsynchronousWindows异步套接字)的启动命令

         if( WSAStartup( wVersionRequested, &wsaData ) != 0 )

                   returnFALSE ;

(2). WSACleanup函数

int WSACleanup (void);

WSACleanup函数完成与socket库绑定的解除,并释放socket库所占用的系统资源。该函数应该作为某次socket操作的最后一个函数,否则之后任何socket操作都会导致出错。

(3) socket函数

socket函数创建一个socket套接字。

SOCKET socket( 

 int     af,  //网络通信协议族,一般情况下用AF_INET,表示选择IPv4协议。

 int     type,  //指明协议的采用连接类型  (SOCK_STREAM,SOCK_DGRAM,SOCK_RAW)

 int     protocol  //protocol:指定要用的协议。如果第二个参数type不是SOCK_RAW,则此参数一般是0,表示采用默认协议。如果typeSOCK_RAW,则此参数就可以指定相应的协议,比如IPPROTO_IP表示采用IP协议,IPPROTO_ICMP表示采用ICMP协议。);

(4). closesocket函数

int closesocket(SOCKET s);

closesocket关闭之前打开的socket套接字。

(5). setsockopt函数

int setsockopt(SOCKET s,int level,intoptname,const char *optval,int optlen);

setsockopt函数设置一个socket的参数选项。通常情况下,默认的选项就够用,但扫描器本身的特点是,几乎每一个程序都需要修改其默认的选项,所以该函数也是socket中一个重要的函数。在调用顺序上,如果setsockopt函数在bind函数之前,则设置的项会直到bind函数时才有效。即使setsockopt功能成功,bind函数也会因为setsockopt函数过早调用而失败

与setsockopt作用相反的一个函数是getsockopt,getsockopt函数的功能是读取一个socket的参数选项,由于其函数参数项数目和各项的意义与setsockopt一样,所以此处不再重复。

s:由socket函数创建的socket套接字。

level:设置选项所定义的级别,当前支持的级别主要有SOL_SOCKETIPPROTO_TCPIPPROTO_IP,分别对应于应用层、传输层(TCP)和网络层(IP的设置。

optname:要设置的选项。这些选项参数一次只能设置一个,所以要同时设置两个或两个以上参数时,需要多次重复调用setsockopt函数,并在每次调用时设置一个参数,如果连续重复设置同一个参数,则以最后一次的设置为有效设置。这些选项参数有:


optval:一个指向选项值的指针,具体的值由optname决定。

optlen:一个指向选项值长度的指针。

Eg:

// 创建原始套接字

         SOCKET    RawSock = socket ( AF_INET, SOCK_RAW,IPPROTO_ICMP );//注意发送ICMP包涉及的套接字类型:IPPROTO_ICMP

// 设置接收ICMP数据包(IP层à IPPROTO_IP)的接收超时为1秒(1000ms)

         intnTime = 1000 ;

         intret = ::setsockopt ( RawSock, IPPROTO_IP,SO_RCVTIMEO,(char*)&nTime, sizeof(nTime));

(6). select函数

int select(int nfds, fd_set *readfds,fd_set*writefds,fd_set *exceptfds,const struct timeval FAR *timeout);

select函数的作用是监视阻塞状态下端口的状态,例如当前是否有数据到达,从而进入读端口的状态。

返回值:如果调用成功,则返回所监视的端口处于“准备”(ready)状态的socket句柄个数,并且将这些句柄保存在一个fd_set结构中;如果超时,则返回0;否则返回SOCKET_ERROR。

nfds:在很多Linux版本下,该参数用于表示监视的句柄个数;在Windows下,该参数被忽略,系统会自动选择合适的数。

readfds:指向要监视的可读句柄集合。

writefds:指向要监视的可写句柄集合。

exceptfds: 指向要监视的异常句柄集合。

timeout:最大的超时值,该值指向一个TIMEVAL结构,如果设置为NULL,则表示进入该函数阻塞状态,直到读到结果或超时。

(7). bind函数可以将一个本地的地址与socket套接字进行绑定。一旦绑定成功,则此后该socket的操作将与该地址有关。

 

int bind( 

 SOCKET s,   //

 const struct sockaddr *name,   //分配给该sockdet套接字的一个地址SOCKADDR结构。

  intnamelen   // SOCKADDR结构的长度。

);

该函数既可用于面向连接的TCP通信中,也可以用于面向非连接的UDP通信中。

(8). listen函数

int listen( 

 SOCKET    s, 

 int   backlog //能连接的最大客户端数。如果设置成SOMAXCONN,则服务提供者尽可能地创建最大的值。

);

listen函数使用socket状态监听状态,并等待其他socket的连接。该函数仅用于面向连接的TCP通信中,UDP通信是不需要listen函数的。

(9). accept函数

SOCKET accept( 

 SOCKET  s,  

 struct sockaddr  *addr, //连接一个sockaddr结构的指针,该指针中保存着远端socket的一些信息  

 int  *addrlen  // sockaddr结构的长度,调用之后,函数会返回实际需要的长度。

);

accept函数允许和接收一个远端的连接,该函数仅用于面向连接的TCP通信中,并且只用于服务器端,用于接收客户端通过connect函数发来的连接申请;面向非连接的UDP通信是不需要处理此函数的。

调用成功后,该函数将会处于阻塞状态,直到有远端的连接,才会返回。从外表上看,程序很像是死掉了,因此除非程序本身没有要求,否则一般建议将此函数放入线程中使用,以避免整个程序像“僵死”一样。

该函数看似简单,其实比较复杂,也是多线程处理效果的关键,首先调用此函数之前,应该已成功地调用了listen函数。然后在调用该函数时,如果调用成功,则返回一个新的socket,所以如果后面服务端的处理很简单,可以在当前线程中用这个新创建的socket进行处理,俗称“短连接”;如果处理很复杂,并且仍在当前线程中处理,则会影响到accept函数对其他线程通过connect进行连接,此时就需要再创建一个线程,由新建的线程,并使用返回的一个socket专门处理此次连接后的各项操作,俗称“长连接”。

返回值:如果调用成功,则返回接收远端通过connect连接后,新创建的一个socket.

Eg:

int len = sizeof(SOCKADDR) ;

sockaddr_in ConnAddr ;

SOCKET ConnSock = accept ( LocalSocket,(SOCKADDR*)&ConnAddr, &len ) ;// 接受连接

(10). connect函数

int connect( 

 SOCKET s, 

 const struct sockaddr  *name,  //指向一个sockaddr的结构指针,该结构中保存了要连接的远端主机的IP地址和端口。

  intnamelen  ); 

connect函数以客户端的身份与远端主机建立连接。在扫描器的应用中,connect是一种简单而有效的连接方式,连接成功,则可以认为对方的端口是打开的

 

(11). send函数

int send( 

 SOCKET        s,  

 const char  *buf, //指向一个发送缓冲区的指针。  

 int       len,   // buff缓冲区的长度,以字节为单位。

 int       flags   发送的方法,一般置0。

);

send函数发送数据到已建立连接的socket上,该函数既可以用于服务器端,也可以用于客户端,但双方都必须是采用TCP连接

返回值:如果调用成功,则返回实际发送的字节数;否则返回SOCKET_ERROR。

(12). recv函数

int recv( 

 SOCKET   s,  

 char  *buf,  

  intlen, 

  intflags 

);

recv函数用于接收从已建立连接的socket上的数据,该函数既可以用于服务器端,也可以用于客户端,但双方都必须是采用TCP连接。参数与上节大同小异。

(13). shutdown函数

int shutdown( 

 SOCKET    s, 

 int       how 

);

shutdown函数禁止当前的发送或接收。

很多程序在关闭socket的时候,在收发完成后,直接就调用closesocket函数了,这样做有的时候会使对方仍处于连接中,而已方已断开

how:要关闭的类型,其中how的取值可以是:


(14). sendto函数

int sendto( 

 SOCKET        s,  

 const char *buf,  

 int       len,  

 int       flags,  

 const struct sockaddr  *to,  

 int       tolen   

);

sendto函数发送数据报到远端的主机指定的端口上。该函数只能用于面向非连接的通信中。

(15).recvfrom函数

int recvfrom( 

 SOCKET        s,  

 char      * buf,  

 int       len,  

 int       flags,  

 struct sockaddr  *from,  

 int    *fromlen  

);

recvfrom函数接收远端发过来的数据报。该函数只能用于面向非连接的通信中。

 

5. 原始套接字

http://book.51cto.com/art/201202/316546.htm

上述Socket函数介绍中,提到一个原始套接字(Raw Socket),如果不使用原始套接字,则无论是发送和接收,系统都会自动处理IP包头、TCP/UDP包头的数据,这时用户只需要关心发送和接收的数据本身即可。这种自动处理虽然方便,但也使系统失去了灵活性。而当使用原始套接字时,如果发送数据,系统会将要发送的数据包的前面加上若干字节数据IP头、TCP/UDP;如果接收数据,系统会将接收到的数据包前面加上数据IP头、TCP/UDP头。

(1). 原始套接字的发送

原始套接字的发送很简单,但实际编写却很麻烦,这主要是因为需自己填充IP头和TCP头的数据内容,并分别计算IP头和TCP头的校验和。由于不再使用Socket提供的IP和TCP头,所以需要通过setsockopt函数告诉系统使用自己定义的IPTCP,并且虽然所填的是面向连接的TCP头,仍然要使用UDP所专用sendto函数,而不是使用send函数,如图2.2所示。


(2). 原始套接字的接收

原始套接字的接收相对复杂,步骤较多,但通常情况下,只要按如图2.3所示的步骤操作即可,每个步骤只有一两行语句,不像“原始套接字的发送”中的填充IP和TCP头那样需要很多行。其中的WSAIoctl函数的SIO_RCVALL参数表示接收经过本机网卡的所有数据包


 

原创粉丝点击