你可以在UDP连接的两端同时使用bind()和connect()吗?

53
我正在编写一个点对点的消息队列系统,它需要能够在UDP上运行。我可以任意选择一端作为“服务器”,但感觉不太对,因为两端都会发送和接收来自另一端相同类型的数据。
是否可能同时将bind()和connect()应用于两端,使它们只从彼此发送/接收数据? 这似乎是一个漂亮对称的方法。

3
听起来有点奇怪,但我不明白为什么不能这样做。connect()只是为套接字设置默认的目标地址/端口。(你试过了吗?如果由于某些原因它不起作用,那就使用sendto()。)个人而言,我会直接使用sendto(),否则如果多个客户端连接到您的服务器,您会感到困惑。 - mpontillo
10个回答

63

你好,我来自2018年的遥远未来,向你们2012年问候。

实际上,connect()一个UDP socket有其原因(尽管受到赐福的POSIX及其实现理论上不要求这样做)。

普通的UDP socket不知道它将要发送到哪个目标,所以每次调用sendmsg()时都会执行一次路由查找

然而,如果先调用connect()并指定特定的远程接收方IP和端口,则操作系统内核将能够记下对路由的引用并将其分配给socket,从而使得在后续的sendmsg()调用中,如果没有指定接收方 (否则之前的设置将被忽略),就能更快地发送消息并选择默认接收方。

请看1070行到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正式版中)它也可以完全支持IPv6套接字

这可能会带来显著的性能优势,尽管这将严重取决于您使用的操作系统。至少,如果您使用的是Linux,并且不使用套接字进行多个远程处理程序,那么应该尝试一下。


1
请问您能否解释一下路由查找是什么?它只是CPU周期来确定数据报必须使用哪个以太网设备吗?还是会导致某种类型的I/O,可能需要花费相当长的时间? - Secto Kia
2
@SectoKia 不,那只是哈希表查找。哈希表保留在RAM中,因此只需要CPU周期和RAM查找。 - ximaera
2
我又回来看一个类似的问题,所以来自2019年遥远的未来问候大家! - gct

28

UDP是无连接的,因此对于操作系统实际上没有太多意义去建立某种连接。

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

在UDP套接字上绑定(bind)指定了操作系统应该接受哪个本地接口地址的数据包(所有其他地址的数据包都将被丢弃),而与套接字类型无关。

当接收到数据包时,必须使用recvfrom来识别数据包来自哪个源头。请注意,如果你想要一些身份验证方式,那么仅使用涉及的地址和端口就和没有任何锁一样不安全。TCP连接可以被劫持,而纯粹的UDP协议则很容易被IP欺骗攻击。你必须添加一些形式的HMAC认证。


13
在SOCK_DGRAM套接字上使用connect()函数会设置默认的发送和接收地址,因此你只需要使用send和recv函数即可。我还在编写它以使其可以在TCP上工作,这样会使一些其他代码适用于两种协议。 - gct
2
@nhed:不,我的意思是,Linux支持一个带有多个扩展的BSD套接字超级集。我不得不查阅一个普通的参考页面,以确保我没有写关于Linux特定的扩展。 - datenwolf
1
@user1511417:不是的。您仍然可以在调用bind()的UDP套接字上使用recvfrom,它仍将接受来自任意地址的数据包。 - datenwolf
@DanielGriscom: 你考虑的是connect系统调用。bind将一个套接字分配给本地地址。例如,如果您为同一服务(例如DNS)运行多个守护程序,则这很重要,为不同的地址提供服务。例如,手动的djbdns明确描述了如何在127.x.y.z:53上运行多个tinydns实例,在单个公共可达的dnscache后面进行代理。这个目的地的区别是使用bind设置的! - datenwolf
1
@datenwolf 啊:我把“incoming address”读成远程机器的地址了。也许“本地地址”更清晰明了? - Daniel Griscom
显示剩余6条评论

17
以下是一个演示如何在同一个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根据参数1(目标地址)和参数2(目标端口)获取地址信息结构dstinfo。 3. 用dstinfo中的第一个有效条目创建套接字。 4. 使用getaddrinfo获取地址信息结构srcinfo,主要是为了源端口的详细信息。 5. 使用srcinfo将其绑定到获取的套接字上。 6. 现在连接到dstinfo中的第一个有效条目。 7. 如果一切顺利,则进入循环。 8. 循环使用select在读取描述符列表上阻塞,该列表包括STDIN和创建的sockfd套接字。 9. 如果STDIN有输入,则使用sendall函数将其发送到目标UDP连接。 10. 如果收到EOM,则退出循环。 11. 如果sockfd有一些数据,则通过recv读取它。 12. 如果recv返回-1,则表示出现错误,我们尝试使用perror解码它。 13. 如果recv返回0,则表示远程节点已关闭连接。但我相信这对于无连接的UDP没有影响。
#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;
}

6

实际上,关键在于connect()函数:

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


4
在我的理解中,“服务器”无论如何都需要使用bind()方法,这样才能真正绑定到一个端口,从而让客户端有一个真实的位置来发送和接收数据... - gct
1
@gct:你可以发送UDP数据包而不绑定源端口,但是你无法接收它们。 - datenwolf
@datenwolf:当然可以,但是我的消息队列两端都需要发送和接收。我可以任意选择哪个是客户端,哪个是服务器,但之后它们都运行相同的代码,所以如果我能使它们完全对称就更好了。 - gct
@gct:只需在两侧连接它们即可。 - datenwolf
3
好的,你可以在两侧绑定。 - datenwolf
1
连接的套接字总是会被绑定。来自connect手册页的描述:如果套接字尚未绑定到本地地址,则connect()将绑定到一个地址,该地址(除非套接字的地址族为AF_UNIX)是未使用的本地地址。 - daurnimator

1

你的代码出现了问题:

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套接字,这就是它无法工作的原因。


0

-2

我从未在UDP下使用过connect()。我觉得connect()在UDP和TCP下的设计目的完全不同。

man页面上有一些关于在UDP下使用connect()的简要说明:

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


-2

是的,你可以这样做。我也这么做。

而且你的使用情况正是这种方式有用的情况:双方都充当客户端和服务器,并且两侧只有一个进程。


-2

我更多地从UDP提供的功能角度来看待它。UDP是一个8字节的头,其中添加了2字节的发送和接收端口(总共4字节)。这些端口与伯克利套接字交互,提供传统的套接字接口。也就是说,您不能在没有端口的情况下绑定地址,反之亦然。

通常情况下,当您发送UDP数据包时,接收端口(源)是临时的,而发送端口(目标)是远程计算机上的目标端口。您可以通过先绑定再连接来打败这种默认行为。现在,只要两台计算机上的相同端口空闲,您的源端口和目标端口将是相同的。

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

顺便说一句,UDP负载的另外四个字节,长度和CRC几乎完全无用,因为它们已经在IP数据包中提供,并且UDP头是固定长度的。电脑很擅长做一点减法,对吧?


源端口和目的端口不必相同。 - user207421
@EJP:我在哪里说源端口和目标端口是相同的了? - Clarus
1
IPv4头部的校验和是冗余的,而不是UDP的。IPv6没有校验和。 - Navin
@Claris 当你说“现在你的源端口和目标端口将是相同的”时。 - user207421

-4
如果你是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;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接