ネットワークプログラミングソケット (オン)

目次

1.予備知識

1.1 ポート番号

1.2 TCP プロトコルと UDP プロトコルの予備知識

1.3 ネットワークバイトオーダー

2、ソケット プログラミング インターフェイス

2.1 一般的なソケット API

2.2 sockaddr 構造

3.UDPネットワークプログラム

3.1 サーバーの初期化

3.1.1 サーバーがソケットを作成する

3.1.2 サーバーバインディング

3.1.3 文字列 IP VS 整数 IP

3.2 サーバーの起動と実行

3.3 クライアントの初期化

3.3.1 クライアントがソケットを作成する

3.3.2 クライアントバインディングの問題

3.4 クライアントの起動と実行

3.5 ローカルテスト

3.6 INADDR_ANY

3.7 エコー機能

3.8 ネットワークテスト


1.予備知識

1.1 ポート番号

ソケット通信の本質

IPアドレスとMACアドレスを介してピアホストにデータを送信することもできますが、実際にはピアホスト上のサービスプロセスにデータを送信したい.また、データの送信者はホストではなく、ブラウザを使用してアクセスする場合など、特定のプロセスは、実際にはブラウザ プロセスによってピア サービス プロセスに対して開始される要求です。

ソケット通信は本質的には 2 つのプロセス間の通信ですが、ここではネットワークを介したプロセス間通信です。たとえば、Taobao にアクセスして Douyin を閲覧するというアクションは、実際には、携帯電話上の Taobao クライアント プロセスおよび Douyin クライアント プロセスが、相手のサーバー ホスト上の Taobao サービス プロセスおよび Douyin サービス プロセスと通信することを意味します。

パイプライン、メッセージキュー、セマフォ、共有メモリなどに加えて、プロセス間通信方法にはソケットも含まれますが、前者はネットワークを横断しませんが、後者はネットワークを横断できるかどうかにかかわらず

ポート番号

2 つのホスト上で同時にネットワークを介して通信している複数のプロセスが存在する可能性があるため、データがピア ホストに到達すると、何らかの方法でホスト上の対応するサービス プロセスを見つけて、そのプロセスを引き渡す必要があります。処理のためのプロセスへのデータ。また、プロセスがデータの処理を終了すると、送信者に応答する必要があるため、ピア ホストは、送信者のどのプロセスがデータ要求を送信したかを知る必要もあります。

ポート番号の役割は、ホスト上のプロセスを識別することです。

  • ポート番号は、トランスポート層プロトコルの内容です
  • ポート番号は 2 バイトの 16 ビット整数です
  • ポート番号はプロセスを識別するために使用されるため、オペレーティング システムは現在のデータをどのプロセスに渡す必要があるかを認識します。
  • ポート番号は 1 つのプロセスだけが占有できます

データがトランスポート層でカプセル化されると、送信元ポート番号と宛先ポート番号に対応する情報が追加されます。このとき、データを送信するプロセスは、送信元 IP アドレス + 送信元ポート番号によってネットワーク上で一意に識別でき、データを受信するプロセスは、宛先 IP アドレス + 宛先ポート番号によってネットワーク上で一意に識別できます。コミュニケーション

注:ポート番号は特定のホストに属しているため、2 つの異なるホストでポート番号を繰り返すことができますが、同じホストでネットワーク通信を行うプロセスのポート番号は繰り返すことができません。さらに、プロセスは複数のポート番号をバインドできますが、ポート番号は同時に複数のプロセスによってバインドできません

prot VS PID

ポート番号(ポート)はホスト上のプロセスを一意に識別でき、プロセス ID(PID)もホスト上のプロセスを一意に識別できるため、ネットワーク通信でポートの代わりに PID を直接使用してみませんか?

プロセス ID (PID) はシステム内のすべてのプロセスの一意性を識別するために使用され、これはシステムの概念に属し、ポート番号 (ポート) はネットワーク データを外部に要求する必要があるプロセスの一意性を識別するために使用されます。 、ネットワークの概念に属します

PID はネットワーク プロセスの一意性を識別するために使用できますが、システム部分とネットワーク部分がインターリーブされ、結合度が高くなります。

また、プロセスは複数のポート番号をバインドできますが、プロセスは 1 つの PID にしか対応できません。

ポートを介して対応するプロセスを見つける方法は?

実際の最下層は、ハッシュ方式を使用して、ポート番号とプロセス PID または PCB との間のマッピング関係を確立し、最下層がポート番号を取得すると、対応するハッシュ アルゴリズムを直接実行し、それに対応するプロセスを自然に見つけることができます。ポート番号。

1.2 TCP プロトコルと UDP プロトコルの予備知識

ネットワーク プロトコル スタックは、アーキテクチャ全体で実行され、アプリケーション層、オペレーティング システム層、およびドライバー層に存在します。システム コールを使用してネットワーク通信を実装する場合、直面しなければならないプロトコル層はトランスポート層であり、トランスポート層の最も典型的な 2 つのプロトコルは TCP プロトコルと UDP プロトコルです。

TCP プロトコル

TCP プロトコルはTransmission Control Protocol (Transmission Control Protocol)と呼ばれ、TCP プロトコルは接続指向で信頼性の高い、バイトストリームベースのトランスポート層通信プロトコルです。

TCP プロトコルは接続指向です. 2 つのホスト間でデータを送信する場合は、まず接続を確立する必要があり、接続が正常に確立された後にのみデータ送信を実行できます. 第二に、TCP プロトコルは信頼性を保証するプロトコルであり、データ伝送中にパケットの損失やシーケンスの乱れが発生した場合、TCP プロトコルには対応するソリューションがあります。

UDP プロトコル

UDP プロトコルはUser Datagram Protocol (User Datagram Protocol)と呼ばれます. UDP プロトコルは、接続を確立する必要のない、信頼性の低いデータグラム指向のトランスポート層通信プロトコルです。

