现代TCP/IP网络编程-启航

概念

  • 最具误导性的当属于 TCP/IP 协议了
    • 所谓 TCP/IP 协议指的并不是一个协议,往往在生活中听见的术语如:IP地址TCP连接 等,总会被误导,以为就是一个东西
    • 实际上它们都是彼此独立的 协议 ,只不过会相互合作罢了
    • TCP/IP说的是一个 协议族 ,也就是说是一堆协议的统称
  • 对比 OSITCP/IP 参考模型:
OSI TCP/IP
应用层 表示层 会话层 应用层
传输层 传输层
网络层 网络层
链路层 物理层 网络接口层
  • 其中最常接触的
    • 位于 网络层IP 协议,大家所熟知的 IP地址 就是由它进行封装并传往下一层
    • 位于 传输层TCP/UDP 两个协议, 一个是面向连接(STREAM), 一个是面向数据(DGRAM)的,实际上还有一个但这里不记录。
    • 查看自身 网络信息的办法
      • *nix: 在 Terminal 中输入 ifconfig -a
      • Windows: 在 PowerShell 中输入 ipconfig
  • 概念模糊的 DNS
    • 其实很简单,它的作用就是用来找到域名所对应的 IP地址
    • 为什么?因为 IP地址 太难记了!如果你觉得 IPv4 地址还难不倒你,那请你试试 IPv6
    • 怎么查看域名对应的 IP地址,当然先不考虑 CDN
      • *nixWindows 都可以通过 ping <domain name> 命令进行查询
  • MAC地址端口号
    • 对于前者,实际上应该是最熟悉不过的,对于网络上的主机而言,每一台主机就有一个专属的 MAC地址
    • 后者则是相当于一个房子的门,这个比喻在各大教材中广泛引用,但也的确贴切,假设 IP地址 是房子的地址,那么到了别人家要知道门在哪才行。

一个完整的应用程序传输数据时候 封装 的过程(从右二向左依次封装):

以太网首部 IP TCP/UDP 真实数据 尾部
MAC地址 IP地址 TCP或者UDP协议 应用程序数据 效验码
源和目的MAC地址以及 及前层协议类型 源和目的端口号及前层应用程序首部信息 应用软件信息和真正的数据

其中端口号实际上就是 应用程序的信息

接收数据时的 拆解 顺序与 封装 正好相反。

  • 其中在传输过程中,作为接收方最开始使用的是 网络接口层/数据链路层 的驱动程序(即操作系统自带或另行安装,总之不用使用的程序员写就对了),来判断这个包是否属于我,判断的依据就是 MAC地址,如果是再判断什么协议

    • 在此处的协议可不止 IP协议, 也可能是 ARP协议 等。之后就是就事论事交给相应的处理软件去处理(拆解)就行
    • 科普: MAC地址是 48bit 的, 前24bitIEEE 分配, 后24bit 由厂商分配。原则上是唯一的。
  • MAC地址IP地址

    • 既然前方说到 MAC地址IP地址 都能够作为识别另一个主机的唯一标识,但是为什么需要有两个相同功能的东西?
    • 是,在一开始,网络很小的情况下,例如我们在同一个局域网中,我们之间需要通信的时候,只需要使用ARP协议,进行广播,向在一个网络中的所有主机发送消息就行,剩下的就让其他主机去判断(通过MAC地址)这个数据是不是发给我的。
      • ARP协议 的作用就是在同一个网络中,通过 广播 找出符合自己要求的主机的 MAC地址 ,如果不在同一个网络中,又想知道对方的 MAC地址, 那只能借助把每个网络链接在一起的 网关 来帮助你发送 。 总之进行网络通信时必须知道对方的 IP地址 和 MAC地址
    • 但是如果是现在整个互联网呢?不算 IPv6 ,就算 IPv4 也是几十亿的存在,如果我从中国向国外发送信息,广播整个互联网的所有主机,那就炸了!
    • 所以我们需要对世界网络进行分区,让大区域包含小区域,就像国家-省-市区… , 很遗憾的是 MAC地址 是跟计算机相关而不是和位置相关的。所以我们有了 IP协议
    • IP协议 所附带的产品 IP地址 的作用就在帮助计算机识别自己是否在同一个网络中( 这里省略了子网掩码的作用 )。
  • 实际上,在进行网络编程的时候,以上细节几乎都被隐藏起来,留给我们的只是可供使用的接口。

也许,许多大学计算机基础课程,会讲到 IP地址 有种类,分为 A,B,C…类,老师还介绍了各种类型的地址范围。

但是在现代,这种分类早已经失效,或者说正在逐渐消失,因为当下的 IP 地址的 子网掩码 可以是任意位,并以反斜杠跟在 IP地址后方。

现代的 IP地址 形式一般如此 1.185.223.1/24 代表着子网掩码是由 24个 从左至右连续的的二进制1 组合而成,其余位为0。

夹在中间

事实上有一些实用且挺炫酷的函数,可以先提一下

  • 域名 和 IP地址 的互查
    • gethostbyname 用于域名查找 IP信息及各类信息
      • struct hostent * gethostbyname(const char * hostname)
      • struct hostent 是存储查找到的各类型信息,后方会有介绍
      • hostname 即要查询的域名
    • gethostbyaddr 用于IP地址查找 域名及各类信息
      • struct hostent * gethostbyaddr(const char * addr, socklen_t len, int family)
        • addr 是要查询的 IP地址,之所以是 const char * 是因为C语言历史遗留的原因,实际上其类型应为 struct in_addr *(IPv4)
        • len 地址的长度,即 IPv4 为4, IPv6 为16
        • family 即协议的种类, IPv4AF_INET, IPv6AF_INET6
struct hostent 的成员 . 类型 . 解释
h_name char * 官方名称
h_aliases char ** 域名集合,以NULL结尾
h_addrtype int 地址族的类型 AF_INET 或 AF_INET6
h_length int 地址的长度 4 或 16
h_addr_list char ** IP的集合,以NULL结尾, 实际上每个元素的类型为 struct in_addr*
  • 其中第二和最后一个是关注的重点所在,可以在调用函数之后,输出信息

    实际上,这并不是一个好的方法,在后方将记录 现代人的我们 该如何做到这些事情,以上只是以前的TCP/IP 编程

只适用于 IPv4

套接字网络编程初始

选择使用 C 语言进行编程

  • 在网络编程中,最常实用的两种连接方式 TCPUDP
  • 最常编程的平台 POSIX 标准->*nix平台标准Windows 平台标准
    • 实际上,后者也是参考前者进行一些细微的改变(指的是接口)

对比两种不同连接方式的不同地位的创建,使用

TCP服务器 TCP客户端 UDP服务器 UDP客户端 注释
socket() socket() socket() socket() 创建套接字
bind() bind() bind() 绑定所分配IP地址和端口号
listen() connect() 客户端则绑定IP地址和端口号,并等待连接;服务器则是等待连接
accept() 服务器接受连接
sendto/recvfrom() sendto/recvfrom() 对于UDP即是连接也是操作
close() close() close() close 双向直接关闭连接
shutdown() shutdown() shutdown() shutdown() 可选择方向的关闭连接,即更加灵活

如此对比虽然有一些小瑕疵,但是能够大体上反映出真个网络编程上不同方式的区别

注1: 对于 sendto recvfrom 这两个接口函数,并不一定是只能用在 UDP类型的 套接字上,同样 TCP类型的 套接字也能使用,但是这么做并没有什么意义。

注2: 实际上 UDP 没有所谓的 服务器和和护短,因为本来就是单纯的互相发来发去。客户端端口 一般是随机的

以上是 *nix平台下的标准, Windows下的操作方式和 API有细微不同,但大部分是一致的。

Windows *nix
socket() socket()
bind() bind()
connect() connect()
listen() listen()
accept() accept()
closesocket() close()
send() send()
read() read()
sendto() sendto()
recvfrom() recvfrom()

