Linuxソケットプログラミング:ネットワークプログラミングの基礎

Linuxでは、非常に人気のある言葉があります。Linuxのすべてがファイルです実際、Linuxでは、さまざまなデバイスをファイルによって操作できます周辺デバイスのファイルは、通常、デバイスファイルと呼ばれますLinuxのネットワーク通信は、ネットワークファイル記述子を操作することによっても実現されます。
前回のブログでは、「トランスポート層の概要」たちで知っている:間インターネットデバイスや機器は、両側のIPアドレスとポート番号を知っている必要があります。IPアドレスは通信ホストを見つけることができ、ポート番号は実際の通信プロセスを示しますこれはしばしばソケット通信と呼ばれます、socket = <IP>:<port>。Linuxでは、ネットワーク通信にもこの情報が必要ですこれは、Linuxシステムではソケットアドレス構造と呼ばれますLinuxでは、ソケットアドレス構造は通常sockaddrで始まります。次に、一般的に使用されるいくつかのソケットアドレス構造を紹介します。

struct   sockaddr {                                 
	sa_family_t   		sa_family;/*网络通信协议的域,和socket的第一个参数一致;常用PF_INET(AF_INET)*/
	char          		sa_data[14];
}
 struct sockaddr_in   {                                   
	u8        				sin_len;/*固定长度16*/
	u8						sin_family;	/*协议的domain*/
	u16    					sin_port;/*通信使用的端口号*/
	struct in_addr 		 	sin_addr;/*通信使用的IP地址*/
	char 					sin_zero[8];/*保留字段,为0*/
}
struct sockaddr_un {
	sa_family_t 		sun_family;/*domain*/
	char  				sun_path[UNIX_PATH_MAX];/*108的长度,保存路径*/
}

typedef unsigned short sa_family_t;
struct in_addr{
	u32 s_addr;
}

それらの中で、struct sockaddrは一般的なソケットアドレス構造であり、異なるプロトコルファミリ間での変換を強制できます。struct sockaddrとstruct sockaddr_inのサイズは同じで、次の図は対応する関係を示しています。struct sockaddr_inはイーサネットアドレス構造で、struct sockaddr_unはUnixドメインのプロトコルファミリーアドレス構造です。主に同じホスト上のプロセス間の通信を使用し、その速度はイーサネット構造を使用した通信の2倍になります。 。
ここに画像の説明を挿入

WebフレームワークAPIの構築

ここでは主にネットワークフレームワークの構築における各機能の機能を紹介し、特定のテストコードは投稿されていません。

ネットワークソケット関数ソケットを作成します()

Linuxではすべてがファイルです。つまり、ファイルを読み書きしてデバイスを操作できます。同じことがネットワーク通信に当てはまり、socket()関数はカーネル内にソケット記述を作成し、それをファイル記述子に関連付けますこのファイル記述子のその後の操作で、このネットワークインターフェイスソケットを制御できます。成功したsocket()関数は、ソケットファイル記述子を返します。

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

