SO_REUSEADDR と SO_REUSEPORT の違いは何ですか? 質問する

SO_REUSEADDR と SO_REUSEPORT の違いは何ですか? 質問する

man pagesソケット オプションとに関するプログラマー向けのドキュメントは、SO_REUSEADDRオペレーティングSO_REUSEPORTシステムごとに異なり、非常に混乱を招くことがよくあります。オペレーティング システムによっては、オプションが存在しないものもありますSO_REUSEPORT。WWW には、この主題に関する矛盾した情報があふれており、特定のオペレーティング システムの 1 つのソケット実装にのみ当てはまる情報が見つかることもよくあります。その情報は、テキストに明示的に記載されていない場合もあります。

では、 は と具体的にどうSO_REUSEADDR違うのでしょうかSO_REUSEPORT?

システムがない場合SO_REUSEPORTは制限がありますか?

また、異なるオペレーティング システムでどちらかを使用した場合、予想される動作は正確には何ですか?

ベストアンサー1

移植性の素晴らしい世界へようこそ... というか、移植性の欠如の世界へようこそ。これら 2 つのオプションを詳細に分析し、さまざまなオペレーティング システムでの処理方法を詳しく調べる前に、BSD ソケット実装がすべてのソケット実装の母であることに留意してください。基本的に、他のすべてのシステムは、ある時点で BSD ソケット実装 (または少なくともそのインターフェイス) をコピーし、その後独自に進化し始めました。もちろん、BSD ソケット実装も同時に進化したため、後でコピーしたシステムは、以前にコピーしたシステムに欠けていた機能を手に入れました。BSD ソケット実装を理解することは、他のすべてのソケット実装を理解するための鍵となるため、BSD システム用のコードを書くつもりがなくても、BSD ソケット実装について読んでおく必要があります。

これら 2 つのオプションを見る前に、知っておくべき基本事項がいくつかあります。TCP/UDP 接続は、次の 5 つの値のタプルによって識別されます。

{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}

これらの値の一意の組み合わせにより、接続が識別されます。したがって、2 つの接続が同じ 5 つの値を持つことはできません。そうしないと、システムはこれらの接続を区別できなくなります。

ソケットのプロトコルは、 関数を使用してソケットを作成するときに設定されますsocket()。送信元アドレスとポートは 関数で設定されますbind()。宛先アドレスとポートは 関数で設定されますconnect()。UDP はコネクションレス プロトコルであるため、UDP ソケットは接続せずに使用できます。ただし、接続が許可されており、コードや一般的なアプリケーション設計に非常に有利になる場合もあります。コネクションレス モードでは、データが初めて送信されたときに明示的にバインドされなかった UDP ソケットは、バインドされていない UDP ソケットは (応答) データを受信できないため、通常はシステムによって自動的にバインドされます。バインドされていない TCP ソケットについても同様で、接続される前に自動的にバインドされます。

ソケットを明示的にバインドする場合、ポートにバインドすることができます0。これは「任意のポート」を意味します。ソケットは実際にはすべての既存のポートにバインドすることはできないため、その場合、システムは特定のポートを自分で選択する必要があります (通常は、事前定義された OS 固有のソース ポートの範囲から)。ソース アドレスにも同様のワイルドカードがあり、これは「任意のアドレス」にすることができます ( 0.0.0.0IPv4 の場合と::IPv6 の場合)。ポートの場合とは異なり、ソケットは実際には「任意のアドレス」にバインドできます。これは「すべてのローカル インターフェイスのすべてのソース IP アドレス」を意味します。ソケットが後で接続される場合、システムは特定のソース IP アドレスを選択する必要があります。ソケットを接続すると同時に任意のローカル IP アドレスにバインドすることはできないためです。宛先アドレスとルーティング テーブルの内容に応じて、システムは適切なソース アドレスを選択し、「任意」のバインドを選択したソース IP アドレスへのバインドに置き換えます。

デフォルトでは、2 つのソケットを同じソース アドレスとソース ポートの組み合わせにバインドすることはできません。ソース ポートが異なっている限り、ソース アドレスは実際には無関係です。 が真である場合、 の場合でも を に、を にsocketAバインドすることは常に可能です。たとえば、が FTP サーバー プログラムに属していて にバインドされ、 が別の FTP サーバー プログラムに属していて にバインドされている場合、両方のバインドが成功します。ただし、ソケットはローカルで「任意のアドレス」にバインドされる可能性があることに注意してください。ソケットが にバインドされている場合、同時にすべての既存のローカル アドレスにバインドされ、その場合、 はすべての既存のローカルIP アドレスと競合するため、どの特定の IP アドレスにバインドしようとしても、他のソケットをポート にバインドすることはできません。ipA:portAsocketBipB:portBipA != ipBportA == portBsocketA192.168.0.1:21socketB10.0.0.1:210.0.0.0:21210.0.0.0

