Skip to content

エコーサーバー・クライアントの実装(追加機能)#2

Merged
thonda28 merged 41 commits intomainfrom
feature/additional-functions
Jan 30, 2025
Merged

エコーサーバー・クライアントの実装(追加機能)#2
thonda28 merged 41 commits intomainfrom
feature/additional-functions

Conversation

@thonda28
Copy link
Copy Markdown
Owner

@thonda28 thonda28 commented Jan 7, 2025

Description

#1 の続き

この PR では以下3つの機能追加を行う

  • 大量のリクエスト処理
    • epoll を使って I/O 多重化
    • listen_sockets および client_socketsSocketManager という構造体を定義して管理
  • IPv6 対応
    • getaddrinfo() で取得した情報を使って socket を作成
    • client 側も引数として与えたアドレスの形式によって、該当のプロトコルにリクエスト(127.0.0.1 なら IPv4、::1 なら IPv6)
  • 適切な終了
    • pipeepoll に登録して、SIGINT がきたら Cleanup 処理に移行

Note

対応できてないものは以下(他にも対応できていないものはありそうだが、ひとまず自分で認識しているもの)

  • binary の入力
    • fread() で入力できそうだったが、Interactive に入力することはできない?
  • struct SockData で管理している buffer のサイズが固定
    • ここも動的に管理できるとよさそう

Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
while ((received_bytes = recv(conn_sock, buf, sizeof(buf) - 1, 0)) > 0)
{
printf("received_bytes: %ld\n", received_bytes);
buf[received_bytes] = '\0'; // Null-terminate the string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

received buffer が文字列である (バイナリで \0 を含んでいない) ことが前提になっている?

Comment thread server.c
Comment thread client.c
Comment thread client.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread Makefile Outdated
Comment thread Makefile Outdated
Comment thread utils.h Outdated
Comment thread client.c Outdated
Comment thread client.c Outdated
while (1)
{
char buf[BUFFER_SIZE];
fgets(buf, sizeof(buf), stdin); // Null-terminate the string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

入力がバイト列の場合はどうする?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fgets が一番最後まで読み切ったときは break したい。

Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
Comment thread server.c Outdated
@thonda28 thonda28 marked this pull request as ready for review January 17, 2025 10:30
Comment thread Makefile
run-client:build-client
./client
server: server.o utils.o
$(CC) $(CFLAGS) -o server server.o utils.o
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$@
$^

という変数もあります

Comment thread client.c
{
perror("client: connect()");
close(sock);
puts("Failed to create a connected socket\n");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perror は stderr に表示されるので、合わせてこれも stderr に出力した方が良いと思います。

Comment thread client.c
struct sockaddr_in6 server_addr6;
memset(&server_addr4, 0, sizeof(server_addr4));
memset(&server_addr6, 0, sizeof(server_addr6));
int is_ipv4 = inet_pton(PF_INET, ip, &server_addr4.sin_addr);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_xxxxx という名前は true or false を連想させるので違う名前にしたい。 (そのままif 文で評価して -1 を true 扱いしてしまう事故が発生しそう)

Comment thread client.c
int socket_fd;
if (is_ipv4 == 1)
{
if ((socket_fd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

パラメータが違う同じコードが2か所(v4/v6)にあるので、共通化したい。

Comment thread utils.c

#include "utils.h"

void init_socket_manager(SocketManager *manager, int max_size)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

メモリ確保が init_socket_manager で
解放が close_all_sockets で対で呼ぶ必要があるのは、名前からだと読み解くのが難しいです。

SocketManager *new_socket_manager(int max_size)
void free_socket_manager(SocketManager *)

などとするのはいかがでしょうか?

Comment thread server.c
ssize_t sent_bytes;
if ((sent_bytes = send(client_socket_data->socket_fd, client_socket_data->buffer, strlen(client_socket_data->buffer), 0)) >= 0)
{
memmove(client_socket_data->buffer, client_socket_data->buffer + sent_bytes, strlen(client_socket_data->buffer) - sent_bytes);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

データのコピーにデータ量に時間がかかるためmemmove は避けたい
(send に バッファの途中のポインタを与えたい)

Comment thread server.c
}

// Check if the client socket is ready to write
if (event.events & EPOLLOUT)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

送るデータが無かったら? (バッファが0の時に EPOLLOUT を受信しない)

Comment thread server.c
if ((received_bytes = recv(client_socket_data->socket_fd, client_socket_data->buffer, BUFFER_SIZE - 1, 0)) > 0)
{
printf("received_bytes: %ld (fd: %d)\n", received_bytes, client_socket_data->socket_fd);
client_socket_data->buffer[received_bytes] = '\0'; // Null-terminate the string
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

外からやってくるデータがバイナリデータ (\0を含む)かもしれないので、データがどこまで存在しているかを \0 に依存したくない。
(strlen は O(N) 時間がかかるのもあります)
client_socket_data にバッファに何文字入っているかを保存させては?

Comment thread server.c
}

struct epoll_event event;
event.events = EPOLLIN | EPOLLOUT;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

送り返すデータが存在しない場合に EPOLLOUT を受け取ってもやることがない (= 無限に空パケットを送ったり CPU を無駄にするループになる可能性)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここでおっしゃってるのは、そもそも EPOLLOUT を受け取らないように修正する、ということですかね?(#3 では送り返すデータが存在しなければ処理を終えるように修正しました)
もし上記の意図だとすると、バッファの状況を見ながら epoll_event の監視をやめたり始めたりとかそんな感じでしょうか?

Comment thread server.c
}
}
// Check if the event is for a client socket
else if ((socket_data = find_socket(&client_socket_manager, events[i].data.fd)) != NULL)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_socket は O(N) かかり、 epoll 自体がソケットの数に比例する時間を必要としないように作られている利点を消してしまっています。
epoll_event->data を epollの設定時にセットすることで、イベントの受信時に受け取ることができるので、ここに(data->ptrなどに) SocketData を保持するのはいかがでしょうか?
(close時も O(1)にすることを考えるとSocketData を少し拡張する必要があるかもしれません)

handle_new_connection の分岐も同様

Copy link
Copy Markdown
Owner Author

@thonda28 thonda28 Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提案ありがとうございます。epoll_event->data->ptr の存在に気づいていなかったですが、たしかにここを活用できますね。SocketData のポインタをもたせることで socket_manager を線形探索せずに SocketData を取得できるよう #3 で修正してみました。イメージ合っていそうでしょうか?

ただ削除時の実装まではできていないです。(断念しました)
実装するとすると、SocketManager では現状配列で SocketData を管理していますが、ここをハッシュテーブルにして、キーを fd に、バリューに SocketData のポインタを持たせるとかですかね?

@thonda28
Copy link
Copy Markdown
Owner Author

@inazz
レビューおよびたくさんのコメントありがとうございます、勉強になります!
この PR のまま修正するとややこしく感じるので一度この PR をマージして、コメントいただいたものについては別 PR で対応しようと思います!

Copy link
Copy Markdown
Owner Author

@thonda28 thonda28 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@inazz
レビューありがとうございました!いただいたコメントに対する修正自体は #3 で行いましたが、意図を汲めていないかもと思う部分が3点あったのでそれはそのままこちらの PR で返信させていただきました。

#2 でも #3 でも返信がしやすい方で、お時間あるときにご回答いただけると助かります。(#3 の Description にも同様のことを書いています)

Comment thread server.c
}
}
// Check if the event is for a client socket
else if ((socket_data = find_socket(&client_socket_manager, events[i].data.fd)) != NULL)
Copy link
Copy Markdown
Owner Author

@thonda28 thonda28 Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

提案ありがとうございます。epoll_event->data->ptr の存在に気づいていなかったですが、たしかにここを活用できますね。SocketData のポインタをもたせることで socket_manager を線形探索せずに SocketData を取得できるよう #3 で修正してみました。イメージ合っていそうでしょうか?

ただ削除時の実装まではできていないです。(断念しました)
実装するとすると、SocketManager では現状配列で SocketData を管理していますが、ここをハッシュテーブルにして、キーを fd に、バリューに SocketData のポインタを持たせるとかですかね?

Comment thread server.c
}

struct epoll_event event;
event.events = EPOLLIN | EPOLLOUT;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここでおっしゃってるのは、そもそも EPOLLOUT を受け取らないように修正する、ということですかね?(#3 では送り返すデータが存在しなければ処理を終えるように修正しました)
もし上記の意図だとすると、バッファの状況を見ながら epoll_event の監視をやめたり始めたりとかそんな感じでしょうか?

Comment thread server.c
{
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
{
continue;
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここちょっとわからなかったです。
EWOULDBLOCK の場合にも書き込み自体は発生していて、パイプのバッファサイズ上限に達するまで書き込み続けてしまう、みたいな感じでしょうか。EWOULDBLOCK が起きるケースのイメージがあまりついておらず、どんな動きになるかがわかっていないです。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants