第三章 地址组与数据序列
内容概要
上一章讲了设置套接字 也就是 socket函数
这章主要将如何为套接字分配IP地址和端口号
IP地址的分类和区分
网络字节序(大端序和小端序),htons
htonl
函数的使用
inet_addr
函数使用、inet_aton
inet_ntoa
函数使用
正文
3.1 分配给套接字的IP地址与端口号
IP是 Internet Protocol (网络协议)的简写,是为收发网络数据而分配给计算机的值
端口号是另一回事,不是给计算机的值,是给指定ip下的应用程序的需要。
3.1.1 网络地址(Internet Address)
为了是计算机连网,并且收发数据,必须分配IP地址,IP地址分两类
1)IPv4:(Internet Protocol version 4) 4字节地址族
2)IPv6:(Internet Protocol version 6) 16字节地址族
目前通用 IPv4,其标准的4字节IP地址分为两部分,网络地址和主机地址(主机地址指计算机地址),且分为A\B\C\D\E 等类型
下图展示了几个不同类型的地址族(一般不用E类)
xxxxxxxxxxxxxxx
网络ID(地址)是为了区分网络而设置的一部分IP地址。
例如:
向 WWW.XXXX.COM公司传输数据,并不是4个字节的IP地址都是表示一个东西,
前半部分的网络ID 是用来寻找到XXXX.COM网络的。在该公司的网络路由器接收到数据后,去看IP地址的后半部分,找到主机ID,将数据传输给目标计算机。
以C类IPv4网络协议为例:某主机向 211.112.103.222 输数据,其中 211.112.103 为网络ID,222 为主机ID
XXXXXXXXXXXXXXXXXXXX
3.1.2 网络地址分类与主机地址边界
刚刚说,有ABCDE好几种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。
操作系统负责吧传递到内部的数据分配给相应客户端的套接字,怎么确定分给哪个?就看端口号啦~
端口号就是为了在同一个操作系统内区分不同的套接字而设置的,因此不能将一个端口号分配给不同套接字
端口号的范围 0~65535
端口号不能重复,但是TCP套接字和UDP套接字不会公用端口号,所以允许重复。(TCP套接字用了1111端口号,其他TCp不能用这个了,但是UDP可以用)
3.2 地址信息的表示
应用程序中使用IP地址和端口号以结构体的形式给出定义。
3.2.1表示IPv4地址的结构体
首先,IP地址和端口号的信息一定要存储进去,同时也要表明是使用的是基于IPv4的地址族
下面给出bind函数的参数之一 sockaddr_in
结构体。
int bind(int sockfd,struct sockaddr *myaddr,socklen_t addrlen);
可以看到标准的bind函数里面第二个参数的结构体名字叫 sockaddr,好像和我们说的不太一样哈,一会看看是怎么回事
首先先说 sockaddr_in
结构体(这是一个专门用来保存IPv4信息的结构体)
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
定义如下,她用来存放32位IP地址
struct in_addr
{
In_addr_t s_addr; //32位IPv4地址
};
这里面可能有一些数据类型并不知道是什在这里插入图片描述么重命名得来的,我们来看一下下面的图
按照上面的图片我们来分析一下结构体 sockaddr_in
的成员
协议族:IPv4 和 IPv6
地址族:IPv4 使用4字节地址族,IPv6使用16字节地址族
-
sin_family:
每种协议族使用的地址族不相同。
可以参考下面的表选择 sin_family 地址信息
我们一般就选择 AF_INET 就ok了 -
成员 sin_port
该成员保存16位端口号,重点在于它以 网络字节序 保存(什么是网络字节序呢?往后看) -
成员sin_addr
重点!!! 这个成员就是保存ip地址的呀!并且也以网络字节序保存(又出现了。。到底什么意思)
因为这个成员本身又是一个结构体,来看看这个成员的结构体中的成员(其实就一个,也不知道为啥非得多弄一层)
里面是一个In_addr_t
类型的s_addr
其实就是unint_32
类型的一个数据 -
成员sin_zero
没有特殊含义,就是为了结构体sockaddr_in
的大小和sockaddr
结构体一样大加入的成员(又一次出现了sockaddr
类型的结构体,这到底长啥样= - =),必须填充0,否则得不到想要的结果。
我们假设现在用 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]; // 地址信息
}
此结构体的成员 sa_data
保存的地址信息中 需要包含IP地址和端口号,剩余的部分应该填充0,这是bind函数需要的。
而这对于包含地址信息来讲非常麻烦,继而有了新的结构体 也就是我们上面一直说的那个 sock_addr_in
如果按照 sock_addr_in
结构体,这将生成符合bind
函数要求的字节流。最后强制转换为sockaddr
型的结构体变量地址,再传给bind
函数即可。
说白了!sock_addr_in
好用!但是!最后还是得用转变成的 sock_addr
! 为什么! 因为他是亲儿子!!(具有通用性…吧)
还有一个小问题:
为什么用 sockaddr_in
中 为什么要有sin_family
呢? (sin_family // 地址族信息 IPv4 一般选 AF_INET)
我们用这个参数来指定地址族的类型,但是IPv4 面向连接的协议 只有4字节地址族类型,而且sockaddr_in是专门为IPv4设计的信息保存的结构体呀,为什么还用这个再说一遍呢?
哎,这也是 sockaddr 作为亲儿子的厉害之处。
因为 sockaddr 并非只为IPv4设计,这从保存地址信息的数组 sa_data长度为14字节可以看出。 因此结构体 sockaddr要求在sin_famuly中指定地址族的信息。为了与sockaddr保持一致,sockaddr_in中也有地址族信息。
都得和 sockaddr一样= - =,厉害了。
3.3 网络字节序与地址变换
之前 sock_addr_in
结构体中的 sin_addr
就是保存的IP地址信息的结构体,当时留了一个问题,什么是网络字节序???
讲清楚之前我们要搞懂一件事
不同的CPU中,4字节整数型 1 在内存中的保存方式是不同的。4字节整型值 1 可以用二进制表示如下
00000000 00000000 00000000 00000001
有一些CPU以上面这种顺序保存,另一些以下面这种倒序保存。
00000001 00000000 00000000 00000000
如果不考虑到这些问题,就收发数据,会导致解析顺序不同
3.3.1 字节序(Order)与网络字节序
CPU向内存保存数据有2中方式,解析同样对应两种方式
1)大端序(big endian):高位字节序放到低位地址
2)小端序(little endian):高位字节序放到高位地址
如下图所示,0x20号开始的地址中保存4字节int类型数 0x12345678.(这是16进制的数,可以表示为 8位二进制的数也就是4个字节)其中 0x12 是最高位字节,0x78 是最低位字节
我们的Intel系列CPU都是以小端序方式保存数据。
那么如果接收端是小端序的系统,发送数据的服务器是大端序怎么整?
(比如:服务器发送 0x1234 从低位地址开始发送,先发送 0x12,再发送 0x34
客户端接收数据,先存到 低位地址 0x12 再 0x34。 因为是小端序系统,解读为 0x3412)
所以!! 我们在进行网络通讯传输数据时约定了一种统一的方式:统一为大端序! 这种约定就叫:网络字节序
也就是说,我们在网络传输之前先把数据数组转变成大端序格式,再进行网络传输,因此所有计算机接收数据是应识别该数据是网络字节序格式!(其实也就是大端序格式)
3.3.2 字节序转换(Endian Conversions)
之前说的是,为什么要在填充sockadr_in
结构体前将数据转换成网络字节序,接下来介绍一下帮助转换字节序的函数。
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 -> h to n s:h为主机host字节序 , n为网络network字节序 , s为 short
"把short类型数据从主机字节序转变成网络字节序"
在Linux中,short 占两个字节, long 占4个字节
因此用s后缀的函数来转换端口号信息,l后缀的函数来转换端口号信息。
可能会又问觉得:那我的电脑cpu是大端序的 sockaddr_in 赋值之前就不需要转换字节了呀~
这样其实是没问题的,但是建议还是统一代码,避免出现问题。
下面上代码:
endian_conv.c
#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;
}
下面运行一下代码:(先预测一下,因为我的电脑应该是小端字节序的,因此应该转变为 0x3412 0x78563412
3.4 网络地址的初始化与分配
这里讲解一下 bind函数为代表的结构体的应用。
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);
}
}
下面是调用给结果:
可以看出第一个IP地址没问题,第二个格式不对,被检测出来了
再介绍一个函数: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);
}
下面是运行结果~
最后一个与上面 inet_aton
函数相反的函数,此函数可以把 网络字节序的 整数型IP地址转换成我们熟悉的 字符串形式
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);
->成功时返回转换的字符串地址值,失败时返回-1;
需要注意:返回值类型是 char ,但是并没有给结果分配内存空间,所以别忘了接收结果保存到内存空间中~
如果不保存下次返回新的地址,原来的结果地址里面就是别的结果值啦~。*
下面给出示例
inet_ntoa.c
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
int main(int argc,char* argv[])
{
struct sockaddr_in addr1,addr2; // 主机上的 IPv4信息结构体
char* str_ptr; // 一会用来接收返回值的、
char str_arr[20]; // 一会用来保存返回值地址的值的
addr1.sin_addr.s_addr = htonl(0x1020304); // 将IPv4 的本地字节序转变成网络字节序存进去
addr2.sin_addr.s_addr = htonl(0x1010101); // 同上
str_ptr = inet_ntoa(addr1.sin_addr);
strcpy(str_arr,str_ptr); // char *strcpy(char *dest, const char *src)
printf("十进制整数 第一个:%s\n", str_ptr);
inet_ntoa(addr2.sin_addr); // 故意没有用返回值来接收,看看这个地址里的值是不是变了
printf("十进制整数 第2个:%s\n", str_ptr);
printf("十进制整数 第3个:%s\n", str_arr);
}
运行结果:
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 解读第1章的 hello server.c、 hello client c运行过程
第1章中执行以下命令以运行相当于服务器端的 hello_server.c
./server 9190
通过代码可知,向main函数传递的 9190 为端口号。通过此端口创建服务器端套接字并运行程序,但未传递IP地址,
因为可以通过 INADDR_ANY指定IP地址。现在再去读代码会感觉简单很多。
执行下列命令以运行相当于客户端的 hello_client.c 与服务器端运行方式相比,最大的区别是传递了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 向套接字分配网络地址
上面说了 sockaddr_in 结构体的初始化,接下来就把初始化的地址信息分配给套接字。 即使用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));
4.4 Windows下的实现
暂略