2012-03-16 50 views
29

我正在写一个点对点消息队列系统,它必须能够通过UDP进行操作。我可以任意选择一方或另一方作为“服务器”,但由于两端正在发送和接收来自另一端的相同类型的数据,所以它看起来不太正确。你可以绑定()和连接()两端的UDP连接

是否有可能绑定()和连接()两端,使他们只发送/接收对方?这似乎是一个很好的对称方式。

+2

似乎有点奇怪,但我不明白为什么不。 connect()只设置套接字的默认目标地址/端口。 (你有没有尝试过?如果因为某种原因无法正常工作,只需使用'sendto()'。)就我个人而言,我只是使用'sendto()',否则如果多个客户端连接到你的服务器,你会感到困惑。 – mpontillo 2012-03-16 17:37:48

回答

22

UDP是无连接的,所以在实际进行某种连接时,操作系统没什么意义。

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

在UDP套接字上绑定会告诉操作系统实际接受数据包的传入地址(所有到其他地址的数据包都被丢弃),无论套接字类型如何。

收到后您必须使用recvfrom来确定数据包来自哪个源。请注意,如果您需要某种身份验证,那么仅使用涉及的地址与不锁定一样不安全。 TCP连接可能被劫持,并且裸露的UDP字面上具有在其头部写入的IP欺骗。您必须添加某种HMAC

+9

在SOCK_DGRAM套接字上的connect()设置了默认/发送接收地址,所以你可以使用send和recv。我正在写它来通过TCP进行工作,最终使两个协议的其他代码变得通用。 – 2012-03-16 18:18:49

+0

@gct:确实。我第一次不太确定,必须先查看BSD套接字的连接手册(这里只有Linux版本,我认为这对所有操作系统来说都不是合法的)。 – datenwolf 2012-03-16 18:59:44

+0

@datenwolf你说Linux没有实现BSD套接字吗? – nhed 2013-06-01 14:36:13

5

真正的关键是connect()

如果插座的sockfd是SOCK_DGRAM类型,然后addr是哪个数据包默认情况下发送的地址,唯一的地址从数据报被接收。

+4

在我的理解中,'服务器'必须bind()不管,为了实际连接到一个端口,以便客户端可以有一个真正的地方发送和接收数据... – 2012-03-16 18:18:34

+1

@gct:你可以发送UDP数据包绑定到源端口,但不能接收。 – datenwolf 2012-03-16 18:57:39

+0

@datenwolf:当然,但我的消息队列的两端都需要发送和接收。我可以随意选择哪一个是客户端,哪一个是服务器,但是之后它们都运行相同的代码,所以如果我可以使它们完全对称,它会很酷。 – 2012-03-16 19:41:21

-1

我会更多地从UDP提供什么的想法来看它。 UDP是一个8字节的头部,它增加了2个字节的发送和接收端口(总共4个字节)。这些端口与Berkeley套接字进行交互以提供您的传统套接字接口。即您无法绑定到没有端口的地址,反之亦然。

通常,当您发送UDP数据包时,接收方端口(源)是短暂的,并且发送方端口(目标)是远程计算机上的目标端口。您可以通过先绑定然后连接来打败这种默认行为。现在,只要两台计算机上的相同端口都是免费的,您的源端口和目标端口就会相同。

一般来说,这种行为(让我们称之为端口劫持)令人不悦。这是因为您只是将发送端限制为只能从一个进程发送,而不是在动态分配发送端源端口的短暂模型内工作。

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

+0

没有要求源端口和目标端口是相同的。 – EJP 2015-12-31 11:20:59

+0

@EJP:我在哪里说源端口和目的端口是相同的? – Claris 2016-01-06 00:30:48

+0

IPv4标头的校验和是冗余的,而不是UDP的。 IPv6没有校验和。 – Navin 2016-11-23 18:32:59

11

下面是一个程序,演示如何将同一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 

即使我已经测试它在一台机器,我认为它应该也可以在两种不同的工作机器一旦你设置了正确的防火墙设置

下面是对th Ë流量:

  1. 设置提示指示目的地地址的类型为UDP连接的
  2. 使用getaddrinfo基于参数1,其目的地址和参数2,以获得地址信息结构dstinfo这是目的端口
  3. 创建与dstinfo
  4. 使用getaddrinfo的第一个有效条目的插座,以获得地址信息结构srcinfo主要为酸CE端口的详细
  5. 使用srcinfo绑定到获得
  6. 现在连接到的第一有效入境dstinfo
  7. 如果一切顺利进入环
  8. 的循环使用一个选择来阻止插座在由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; 
} 
1

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

The man page对连接()的UDP下使用的一些简要细节:)

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

0

。在你的代码中的问题:

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而已,你得到所有可能的addrs的列表。所以,当你调用socket时,你使用的地址可能不是你期望的UDP地址。你应该使用

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

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

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