通信にUDPプロトコルを使用する場合、接続を確立する必要はありません.2つのホストがデータを送信したい場合は、ピアホストに直接データを送信するだけですが、UDPプロトコルは信頼できないことも意味します.損失、障害など、UDP プロトコルは認識されません

UDP プロトコルは信頼できないのに、なぜ UDP プロトコルが存在するのですか?

信頼性にはより多くの作業が必要 TCP プロトコルは信頼性の高い伝送プロトコルですが、TCP プロトコルは最下層でより多くの作業を行う必要があるため、TCP プロトコルの最下層の実装はより複雑になります。

UDPプロトコルは信頼性の低い伝送プロトコルですが、UDPプロトコルは最下層であまり多くの作業を行う必要がないため、UDPプロトコルの最下層の実装はTCPプロトコルよりも簡単です.UDPプロトコルは信頼性が低いですが、相手に送ったデータを素早く転送できる

ネットワーク通信コードを記述するときに TCP プロトコルと UDP プロトコルのどちらを使用するかは、上位層のアプリケーション シナリオに完全に依存します。アプリケーション シナリオで送信中のデータの信頼性が厳密に要求される場合は、現時点で TCP プロトコルが使用されます。アプリケーション シナリオでデータ送信中に少量のパケット損失が許容される場合は、UDP プロトコルが優先されます。シンプルで十分に速い

注:一部の優れた Web サイトでは、ネットワーク通信アルゴリズムを設計する際に TCP プロトコルと UDP プロトコルを同時に使用しています.ネットワークがスムーズな場合はデータ転送に UDP プロトコルが使用され、ネットワーク速度が良くない場合は TCP プロトコルがデータ転送に使用されます. . バックグラウンド データ通信を調整するための動的アルゴリズム

1.3 ネットワークバイトオーダー

コンピュータには、データを格納する際に大小の概念があります。

  • ビッグ エンディアン モード:データの上位バイトの内容はメモリの下位アドレスに格納され、データの下位バイトの内容はメモリの上位アドレスに格納されます。
  • リトル エンディアン モード:データの上位バイトの内容はメモリの上位アドレスに保存され、データの下位バイトの内容はメモリの下位アドレスに保存されます。

作成したプログラムがローカル マシンでのみ実行される場合は、ビッグ エンディアンとスモール エンディアンの問題を考慮する必要はありません. 同じマシン上のデータは同じ方法で保存されます. ビッグ エンディアン ストレージ モードが使用されるか、またはスモール エンディアン ストレージ モードが使用されます。

ただし、ネットワーク通信が関係している場合は、ビッグ エンドとスモール エンドの問題を考慮する必要があります。そうしないと、ピア ホストによって識別されるデータが、送信者が送信したいデータと矛盾する可能性があります。

たとえば、2 つのホスト間のネットワーク通信の場合、送信側はリトル エンディアン マシンであり、受信側はビッグ エンディアン マシンです。送信側が送信バッファ内のデータをメモリアドレスの下位から上位へ順に送信した後、受信側がネットワークからデータを取得して受信バッファに順次格納する際にも、メモリ順に格納されます。ただし、リトル エンディアン マシンとビッグ エンディアン マシンでは、メモリ内のデータの解釈が異なります。

メモリアドレスが下位から上位へ44332211の配列は、送信側ではリトルエンディアン的には0x11223344と認識され、受信側ではビッグエンディアン的には0x44332211と認識されるため、データが異なるためデータ認識のエラーにつながる大小の端のずれに

ビッグエンディアンとスモールエンディアンの差分情報を解決するには?

TCP/IP プロトコルは、ネットワーク データ フローがビッグ エンディアンのバイト順、つまり下位アドレスと上位バイトを採用することを規定しています。ビッグ エンディアン マシンでもリトル エンディアン マシンでも、TCP/IP プロトコルで指定されたネットワーク バイト オーダーに従ってデータを送受信する必要があります。

  • 送信側がリトル エンディアンの場合は、データをビッグ エンディアンに変換してからネットワークに送信します。
  • 送信先がビッグエンディアンの場合はそのまま送信可能
  • 受信側がリトルエンディアンの場合、受信データをリトルエンディアンに変換してからデータ識別を行う
  • 受信側がビッグエンディアンの場合、直接データ識別が可能

次のように、送信側はリトルエンディアン マシンであり、データを送信する前に、データをビッグ エンディアンに変換してからネットワークに送信します。受信側はビッグ エンディアン マシンであるため、受信側はこのとき、受信側で認識されるデータは、送信側が本来送信するつもりだったデータと同じです。

ビッグエンディアンとスモールエンディアンの変換作業のほとんどはオペレーティング システムによって行われ、この操作は通信の詳細です。ポート番号やIPアドレスなど、プログラマが処理する必要のある情報もあります。

ネットワーク バイト オーダーがビッグ エンディアンを使用しているのはなぜですか? リトルエンディアンの代わりに?

  • TCP は Unix の時代に存在しました. 過去には、Unix マシンはすべてビッグ エンディアン マシンだったので、ネットワーク バイト オーダーはビッグ エンディアンでしたが、後に人々はリトル エンディアンを使用するとハードウェア設計を簡素化できることに気付きました。はすべてリトルエンディアンのマシンですが、プロトコルを変更するのは不便です
  •  ビッグ エンディアンは、現代人の読み書きの習慣により近いものです。

ネットワーク バイト オーダーとホスト バイト オーダー間の変換

ネットワーク プログラムを移植可能にし、同じ C コードをビッグ エンディアンおよびリトルエンディアンのコンピューターでコンパイルした後に正常に実行できるようにするために、次のライブラリ関数を呼び出して、ネットワーク バイト オーダーとホスト バイト オーダー間の変換を実現できます。

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