ここまでに述べたことは、すべての主要なオペレーティング システムでほぼ同等です。アドレスの再利用が関係してくると、状況は OS 固有のものになり始めます。まずは BSD から始めます。上で述べたように、BSD はすべてのソケット実装の母体です。

BSDA の

SO_REUSEADDR

SO_REUSEADDRソケットをバインドする前に を有効にすると、送信元アドレスとポートのまったく同じ組み合わせにバインドされている別のソケットと競合しない限り、ソケットは正常にバインドされます。これで、以前とどう違うのか疑問に思うかもしれません。キーワードは「正確に」です。SO_REUSEADDR主に、競合を検索するときにワイルドカード アドレス (「任意の IP アドレス」) を処理する方法を変更します。

がない場合、を にSO_REUSEADDRバインドしてから をにバインドすると、0.0.0.0 は「任意のローカル IP アドレス」を意味するため、すべてのローカル IP アドレスがこのソケットによって使用されているとみなされ、 も含まれるため、失敗します (エラー ) 。を使用すると、 は成功します。 と はまったく同じアドレスではなく、一方はすべてのローカル アドレスのワイルドカードであり、もう一方は非常に特殊なローカル アドレスであるためです。 上記のステートメントは、と がバインドされる順序に関係なく当てはまることに注意してください。を使用しない場合は常に失敗し、を使用すると常に成功します。socketA0.0.0.0:21socketB192.168.0.1:21EADDRINUSE192.168.0.1SO_REUSEADDR0.0.0.0192.168.0.1socketAsocketBSO_REUSEADDRSO_REUSEADDR

より良い概要を示すために、ここで表を作成し、すべての可能な組み合わせをリストしてみましょう。

SO_REUSEADDR socketA socketB 結果
---------------------------------------------------------------------
  オン/オフ 192.168.0.1:21 192.168.0.1:21 エラー (EADDRINUSE)
  オン/オフ 192.168.0.1:21 10.0.0.1:21 OK
  オン/オフ 10.0.0.1:21 192.168.0.1:21 OK
   オフ 0.0.0.0:21 192.168.1.0:21 エラー (EADDRINUSE)
   オフ 192.168.1.0:21 0.0.0.0:21 エラー (EADDRINUSE)
   オン 0.0.0.0:21 192.168.1.0:21 OK
   オン 192.168.1.0:21 0.0.0.0:21 OK
  オン/オフ 0.0.0.0:21 0.0.0.0:21 エラー (EADDRINUSE)

上記の表では、socketAが に指定されたアドレスにすでに正常にバインドされsocketA、がsocketB作成され、 がSO_REUSEADDR設定されるかどうかが判断され、最後に が に指定されたアドレスにバインドされることを前提としていますsocketBResultは のバインド操作の結果ですsocketB。最初の列に と記載されている場合ON/OFF、 の値はSO_REUSEADDR結果とは無関係です。

SO_REUSEADDRワイルドカード アドレスに影響があるのはわかりました。知っておいてよかったです。しかし、それが唯一の影響ではありません。よく知られている別の影響があり、それがそもそもほとんどの人がサーバー プログラムで使用する理由でもあります。SO_REUSEADDRこのオプションのその他の重要な用途については、TCP プロトコルの動作を詳しく調べる必要があります。

TCPソケットが閉じられる場合、通常は3ウェイハンドシェイクが実行されます。このシーケンスは次のように呼ばれます。FIN-ACKここで問題となるのは、そのシーケンスの最後の ACK が相手側に到着しているか到着していないかのどちらかであり、到着している場合のみ、相手側もソケットが完全に閉じられていると見なすということです。一部のリモート ピアによってまだ開いていると見なされる可能性のあるアドレス + ポートの組み合わせの再利用を防ぐために、システムは最後の ACK を送信した後すぐにソケットがデッドであると見なすのではなくACK、代わりにソケットを一般に と呼ばれる状態にしますTIME_WAIT。ソケットは数分間その状態になることがあります (システム依存の設定)。ほとんどのシステムでは、リンガーを有効にしてリンガー時間を 0 に設定することでその状態を回避できますが、これが常に可能であること、システムが常にこの要求を受け入れること、システムがそれを受け入れる場合でも、ソケットがリセットによって閉じられること (RST)ですが、これは必ずしも良いアイデアとは言えません。 リンガータイムについて詳しくは、このトピックについての私の答え

