Socket Sendto 可以传入不同的目的地址吗

1 前言

通常我们认为 socket 中 地址信息 和 socket句柄 是一一对应的,不能往一个socket句柄中,传入不同的地址信息。

但真是这样的吗?

咨询了一些朋友,有两种答案。特别是做服务器的朋友,说UDP可以这样,一般只建一个socket监听,有client连接过来时,直接循着它的源地址信息,进行sendto操作。

今天刚好有空,于是刨根究底地找找最源头的资料。

2 POSIX Socket 中的介绍

最先找到的是 POSIX Socket 标准,其实就是伯克利的socket标准。

2.1 维基百科

维基百科中,没有详细的描述,但给了UDP的Server及Client的示例。

UDP Server 的 socket 操作:无需 accept socket,bind 本地端口之后,直接recvfrom。

  memset(&sa, 0, sizeof sa);
  sa.sin_family = AF_INET;
  sa.sin_addr.s_addr = htonl(INADDR_ANY);
  sa.sin_port = htons(7654);
  fromlen = sizeof sa;

  sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (bind(sock, (struct sockaddr *)&sa, sizeof sa) == -1) {
    perror("error bind failed");
    close(sock);
    exit(EXIT_FAILURE);
  }

  for (;;) {
    recsize = recvfrom(sock, (void*)buffer, sizeof buffer, 0, (struct sockaddr*)&sa, &fromlen);
    if (recsize < 0) {
      fprintf(stderr, "%s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }
    printf("recsize: %d\n ", (int)recsize);
    sleep(1);
    printf("datagram: %.*s\n", (int)recsize, buffer);
  }

UDP Client 的 socket 操作,没有 connect ,直接 sendto 给一个IP。

  sock = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (sock == -1) {
      /* if socket failed to initialize, exit */
      printf("Error Creating Socket");
      exit(EXIT_FAILURE);
    }

  /* Zero out socket address */
  memset(&sa, 0, sizeof sa);

  /* The address is IPv4 */
  sa.sin_family = AF_INET;

   /* IPv4 adresses is a uint32_t, convert a string representation of the octets to the appropriate value */
  sa.sin_addr.s_addr = inet_addr("127.0.0.1");

  /* sockets are unsigned shorts, htons(x) ensures x is in network byte order, set the port to 7654 */
  sa.sin_port = htons(7654);

  bytes_sent = sendto(sock, buffer, strlen(buffer), 0,(struct sockaddr*)&sa, sizeof sa);
  if (bytes_sent < 0) {
    printf("Error sending packet: %s\n", strerror(errno));
    exit(EXIT_FAILURE);
  }

  close(sock); /* close the socket */

从这两个示例中,应该能大概看到,数据报的socket是可以支持多地址信息的接入。

2.2 POSIX Socket 标准

再深入一点,找到POSIX Socket 标准的介绍 sendto

关键的描述在这里:

扫描二维码关注公众号,回复: 2257413 查看本文章

The sendto() function shall send a message through a connection-mode or connectionless-mode socket.

If the socket is a connectionless-mode socket, the message shall be sent to the address specified by dest_addr if no pre-specified peer address has been set. If a peer address has been pre-specified, either the message shall be sent to the address specified by dest_addr (overriding the pre-specified peer address), or the function shall return -1 and set errno to [EISCONN].

If the socket is connection-mode, dest_addr shall be ignored.

socket 有两种模式,一种是连接模式,一种是无连接模式。

无连接模式下,如果没有预先指定对等地址,则会发消息给 dest_addr 指定的地址。如果已经预先指定了一个对等地址,则该消息要么发送到由dest_addr指定的地址(覆盖预先指定的对等地址),或者该函数应返回-1并将errno设置为[EISCONN]

如果是连接模式,则dest_addr无效。

再看看无连接 socket 的定义

The SOCK_DGRAM socket type supports connectionless data transfer which is not necessarily acknowledged or reliable. Datagrams may be sent to the address specified (possibly multicast or broadcast) in each output operation, and incoming datagrams may be received from multiple sources. The source address of each datagram is available when receiving the datagram. An application may also pre-specify a peer address, in which case calls to output functions that do not specify a peer address shall send to the pre-specified peer.

数据报可以在每次输出操作时发送到指定的地址(可能是多播或者广播),可能会从多个源接收到数据报。当接收数据报时,每个数据报的源地址是可以获知的。

这样看来,sendto可以使用无连接模式的socket,来处理不同地址的信息。

3 XTI 中的介绍

查找过程中,还看到了XTI,做个知识补充。

TCP/IP 应用层位于传输层之上,TCP/IP 应用程序需要调用传输层的接口才能实现应用程序之间通信。目前使用最广泛的传输层的应用编程接口是套接字接口(Socket)。Socket APIs 是于 1983 年在 Berkeley Socket Distribution (BSD) Unix 中引进的。 1986 年 AT&T 公司引进了另一种不同的网络层编程接口 TLI(Transport Layer Interface),1988 年 AT&T 发布了一种修改版的 TLI,叫做 XTI(X/open Transport interface)。XTI/TLI 和 Socket 是用来处理相同任务的不同方法。

这是XTI的维基百科,还找到一份富士通提供的协议pdf

XTI是 POSIX 的超集,协议的前六章也是在梳理POSIX。有两个图很有代表意义。

TCP:

UDP:

在 章节 2.8 和 5.4 给出了无连接模式 UDP socket 的示例,通过命令行输入任意域名,DEMO会解析域名,往该服务器发出数据。

由此再次确认 sendto 是不限制地址信息,不做绑定。

4 一个聊天工具的UDP实现

查阅资料时,发现stackoverflow上一个聊天工具的UDP使用疑问,其中第2个答案,回答了如何在P2P通讯中使用UDP。

https://stackoverflow.com/questions/34966408/posix-udp-socket-not-binding-to-correct-ip

With UDP sockets, while you can use connect, you generally don’t want to, as that restricts you to a single peer per socket. Instead, you want to use a single unconnected UDP socket in each peer with the sendto and recvfrom system calls to send and receive packets with a different address for each packet.

对于UDP套接字,虽然可以使用连接,但通常不希望这样做,因为这会限制您每个套接字使用一个对等端。相反,您希望在sendto和recvfrom系统调用的每个对等方中使用单个未连接的UDP套接字,以便为每个数据包发送和接收具有不同地址的数据包。

The sendto function takes a packet and a peer address to send it to, while the recvfrom function returns a packet and the peer address it came from. With a single socket, there’s no need to multiplexing with select or poll – you just call recvfrom to get the next packet from any source. When you get a packet, you also get the peer address to send packets (back) to.

sendto函数将一个数据包和一个对等地址发送给它,而recvfrom函数返回一个数据包和它来自的对等地址。使用单个套接字时,不需要使用select或poll进行复用 - 只需调用recvfrom即可从任何源获取下一个数据包。当你得到一个数据包时,你也可以得到对方地址来发送数据包(返回)。

On startup, your peer will create a single socket and bind it to INADDR_ANY (allowing it to receive packets on any interface or broadcast address on the machine) and either the specific port assigned to you program or port 0 (allowing the OS to pick any unused port). In the latter case, you’ll need to use getsockname to get the port and report it to the user. Once the socket is set up, the peer program can sendto any peer it knows about, or recvfrom any peer at all (including those it does not yet know about).

在启动时,您的对等体将创建一个套接字并将其绑定到INADDR_ANY(允许它接收任何接口上的数据包或机器上的广播地址)以及分配给您的特定端口或端口0(允许操作系统选择任何未使用的端口)。在后一种情况下,您需要使用getsockname来获取端口并将其报告给用户。一旦套接字建立,对等程序就可以发送给它所知道的任何对等体,或者从任何对等体(包括它还不知道的对等体)接收。

这个答案对无连接模式socket的发送操作基本和参考资料的描述是一致。

5 Lwip 的实现

如下是 lwip 2.0.3 的参考代码。

int lwip_sendto(int s, const void *data, size_t size, int flags,
       const struct sockaddr *to, socklen_t tolen)
{
  ...

  sock = get_socket(s);
  if (!sock) {
    return -1;
  }

  if (NETCONNTYPE_GROUP(netconn_type(sock->conn)) == NETCONN_TCP) {
#if LWIP_TCP
    return lwip_send(s, data, size, flags);
#else /* LWIP_TCP */
    LWIP_UNUSED_ARG(flags);
    sock_set_errno(sock, err_to_errno(ERR_ARG));
    return -1;
#endif /* LWIP_TCP */
  }

  ...

  /* initialize a buffer */
  buf.p = buf.ptr = NULL;
#if LWIP_CHECKSUM_ON_COPY
  buf.flags = 0;
#endif /* LWIP_CHECKSUM_ON_COPY */
  if (to) {
    SOCKADDR_TO_IPADDR_PORT(to, &buf.addr, remote_port);
  } else {
    remote_port = 0;
    ip_addr_set_any(NETCONNTYPE_ISIPV6(netconn_type(sock->conn)), &buf.addr);
  }
  netbuf_fromport(&buf) = remote_port;

  /* make the buffer point to the data that should be sent */
#if LWIP_NETIF_TX_SINGLE_PBUF
  /* Allocate a new netbuf and copy the data into it. */
  if (netbuf_alloc(&buf, short_size) == NULL) {
    err = ERR_MEM;
  } else {
#if LWIP_CHECKSUM_ON_COPY
    if (NETCONNTYPE_GROUP(netconn_type(sock->conn)) != NETCONN_RAW) {
      u16_t chksum = LWIP_CHKSUM_COPY(buf.p->payload, data, short_size);
      netbuf_set_chksum(&buf, chksum);
    } else
#endif /* LWIP_CHECKSUM_ON_COPY */
    {
      MEMCPY(buf.p->payload, data, short_size);
    }
    err = ERR_OK;
  }
#else /* LWIP_NETIF_TX_SINGLE_PBUF */
  err = netbuf_ref(&buf, data, short_size);
#endif /* LWIP_NETIF_TX_SINGLE_PBUF */

  if (err == ERR_OK) {
    /* send the data */
    err = netconn_send(sock->conn, &buf);
  }

  /* deallocated the buffer */
  netbuf_free(&buf);

  sock_set_errno(sock, err_to_errno(err));
  return (err == ERR_OK ? short_size : -1);
}

总结下 lwip 的实现:
1. 看下 socket 是否是 TCP,则直接调用send,无视传递进来的目的地址。
2. 对于 UDP 的方式,没有管是否是连接模式,直接以当前目的地址为主。这样处理是简单处理,没有考虑 connect 的情况,有一点不满足 POSIX 标准。也许 adam 是考虑效率,毕竟它的名字是 lwip。

6 Zephyr中的sendto处理


static int sendto(struct net_pkt *pkt,
          const struct sockaddr *dst_addr,
          socklen_t addrlen,
          net_context_send_cb_t cb,
          s32_t timeout,
          void *token,
          void *user_data)
{
    struct net_context *context = net_pkt_context(pkt);
    int ret;

    if (!net_context_is_used(context)) {
        return -EBADF;
    }

#if defined(CONFIG_NET_TCP)
    if (net_context_get_ip_proto(context) == IPPROTO_TCP) {
        if (net_context_get_state(context) != NET_CONTEXT_CONNECTED) {
            return -ENOTCONN;
        }

        NET_ASSERT(context->tcp);
        if (context->tcp->flags & NET_TCP_IS_SHUTDOWN) {
            return -ESHUTDOWN;
        }
    }
#endif /* CONFIG_NET_TCP */

#if defined(CONFIG_NET_UDP)
    /* Bind default address and port only if UDP */
    if (net_context_get_ip_proto(context) == IPPROTO_UDP) {
        ret = bind_default(context);
        if (ret) {
            return ret;
        }
    }
#endif /* CONFIG_NET_UDP */

    if (!dst_addr) {
        return -EDESTADDRREQ;
    }

#if defined(CONFIG_NET_IPV4)
    if (net_pkt_family(pkt) == AF_INET) {
        struct sockaddr_in *addr4 = (struct sockaddr_in *)dst_addr;

        if (addrlen < sizeof(struct sockaddr_in)) {
            return -EINVAL;
        }

        if (!addr4->sin_addr.s_addr) {
            return -EDESTADDRREQ;
        }
    } else
#endif /* CONFIG_NET_IPV4 */
    {
        NET_DBG("Invalid protocol family %d", net_pkt_family(pkt));
        return -EINVAL;
    }

#if defined(CONFIG_NET_UDP)
    if (net_context_get_ip_proto(context) == IPPROTO_UDP) {
        ret = create_udp_packet(context, pkt, dst_addr, &pkt);
    } else
#endif /* CONFIG_NET_UDP */

#if defined(CONFIG_NET_TCP)
    if (net_context_get_ip_proto(context) == IPPROTO_TCP) {
        net_pkt_set_appdatalen(pkt, net_pkt_get_len(pkt));
        ret = net_tcp_queue_data(context, pkt);
    } else
#endif /* CONFIG_NET_TCP */
    {
        NET_DBG("Unknown protocol while sending packet: %d",
            net_context_get_ip_proto(context));
        return -EPROTONOSUPPORT;
    }

    if (ret < 0) {
        NET_DBG("Could not create network packet to send (%d)", ret);
        return ret;
    }

    return send_data(context, pkt, cb, timeout, token, user_data);
}

总结下 zephyr 的实现:
1. 看下 socket 是否是 TCP,看是否connect过,没有则返回错误。这样比lwip处理的还宽松,没有匹配不同地址。
2. 对于 UDP 的方式,同样也没有匹配不同地址,处理比较宽松。

7 总结

即便参考了两份代码,都没有对这次涉及的问题进行严格处理。但我还是根据POSIX标准文档,提炼出针对sendto情况对IP的正确处理:

1.是TCP连接,直接按照默认的地址处理。

2.是UDP连接,同时已经connect过,同样按照默认的地址处理。

3.是UDP连接,但没有connect过,直接往当前to的地址发送。

对于 lwip 和 zephyr 的处理,也许有一些我还没研究到的地方。对于这个结论还是不够确定,目前先如此,后续有新的发现再更新。

8 End


猜你喜欢

转载自blog.csdn.net/iotisan/article/details/80472911