ソケットプロトタイプは上記のとおりです。domainはネットワーク通信のドメインを設定するために使用され、関数socketはこのパラメーターに基づいて通信プロトコルのファミリーを選択します。イーサネットはPF_INETフィールドを使用する必要があります(AF_INETとその値は同じです)。ドメインの値を以下に示します。赤いマークが一般的に使用されます。
ここに画像の説明を挿入

  • PF_UNIX:主にUNIXドメイン通信、つまりローカル通信に使用されます。Unix通信を使用する場合、速度は他のAPIの2倍になります。
  • PF_INET:IPV4通信。このドメインがほとんどの場合に使用されます。
  • PF_NETLINK:主にnetlink通信、つまりユーザー空間とカーネル間の通信に使用されます。
  • PF_PACKET:主にネットワークカードMACのデータに直接アクセスするために使用されます。つまり、ネットワークカード上のフレームを直接操作します。
    タイプは、共通制御伝送プロトコルTCPタイプSOCK_STREAM、ユーザーデータパケットプロトコルUDPタイプSOCK_DGRAM、元のソケットタイプSOCK_RAMなど、ソケット通信のタイプ(プロトコル)を設定するために使用されます。次の図はオプションの値です
    ここに画像の説明を挿入
    注:すべてのプロトコルファミリードメインがこれらのプロトコルタイプを実装しているわけではありません。
    ソケット関数の3番目のパラメーターprotocolは、プロトコルの特定のタイプ、つまりタイプの拡張を指定するために使用されます。多くのプロトコルには、特定のタイプが1つしかないことがよくあるため、現時点では0にしか設定できません。ただし、SOCK_RAMやSOCK_PACKETなどのプロトコルは、特定のタイプのプロトコルを選択するためにこのパラメーターを設定する必要があります。
    TCPソケットを作成する場合はソケット(PF_INET、SOCK_STREAM、0)を使用し、UDPを作成する場合はソケット(PF_INET、SOCK_DGRAM、0)を使用します。ソケットを作成するプロセスは次のとおりです。
    ここに画像の説明を挿入
    ユーザー空間がソケットを呼び出すと、カーネルでsys_socket()が呼び出されます。その主な目的は、カーネルソケット構造を作成し(アプリケーションレイヤーと一致しない)、キュー(受信、送信、例外)などのリソースを割り当て、パラメーターに従ってopsおよびtypeにコピーすることです。同時に、カーネルソケットとファイル記述子もバインドされます。最後に、ファイル記述子がアプリケーション層に返されます。このように、ファイル記述子を介して対応するカーネルソケット構造を見つけることができます。つまり、ファイルを操作することで、ネットワーク通信の操作を実現できます。
    ここに画像の説明を挿入
    注:ソケットファイル記述子の形式は一般的なファイル記述子と同じです。ファイル記述子がソケット記述子であるかどうかを確認するには、関数fstat()を呼び出してファイル記述子のモードを取得し、モードを変更します。 S_IFMT部分は、識別子S_IFSOCKと比較されます。このようにして、ファイル記述子がソケット記述子であるかどうかを確認できます。次のコードを使用して実現できます。
int issockettype(int fd)
{
	struct stat st;
	int err = fstat(fd,&st);
	if(err<0){return -1;}
	
	if((st.st_mode & S_IFMF) == S_IFSOCK){
		return 1;
	}else{
		return 0;
	}
}

bind()はアドレス構造をバインドするために使用されます

ソケットが正常に確立された後、ファイル記述子を通じてカーネルソケットを見つけることができ、プロトコルパラメータと対応する操作関数を取得できます。ただし、現時点では、ソケットファイル記述子はネットワーク内のIPおよびポート番号とは関係ありません。bind()を使用して、ファイル記述子をネットワークアドレス構造にバインドできます。バインド後、ソケットファイル記述子は、IP、ポート、およびネットワークアドレス構造のタイプに関連付けられます。bind関数は、サーバー側のサーバーネットワークインターフェースをバインドするためにのみ使用され、他の場所では使用できません。関数プロトタイプは次のとおりです

int bind(int sockfd, struct sockaddr*my_addr,socket_len addrlen)

ここに画像の説明を挿入
bind()関数は主にネットワークファイル記述子とネットワークアドレス構造をバインドするために使用されるため、sockfdを使用してネットワークのステータスを監視できますサーバーがオンになってからサーバーがサービスプログラムを実行していて、クライアント接続と通信(パッシブ側の処理)がいつ行われるかわからないため、バインドはサーバー側でよく使用されます。したがって、TCPでバインドした後、accept()関数を使用して接続を確立し、UDPで外部データを受信するためにrecvfrom()関数を使用します。バインドネットワークアドレスがない場合、サーバーはIPおよびポートの動きを監視することを認識しません。したがって、通信は不可能です。

listen()、accept()ローカルポートを監視(TCP通信で使用)

C / Sアーキテクチャでは、サーバー上のサービスプログラムが実行された後、クライアントから送信された接続確立要求を常に監視する必要があります。要求を受信した後、2つのパーティは接続を確立した後でのみ通信できます。サーバー上のリソースが限られているため、サーバーは一度に1つのクライアント接続しか処理できません。複数のクライアント接続要求が同時に到着すると、サーバーはそれらを同時に処理できないため、処理できないクライアント接続要求を待機キューに入れる必要があります。 。キューの長さはlisten()関数によって指定されますが、オペレーティングシステムとハードウェアリソースの影響により、キューの長さを無限にすることはできません。最大長を超えると、カーネルは最大長を使用します。キューの長さがいっぱいになると、クライアントからの要求は失われます。クライアントのconnect()関数はECONNREFUSEDエラーを返しますリッスンプロセスと上記のbind()プロセスタイプは、最初にsocketfd記述子に従って対応するカーネルソケットを検索し、次にsocket-> opsでリッスン関数を呼び出します。
accept()関数は、接続の確立後にクライアント情報を記録するための新しいソケットを作成します。関数が成功すると、クライアントのファイル記述子が返され、クライアントのIP、ポート、タイプなどの情報がパラメーターを介して取得できます。クライアントと通信するときは、新しく接続されたクライアントソケット記述子を使用する必要があります。プロセスは次のとおりです。
ここに画像の説明を挿入

