LoginSignup
3
3

今更ながらC言語でsocket通信を実装した

Last updated at Posted at 2024-02-21

はじめに

タイトル通りですが,今更ながらsocket通信についてアウトプットしたいので投稿します.
これまでにも一度学んでいましたが,一部認識が誤っていたことなどがあったりしたので再度学び直しました.
本記事は,C言語でsocket通信を実現したい!といった初学者に向けて記事を記載していきます.

※プロセスの複製(fork()+exec())を用いたプログラムは複雑になりがちで分かりにくいところでもあるためあえて今回は使っていません.
※書いている本人も初学者です.間違いなどがあればご教授お願いします.

なんでC言語でsocket

インターネットを見てみるとsocketに関するたくさんの素晴らしい記事を見かけます.それこそ,今回実装したechoサーバプログラムなど山ほどあります.しかし,見るだけでは学習になりません.今回は,学んだことを活かしてechoサーバを実装します.
また,C言語で書いた理由はPythonなどはライブラリが豊富で正直通信を理解しなくてもそれなりに動けるものが作れるので,プログラマが最初に学び,一番シンプルな言語であるC言語で作成しました.

意識したこと

今回の作成で意識したことは下記のとおりです.

  1. ポーリングをしない→I/O多重化を行う(poll()を使う)
  2. できるだけセキュリティ対策をする→scanf()などの危険な関数を使わない[2]

1.については特に意識しました.

socketを一度実装したことがある方はお分かりだと思いますが,socket通信でやり取りする際には,基本的には下記の図のようになり,メッセージのやり取りを行うときにwhile()かfor()を必ずといっていいほど使用します.

socket.drawio.png

この時に,問題となるのがクライアント側からの通信はいつ来るかわからないことです.
いつ来るかわからない.つまり,計算機は常に読み込み(read)と書き込み(write)を行える状況でないといけません.しかし,read()とwrite()は基本的にブロッキングを行います.[1]
何を言っているのかというと,read()が呼び出されるとread()を行うまでサーバ側は次の処理ができません.
そこで,for/writeを用いることでread()/write()を不必要に呼び出し続けます.(これがポーリング)

しかし,ポーリングを使用することは適切ではありません.なぜなら,サーバへの負荷が大きくなるためです.
そこで,登場するのが非同期ブロッキングpoll()/select()です.(他にもepoll(7)などがあります)
非同期ブロッキングを用いることで,read(),write()でブロッキングをするのでなく,poll()でブロッキングを行います.[3]
その結果,poll()でサーバのやりとりを管理するため,read()/write()の無駄な呼び出し(ポーリング)をしなくて済むようになります.

2.についてはおまけです.
突然ですが,C言語にて文字列の入力を読み取る際に何の関数を用いますか?
おそらく一番多い回答はscanf()ではないでしょうか.
しかし,scanf()を使うのは今後やめた方がいいです.というよりは,デバッグ等以外では絶対に使わない方が良いでしょう.
scanf()は,かなり危険な関数であることをご存知でしょうか?
何が危険なのでしょうか.
それは,バッファの中身を確認しないことです.
中身を確認しない=プログラムにそのままプログラムに書き込むということになります.
つまり,悪意のあるコードをそのままプログラムに組み込まれる可能性があります.
また,scanf()で読み取った文字列がバッファサイズを超えた時,バッファオーバーランなどの危険性があります.

これらはあくまで,個人の理解です.
関数の動作などはmanページなどをみて一度確認してみてください

プログラム

それでは,プログラムを示します.
動作環境は,Macbook Pro2019でgccを使います.
今回実装したのは,クライアント・サーバ型のプログラムですので,クライアントとサーバの2種のプログラムを解説していきます.

クライアントプログラム

実行方法

gcc client.c -o client
./client
client.c
#include <stdio.h>
#include <sys/socket.h>
#include <poll.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

#define BUFF_SIZE 1024
#define PORT 1000

/***
    改行コード(\n)を削除する(\0に変更する)処理
    args: メッセージ
    returns: None
***/
void delete_newline(char *buff)
{
    for (int cnt = 0; cnt < BUFF_SIZE; cnt++)
    {
        if (buff[cnt] == '\n' || (buff[cnt] == '\r' && buff[cnt + 1] == '\n'))
        {
            buff[cnt] = '\0';
            break;
        }
    }
}

