【问题标题】:Can you bind() and connect() both ends of a UDP connection你能绑定()和连接()UDP连接的两端吗
【发布时间】:2012-03-16 16:57:23
【问题描述】:

我正在编写一个点对点消息队列系统,它必须能够通过 UDP 运行。我可以任意选择一侧或另一侧作为“服务器”,但这似乎不太正确,因为两端都在从另一端发送和接收相同类型的数据。

是否可以绑定()和连接()两端,以便它们只相互发送/接收?这似乎是一种很好的对称方式。

【问题讨论】:

  • 似乎有点奇怪,但我不明白为什么不这样做。 connect() 只是设置套接字的默认目标地址/端口。 (您尝试过吗?如果由于某种原因不起作用,请使用sendto()。)我个人只会使用sendto(),否则如果多个客户端连接到您的服务器,您会感到困惑。

标签: c linux udp


【解决方案1】:

你好,从遥远的未来,即 2018 年,到 2012 年。

事实上,connect()在实践中使用 UDP 套接字是有原因的(尽管有福的 POSIX and its implementations 理论上并不要求你这样做)。

一个普通的 UDP 套接字不知道它未来的目的地,所以it performs a route lookup each time sendmsg() is called

但是,如果事先使用特定远程接收器的 IP 和端口调用 connect(),则操作系统内核将能够 write down the reference to the route and assign it to the socket,从而在后续 sendmsg() 调用 时发送消息的速度显着加快不要指定接收者 (otherwise the previous setting would be ignored),而是选择默认接收者。

lines 1070 through 1171

if (connected)
    rt = (struct rtable *)sk_dst_check(sk, 0);

if (!rt) {
    [..skip..]

    rt = ip_route_output_flow(net, fl4, sk);

    [..skip..]
}

在 Linux 内核 4.18 之前,此功能主要仅限于 IPv4 地址系列。但是,从 4.18-rc4 开始(希望 Linux 内核版本 4.18 也是如此),it's fully functional with IPv6 sockets as well

它可能是a serious performance benefit 的来源,尽管它在很大程度上取决于您使用的操作系统。至少,如果您使用的是 Linux 并且不将套接字用于多个远程处理程序,您应该尝试一下。

【讨论】:

  • 您能解释一下什么是路由查找吗?确定数据报必须使用哪个以太网设备只是 CPU 周期吗?还是会导致某种可能需要大量时间的 I/O?
  • @SectoKia 不,这只是一个哈希表查找。哈希表保留在 RAM 中,因此它只是 CPU 周期和 RAM 查找。
  • 我正在回过头来看一个类似的问题,所以从遥远的 2019 年你好!
【解决方案2】:

UDP 是无连接的,因此操作系统实际上建立某种连接几乎没有意义。

在 BSD 套接字中,可以在 UDP 套接字上执行 connect,但这基本上只是为 send 设置默认目标地址(而不是明确地给 send_to)。

在 UDP 套接字上绑定会告诉操作系统实际接受数据包的传入本地接口地址(发送到其他地址的所有数据包都被丢弃),而不管套接字的类型。

收到后,您必须使用recvfrom 来识别数据包的来源。请注意,如果您想要某种身份验证,那么仅使用所涉及的地址与根本不使用锁一样不安全。 TCP 连接可以被劫持,而裸露的 UDP 确实在其头上写满了 IP 欺骗。您必须添加某种 HMAC

【讨论】:

  • 在 SOCK_DGRAM 套接字上的 connect() 设置默认/发送接收地址,所以你可以只使用发送和接收。我正在编写它以通过 TCP 工作,以便最终使其他一些代码对两种协议都通用。
  • @gct:确实。首先我不太确定,必须先查看 BSD 套接字的连接手册页(这里只有 Linux 的,我认为这不是所有操作系统的权威)。
  • @nhed:不,我的意思是,Linux 确实支持具有多个扩展的 super 组 BSD 套接字。而且我不得不查找一个虚无缥缈的参考页面,以确保我不是在写关于 Linux 特定扩展的文章。
  • @user1511417:不。您仍然可以在调用了bind() 的UDP 套接字上使用recvfrom,它仍然可以接受来自任意地址的数据包。
  • @datenwolf 啊:我读“传入地址”作为远程机器的地址。也许“本地地址”会更清楚?