ターゲットネットワークに接続する()関数

クライアントがソケットを確立した後は、アドレスバインディングなどの操作を実行する必要はありません。サーバーへの接続確立要求を直接開始できます。サーバーに接続するための関数はconnectです。サーバーとの接続を確立するには、サーバーのアドレス構造とクライアントのファイル記述子を指定する必要があります。接続が正常に確立されたら、このソケット記述子を使用してサーバーと通信できます。サーバー上の接続確立要求キューがいっぱいになると、この関数はECONNREFUSEDタイプのエラーコードを返します関数の呼び出しプロセスは次のとおりです。
ここに画像の説明を挿入
上記から、後続の一連の関数(bind、listen、accept、connectなど)はすべてsocket()によって作成されたファイル記述子sockfdに依存しており、対応するカーネルソケットはファイル記述子を介して見つけることができます。そして、カーネル内のソケットのops(操作関数)は、socket()パラメーターに従って特定のプロトコル操作メソッドを指定します。

関数を閉じる

通信が終了したら、close()関数を使用してsockfdを閉じることができます。close()は、カーネル内の対応するsockリソースとファイル記述子を解放します。ネットワーク通信では、シャットダウン関数を使用して通信を閉じることもできます。これは、SHUT_RD(読み取りのカットオフ)、SHUT_WR(書き込みのカットオフ)、SHUT_RDWR(読み取りと書き込みが閉じられ、それに相当するもの)です。

ネットワークバイトオーダーとIPアドレス変換処理

ネットワークによって送信されたデータとローカルデータとの間にバイトオーダー対応の問題がある可能性があるため、バイトオーダーの問題はネットワークプログラミングで処理する必要があります。以下では、エンディアンに関連するいくつかの機能と、エンディアンの簡単な紹介を紹介します。
バイト順は、マルチバイト変数の CPUとOSのメモリストレージ順序が異なるために生成され、ビッグエンディアンとリトルエンディアンのストレージに分けられます。リトルエンディアンバイトオーダーは変数を表すメモリアドレスの開始アドレスに下位バイトを格納し、ビッグエンディアンバイトオーダーは変数を表すメモリアドレスの開始アドレスに上位バイトを格納します。
ホストには大きな違いがあるため、ホストのバイトオーダーは統一できませんが、ネットワーク上で送信される変数の場合、それらの値は統一された表現でなければなりません。ネットワークバイトオーダーは、ネットワーク転送中のマルチバイト変数の表現方法を指し、ネットワークバイトオーダーはビッグエンディアンバイトオーダー表現方法を使用します。Linuxでの主な機能は次のとおりです。

       #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はlong型の変数を表し、sはshort型の変数を表しますプログラミング時には、バイトオーダー変換関数を呼び出して、ホストのバイトオーダーをネットワークのバイトオーダーに変換する必要があります。ネットワークのポートとIPを最初に置き換えてから、対応するアドレス構造に割り当てる必要があります。