int main(void)
{
    int sock;                    // 通信用のソケット
    struct pollfd fds[2];        // poll()で見るための読み込みソケット
    char buff[BUFF_SIZE] = {0};  // 送受信用のデータ
    struct sockaddr_in servaddr; // サーバの情報

    // ソケット制作
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // サーバの設定
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_len = sizeof(servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    // 名前の入力を行う
    fprintf(stdout, "名前を入力してください -> ");
    // scanf()は用いない!
    fgets(buff, BUFF_SIZE, stdin); // 文字列入力
    delete_newline(buff);

    // コネクションを張るための要求をサーバに送る
    printf("接続中...\n");
    connect(sock, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in));

    // poll()で監視するFDを登録
    // ソケットと標準入力
    fds[0].fd = sock;
    fds[0].events = POLLIN | POLLERR;
    fds[1].fd = STDIN_FILENO;
    fds[1].events = POLLIN | POLLERR;

    // 名前をサーバに送る
    write(fds[0].fd, buff, BUFF_SIZE);

    for (;;)
    {
        // sockと標準入力を監視
        poll(fds, 2, -1);
        // 自分が何かアクションを起こした時用(データの送信)
        if (fds[1].revents == POLLIN)
        {
            memset(buff, 0, sizeof(buff));
            fgets(buff, BUFF_SIZE, stdin);
            delete_newline(buff);
            write(fds[0].fd, buff, BUFF_SIZE);
        }
        // サーバからの通信要求がある場合に入る(データの受信)
        else if (fds[0].revents == POLLIN)
        {
            memset(buff, 0, sizeof(buff));
            read(fds[0].fd, buff, BUFF_SIZE);

            if (strcmp(buff, "connected_refused") == 0)
            {
                printf("すみません...\n接続に失敗しました\n");
                break;
            }

            if (strcmp(buff, "logout") == 0)
            {
                printf("ログアウトが完了しました\n");
                break;
            }
            if (strcmp(buff, "server_end") == 0)
            {
                printf("サーバが止まりました\n");
                break;
            }
            printf("%s", buff);
        }
    }
    // ソケットの終了
    close(sock);
}

サーバプログラム

実行方法

gcc server.c -o server 
./server
server.c
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <signal.h>
#include <time.h>

#define PORT 1000
#define BUFF_SIZE 1024
#define MAX_USER 10 // 接続するユーザ(クライアント)の上限を設定
#define MAX_USER_PLUS2 MAX_USER + 2

// ユーザの情報を持つ構造体
typedef struct
{
    char name[BUFF_SIZE]; // クライアントの名前
    int login;            // 0: ログアウト状態,1: ログイン状態
} User;

/***
    構造体の初期化を行う
    args: None
    returns: Struct
***/
User UserInit()
{
    User user;
    user.login = 0;
    return user;
}

/***
    改行コード(\n)を削除する(\0に変更する)処理
    args: buff(char配列)
    returns: None
***/
void delete_newline(char *buff)
{
    for (int cnt = 0; cnt < BUFF_SIZE; cnt++)
    {
        if (buff[cnt] == '\n' || (buff[cnt] == '\r' && buff[cnt + 1] == '\n'))
        {
            buff[cnt] = '\0';
            break;
        }
    }
}