【解决方案3】:

下面的程序演示了如何将同一 UDP 套接字上的 bind() 和 connect() 分别绑定到一组特定的源端口和目标端口。该程序可以在任何Linux机器上编译,用途如下:

usage: ./<program_name> dst-hostname dst-udpport src-udpport

我在打开两个终端时测试了这段代码。您应该能够向目标节点发送消息并从中接收消息。

在终端 1 中运行

./<program_name> 127.0.0.1 5555 5556

在终端 2 中运行

./<program_name> 127.0.0.1 5556 5555

尽管我已经在一台机器上测试过它,但我认为一旦你设置了正确的防火墙设置,它也应该可以在两台不同的机器上运行

这里是流程的描述:

  1. 设置提示将目标地址类型指示为 UDP 连接的类型
  2. 使用getaddrinfo获取地址信息结构dstinfo,参数1是目的地址,参数2是目的端口
  3. 使用 dstinfo 中的第一个有效条目创建一个套接字
  4. 使用getaddrinfo获取地址信息结构srcinfo主要用于源端口详情
  5. 使用srcinfo绑定到得到的socket
  6. 现在连接到 dstinfo 的第一个有效条目
  7. 如果一切顺利,进入循环
  8. 循环使用 select 来阻止读取描述符列表,该列表由创建的 STDIN 和 sockfd 套接字组成
  9. 如果 STDIN 有输入,则使用 sendall 函数将其发送到目标 UDP 连接
  10. 如果收到 EOM,则退出循环。
  11. 如果 sockfd 有一些数据,它会通过 recv 读取
  12. 如果 recv 返回 -1 这是一个错误,我们会尝试使用 perror 对其进行解码
  13. 如果recv 返回0,则表示远程节点已关闭连接。但我相信对于无连接的 UDP a 没有任何影响。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define STDIN 0

int sendall(int s, char *buf, int *len)
{
    int total = 0;        // how many bytes we've sent
    int bytesleft = *len; // how many we have left to send
    int n;

    while(total < *len) {
        n = send(s, buf+total, bytesleft, 0);
        fprintf(stdout,"Sendall: %s\n",buf+total);
        if (n == -1) { break; }
        total += n;
        bytesleft -= n;
    }

    *len = total; // return number actually sent here

    return n==-1?-1:0; // return -1 on failure, 0 on success
} 