関数名の h はホスト、n はネットワーク、l は 32 ビットの長整数、s は 16 ビットの短整数を意味します。

  • たとえば、htonl() は、32 ビット長整数をホスト バイト オーダーからネットワーク バイト オーダーに変換することを意味します。
  • ホストがリトル エンディアンの場合、関数はパラメータを対応するビッグ エンディアンに変換し、戻り値を返します。
  • ホストがビッグ エンディアンの場合、これらの関数は変換を実行せず、引数を変更せずに返します。

2、ソケット プログラミング インターフェイス

2.1 一般的なソケット API

ソケットの作成: (TCP/UDP、クライアント + サーバー)

int socket(int domain, int type, int protocol);

バインドポート番号: (TCP/UDP、サーバー)

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

リッスン ソケット: (TCP、サーバー)

int listen(int sockfd, int backlog);

受信リクエスト: (TCP、サーバー)

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接続の確立: (TCP、クライアント)

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

2.2 sockaddr 構造

sockaddr 構造体の外観

ソケットは、ネットワークを介したプロセス間通信だけでなく、ローカルのプロセス間通信 (ドメイン間ソケット) もサポートします。クロスネットワーク通信中に渡す必要があるが、ローカル通信には必要ないポート番号と IP アドレス。したがって、ソケットは sockaddr_in 構造体と sockaddr_un 構造体を提供し、sockaddr_in 構造体はクロスネットワーク通信に使用され、sockaddr_un 構造体は構造体はローカル通信に使用されます

ソケットのネットワーク通信とローカル通信が同じ関数インターフェイスのセットを使用できるようにするために、sockaddr_in と sockaddr_un の構造とは異なる sockaddr 構造が登場しましたが、これら 3 つの構造の 16 個のヘッダー ビット、このフィールドプロトコルファミリーと呼ばれる

このとき、パラメータを渡す場合は、sockeaddr_in 構造体や sockeaddr_un 構造体で渡す必要はなく、一律に sockeaddr などの構造体で渡す必要があります。パラメータを設定するときに、プロトコル ファミリ フィールドを設定して、ネットワーク通信またはローカル通信が必要かどうかを示すことができます。これらの API では、sockeaddr 構造体のヘッダーの 16 ビットを抽出して識別し、ネットワーク通信が必要かローカル通信が必要かを判断し、対応する操作を実行します。このとき、ソケットネットワーク通信とローカル通信のパラメータ型は、一般的な sockaddr 構造体で統一されています。

注:実際のネットワーク通信では、sockaddr_in 構造体変数は定義されたままですが、パラメータを渡すときに、構造体変数アドレスの型を sockaddr* に強制する必要があります。

  • IPv4 および IPv6 のアドレス形式は netinet/in.h で定義され、IPv4 アドレスは sockaddr_in 構造体で表され、16 ビットのアドレス タイプ、16 ビットのポート番号、および 32 ビットの IP アドレスが含まれます。
  • IPv4 および IPv6 アドレス タイプは、それぞれ定数 AF_INET および AF_INET6 として定義されます。sockaddr 構造体の最初のアドレスが取得されている限り、それがどのような sockaddr 構造体であるかを知らなくても、アドレス タイプ フィールドに従って構造体の内容を判断できます。
  • ソケット API は、使用時に sockaddr_in に変換する必要がある struct sockaddr* 型で表すことができます; これの利点は、IPv4、IPv6、および UNIX のさまざまな種類の sockaddr 構造体ポインターを受け取ることができるプログラムの汎用性です。パラメータとしてのドメインソケット

struct sockaddr* 型の代わりに void* を使用しないのはなぜですか? 

これらの関数の struct sockaddr* パラメータの型を void* に変更することもできます.このとき、抽出するヘッダの16ビットを関数内で直接指定して識別し、最終的にネットワーク通信かローカル通信かを判断することができます.なぜですか? sockaddr のような構造を設計するのはどうですか?

実際、この一連のネットワーク インターフェイスを設計するとき、C 言語は void* をサポートしていなかったため、sockaddr などのソリューションが設計されました。また、C 言語が void* をサポートした後も、元に戻すことはありません。これは、これらのインターフェイスがシステム インターフェイスであり、システム インターフェイスがすべての上位層ソフトウェア インターフェイスの基礎であるためです。システム インターフェイスは簡単に変更できません。そうしないと、結果が変わります。これが、sockaddr 構造がまだ保持されている理由です。

3.UDPネットワークプログラム

3.1 サーバーの初期化

3.1.1 サーバーがソケットを作成する

サーバーをクラスにカプセル化します。サーバー オブジェクトが定義されたら、サーバーをすぐに初期化する必要があります。サーバーを初期化するときに最初に行うことは、ソケットを作成することです。

ソケット機能

  • domain:ソケットが作成されるドメイン (プロトコルファミリー)、つまり、作成されるソケットのタイプ。このパラメーターは、struct sockaddr 構造体の最初の 16 ビットに相当します。ローカル通信の場合は AF_UNIX、ネットワーク通信の場合は AF_INET (IPv4) または AF_INET6 (IPv6) に設定します。
  • type:ソケットの作成時に必要なサービスのタイプ。最も一般的なサービス タイプは SOCK_STREAM と SOCK_DGRAM です。UDPベースのネットワーク通信であればSOCK_DGRAM(ユーザーデータグラムサービス)を利用し、TCPベースのネットワーク通信であればストリーミングサービスを提供するSOCK_STREAM(ストリーミングソケット)を利用します。
  • protocol:ソケットを作成するためのプロトコル クラス。TCP または UDP として指定できますが、通常、このフィールドはデフォルトを意味する 0 に設定できます。このとき、渡された最初の 2 つのパラメーターに従って、最終的に使用するプロトコルが自動的に推定されます。

戻り値:ソケットの作成に成功した場合はファイル記述子が返され、作成に失敗した場合は -1 が返され、同時にエラー コードが設定されます。

ソケット関数はどのタイプのインターフェースに属しますか?