コンピュータで認識できるのはバイナリデータのみであり、IPアドレスなどの32ビットデータはメモリと書き込みに特に不便です。メモリを簡単にするために、8ビットと8ビットのIPアドレスを分離します。これは、多くの場合、ドット付き10進数(192.168.1.123など)と呼ばれます。これは覚えやすいです。Linux には、文字列IPアドレスとバイナリアドレスの間で変換するための対応する関数もありますLinuxで一般的に使用される関数は次のとおりです。

       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>
       
       int inet_aton(const char *cp, struct in_addr *inp);
       in_addr_t inet_addr(const char *cp);
       in_addr_t inet_network(const char *cp);
       char *inet_ntoa(struct in_addr in);
       struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
       in_addr_t inet_lnaof(struct in_addr in);
       in_addr_t inet_netof(struct in_addr in);
       struct in_addr{ 
			unsigned long int s_addr;/*IP地址*/
		}
		 const char *inet_ntop(int af, const void *src,
                             char *dst, socklen_t size);
		int inet_pton(int af, const char *src, void *dst);
  • inet_aton()関数は、cpに格納されているドット付き10進文字列タイプのIPアドレスをバイナリIPアドレスに変換し、それをinpに保存します。
  • inet_addr()は、cpに格納されているドット付き10進文字列タイプのIPアドレスを、ネットワークバイトオーダーで表されるバイナリIPアドレスに変換します。
  • inet_network()は、cpに格納されているドット付き10進文字列タイプのIPアドレスをバイナリIPアドレスに変換し、IPアドレスはネットワークバイトオーダーで表されます。cpには、abcd形式、abc形式、またはab形式を使用できます。
  • inet_ntoa()関数は、inet_aton()変換の逆で、バイナリIPアドレスをドット付き10進4セグメント文字列IPアドレスに変換します。このメモリ領域は静的であるため、複数の変換で最後のデータが保存されます。
  • inet_makeaddr()関数は、ホストバイトオーダーネットワークアドレスとホストアドレスをネットワークバイトオーダーIPアドレスにマージします。
  • inet_lnaof()関数は、IPアドレスのホスト部分を返します。
  • inet_netof()関数は、IPアドレスのネットワーク部分を返します。

IPアドレスとドメイン名間の変換

実際の使用では、ホストのドメイン名がよく使用され、そのIPアドレスはほとんど使用されません。結局のところ、ドメイン名メモリはより便利です。www.baidu.comやwww.google.comなどのドメイン名は、ドット付き10進IPアドレスよりも覚えておくと便利です。ただし、ソケットプログラミングのAPIはIPアドレスに基づいているため、ホストドメイン名とIPアドレスの間で変換する必要があります。これはDNS(ドメインネームシステム)サービスで、ホストドメイン名とIPアドレス間の変換として機能します。
どちらのgethostbynameとはgethostbyaddr機能ホストのための情報を得ることができますgethostbyname関数はホストの名前でホスト情報を取得し、gethostbyaddr関数はIPアドレスでホスト情報を取得します。

       #include <netdb.h>
       extern int h_errno;

       struct hostent *gethostbyname(const char *name);
       #include <sys/socket.h>       /* for AF_INET */
       struct hostent *gethostbyaddr(const void *addr,
                                     socklen_t len, int type);

ここに画像の説明を挿入
どちらの関数もホストの一部の情報を返すことができ、その構造は上の図に示されています。h_nameは、www.baidu.comなどのホストの公式名です。ここで、h_lengthはIPアドレスの長さで、IPv4の場合は4、つまり4バイトです。h_addr_listに格納されているホストIPアドレスのリストの各長さこれはh_lengthであり、リストの最後はNULLポインターです。関係は次のとおりです。
ここに画像の説明を挿入

プロトコル名処理機能

Linuxは、操作を容易にするために、プロトコルの値と名前を照会するための一連の関数を提供しています。関連する機能、使用方法、注意事項を簡単に紹介します。Linuxでの操作機能は以下の通りであり、/ etc / protocolsのレコードを操作します

       #include <netdb.h>

       struct protoent *getprotoent(void);/*从协议文件中读取一行*/
       struct protoent *getprotobyname(const char *name);/*从协议文件中找到匹配项*/
       struct protoent *getprotobynumber(int proto);/*按照协议类型的值获取匹配项*/
       void setprotoent(int stayopen);/*设置协议文件打开状态*/
       void endprotoent(void);/*关闭协议文件*/

/ etc / protocolsファイルの内容は、プロトコルの名前、値、エイリアスを記録する次の図に示すとおりです。struct protoent構造は、この情報を記述するためにLinuxで定義されています。
ここに画像の説明を挿入
ここに画像の説明を挿入
p_nameはプロトコル名へのポインタ、p_aliasesはエイリアスリストへのポインタ、プロトコルエイリアスは文字列、p_protoはプロトコル値です。
ここに画像の説明を挿入
/ etc / protocolsファイルの情報を読み取る前に、事前にこのファイルを開き、setprotoent()関数を呼び出す必要があります。パラメーターが1の場合、常に開いています。操作が完了したら、endprotoent()を呼び出して閉じます。
getprotobyname()関数は、指定されたプロトコル名の情報を取得するために使用されます。これは、IP rawソケットデータにアクセスするときに使用されます。IPrawソケットを作成するには、プロトコルを指定する必要があるため、これらの値は記憶されないので、この関数は

