web-dev-qa-db-ja.com

UDP接続の両端をbind()およびconnect()できますか

私はポイントツーポイントのメッセージキューシステムを書いています、そしてそれはUDP上で動作できなければなりません。どちらか一方を勝手に「サーバー」として選択することもできますが、両端が同じタイプのデータをもう一方から送受信しているため、正しくないようです。

相互にのみ送受信するように、両端をbind()およびconnect()することは可能ですか?これは、対称的な方法のように思えます。

34
gct

UDPはコネクションレスであるため、OSが実際に何らかの接続を行うことにはほとんど意味がありません。

BSDソケットでは、UDPソケットでconnectを実行できますが、これは基本的にsendのデフォルトの宛先アドレスを設定するだけです(代わりにsend_to)。

UDPソケットのバインドは、ソケットの種類に関係なく、実際にパケットを受け入れる着信アドレスをOSに指示します(他のアドレスへのすべてのパケットはドロップされます)。

受信時には、recvfromを使用して、パケットの送信元を識別する必要があります。何らかの認証が必要な場合、関係するアドレスだけを使用すると、ロックがまったくないのと同じくらい安全ではないことに注意してください。 TCP接続がハイジャックされる可能性があり、ネイキッドUDPは文字通りIPスプーフィングが全面的に書き込まれます。何らかのHMACを追加する必要があります

27
datenwolf

こんにちは、2018年の遠い未来から2012年まで。

実際には、実際にUDPソケットをconnect() ingする背後に理由があります(ただし、祝福されたPOSIXは理論的にはそうする必要はありません)。

通常のUDPソケットは将来の宛先について何も知らないため、 sendmsg()が呼び出されるたびにルートルックアップを実行します

ただし、特定のリモートレシーバーのIPとポートでconnect()が事前に呼び出された場合、オペレーティングシステムカーネルは ルートへの参照を書き留めてソケットに割り当てる 、後続の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ソケットでも完全に機能します

深刻なパフォーマンス上の利点 のソースである可能性がありますが、使用しているOSに大きく依存します。少なくとも、Linuxを使用していて、複数のリモートハンドラーにソケットを使用しない場合は、試してみてください。

20
ximaera

以下は、同じUDPソケットで特定の送信元ポートと宛先ポートのセットにそれぞれbind()およびconnect()する方法を示すプログラムです。このプログラムは任意のLinuxマシンでコンパイルでき、次の使用方法があります。

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

このコードをテストして、2つの端末を開きました。宛先ノードにメッセージを送信し、そこからメッセージを受信できるはずです。

ターミナル1の実行時

./<program_name> 127.0.0.1 5555 5556

ターミナル2の実行時

./<program_name> 127.0.0.1 5556 5555

単一のマシンでテストしましたが、正しいファイアウォール設定をセットアップすると、2つの異なるマシンでも動作するはずです。

フローの説明は次のとおりです。

  1. セットアップヒントは、宛先アドレスのタイプをUDP接続のタイプとして示しました
  2. Getaddrinfoを使用して、宛先アドレスである引数1と宛先ポートである引数2に基づいてアドレス情報構造dstinfoを取得します。
  3. dstinfoの最初の有効なエントリでソケットを作成します
  4. Getaddrinfoを使用して、主に送信元ポートの詳細についてアドレス情報構造srcinfoを取得します。
  5. srcinfoを使用して、取得したソケットにバインドします
  6. dstinfoの最初の有効なエントリに接続します
  7. すべてが順調にループに入った場合
  8. ループは、作成されたSTDINおよびsockfdソケットで構成される読み取り記述子リストでブロックするためにselectを使用します
  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;
}
14
Conrad Gomes

本当にキーはconnect()です:

ソケットsockfdのタイプがSOCK_DGRAMの場合、addrはデフォルトでデータグラムが送信されるアドレスであり、データグラムが受信される唯一のアドレスです。

5
Geoff Reedy

UDPでconnect()を使用していません。 connect()はUDPとTCPの下で2つの全く異なる目的のために設計されたと感じています。

manページ には、UDPでのconnect()の使用に関する簡単な詳細があります。

一般に、接続ベースのプロトコル(TCPなど)ソケットは、一度だけ正常にconnect()できます。コネクションレスプロトコル(UDPなど)ソケットは、connect()を複数回使用して関連付けを変更できます。

1

このページには、接続ソケットと非接続ソケットに関する優れた情報が含まれています。 http://www.masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch08lev1sec11.html

この引用はあなたの質問に答えます:

通常、接続を呼び出すのはUDPクライアントですが、UDPサーバーが単一のクライアントと長時間通信するアプリケーションがあります(TFTPなど)。この場合、クライアントとサーバーの両方がconnectを呼び出すことができます。

1
konrad

コードに問題があります:

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のリストを取得します。そのため、ソケットを呼び出すとき、使用しているアドレスは期待したUDPアドレスではない可能性があります。あなたが使用する必要があります

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

代わりに、取得するaddrinfoが希望どおりであることを確認してください。

別のWordでは、作成したソケットはUDPソケットではない可能性があり、それが機能しない理由です。

1
Jack

はい、できます。私もやります。

そして、あなたのユースケースはこれが有用なものです:両側がクライアントとサーバーの両方として機能し、両側に1つのプロセスしかありません。

0
Droopycom