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 向内存保存数据的方式有两种:

  1. 大端序:高位字节存放到低位地址。网络字节序为大端序。

  2. 小端序:高位字节存放到高位地址。目前主流的 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));    // 基于字符串的端口号初始化

服务器端和客户端都要进行网络地址信息的初始化,但目的不同:

  1. 服务器端要将声明的 sockaddr\_in 结构体变量初始化为自己的 IP 地址和端口号,用于在 bind 函数中与自己的套接字相绑定。

  2. 客户端也要将声明的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 

*******************************************/
posted @ 2021-12-16 18:14  MyBluehat  阅读(72)  评论(0)    收藏  举报