一 分配给套接字的IP地址与端口号
IP是 Internet Protocol(网络协议)的简写,是为了收发网络数据而分配给计算机的值。
端口号(Port)是为了区分在同一操作系统内不同套接字(Socket)而设置的,端口号只具有本地意义,它只是为了标识本计算机应用层中的各个进程在和运输层进行数据交互时的层间接口。在互联网不同计算机中,相同的端口号是没有关联的。16 位的端口号可允许有 65535 个不同的端口号,这个数目对于一个计算机来说是足够用的。
1.1 网络地址
为使计算机连接到网络并收发数据,必须向其分配IP地址。IP地址分为两类:
- IPv4(Internet Protocol Version 4)4字节地址族(32位)
- IPv6(Internet Protocol Version 6)16字节地址族(128位)
IPv4 与 IPv6 的差别主要是表示IP地址所用的字节数,目前通用的地址族是 IPv4。IPv6是为了应对 IPv4 地址耗尽的问题而提出的新标准。
IPv4标准的4字节IP地址分为网络地址和主机(指计算机)地址,且分为 A、B、C、D、E 五大类,如下图所示:
上图中的网络号即为网络地址(网络ID),是为了区分不同网络而设置的一部分IP地址。假设向 www.semi.com 公司传输数据,该公司内部构建了局域网,把所有的计算机连接起来。因此,首先应向 semi.com 网络传输数据,也就是说,并非一开始就浏览所有 4 字节IP地址,进而找到目标主机;而是先浏览 4 字节IP地址的网络地址,先把数据传到 semi.com 的网络。semi.com 网络(构成网络的路由器)接收到数据后,浏览发送数据的主机号(主机ID)并将数据传送给目标主机。下图展示了数据传输过程。
《分析》某主机向 203.211.172.103 和 203.211.217.202 目标主机传输数据,其中 203.211.172.0 和 203.211.217.0 分别为 mid.com 和 semi.com 网站的网络地址(亦即网络号),第4个字节的103 和 202 表示的是主机号,可以看出这是IPv4标准的C类地址表示法。“向相应网络传输数据”实际上是向构成网络的路由器(Router)或交换机(Switch)传递数据,再由接收到数据的路由器根据数据中的主机号向目标主机传递数据。
《拓展知识》路由器和交换机
若想构建网络,需要一种物理设备完成外网与本地网主机之间的数据交换,这种设备便是路由器或交换机。它们实际上也是一种计算机,只不过是为特殊目的而设计运行的,因此有了别名。所以,如果在我们使用的计算机上安装适当的软件,也可以将其用作交换机。另外,交换机比路由器功能要简单一些,而实际用途差别不大。
1.2 网络地址分类与主机地址边界
只需要通过IP地址的第一个字节即可判断网络地址(即网络号)即可判断网络地址占用的字节数,因为我们根据IP地址的边界即可区分网络地址。如下所示:
- A类地址的首字节范围:0~127
- B类地址的首字节范围:128~191
- C类地址的首字节范围:192~223
还有如下这种表述方式:
- A类地址的首位以 0 开始
- B类地址的前两位以 10 开始
- C类地址的前三位以 110 开始
正因如此,通过套接字收发数据时,数据传到网络后即可轻松找到正确的主机。
1.3 用于区分套接字的端口号
IP地址用于区分计算机,只要有IP地址就能向目标主机传输数据,但仅凭这些无法传输给最终的应用程序。例如,当我们在观看视频的同时还在浏览网页,这是至少需要1个接收视频数据的套接字和1个接收网页信息的套接字。问题在于如何区分二者呢?简言之,传输到计算机的网络数据是发给播放器,还是发送给浏览器,该如何区分呢?
假设我们开发了一个收发数据的P2P应用程序,该程序用块单位分割1个文件,可以从多台计算机接收数据。若想接收多台主机发来的数据,则需要相应个数的套接字。那如何区分这些套接字呢?
计算机中一般配有 NIC(Network Interface Card,网络接口卡,简称网卡) 数据传输设备。通过NIC向计算机内部传输数据时会用到IP地址。操作系统负责把传递到内部的数据适当分配给套接字,这时就要利用端口号。也就是说,通过 NIC 接收的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口号的套接字,如下图所示:
端口号就是在同一操作系统内为区分不同套接字而设置的,因此无法将同一个端口号分配个不同的套接字。另外,端口号由16位构成,可分配的端口号范围是 0~65535。但 0~1023 是知名端口(Well-known Port),一般分配给特定应用程序,所以用户程序应当分配此范围之外的值作为端口号。另外,虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。例如:如果某TCP套接字使用 9190 为端口号,则其他TCP套接字就无法使用该端口号,但UDP套接字可以使用。
总之,数据传输目标地址同时包含IP地址和端口号,只有这样,数据才会被传输到最终的目的应用程序(应用程序套接字)。
二 地址信息的表示
应用程序中使用的IP地址和端口号是以结构体的形式给出了定义。我们将以IPv4为中心,围绕此结构体讨论目标地址的表示方法。
2.1 表示IPv4地址的结构体
填写地址信息时应以如下提问为线索进行:
- 问题1:“采用哪一种地址族?”
- 答案1:“基于IPv4的地址族。”
- 问题2:“IP地址是多少?”
- 答案2:“211.204.214.76”
- 问题3:“端口号是多少?”
- 答案3:“2048。”
结构体定义为如下形态就能回答上述提问,此结构体将作为地址信息传递给bind()函数。
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Address Family)
uint16_t sin_port; //16位端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //填充字段,不使用
};
该结构体中提到的另一个结构体 struct in_addr 定义如下,它用来存放32位IP地址。
struct in_addr
{
in_addr_t s_addr; //32位IPv4地址
};
讲解以上2个结构体前先观察一些数据结构。uint16_t, in_addr_t 等类型可以参考 POSIX(Portable Operating System Interface,可移植操作系统接口)。POSIX 是为 UNIX系统操作系统设立的标准,它定义了一些其他数据类型,如下表所示:
数据类型名称 | 数据类型说明 | 声明的头文件 |
int8_t | signed 8-bit int | sys/types.h |
uint8_t | unsigned 8-bit int (unsigned cahr) | 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 | 地址族(address family) | sys/socket.h |
socklen_t | 结构体长度(length of struct) | sys/socket.h |
in_addr_t | IP地址,声明为 uint32_t | netinet/in.h |
in_port_t | 端口号,声明为 uint16_t | netinet/in.h |
《说明》从这些数据类型声明中可以了解前面结构体的含义。那为什么需要额外定义这些数据类型呢?如前所述,这是考虑到代码的可移植性的结果。如果使用 int32_t 类型的数据,就能保证在任何时候都是占用4字节,即使将来用64位表示int类型也是如此。
2.2 结构体 sockaddr_in 的成员分析
- 成员 sin_family
每种协议族使用的地址族均不同。比如,IPv4使用4字节地址族,IPv6使用16字节地址族。可以参考表2-2保存 sin_family 地址信息。
地址族(Address Family) | 含义 |
AF_INET | IPv4网络协议中使用的地址族 |
AF_INET6 | IPv6网络协议中使用的地址族 |
AF_LOCAL | 本地通信中采用的UNIX协议的地址族 |
- 成员 sin_port
该成员保存16位端口号,重点在于,它是以网络字节序保存的。
- 成员 sin_addr
该成员保存32位IP地址信息,且也是以网络字节序保存的。为理解好该成员,应同时观察结构体 in_addr。但结构体 in_addr 声明为 uint32_t,因此只需当作32位整型值看待即可。
- 成员 sin_zero
无特殊含义。只是为了使结构体 sockaddr_in 的大小与 结构体 sockaddr 保持一致而插入的成员。必须填充为0,否则无法得到想要的结果。
sockaddr_in 结构体变量地址值将以如下方式传递给bind()函数。代码如下:
struct sockaddr_in svr_addr;
....
if(bind(svr_addr, (struct sockaddr*)&svr_addr, sizeof(svr_addr)) == -1){
perror("bind() error");
return -1;
}
....
此处重要的是第2个参数的传递。实际上,bind函数的第2个参数期望得到 sockaddr 结构体变量地址值,包括地址族、端口号、IP地址等信息。从下列代码也可看出,直接向 sockaddr 结构体填充这些信息会带来麻烦。
sockaddr 结构体的定义如下:
struct sockaddr
{
sa_family_t sin_family; //地址族(Address Family)
char sa_data[14]; //地址信息
};
//说明: sockaddr_in 结构体的大小是等于 sockaddr 结构体的大小的
sockaddr 结构体中的 sa_data 数组保存的是地址信息中需包含IP地址和端口号,剩余部分应填充0,这也是bind函数要求的。而这对于包含地址信息来将非常麻烦,继而就有了新的结构体 sockaddr_in。若按照之前的讲解填写 sockaddr_in 结构体的内容,则将生成符合bind函数要求的字节流。最后转换为 sockaddr 型的结构体变量,再传递给bind函数即可。
《补充说明》sin_family
sockaddr_in 结构体是保存IPv4地址信息的结构体。那为何还需要通过 sin_family 单独指定地址族信息呢?这与之前讲过的 sockaddr 结构体有关。结构体 sockaddr 并非只为IPv4设计,还包括IPv6,这从保存地址信息的数据 sa_data长度为14字节也可以看出。因此,结构体 sockaddr 要求在 sin_family 中指定地址族信息。为了与 sockaddr 大小保持一致,sockaddr_in 结构体中也要有地址族信息。
三 网络字节序与地址转换
不同CPU架构,4字节整型值1在内存空间的保存方式是不同的。4字节整型数值1用二进制表示如下:
00000000 00000000 00000000 00000001
有些CPU是以上面这种顺序保存到内存,另外一些CPU架构则以倒序保存到内存,用二进制表示如下:
00000001 00000000 00000000 00000000
若不考虑字节序问题就会在收发数据时产生问题,因为保存数据的顺序的不同意味着对接收数据的解析顺序也不同。
3.1 字节序(Order)与网络字节序
CPU向内存存放数据的方式有两种,这意味着CPU解析数据的方式也分为两种。
- 小端模式(Little Endian):低位字节存放在低位地址,高位字节存放在高位地址。
- 大端模式(Big Endian):低位字节存放在高位地址,高位字节存放在低位地址。
下面我们通过一个示例进行说明。假设在0x20号开始的地址中保存4字节 int 型数值 0x12345678。
小端序CPU保存方式,如下图所示:
大端序CPU保存方式,如下图所示:
《说明》整数0x12345678中,0x12是最高位字节,0x78是最低位字节。存储单元地址是按升序编号的,从0x20~0x23。
1、在小端序模式中,从低位地址0x20存储单元开始,最先保存的是低位字节0x78,以此类推,直到最高位字节0x12存放完成。
2、在大端序模式中,从地位地址0x20存储单元开始,最先保存的是高位字节0x12,以此类推,直到最低位字节0x78存放完成。
因此,代表CPU数据保存方式的主机字节序(Host Byte Order)在不同的CPU中也各不相同。目前主流的Intel系列的CPU以小端序方式保存数据。接下来分析两台不同字节序的计算机之间数据传递过程中可能出现的问题,如下图所示:
《分析》0x12 和 0x34 构成的大端序系统值与 0x34 和 0x12 构成的小端序系统值相同。换言之,只有改变数据保存顺序才能被识别为同一值。图3-6中,大端序系统传输数据 0x1234时未考虑字节序问题,而字节以 0x12、0x34的顺序发送。结果接收端以小端序方式保存数据,因此小端序接收的数据变成了 0x3412。正因如此,在通过网络传输数据时约定统一方式,这种约定称为网络字节序(Network Byte Order),非常简单,统一约定使用大端序。即先把本地数据转换成大端序模式,然后再进行网络传输。因此,所有计算机接收数据时应识别该数据为网络字节序模式。
3.2 字节序转换(Endian Conversions)
几个帮助转换字节序的函数:
#include <arpa/inet.h>
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
其中,htons 中的 h 代表主机(host)字节序,一般是小端序;其中的 n 代表网络(network)字节序,是大端序。另外,s 表示的是short,l 表示的是long(Linux系统中,long类型占用4个字节,这很关键)。
因此,htons 是 h、to、n、s 的组合,也可以解释为“把short型数据从主机字节序转换为网络字节序”;ntohs 可以理解为“把short型数据从网络字节序转换为主机字节序”。
通常,以字母 s 为后缀的函数,s 代表2个字节的short类型,因此用于端口号的转换;以字母 l 为后缀的函数,l 代表4个字节的long类型,因此用于IP地址的转换。
实例:通过下面的示例代码说明以上函数的调用过程。
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, cahr *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;
}
编译程序:gcc endian_conv.c -o conv
运行程序:./conv
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412
代码说明:这是在小端序CPU中运行的结果。如果在大端序CPU中运行,则变量值不会发生改变。Intel 和 AMD 系列的CPU都是采用小端序模式。
四 网络IP地址的初始化与分配
4.1 将字符串信息转换为网络字节序的整型数
前面我们已经讨论过,结构体 sockaddr_in 中保存IP地址信息的成员为32位整数型。因此,为了分配IP地址,需要将其表示为32位整数型数据。这对于只熟悉字符串信息的我们来说绝非易事。我们可以手动尝试一下将字符串化的IP地址 "201.211.214.36" 转换为4字节整数型数据。
对于IP地址的表示,我们熟悉的是点分十进制表示法(Dotted Decimal Notation),而非整数型数据表示法。幸运的是,有个函数可以帮助我们将字符串形式的IP地址转换成32位整数型数据。此函数在转换类型的同时也会进行网络字节序的转换。
- inet_addr()函数
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
//成功时返回32位大端序整数型数值,失败时返回 INADDR_NONE 错误码
《函数说明》如果向该函数传递类似 "211.214.107.99" 的点分十进制格式的字符串,它会将其转换为32位整数型数据并返回。当然,该整型值满足网络字节序。另外,该函数的返回值类型为 in_addr_t 在内部声明为32位无符号整型(即 uint32_t)。
实例:inet_addr() 函数的使用示例代码。
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, cahr *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 occurred!\n");
else
printf("Network ordered integer addr: %#lx\n", conv_addr);
conv_addr = inet_addr(addr2);
if(conv_addr == INADDR_NONE)
printf("Error occurred!\n");
else
printf("Network ordered integer addr: %#lx\n", conv_addr);
return 0;
}
编译程序:gcc inet_addr.c -o addr
运行结果:./addr
Network ordered integer addr: 0x4030201
Error occurred!
- inet_aton()函数
inet_aton() 函数与 inet_addr() 函数在功能上完全相同,也是将字符串形式IP地址转换为32位网络字节序整型数并返回。只不过该函数利用了 in_addr 结构体,且其使用频率更高。
#include <arpa/inet.h>
int inet_aton(cosnt char *string, struct in_addr *addr);
//形参说明
//string: 含有需转换的IP地址信息的字符串地址值
// addr: 保存转换结果的in_addr结构体变量的地址值
//成功时返回1(true),失败时返回0(false)
《函数说明》实际编程中,若要调用 inet_addr()函数,需将转换后的IP地址信息赋值给 sockaddr_in 结构体中声明的 in_addr 结构体类型的成员变量。而 inet_aton()函数则不需要此过程。原因在于,若实参传递的是 in_addr 结构体类型变量的地址值,该函数会自动把结果填入该结构体变量,注意传递的结构体变量的地址值。
inet_aton() 函数中的 a 是alphabet的首字母,n 是 network的首字母,字面意思就是将字母格式的字符串转换成网络字节序的整型值。
实例:通过下面的示例代码可以了解 inet_aton()函数的使用方法。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
int main(int argc, cahr *argv[])
{
char *addr = "127.232.124.79";
struct sockaddr_in addr_inet;
if(!inet_aton(addr, &addr_inet.sin_addr)) //使用inet_aton函数省去了手动保存IP地址信息的过程
printf("Conversion error: %s\n", strerror(errno));
else
printf("Network ordered integer addr: %#lx\n", addr_inet.sin_addr.s_addr);
return 0;
}
程序编译:gcc inet_aton.c -o aton
程序运行:./aton
Network ordered integer addr: 0x4f7ce87f
- inet_ntoa()函数
该函数正好与 inet_aton()函数的功能相反,此函数是把网络字节序整型值的IP地址转换成我们熟悉的点分十进制字符串形式。
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr addr);
//成功时返回转换的字符串地址值,失败时返回-1
《函数说明》该函数将通过参数传入的整型IP地址转换为字符串格式并返回。但调用时需小心,返回值类型为char指针。返回字符串地址意味着字符串已保存到内存空间,但该函数并未向程序员要求分配内存,而是在函数内部申请了内存并保存了字符串。也就是说,调用完该函数后,应立即将字符串信息复制到其他内存空间。因为,若再次调用inet_ntoa()函数,则有可能会覆盖之前保存的字符串信息。总之,再次调用inet_ntoa()函数前返回的字符串地址值是有效的。若需要长期保存,则应将字符串复制到其他内存空间。
实例:下面给出 inet_ntoa()函数的使用方法的示例代码。
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc, cahr *argv[])
{
struct sockaddr_in addr1,addr2;
char *str_ptr;
char str_arr[20]={0};
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 notiton1: %s\n", str_ptr);
inet_ntoa(addr2.sin_addr); //返回的地址已覆盖成新的IP地址字符串
printf("Dotted-Decimal notiton2: %s\n", str_ptr);
printf("Dotted-Decimal notiton3: %s\n", str_arr);
return 0;
}
编译程序:gcc inet_ntoa.c -o ntoa
运行程序:./ntoa
Dotted-Decimal notiton1: 1.2.3.4
Dotted-Decimal notiton2: 1.1.1.1
Dotted-Decimal notiton3: 1.2.3.4
拓展:inet_pton()和 inet_ntop() 函数 —— 适用于IPv4和IPv6
inet_aton() 和 inet_ntoa() 函数仅仅适用于IPv4格式的地址转换,而 inet_pton() 和 inet_ntop() 函数功能和前两者相同,并且同时支持IPv4和IPv6。
- inet_pton() 函数 — 用于将文本字符串格式转换成网络字节序整型数地址。
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
//函数参数说明
//af: 地址族,IPv4为AF_INET,IPv6为AF_INET6
//src: 指向字符串格式IP地址的指针
//dst: 指向IP地址in_addr结构体类型变量的s_addr成员的指针
//返回值:若成功,返回1;若格式无效,返回0;若出错,返回-1
- inet_ntop() 函数 — 用于将网络字节序的整型数地址转换成文本字符串格式。
#include <arpa/inet.h>
const char* inet_ntop(int af, const void *src, char *dst, socklen_t size);
//函数参数说明
//af: 地址族,IPv4为AF_INET,IPv6为AF_INET6
//src: 指向IP地址in_addr结构体类型变量的成员s_addr的指针
//dst: 指向用于存放字符串格式IP地址内存空间的指针
//size: 第三个参数dst指向的缓存区的大小,避免溢出,如果缓存区太小无法存储IP地址的值,则返回一个空指针,并将errno置为ENOSPC
//若成功,返回地址字符串指针;若出错,返回NULL
实例:inet_pton()、inet_ntop() 函数的使用方法示例程序。
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
int main()
{
struct sockaddr_in addr;
if(inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr) == 1)
printf("NetIP: %#lx\n", addr.sin_addr.s_addr); //得到的是网络字节序格式的IP地址
char str[20];
if(inet_ntop(AF_INET, &addr.sin_addr.s_addr, str, sizeof(str)) != NULL)
printf("StrIP: %s\n", str);
return 0;
}
编译程序:gcc inet_pton.c -o pton
运行程序:./pton
NetIP: 0x100007f StrIP: 127.0.0.1
4.2 网络地址初始化
结合前面所述的内容,我们下面介绍套接字(socket)创建过程中常见的网络地址信息初始化方法。
struct sockaddr_in addr;
char *svr_ip = "211.217.168.13"; //声明IP地址字符串
char *svr_port = "9190"; //声明端口号字符串
memset(&addr, 0, sizeof(addr)); //结构体变量addr的所有成员初始化为0
//填充sockaddr_in结构体变量addr的成员
addr.sin_family = AF_INET; //指定地址族
addr.sin_addr.s_addr = inet_addr(svr_ip); //基于字符串的IP地址初始化
addr.sin_port = htons(atoi(svr_port)); //基于字符串的端口号初始化
//atoi()函数的作用是把字符串类型的值转换成整数型
《说明》上述代码利用字符串格式的IP地址和端口号初始化了 sockaddr_in 结构体类型的变量addr。另外,代码中队IP地址和端口号进行了硬编码,这并非良策,因为运行环境改变就得更改代码。因此,我们是实际应用中都是通过参数传入IP地址和端口号,比如说使用main()函数的参数进行传入。
4.3 客户端地址信息初始化
上述网络地址信息的初始化过程主要是针对服务器端而非客户端。给套接字分配IP地址和端口号主要是为下面这件事做准备:
“请把进入IP 211.217.168.13 、端口号为9190 的数据传给我!”
反观客户端中连接请求如下:
“请连接到IP 211.217.168.13、端口号为9190!”
《说明》请求方法不同意味着调用的函数也不同。服务器端的准备工作通过bind()函数完成,而客户端则通过connect()函数完成。因此,函数调用前需准备的地址值类型也不同。。服务器端声明 sockaddr_in 结构体变量,将其初始化为赋予服务器端IP地址和套接字的端口号,然后调用bind()函数;而客户端则声明 sockaddr_in 结构体,并初始化为要与之连接的服务器端套接字的IP地址和端口号,然后调用connect()函数。
服务器端:socket() —> bind() —> listen() —> accept() —> recv/send() —> close()
客户端:socket() —> connect() —> send/recv() —> close()
4.4 INADDR_ANY
每次创建服务器端套接字都要输入IP地址会有些繁琐,此时可如下初始化地址信息。
struct sockaddr_in svr_addr;
cahr *svr_port = "9190";
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(atoi(svr_port));
《说明》与前面的初始化方式最大的区别在于,利用常数 INADDR_ANY 分配服务器端的IP地址。若采用这种方式,则可自动获取服务器端的主机IP地址,不必亲自输入。而且,若同一主机中已分配多个IP地址(多宿主(Multi-homed)主机,一般路由器属于这一类),则只要端口号一致,就可以从不同IP地址接收数据。因此,服务器端中优先考虑这种方式。而客户端除非带有一部分服务器功能,否则不会采用。
《拓展知识》创建服务器套接字时需要IP地址的原因
初始化服务器端套接字时应分配所属主机的IP地址,因为初始化时使用的IP地址非常明确,那为何还要进行IP地址初始化呢?
如前所述,同一主机中可以分配多个IP地址,实际IP地址的个数与主机中安装的NIC(Network Interface Card,网卡)的数量相等。即使是服务器端套接字,也需要决定应接收哪个IP传来的(即哪个NIC设备传来的)数据。因此,服务器端套接字初始化过程中要求填入IP地址信息。另外,若只有一个 NIC 设备,则直接使用符号常数 INADDR_ANY。
4.5 向套接字分配网络地址
上面已经讨论了 sockaddr_in 结构体的初始化方法,接下来就是把初始化的地址信息分配给套接字(socket)。这个工作是由 bind()函数来负责完成的。
- bind() 函数
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
//函数参数说明
//sockfd: 要分配地址信息(IP地址和端口号)的套接字文件描述符
//myaddr: 存有地址信息的结构体变量地址值
//addrlen: 第二个结构体变量的长度
//成功时返回0,失败时返回-1
如果此函数调用成功,则将第二个参数指定的地址信息分配给第一个参数中相对应的套接字。
下面给出服务器常见套接字初始化的过程:
int sockfd;
struct sockaddr_in svr_addr;
char *svr_port = "9190";
//创建服务器端套接字(监听套接字)
sockfd = socket(PF_INET, SOCK_STREAM, 0);
//服务器端地址信息初始化
memset(&svr_addr, 0, sizeof(svr_addr));
svr_addr.sin_family = AF_INET;
svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
svr_addr.sin_port = htons(atoi(svr_port));
//分配地址信息
bind(sockfd, (struct sockaddr*)&svr_addr, size(svr_addr));
五 练习题
1、IP地址族IPv4和IPv6有何区别?在何种背景下诞生了IPv6?
答:IPv4与IPv6主要区别是表示IP地址所用的字节数不同,IPv4是4字节(32位)地址族,而IPv6是16字节(128位)地址族。IPv6的诞生是为了应对2010年前后IPv4标准的IP地址可能耗尽的问题而提出的新标准。
2、通过IPv4网络ID、主机ID及路由器的关系说明向公司局域网中的计算机传输数据的过程。
答:首先是通过IPv4地址中的网络号(也叫网络ID)向公司局域网传输数据,传输的数据将先被临时保存到管理网络的路由器中,路由器接收到数据后,参照IP地址中的主机号(也叫主机ID),找到对应的主机并将数据传给目标主机。
3、套接字地址分为IP地址和端口号。为什么需要IP地址和端口号?或者说,通过IP可以区分哪些对象?通过端口号可以区分哪些对象?
答:IP地址是为了区分网络上的主机。而端口号是为了区分同一主机下的不同套接字(socket),以确保数据的准确收发。
4、请说明IP地址的分类方法,并据此说出下面这些IP地址的分类。
- 214.121.212.102
- 120.101.122.89
- 129.78.102.211
答:IPv4地址是根据首字节固定前缀的不同来进行分类的,可以分为 A、B、C、D、E 五大类。
A类地址前缀为:0,首字节范围:0~127
B类地址前缀为:10,首字节范围:128~191
C类地址前缀为:110,首字节范围:192~223
D类地址前缀为:1110,首字节范围:224~239
E类地址前缀为:1111,首字节范围:240~255
214.121.212.102,首字节为214,在C类地址首字节范围内,因此其为C类IP地址。
120.101.122.89,首字节为120,在A类地址首字节范围内,因此其为A类IP地址。
129.78.102.211,首字节为129,在B类地址首字节范围内,因此其为B类IP地址。
5、计算机通过路由器或交换机连接到互联网。请说出路由器和交换机的作用。
答:路由器和交换机的作用主要是完成本地局域网内的主机与外网主机之间的通信连接和数据交换功能,相当于一个中介。
6、什么是知名端口?其范围是多少?知名端口中具有代表性的HTTP和FTP端口号各是多少?
答:知名端口(Well-known Port)是指预定分配给特定应用程序的端口号,0~1023范围的端口号都是知名端口。
HTTP的端口号是:80。
FTP的端口号是:21。
7、向套接字分配地址的 bind 函数原型如下:
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
而调用时则用:
bind(svr_port, (struct sockaddr*)&svr_addr, sizeof(svr_addr));
此处 svr_addr 为 sockaddr_in 结构体变量。与函数原型不同,传入的是 sockaddr_in 结构体变量,请说明原因。
答:sockaddr 结构体它是通用型的网络地址信息结构体,它既可以支持IPv4,又可以支持IPv6,而 sockaddr_in 结构体是只用于IPv4格式的,可以更好地填充IPv4格式的网络地址信息,而IPv6格式的则是使用 sockaddr_in6 结构体来描述其网络地址信息。
8、请解释大端序、小端序、网络字节序,并说明为何需要网络字节序。
答:大端序:高位字节存放在低位地址,低位字节存放在高位地址。书写顺序与存放在内存中的顺序保持一致。
小端序:高位字节存放在高位地址,低位字节存放在低位地址。书写顺序与存放在内存中的顺序正好相反。
网络字节序:在通过网络传输时约定的一种统一字节序,把这种约定称为网络字节序(Network Byte Order),统一使用大端序方式。
因为互联网主机中使用的CPU保存数据的方式可能存在不同,有的是大端序,有的是小端序,为了保证不同主机之间的数据传递的一致性,所以有必要在网络传输过程中采用统一的字节序,当数据到达目标主机后,只需要转换成本地主机字节序即可。
9、大端序计算机希望将4字节整数型数据12传到小端序计算机。请说出数据传输过程中发生的字节序变换过程。
答:12 的 16进制表示为:0x0000 000C
大端序计算机 ——> 网络(大端序):0x0000 000C
网络(大端序)——> 小端序计算机:0x0C00 0000
10、怎样表示回送地址?其含义是什么?如果向回送地址处传输数据将会发生什么情况?
答:回送地址是用 127.0.0.1(这是一个A类IP地址)来表示的。其含义是表示计算机本身的IP地址。向回送地址发送数据,数据不会被传输到网络中去,而只是经过本地NIC(网卡)设备后立即返回。回送地址一般用于同一主机内的进程间通信。
参考
《TCP/IP网络编程(尹圣雨)》第3章 - 地址族与数据序列