ネットワーク プロトコル スタックは階層化されており、TCP/IP の 4 層モデルによれば、上からアプリケーション層、トランスポート層、ネットワーク層、データ リンク層です。現在書かれているコードはユーザーレベルコード、つまりアプリケーション層で書かれているので、実際の呼び出しは下位3層のインターフェースで、トランスポート層とネットワーク層はオペレーティングシステムで完結するであるため、socket( ) はシステム コール インターフェイスに属します。

基礎となるソケット関数は何をしますか?

ソケット関数はプロセスによって呼び出され、各プロセスには PCB (task_struct)、ファイル記述子テーブル (files_struct)、およびシステム レベルで開いているさまざまなファイルがあります。ファイル記述子テーブルには、配列 fd_array が含まれています。配列内の添字 0、1、および 2 は、標準入力、標準出力、および標準エラーに対応しています。

ソケット関数を呼び出してソケットを作成することは、実際には「ネットワーク ファイル」を開くことと同じです. 開いた後、対応する構造体ファイル構造がカーネル レベルで形成され、その構造体が対応するプロセスに接続されます。二重連結リストをファイルし、構造体の先頭アドレスを配列 fd_array の添字 3 の位置に埋めます.このとき、fd_array 配列の添字 3 のポインタは、開いている「ネットワークファイル」を指し、最後のnumber 3 ファイル記述子は、ソケット関数の戻り値としてユーザーに返されます

各構造体ファイルの構造体には、ファイル属性情報、操作方法、ファイル バッファなど、開いているファイルに対応するさまざまな情報が含まれています。ファイルに対応する属性は、カーネル内の struct inode 構造によって維持され、ファイルに対応する操作メソッドは、実際にはカーネル内の一連の関数ポインター (read* や write* など) によって維持されます。 struct file_operations の構造体。ファイルバッファは通常、開いた通常のファイルのディスクに対応しますが、現在開いている「ネットワークファイル」の場合、ファイルバッファはネットワークカードに対応します

通常のファイルの場合、ユーザーがファイル記述子を介してファイル バッファーにデータを書き込み、そのデータをディスクにフラッシュすると、データの書き込み操作が完了します。ソケット関数によって開かれた「ネットワーク ファイル」の場合、ユーザーがファイル バッファにデータを書き込むと、オペレーティング システムは定期的にデータをネットワーク カードにフラッシュし、ネットワーク カードはデータの送信を担当し、データは最終的にネットワークに送信されます

コード

ソケットを作成するためにサーバーを初期化するときは、socket 関数を呼び出してソケットを作成することです. ソケットを作成するときに入力する必要があるプロトコル ファミリは、ネットワーク通信が実行されるため、AF_INET であり、必要なサービスタイプはSOCK_DGARMです。これは、UDPサーバーが書き込まれるため、データグラム指向であり、3番目のパラメーターを0に設定できるためです。

//UdpServer.h
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>

using std::cout;
using std::cerr;
using std::endl;

class UdpServer
{
public:
    bool InitServer();
    ~UdpServer();
private:
    int _socket_fd;
};
//UdpServer.cc
#include "UdpServer.h"

bool UdpServer::InitServer() {
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success" << endl;
    return true;
}

UdpServer::~UdpServer() {
    if(_socket_fd > 0) close(_socket_fd);
}

サーバーが破壊された場合、_socket_fd に対応するファイルを閉じることができますが、実際には、一般的なサーバーは実行後に停止しないため、この操作を行う必要はありません。

#include "UdpServer.h"

int main() 
{
    UdpServer* server = new UdpServer;
    server->InitServer();
    return 0;
}

プログラム実行後、正常にソケットが作成され、デフォルトで標準入力ストリーム、標準出力ストリーム、標準エラーストリームが 0、1、2 を占有しているため、取得したファイル記述子は 3 です。最小かつ未使用のファイル記述子は 3 です

3.1.2 サーバーバインディング

ソケットは正常に作成されましたが、サーバーとして、ソケットが作成されただけの場合、それはシステム レベルで開かれたファイルにすぎず、オペレーティング システムは将来ディスクにデータを書き込むことを認識しません。まだネットワーク カードに点滅しており、現時点ではファイルはネットワークに関連付けられていません。したがって、サーバーを初期化するために行う 2 番目のことは、バインドすることです

バインド機能

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:バインドされたファイルのファイル記述子。つまり、ソケットを作成したときに取得したファイル記述子です
  • addr:プロトコル ファミリ、IP アドレス、ポート番号などのネットワーク関連の属性情報。
  • addrlen:着信 addr 構造体の長さ

戻り値:バインド成功なら0、失敗なら-1を返し、同時にエラーコードをセットする

struct sockaddr_in 構造体

バインドするときは、ネットワーク関連の属性情報を構造体に入力し、その構造体を bind 関数の 2 番目のパラメーター (struct sockaddr_in 構造体) として渡す必要があります。

struct sockaddr_in の定義は /usr/include/linux/in.h にあります。

  • sin_family: プロトコル ファミリを示します
  • sin_port: ポート番号を示します。これは 16 ビットの整数です。
  • sin_addr: 32 ビット整数である IP アドレスを示します

残りのフィールドは通常処理されません。もちろん、初期化することもできます

sin_addr の型は struct in_addr です. 実際, 構造体には 32 ビット整数であるメンバが 1 つだけあります. IP アドレスは実際にはこの整数に格納されます.

バインディングを理解するには?

バインド時には、対応するネットワークファイルにIPアドレスとポート番号を伝える必要があります.このとき、ネットワークファイル内のファイル操作関数のポイントを変更し、対応する操作関数を対応するネットワークの操作方法に変更することができます.データの書き込みに対応する操作対象はネットワークカードなので、実際のバインディングはファイルをネットワークに関連付けることです。

コード

バインドには IP アドレスとポート番号が必要なため、サーバー クラスに IP アドレスとポート番号を導入し、サーバー オブジェクトの作成時に対応する IP アドレスとポート番号を渡す必要があります。初期化する番号

