私たちは情報交換の価値をよく知っていますが、ネットワーク内のプロセスはどのように通信するのでしょうか? たとえば、毎日ブラウザを開いて Web を閲覧するとき、ブラウザのプロセスはどのように Web サーバーと通信するのでしょうか? QQ を使用してチャットする場合、QQ プロセスは、友達がいるサーバーまたは QQ プロセスとどのように通信しますか?これらはすべてソケットに依存しているのでしょうか?では、ソケットとは何でしょうか?ソケットにはどんな種類があるの?ソケットの基本的な機能もありますので、それをこの記事で紹介したいと思います。この記事の主な内容は次のとおりです:
1. ネットワーク内のプロセス間で通信するにはどうすればよいですか?
2. ソケットとは何ですか?
3.socket()関数
3.1、socket()関数
3.3、listen()、connect()関数
3.4、accept()関数
3.5、read( )、write()関数など
3.6. close()関数
4. ソケットで接続を確立するためのTCPの3ウェイハンドシェイクの詳細説明
5. 接続を解放するためのTCPの4ウェイハンドシェイクの詳細な説明ソケット内
6. 例 (練習 (ちょっと))
7. 質問を残してください。誰でも返信することを歓迎します。 ! !
1. ネットワーク内のプロセス間で通信するにはどうすればよいですか?
ローカルプロセス間通信 (IPC) には多くの方法がありますが、次の 4 つのカテゴリに要約できます:
メッセージ受け渡し (パイプライン、FIFO、メッセージキュー)
同期 (ミューテックス、条件変数、読み取り)書き込みロック、ファイルおよび書き込みレコード ロック、セマフォ)
共有メモリ (匿名および名前付き)
リモート プロシージャ コール (Solaris ゲートおよび Sun RPC)
ただし、これらはこの記事のトピックではありません。これから議論するのは、ネットワーク内のプロセス間でどのように通信するかということです。解決すべき最初の問題は、プロセスを一意に識別する方法です。そうしないと通信が不可能になります。プロセスはプロセス PID によってローカルで一意に識別できますが、これはネットワーク上では機能しません。実際、TCP/IP プロトコル ファミリは、この問題の解決に役立ちました。ネットワーク層の「IP アドレス」はネットワーク内のホストを一意に識別でき、トランスポート層の「プロトコル + ポート」はアプリケーションを一意に識別できます。ホスト内の(プロセス)。このように、トリプレット (IP アドレス、プロトコル、ポート) を使用してネットワーク プロセスを識別でき、ネットワーク内のプロセス通信ではこのマークを使用して他のプロセスと対話できます。
TCP/IP プロトコルを使用するアプリケーションは通常、アプリケーション プログラミング インターフェイス、つまり UNIX BSD のソケットと UNIX System V (すでに廃止されました) の TLI を使用して、ネットワーク プロセス間の通信を実現します。今のところ、ほとんどすべてのアプリケーションがソケットを使用しており、ネットワーク上のプロセス通信が普及しているのはこのためです。
2. ソケットとは何ですか?
ネットワーク内のプロセスがソケットを介して通信することはすでにわかっていますが、ソケットとは何でしょうか?ソケットは Unix に由来しており、Unix/Linux の基本理念の 1 つは「すべてはファイルである」というものであり、「オープン -> 読み取りと書き込み書き込み/読み取り -> クローズ」モードで動作します。私の理解では、ソケットはこのモードの実装であり、いくつかのソケット関数はそのファイルに対する操作 (読み取り/書き込み IO、オープン、クローズ) です。これらの関数については後で紹介します。
ソケットという言葉の起源
ネットワーキングの分野で最初に使用されたのは、1970 年 2 月 12 日にリリースされた、Stephen Carr、Steve Crocker、Vint Cerf によって書かれた文書 IETF RFC33 で見つかりました。コンピュータ歴史博物館によると、Croker 氏は次のように書いています。「名前空間の要素はソケット インターフェイスと呼ばれることがあります。ソケット インターフェイスは接続の一端を形成し、接続は 1 組のソケット インターフェイスによって完全に指定される場合があります。」コンピュータ歴史博物館追加: 「これは、BSD ソケット インターフェイスの定義より約 12 年前です。」
3. ソケットの基本操作
ソケットは「open-write/read-close」モードの実装の一部であるため、ソケットは次の機能を提供します。これらの操作に対応する機能インターフェイス。以下では、例として TCP を使用して、いくつかの基本的なソケット インターフェイス関数を紹介します。
3.1、socket()関数
intソケット(intドメイン, int型, intプロトコル);
socket関数は通常のファイルを開く操作に相当します。通常のファイルオープン操作はファイル記述子を返し、socket() を使用してソケットを一意に識別するソケット記述子 (ソケット記述子) を作成します。このソケット記述子はファイル記述子と同じであり、後続の操作でいくつかの読み取りおよび書き込み操作を実行するためのパラメーターとして使用されます。
異なるパラメータ値を fopen に渡して異なるファイルを開くことができるのと同じように。ソケットを作成するときに、異なるパラメーターを指定して異なるソケット記述子を作成することもできます。 ソケット関数の 3 つのパラメーターは次のとおりです:
domain: プロトコル ドメイン。プロトコル ファミリとも呼ばれます。一般的に使用されるプロトコル ファミリには、AF_INET、AF_INET6、AF_LOCAL (または AF_UNIX、Unix ドメイン ソケット)、AF_ROUTE などが含まれます。プロトコル ファミリによってソケットのアドレス タイプが決定され、対応するアドレスが通信で使用される必要があります。たとえば、AF_INET は ipv4 アドレス (32 ビット) とポート番号 (16 ビット) の組み合わせを使用するかどうかを決定し、AF_UNIX は決定します。絶対パスをアドレスとして使用します。
type: ソケットのタイプを指定します。一般的に使用されるソケット タイプには、SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET などが含まれます (ソケットのタイプは何ですか?)。
プロトコル: その名前のとおり、指定されたプロトコルを意味します。一般的に使用されるプロトコルには、IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC などがあり、それぞれ TCP 送信プロトコル、UDP 送信プロトコル、STCP 送信プロトコル、TIPC 送信プロトコルに対応します (このプロトコルについては別途説明します)。
注: 上記のタイプとプロトコルは自由に組み合わせることができません。たとえば、SOCK_STREAM と IPPROTO_UDP を組み合わせることはできません。プロトコルが 0 の場合、タイプ type に対応するデフォルトのプロトコルが自動的に選択されます。
socket を呼び出してソケットを作成すると、返されたソケット記述子はプロトコル ファミリ (アドレス ファミリ、AF_XXX) 空間に存在しますが、特定のアドレスを持ちません。アドレスを割り当てたい場合は、bind() 関数を呼び出す必要があります。そうしないと、connect() または listen() を呼び出すときにシステムが自動的にポートをランダムに割り当てます。
3.2. binding() 関数
上で述べたように、bind() 関数はアドレス ファミリ内の特定のアドレスをソケットに割り当てます。たとえば、AF_INET および AF_INET6 に対応して、ipv4 または ipv6 アドレスとポート番号の組み合わせがソケットに割り当てられます。
int binding(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
関数の 3 つのパラメータは次のとおりです:
sockfd: ソケット記述子。socket() 関数を通じて作成され、一意に識別されます。 .ソケット。 bind() 関数は、名前をこの記述子にバインドします。
addr: const struct sockaddr * sockfd にバインドされるプロトコル アドレスを指すポインター。このアドレス構造は、アドレスがソケットを作成するときのアドレス プロトコル ファミリによって異なります。たとえば、ipv4 は以下に対応します。
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */};/* Internet address. */struct in_addr { uint32_t s_addr; /* address in network byte order */}; ipv6对应的是: struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ };struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ }; Unix域对应的是: #define UNIX_PATH_MAX 108struct sockaddr_un { sa_family_t sun_family; /* AF_UNIX */ char sun_path[UNIX_PATH_MAX]; /* pathname */ }; addrlen:对应的是地址的长度。
通常、サーバーは起動時に既知のアドレス (IP アドレス + ポート番号など) をバインドします。 、サービスを提供するために、顧客はそれを介してサーバーに接続できます。クライアントは指定する必要はなく、システムがポート番号と独自の IP アドレスの組み合わせを自動的に割り当てます。このため、サーバーは通常、リッスンする前にbind()を呼び出しますが、クライアントはそれを呼び出さず、代わりにconnect()のときにシステムがランダムにbind()を生成します。
ネットワークのバイトオーダーとホストのバイトオーダー
ホストのバイトオーダーは、通常ビッグエンディアンモードとリトルエンディアンモードと呼ばれるものです。異なるCPUには異なるバイトオーダータイプがあり、これらのバイトオーダーはメモリ内の整数を参照し、それらが保存される順序を指します。をホスト順序といいます。ビッグエンディアンとリトルエンディアンの標準的な定義は以下のように引用されています:
a) リトルエンディアンとは、下位バイトがメモリの下位アドレス端に配置され、上位バイトがメモリの下位アドレス端に配置されることを意味します。メモリの上位アドレスの端。
b)ビッグエンディアンとは、上位バイトがメモリの下位アドレス端に配置され、下位バイトがメモリの上位アドレス端に配置されることを意味します。
ネットワークバイトオーダー: 4バイトの32ビット値は次の順序で送信されます: 最初に0~7ビット、次に8~15ビット、次に16~23ビット、最後に24~31ビット。この転送順序はビッグエンディアンと呼ばれます。 TCP/IP ヘッダー内のすべての 2 進整数は、ネットワーク経由で送信されるときにこの順序である必要があるため、ネットワーク バイト オーダーとも呼ばれます。名前が示すように、バイト オーダーは 1 バイト型を超えるデータがメモリに格納される順序です。1 バイトのデータには順序の問題はありません。
つまり: アドレスをソケットにバインドするときは、まずホストのバイトオーダーをネットワークのバイトオーダーに変換し、ホストのバイトオーダーがネットワークのバイトオーダーと同様にビッグエンディアンを使用すると想定しないでください。この問題が原因で殺人事件も起きています!この問題は、同社のプロジェクト コードに多くの不可解な問題を引き起こしているため、ホストのバイト オーダーについては何も仮定せず、ソケットに割り当てる前に必ずネットワーク バイト オーダーに変換してください。
3.3、listen()、connect()関数
あなたがサーバーの場合、socket() と binding() を呼び出した後、listen() を呼び出してソケットをリッスンします。クライアントが connect() を呼び出して接続リクエストを発行すると、サーバーはリクエストを受け取ります。 int listen(int sockfd, int backlog);int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
# include
#include
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void * buf , size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t rec vfrom(int sockfd, void *buf, size_t len , int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int so ckfd, struct msghdr *msg, int flags) ;
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
#include
close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。
注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。
4、socket中TCP的三次握手建立连接详解
我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:
客户端向服务器发送一个SYN J
服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
客户端再想服务器发一个确认ACK K+1
只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:
图1、ソケットの詳しい説明
从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。
5、socket中TCP的四次握手释放连接详解
上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:
图2、ソケットの詳しい説明
图示过程如下:
某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
接收到这个FIN的源发送端TCP对它进行确认。
这样每个方向上都有一个FIN和ACK。
6、一个例子(实践一下)
说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。
服务器端代码:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> #include<sys/types.h> #include<sys/socket.h> #include<netinet/in.h> #define MAXLINE 4096 int main(int argc, char** argv) { int listenfd, connfd; struct sockaddr_in servaddr; char buff[4096]; int n; if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){ printf("create socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(6666); if( bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1){ printf("bind socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } if( listen(listenfd, 10) == -1){ printf("listen socket error: %s(errno: %d)\n",strerror(errno),errno); exit(0); } printf("======waiting for client's request======\n"); while(1){ if( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1){ printf("accept socket error: %s(errno: %d)",strerror(errno),errno); continue; } n = recv(connfd, buff, MAXLINE, 0); buff[n] = '\0'; printf("recv msg from client: %s\n", buff); close(connfd); } close(listenfd); }
当然上面的代码很简单,也有很多缺点,这就只是简单的演示socket的基本函数使用。其实不管有多复杂的网络程序,都使用的这些基本函数。上面的服务器使用的是迭代模式的,即只有处理完一个客户端请求才会去处理下一个客户端的请求,这样的服务器处理能力是很弱的,现实中的服务器都需要有并发处理能力!为了需要并发处理,服务器需要fork()一个新的进程或者线程去处理请求等。
7、动动手
留下一个问题,欢迎大家回帖回答!!!是否熟悉Linux下网络编程?如熟悉,编写如下程序完成如下功能:
服务器端:
接收地址192.168.100.2的客户端信息,如信息为“Client Query”,则打印“Receive Query”
客户端:
アドレス192.168.100.168のサーバーに「Client Query test」、「Cleint Query」、「Client Query Quit」という情報を順番に送信して終了します。
質問に表示されるIPアドレスは、実際の状況に応じて決定できます。