int main(int argc, char *argv[])
{
   int sockfd;
   struct addrinfo hints, *dstinfo = NULL, *srcinfo = NULL, *p = NULL;
   int rv = -1, ret = -1, len = -1,  numbytes = 0;
   struct timeval tv;
   char buffer[256] = {0};
   fd_set readfds;

   // don't care about writefds and exceptfds:
   //     select(STDIN+1, &readfds, NULL, NULL, &tv);

   if (argc != 4) {
      fprintf(stderr,"usage: %s dst-hostname dst-udpport src-udpport\n");
      ret = -1;
      goto LBL_RET;
   }


   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_UNSPEC;
   hints.ai_socktype = SOCK_DGRAM;        //UDP communication

   /*For destination address*/
   if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo)) != 0) {
      fprintf(stderr, "getaddrinfo for dest address: %s\n", gai_strerror(rv));
      ret = 1;
      goto LBL_RET;
   }

   // loop through all the results and make a socket
   for(p = dstinfo; p != NULL; p = p->ai_next) {

      if ((sockfd = socket(p->ai_family, p->ai_socktype,
                  p->ai_protocol)) == -1) {
         perror("socket");
         continue;
      }
      /*Taking first entry from getaddrinfo*/
      break;
   }

   /*Failed to get socket to all entries*/
   if (p == NULL) {
      fprintf(stderr, "%s: Failed to get socket\n");
      ret = 2;
      goto LBL_RET;
   }

   /*For source address*/
   memset(&hints, 0, sizeof hints);
   hints.ai_family = AF_UNSPEC;
   hints.ai_socktype = SOCK_DGRAM;        //UDP communication
   hints.ai_flags = AI_PASSIVE;     // fill in my IP for me
   /*For source address*/
   if ((rv = getaddrinfo(NULL, argv[3], &hints, &srcinfo)) != 0) {
      fprintf(stderr, "getaddrinfo for src address: %s\n", gai_strerror(rv));
      ret = 3;
      goto LBL_RET;
   }

   /*Bind this datagram socket to source address info */
   if((rv = bind(sockfd, srcinfo->ai_addr, srcinfo->ai_addrlen)) != 0) {
      fprintf(stderr, "bind: %s\n", gai_strerror(rv));
      ret = 3;
      goto LBL_RET;
   }

   /*Connect this datagram socket to destination address info */
   if((rv= connect(sockfd, p->ai_addr, p->ai_addrlen)) != 0) {
      fprintf(stderr, "connect: %s\n", gai_strerror(rv));
      ret = 3;
      goto LBL_RET;
   }

   while(1){
      FD_ZERO(&readfds);
      FD_SET(STDIN, &readfds);
      FD_SET(sockfd, &readfds);

      /*Select timeout at 10s*/
      tv.tv_sec = 10;
      tv.tv_usec = 0;
      select(sockfd + 1, &readfds, NULL, NULL, &tv);

      /*Obey your user, take his inputs*/
      if (FD_ISSET(STDIN, &readfds))
      {
         memset(buffer, 0, sizeof(buffer));
         len = 0;
         printf("A key was pressed!\n");
         if(0 >= (len = read(STDIN, buffer, sizeof(buffer))))
         {
            perror("read STDIN");
            ret = 4;
            goto LBL_RET;
         }

         fprintf(stdout, ">>%s\n", buffer);

         /*EOM\n implies user wants to exit*/
         if(!strcmp(buffer,"EOM\n")){
            printf("Received EOM closing\n");
            break;
         }

         /*Sendall will use send to transfer to bound sockfd*/
         if (sendall(sockfd, buffer, &len) == -1) {
            perror("sendall");
            fprintf(stderr,"%s: We only sent %d bytes because of the error!\n", argv[0], len);
            ret = 5;
            goto LBL_RET;
         }  
      }

      /*We've got something on our socket to read */
      if(FD_ISSET(sockfd, &readfds))
      {
         memset(buffer, 0, sizeof(buffer));
         printf("Received something!\n");
         /*recv will use receive to connected sockfd */
         numbytes = recv(sockfd, buffer, sizeof(buffer), 0);
         if(0 == numbytes){
            printf("Destination closed\n");
            break;
         }else if(-1 == numbytes){
            /*Could be an ICMP error from remote end*/
            perror("recv");
            printf("Receive error check your firewall settings\n");
            ret = 5;
            goto LBL_RET;
         }
         fprintf(stdout, "<<Number of bytes %d Message: %s\n", numbytes, buffer);
      }

      /*Heartbeat*/
      printf(".\n");
   }

   ret = 0;
LBL_RET:

   if(dstinfo)
      freeaddrinfo(dstinfo);

   if(srcinfo)
      freeaddrinfo(srcinfo);

   close(sockfd);

   return ret;
}

