网络通信地址
- MAC地址
MAC地址则标识了一个机器的物理地址,在出厂的时候就固定在网卡上,具有唯一性。
- IP地址
IP地址是为收发数据而分配给计算机的唯一的值,是与物理地址(以太网地址)MAC有所区别的逻辑地址,可以说根据IP地址,就能将信息传输给对应的主机。
- 端口号
而一个主机上可能运行多个程序,分配给每个程序的套接字的序号则是端口号,对应的主机又可以通过端口号,将信息传输给对应的程序。
- 下面从流程上简单阐述一下PC-A如何传输数据到PC-B:
1. 由于 B 的 IP 地址并没有和 A 在一个网段,所以当 A 向 B 发送数据时, A 并不会直接把数据给 B ,而是交给自己的网关,也就是 192.168.0.254 ,所以 A 首先会 ARP 广播请求 192.168.0.254 的 MAC 地址,A 得到网关的 MAC 地址后,以它为数据帧的目标 MAC 地址进行封装数据,并发送出去。而这其中的网关实质上是一个网络通向其他网络的IP地址,是一个IP地址,具有路由功能。
2. 网关收到数据之后为适应目标主机,对数据重新打包,然后检查该帧的目标IP,然后根据自己的路由表找到下一站路由的端口,并把其MAC地址作为目标MAC地址,然后把数据帧发出去,这其中又涉及到IP地址中的网络ID
与主机ID
。
3. 路由器A 收到该帧后,检查该帧的目标 IP ,并到自己的路由表查找如何到达该网段(如果在该路由表中可以查到目的地址,则传输给指定的路由,如果目的地址不在路由表,则将信息传输给默认路由,默认路由再去连接另一个路由器,进行同样操作),找到下一跳地址是路由器B的s0端口,于是将数据重新封装,将源MAC地址改为自己的MAC 地址,目标MAC地址改为路由器B的s0端口的MAC 址址,并发送给 routerB。中间路由器传递过程同理。
4. 经过路由器不断转发直到到达目标IP,目标路由发现目标 IP 就在自己的直连网段,于是查看 ARP 缓存,如果找到该 IP 的 MAC 地址,则以该 MAC 地址封装数据发送出去,如果在 ARP 缓存没找到,则发出 ARP 广播,请求该 IP 的 MAC 地址,得到对应的 MAC 地址后,再发送给主机B。
在以上数据传递过程中,我们发现,数据帧的源 IP 和目标 IP 始终是不变的,而经过每个路由进行重新封装数据时 MAC 地址则在不断的变化,总是以自己的地址作为源 MAC 地址,下一跳的地址作为目标 MAC 地址。
涉及知识
-
子网掩码
子网掩码是一种用来指明一个IP地址的哪些位标识的是网络ID,以及哪些位标识的是主机ID的位掩码。不能单独存在,必须结合IP地址一起使用。子网掩码是一个32位地址,用于屏蔽IP地址的一部分以区别网络标识和主机标识,并说明该IP地址是在局域网上,还是在远程网上。
对于A类地址来说,默认的子网掩码是255.0.0.0;对于B类地址来说默认的子网掩码是255.255.0.0;对于C类地址来说默认的子网掩码是255.255.255.0。通过子网掩码,就可以判断两个IP在不在一个局域网内部。以及有多少位是网络号,有多少位是主机号。 -
网关
默认网关在网络层上以实现网络互连,是最复杂的网络互连设备,仅用于两个高层协议不同的网络互连。网关的结构也和路由器类似,不同的是互连层。网关既可以用于广域网互连,也可以用于局域网互连。如上所说,网关实质上是一个网络通向其他网络的IP地址。
比如有网络A和网络B,网络A的IP地址范围为“192.168.1.1 - 192. 168.1.254”,子网掩码为255.255.255.0;网络B的IP地址范围为“192.168.2.1 - 192.168.2.254”,子网掩码为255.255.255.0。在没有路由器的情况下,两个网络之间是不能进行TCP/IP通信的,即使是两个网络连接在同一台交换机(或集线器)上,TCP/IP协议也会根据子网掩码(255.255.255.0)判定两个网络中的主机处在不同的网络里。
而要实现这两个网络之间的通信,则必须通过网关。如果网络A中的主机发现数据包的目的主机不在本地网络中,就把数据包转发给它自己的网关,再由网关转发给网络B的网关,网络B的网关再转发给网络B的某个主机。
所以说,只有设置好网关的IP地址,TCP/IP协议才能实现不同网络之间的相互通信。网关的IP地址是具有路由功能的设备的IP地址,具有路由功能的设备有路由器、启用了路由协议的服务器(实质上相当于一台路由器)、代理服务器(也相当于一台路由器)。
IP地址
IP地址分为两类,IPv4与IPv6。当前主要使用的是IPv4。占有的字节数是4个字节,分为A、B、C、D、E五类。他们的边界有所不同:
- A类地址首字节范围是0-127
- B类地址首字节范围是128-191
- C类地址首字节范围是192-223
- D类地址首字节范围是224-239
- E类地址首字节范围是240-255
将他们转化为二进制也可以说
- A类地址以0开头
- B类地址以10开头
- C类地址以110开头
- D类地址以1110开头
- E类地址以11110开头
端口号
端口号就是同一操作系统内为区分不同的套接字而设置的,所以多个同类型套接字不可以分配同一个端口号。端口号是由16位构成,所以可分配的端口号为0-65535,但是0-1023是知名端口号,所以在网络编程中,我们不可以使用这些端口号。但是不同类型的套接字是可以使用同一端口号的,允许重复,即一个TCP套接字设置了9190端口号,其他的TCP套接字就不可以设置这个端口了,但是UDP套接字可以继续设这个端口。
这是因为在IP数据包的首部有一个协议字段,可以指明上层协议的类型,TCP类,该协议字段为6,UDP类,该协议字段为17。操作系统会根据IP数据包的首部中的协议类型来判断是上层协议是TCP还是UDP,从而将数据包交给相关的内核或者协议栈进行处理。
网络通信地址的表示
- 第一种表示方法
在网络编程中,如果使用IPv4,则采用如下结构体来表示其网络通信地址,然后将上述结构体传递给bind()函数,即可设置对应套接字的网络通信地址。
struct sockaddr_in
{
sa_family_t sin_family; // 用来指明地址族
uint16_t sin_port; // 16位TCP/UDP端口号
struct in_addr sin_addr; // 32位IP地址
char sin_zero[8]; // 一般不使用
}
sin_family:
每种协议族所对应的地址族是不同的,当使用IPv4协议族的时候,地址族是4字节,将sin_family
设置为AF_INET
;当使用IPv6协议族时、地址族则是16字节,sin_family
设置为AF_INET6
;本地通信则使用UNIX协议的地址族,sin_family
设置为AF_LOCAL
。
sin_port:
以网络字节序来保存16位的端口号。因为是16位的,所以根据二进制转换,端口号的选择范围是0-65535,而0-1023是知名端口,一般用来分配给特定程序,所以这项设置一般采用之后的端口。
sin_addr:
上述结构体是用来保存整个网络通信地址的,而其中IPv4又是使用一个独特的结构体来保存。
struct in_addr
{
In_addr_t s_addr;
}
sin_zero:
用来将sockaddr_in
结构填充到与struct sockaddr
同样的长度,可以用bzero()
或memset()
函数将其置为零。指向sockaddr_in
的指针和指向sockaddr
的指针可以相互转换。
- 第二种表示方法:
上面的第一种表示方法,主要是让开发人员对地址进行设置,但对于操作系统来说,他们则是识别的这种表示方法:
struct sockaddr
{
sa_family_t sin_family; // 用来表示地址族信息
char sa_data[14]; // 保存地址信息
}
但是这种方法将目标地址和端口信息混合在了一起,不便于开发人员对其进行设置,所以当我们需要设置这些网络通信地址的时候,我们要将其转换为sockaddr_in
类型,当我们要将其传给bind()、connect()、recvfrom()、sendto()
等函数让操作系统识别的时候,要转换为sockaddr
类型。例如传给bind()
函数:
struct sockaddr_in serv_addr;
if(bind(serv_sock, (sockaddr *)&servaddr, sizeof(servaddr)) == -1)
error_handing("bind() error");
我们首先定义了一个sockaddr_in
结构体类型的变量,然后在传给bind()函数的时候将其转为 sockaddr *
类型。
- IPv4的点分十进制格式
上面是整个网络通信地址的表示形式,那么机器识别IP的时候是以二进制来识别的,比如:
11000000 10101000 00000001 00000001
它的点分十进制形式就是192.168.1.1
。
大端序与小端序
CPU向内存中保存数据的方式有2种:
- 大端序:高位字节存放在低位地址
- 小端序:高位字节存放在高位地址
比如说要将一个4字节int类数 0x12345678 存放在 0x20 号开始的地址中,大端序存储方式如下:
0x20号 | 0x21号 | 0x22号 | 0x23号 |
---|---|---|---|
0x12 | 0x34 | 0x56 | 0x78 |
这其中0x20号内存就是低位地址,0x23号内存就是高位地址,0x12就是高位字节,0x78就是低位字节。
小端序存储方式则如下:
0x20号 | 0x21号 | 0x22号 | 0x23号 |
---|---|---|---|
0x78 | 0x56 | 0x34 | 0x12 |
网络字节序与主机字节序
网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。网络字节序采用大端排序方式。
不同的机器主机字节序不相同,与CPU设计有关,数据的顺序是由cpu决定的,而与操作系统无关。我们把某个给定系统所用的字节序称为主机字节序。由于这个原因不同体系结构的机器之间无法通信,所以要转换成一种约定的数序,也就是网络字节顺序。
网络字节序与主机字节序之间的转换函数:htons(), ntohs(), htons(),htonl(),位于头文件 <netinet/in.h>,htons和ntohs完成16位无符号数的相互转换,htonl和ntohl完成32位无符号数的相互转换。h代表主机(host)字节序,n代表网络(network)字节序,s表示short,l表示long,在Linux中,long类型占用4个字节。
- 在使用little endian的系统中,这些函数会把字节序进行转换;
- 在使用big endian类型的系统中,这些函数会定义成空宏;
在网络程序开发时 或是跨平台开发时,也应该注意保证只用一种字节序,不然两方的解释不一样就会产生bug。
- 点分十进制进行大小端顺序与转换
用IP地址127.0.0.1
为例:
第一步:127.0.0.1
把IP地址每一部分转换为8位的二进制数。
第二步:01111111 00000000 00000000 00000001
= 2130706433
(主机字节序)
第三步:从右往左重排变为:00000001 00000000 00000000 01111111
= 16777343
(网络字节序)
网络地址的初始化与分配
在实际的开发当中,我们在设置IP地址的时候,往往是通过传入字符串来设置的,但是保存网络地址的sockaddr_in
中保存IP的形式是32位整数型,所以还需要将字符串类型的IP转换为32位整数型的IP,可以通过以下函数来达到这一目的:
- inet_addr
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);
尝试调用一下这个函数,来将127.0.0.1
转化位32位整数
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
char *addr1 = "127.0.0.1";
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\n", conv_addr);
return 0;
}
运行结果如下:
从运行结果可以看出,inet_addr
函数具有三个功能:
- 将字符串转换为32位整数型
- 将IP地址的格式转换为网络字节序(大端序)
- 可以检查IP是否有效,因为我们设置了一个IP中某位超过了255,显示
Error occured
。
- inet_addr:
#include <arpa/inet.h>
in_addr_t inet_aton(const char *string, struct in_addr *addr);
同样调用一下这个函数,来将127.0.0.1
转化位32位整数
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
void error_handing(char *message);
int main(int argc, char *argv[])
{
char *addr = "127.0.0.1";
struct sockaddr_in addr_inet;
if(!inet_aton(addr, &addr_inet.sin_addr)) error_handing("conversion error");
else printf("Network ordered integer addr: %#x \n", addr_inet.sin_addr.s_addr);
return 0;
}
void error_handing(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
运行结果如下:
inet_aton
与inet_addr
的不同之处在于,inet_addr
在转换完成后,需要将其传给sockaddr_in
中in_addr
类型的保存IP的变量,而inet_aton
可以通过将in_addr
类型的变量传入函数,直接将转换后的IP保存在这个变量中。
上述两个函数可以将字符串转换为网络字节序的32位十进制,以下函数则可以进行反向的转换:
- inet_ntoa
#include <arpa/inet.h>
char * inet_aton(struct in_addr addr);
对其进行调用进行一下实验:
#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 = 0x100007f;
addr2.sin_addr.s_addr = 0x1010101;
str_ptr = inet_ntoa(addr1.sin_addr);
printf("转换后的IP:%s \n", str_ptr);
strcpy(str_arr, str_ptr);
inet_ntoa(addr2.sin_addr);
printf("转换后的IP:%s \n", str_ptr);
printf("转换后的IP:%s \n", str_arr);
return 0;
}
实验结果如下:
对比代码与运行结果我们可以发现,inet_ntoa
函数在转换完成,将其保存到内存后,就绑定了那块内存,之后再进行转换,会直接覆盖掉那块内存,所以我们通过inet_ntoa
函数转换完成后,要及时将转换结果复制到其他内存中。
INADDR_ANY
以上是我们直接设定IP
以及针对IP进行的一系列操作,服务器也可以IP也可以设置为INADDR_ANY
,代表的IP是0.0.0.0
,即本机上所有的IP,那么我们客户端只要申请连接的时候,使用的IP是服务器机子上任意网卡的IP,不管是哪个,都可以与服务器进行通信。
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);