现代TCP/IP网络编程-UDP

UDP 的 connect()

  • 前方提到,如果使用的是 UDP 套接字进行通信的话,可以采取 connect 来进行优化,但是却不知原因。
  • 首先 UDP 的别称叫做 不可靠连接, 也就是说它可以不需要对发送出去的数据负责任,在默认情况下这是对的,因为“效率”。
  • 但是如果一个 UDP 套接字端 需要与另一个端进行多于一次的通信的时候,就会出现性能问题:
    • 具体是: 连接两端通信 -> 发送数据 -> 断开连接 -> 连接两端通信 -> 发送数据 -> 断开连接 ......
    • 可以看出,需要重复的进行 连接和断开, 且这两个操作都是涉及 内核 操作,耗费的资源不可忽略
    • 所以在必要时对 UDP 套接字调用 connect,是有必要的(并不硬性要求两端同时都要调用 connect)
    • 需要注意的是 TCP套接字 同样需要调用(必须调用) connect,虽然调用的函数接口一样,但是意义却是不相同的,前者是为了三次握手建立连接,而 UDP却只是为了能够省去 不必要的断开连接 以及接收到 ICMP错误报文
  • ICMP错误报文,指的是如果对端没办法接收到本端发送的信息的话,会返回一个错误,这个错误使用的就是ICMP(ICMPv4和ICMPv6两种)
    • 如果 UDP 套接字通信时采用的是 未连接(unconnected) 的形式,那么在调用 sendto 接口之后,不管对端有没有收到信息,都会立即返回成功的信息,而即使对端没办法收到信息,且向本端发送了 ICMP 报文,我们也是无法检测到的。
    • 但是如果 UDP 套接字通信时采用的是 连接(connected) 的形式,那么就会接收到一个 EHOSTUNREACH 的错误,我们就能够捕捉到(这点与TCP的处理方式一样)。
  • 假设我们想要断开连接 或者 重新选择一个对端进行通信,也是可以的
    • 所需要做的也仅仅是在此调用一下 connect 接口函数
      struct sockaddr_storage unconnect;
      memset(&unconnect, 0, sizeof unconnect);
      unconnect.ss_family = AF_UNSPEC; /* 将 xxx_family 位置为 AF_UNSPEC 就表明要断开连接 */
      connect(udp_sock, (struct sockaddr *)&unconnect, sizeof unconnect); /* 断开连接 */
      

注: 断开连接或者重新对套接字建连接是 UDP 才可以使用的,千万不要用在 TCP套接字上面!

不要疑惑,每次发送的时候只指定了对端的 IP 和 端口,那我们自己的 IP 和端口呢?这个是由内核为我们完成,自动化就是这么方便

  • TCP 这种面向连接的方式不同, UDP 不管是否是连接的(connected),它依旧是一种不可靠的传输方式,所以当它调用 connect 时,即使对端没有运行,这个函数也不会有任何错误,知道发送第一条信息时才能知道对端到底可不可达。

    那些通用的操作

  • 在上述记录的代码中,最主要的过程莫过于,创建套接字绑定连接发送/接收关闭, 这过程中使用的都是由操作系统提供给程序员的接口,但前方并未详细记录。此处给出

  • socket()

    /* 此处,该接口调用成功则返回 描述符(*nix 下的概念), 否则错误的话返回 -1 */
    /*
    * @param family   用来指定IP的协议族 也就是IP地址的类型
    * @param type     调用该接口想要创建什么类型的套接字,例如是 TCP 的还是 UDP 的,等等?
    * @param protocol 一般传入0作为参数,代表任意都接受的意思,实际上有三个选择。
    */
    int socket(int family, int type, int protocol);
    
