LoginSignup
1
3

More than 1 year has passed since last update.

[備忘] TCPコネクションができるまでを少しだけミクロに見てみる

Posted at

TCPコネクション/接続を確立するとはどういうことか?

image.png

もう少し詳細化してみる。図の上から下に向かって時間が流れる。
クライアント・サーバーでそれぞれ行っていることは、簡単には次の通り。

クライアント側:

  • ソケットを作成する
  • サーバー側のソケットへ接続しに行く
  • データの送受信を開始する

サーバー側:

  • ソケットを作成する
  • ソケットを接続待ち状態にする
  • クライアントからの接続を受け入れる
  • データの送受信を開始する

image.png

更に、実行されるシステムコールと紐付けて見てみる。
image.png

簡単なデモプログラムで上記の雰囲気を追ってみる

クライアント側 ①Socket作成

    int sock_fd;
    if ((sock_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) // ① Socket作成
    {
        perror("socket");
        return -1;
    }

サーバー側 ①Socket作成 ② Socketを接続待ち状態に

    // Socket作成
    int sock_fd;
    if ((sock_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) // ① Socket作成
    {
        perror("socket");
        return -1;
    }

    // bindするアドレスの情報をセット
    struct sockaddr_in sv_addr;
    memset((char *)&sv_addr, 0, sizeof(sv_addr)); // 0に初期化
    sv_addr.sin_family = AF_INET;                 // INETドメイン
    // ネットワークバイトオーダーへ変換するため、
    // htons(), htons()を通す
    sv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    sv_addr.sin_port = htons(80);                // port 80

    if (bind(sock_fd, (struct sockaddr *)&sv_addr, sizeof(sv_addr)) < 0)
    {
        perror("bind");
        return -1;
    }

    // Listen
    if (listen(sock_fd, SOMAXCONN) < 0) // ② Socketを接続待ち状態にする
    {
        perror("listen");
        return -1;
    }

クライアント側 ③サーバー側のSocketへ接続しに行く

    // 接続
    if (connect(sock_fd, (struct sockaddr *)&sv_addr, sizeof(sv_addr)) < 0) //  ③サーバー側のSocketへ接続しに行く
    {
        perror("connect");
        return -1;
    }

サーバー側 ④ 接続を受け入れる

    // 接続受付
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int acc_fd;
    if ((acc_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &len)) < 0) // ④ 接続を受け入れる
    {
        perror("accept");
        return -1;
    }

クライアント側 ⑤データの送信を開始する

    // 送信
    write(sock_fd, "Hello World!\n", 12);

サーバー側 ⑤データを受信する

    // データ受信
    ssize_t n;
    char buf[4096];
    while (1)
    {
        n = read(acc_fd, buf, sizeof(buf));
        if (n <= 0)
        {
            break;
        }

        write(1, buf, n); // そのまま標準出力に表示
    }

前項のプログラムについて

  • サーバー側(server.c)は、 0.0.0.0:80 で接続を待ち受け、クライアントから接続があるとクライアント側からの入力をそのままクライアントへ返却し、且つ標準出力にも表示して終了する
  • クライアント側(client.c)は、localhost:80へ接続しに行き、接続後にHello World!の文字列を送信し、サーバーから帰ってきた文字列を標準出力に表示して終了する。
  • 実行方法:
# Server
$ gcc -o server -O0 server.c
$ sudo ./server 

# Client
$ gcc -o client -O0 client.c
$ ./client

全文

client.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(int argc, char **argv)
{

    // Socket作成
    int sock_fd;
    if ((sock_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");
        return -1;
    }

    struct sockaddr_in sv_addr;
    memset((char *)&sv_addr, 0, sizeof(sv_addr)); // 0に初期化
    sv_addr.sin_family = AF_INET;                 // INETドメイン
    // ネットワークバイトオーダーへ変換するため、
    // htons(), htons()を通す
    sv_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost
    sv_addr.sin_port = htons(80);                     // port 80

    // 接続
    if (connect(sock_fd, (struct sockaddr *)&sv_addr, sizeof(sv_addr)) < 0)
    {
        perror("connect");
        return -1;
    }


    // 送信
    write(sock_fd, "Hello World!\n", 12);

    // データ受信
    ssize_t n;
    char buf[4096];
    if ((n = read(sock_fd, buf, sizeof(buf))) < 0)
    {
        perror("read");
        return -1;
    }

    write(1, buf, n);


    if (shutdown(sock_fd, SHUT_RDWR) < 0)
    {
        perror("shutdown");
        return -1;
    }

    return 0;
}
server.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(int argc, char *argv[])
{
    // Socket作成
    int sock_fd;
    if ((sock_fd = socket(PF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket");
        return -1;
    }
    int opt = 1; // True
    // INETドメインで一度通信に使用されたポートは通信終了後も一定時間は再割当てされない
    // 同じポートをbindしようとすると Addresss already in use
    // がでるのを回避するため
    if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (char *)&opt, sizeof(int)) < 0)
    {
        perror("setsockopt");
        return -1;
    }

    // bindするアドレスの情報をセット
    struct sockaddr_in sv_addr;
    memset((char *)&sv_addr, 0, sizeof(sv_addr)); // 0に初期化
    sv_addr.sin_family = AF_INET;                 // INETドメイン
    // ネットワークバイトオーダーへ変換するため、
    // htons(), htons()を通す
    sv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    sv_addr.sin_port = htons(80);                // port 80

    if (bind(sock_fd, (struct sockaddr *)&sv_addr, sizeof(sv_addr)) < 0)
    {
        perror("bind");
        return -1;
    }

    // Listen
    if (listen(sock_fd, SOMAXCONN) < 0) 
    {
        perror("listen");
        return -1;
    }

    // 接続受付
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int acc_fd;
    if ((acc_fd = accept(sock_fd, (struct sockaddr *)&client_addr, &len)) < 0)
    {
        perror("accept");
        return -1;
    }
    fprintf(stderr, "connect from %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // データ受信
    ssize_t n;
    char buf[4096];
    while (1)
    {
        n = read(acc_fd, buf, sizeof(buf));
        if (n <= 0)
        {
            break;
        }

        write(acc_fd, buf, n);
        write(1, buf, n);
    }

    // 接続終了
    if (shutdown(acc_fd, SHUT_RDWR) < 0)
    {
        perror("shutdown");
        return -1;
    }

    return 0;
}

参考にした本

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3