不仅仅是接口名字相同,参数个数以及功能也是一致,即使有一个例外,其参数以及使用方法也相同。

那岂不是可以直接移植了?

并不!

Windows 套接字编程时 , 由于 Windows 将其实现为动态库,所以在使用时需要将其加载进程序。

故而多加了加载操作。

int WSAStartup(
  WORD      wVersionRequested,
  LPWSADATA lpWSAData  /* 这是一个结构体, 传入类型为WSADATA*  */
);
int WSACleanup(void);

每当在 Windows 上进行套接字编程时,总要指定某个版本的套接字库:

WSADATA wsaData;
int err_code;
/*
* MAKEWORD()的作用在于将版本号转为指定格式传入
* 当下(2015-10)套接字库的版本号最高是 2.2
*/
err_code = WSAStartup(MAKEWORD(2, 2), &wsaData);
/* TODO Something */
WSACleanup();

这是最基本的在 Windows 上使用 套接字 编程的流程,但是如果本平台的套接字库最高版本并不符合当前要求呢?

那么首先会将套接字版本库尽可能设置到平台的 最高版本 ,可以通过结构体 WSADATA 进行查询

if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
  printf("Could not find a usable version of Winsock.dll\n");
  WSACleanup();
  return 1;
}

总体而言, Windows平台*uix平台 的区别在于,前者使用时需要 加载和清除 套接字库
其余逻辑流程一致,毕竟只有统一才能越利于编程世界的发展。

套接字编程

  • 两种协议 TCPUDP
    • 前者可以理解为有保证的连接,后者是追求快速的连接
    • 当然最后一点有些 太过绝对 ,但是现在不需熬考虑太多,因为初入套接字编程,一切从简
    • 稍微试想便能够大致理解, TCP 追求的是可靠的传输数据, UDP 追求的则是快速的传输数据
    • 前者有繁琐的连接过程,后者则是根本不建立可靠连接(不是绝对),只是将数据发送而不考虑是否到达。

以下例子以 *nix 平台的便准为例,因为 Windows平台需要考虑额外的加载问题,稍作添加就能在 Windows 平台上运行

