0x12-套接字编程-2

新时代的 套接字网络编程

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

      	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 就可以,使用时直接强制转换类型即可

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

            /* 首先将多于的变量化简 */
            // - 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 的年代*/
          
        2. 补充完整上方对应的 发送端 代码

            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);                
          
        3. 到了此处,实际上是开了网络编程的一个初始,解除了现代的 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


书籍推荐