問題は、システムが状態 のソケットをどのように扱うかということです。が設定されていないTIME_WAIT場合、状態 のソケットはソース アドレスとポートにバインドされたままであると見なされ、ソケットが実際に閉じられるまで、同じアドレスとポートに新しいソケットをバインドしようとすると失敗します。したがって、ソケットを閉じた直後にそのソース アドレスを再バインドできるとは思わないでください。ほとんどの場合、これは失敗します。ただし、バインドしようとしているソケットに が設定されている場合、状態 で同じアドレスとポートにバインドされている別のソケットは単に無視されます。結局のところ、そのソケットはすでに「半分死んで」おり、ソケットはまったく同じアドレスに問題なくバインドできます。その場合、他のソケットがまったく同じアドレスとポートを持っていることは何の影響も及ぼしません。状態 で死にかけのソケットとまったく同じアドレスとポートにソケットをバインドすると、他のソケットがまだ「動作中」の場合、予期しない、通常は望ましくない副作用が生じる可能性があることに注意してください。ただし、これはこの回答の範囲外であり、幸いなことに、実際にはそのような副作用はまれです。SO_REUSEADDRTIME_WAITSO_REUSEADDRTIME_WAITTIME_WAIT

最後に、知っておくべきことが 1 つありますSO_REUSEADDR。バインドするソケットでアドレスの再利用が有効になっている限り、上記のすべては機能します。バインド済みのソケットまたは状態にあるソケットでもTIME_WAIT、バインド時にこのフラグが設定されている必要はありません。バインドが成功するか失敗するかを決定するコードは、呼び出しSO_REUSEADDRに渡されたソケットのフラグのみを検査しますbind()。検査される他のすべてのソケットでは、このフラグは確認されません。

SO_REUSEポート

SO_REUSEPORT is what most people would expect SO_REUSEADDR to be. Basically, SO_REUSEPORT allows you to bind an arbitrary number of sockets to exactly the same source address and port as long as all prior bound sockets also had SO_REUSEPORT set before they were bound. If the first socket that is bound to an address and port does not have SO_REUSEPORT set, no other socket can be bound to exactly the same address and port, regardless if this other socket has SO_REUSEPORT set or not, until the first socket releases its binding again. Unlike in case of SO_REUSEADDR the code handling SO_REUSEPORT will not only verify that the currently bound socket has SO_REUSEPORT set but it will also verify that the socket with a conflicting address and port had SO_REUSEPORT set when it was bound.

SO_REUSEPORT does not imply SO_REUSEADDR. This means if a socket did not have SO_REUSEPORT set when it was bound and another socket has SO_REUSEPORT set when it is bound to exactly the same address and port, the bind fails, which is expected, but it also fails if the other socket is already dying and is in TIME_WAIT state. To be able to bind a socket to the same addresses and port as another socket in TIME_WAIT state requires either SO_REUSEADDR to be set on that socket or SO_REUSEPORT must have been set on both sockets prior to binding them. Of course it is allowed to set both, SO_REUSEPORT and SO_REUSEADDR, on a socket.

There is not much more to say about SO_REUSEPORT other than that it was added later than SO_REUSEADDR, that's why you will not find it in many socket implementations of other systems, which "forked" the BSD code before this option was added, and that there was no way to bind two sockets to exactly the same socket address in BSD prior to this option.

Connect() Returning EADDRINUSE?

Most people know that bind() may fail with the error EADDRINUSE, however, when you start playing around with address reuse, you may run into the strange situation that connect() fails with that error as well. How can this be? How can a remote address, after all that's what connect adds to a socket, be already in use? Connecting multiple sockets to exactly the same remote address has never been a problem before, so what's going wrong here?

As I said on the very top of my reply, a connection is defined by a tuple of five values, remember? And I also said, that these five values must be unique otherwise the system cannot distinguish two connections any longer, right? Well, with address reuse, you can bind two sockets of the same protocol to the same source address and port. That means three of those five values are already the same for these two sockets. If you now try to connect both of these sockets also to the same destination address and port, you would create two connected sockets, whose tuples are absolutely identical. This cannot work, at least not for TCP connections (UDP connections are no real connections anyway). If data arrived for either one of the two connections, the system could not tell which connection the data belongs to. At least the destination address or destination port must be different for either connection, so that the system has no problem to identify to which connection incoming data belongs to.