class UdpServer
{
public:
    UdpServer(uint16_t port,string ip):_socket_fd(-1),_port(port),_ip(ip) {}
    bool InitServer();
    ~UdpServer();
private:
    int _socket_fd;
    uint16_t _port;
    string _ip;
};

ソケットを作成したらバインドする必要がありますが、バインドする前に struct sockaddr_in 構造体変数を定義し、対応するネットワーク属性情報を構造体に入力する必要があります。構造体にはオプションのフィールドがいくつかあるため、入力する前に構造体変数の内容をクリアしてから、プロトコル ファミリ、ポート番号、IP アドレス、およびその他の情報を構造体変数に入力することをお勧めします。

ネットワークに送信する前に、ポート番号をネットワーク シーケンスとして設定する必要があります.ポート番号は 16 ビットであるため、htons()関数を使用してポート番号をネットワーク シーケンスに変換する必要があります. また、整数 IP はネットワークで送信されるため、inet_addr() 関数を呼び出して文字列 IP を整数 IP に変換し (同時にネットワーク シーケンスに変換し)、変換された整数 IP を設定する必要があります。

ネットワーク属性情報を埋めた後、バインド関数は一般的なパラメータ型を提供するため、構造体アドレスを渡す前に struct sockaddr_in* を struct sockaddr* 型に変換する必要もあります。

bool UdpServer::InitServer() {
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success , fd:" << _socket_fd << endl;

    //填充网络通信相关信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = inet_addr(_ip.c_str());
    //绑定
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        cerr << "bind fail" << endl;
        return false;
    }
    cout << "bind success" << endl;

    return true;
}

3.1.3 文字列 IP VS 整数 IP

ネットワークを介してデータを送信する場合、土地は非常に高価です. IPアドレスがネットワーク送信中に文字列ベースのドット付き10進IPの形式で直接送信される場合、IPアドレスはこの時点で15バイト必要ですが、実際にはそうではありません.非常に多くのバイトが必要です

IP アドレスは実際には 4 つの領域に分けることができ、各領域の値は 0 ~ 255 であり、この範囲の数値は 8 ビットで表すだけでよいため、IP アドレスを表すのに必要なのは 32 ビットだけです。32 ビット整数の各バイトは IP アドレスの特定の領域に対応し、この IP アドレスの表現は整数 IP と呼ばれます。

整数 IP を使用して IP アドレスを表すスキームは 4 バイトしか必要とせず、同じ意味を表すこともできます. したがって、ネットワーク通信では、文字列 IP は使用されず、整数 IP が使用されます。ネットワーク通信のデータ。

文字列 IP と整数 IP の間の変換

変換方法はいろいろあります. 例えば, ビットセグメントAを定義することができます. ビットセグメントAには4つのメンバがあり, 各メンバのサイズは8ビットです. 32ビット

次に、2 つのメンバーを持つコンソーシアム IP を定義します。1 つは整数 IP を表す 32 ビット整数で、もう 1 つは文字列 IP を表すビット セグメント A タイプのメンバーです。

ユニオンのスペースはメンバー共有なので、IPの設定と読み込みの方法は以下の通りです。

  • 整数 IP の形式で IP を設定する場合は、ユニオンの最初のメンバーに直接割り当てることができます。
  • IP を文字列 IP の形式で設定する場合は、まず文字列を対応する 4 つの部分に分割し、次に各部分を対応するバイナリ シーケンスに変換して、p1、p2、p3、および P4 に設定します。
  • 整数の IP を取得する場合は、共用体の最初のメンバーを直接読み取ることができます
  • 文字列 IP を取り出したい場合は、コンソーシアムの 2 番目のメンバーの p1、p2、p3、p4 を順番に取得し、それぞれの部分を文字列に変換してつなぎ合わせます

注:ビット セグメントと列挙は、文字列 IP と整数 IP の間の変換を完了するために、オペレーティング システム内で実際に使用されます。

inet_addr 関数

文字列 IP を整数 IP に変換し、それをネットワーク シーケンスに変換します

in_addr_t inet_addr(const char *cp);

inet_ntoa 関数

整数 IP を文字列 IP に変換し、ホスト シーケンスに変換します

char *inet_ntoa(struct in_addr in);

inet_ntoa 関数に渡されるパラメーターの型は in_addr であるため、パラメーターを渡すときに in_addr 構造体で 32 ビット メンバーを選択して渡す必要はなく、in_addr 構造体に直接渡すだけです。

3.2 サーバーの起動と実行

UDP サーバーの初期化は、ソケットを作成してバインドするだけで済み、サーバーが初期化されると、サーバーを起動できます。

サーバーは実際に何らかのサービスを何度も提供しています. サーバーがサーバーと呼ばれる理由は、サーバーは実行後に終了することはなく、サーバーが実際に実行するのは無限ループコードです. UDP サーバーは接続指向ではないため、UDP サーバーが起動している限り、クライアントから送信されたデータを直接読み取ることができます。

recvfrom 関数

データを読む

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
  • sockfd: 操作に対応するファイル記述子。このファイル記述子によってインデックス付けされたファイルからデータを読み取ることを示します
  • buf: 読み込んだデータの保存場所
  • len: 読み取られると予想されるデータのバイト数
  • flags: 読み方。通常は 0 に設定されます。これは読み取りをブロックすることを意味します
  • src_addr: プロトコル ファミリ、IP アドレス、ポート番号など、ピア ネットワークに関する属性情報 (出力パラメーター。nullptr または NULL には設定できません)
  • addrlen: 呼び出し時に読み込まれると予想される src_addr 構造体の長さが渡され、実際に読み込まれた src_addr 構造体の長さが返されるときに表されます (入力および出力パラメーター)

