TCP/IP网络编程C03-地址族与数据序列
学习笔记
IP地址
IPv4 地址为 4 字节,IPv6 是 16 字节地址族。
端口号是 2 字节,范围是 0~65535。其中 0~1023 是熟知端口号。
虽然端口号不能重复,TCP 套接字和 UDP 套接字不会共用端口号,所以两者之间允许重复。
IP地址分两类:IPv4(4字节)IPv6(16字节)
IPv4地址族:
+--------+--------+--------+--------+
A: |0aaaaaaa|bbbbbbbb|bbbbbbbb|bbbbbbbb| 首字节:00000000~01111111=0~127
+--------+--------+--------+--------+
+--------+--------+--------+--------+
B: |10aaaaaa|aaaaaaaa|bbbbbbbb|bbbbbbbb| 首字节:10000000~10111111=128~191
+--------+--------+--------+--------+
+--------+--------+--------+--------+
C: |110aaaaa|aaaaaaaa|aaaaaaaa|bbbbbbbb| 首字节:11000000~11011111=192~223
+--------+--------+--------+--------+
以上为常用的3种类别的IP地址,其中,不含b的部分称为网络号,b的部分则为主机号。网络号字段中的0,10,110为类别位(即A,B,C类地址)
在A类地址中,0段与127段是不使用的(0段为保留地址,表示本网络;而127段为环回地址),
故A类地址范围为:1.0.0.0~126.255.255.255
B类地址的范围为:128.0.0.0~191.255.255.255
C类地址的范围为:192.0.0.0~223.255.255.255
示意图如下


