TCP/IP网络编程 第三章:地址族和数据序列

在前一章中我们介绍了有关套接字创建的各种细节,本章我们将讲解给套接字分配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;
}

猜你喜欢

转载自blog.csdn.net/Reol99999/article/details/131615932
今日推荐