UDP

  • UDP

    • 这是一个十分简洁的连接方式,假设有两台主机进行通信,一台只发送,一台只接收。
    • 接收端:

      int sock; /* 套接字 */
      socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */
      char mess[15];
      char get_mess[GET_MAX]; /* 后续版本使用 */
      struct sockaddr_in recv_host, send_host;
      
      /* 创建套接字 */
      sock = socket(PF_INET, SOCK_DGRAM, 0);
      
      /* 把IP 和 端口号信息绑定在套接字上 */
      memset(&recv_host, 0, sizeof(recv_host));
      recv_host.sin_family = AF_INET;
      recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
      recv_host.sin_port = htons(6000); /* 使用6000 端口号 */
      bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
      
      /* 进入接收信息的状态 */
      recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
      
      /* 接收完成,关闭套接字 */
      close(sock);
      

      上述代码省略了许多必要的 错误检查 ,在实际编写时要添加

    • 代码解释:
      1. PF_INET 代表协议的类型,此处代表 IPv4 网络协议族, 同样 PF_INET6 代表 IPv6 网络协议族,这个范围在后方单独记录,不与IPv4混在一起(并不意味着更复杂,实际上更简便)。
      2. AF_INET 代表地址的类型,此处代表 IPv4 网络协议使用的地址族, 同样有 AF_INET6 (在操作系统实现中 PF_INET 和 AF_INET 的值一样,但是还是要写宏更好,而不应该直接用数字或者,混淆使用)
      3. htonlhtons 两个函数的使用涉及到 大端小端问题, 这里不叙述,需要记住的是在网络编程时一定要使用这种函数将必要信息转为 大端表示法
      4. (struct sockaddr *) 这个强制转换是为了参数的必须,但不会出错,因为 sizeof(struct sockaddr_in) == sizeof(struct sockaddr) 具体可以查询相关信息,之所以这么做是为了方便编写套接字程序的程序员。
    • 发送端:
      int sock;
      const char* mess = "Hello Server!";
      char get_mess[GET_MAX]; /* 后续版本使用 */
      struct sockaddr_in recv_host;
      socklen_t addr_len;
      /* 创建套接字 */
      sock = socket(PF_INET, SOCK_DGRAM, 0);
      /* 绑定 */
      memset(&recv_host, 0, sizeof(recv_host));
      recv_host.sin_family = AF_INET;
      recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
      recv_host.sin_port = htons(6000);
      /* 发送信息 */
      /* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */
      sendto(sock, mess, strlen(mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
      /* 完成,关闭 */
      close(sock);
      
      上述代码是发送端。
    • 代码解释:
      1. inet_addr 函数是用于将字符串格式的 IP地址 转换为 大端表示法的 地址类型,即 s_addr 的类型 in_addr_t
      2. 与之相反,同样也有功能相反的函数 inet_ntoa 用于将 in_addr_t 类型转为字符串,但是使用时一定要记住及时拷贝返回值
        char addr[16];
        recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
        strcpy(addr, inet_ntoa(recv_host.sin_addr.s_addr));
        
    • 从上述代码看出, UDP 协议的使用十分简洁,几乎就是 创建套接字->准备数据->装备套接字->发送/接收->结束
    • 其中,都没有连接的操作,但是实际上这是为了方便 UDP 随时和 不同的主机 进行通信所默认的设置,如果需要和相同主机一直通信呢?
    • 此中的原由暂时不需要知道,记录方法,即长时间使用 UDP 和同一主机通信时,可以使用 connect 函数来进行优化自身。此时 假设两台主机的实际功能一致,既接收也发送
    • 发送端:
      /* 前方高度一致,将 bind函数替换为 */
      connect(sock, (struct sockaddr *)&recv_host, sizeof(recv_host); // 将对方的 IP地址和 端口号信息 注册进UDP的套接字中)
      while(1) /* 循环的发送和接收信息 */
      {
        size_t read_len = 0;
        /* 原先使用的 sendto 函数,先择改为使用 write 函数, Windows平台为 send 函数 */
        write(sock, mess, strlen(mess));            /* send(sock, mess, strlen(mess), 0) FOR Windows Platform */
        read_len = read(sock, get_mess, GET_MAX-1); /* recv(sock, mess, strlen(mess)-1, 0) FOR Windows Platform */
        get_mess[read_len-1] = '\0';
        printf("In Client like Host Recvive From Other Host : %s\n", get_mess);
      }
      /* 后方高度一致 */
      
    • 接收端:
      /* 前方一致, 添加额外的 struct sockaddr_in send_host; 并添加循环,构造收发的现象*/
      while(1)
      {
        size_t read_len = 0;
        char sent_mess[15] = "Hello Sender!"; /* 用于发送的信息 */
        sendto(sock, mess, strlen(sent_mess), 0, (struct sockaddr *)&recv_host, sizeof(recv_host));
        read_len = recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len)
        mess[read_len-1] = '\0';
        printf("In Sever like Host Recvive From other Host : %s\n", mess);
      }
      /* 后方高度一致 */
      /*
      * 之所以只在接收端使用 connect 的原因,便在于我们模拟的是 客户端-服务器 的模型,而服务器的各项信息是不会随意变更的
      * 但是 客户端就不同了,可能由于 ISP(Internet Server Provider) 的原因,你的IP地址不可能总是固定的,所以只能
      * 保证 在客户端 部分注册了 服务器 的各类信息,而不能在 服务器端 注册 客户端 的信息。
      * 当然也有例外,例如你就想这个软件作为私密软件,仅供两个人使用, 且你有固定的 IP地址,那么你可以两边都connect,但是
      * 一定要注意,只要有一点信息变动,这个软件就可能无法正常的收发信息了。
      */
      
    • 代码解释
      • 故而实际上,虽然前方的表格显示,UDP 似乎并没有 connect 的使用必要,但是实际上还是有用到的地方。
      • *nixAPI 来说,sendtowrite 的区别十分明显,便是一个需要在参数中提供目标主机的各类信息,而后者则不需要提供。同样的道理recvfromread也是如此。
      • 这个代码只是做演示而已,所以将代码置于无限循环当中,现实中可以自行定义出口条件。

以上是 UDP 的一些简单说明,入门足矣,并未详细叙述某些 函数 的具体用法,而是用实际例子来体现。
在 记录 TCP 之前,还是需要讲一个函数 shutdown

  • shutdownclose(closesocket)
    • 首先要知道,网络通信一般而言是双方的共同进行的,换而言之就是双向的,一个方向只用来发送消息,一个方向只用来读取消息。
    • 这就导致了,在结束套接字通信的时候,需要关闭两个方向的通道(暂时叫它们通道),那同时关闭不行吗?可以啊
      • close(sock); // closesocket(sock); FOR Windows PlatForm 就是这么干的,同时断开两个方向的连接。
      • 简单的通信程序或者单向通信程序这么做的确无甚大碍,但是万一在结束通信的时候需要接收最后一个信息那该怎么办?
        • 假设通信结束,客户端向服务器发送 “Thank you”
        • 服务器需要接收这个信息,之后才能关闭通信
        • 问题就在这之间,服务器并不知道客户端会在通信结束后的什么时刻传来信息
        • 所以我们选择在通信完成后先关闭 服务器的 发送通道(写流),等待客户端发来消息后,关闭剩下的 接收通道(读流)
    • 发送端:
      /* 假设有一个 TCP 的连接,此为客户端 */
      write(sock, "Thank you", 10);
      close(sock); // 写完直接关闭通信
      
    • 接收端:
      /* 此为服务器 */
      /* 首先关闭写流 */
      shutdown(sock_c, SHUT_WR);
      read(sock_c, get_mess, GET_MAX);
      printf("Message : %s\n", get_mess);
      close(sock_c);
      close(sock_s); // 关闭两个套接字是因为 TCP 服务器端的需要,后续会记录
      
    • 代码解释
      • shutdown 函数的作用就是 可选择的关闭那个方向的输出
        • int shutdown(int sock, int howto);
        • sock 代表要操作的套接字
        • howto有几个选择
          • *nix : SHUT_RD SHUT_WR SHUT_RDWR
          • Windows : SD_RECEIVE SD_SEND SD_BOTH

停下来

  1. 程序员应该越来越来,做的事情应该越来越少,但是能达到的成就应该越来越多
  2. 在 IPv6 出现的今天,网络编程已经开始向简洁和强大靠近,即便是身为底层语言的 C语言
  3. 实际上由于 C语言 并没有自己的网络库, 故为了能进行网络编程,不得不依赖于系统函数,这就是所谓的系统编程, 你已经是一个系统程序员了。
  4. 而 系统函数 随着时代的变化,正在不断完善,增加(几乎没有废除的先例,所以不用担心之前的程序无法运行)。
  5. 相应的,由于以前的网络编程只适合于 IPv4 的地址,自从出现了 IPv6, 我们需要一套全新的方式,正好他来了。

新时代的 套接字网络编程

  1. 首先有几个结构体,以及一个接口十分重要及常用:
    • struct sockaddr_in6 : 代表的是 IPv6 的地址信息
    • struct addrinfo : 这是一个通用的结构体,里面可以存储 IPv4 或 IPv6 类型地址的信息
    • getaddrinfo : 这是一个十分方便的接口,在上述 UDP 程序中许多手动填写的部分,都能够省去,有该函数替我们完成
  2. 改写一下上方的例子:

    • 接收端:

      int sock; /* 套接字 */
      socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */
      char mess[15];
      char get_mess[GET_MAX]; /* 后续版本使用 */
      struct sockaddr_in host_v4; /* IPv4 地址 */
      struct sockaddr_in6 host_v6; /* IPv6 地址 */
      struct addrinfo easy_to_use; /* 用于设定要获取的信息以及如何获取信息 */
      struct addrinfo *result;    /* 用于存储得到的信息(需要注意内存泄露) */
      struct addrinfo * p;
      
      /* 准备信息 */
      memset(&easy_to_use, 0, sizeof easy_to_use);
      easy_to_use.ai_family = AF_UNSPEC; /* 告诉接口,我现在还不知道地址类型 */
      easy_to_use.ai_flags = AI_PASSIVE; /* 告诉接口,稍后“你”帮我填写我没明确指定的信息 */
      easy_to_use.ai_socktype = SOCK_DGRAM; /* UDP 的套接字 */
      /* 其余位都为 0 */
      
      /* 使用 getaddrinfo 接口 */
      getaddrinfo(NULL, argv[1], &easy_to_use, &result); /* argv[1] 中存放字符串形式的 端口号 */
      
      /* 创建套接字,此处会产生两种写法,但更保险,可靠的写法是如此 */
      /* 旧式方法
      *  sock = socket(PF_INET, SOCK_DGRAM, 0);
      */
      /* 把IP 和 端口号信息绑定在套接字上 */
      /* 旧式方法
      *  memset(&recv_host, 0, sizeof(recv_host));
      *  recv_host.sin_family = AF_INET;
      *  recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
      *  recv_host.sin_port = htons(6000); /* 使用6000 端口号 */
      *  bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
      */
      
      for(p = result; p != NULL; p = p->ai_next) /* 该语法需要开启 -std=gnu99 标准*/
      {
        sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if(sock == -1)
          continue;
        if(bind(sock, p->ai_addr, p->ai_addrlen) == -1)
        {
          close(sock);
          continue;
        }
        break; /* 如果能执行到此,证明建立套接字成功,套接字绑定成功,故不必再尝试。 */
      }
      
      /* 进入接收信息的状态 */
      //recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
      switch(p->ai_socktype)
      {
        case AF_INET :
          addr_len = sizeof host_v4;
          recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v4, &addr_len);
          break;
        case AF_INET6:
          addr_len = sizeof host_v6
          recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v6, &addr_len);
          break;
        default:
          break;
      }
      freeaddrinfo(result); /* 释放这个空间,由getaddrinfo分配的 */
      /* 接收完成,关闭套接字 */
      close(sock);
      
      • 代码解释:

        • 首先解释几个新的结构体

          1. struct addrinfo 这个结构体的内部顺序对于 *nixWindows 稍有不同,以 *nix 为例

            struct addrinfo{
              int ai_flags;
              int ai_family;
              int ai_socktype;
              int ai_protocol;
              socklen_t ai_addrlen;
              struct sockaddr * ai_addr; /* 存放结果地址的地方 */
              char * ai_canonname; /* 忽略它吧,很长一段时间你无须关注它 */
              struct addrinfo * ai_next; /* 一个域名/IP地址可能解析出多个不同的 IP */
            };
            
          2. ai_family 如果设定为 AF_UNSPEC 那么在调用 getaddrinfo 时,会自动帮你确定,传入的地址是什么类型的
          3. ai_flags 如果设定为 AI_PASSIVE 那么调用 getaddrinfo 且向其第一个参数传入 NULL 时会自动绑定自身 IP,相当于设定 INADDR_ANY
          4. ai_socktype 就是要创建的套接字类型,这个必须明确声明,系统没法预判(日后人工智能说不定呢?)
          5. ai_protocol 一般情况下我们设置为 0,含义可以自行查找,例如 MSDN 或者 UNP
          6. ai_addr 这里保存着结果,可以通过 调用getaddrinfo之后第四个参数获得。
          7. ai_addrlen 同上
          8. ai_next 同上
          9. getaddrinfo 强大的接口函数

            int getaddrinfo(const char * node, const char * service,
                              const struct addrinfo * hints, struct addrinfo ** res);
            
          10. 通俗的说这几个参数的作用
          11. node 便是待获取或者待绑定的 域名 或是 IP,也就是说,这里可以直接填写域名,由操作系统来转换成 IP 信息,或者直接填写IP亦可,是以字符串的形式
          12. service 便是端口号的意思,也是字符串形式
          13. hints 通俗的来说就是告诉接口,我需要你反馈哪些信息给我(第四个参数),并将这些信息填写到第四个参数里。
          14. res 便是保存结果的地方,需要注意的是,这个结果在API内部是动态分配内存了,所以使用完之后需要调用另一个接口(freeaddrinfo)将其释放
          15. 实际上对于现代的 套接字编程 而言,多了几个新的存储 IP 信息的结构体,例如 struct sockaddr_in6struct sockaddr_storage 等。

            • 其中,前者是后者的大小上的子集,即一个 struct storage 一定能够装下一个 struct sockaddr_in6,具体(实际上根本看不到有意义的实现)

              struct sockaddr_in6{
                u_int16_t sin6_family;
                u_int16_t sin6_port;
                u_int32_t sin6_flowinfo; /* 暂时忽略它 */
                struct in6_addr sin6_addr; /* IPv6 的地址存放在此结构体中 */
                u_int32_t sin_scope_id;  /* 暂时忽略它 */
              };
              struct in6_addr{
                unsigned char s6_addr[16];
              }
              ------------------------------------------------------------
              struct sockaddr_storage{
                sa_family_t ss_family; /* 地址的种类 */
                char __ss_pad1[_SS_PAD1SIZE]; /* 从此处开始,不是实现者几乎是没办法理解 */
                int64_t __ss_align;           /* 从名字上可以看出大概是为了兼容两个不同 IP 类型而做出的妥协 */
                char __ss_pad2[_SS_PAD2SIZE]; /* 隐藏了实际内容,除了 IP 的种类以外,无法直接获取其他的任何信息。 */
                /* 在各个*nix 的具体实现中, 可能有不同的实现,例如 `__ss_pad1` , `__ss_pad2` , 可能合并成一个 `pad` 。 */
              };
              

              在实际中,我们往往不需要为不同的IP类型声明不同的存储类型,直接使用 struct sockaddr_storage 就可以,使用时直接强制转换类型即可

          16. 改写上方 接收端 例子中,进入接收信息的状态部分

            /* 首先将多于的变量化简 */
            // - struct sockaddr_in host_v4; /* IPv4 地址 */
            // - struct sockaddr_in6 host_v6; /* IPv6 地址
            struct sockaddr_storage host_ver_any; /* + 任意类型的 IP 地址 */
            ...
            /* 进入接收信息的状态部分 */
            recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_ver_any, &addr_len); /* 像是又回到了只有 IPv4 的年代*/
            
          17. 补充完整上方对应的 发送端 代码

            int sock;
            const char* mess = "Hello Server!";
            char get_mess[GET_MAX]; /* 后续版本使用 */
            struct sockaddr_storage recv_host; /* - struct sockaddr_in recv_host; */
            struct addrinfo tmp, *result;
            struct addrinfo *p;
            socklen_t addr_len;
            
            /* 获取对端的信息 */
            memset(&tmp, 0, sizeof tmp);
            tmp.ai_family = AF_UNSPEC;
            tmp.ai_flags = AI_PASSIVE;
            tmp.ai_socktype = SOCK_DGRAM;
            getaddrinfo(argv[1], argv[2], &tmp, &result); /* argv[1] 代表对端的 IP地址, argv[2] 代表对端的 端口号 */
            
            /* 创建套接字 */
            for(p = result; p != NULL; p = p->ai_next)
            {
              sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);  /* - sock = socket(PF_INET, SOCK_DGRAM, 0); */
              if(sock == -1)
                continue;
              /* 此处少了绑定 bind 函数,因为作为发送端不需要讲对端的信息 绑定 到创建的套接字上。 */  
              break; /* 找到就可以退出了,当然也有可能没找到,那么此时 p 的值一定是 NULL */
            }
            if(p == NULL)
            {
              /* 错误处理 */
            }
            /* -// 设定对端信息
            memset(&recv_host, 0, sizeof(recv_host));
            recv_host.sin_family = AF_INET;
            recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
            recv_host.sin_port = htons(6000);
            */
            
            /* 发送信息 */
            /* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */
            sendto(sock, mess, strlen(mess), 0, p->ai_addr, p->ai_addrlen);
            /* 完成,关闭 */
            freeaddrinfo(result); /* 实际上这个函数应该在使用完 result 的地方就予以调用 */
            close(sock);                
            
          18. 到了此处,实际上是开了网络编程的一个初始,解除了现代的 UDP 最简单的用法(甚至还算不上完整的使用),但是确实是进行了交互。