戻り値:読み込みに成功した場合は実際に読み込んだバイト数を返し、読み込みに失敗した場合は-1を返し、同時にエラーコードをセットする

  • UDPはコネクション型ではないため、データの取得だけでなく、IPアドレスやポート番号など、ピアネットワークに関する属性情報も取得する必要があります。
  • recvfrom を呼び出してデータを読み取る場合、addrlen は、読み取る構造体に対応するサイズに設定する必要があります。
  • recvfrom 関数が提供するパラメータも struct sockaddr* 型であるため、構造体アドレスを渡す場合は struct sockaddr_in* 型を強制的に転送する必要があります。

サーバーを起動するためのインターフェースを提供します

サーバーは recvfrom 関数を介してクライアント データを読み取ります. 読み取ったデータは最初に文字列とみなすことができ, 読み取ったデータの最後の位置は '\0' に設定されます. このとき, 読み取ったデータを読み取ることができます.が出力され、取得したクライアントのIPアドレスとポート番号も一緒に出力できます。

取得したクライアントのポート番号は、この時点でネットワーク シーケンスであり、ntohs 関数を呼び出してホスト シーケンスに変換し、出力する必要があります。取得したクライアントの IP アドレスは整数の IP であり、これを inet_ntoa 関数を呼び出して文字列 IP (ホスト シーケンスに変換) に変換し、出力する必要があります。

void UdpServer::Start()
{
    char buffer[SIZE];
    while(true) 
    {
        struct sockaddr_in ping;
        socklen_t length =  sizeof(ping);
        ssize_t size = recvfrom(_socket_fd, buffer, SIZE - 1, 0, (struct sockaddr*)&ping, &length);
        if(size > 0) {
            buffer[size] = '\0';
            uint16_t port = ntohs(ping.sin_port);
            string ip = inet_ntoa(ping.sin_addr);
            cout << "[" << ip << ":" << port << "]#" << buffer << endl;
        }
        else {
            cerr << "recvfrom fail" << endl;
        }
    } 
}

注: recvfrom 関数の呼び出しがデータの読み取りに失敗した場合は、プロンプト メッセージを出力できますが、サーバーを終了させないでください. 特定のクライアントからのデータの読み取りに失敗したため、サーバーは終了できません.

コマンド ライン パラメーターの導入

サーバーの構築時に IP アドレスとポート番号を渡す必要があるため、コマンド ライン パラメーターを導入できます。このとき、サーバーを実行するときは、対応する IP アドレスとポート番号に従ってください。

現在、IP アドレス 127.0.0.1 を使用しています。127.0.0.1というIPアドレスは、ローカルホストを示すローカルホストに相当し、ローカルループバックと呼ばれ、データはローカルのプロトコルスタックのみを流れ、ネットワークを通過しません。ローカルで正常に通信できるかテストしてから、ネットワーク通信テストを行う

int main(int argc, char* argv[]) 
{
    if(argc < 3) {
        cerr << "Usage " << argv[0] << " ip port" << endl;
        return 1;
    }
    UdpServer* server = new UdpServer(string(argv[1]),atoi(argv[2]));
    server->InitServer();
    server->Start();
    return 0;
}

agrv 配列には文字ポインタが格納され、ポート番号は整数です.atoi 関数を使用して文字列を整数に変換する必要があります. 次に、この IP アドレスとポート番号を使用してサーバーを構築し、サーバーの構築と初期化が完了したら、Start 関数を呼び出してサーバーを起動します。

クライアント コードはまだ作成されていません。netstat コマンドを使用して現在のネットワーク ステータスを表示できます。ここで nlup オプションを選択できます。

  • -n: ドメインネームサーバーを介さずに直接IPアドレスを使用
  • -l: 監視しているサーバーのソケットを表示します
  • -t: TCP トランスポート プロトコルの接続状態を表示します。
  • -u: UDP トランスポート プロトコルの接続状態を表示します
  • -p:Socketを使用しているプログラム識別コードとプログラム名を表示

n オプションを削除すると、最初に IP アドレスが表示された場所が対応するドメイン ネーム サーバーになります。

Protoはプロトコルの種類、Recv-Qはネットワーク受信キュー、Send-Qはネットワーク送信キュー、Local Addressはローカル アドレス、Foreign Addressは外部アドレス、Stateは現在の状態、PIDはプロセス ID を示します。プロセスのプログラム名プロセスのプログラム名を示します

その中で、Foreign Address は 0.0.0.0 と書かれています:*。これは、任意の IP アドレスと任意のポート番号を持つすべてのプログラムが現在のプロセスにアクセスできることを意味します。

3.3 クライアントの初期化

3.3.1 クライアントがソケットを作成する

クライアントをクラスにカプセル化します。クライアント オブジェクトを定義するときは、それも初期化する必要があり、クライアントは初期化時にソケットも作成する必要があります。その後、クライアントはこのソケットにデータを送信または受信します。操作する

ソケットの作成時にクライアントが選択したプロトコル ファミリは AF_INET であり、必要なサービス タイプは SOCK_DGARM です. クライアントが破棄されると、対応するソケットを閉じることを選択できます. サーバーとは異なり、クライアントはバインディングなしで初期化中にソケットを作成するだけで済みます

class UdpClient
{
public:
    UdpClient(string server_ip,uint16_t server_port):_server_ip(server_ip),_server_port(server_port) {}
    bool InitClient();
    ~UdpClient() {}
private:
    int _socket_fd;
    string _server_ip;
    uint16_t _server_port;
};


bool UdpClient::InitClient() 
{
	_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (_socket_fd < 0){
		std::cerr << "socket create error" << std::endl;
		return false;
	}
	return true;
}

UdpClient::~UdpClient() { if(_socket_fd < 0) close(_socket_fd); }

3.3.2 クライアントバインディングの問題

ネットワーク通信であるため、双方がお互いを見つける必要があるため、サーバーとクライアントの両方が独自の IP アドレスとポート番号を持っている必要がありますが、サーバーはポート番号をバインドする必要がありますが、クライアントはバインドしていません。