family 选择 type 选择 protocol 选择
AF_INET IPv4地址族 SOCK_STRAAM 代表TCP IPPROTO_TCP TCP的协议
AF_INET6 IPv6地址族 SOCK_DGRAM 代表UDP IPPROTO_UDP UDP的协议
剩下的暂时不记录 SOCK_RAW 用于更底层的实现
..
…代表还有可用选项,但不予记录
  • 这里引出了 Windows*nix 的一点小差异,那就是 socket的返回值问题
  • 如果正确且成功的调用了这个函数,其会返回一个 句柄(Windows) 或者 文件描述符(*nix)

    • 我们知道,在 *nix 操作系统中,任何的一切都被视为文件,包括套接字,所以由 socket()调用后返回的文件描述符的值也遵循着这个规则: 从 0 开始依次递增的一个整形值 ,所以当我们使用 select(后续会提到的一个重要概念),可以很方便的将最后得到的文件描述符的值,当成搜索范围。
    • Windows下,句柄实质是也是一种整形,但是其值就不是程序员想象中的从零有序的向前递增了,而是由操作系统来自行决定,程序员无法预测,所以在 调用 select 的时候,会稍微有一些差异。
  • connect

    • 在此处不详细记录,因为在 TCP 中,其作用更加重要
      /* UDP 中该函数的作用十分单一,就是将IP和端口注册进套接字,且可对同一个套接字重复调用 */
      int connect(int sockfd,  
                const struct sockaddr * servaddr,
                              socklen_t addrlen);
      
  • bind

    • 这个接口一般用于 接收端 或者 称为服务端 的地方,具体作用在一般情况下是为制定一个具体的监听端口, 当然也可以指定具体的 IP地址,但是一般不这么做(并非绝对),因为我们大部分时候是想要接收来自四面八方的各地IP主机的访问。
      /* 通过在 myaddr指向的结构体中 填入 端口号 和 IP地址来达到 绑定的目的 */
      /*
      * @param sockfd 用于绑定的套接字
      * @param myaddr 在前方准备好的信息地址结构体
      * @param len    这个结构体的长度
      * 实际上,信息地址结构体在 getaddrinfo 这个接口没有出现之前是需要自己填写的,但是现在却省去很多工作
      */
      int bind(int sockfd, const struct sockaddr * myaddr, socklen_t len);
      /*
      * 提到的省去的工作就是对 信息结构体的填入工作
      * 在最原始操作中有一个操作是 让该监听套接字 接收来自任意 IP 的访问
      */
      ...
      recv_host.sin_addr.s_addr = htonl(INADDR_ANY);/* 接收任意的IPv4 类型的 IP */
      recv_host.sin6_addr = in6addr_any; /* 接收任意的IPv6 类型的 IP */
      ...
      /* 上述操作需要自行填写,十分不便,但是在调用getaddrinfo接口,且设置了相应的 flags以后,其会自动帮你设置这个选项,也就不需要程序员操心 */
      /* 直接使用 bind(sockfd, result->ai_addr, result->ai_addrlen); */
      
  • sendto

    int sendto(int sockfd, const void * buff, size_t nbytes,  /* 用于"监听"的套接字,将要发送的信息存储首地址,信息的长度 */
                  int flags, /* 暂时忽略 */
                  const struct sockaddr * to, socklen_t addrlen); /* 目的段的地址信息, 这个结构体的长度 */
    
    • 一般而言,对于服务端(接收端)的UDP而言,会显式的调用 bind 函数,进行绑定一个地址(通配地址或者指定一个地址,这对于一台由多个网卡组成的主机有意义)以及端口号
      对于普通的服务器而言,直接设定 统配地址( INADDR_ANYin6addr_any ) 即可,系统会自动帮你选择有效的 地址端口绑定到 套接字上。

    • 但,对于没有显式调用 bind 函数的UDP 客户端(发送端)而言,在创建了套接字(调用socket())之后,得到的套接字是没有地址和端口信息的,其真正获得这些信息的时候就是在 sendto函数调用的时候,它会隐式的为 sockfd 绑定上 通配地址,并选择一个临时端口。

    • flags

      • MSG_DONTWAIT, MSG_OOB, MSG_PEEK, ‘MSG_ERRQUEUE’, MSG_TRUNC, MSG_WAITALL
  • recvfrom

    int recvfrom(int sockfd, void * buff, size_t nbytes,
                int flags
                struct sockaddr * from, socklen_t * len); /* 另一端的地址信息,以及长度 */
    
    • 用于接收信息,如果最后两个参数为 NULL,就代表不想要知道对端的信息。也就没有办法回复信息给对方。

注: sendto / recvfrom 并不是 UDP 专用,而是协议无关的函数接口,也就是说同样可以将这两个接口用于 TCP 套接字,只需要将最后两个参数设置为 NULL即可

至于有没有必要,就仁者见仁智者见智了。

转载注明: http://www.wushxin.top/2015/12/04/%E7%8E%B0%E4%BB%A3TCP-IP%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B-UDP.html