diff --git a/README.md b/README.md index 23ec05f..46d075e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# socket-programming -CSZAP2 +## 概要 +echoサーバーを実装しています。 + +## 要件 +- C言語で実装 +- TCPで通信 +- ちゃんとエラーハンドリングをする +- 余裕があれば、 + - スレッドを使って高速化 + - シグナルでサーバ停止 + +## 使い方 +サーバーの起動: +```terminal +./server +``` +クライアントの起動: +```terminal +./client +``` diff --git a/client.c b/client.c new file mode 100644 index 0000000..0f011a2 --- /dev/null +++ b/client.c @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +#define BUFFER_SIZE 1024 + +int main(int argc, char *argv[]) { + int s; + int status; + char *node_name; + char *service_name; + struct addrinfo hints; + struct addrinfo *ai0; + struct addrinfo *ai; + int receive_message_size; + char receive_buffer[BUFFER_SIZE]; + char send_buffer[BUFFER_SIZE]; + + if (argc != 3) { + fprintf(stderr, "usage: ./client \n"); + exit(EXIT_FAILURE); + } + node_name = argv[1]; + service_name = argv[2]; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + status = getaddrinfo(node_name, service_name, &hints, &ai0); + if (status) { + fprintf(stderr, "%s", gai_strerror(status)); + exit(EXIT_FAILURE); + } + + for (ai = ai0; ai; ai = ai->ai_next) { + // socket + s = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (s < 0) { + continue; + } + // connect + if (connect(s, ai->ai_addr, ai->ai_addrlen)) { + close(s); + s = -1; + continue; + } + break; + } + if (s < 0) { + fprintf(stderr, "cannot connect %s.\n", node_name); + exit(EXIT_FAILURE); + } + printf("connect to %s.\n", node_name); + freeaddrinfo(ai0); + + while(1) { + printf("please enter the characters: "); + if (fgets(send_buffer, BUFFER_SIZE, stdin) == NULL) { + if (feof(stdin)) { + printf("end of input detected (EOF).\n"); + break; + } else { + perror("error reading from stdin"); + exit(EXIT_FAILURE); + } + } + // send + send_all(s, send_buffer, strlen(send_buffer)); + // receive + receive_message_size = receive_all(s, receive_buffer, BUFFER_SIZE); + if (receive_message_size == 0) { + printf("server disconnected.\n"); + break; + } + receive_buffer[receive_message_size] = '\0'; + printf("received from server: %s\n", receive_buffer); + } + close(s); + + return EXIT_SUCCESS; +} diff --git a/common.c b/common.c new file mode 100644 index 0000000..952a127 --- /dev/null +++ b/common.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include + +ssize_t send_all(int s, char *buf, size_t len) { + int sent_total = 0; + int n; + + while (sent_total < len) { + n = send(s, buf + sent_total, len - sent_total, 0); + if (n == -1) { + perror("send() failed.\n"); + exit(EXIT_FAILURE); + } + if (n == 0) { + printf("end of input detected (EOF).\n"); + return sent_total; + } + sent_total += n; + } + return sent_total; +} + +ssize_t receive_all(int s, char *buf, size_t len) { + int received_total = 0; + int n; + + while (received_total < len) { + n = recv(s, buf + received_total, len - received_total, 0); + if (n == -1) { + perror("recv() failed.\n"); + exit(EXIT_FAILURE); + } + if (n == 0) { + printf("end of input detected (EOF).\n"); + return received_total; + } + received_total += n; + if (strchr(buf, '\n') != NULL) { + break; + } + } + return received_total; +} diff --git a/common.h b/common.h new file mode 100644 index 0000000..94cf5ec --- /dev/null +++ b/common.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +ssize_t send_all(int s, char *buf, size_t len); +ssize_t receive_all(int s, char *buf, size_t len); diff --git a/docs/memo.md b/docs/memo.md new file mode 100644 index 0000000..e2643cd --- /dev/null +++ b/docs/memo.md @@ -0,0 +1,130 @@ +調べたことを適宜メモする + +## 参考資料 +- https://beej.us/guide/bgnet/html/#sendrecv + +# Tips +## atoiとstrtol +- atoi + - https://man7.org/linux/man-pages/man3/atol.3.html +- strtol + - https://man7.org/linux/man-pages/man3/strtol.3.html +- 以下のような理由で、strtolを利用した方が良い + - https://www.gavo.t.u-tokyo.ac.jp/~dsk_saito/lecture/software2/misc/misc02.html + - 整数以外の文字列が入力された時にエラーを検知できない + - intの範囲を大きく超える整数の検出などもできない +- atoiに与える文字列が完全に管理できるなら、strtolより軽量(エラーチェックなし、10進数しか扱えない)なのでメリットもある + +## 3ハンドシェイクとソケット関数との対応 +| 3ウェイハンドシェイクのステップ | ソケット関数 | 説明 | +| ---- | ---- | ---- | +| 1. SYN(接続要求) | connect() | サーバーにSYNを送り、3ウェイハンドシェイクを開始 | +| 2. SYN-ACK(応答) | accept() | サーバーが接続要求に応答(SYN-ACK)し、接続を確立 | +| 3. ACK(確認応答、接続完了) | accept() | クライアントからACKを受信し、通信準備が整う | + +## 主要なシステムコール +### getaddrinfo +```c +#include +#include +#include + +int getaddrinfo(const char *node, // e.g. "www.example.com" or IP + const char *service, // e.g. "http" or port number + const struct addrinfo *hints, + struct addrinfo **res); +``` +- 関連情報を記載したaddrinfo構造体(hints)へのポインタを渡すことで、それを補完した完全な状態のaddrinfoのリンクドリストを作成してくれる +- エラーは```gai_strerror()```で拾う +- 使い終わったリンクドリストは```freeaddrinfo()```で解放する +- ```AI_PASSIVE```は、ローカルホストのアドレスを割り当てる設定 + +### socket +```c +#include +#include + +int socket(int domain, int type, int protocol); +``` +- ソケットディスクリプタを返す +- domain = PF_INET / PF_INET6 +- type = SOCK_STREAM / SOCK_DGRAM +- 以下のように```getaddrinfo```で取得した情報を使って呼び出す +```c +getaddrinfo("www.example.com", "http", &hints, &res); +s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); +``` + +### bind +```c +#include +#include + +int bind(int sockfd, struct sockaddr *my_addr, int addrlen); +``` +- ソケットをポートに関連付けする +- 1024以下のポートは予約されているので使わないこと +- ポートの再利用を許可するよう、以下のように設定できる +```c +int yes=1; +//char yes='1'; // Solaris people use this + +// lose the pesky "Address already in use" error message +if (setsockopt(listener,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof yes) == -1) { + perror("setsockopt"); + exit(1); +} +``` + +### connect +```c +#include +#include + +int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); +``` +- 特定のIP、ポートに接続する +- ローカルのポートは気にしないので```bind```は呼ばない(カーネルが設定してくれる) + +### listen +```c +int listen(int sockfd, int backlog); +``` +- 接続を待つ +- backlogは、着信キューで許可されるコネクション数 + - 着信してきたコネクションは、```accept()```するまでこのキューで待機することになる + +### accept +```c +#include +#include + +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); +``` +- 接続を許可する +- 新しいソケットファイルディスクリプタが返される(こちらで```send/recv```する) + +### send +```c +int send(int sockfd, const void *msg, int len, int flags); +``` +- 送信したいデータへのポインタとデータのバイト長を渡すことで、データを送信できる +- 実際に送信されたバイト長を返す + - ```len```と一致しない場合は一部しか送信できていないことを意味する + +### recv +```c +int recv(int sockfd, void *buf, int len, int flags); +``` +- ```buf```に送られてきたデータを読み込む +- 0が返ってきた場合は、リモート側が接続を閉じたことを意味する + +### close +```c +close(sockfd); +``` +- ソケットディスクリプタの接続を閉じる +- ```shutdown```を使うとより細かい制御(受信、送信、送受信の拒否)ができる + - ```close```と異なり、ソケットディスクリプタを閉じるわけではないことに注意 + +# 用語集 diff --git a/server.c b/server.c new file mode 100644 index 0000000..fa7d8b6 --- /dev/null +++ b/server.c @@ -0,0 +1,97 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" + +#define QUEUE_LIMIT 5 +#define BUFFER_SIZE 1024 + +int main(int argc, char *argv[]) { + int client_sd; + int server_sd; + int status; + char *service_name; + struct addrinfo hints; + struct addrinfo *ai0; + struct addrinfo *ai; + struct sockaddr_storage ss; + unsigned int ss_len; + int send_message_size; + int receive_message_size; + char receive_buffer[BUFFER_SIZE]; + + if (argc != 2) { + fprintf(stderr, "usage: ./server \n"); + exit(EXIT_FAILURE); + } + service_name = argv[1]; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = AI_PASSIVE; + + status = getaddrinfo(NULL, service_name, &hints, &ai0); + if (status) { + fprintf(stderr, "%s", gai_strerror(status)); + exit(EXIT_FAILURE); + } + + for (ai = ai0; ai; ai = ai->ai_next) { + // socket + server_sd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (server_sd < 0) { + continue; + } + // bind + if (bind(server_sd, ai->ai_addr, ai->ai_addrlen) < 0) { + perror("bind() failed.\n"); + close(server_sd); + continue; + } + //listen + if (listen(server_sd, QUEUE_LIMIT) < 0) { + perror("listen() failed.\n"); + close(server_sd); + continue; + } + break; + } + if (server_sd < 0) { + fprintf(stderr, "cannot create server socket.\n"); + exit(EXIT_FAILURE); + } + freeaddrinfo(ai0); + + while(1) { + // accept + if ((client_sd = accept(server_sd, (struct sockaddr*) &ss, &ss_len)) < 0) { + perror("accept() failed.\n"); + exit(EXIT_FAILURE); + } + + while(1) { + //recv + receive_message_size = receive_all(client_sd, receive_buffer, BUFFER_SIZE); + if (receive_message_size == 0) { + printf("connection has already closed.\n"); + break; + } + //send + send_message_size = send_all(client_sd, receive_buffer, receive_message_size); + if(send_message_size == 0) { + printf("connection has already closed.\n"); + break; + } + } + close(client_sd); + } + close(server_sd); + + return EXIT_SUCCESS; +}