サーバーは他人にサービスを提供するため、サーバーは自分のIPアドレスとポート番号を他人に知らせる必要があります.IPアドレスは一般的にドメイン名に対応し、ポート番号は一般的に示されていないため、サーバーのポート番号は既知のポート番号であり、選択後に簡単に変更することはできません. そうしないと、クライアントはサーバーのポート番号を知ることができません. そのため、サーバーはバインドする必要があります. バインド後にのみ、ポート番号は実際にはそれ自体に属します. Aポートは1つのプロセスによってのみバインドでき、サーバーはポートをバインドしてポートを独占するため

クライアントも通信時にポート番号を必要としますが、クライアントは一般にバインドされていません. クライアントがサーバーにアクセスする場合、ポート番号は一意である必要があり、特定のクライアントプロセスに強く関連している必要はありません.

クライアントが特定のポート番号にバインドされている場合、このポート番号は将来、このクライアントのみが使用できます。このクライアントが起動していなくても、このポート番号を他のユーザーに割り当てることはできません。また、このポート番号が使用されている場合他の人によって、クライアントを起動できません。したがって、クライアントのポートは一意である必要があります。したがって、クライアント ポートは動的に設定でき、プログラマがクライアントのポート番号を設定する必要はありません. sendto() などのインターフェイスを呼び出すと、オペレーティング システムは現在のクライアントの一意のポート番号を自動的に取得します。 .

クライアントが使用するポート番号は、起動するたびに変わる場合がありますが、このとき、ポート番号が枯渇しない限り、クライアントは正常に起動できます。

3.4 クライアントの起動と実行

クライアントが初期化された後, クライアントを実行することができます. クライアントとサーバーは機能的に補完的であるため, サーバーはクライアントから送信されたデータを読み取っているため, クライアントはデータをサーバーに送信する必要があります.

sendto 関数

データを送る

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
  • sockfd: 操作に対応するファイル記述子。このファイル記述子によって索引付けされたファイルにデータを書き込むことを示します
  • buf: 書き込むデータの格納場所
  • len: データを書き込むと予想されるバイト数
  • flags: 書き方。通常は 0 に設定します。これは、書き込みをブロックすることを意味します
  • dest_addr: プロトコル ファミリ、IP アドレス、ポート番号など、ピア ネットワークに関連する属性情報。
  • addrlen: 着信 dest_addr 構造体の長さ

戻り値:書き込み成功時は実際に書き込んだバイト数、書き込み失敗時は-1を返し、同時にエラーコードをセットする

  • UDP はコネクション型ではないため、送信するデータを渡すだけでなく、IP アドレスやポート番号など、ピア ネットワークに関する情報を指定する必要があります。
  • sendto() で提供されるパラメータは struct sockaddr* 型であるため、パラメータを渡す際に struct sockaddr* の型を強制的に変換する必要があります。

クライアントを起動するためのインターフェイスを提供します

クライアントがサーバーにデータを送信したい場合、クライアントはユーザー入力を取得し、ユーザーが入力したデータを継続的にサーバーに送信できます。

クライアントに格納されているサーバーのポート番号は、この時点でホスト シーケンスです。htons() 関数を呼び出してネットワーク シーケンスに変換し、それを struct sockaddr_in 構造体に設定する必要があります。クライアントに格納されているサーバーの IP アドレスは文字列 IP であり、 inet_addr() 関数を呼び出して整数 IP に変換 (およびネットワーク シーケンスに変換) し、次に struct sockaddr_in 構造に設定する必要があります。

void UdpClient::Start()
{
    string message;
    struct sockaddr_in receive;
    memset(&receive, 0, sizeof(receive));
    receive.sin_port = htons(_server_port);
    receive.sin_family = AF_INET;
    receive.sin_addr.s_addr = inet_addr(_server_ip.c_str());

    while(true)
    {
        cout << "please Enter#";
        getline(cin, message);
        sendto(_socket_fd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr*)&receive, sizeof(receive));
    }
}

コマンド ライン パラメーターの導入

コマンド ライン パラメーターを導入し、クライアントの実行時に対応するサーバーの IP アドレスとポート番号を直接たどります。

int main(int argc, char* argv[])
{
    if(argc < 3) {
        cerr << "Usage " << argv[0] << " ip port " << endl;
        return 1;
    }
    string serve_ip = argv[1];
    uint16_t serve_port = atoi(argv[2]);
    UdpClient* client = new UdpClient(serve_ip, serve_port);
    client->InitClient();
    client->Start();
    return 0;
}

3.5 ローカルテスト

サーバーとクライアントのコードが作成され、最初にローカル テストを実行できます.この時点で、サーバーは外部ネットワークにバインドされていませんが、ローカル ループバックにバインドされています。ここで、サーバーを実行するときにポート番号を 8080 に指定してから、クライアントを実行します.このとき、クライアントがアクセスするサーバーの IP アドレスはローカル ループバックであり、サーバーのポート番号は 8081 です.

netstat コマンドを使用してネットワーク情報を表示すると、サーバーのポートが 8081、クライアントのポートが 36577 であり、クライアントが動的にバインドされていることがわかります。

3.6 INADDR_ANY

ネットワーク テストを実行し、サーバーをパブリック IP に直接バインドすると、外部ネットワークからサーバーにアクセスできるようになります。

サーバーに設定されているローカル ループバックをブロガーのパブリック IP に変更します。このとき、サーバーを再実行すると、サーバー バインディングが失敗することがわかります。

クラウド サーバーの IP アドレスは対応するクラウド ベンダーによって提供されるため、この IP アドレスは必ずしも実際のパブリック ネットワーク IP であるとは限りません.この IP アドレスは直接バインドできません.外部ネットワーク アクセスを許可する必要がある場合は、0 をバインドする必要があります. システムによって提供される INADDR_ANY (マクロ値)、対応する値は 0

