0x12-套接字编程-2

新时代的 套接字网络编程

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

    • 接收端:

      1. int sock; /* 套接字 */
      2. socklen_t addr_len; /* 发送端的地址长度,用于 recvfrom */
      3. char mess[15];
      4. char get_mess[GET_MAX]; /* 后续版本使用 */
      5. struct sockaddr_in host_v4; /* IPv4 地址 */
      6. struct sockaddr_in6 host_v6; /* IPv6 地址 */
      7. struct addrinfo easy_to_use; /* 用于设定要获取的信息以及如何获取信息 */
      8. struct addrinfo *result; /* 用于存储得到的信息(需要注意内存泄露) */
      9. struct addrinfo * p;
      10. /* 准备信息 */
      11. memset(&easy_to_use, 0, sizeof easy_to_use);
      12. easy_to_use.ai_family = AF_UNSPEC; /* 告诉接口,我现在还不知道地址类型 */
      13. easy_to_use.ai_flags = AI_PASSIVE; /* 告诉接口,稍后“你”帮我填写我没明确指定的信息 */
      14. easy_to_use.ai_socktype = SOCK_DGRAM; /* UDP 的套接字 */
      15. /* 其余位都为 0 */
      16. /* 使用 getaddrinfo 接口 */
      17. getaddrinfo(NULL, argv[1], &easy_to_use, &result); /* argv[1] 中存放字符串形式的 端口号 */
      18. /* 创建套接字,此处会产生两种写法,但更保险,可靠的写法是如此 */
      19. /* 旧式方法
      20. * sock = socket(PF_INET, SOCK_DGRAM, 0);
      21. */
      22. /* 把IP 和 端口号信息绑定在套接字上 */
      23. /* 旧式方法
      24. * memset(&recv_host, 0, sizeof(recv_host));
      25. * recv_host.sin_family = AF_INET;
      26. * recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IP */
      27. * recv_host.sin_port = htons(6000); /* 使用6000 端口号 */
      28. * bind(sock, (struct sockaddr *)&recv_host, sizeof(recv_host));
      29. */
      30. for(p = result; p != NULL; p = p->ai_next) /* 该语法需要开启 -std=gnu99 标准*/
      31. {
      32. sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
      33. if(sock == -1)
      34. continue;
      35. if(bind(sock, p->ai_addr, p->ai_addrlen) == -1)
      36. {
      37. close(sock);
      38. continue;
      39. }
      40. break; /* 如果能执行到此,证明建立套接字成功,套接字绑定成功,故不必再尝试。 */
      41. }
      42. /* 进入接收信息的状态 */
      43. //recvfrom(sock, mess, 15, 0, (struct sockaddr *)&send_host, &addr_len);
      44. switch(p->ai_socktype)
      45. {
      46. case AF_INET :
      47. addr_len = sizeof host_v4;
      48. recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v4, &addr_len);
      49. break;
      50. case AF_INET6:
      51. addr_len = sizeof host_v6
      52. recvfrom(sock, mess, 15, 0, (struct sockaddr *)&host_v6, &addr_len);
      53. break;
      54. default:
      55. break;
      56. }
      57. freeaddrinfo(result); /* 释放这个空间,由getaddrinfo分配的 */
      58. /* 接收完成,关闭套接字 */
      59. close(sock);
      • 代码解释:

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

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

            1. struct addrinfo{
            2. int ai_flags;
            3. int ai_family;
            4. int ai_socktype;
            5. int ai_protocol;
            6. socklen_t ai_addrlen;
            7. struct sockaddr * ai_addr; /* 存放结果地址的地方 */
            8. char * ai_canonname; /* 忽略它吧,很长一段时间你无须关注它 */
            9. struct addrinfo * ai_next; /* 一个域名/IP地址可能解析出多个不同的 IP */
            10. };
          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 强大的接口函数

            1. int getaddrinfo(const char * node, const char * service,
            2. 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,具体(实际上根本看不到有意义的实现)

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

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

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

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

            1. int sock;
            2. const char* mess = "Hello Server!";
            3. char get_mess[GET_MAX]; /* 后续版本使用 */
            4. struct sockaddr_storage recv_host; /* - struct sockaddr_in recv_host; */
            5. struct addrinfo tmp, *result;
            6. struct addrinfo *p;
            7. socklen_t addr_len;
            8. /* 获取对端的信息 */
            9. memset(&tmp, 0, sizeof tmp);
            10. tmp.ai_family = AF_UNSPEC;
            11. tmp.ai_flags = AI_PASSIVE;
            12. tmp.ai_socktype = SOCK_DGRAM;
            13. getaddrinfo(argv[1], argv[2], &tmp, &result); /* argv[1] 代表对端的 IP地址, argv[2] 代表对端的 端口号 */
            14. /* 创建套接字 */
            15. for(p = result; p != NULL; p = p->ai_next)
            16. {
            17. sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol); /* - sock = socket(PF_INET, SOCK_DGRAM, 0); */
            18. if(sock == -1)
            19. continue;
            20. /* 此处少了绑定 bind 函数,因为作为发送端不需要讲对端的信息 绑定 到创建的套接字上。 */
            21. break; /* 找到就可以退出了,当然也有可能没找到,那么此时 p 的值一定是 NULL */
            22. }
            23. if(p == NULL)
            24. {
            25. /* 错误处理 */
            26. }
            27. /* -// 设定对端信息
            28. memset(&recv_host, 0, sizeof(recv_host));
            29. recv_host.sin_family = AF_INET;
            30. recv_host.sin_addr.s_addr = inet_addr("127.0.0.1");
            31. recv_host.sin_port = htons(6000);
            32. */
            33. /* 发送信息 */
            34. /* 在此处,发送端的IP地址和端口号等各类信息,随着这个函数的调用,自动绑定在了套接字上 */
            35. sendto(sock, mess, strlen(mess), 0, p->ai_addr, p->ai_addrlen);
            36. /* 完成,关闭 */
            37. freeaddrinfo(result); /* 实际上这个函数应该在使用完 result 的地方就予以调用 */
            38. 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