【讨论】:

    【解决方案4】:

    真正的关键是connect()

    如果套接字 sockfd 是 SOCK_DGRAM 类型,那么 addr 是默认发送数据报的地址,也是接收数据报的唯一地址。

    【讨论】:

    • 据我了解,“服务器”必须绑定()无论如何,以便实际附加到端口,以便客户端可以有一个真实的地方来发送和接收数据......
    • @gct:你可以发送UDP数据包而不绑定到源端口,但是你不能接收。
    • @datenwolf:当然,但是我的消息队列的两端都需要发送和接收。我可以任意选择哪个是客户端,哪个是服务器,但之后它们都运行相同的代码,所以如果我能让它们完全对称就好了。
    • @gct:嗯,两边都可以绑定。
    • connected 套接字无论如何都是绑定的。来自连接手册页:If the socket has not already been bound to a local address, connect() shall bind it to an address which, unless the socket's address family is AF_UNIX, is an unused local address.
    【解决方案5】:

    你的代码有问题:

    memset(&hints, 0, sizeof hints);
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;        //UDP communication
    
    /*For destination address*/
    if ((rv = getaddrinfo(argv[1], argv[2], &hints, &dstinfo)) 
    

    仅通过使用 AF_UNSPEC 和 SOCK_DGRAM,您可以获得所有可能的地址列表。因此,当您调用 socket 时,您使用的地址可能不是您预期的 UDP 地址。你应该使用

    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_protocol = IPPROTO_UDP;
    hints.ai_flags = AI_PASSIVE;
    

    而是确保您正在检索的 addrinfo 是您想要的。

    换句话说,你创建的套接字可能不是UDP套接字,这就是它不起作用的原因。

    【讨论】:

      【解决方案6】:

      此页面包含有关已连接和未连接套接字的一些重要信息: http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html

      这句话回答了你的问题:

      通常情况下,调用connect的是UDP客户端,但也有UDP服务器与单个客户端长时间通信的应用(如TFTP);在这种情况下,客户端和服务器都可以调用connect。

      【讨论】:

        【解决方案7】:

        我没有在 UDP 下使用过 connect()。我觉得 connect() 在 UDP 和 TCP 下是为两个完全不同的目的而设计的。

        The man page 简要介绍了 UDP 下 connect() 的用法:

        通常,基于连接的协议(如 TCP)套接字只能成功 connect() 一次;无连接协议(如 UDP)套接字可以多次使用 connect() 来更改它们的关联。

        【讨论】:

          【解决方案8】:

          是的,你可以。我也是。

          而您的用例是有用的:双方都充当客户端和服务器,并且双方只有一个进程。

          【讨论】:

            【解决方案9】:

            我会更多地从 UDP 提供的概念来看待它。 UDP 是一个 8 字节的报头,它增加了 2 字节的发送和接收端口(总共 4 字节)。这些端口与 Berkeley 套接字交互以提供您的传统套接字接口。 IE。你不能绑定到没有端口的地址,反之亦然。

            通常,当您发送 UDP 数据包时,接收端端口(源)是临时的,而发送端端口(目标)是远程计算机上的目标端口。您可以通过先绑定然后连接来消除此默认行为。现在,只要两台计算机上的相同端口可用,您的源端口和目标端口就会相同。

            一般来说,这种行为(我们称之为端口劫持)是不受欢迎的。这是因为您刚刚将发送端限制为只能从一个进程发送,而不是在动态分配发送端源端口的临时模型中工作。

            顺便说一句,8 字节 UDP 有效载荷的其他 4 字节、长度和 CRC 几乎完全没有用,因为它们已经在 IP 数据包中提供,并且 UDP 标头是固定长度的。就像来人一样,计算机非常擅长做一点减法。

            【讨论】:

            • 源端口和目的端口不要求相同。
            • @EJP:我哪里说源端口和目的端口是一样的?
            • IPv4 标头的校验和是冗余的,而不是 UDP 的。 IPv6 没有校验和。
            • @Claris 当你说'现在你的源端口和目标端口将是相同的'时。
            【解决方案10】:

            如果你是c/c++爱好者,可以试试route_io

            使用简单,创建一个实例来接受不同的端口路由到你的函数。

            例子:

              void read_data(rio_request_t *req);
              void read_data(rio_request_t *req) {
              char *a = "CAUSE ERROR FREE INVALID";
            
              if (strncmp( (char*)req->in_buff->start, "ERROR", 5) == 0) {
                free(a);
              }
              // printf("%d,  %.*s\n", i++, (int) (req->in_buff->end - req->in_buff->start), req->in_buff->start);
              rio_write_output_buffer_l(req, req->in_buff->start, (req->in_buff->end - req->in_buff->start));
              // printf("%d,  %.*s\n", i++, (int) (req->out_buff->end - req->out_buff->start), req->out_buff->start);
            }
            
            int main(void) {
            
              rio_instance_t * instance = rio_create_routing_instance(24, NULL, NULL);
              rio_add_udp_fd(instance, 12345, read_data, 1024, NULL);
              rio_add_tcp_fd(instance, 3232, read_data, 64, NULL);
            
              rio_start(instance);
            
              return 0;
            }
            

            【讨论】:

              猜你喜欢
              • 2021-07-15
              • 1970-01-01
              • 1970-01-01
              • 2020-01-01
              • 2011-02-27
              • 2021-12-14
              • 2012-02-25
              • 2018-02-18
              • 1970-01-01
              相关资源
              最近更新 更多