0x12-套接字编程-2

新时代的 套接字网络编程

  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差错报文中总是带着 出错数据报中的一部分真实数据,用于配对。

注意,对于UDP而言,只有connected状态下,才会收到 ICMP 报文,可以通过errno == ECONNREFUSED来确定,具体来说就是在你发送完本次数据之后,的下一次系统调用时会有这个想想,代表你的小心没有被送到对方手里,而是被对方丢弃了。

在没有一个完备的思路以及良好的设计之前,UDP是一个十分艰难的挑战,只有在TCP实在无法满足我们的性能需求时,我们才来重新考虑 UDP

results matching ""

    No results matching ""