在前一章中我们介绍了有关套接字创建的各种细节,本章我们将讲解给套接字分配IP地址和端口号。
分配给套接字的IP地址和端口号
网络地址(Internet Address)
大家如果现在实在学习网络编程的话,那么肯定是掌握了一定的计算机网络的基础。像网络地址这一类的东西应该是烂熟于心了,下面我就来简单的介绍一下。
IP地址有两种表达形式:
1.IPv4 4字节地址族
2.IPv6 16字节地址族
一定要记住IPv4和IPv6不只是在地址长度不同,在其具体的协议实现上是有很大程度的不同的。IPv6出现的主要目的是为了解决由于计算机数量的暴增导致IP地址可能出现不足的问题的,现在IPv6的地址范围可以让地球上任何一个沙子都拥有IP地址。
让我们继续说回到IPv4上,IPv4标准的4字节IP地址分为网络地址和主机(指计算机)地址,且分为A、B、C、D、E等类型。
类型 | 地址范围 | 网络地址位数 | 主机地址位数 | 可分配的网络数量 | 每个网络可分配的主机数量 |
---|---|---|---|---|---|
A | 1.0.0.0 - 126.255.255.255 | 8 | 24 | 128 | 16,777,216 |
B | 128.0.0.0 - 191.255.255.255 | 16 | 16 | 16,384 | 65,536 |
C | 192.0.0.0 - 223.255.255.255 | 24 | 8 | 2,097,152 | 256 |
D | 224.0.0.0 - 239.255.255.255 | 未分配 | 未分配 | 未分配 | 未分配 |
上方的表可以非常简单的理解四个类型。
现在我来举个例子来具体理解一下网络地址和主机地址的含义。假设向WWW.SEMI.COM公司传输数据,该公司内部构建了局域网,把所有计算机连接起来。因此,首先应向SEMI.COM网络传输数据,也就是说,并非一开始就浏览所有4字节IP地址,进而找到目标主机;而是仅浏览4字节IP地址的网络地址,先把数据传到SEMI.COM的网络。SEMI.COM网络(构成网络的路由器)接收到数据后,浏览传输数据的主机地址(主机ID)并将数据传给目标计算机。
网络地址分类和主机地址边界
只需通过地址的第一个字节即可判断网络地址占用的字节数,因为我们根据地址的边界
区分网络地址,如下所示。
□A类地址的首字节范围:0~127
□B类地址的首字节范围:128-191
□C类地址的首字节范围:192~223
还有如下这种表述方式。
□A类地址的首位以0开始
□B类地址的前2位以10开始
□C类地址的前3位以110开始
正因如此,通过套接字收发数据时,数据传到网络后即可轻松找到正确的主机。
区分套接字的端口号
当我开始学习计算机网络的时候觉得端口这个东西很难理解,当时我就觉得端口不就是口嘛,就和路由器上的口一样,有多少个口就有多少个端口。现在想想这个想法非常的幼稚。实际上IP地址和端口号这两个东西是相伴而生的,IP地址就是让一个数据通过漫长线路路由到你的计算机上,当数据到达你的计算机上时IP地址的工作就结束了。既然IP地址可以让数据到达你的计算机上,那么这个数据可能是来自于不同的应用,那么该如何区分不同的应用呢?没错这时候就要轮到端口了,端口的作用就是用来区分不同的套接字,因此无法将1个端口号分配给不同套接字。另外,端口号由16位构成,可分配的端口号范围是0-65535。但0-1023是知名端口(Well-known PORT),一般分配给特定应用程序,所以应当分配此范围之外的值。另外,虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。例如:如果某TCP套接字使用9190号端口,则其他TCP套接字就无法使用该端口号,但UDP套接字可以使用。总之,数据传输目标地址同时包含I地址和端口号,只有这样,数据才会被传输到最终的目的应用程序(应用程序套接字)。
地址信息的表示
在前几章内容中,我们调用的bind函数中有一些我们看不懂的参数,但我们知道bind函数的作用是将某一个IP地址和端口绑定到一个套接字上,那么其中我们看不懂的参数肯定是包含某个IP地址和端口号的。接下来我们就来介绍一下参数其中的内容。
struct sockaddr_in{
sa_family_t sin_family;//地址族
uint16_t sin_port;//16位TCO/UDP端口号
struct in_addr sin_addr;//32位IP地址
char sin_zero[8];//不使用
};
该结构体中提到的另一个结构体 in_addr定义如下,它用来存放32位IP地址。
struct in_addr{
In_addr_t s_addr;//32位IPv4地址
};
数据类型名称 | 数据类型说明 | 声明的头文件 |
int8_t | signed 8-bit int | sys/types.h |
uint8_t | unsigned 8-bit int (unsigned char) | sys/types.h |
int16_t | signed 16-bit int | sys/types.h |
uint16_t | unsigned 16-bit int(unsigned short) | sys/types.h |
int32_t | signed 32-bit int | sys/types.h |
uint32_t | unsigned 32-bit int(unsigned long) | sys/types.h |
sa_family_t | 地址族 | sys/socket.h |
socklen_t | 长度 | sys/socket.h |
in_addr_t | IP地址,声明为uint32_t | netinet/in.h |
in_port_t | 端口号,声明为uint16_t | netinet/in.h |
看到这么长的类型表,有人不禁会问,为什么要高出这么长的类型名呢?其中一个很大的原因就是移植性的问题,如果适用于一个32位计算机的代码搬到64位的计算机上运行可定会出现由于位数不同导致的int被解释为不同的字节大小,这种问题是万万不可发生的。因此如果使用int32_t类型的数据,就能保证任何时候都占用4字节,即使转到不同字节的计算机上。
结构体sockaddr_in的成员分析
成员sin_family
每种协议族适用的地址族均不同。比如,IPv4使用4字节地址族,IPv6使用16字节地址族。
地址族(Address Family) | 含义 |
AF_INET | IPv4网络协议中使用的地址族 |
AF_INET6 | IPv6网络协议中使用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
有这个成员只是为了解释接下来的成员而已
成员sin_port
该成员保存16位端口号,重点在于,它以网络字节序保存。
成员sin_addr
该成员保存32位地址信息,且也以网络字节序保存。为理解好该成员,应同时观察结构体
in_addr。但结构体in_addr明为uint_32,因此只需当作32位整数型即可。
成员sin_zero
无特殊含义。只是为使结构体sockaddr_in的大小和sockaddr结构体保持一致而插入的成员。必需填充为0,否则无法得到想要的结果。后面会另外讲解sockaddr。从之前介绍的代码也可看出,sockaddr_in结构体变量地址值将以如下方式传递给bind函数。稍后将给出关于bind函数的详细说明,希望各位重点关注参数传递和类型转换部分的代码。
struct sockaddr_in serv_addr;
if(bind(serv_sock,(struct sockaddr *)&serv_addr, sizeof(serv_addr))==-1)
error_handling("bind() error");
此处重要的是第二个参数的传递。实际上,bind函数的第二个参数期望得到sockaddr结构体变量地址值包括地址族、端口号、IP地址等。
struct sockaddr{
sa_family_t sin_family;//地址族
char sa_data[14];//地址信息
};
这个结构体结构相对于sockaddr_in来说,他将后三个成员都放入sa_data之中。而这对于包含地址信息非常麻烦,继而有了新的结构体sockaddr_in。但是最后还是要转换为sockaddr型的结构体变量,再传递给bind函数即可。
网络字节序与地址变换
字节序与网络字节序
在计算机组成原理中说明了CPU内存保存数据有两种方式
方式1:高位字节存放到低位地址
方式2:高位字节存放到高位地址
0x12和0x34构成的大端序系统值与0x34和0x12构成的小端序系统值相同。换言之,只有改变数据保存顺序才能被识别为同一值。大端序系统传输数据0x1234时未考虑字节序问题,而直接以0x12、0x34的顺序发送。结果接收端以小端序方式保存数据,因此小端序接收的数据变成0x3412,而非0x1234。正因如此,在通过网络传输数据时约定统一方式,这种约定称为网络字统一节序(Network Byte Order),非常简单统一为大端序。因此,所有计算机接受数据时应识别该数据时网络字节格式,小端序系统传输数据时应转换为大端序的排列方式。
接下来介绍帮助转换字节序的函数
□ unsigned short htons(unsigned short)
□ unsigned short ntohs(unsigned short)
□ unsigned long htonl(unsigned long)
□ unsigned long ntohl(unsigned long)
通过函数名应该能掌握其功能,只需了解以下细节。
□ htons中的h代表主机(host)字节序。
□ htons中的n代表网络(network)字节序。
另外,s指的是short,l指的是long(Linux中long类型占用4个字节,这很关键)。因此,htons是h、to、n、s的组合,也可以解释为“把short型数据从主机字节序转化为网络字节序"。通常,以作为后缀的函数中,s代表2个字节short,因此用于端口号转换;以l作为后缀的函数中,代表4个字节,因此作为IP地址转换。
通过以下示例说明以上函数调用过程
#include<stdio.h>
#include<arpa/inet.h>
int main(int argc,char *argv[]){
unsigned short host_port=0x1234;
unsigned short net_port;
unsigned long host_addr=0x12345678;
unsigned long net_addr;
net_port=htons(host_port);
net_addr=htonl(host_addr);
printf("Host ordered port: %#x \n",host_port);
printf("Network ordered port: %#x \n",net_port);
printf("Host ordered address: %#lx \n", host_addr);
printf("Network ordered address:%#lx \n", net_addr);
return 0;
}
网络地址的初始化与分配
将字符串信息转换为网络字节序的整数型
sockaddr_in中保存地址信息的成员为32位整数型。因此,为了分配I地址,需要将其表示为32位整数型数据。这对于只熟悉字符串信息的我们来说实非易事。各位可以尝试将IP地址
201.211.214.36转换为4字节整数型数据。
对于IP地址的表示,我们熟悉的是点分十进制表示法(Dotted Decimal Notation),而非整数型数据表示法。幸运的是,有个函数会帮我们将字符串形式的I地址转换成32位整数型数据。此函数在转换类型的同时进行网络字节序转换。
#include<arpa/inet.h>
in_addr_t inet_addr(const char*string);//成功时返回32位大端序整数型值,失败时返回INADDR_NONE
如果向该函数传递类似“211.214.107.99”的点分十进制格式的字符串,它会将其转换为32位整数型数据并返回。当然,该整数型值满足网络字节序。另外,该函数的返回值类型in_addr_t在内部声明为32位整数型。下列示例表示该函数的调用过程。
#include<stdio.h>
#include<arpa/inet.h>
int main(int argc,char*argv[]){
char*addr1="1.2.3.4";
char*addr2="1.2.3.256";
unsigned long conv_addr=inet_addr(addr1);
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n",conv_addr);
conv_addr=inet_addr(addr2);
if(conv_addr==INADDR_NONE)
printf("Error occureded \n");
else
printf("Network ordered integer addr: %#lx \n\n", conv_addr);
return 0;
}
从运行结果可以看出,inet_addr函数不仅可以把IP地址转成32位整数型,而且可以检测无效
的IP地址。另外,从输出结果可以验证确实转换为网络字节序。
inet_aton函数与inet_addr函数在功能上完全相同,也将字符串形式IP地址转换为32位网络字
节序整数并返回。只不过该函数利用了in_addr结构体,且其使用频率更高。
#include <arpa/inet.h>
int inet_aton(const char * string, struct in_addr * addr);
//成功时返回1(true),失败时返回0(false)。
通过以下示例来了解inet_aton函数调用过程。
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error _handling(char *message);
int main(int argc, char *argv[]){
char *addr="127.232.124.79";
struct sockaddr_in addr_inet;
if(!inet_aton(addr, &addr_inet.sin_addr))
error _handling("Conversion error");
else
printf("Network ordered integer addr: %#x \n",addr _inet.sin_addr.s_addr);
return 0;
}
下面再介绍一个做的功能完全和上述函数相反的函数
#include <arpa/inet.h>
char * inet_ntoa(struct in_addr adr);//成功时返回转换的字符串地址值,失败时返回-1。
该函数将通过参数传入的整数型IP地址转换为字符串格式并返回。但调用时需小心,返回值类型为char指针。返回字符串地址意味着字符串已保存到内存空间,但该函数未向程序员要求分配内存,而是在内部申请了内存并保存了字符串。也就是说,调用完该函数后,应立即将字符串信息复制到其他内存空间。因为,若再次调用inet_ntoa函数,则有可能覆盖之前保存的字符串信息。总之,再次调用inet_ntoa函数前返回的字符建地此值是有效的。若需要长期保存,则应将字符串复制到其他内存空间。下面给出上述函数调用示例。
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, char *argv[]){
struct sockaddr_in addr1, addr2;
char *str_ptr;
char str_arr[20];
addr1.sin_addr.s_addr=htonl(0x1020304);
addr2.sin_addr.s_addr=htonl(0x1010101);
str_ptr=inet_ntoa(addr1.sin_addr);
strcpy(str_arr, str_ptr);
printf("Dotted-Decimal notation1: %s \n", str_ptr);
inet_ntoa(addr2.sin_addr);
printf("Dotted-Decimal notation2: %s \n", str_ptr);
printf("Dotted-Decimal notation3: %s \n", str_arr);
return 0;
}
INADDR_ANY
每次创建服务器端套接字都要输入IP地址会有些繁琐,此时可如下初始化地址信息。
struct sockaddr_in addr;
char * serv_port =“9190*;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(atoi(serv_port));
与之前方式最大的区别在于,利用常数INADDR_ANY分配服务器端的IP地址。若采用这种方式,则可自动获取运行服务器端的计算机IP地址,不必亲自输入。而且,若同一计算机中已分配多个IP地址(多宿主(Multi-homed)计算机,一般路由器属于这一类),则只要端口号一致就可以从不同IP地址接收数据。因此,服务器端中优先考虑这种方式。而客户端中除非带有一部分服务器端功能,否则不会采用。
向套接字分配网络地址
既然已讨论了sockaddr_in结构体的初始化方法,接下来就把初始化的地址信息分配给套接
字。bind函数负责这项操作。
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr * myaddr, socklen_t addrlen);//成功时返回0,失败时返回-1。
sockfd //要分配地址信息(IP地址和端口号)的套接字文件描述符。
myaddr //存有地址信息的结构体变量地址值。
addrlen //第二个结构体变量的长度。
如果此函数调用成功,则将第二个参数指定的地址信息女配给第一个参数中的相应套接字
基于Windows的实现
函数htons,htonl在Windows中的使用
#include<stdio.h>
#include<winsock2.h>
void ErrorHandling(char* message);
int main(int argc, char *argv[]){
WSADATA wsaData;
unsigned short host_port=0x1234;
unsigned short net_port;
unsigned long host_addr=0x12345678;
unsigned long net_addr;
if(WSAStartup(MAKEWORD(2,2), &wsaData)!=0)
ErrorHandling("WSAStartup() error!");
net_port=htons(host_port);
net_addr=htonl(host_addr);
printf("Host ordered port: %#x \n",host_port);
printf("Network ordered port: %#x \n", net_port);
printf("Host ordered address: %#lx \n", host_addr);
printf("Network ordered address: %#lx \n", net_addr);
WSACleanup();
return 0;
}
void ErrorHandling(char* message){
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
函数inet_addr,inet_ntoa在Windows中的使用
#include <stdio.h>
#include <string.h>
#include <winsock2.h>
void ErrorHandling(char* message);
int main(int argc, char *argv[]){
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
ErrorHandling("WSAStartup() error!");
/* inet_addr函数调用示例*/
char *addr="127.212.124.78";
unsigned long conv_addr=inet_addr(addr);%记向具体传构街
if(conv_addr==INADDR_NONE)
printf("Error occured! \n");
else
printf("Network ordered integer addr: %#lx \n", conv_addr);
/* inet_ntoa函数调用示例*/
struct sockaddr_in addr;
char *strptr;
char strArr[20];
addr.sin_addr.s_addr=htonl(0x1020304);
strptr=inet_ntoa(addr.sin_addr);
strcpy(strArr, strptr);
printf("Dotted-Decimal notation3 %s \n", strArr);
WSACleanup();
return e;
}
void ErrorHandling(char* message)
//与之前示例一致,故省略!
WSAStringToAddress & WSAAddressToString
下面介绍Winsock2中增加的2个转换函数。它们在功能上和inet_ntoa和inet_addr全相同,但优点在于只持多种协议,在IPv4和IPv6中均可适用。当然它们也有缺点,使用inet_ntoa、inet_addr可以很容易地在Linux和Windows之间切换程序。而将要介绍的这2个函数则依赖于特定平台,会降低兼容性。
#include <winsock2.h>
INT WSAStringToAddress(
LPTSTR AddressString, INT AddressFamily, LPWSAPROTOCOL_INFO lpProtocolInfo,
LPSOCKADDR lpAddress, LPINT lpAddressLength
);
//成功时返回0,失败时返回SOCKET_ERROR
参数一:含有IP和端口号的字符串地址值
参数二:第一个参数中地址所属的地址族信息
参数三:设置协议提供者(Provider),默认为NULL
参数四:保存地址信息的结构体变量地址值
参数五:第四个参数中传递的结构体长度所在的变量地址值
上述函数中新出现的各种类型几乎都是针对默认数据类型的typedef声明。
WSAAddressToString与WSAStringToAddress在功能上正好相反,它将结构体中的地址信息转
换成字符串形式。
#include <winsock2.h>
INT WSAAddressToString(
LPSOCKADDR lpsaAddress, DWORD dwAddressLength,
LPWSAPROTOCOL_INFO lpProtocolInfo, LPSTR lpszAddressstring,LPDWORD
lpdwAddressStringLength);
//成功时返回0,失败时返回 SOCKET_ERROR。
参数一:需要转换的地址信息结构体变量地址值
参数二:第一个参数中结构体的长度
参数三:设置协议提供者,默认为NULL
参数四:保存转换结果的字符串地址值
参数五:第四个参数中存有地址信息的字符串长度
以下是这两个函数的示例:
#undef UNICODE
#undef _UNICODE
#include <stdio.h>
#include <winsock2.h>
int main(int argc, char *argv[]){
char *strAddr="203.211.218.102:9190";
char strAddrBuf[50];
SOCKADDR_IN servAddr;
int size;
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData);
size=sizeof(servAddr);
WSAStringToAddress(strAddr, AF_INET, NULL,(SOCKADDR*)&servAddr, &size);
size=sizeof(strAddrBuf);
WSAAddressToString((SOCKADDR*)&servAddr,sizeof(servAddr), NULL, strAddrBuf,&size);
printf("Second conv result: %s \n", strAddrBuf);
WSACleanup();
return 0;
}