So if you bind two sockets of the same protocol to the same source address and port and try to connect them both to the same destination address and port, connect() will actually fail with the error EADDRINUSE for the second socket you try to connect, which means that a socket with an identical tuple of five values is already connected.

Multicast Addresses

Most people ignore the fact that multicast addresses exist, but they do exist. While unicast addresses are used for one-to-one communication, multicast addresses are used for one-to-many communication. Most people got aware of multicast addresses when they learned about IPv6 but multicast addresses also existed in IPv4, even though this feature was never widely used on the public Internet.

The meaning of SO_REUSEADDR changes for multicast addresses as it allows multiple sockets to be bound to exactly the same combination of source multicast address and port. In other words, for multicast addresses SO_REUSEADDR behaves exactly as SO_REUSEPORT for unicast addresses. Actually, the code treats SO_REUSEADDR and SO_REUSEPORT identically for multicast addresses, that means you could say that SO_REUSEADDR implies SO_REUSEPORT for all multicast addresses and the other way round.


FreeBSD/OpenBSD/NetBSD

All these are rather late forks of the original BSD code, that's why they all three offer the same options as BSD and they also behave the same way as in BSD.


macOS (MacOS X)

At its core, macOS is simply a BSD-style UNIX named "Darwin", based on a rather late fork of the BSD code (BSD 4.3), which was then later on even re-synchronized with the (at that time current) FreeBSD 5 code base for the Mac OS 10.3 release, so that Apple could gain full POSIX compliance (macOS is POSIX certified). Despite having a microkernel at its core ("Mach"), the rest of the kernel ("XNU") is basically just a BSD kernel, and that's why macOS offers the same options as BSD and they also behave the same way as in BSD.

iOS / watchOS / tvOS

iOS is just a macOS fork with a slightly modified and trimmed kernel, somewhat stripped down user space toolset and a slightly different default framework set. watchOS and tvOS are iOS forks, that are stripped down even further (especially watchOS). To my best knowledge they all behave exactly as macOS does.


Linux

Linux < 3.9

Prior to Linux 3.9, only the option SO_REUSEADDR existed. This option behaves generally the same as in BSD with two important exceptions:

  1. As long as a listening (server) TCP socket is bound to a specific port, the SO_REUSEADDR option is entirely ignored for all sockets targeting that port. Binding a second socket to the same port is only possible if it was also possible in BSD without having SO_REUSEADDR set. E.g. you cannot bind to a wildcard address and then to a more specific one or the other way round, both is possible in BSD if you set SO_REUSEADDR. What you can do is you can bind to the same port and two different non-wildcard addresses, as that's always allowed. In this aspect Linux is more restrictive than BSD.

  2. The second exception is that for client sockets, this option behaves exactly like SO_REUSEPORT in BSD, as long as both had this flag set before they were bound. The reason for allowing that was simply that it is important to be able to bind multiple sockets to exactly to the same UDP socket address for various protocols and as there used to be no SO_REUSEPORT prior to 3.9, the behavior of SO_REUSEADDR was altered accordingly to fill that gap. In that aspect Linux is less restrictive than BSD.

Linux >= 3.9

Linux 3.9 added the option SO_REUSEPORT to Linux as well. This option behaves exactly like the option in BSD and allows binding to exactly the same address and port number as long as all sockets have this option set prior to binding them.

Yet, there are still two differences to SO_REUSEPORT on other systems:

  1. To prevent "port hijacking", there is one special limitation: All sockets that want to share the same address and port combination must belong to processes that share the same effective user ID! So one user cannot "steal" ports of another user. This is some special magic to somewhat compensate for the missing SO_EXCLBIND/SO_EXCLUSIVEADDRUSE flags.

  2. Additionally the kernel performs some "special magic" for SO_REUSEPORT sockets that isn't found in other operating systems: For UDP sockets, it tries to distribute datagrams evenly, for TCP listening sockets, it tries to distribute incoming connect requests (those accepted by calling accept()) evenly across all the sockets that share the same address and port combination. Thus an application can easily open the same port in multiple child processes and then use SO_REUSEPORT to get a very inexpensive load balancing.


Android

Even though the whole Android system is somewhat different from most Linux distributions, at its core works a slightly modified Linux kernel, thus everything that applies to Linux should apply to Android as well.


Windows

Windows only knows the SO_REUSEADDR option, there is no SO_REUSEPORT. Setting SO_REUSEADDR on a socket in Windows behaves like setting SO_REUSEPORT and SO_REUSEADDR on a socket in BSD, with one exception:

