ホームページ  >  記事  >  php教程  >  ソケット通信の概要

ソケット通信の概要

高洛峰
高洛峰オリジナル
2016-12-13 10:09:181033ブラウズ

私たちは情報交換の価値をよく知っていますが、ネットワーク内のプロセスはどのように通信するのでしょうか? たとえば、毎日ブラウザを開いて 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() はソケットを一意に識別するソケット記述子 (ソケット記述子) を作成するために使用されます。このソケット記述子はファイル記述子と同じであり、後続の操作でいくつかの読み取りおよび書き込み操作を実行するためのパラメーターとして使用されます。

異なるパラメータ値を 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 アドレス + ポート番号など) にバインドされ、顧客はそれを介してサーバーに接続できます。クライアントはそれを指定する必要はありません。システムは、1 つのポート番号と独自の 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()、bind()を呼び出した後、クライアントがconnect()を呼び出すと、listen()が呼び出され、ソケットをリッスンします。今回は接続リクエストを発行し、サーバーはこのリクエストを受信します。

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

listen 関数の最初のパラメーターはリッスンするソケット記述子で、2 番目のパラメーターは対応するソケットのキューに入れることができる接続の最大数です。デフォルトでは、socket() 関数によって作成されたソケットはアクティブ型ですが、listen 関数はソケットをパッシブ型に変更し、クライアントの接続要求を待ちます。

connect 関数の最初のパラメータはクライアントのソケット記述子、2 番目のパラメータはサーバーのソケット アドレス、3 番目のパラメータはソケット アドレスの長さです。クライアントは、connect 関数を呼び出して TCP サーバーとの接続を確立します。

3.4、accept()関数

TCPサーバーはsocket()、bind()、listen()を順番に呼び出した後、指定されたソケットアドレスをリッスンします。 TCP クライアントは、socket() と connect() を順番に呼び出した後、TCP サーバーに接続要求を送信します。 TCP サーバーはこのリクエストをリッスンした後、accept() 関数を呼び出してリクエストを受信し、接続が確立されます。その後、通常のファイルの読み取りおよび書き込み I/O 操作と同様のネットワーク I/O 操作を開始できます。

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

accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。

3.5、read()、write()等函数

万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

read()/write()

recv()/send()

readv()/writev()

recvmsg()/sendmsg()

recvfrom()/sendto()

我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

      #include <unistd.h>

       ssize_t read(int fd, void *buf, size_t count);
       ssize_t write(int fd, const void *buf, size_t count);

       #include <sys/types.h>
       #include <sys/socket.h>

       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 recvfrom(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 sockfd, struct msghdr *msg, int flags);

read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google,下面的例子中将使用到send/recv。

3.6、close()函数

在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

#include <unistd.h>
int close(int fd);

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、socket中发送的TCP四次握手

图示过程如下:

某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;

另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;

一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;

接收到这个FIN的源发送端TCP对它进行确认。

这样每个方向上都有一个FIN和ACK。

6、一个例子(实践一下)

说了这么多了,动手实践一下。下面编写一个简单的服务器、客户端(使用TCP)——服务器端一直监听本机的6666号端口,如果收到连接请求,将接收请求并接收客户端发来的消息;客户端与服务器端建立连接并发送一条消息。

服务器端代码:

服务器端

#include
#include
#include
#include
#include
#include
#include

#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&#39;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] = &#39;/0&#39;;
    printf("recv msg from client: %s/n", buff);
    close(connfd);
    }
    close(listenfd);
}

客户端代码:

客户端

#include
#include
#include
#include
#include
#include
#include

#define MAXLINE 4096

int main(int argc, char** argv)
{
    int    sockfd, n;
    char    recvline[4096], sendline[4096];
    struct sockaddr_in    servaddr;
    if( argc != 2){
    printf("usage: ./client <ipaddress>/n");
    exit(0);
    }
    if( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
    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_port = htons(6666);
    if( inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0){
    printf("inet_pton error for %s/n",argv[1]);
    exit(0);
    }
    if( connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
    printf("connect error: %s(errno: %d)/n",strerror(errno),errno);
    exit(0);
    }
    printf("send msg to server: /n");
    fgets(sendline, 4096, stdin);
    if( send(sockfd, sendline, strlen(sendline), 0) < 0)
    {
    printf("send msg error: %s(errno: %d)/n", strerror(errno), errno);
    exit(0);
    }
    close(sockfd);
    exit(0);
}

当然上面的代码很简单,也有很多缺点,这就只是简单的演示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地址可以根据实际情况定。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。