TCP / UDP通信プロセス

ネットワークの通信モードには、主にC / S、B / S、P2Pお​​よびその他のモードが含まれ、その中でC / Sが主に使用されます。以下にC / S通信モードについて説明します。トランスポート層は、TCP通信とUDP通信の2つのタイプに分類できます。TCPおよびUDP通信フレームワークの構成を以下に説明します。

TCP通信

ここに画像の説明を挿入
TCP通信プロセスは比較的単純なので、ここでは紹介しません。

UDP通信

UDPはコネクションレスで信頼性の低い通信プロトコルであるため、そのプログラミングアーキテクチャはまだTCPとは大きく異なります。UDP通信は接続を確立する必要がないため、接続()、リッスン()、受け入れ()およびその他の機能は必要ありません。プログラミングプロセスは次のとおりです。
ここに画像の説明を挿入
パラメータとTCP通信が大きく異なる場合、UDPはソケットファイル記述子を作成し、使用される関数はsocket()です。typeはSOCK_DGRAMを使用します。リスニングポートのバインドはTCPと同じであり、アドレス構造のメンバーが入力されてバインドされます。sendto、recvfrom、その他の関数のインタラクティブな使用。UDP通信プロトコルには接続確立プロセスがないため、recvfromを使用すると、さまざまな送信者から送信されたデータを受信でき、パラメーターを使用してアドレス構造情報(IP、ポート、タイプなど)を取得できます。
recvfromのプロトタイプは次のとおりです。ここで、src_addrは送信者のアドレス構造情報を保存するために使用されます。関数の流れは次のとおりです。

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

ここに画像の説明を挿入

  • ファイル記述子に対応するカーネルソケットを見つける
  • メッセージ構造を作成し、ユーザー空間のアドレスバッファーポインターとデータバッファーポインターをメッセージ構造にパックします。
  • ソケットファイル記述子の対応するデータチェーンで対応するデータを検索し、データをメッセージにコピーします。
  • データチェーン内のデータを破棄し、データをアプリケーション層スペースにコピーして、ファイル記述子の参照カウントを減らします。
    カーネルスペースは、メッセージ構造msghdrを使用してすべてのデータ構造を格納します
    ここに画像の説明を挿入
    。Msg_nameおよびmsg_namelenは、送信者のアドレス関連情報を格納するために使用されます、そしてメッセージはmsg_iovに格納され、baseはユーザー空間に渡された受信データバッファーのアドレス、lenはユーザーから渡された受信バッファーの長さです。
    ここに画像の説明を挿入
    UDPでのデータの送信は、基本的にsendto関数を使用します。プロトタイプは次のとおりです。UDPは接続を確立しないため、宛先のアドレス構造情報をパラメーターdest_addrに入力する必要があります。ソケットが送信中にローカルIPアドレスとポートにバインドされていない場合、図に示すように、ネットワークプロトコルスタックに自動的に入力されます。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

ここに画像の説明を挿入
UDPプロトコルは送信のサービス品質を保証しないため、送信中にパケット損失、異常、フロー制御、送信ネットワークインターフェイスなどの問題が発生する可能性があります。メッセージの損失と順序が狂ったメッセージの場合、TCP方式を使用できます。UDPでメッセージのシーケンス番号をマークします。UDPメッセージを受信した後、受信者にメッセージが受信されたことを通知する確認応答を返します。指定された時間内に確認メッセージが受信されない場合、メッセージは失われ、再送信されたと見なされます。順序が正しくないメッセージは、メッセージのシーケンス番号に従って回復できます。
UDP通信でsendtoとrecvfromを使用すると、各ホストと簡単に通信でき、これら2つの関数を使用すると、相手のアドレス構造情報を指定/取得できます。接続関数はUDPでも使用できます。使用後、ソケット記述子とネットワークアドレス構造がバインドされます(バインドは相手側のものです)。バインド後は、sendtoおよびrecvfromを使用できません。使用できるのは、読み取り/書き込みまたは送信/ recv関数のみです。UDPプロトコルでのconnect()関数の使用は、相手のアドレスが決定されることを意味し、バインド関数は、受信用のローカルアドレスとポートのみをバインドします

元の記事を35件公開 Like1 Visits 1870

おすすめ

転載: blog.csdn.net/lzj_linux188/article/details/105206832