RubyのTCPサーバとCのクライアントで通信するときに苦戦したのでメモ。
結果的にRubyとCがstreamに対して送受信する見かけのデータの量の単位が違うことが問題でした。そこでRubyと同じ単位で通信できるようにCのwriteとreadをラップしたsputs関数とsgets関数を実装することで対応しました。
環境は
- ubuntu 16.04 LTS
- ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]
- gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609
です。
Rubyのサーバ
通信を開始してクライアントから受け取った文字列をほぼそのまま返却するサーバです。
これを5回繰り返して終了です。
require 'socket'
# ポート20000番でサーバを起動
server = TCPServer.open(20000)
# 通信を受け付ける
sock = server.accept
5.times do
# バッファから1行受け取る。(改行まで受け取る)
line = sock.gets.chomp
# コンソールに表示。pだとnullも\x00として表示してくれる。
p line
# 返却。末尾に\nが追加される。
sock.puts("you sent <<<#{line}>>>.")
end
# 通信を切断
sock.close
Cのクライアント
rubyのgetsとputsに対応して、sputsとsgets関数を実装します。
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
# include <sys/fcntl.h>
# include <sys/types.h>
# include <sys/socket.h>
# include <netinet/in.h>
# include <netdb.h>
# define RECV_SIZE (10000)
# define SEND_SIZE (10000)
/*
* バッファから1行取り出します。(get string from stream)
* バッファに1行分のデータがない場合は待機します。
*/
char* sgets(int sd, char* line) {
// 前回改行までに取り出した部分
static char* read_buf = NULL;
if (read_buf == NULL) {
read_buf = malloc(sizeof(char) * RECV_SIZE);
read_buf[0] = '\0';
}
while (1) {
int e;
if (strlen(read_buf) != (e = strcspn(read_buf, "\n"))) {
// lineを初期化
memset(line, '\0', sizeof(char) * strlen(line));
// 1行分をlineへコピー
strncpy(line, read_buf, (e + 1) - 0);
// 次の行をread_bufの先頭からコピー
strcpy(read_buf, strchr(read_buf, '\n') + 1);
break;
}
// 初期化して受け取り用の配列を用意
char r[RECV_SIZE] = { 0 };
// バッファから今送信された分のcharを受け取り。(末尾にNULLが付加されない)
if (read(sd, r, sizeof(r) * RECV_SIZE) < 0) {
perror("recv");
fflush(0);
return NULL;
}
// 読みだしたデータをためる。
strcat(read_buf, r);
}
return line;
}
/*
* 1行送信します。(put string to stream)
* 末尾に"\n"を付加します。
*/
void sputs(int sd, char* str) {
char* send_str = malloc(sizeof(char) *(strlen(str) + 2));
memset(send_str, '\0', sizeof(char) *(strlen(str) + 2));
strcat(send_str, str);
send_str[strlen(str)] = '\n';
if (write(sd, send_str, sizeof(char) * strlen(send_str)) < 0) {
perror("send");
return;
}
free(send_str);
}
int main(int argc, char *argv[]) {
int sd; //ソケット作成用の変数
struct sockaddr_in addr; //サーバ接続用の変数
char *recv[sizeof(char) * RECV_SIZE] = {0};
// IPv4 TCP のソケットを作成する
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return -1;
}
// 送信先アドレスとポート番号を設定する
addr.sin_family = AF_INET;
addr.sin_port = htons(20000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// サーバ接続(TCP の場合は、接続を確立する必要がある)
connect(sd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
char* strs[5] = {"abcde", "fg", "hijklmn", "opqrs", "tuvwxyz"};
for(int i = 0; i < 5; i++) {
// 1行送信。末尾に\nが付加される。
sputs(sd, strs[i]);
// 1行取得。
sgets(sd, recv);
// 表示
printf("I recived <<<%s>>>\n", recv);
}
// ソケットを閉じる
close(sd);
return 0;
}
sgets内で使われているread関数は注意が必要です。read(sd, r, sizeof(r) * RECV_SIZE)
でr
にバッファの内容が書き込まれる訳ですが、read
関数は文字列の終わりを切り良く受け渡すのではなく、1バイトごとに受け取ったところまでを書き込みます。また、書き込み終わった最後にnullを付加することもありません。そのため、rは毎回nullで満たしておくことをおすすめします。
また、sputs内で使われているwrite関数も注意が必要です。write(sd, send_str, sizeof(char) * strlen(send_str)
で第3引数を多めにとってSEND_SIZE
とすると、第2引数の文字列の長さにかかわらず、SEND_SIZEバイト分を送られてしまいます。足りない分はNULLで埋めるようです。
ちなみにうまく動かない実装は以下です。
int main(int argc, char *argv[]) {
int sd; //ソケット作成用の変数
struct sockaddr_in addr; //サーバ接続用の変数
char *recv[sizeof(char) * RECV_SIZE] = {0};
// IPv4 TCP のソケットを作成する
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket");
return -1;
}
// 送信先アドレスとポート番号を設定する
addr.sin_family = AF_INET;
addr.sin_port = htons(20000);
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// サーバ接続(TCP の場合は、接続を確立する必要がある)
connect(sd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in));
char* strs[5] = {"abcde", "fg", "hijklmn", "opqrs", "tuvwxyz"};
for(int i = 0; i < 5; i++) {
// 1行送信。
write(sd, strs[i], SEND_SIZE);
// 1行取得。
read(sd, recv, RECV_SIZE);
// 表示
printf("I recived <<<%s>>>\n", recv);
}
// ソケットを閉じる
close(sd);
return 0;
}
Rubyと同じgetsとputsの感覚でreadとwriteを書くと失敗します。
実行する
- サーバの起動
$ ruby server.rb
- クライアントの起動
$ gcc -O2 -o client client.c
$ ./clinet
- 実行結果
"abcde\n"
"fg\n"
"hijklmn\n"
"opqrs\n"
"tuvwxyz\n"
I recived <<<you sent <<<abcde>>>.
>>>
I recived <<<you sent <<<fg>>>.
>>>
I recived <<<you sent <<<hijklmn>>>.
>>>
I recived <<<you sent <<<opqrs>>>.
>>>
I recived <<<you sent <<<tuvwxyz>>>.
>>>
CではRubyのように良い感じに文字列に変換してくれたりしないので、バイト単位の処理に注意しなくてはいけませんでした。おそらく他言語同士でTCP通信をすると通信の扱いの違いでいろいろ問題が起こりそうです。