int main(void)
{
    int sock;                                      // ソケットを作る(ソケットはFDである)
    User user[MAX_USER_PLUS2];                     // クライアントを管理する構造体
    struct pollfd fds[MAX_USER_PLUS2];             // poll()で監視するクライアント(FD)用の構造体
    struct sockaddr_in servaddr;                   // サーバの情報
    int i, j, count_user = 0;                      // i,j: for用,count_user: 何人ログインしているか数える
    char buff[BUFF_SIZE], s_buff[BUFF_SIZE] = {0}; // バッファ = メッセージなどを一時保管する

    // 構造体の初期化を行う
    for (j = 0; j < MAX_USER_PLUS2; j++)
    {
        user[j] = UserInit();
    }

    printf("サーバの起動中...\n");
    // ソケットを作る
    sock = socket(AF_INET, SOCK_STREAM, 0);

    // アドレスを作る
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_len = sizeof(servaddr);
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);

    // ソケットにアドレスを割り当てる
    bind(sock, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in));

    // pollでソケットと標準入力を監視するため構造体に入れる
    fds[0].fd = sock;
    fds[0].events = POLLIN | POLLERR;
    fds[1].fd = STDIN_FILENO;
    fds[1].events = POLLIN | POLLERR;

    // コネクション要求を待ち始める
    listen(fds[0].fd, 5);

    printf("起動完了!\n管理者 ->このサーバの残り接続可能人数は%d人です\n", MAX_USER);
    for (;;)
    {
        // ここでpollでブロッキングを行う
        poll(fds, MAX_USER_PLUS2, -1);

        // ソケットのFDが変化(最初の接続)=fds[0]のこと かつ 最大許容人数を超えていない場合
        if (fds[0].revents & POLLIN && count_user < MAX_USER)
        {
            for (i = 2; i < MAX_USER_PLUS2; i++)
            {
                if (user[i].login == 0)
                {
                    user[i] = UserInit();                      // 構造体の初期化をする
                    fds[i].fd = accept(fds[0].fd, NULL, NULL); // 接続してきたクライアントのFD(socket)を管理するように配列に入れる
                    fds[i].events = POLLIN | POLLERR;          // 接続してきたクライアントの何を管理するか

                    memset(buff, 0, sizeof(buff));
                    read(fds[i].fd, buff, BUFF_SIZE);       // 送られてきた名前を読み取る(buffに保存)
                    strncpy(user[i].name, buff, BUFF_SIZE); // user.nameの構造体にbuffの情報(名前)を保存する
                    user[i].login = 1;                      // ログイン状態にする
                    count_user++;                           // 接続しているクライアントの人数を1人増やす

                    snprintf(buff, sizeof(buff), "->%s がログインしました! 残りの接続数:%d\n", user[i].name, MAX_USER - count_user);
                    // 全クライアントにブロードキャスト
                    for (j = 2; j < MAX_USER_PLUS2; j++)
                    {
                        if (user[j].login == 1)
                        {
                            if (j != i)
                            {
                                snprintf(s_buff, sizeof(s_buff), "->%sさんがすでにログインしています!\n", user[j].name);
                                write(fds[i].fd, s_buff, BUFF_SIZE);
                            }
                            write(fds[j].fd, buff, BUFF_SIZE);
                        }
                    }

                    // 最大人数になった時(サーバ側のみに送信)
                    if (count_user == MAX_USER)
                    {
                        printf("管理者 -> 最大接続数になりました.\n今後は新しい接続を拒否します.\n");
                    }
                    break;
                }
            }
        }
        // 接続台数が超えていた場合は拒否する
        else if (fds[0].revents == POLLIN && count_user == MAX_USER)
        {
            printf("接続の拒否を行いました.\n");
            int fd = accept(fds[0].fd, NULL, NULL);
            write(fd, "connected_refused", BUFF_SIZE);
            close(fd);
        }
        // サーバ側が何かアクションを起こした時
        // 今回は,サーバのターミナルで"exit"と入力するとサーバを停止するようにした
        else if (fds[1].revents == POLLIN)
        {
            memset(buff, 0, sizeof(buff));
            fgets(buff, BUFF_SIZE, stdin);
            delete_newline(buff);
            if (strcmp(buff, "exit") == 0)
            {
                break;
            }
            else
            {
                printf("%s\n", buff);
            }
        }
        // 接続が完了したクライアントが何かアクションを起こした時
        else
        {
            for (i = 2; i < MAX_USER_PLUS2; i++)
            {
                if (fds[i].revents & POLLIN && user[i].login == 1)
                {
                    memset(buff, 0, sizeof(buff));
                    memset(s_buff, 0, sizeof(buff));
                    read(fds[i].fd, buff, BUFF_SIZE);
                    // クライアントが"logout"と入力した時
                    if (strcmp(buff, "logout") == 0)
                    {
                        snprintf(s_buff, sizeof(s_buff), "->%sさんがログアウトしました\n", user[i].name); // s_buffにメッセージを入れる
                        // "logout"と打ち込んだクライアントのみにbuff(中身は"logout"という文字列)
                        write(fds[i].fd, buff, BUFF_SIZE);
                        
                        // s_buff(上2行目の内容)をブロードキャスト
                        for (j = 2; j < MAX_USER_PLUS2; j++)
                        {
                            if (user[j].login == 1)
                            {
                                write(fds[j].fd, s_buff, BUFF_SIZE);
                            }
                        }
                        user[i] = UserInit();
                        count_user--; // クライアントを1人減らす
                        break;
                    }
                    // その他の入力の場合
                    else
                    {
                        snprintf(s_buff, sizeof(s_buff), "%s: %s\n", user[i].name, buff);

                        // s_buff(上2行目)の内容をブロードキャスト
                        for (j = 2; j < MAX_USER_PLUS2; j++)
                        {
                            if (user[j].login == 1)
                            {
                                write(fds[j].fd, s_buff, BUFF_SIZE);
                            }
                        }
                    }
                    printf("%s", s_buff);
                }
            }
        }
    }

    // サーバ側のクローズ処理
    // 初めに,現時点で接続しているクライアントの接続を切る
    // その後に自信のsocketをクローズさせる
    for (i = 0; i < MAX_USER_PLUS2; i++)
    {
        if (user[i].login == 1)
        {
            write(fds[i].fd, "server_end", BUFF_SIZE);
            read(fds[i].fd, buff, BUFF_SIZE);
        }
    }
    printf("Stopped server\n");
    close(sock);
}

おわりに

今回は,基本的なsocket通信について実装してみました.
socketはどこで使われてるのかというと,ブラウザ(Chrome,Firefoxなどなど)で使われています.
またサーバを構築するとなると,nginxやapacheなどを用いることが多いと思いますが,基本的な通信について知っておくことは大事なのではないかと考えています.
本記事がC言語を学んでいる人などに役立ててもらえれば嬉しく思います.
また,記事を書いている本人もまだまだ学んでいかなければならない身なので,間違いや新たな考えなどがあればぜひともご教授していただければ幸いです.
拙い記事ですが,見ていただいてありがとうございました.

筆者がこの記事を書くまでに使用した本

もし,興味が湧いた人は下記の本を参考にするといいと思います.

参考文献

3
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
3
3