外部ネットワークからのアクセスを許可する必要がある場合は、バインド時に INADDR_ANY をバインドして、外部ネットワークからサーバーにアクセスできるようにする必要があります。

INADDR_ANY をバインドする利点

サーバーの帯域幅が十分に大きい場合、データを受信するマシンの能力がこのマシンの IO 効率を制限するため、サーバーには最下層に複数のネットワーク カードがインストールされている可能性があり、このサーバーは複数の IP アドレスを持つ可能性がありますが、サーバーには、ポート番号 8081 を持つサービスが 1 つだけあります。このサーバーがデータを受信して​​いる時、実際には複数のネットワークカードが最下層でデータを受信して​​おり、これらのデータもポート番号 8081 でサービスにアクセスしたいのかもしれません。

このとき、サーバーがバインド時にバインド IP アドレスを指定すると、サーバーはデータ受信時にバインド IP に対応するネットワーク カードからのみデータを受信できます。サーバーが INADDR_ANY にバインドされている場合、データがポート番号 8081 でサービスに送信される限り、システムはデータをサーバーに下から上に渡すことができます。

したがって、サーバー側で INADDR_ANY をバインドするスキームが強く推奨され、すべてのサーバーも運用中にこのスキームを採用します。

外部ネットワークからサーバーにアクセスしたいが、特定の IP アドレスをポイントしたい場合、クラウド サーバーを使用することはできません. 現時点では、仮想マシンまたはカスタム インストールされた Linux オペレーティング システムを使用することを選択できます。 IP アドレスはカスタム バインドをサポートしていますが、クラウド サーバーはサポートしていません。

コードを変更

bool UdpServer::InitServer() {
    //创建套接字
    _socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_socket_fd < 0) {
        cerr << "socket fail" << endl;
        return false;
    }
    cout << "socket create success , fd:" << _socket_fd << endl;

    //填充网络通信相关信息
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;
    //绑定
    if(bind(_socket_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        cerr << "bind fail" << endl;
        return false;
    }
    cout << "bind success" << endl;

    return true;
}

この時点で、サーバーを再コンパイルして実行すると、バインディングは失敗しません。このとき、netstat コマンドを使用して表示すると、サーバーのローカル IP アドレスが 0.0.0.0 になっていることがわかります。これは、UDP サーバーをローカルで読み取ることができることを意味します。任意のネットワーク カードからデータを取得します。

3.7 エコー機能

ネットワークテスト中にクライアントがサーバーにデータを送信すると、サーバーはクライアントから受信したデータを印刷するため、サーバーは現象を確認できます。しかし、クライアントはサーバーにデータを送信しており、クライアントは送信したデータをサーバーが受信したかどうかを確認できません。

サーバーコードの書き方

このサーバーは、エコー サーバーに変更できます。サーバーがクライアントから送信されたデータを受信すると、サーバーでの印刷に加えて、サーバーはsendto関数を呼び出して、受信したデータを対応するクライアントに再送信できます

サーバーは sendto 関数を呼び出すときにクライアントのネットワーク属性情報を渡す必要がありますが、サーバーはクライアントのネットワーク属性情報を知っています。

void UdpServer::Start()
{
    char buffer[SIZE];
    while(true) 
    {
        struct sockaddr_in ping;
        socklen_t length =  sizeof(ping);
        ssize_t size = recvfrom(_socket_fd, buffer, SIZE - 1, 0, (struct sockaddr*)&ping, &length);
        if(size > 0) {
            buffer[size] = '\0';
            uint16_t port = ntohs(ping.sin_port);
            string ip = inet_ntoa(ping.sin_addr);
            cout << "[" << ip << ":" << port << "]#" << buffer << endl;
        }
        else {
            cerr << "recvfrom fail" << endl;
        }

        string echo_message = "server echo:";
		echo_message += buffer;
		sendto(_socket_fd, echo_message.c_str(), echo_message.size(), 0, (struct sockaddr*)&ping, length);
    } 
}

クライアントコードの書き方

クライアントがサーバーにデータを送信した後、サーバーはクライアントにデータを再送信するため、クライアントはサーバーから送信された応答データを読み取るために recvfrom を呼び出す必要があり、クライアントはサーバーから応答データを受信した後、データをそのまま印刷するだけです。

このとき、クライアントからサーバーに送信されたデータは、サーバー上で印刷されて表示されるだけでなく、サーバーもクライアントにデータを再送信します.このとき、クライアントは応答データも受信し、データを印刷する

void UdpClient::Start()
{
    string message;
    struct sockaddr_in receive;
    memset(&receive, 0, sizeof(receive));
    receive.sin_port = htons(_server_port);
    receive.sin_family = AF_INET;
    receive.sin_addr.s_addr = inet_addr(_server_ip.c_str());

    while(true)
    {
        cout << "please Enter#";
        getline(cin, message);
        sendto(_socket_fd, message.c_str(), strlen(message.c_str()), 0, (struct sockaddr*)&receive, sizeof(receive));

        char buffer[SIZE];
		struct sockaddr_in tmp;
		socklen_t length = sizeof(tmp);
		ssize_t size = recvfrom(_socket_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&tmp, &length);
		if (size > 0) {
			buffer[size] = '\0';
			cout << buffer << endl;
		}
    }
}

 

3.8 ネットワークテスト

この時点で、sz コマンドを使用してクライアント実行可能プログラムをローカル マシンにダウンロードし、そのプログラムを友人に送信できます。

友人がクライアントの実行可能プログラムを受け取った後、rz コマンドまたはドラッグ アンド ドロップを使用して実行可能プログラムをクラウド サーバーにアップロードし、chmod コマンドを使用してファイルに実行権限を追加します。

最初にサーバーを起動します。次に、コマンド ライン パラメーターとして IP アドレスとポート番号を指定してクライアントを実行することで、友人がサーバーにアクセスできます。

おすすめ

転載: blog.csdn.net/GG_Bruse/article/details/129760960