Prior to Windows 2003, a socket with SO_REUSEADDR could always been bound to exactly the same source address and port as an already bound socket, even if the other socket did not have this option set when it was bound. This behavior allowed an application "to steal" the connected port of another application. Needless to say that this has major security implications!

Microsoft realized that and added another important socket option: SO_EXCLUSIVEADDRUSE. Setting SO_EXCLUSIVEADDRUSE on a socket makes sure that if the binding succeeds, the combination of source address and port is owned exclusively by this socket and no other socket can bind to them, not even if it has SO_REUSEADDR set.

This default behavior was changed first in Windows 2003, Microsoft calls that "Enhanced Socket Security" (funny name for a behavior that is default on all other major operating systems). For more details just visit this page. There are three tables: The first one shows the classic behavior (still in use when using compatibility modes!), the second one shows the behavior of Windows 2003 and up when the bind() calls are made by the same user, and the third one when the bind() calls are made by different users.


Solaris

Solaris is the successor of SunOS. SunOS was originally based on a fork of BSD, SunOS 5 and later was based on a fork of SVR4, however SVR4 is a merge of BSD, System V, and Xenix, so up to some degree Solaris is also a BSD fork, and a rather early one. As a result Solaris only knows SO_REUSEADDR, there is no SO_REUSEPORT. The SO_REUSEADDR behaves pretty much the same as it does in BSD. As far as I know there is no way to get the same behavior as SO_REUSEPORT in Solaris, that means it is not possible to bind two sockets to exactly the same address and port.

Windows と同様に、Solaris にはソケットに排他的バインドを与えるオプションがあります。このオプションは と呼ばれますSO_EXCLBIND。ソケットをバインドする前にこのオプションを設定すると、SO_REUSEADDR2 つのソケットでアドレス競合がテストされる場合、別のソケットで を設定しても効果はありません。たとえば、socketAがワイルドカード アドレスにバインドされ、が有効になってsocketBおりSO_REUSEADDR、 が非ワイルドカード アドレスで と同じポートにバインドされている場合、 が有効になっていないsocketA限り、このバインドは通常成功します。 が有効になってsocketAいるSO_EXCLBIND場合は、SO_REUSEADDRのフラグに関係なく失敗しますsocketB


その他のシステム

お使いのシステムが上記に記載されていない場合は、システムがこれら 2 つのオプションをどのように処理するかを調べるために使用できる小さなテスト プログラムを作成しました。また、私の結果が間違っていると思われる場合は、コメントを投稿して虚偽の主張をする前に、まずそのプログラムを実行してください。

コードをビルドするために必要なのは、少しの POSIX API (ネットワーク部分用) と C99 コンパイラー (実際には、 と を提供している限り、ほとんどの非 C99 コンパイラーでも同様に動作します。inttypes.hたとえばstdbool.hgcc完全な C99 サポートを提供するずっと前から両方をサポートしていました) だけです。

プログラムを実行するために必要なのは、システム内の少なくとも 1 つのインターフェイス (ローカル インターフェイス以外) に IP アドレスが割り当てられ、そのインターフェイスを使用するデフォルト ルートが設定されていることです。プログラムはその IP アドレスを収集し、それを 2 番目の「特定のアドレス」として使用します。

考えられるすべての組み合わせをテストします。

  • TCP および UDP プロトコル
  • 通常のソケット、リッスン(サーバー)ソケット、マルチキャストソケット
  • SO_REUSEADDRソケット1、ソケット2、または両方のソケットに設定する
  • SO_REUSEPORTソケット1、ソケット2、または両方のソケットに設定する
  • 0.0.0.0(ワイルドカード)、127.0.0.1(特定のアドレス)、およびプライマリ インターフェイスで見つかった 2 番目の特定のアドレスから作成できるすべてのアドレスの組み合わせ(マルチキャストの場合224.1.2.3はすべてのテストで使用されます)

結果をわかりやすい表に出力します。 を認識しないシステムでも動作しますがSO_REUSEPORT、その場合このオプションはテストされません。

プログラムで簡単にテストできないのは、SO_REUSEADDRソケットをその状態に強制して維持するのは非常に難しいため、ソケットの状態に対して がどう作用するかということですTIME_WAIT。幸いなことに、ほとんどのオペレーティング システムはここでは BSD のように単純に動作するようで、ほとんどの場合、プログラマーはその状態の存在を単純に無視できます。

コードはこちら(ここには含めることができません。回答にはサイズ制限があり、コードによってこの返信が制限を超えてしまいます)。

おすすめ記事