地址信息表示
表示IPv4的结构体
struct sockaddr_in
{
sa_family sin_family; //地址族(AF_INET|AF_INET6|...),两个字节
uint16_t sin_port; //16位端口号
struct in_addr sin_addr; // 表示 32 位 IP 地址的结构体
char sin_zero[8]; //占位用(必须填充为0)
}
struct in_addr
{
in_addr_t s_addr; // 32 位 IP 地址,实际位为 uint32_t 类型
}
sockaddr_in的传递
bind 的第二个参数期望得到的是 sockaddr 结构体变量的地址值,但是 sockaddr 的成员填充起来比较麻烦,因此使用 sockaddr_in 结构体来代替它。
使用 sockaddr_in 结构体生成的字节流也符合 bind 函数的要求,只需在传递地址时转换为 sockaddr* 类型即可。示例代码如下
struct sockaddr_in serv_addr;
...
if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");
sockaddr结构体
sockaddr 结构体定义如下,它是通用的结构体,并非只为 IPv4 设计,而 sockaddr_in 是保存 IPv4 地址信息的结构体。
struct sockaddr {
sa_family_t sin_family; // 地址族
char sa_data[14]; // 地址信息,14个字节
}
网络字节序与地址转换
字节序
CPU 向内存保存数据的方式有两种:
-
大端序:高位字节存放到低位地址。网络字节序为大端序。
-
小端序:高位字节存放到高位地址。目前主流的 Intel 系列 CPU 按小端序方式保存数据。
在使用网络发送数据时要先把数据转化成大端序,接收时也要先转换为主机字节序。
对于0x12345678, 0x12为高位字节,0x78为低位字节。
其大端序:
+----+
|0x78|
0x03: +----+
|0x56|
0x02: +----+
|0x34|
0x01: +----+
|0x12|
0x00: +----+
其小端序:
+----+
|0x12|
0x03: +----+
|0x34|
0x02: +----+
|0x56|
0x01: +----+
|0x78|
0x00: +----+
字节序转换
htons 中的 h 代表主机字节序(host),n 代表网络字节序(network)。
s 代表 short 类型,处理 2 字节数据,用于端口号转换;l 代表 long 类型(Linux 中 long 占用 4 字节),处理 4 字节数据,用于 IP 地址转换。
//short 类型,用于端口号的转换
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
//long 类型,用于 IP 地址的转换
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);
除了向 sockaddr_in 结构体变量填充数据时需要进行字节序转换外,其他情况无需考虑字节序问题,会自动转换。
网络地址初始化及分配
sockaddr_in 中保存地址信息的成员是 32 位整型,而一般我们描述 IP 地址时用的是字符串格式的点分十进制表示法,因此需要将字符串形式的 IP 地址转换为 32 位整型数据。
有两个函数可以完成以上功能:inet\_addr 函数和 inet\_aton 函数。
字符串转网络字节序(inet_addr和inet_aton)
//inet_addr 函数在转换类型的同时也会完成网络字节序的转换,它还可以检测无效的 IP 地址。
#include <arpa inet.h="">
in_addr_t inet_addr(const char* string);
// 功能:将字符串形式的 IP 地址转换为 32 位整型数据并返回。
// 返回值:成功时返回 32 位大端序整型值,失败时返回 INADDR_NONE。
//inet_aton 函数和 inet_addr 函数的功能相同,也是将字符串形式的 IP 地址转换为 32 位网络字节序整数,但是它利用了 in_addr 结构体,使用频率更高。
//inet_aton 需要传递一个 in_addr 类型结构体的指针,它会将转换结果直接放入该指针所指的 in_addr 结构体中。
#include <arpa inet.h="">
int inet_aton(const char* string, struct in_addr* addr);
// 功能:将字符串形式的 IP 地址转换为 32 位网络字节序整数并存储到 addr 中。
// 返回值:成功时返回 1,失败时返回 0
网络字节序转字符串(inet_ntoa)
//inet_ntoa 函数与 inet_aton 函数相反,它将网络字节序的整数型 IP 地址转换为字符串形式。
#include <arpa inet.h="">
char* inet_ntoa(struct in_addr adr);
// 功能:将网络字节序的整数型 IP 地址转换为字符串形式
// 返回值:成功时返回转换的字符串地址值,失败时返回 -1
该函数使用时要小心:返回值类型为 char 指针,返回字符串地址意味着字符串已保存到内存中,但该函数是在内部申请了内存并保存了字符串,因此如果再次调用 inet_ntoa 函数,也有可能覆盖之前保存的字符串信息。
因此要将返回的字符串信息复制到其他内存空间。
网络地址初始化
struct sockaddr_in addr;
char *serv_ip = "211.217.168.13"; // 声明 IP 地址字符串
char *serv_port = "9190"; // 声明端口号字符串
memset(&addr, 0, sizeof(addr)); // 结构体变量 addr 的所有成员初始化为 0,主要是为了将 sockaddr_in 的成员 sin_zero 初始化为 0。
addr.sin_family = AF_INET; // 指定地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的 IP 地址初始化
addr.sin_port = htons(atoi(serv_port)); // 基于字符串的端口号初始化
服务器端和客户端都要进行网络地址信息的初始化,但目的不同:
-
服务器端要将声明的
sockaddr\_in结构体变量初始化为自己的 IP 地址和端口号,用于在 bind 函数中与自己的套接字相绑定。 -
客户端也要将声明的
sockaddr\_in结构体变量初始化为服务器端的 IP 地址和端口号,用于在 connect 函数中向服务器发起连接请求。
INADDR_ANY
addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 相当于主机字节序的 32 位整型 IP 地址
可利用此常数来自动分配服务器端的IP地址(适用于单网卡情况)
习题答案
Q01
IP 地址族 IPv4 和 IPv6 有何区别?在何种背景下诞生了 IPv6?
IPV4 是 4 字节地址族,IPV6 是 16 字节地址族。IPV6 的诞生是为了应对 2010 年前后 IP 地址耗尽的问题而提出的标准
Q02
通过 IPV4 网络 ID、主机 ID 及路由器的关系说明向公司局域网中的计算机传输数据的过程
首先数据传输的第一个环节是向目标 IP 所属的网络传输数据。此时使用的是 IP 地址中的网络 ID。传输的数据将被传到管理网络的路由器,接受数据的路由器将参照 IP 地址的主机号找自己保存的路由表,找到对应的主机发送数据
网络ID是为了区分网络而设置的一部分IP地址,假设向www.baidu.com公司传输数据,该公司内部构建了局域网。因为首先要向baidu.com传输数据,也就是说并非一开始就浏览所有四字节IP地址,首先找到网络地址,进而由baidu.com(构成网络的路由器)接收到数据后,传输到主机地址。比如向 203.211.712.103 传输数据,那就先找到 203.211.172 然后由这个网络的网关找主机号为 172 的机器传输数据。
Q03
套接字地址分为 IP 地址和端口号。为什么需要 IP 地址和端口号?或者说,通过 IP 可以区分哪些对象?通过端口号可以区分哪些对象?
IP 地址是为了区分网络上的主机。端口号是区分同一主机下的不同的 SOCKET,以确保软件准确收发数据。
Q04
请说明IP地址的分类方法,并据此说出下面这些IP的分类。
- 214.121.212.102(C类)
- 120.101.122.89(A类)
- 129.78.102.211(B类)
分类方法:A 类地址的首字节范围为:0~127、B 类地址的首字节范围为:128~191、C 类地址的首字节范围为:192~223
Q05
计算机通过路由器或交换机连接到互联网。请说出路由器和交换机的作用
路由器充当中间媒介,帮助数据传输目的地。不仅如此,它还负责帮助连接到本地网络的计算机和互联网的连接。因此,路由器也被称为交换机。
Q06
什么是知名端口?其范围是多少?知名端口中具有代表性的 HTTP 合同 FTP 端口号各是多少?
知名端口是要把该端口分配给特定的应用程序,范围是 0~1023 ,HTTP 的端口号是 80 ,FTP 的端口号是20和21
Q07
向套接字分配地址的 bind 函数原型如下:
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
而调用时则用:
bind(serv_sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)
此处 serv_addr 为 sockaddr_in 结构体变量。与函数原型不同,传入的是 sockaddr_in 结构体变量,请说明原因。
题目大概意思是:为什么 bind 中第二个参数是sockaddr,但是传入的是sockaddr_in
bind 函数第二个参数类型是sockaddr结构体,很难份分配 IP 地址和端口号,因此 IP 地址和 PORT 号的分配是通过sockaddr_in完成的。因为该结构体和sockaddr结构体的组成字节序和大小完全相同,所以可以强转
因为对于详细的地址信息使用 sockaddr 类型传递特别麻烦,进而有了 sockaddr_in 类型,其中基本与前面的类型保持一致,还有 sa_sata[4] 来保存地址信息,剩余全部填 0,所以强制转换后,不影响程序运行。
Q08
请解释大端序、小端序、网络字节序,并说明为何需要网络字节序
小端序是把高位字节存储到高位地址上;大端序是把高位字节存储到低位地址上。这样,由于值的表达方式不同,所以通过网络发送和接收数据制定了标准,并称为“网络字节顺序”。而且,在网络字节序中,数据传输的标准是 “大端序”
Q09
大端计算机希望将 4 字节整型数据 12 传到小端序计算机。请说出数据传输过程中发生的字节序变换过程
因为网络字节序的顺序标准是 “大端序”,所以大端序的计算机在网络传输中不需要先转换字节顺序,直接传输。但是接受数据的是小端序计算机,因此,要经过网络转本地序的过程,再保存到存储设备上
Q10
怎么表示回送地址?其含义是什么?如果向回送地址传输数据将会发生什么情况?
127.0.0.1 表示回送地址,指的是计算机自身的IP地址,无论什么程序,一旦使用回送地址发送数据,协议软件立即返回,不进行任何网络传输。
书本源码
01-endian_conv.c
#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);//host to network
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;
}
/******************** input******************
description:
ssize_t write(int fd, const void* buf, size_t nbytes);
函数说明:write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。
content:
*******************************************/
/******************** output******************
description:
主机字节序为小端
网路字节序为大端
content:
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412
*******************************************/
02-inet_addr.c
#include <stdio.h>
#include <arpa inet.h="">
int main(int argc, char *argv[])
{
char *addr1="127.212.124.78";
char *addr2="127.212.124.256";
unsigned long conv_addr=inet_addr(addr1);//将字符串格式的IP地址转换为32位整数型(网络字节序)
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;
}
/******************** input******************
description:
content:
*******************************************/
/******************** output******************
description:
对于IP地址的表示,我们熟悉的是点分十进制表示法( Dotted Decimal Notation),而非整数型数据表示法。
幸运的是,有个函数会帮我们将字符串形式的IP地址转换成32位整数型数据。
此函数在转换类型的同时进行网络字节序转换。
从运行结果可以看出, inet addr函数不仅可以把IP地址转成32位整数型,而且可以检测无效IP地址。
另外,从输出结果可以验证确实转换为网络字节序。
content:
Network ordered integer addr: 0x4e7cd47f
Error occureded
0x7f(16)=127(10)
*******************************************/
03-inet_aton.c
#include <stdio.h>
#include <stdlib.h>
#include <arpa inet.h="">
void error_handling(char *message);
/*
struct sockaddr_in
{
sa_family sin_family; //地址族(AF_INET|AF_INET6|...),两个字节
uint16_t sin_port; //16位端口号
struct in_addr sin_addr; // 表示 32 位 IP 地址的结构体
char sin_zero[8]; //占位用(必须填充为0)
}
struct in_addr//in_addr 是网络字节序的
{
in_addr_t s_addr; // 32 位 IP 地址,实际位为 uint32_t 类型
}
*/
/*
隆重推出他们:inet_network(), inet_addr(), inet_aton()!!
三者定义:
int inet_aton(const char *cp, struct in_addr *inp);//网络字节序
in_addr_t inet_addr(const char *cp);//inet_addr返回的整数形式是网络字节序
in_addr_t inet_network(const char *cp);//inet_network返回的整数形式是主机字节序
*/
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))//成功时返回1(true),失败时返回(false)。
error_handling("Conversion error");
else
printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
/******************** input******************
description:
content:
*******************************************/
/******************** output******************
description:
content:
Network ordered integer addr: 0x4f7ce87f
*******************************************/
04-inet_ntoa.c
#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);//自动进行了大小端转换,char* inet_ntoa(struct in_addr adr);
strcpy(str_arr, str_ptr);//char *strcpy(char *dest, const char *src)
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;
}
/******************** input******************
description:
content:
*******************************************/
/******************** output******************
description:
content:
Dotted-Decimal notation1: 1.2.3.4
Dotted-Decimal notation2: 1.1.1.1
Dotted-Decimal notation3: 1.2.3.4
*******************************************/

浙公网安备 33010602011771号