网络协议和网络编程例程
常用的网络编程---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_addr为htonl(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来表示。
(3)servent结构
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 Asynchronous,Windows异步套接字)的启动命令
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,表示采用默认协议。如果type是SOCK_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_SOCKET,IPPROTO_TCP和IPPROTO_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函数告诉系统使用自己定义的IP和TCP头,并且虽然所填的是面向连接的TCP头,仍然要使用UDP所专用sendto函数,而不是使用send函数,如图2.2所示。
(2). 原始套接字的接收
原始套接字的接收相对复杂,步骤较多,但通常情况下,只要按如图2.3所示的步骤操作即可,每个步骤只有一两行语句,不像“原始套接字的发送”中的填充IP和TCP头那样需要很多行。其中的WSAIoctl函数的SIO_RCVALL参数表示接收经过本机网卡的所有数据包。