#

  • 首先介绍 UDP 并不是因为它简单,而是因为他简洁,也不是因为它不重要,相反他其实很强大。
  • 永远不要小看一个简洁的东西,就像 C语言
  • 下一篇将详细记录 UDP 的相关记录

 在这之前

ARP 协议

  • 最简便的方法就是找一个有 WireShark 软件或者 tcpdump*nix 平台,前者你可以选择随意监听一个机器,不多时就能看见 ARP
    协议的使用,因为它使用的太频繁了。
  • 对于 ARP 协议而言,首先对于一台机器 A,想与 机器B 通信,(假设此时 机器A 的高速缓存区(操作系统一定时间更新一次)中 没有 机器B的缓存),
    • 那么机器A就向广播地址发出 ARP请求,如果 机器B 收到了这个请求,就将自己的信息(IP地址,MAC地址)填入 ARP应答 中,再发送回去就行。
    • 上述中, ARP请求ARP应答 是一种报文形式的信息,是 ARP协议 所附带的实现产品,也是用于两台主机之间进行通信。
    • 这是当 机器A 和 机器B 同处于一个网络的情况下,可以借由本网络段的广播地址 发送请求报文。
  • 对于不同网络段的 机器A 与 机器B 而言,想要通过 ARP协议 获取 MAC地址 ,就需要借助路由器的帮助了,可以想象一下,路由器(可以不止一个)在中间,机器A 和 机器B 分别在这些路由器的两边(即在不同子网)
    • 由于 A 和 B 不在同一个子网内,所以没办法通过通过直接通过广播到达,但是有了路由器,就能进行 ARP代理 的操作,大概就是将路由器当成机器B, A向自己的本地路由器发送 ARP请求
    • 之后路由器判断出是发送给B的ARP请求,又正好 B 在自己的管辖范围之内,就把自己的硬件地址 写入 ARP应答 中发回去,之后再有A向B 的数据,就都是A先发送给路由器,再经由路由器发往B了
    • 一篇比较好的资源是 Proxy ARP

      ICMP

  • 这个协议比较重要,后方的概念也会涉及。
    • 请求应答报文差错报文 ,重点在于差错报文。
    • 请求应答报文在 ICMP 的应用中可以拿来查询本机的子网掩码之类的信息,大致通过向本子网内的所有主机发送该请求报文(包括自己,实际上就是广播),后接收应答,得到信息
    • 差错报文在后续中会有提到,这里需要科普一二。
    • 首先对于差错报文的一大部分是关于 xxx不可达 的类型,例如主机不可达,端口不可达等等,每次出现错误的时候,ICMP报文总是第一时间返回给对端,(它一次只会出现一份,否则会造成网络风暴),但是对端是否能够接收到,就不是发送端的问题了。
    • 这点上 套接字的类型 有着一定的联系,例如 UDP 在 unconnected 状态下是会忽略 ICMP报文的。而 TCP 因为总是 connected 的,所以对于 ICMP报文能很好的捕捉。
    • ICMP差错报文中总是带着 出错数据报中的一部分真实数据,用于配对。

转载注明: http://www.wushxin.top/2015/10/30/%E7%8E%B0%E4%BB%A3TCP-IP%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B-%E5%90%AF%E8%88%AA.html