Skip to content
Open
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
# socket-programming
CSZAP2
## 概要
echoサーバーを実装しています。

## 要件
- C言語で実装
- TCPで通信
- ちゃんとエラーハンドリングをする
- 余裕があれば、
- スレッドを使って高速化
- シグナルでサーバ停止

## 使い方
サーバーの起動:
```terminal
./server <port>
```
クライアントの起動:
```terminal
./client <ip> <port>
```
88 changes: 88 additions & 0 deletions client.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include <stdio.h>
Copy link

Choose a reason for hiding this comment

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

#include の順番に何か規則を持たせたい。 (<> -> "" の順番で、アルファベット順など)

#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>

#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 <hostname> <port>\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) {
Copy link

Choose a reason for hiding this comment

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

入力の終了に対応してください (EOF -- 一般的には、端末で Ctrl-D を押した場合)

Copy link
Owner Author

@TORUS0818 TORUS0818 Oct 9, 2024

Choose a reason for hiding this comment

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

以下で修正しました。
fc9b5be

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';
Copy link

Choose a reason for hiding this comment

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

受け取ったデータがバッファ全部に詰まっていた際に、これはバッファの次のメモリ (out of bounds) になってしまう。

printf("received from server: %s\n", receive_buffer);
}
close(s);

return EXIT_SUCCESS;
}
46 changes: 46 additions & 0 deletions common.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>

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);
Copy link

Choose a reason for hiding this comment

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

もし失敗時に exit するなら、 send_all_or_die みたいな名前にしたい。

}
if (n == 0) {
printf("end of input detected (EOF).\n");
return sent_total;
Copy link

Choose a reason for hiding this comment

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

send_all なので、 paritial に終わらせたくない。
(partial な時に exit を呼ぶならエラー時と同じ挙動でまだ理解しやすいかも)

}
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) {
Copy link

Choose a reason for hiding this comment

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

receive_all と書いてあるので、データの内容については触れたくない。

break;
}
}
return received_total;
}
6 changes: 6 additions & 0 deletions common.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

#include <sys/types.h>

ssize_t send_all(int s, char *buf, size_t len);
ssize_t receive_all(int s, char *buf, size_t len);
130 changes: 130 additions & 0 deletions docs/memo.md
Original file line number Diff line number Diff line change
@@ -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 <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

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 <sys/types.h>
#include <sys/socket.h>

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 <sys/types.h>
#include <sys/socket.h>

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 <sys/types.h>
#include <sys/socket.h>

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 <sys/types.h>
#include <sys/socket.h>

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```と異なり、ソケットディスクリプタを閉じるわけではないことに注意

# 用語集
97 changes: 97 additions & 0 deletions server.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>

#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 <port>\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);
Copy link

Choose a reason for hiding this comment

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

receive_all はエラー時に exit するけれど、
client の一つが通信エラー発生しただけでサーバーを終了させたくない。

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;
}