IP、端口、存储格式、网络通信

网络通信地址

  • 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号
0x120x340x560x78

  这其中0x20号内存就是低位地址,0x23号内存就是高位地址,0x12就是高位字节,0x78就是低位字节。

  小端序存储方式则如下:

0x20号0x21号0x22号0x23号
0x780x560x340x12

网络字节序与主机字节序

  网络字节序是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函数具有三个功能:

  1. 将字符串转换为32位整数型
  2. 将IP地址的格式转换为网络字节序(大端序)
  3. 可以检查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_atoninet_addr的不同之处在于,inet_addr在转换完成后,需要将其传给sockaddr_inin_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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值