第三章 地址族和数据序列
这章主要将主要讲解:
如何为套接字分配IP地址和端口号
IP地址的分类和区分
网络字节序(大端序和小端序),htons htonl函数的使用
inet_addr函数使用、inet_aton inet_ntoa函数使用
3.1 分配给套接字的IP地址和端口号
IP是 Internet Protocol (网络协议)的简写,是为收发网络数据而分配给计算机的值
端口号是另一回事,不是给计算机的值,是给指定ip下的应用程序的需要。
3.1.1 网络地址
为了是计算机连网,并且收发数据,必须分配IP地址,IP地址分两类
1)IPv4:(Internet Protocol version 4) 4字节地址族
2)IPv6:(Internet Protocol version 6) 16字节地址族
目前通用的是IPv4,标准的四字节IP地址分为网络地址和主机地址,并且分为了A B C D E等类型
3.1.2 网络地址分类与主机地址边界
区分是哪类IP地址
1.A类地址的首字节范围:0~127
2.B类地址的首字节范围:128~191
3.C类地址的首字节范围:192~223
4.A类地址的首位以0开始
5.B类地址的前两位以10开始
6.C类地址的前三位以110开始
3.1.3 用于区分套接字的端口号
上面说的IP地址是用来区分计算机的(前半部分的网络ID进入网络,后半部分的主机ID确定机器)
但是一个机器有很多应用程序啊,比如视频播放和音乐播放等等,这个怎么确定传给哪个应用程序数据呢?这就需要端口号啦~
实际上,计算机中有NIC(network internet card 网络接口卡)数据传输设备。 通过NIC向计算机内部传输数据时会用到IP。
操作系统负责吧传递到内部的数据分配给相应客户端的套接字,怎么确定分给哪个?就看端口号啦~
1.端口号是为了在同一个操作系统内区分不同的套接字设置的,因此不能将同一个端口号分配给不同的套接字
2.端口号的范围是0-65535
3.端口号不能重复,但是TCP套接字和UDP套接字不会共用端口号,所以允许重复。(TCP套接字用了1111端口号,其他TCP不能用这个了,但是UDP可以用)
3.2 地址信息的表示
应用程序中使用的IP地址和端口号以结构体的形式给出定义。
3.21. 表示IPv4地址的结构体
介绍一个专门用来保存IPv4信息的结构体:sockaddr_in结构体
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Adress Family)
uint16_t sin_port; //16位TCP/UDP 端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
};
在这里插入代码片
其中一个结构体in_addr的定义如下:
struct in_addr
{
In_addr_t s_addr; //32位IPv4地址
};
在这里插入代码片
分析一下结构体sockaddr_in的成员:
协议族:IPv4 和 IPv6
地址族:IPv4 使用4字节地址族,IPv6使用16字节地址族
第一个成员:sin_family
每种协议族使用的地址族不相同。
可以参考下面的表选择 sin_family 地址信息
一般选择AF_INET即可。
第二个成员sin_port
该成员保存16位端口号,重点在于它以 网络字节序 保存
第三个成员sin_addr
重点!!! 这个成员就是保存ip地址的呀!并且也以网络字节序保存
因为这个成员本身又是一个结构体,来看看这个成员的结构体中的成员
里面是一个 In_addr_t 类型的 s_addr 其实就是 unint_32 类型的一个数据
第四个成员sin_zero
没有特殊含义,就是为了结构体 sockaddr_in 的大小和 sockaddr结构体一样大加入的成员
我们假设现在用 sockaddr_in 结构体的变量作为参数传入 bind函数中,需要什么样的调用呢?
struct sockaddr_in serv_addr;
......
if(bind(serv_sock,(struct sockaddr_in*) &serv_addr,sizeof(serv_addr)) == -1)
{
error_hangling("bind() error!");
}
......
在这里插入代码片
其实这里的强制类型转换是完全没有必要的,因为我们声明的时候就是使用的sockaddr_in结构体类型
那最上面我们写的 sockaddr 结构体是什么呢
struct sockaddr
{
sa_family_t sin_family; // 地址族(Address Family)
char sa_data[14]; // 地址信息
}
在这里插入代码片
1)此结构体的成员 sa_data 保存的地址信息中 需要包含IP地址和端口号,剩余的部分应该填充0,这是bind函数需要的。
而这对于包含地址信息来讲非常麻烦,继而有了新的结构体 也就是我们上面一直说的那个 sock_addr_in
如果按照 sock_addr_in 结构体,这将生成符合bind函数要求的字节流。最后强制转换为sockaddr 型的结构体变量地址,再传给bind函数即可。
2)说白了!sock_addr_in 好用!但是!最后还是得用转变成的 sock_addr!
3)还有一个小问题:
为什么用 sockaddr_in 中 为什么要有sin_family 呢? (sin_family // 地址族信息 IPv4 一般选 AF_INET)
我们用这个参数来指定地址族的类型,但是IPv4 面向连接的协议 只有4字节地址族类型,而且sockaddr_in是专门为IPv4设计的信息保存的结构体呀,为什么还用这个再说一遍呢?
因为 sockaddr 并非只为IPv4设计,这从保存地址信息的数组 sa_data长度为14字节可以看出。 因此结构体 sockaddr要求在sin_famuly中指定地址族的信息。为了与sockaddr保持一致,sockaddr_in中也有地址族信息。
3.3.1 字节序和网络字节序 重点!!!!!!!!!!!
CPU向内存保存数据有2中方式,解析同样对应两种方式
1)大端序(big endian):高位字节序放到低位地址
2)小端序(little endian):高位字节序放到高位地址
如下图所示,0x20号开始的地址中保存4字节int类型数 0x12345678.(这是16进制的数,可以表示为 8位二进制的数也就是4个字节)其中 0x12 是最高位字节,0x78 是最低位字节
小端序
大端序
图中:0x20-0x23表示地址 :0x12 是最高位字节,0x78 是最低位字节
Intel系列CPU都是以小端序方式保存数据。
因为有个大小端的区别,因此会出现问题:
那么如果接收端是小端序的系统,发送数据的服务器是大端序怎么整?
(比如:服务器发送 0x1234 从低位地址开始发送,先发送 0x12,再发送 0x34; 客户端接收数据,先存到低位地址 0x12 再 0x34。 因为是小端序系统,解读为 0x3412),其中12是高位字节,34是低位字节;
为了避免以上问题的出现,然后我们进行统一规定:
所以!! 我们在进行网络通讯传输数据时约定了一种统一的方式:统一为大端序! 这种约定就叫:网络字节序
也就是说,我们在网络传输之前先把数据数组转变成大端序格式,再进行网络传输,因此所有计算机接收数据是应识别该数据是网络字节序格式!(其实也就是大端序格式)
3.3.2 字节序转换
介绍几个用于转换字节序的函数
1) unsigned short htons(unsigned short);
2) unsigned short ntohs(unsigned short);加粗样式
3) unsigned long htonl(unsigned long);
4) unsigned long ntohl(unsigned long);
理解htons的含义:
htons
h 主机host字节序; n 网络network字节序 ; s为short
含义就是:把short类型的数据从主机字节序转换为网络字节序
在Linux中,short 占两个字节, long 占4个字节
因此用s后缀的函数来转换端口号信息,l后缀的函数来转换端口号信息。
案例实现:自己敲一下感受
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
unsigned short host_port = 0x1234; // 没转换之前的字节序
unsigned long host_addr = 0x12345678;
unsigned short net_port; // 转换后的 网络字节序
unsigned long net_addr;
net_port = htons(host_port);
net_addr = htonl(host_addr);
printf("主机字节序端口号为:%#x\n",host_port);
printf("主机字节序IP地址为:%lx\n",host_addr);
printf("网络字节序端口号为:%#x\n",net_port);
printf("网络字节序IP地址为:%lx\n",net_addr);
return 0;
}
在这里插入代码片
3.4 网络地址的初始化与分配
3.4.1 将字符串信息转换为网络字节序的整数型
“211.214.107.99” -> 转变为 32位整数型数据 (4字节)怎么变呢?
下面这个函数在完成上面过程的同时,还能完成网络字节序的转换,还能检测无效的IP地址!
功能强大!
#include <arpa/inet.h>
in_addr_t inet_addr(const char* string);
->成功时返回32位(4字节)大端序整数型值,失败时返回 INADDR_NONE.
在这里插入代码片
测试代码:inet_addr.c
#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 occured!\n");
}
else{
printf("Network ordered integer addr: %#lx\n", conv_addr);
}
}
在这里插入代码片
再介绍另一个函数
inet_aton 它与上面的 inet_addr 功能上相同,也将字符串形式的IP地址转换为32位(4字节)网络字节序返回。只不过这个函数利用了 in_addr 结构体,并且使用的更多一些~
in_addr结构体是 sockaddr_in 中的 sin_addr 的类型(下方)
struct sockaddr_in
{
sa_family_t sin_family; //地址族(Adress Family)
uint16_t sin_port; //16位TCP/UDP 端口号
struct in_addr sin_addr; //32位IP地址
char sin_zero[8]; //不使用
};
在这里插入代码片
也就是说inet_aton函数常与sockaddr_in结构体一起使用
#include <arpa/inet.h>
int inet_aton(const char* string,struct in_addr* addr);
-> 成功时返回1(TRUE) 失败时返回0 (false)
string:含有需要转换的IP地址字符串地址值
addr: 保存转换结果的 in_addr结构体变量 的地址值
(in_addr结构体 就是 sock_addr_in.sin_addr结构体,这里面保存的是s_addr 真正存放ip的名字)
在这里插入代码片
这俩的差别就在于,一个参数中是 in_addr结构体变量 一个是IP地址字符串,所以说第二个在使用的时候更方便,传入的结构体变量地址,可以直接把结果填到该结果中了
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;
}
void error_handling(char* message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(1);
}
在这里插入代码片
3.4.2 网络地址的初始化
流程:
struct sockaddr_in addr; // 初始化一个IPv4的数据存储结构体
char* serv_ip = "211.217.168.13"; // 声明IP地址的字符串
char* serv_port = "9190"; // 声明端口号字符串
memset(&addr , 0 , sizeof(addr)); // 初始化结构体变量addr 的所有成员为 0
addr.sin_family = AF_INET; // 地址族赋值为 IPv4的地址族
addr.sin_addr.s_addr = inet_addr(serv_ip); // 基于字符串的IP地址初始化
addr.sin_port = htons(atoi(serv_port)); // 基于字符串的端口号初始化 (atoi是把字符串转为整型)
// ascII to integer
在这里插入代码片
3.4.3 客户端地址信息的初始化
我们理解一下客户端和服务器之间的关系~
**客户端:**请链接到IPxxxxx、端口号xxxx(发送数据,发送请求)
服务器:请把连接到IPxxx,端口号xxx的 客户端的数据传给我!(接收数据,应答请求,是监听套接字)
当然这不是觉得的,双边都会有请求都会有接收,这是就转变身份~
服务器端的准备工作通过 bind 函数完成,声明 sockaddr_in 结构体变量为其赋值初始化服务器端的IPv4信息
客户端的准备工作通过 connect 函数完成, 声明 sockaddr_in 结构体,并把想要连接的服务器套接字的信息填进去,然后调用 connect 函数连接。
3.4.4 INADDR_ANY 关键字
每次创建服务器端套接字是都要输入 IP地址有些繁琐,可以利用 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));
在这里插入代码片
问题一:为什么创建服务器端套接字时需要IP地址?
问题:初始化服务器端套接字时应分配所属计算机的IP地址,因为初始化时使用的IP地址。非常明确,那为何还要进行IP初始化呢?
如前所述,同一计算机中可以分配多个IP地址,实际IP地址的个数与计算机中安装的NIC的教量相等。
即使是服务器端套接字,也需要决定应接收哪个IP传来的(哪个NIC传来的)数据。因此,服务器端套接字初始化过程中要求IP地址信息。
另外,若只有1个NIC,则直接使用 INADDR ANY。
3.4.5 解读hello server.c hello_client.c的过程
./server 9190
在这里插入代码片
向main函数传递的9190为端口号,通过此端口创建服务器端套接字并运行程序,但未传递IP地址
因为可以通过 **INADDR_ANY指定IP地址。**现在再去读代码会感觉简单很多
./client 127.0.0.1 9190
在这里插入代码片
127.0.0.1 是回送地址( loopback address),指的是计算机自身IP地址。
在第1章的示例中,服务器端和客户端在同一计算机中运行,因此,连接目标服务器端的地址为 127.0.0.1。
当然,若用实际IP地址代替此地址也能正常运转。
如果服务器端和客户端分别在2台计算机中运行,则可以正常输入 服务器端的IP地址
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
3.4.6 向套接字分配网络地址
调用bind函数:
#include <sys/socket.h>
int bind(int sockfd,struct sockaddr *myaddr,socklen_t addrlen);
-> 成功返回0,失败返回-1
sockfd:文件描述符(socket得到的)
myaddr:存有地址信息的结构体变量地址值
addrlen:第二个结构体变量的长度
在这里插入代码片
下面引入一个常见的服务器端初始化套接字的过程:
int serv_sock;
struct sockaddr_in serv_addr;
char* serv_port = "9190";
/* 创建服务器套接字(监听套接字)int socket(int domain, int type, int protocol) */
serv_sock = socket(PF_INET,SOCK_STREAM,0);
/* 地址信息初始化 */
memset(&serv_addr,0,sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(serv_port));
/* 分配地址信息 */
bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
在这里插入代码片