質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

Q&A

解決済

2回答

3046閲覧

C言語のソケット間通信で受け渡されるデータ

lushimo

総合スコア1

C

C言語は、1972年にAT&Tベル研究所の、デニス・リッチーが主体となって作成したプログラミング言語です。 B言語の後継言語として開発されたことからC言語と命名。そのため、表記法などはB言語やALGOLに近いとされています。 Cの拡張版であるC++言語とともに、現在世界中でもっとも普及されているプログラミング言語です。

0グッド

0クリップ

投稿2020/08/10 15:11

編集2020/08/10 15:16

前提・実現したいこと

C言語の勉強のためにソケット間通信を使用したDBのようなアプリを作成しています。
クライアント実行時にテーブル名を引数として渡して、サーバー側が該当ファイルを検索しクライアント側へ返すというものです。

発生している問題・エラーメッセージ

サーバー側で取得した結果をクライアントでrecvするんですが第三引数のlenの長さによってエラーになります。
以下は後述するソースDBClient.cの22行目辺りです。
NG

int resultLen = recv(sock, result, 3000, 0); printf("%s\n", result); // a printf("%d", resultLen); // -1を返す

OK

int resultLen = recv(sock, result, 35, 0); printf("%s\n", result); // 指定したファイルの内容 printf("%d", resultLen); //35

質問させていただきたいことは
1.第三引数が大きくなりすぎるとエラーになってしまう理由
2.サーバー側から受け取るサイズがわからない状態でこの引数にどの値を指定するべきでしょうか
自分で思いつくのは事前にサイズを別のソケット通信で受け取っておくぐらいしか思い浮かびませんが他にいい方法はありますか

該当のソースコード

DBClient.c

C

1#include <stdio.h> 2#include <WinSock2.h> 3 4int main(int argc, char *argv[]) 5{ 6 WSADATA w; 7 struct sockaddr_in server; 8 char buf[256]; 9 char *result; 10 11 WSAStartup(MAKEWORD(2, 0), &w); 12 13 int sock = socket(AF_INET, SOCK_STREAM, 0); 14 server.sin_addr.s_addr = inet_addr("127.0.0.1"); 15 server.sin_port = htons(9999); 16 server.sin_family = AF_INET; 17 18 strcpy(buf, argv[1]); 19 20 connect(sock, (struct sockaddr *)&server, sizeof(server)); 21 send(sock, buf, sizeof(buf), 0); 22 int resultLen = recv(sock, result, 3000, 0); 23 printf("%s\n", result); 24 printf("%d", resultLen); 25 26 closesocket(sock); 27 WSACleanup; 28 29 return 0; 30}

DBServer.c

C

1#include <stdio.h> 2#include <WinSock2.h> 3#include "tableSelect.h" 4 5int main() 6{ 7 WSADATA w; 8 struct sockaddr_in server, client; 9 char buf[20]; 10 char *result; 11 12 WSAStartup(MAKEWORD(2, 0), &w); 13 int sock = socket(AF_INET, SOCK_STREAM, 0); 14 server.sin_addr.s_addr = INADDR_ANY; 15 server.sin_port = htons(9999); 16 server.sin_family = AF_INET; 17 18 bind(sock, (struct sockaddr *)&server, sizeof(server)); 19 listen(sock, 1); 20 int len = sizeof(client); 21 int sock2 = accept(sock, (struct sockaddr *)&client, &len); 22 23 recv(sock2, buf, sizeof(buf), MSG_PEEK); 24 result = tableSelect(buf); 25 printf("%s", result); 26 send(sock2, result, 3000, 0); 27 28 closesocket(sock); 29 closesocket(sock2); 30 31 WSACleanup(); 32 33 return 0; 34}

tableSelect.c

C

1#include <stdio.h> 2#include <string.h> 3#include "tableSelect.h" 4 5char *tableSelect(char tblName[20]) 6{ 7 static char result[3000]; 8 FILE *fp; 9 char filePath[256]; 10 int chr; 11 int i = 0; 12 sprintf(filePath, "data/%s/%s.csv", tblName, tblName); 13 14 fp = fopen(filePath, "r"); 15 while ((chr = fgetc(fp)) != EOF) 16 { 17 result[i] = chr; 18 i++; 19 } 20 21 fclose(fp); 22 return result; 23}

実行に成功した時のイメージ

D:\C practice\C_Database\Client>client.exe T_USER UserId,Name,deleteFLag 1,Shimo,0 3000

試したこと

Client側のresultをポインタではなくてchar型配列(char result[3000])を使用すれば第三引数が3000であってもファイルの内容がクライアントへ渡せることは確認できました。
ただファイルの内容は大きいので複数箇所でサイズの大きいメモリを作るのは良くないかと思いポインタで解決したいと思っています。

補足情報(FW/ツールのバージョンなど)

Windows 10 64bit
gcc 9.2.0

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

segavvy

2020/08/10 15:26

エラーの内容を明記いただくと、回答が付きやすくなるかと思います。 あと、パッと見ですが、DBClient.cのchar *resultに、有効なバッファのポインタが格納されていないように見えます。事前にmallocなどでバッファの確保が必要なのではないでしょうか?
lushimo

2020/08/10 15:49

アドバイスありがとうございます。エラー内容は出力方法が分からなかったのですがperror使ってみたところ、「No error」と出ました。以下実行結果です。 D:\C practice\C_Database2\Client>client.exe T_USER error message: No error // perror("error message"); a // printf("%s\n", result); -1 // printf("%d", resultLen); バッファのポインタ確保については調べて試してみます。
dodox86

2020/08/10 16:43

segavvyさんのご指摘「DBClient.cのchar *resultに、有効なバッファのポインタが格納されていない」を最初に直さないと、まずまともに動かないと思いますよ。不正なアドレスの領域にアクセスして、受信データでもって領域を破壊しているはずです。
cateye

2020/08/10 17:10 編集

>ポインタで解決したい・・・ポインタだけあっても意味がありません。 malloc()で領域確保しましょう。今どきのマシンなら、1MB程度問題無いと思いますよ。 ・・・使い終わったら、開放を忘れないように・・・
dodox86

2020/08/10 17:17

私の[2020/08/11 01:43]のコメントより > 不正なアドレスの領域にアクセスして、受信データでもって領域を破壊しているはず まぁ、破壊する前に、recv内で不正アドレスを検知できていてエラー(-1)で返しているのだと思いますけど。SHOMIさんのご指摘のように、WSAGetLastError()で確認してください。 ※ https://docs.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-recv のReturn valueの項を参照
lushimo

2020/08/12 15:22

すみません、遅くなりました。WSAGetLastErrorでエラー番号を調べたところ10014、不正なアドレスというエラーでした。皆さんご指摘の通りmallocでメモリの確保を行うとクライアント側でデータの受け取りができることを確認できました。 ここで疑問なのは 1.recvの第2引数のresultにメモリを確保していないことが原因だったのでしたら、その状態でも第三引数が35であればデータの受け取りができたのは何故でしょうか? 発生している問題・エラーメッセージのOKの結果です。これはresultに何も設定していない状態でもこの結果になりました。 2.resultに確保すべきメモリはいくつ必要になりますか? ポインタ変数なので4でいいのか、それとも返ってくるファイルの内容のサイズ分確保すべきなのでしょうか?試してみたところ4でも3000でも1でも0でもデータの受け取りができておりますので、何が正解なのかわからなくなってしまいました。
guest

回答2

0

ベストアンサー

「質問への追記・修正欄」における質問者lushimoさんの[2020/08/13 00:22]のコメントより引用:

1.recvの第2引数のresultにメモリを確保していないことが原因だったのでしたら、その状態でも第三引数が35であればデータの受け取りができたのは何故でしょうか?

第3引数が35でもデータ受け取りができたのは、たまたまです。resultはこの場合char *型のポインタだったわけですが、その値がたまたま有効なアドレスを示していて、その領域が35バイト以上有効であれば、recv()は実行できます。ですが、たまたま有効なアドレスだったに過ぎないその領域は、その受信したデータで元のデータが上書きされ、壊されているかもしれません。

2.resultに確保すべきメモリはいくつ必要になりますか?
ポインタ変数なので4でいいのか、それとも返ってくるファイルの内容のサイズ分確保すべきなのでしょうか?試してみたところ4でも3000でも1でも0でもデータの受け取りができておりますので、

確保すべきサイズは「1回のrecv()実行で受信可能とするデータの最大のサイズ」です。例えばもし最終的に受信したいデータの総サイズが1万バイトだったとしても、小分けに10バイトづつ受信するのであればrecv()に渡すバッファのサイズは10バイトで構いません。プログラマーが責任をもってその小分けされたデータを連結します。言い方を変えるとその前後のプログラミング次第です。

ソケットプログラミング、特にTCPの前提として、相手側(質問者さんの場合はサーバー)が1回のsend()で1万バイトを送っても、1回のrecv()で1万バイト受信できるとは限りません。場合によっては1000バイトづつ小分けにされて受信する場合もありますし、そうなることを見越してコードを書く必要があります。サーバー側で合計何バイト送ってくるか分からない場合は、そのデータの中にデータの末端を示すマークを含めて送るとか、データ長自体を示すデータを含めます。TCPの代表的なHTTPでも、サーバーからクライアントに送られてくるHTTPレスポンスにはデータの末端を示す2つの連続する空行("\r\n\r\n")であるとか、データ部の長さを示す"Content-Length: 1234\r\n"のようなヘッダーフィールドがあります。クライアント側はそれらを適時判断して、受信すべき総データ長を決めることになります。それは質問者さんのコードでも同様です。

尚、ソケットプログラミングのレベルでいえばrecvの返り値に0が返ってきたときはコネクションが切断されたことを示します。それ以降データは受信できないので、それをもって終了とする場合もあります。ソケットプログラミングのサンプルコードでもそのようなコードになっていることが多いので、気を付けて確認してみてください。

上記をある程度踏まえて質問者さんのコードを修正してみたのが以下のコードです。コメントのNOTE:部分にも注意して読んでみてください。(私の方ではMinGW/gccではなく、Visual Studio 2019 で確認しています)

C

1#include <stdio.h> 2#include <WinSock2.h> 3 4int main(int argc, char* argv[]) 5{ 6 WSADATA w; 7 struct sockaddr_in server; 8 char buf[256]; 9 char* result; 10 11 WSAStartup(MAKEWORD(2, 0), &w); 12 13 int sock = socket(AF_INET, SOCK_STREAM, 0); 14 server.sin_addr.s_addr = inet_addr("127.0.0.1"); 15 server.sin_port = htons(9999); 16 server.sin_family = AF_INET; 17 18 strcpy(buf, argv[1]); 19 20 connect(sock, (struct sockaddr*)&server, sizeof(server)); 21 22 /* NOTE: sizeof(buf)だと不要部分のゴミまで送ってしまうので、有効なデータの長さとする */ 23 size_t send_len = strlen(buf); 24 //send(sock, buf, sizeof(buf), 0); 25 send(sock, buf, send_len, 0); 26 27 /* NOTE: 28 * 未初期化のchar *resultを利用して recv(sock, result, 35, 0) が使えたのはたまたま。 29 * ゴミの値のresultでも何かしら使える領域のアドレスとなっていて、 30 * たまたまその領域が35バイト使えていたにすぎない。 31 */ 32 /* 33 int resultLen = recv(sock, result, 3000, 0); 34 printf("%s\n", result); 35 printf("%d", resultLen); 36 */ 37 38 /* NOTE: 細切れに受信しても大丈夫なように修正 */ 39 const int recv_len_max = 3000; 40 result = (char *)malloc(recv_len_max + 1); 41 int data_len = 0; /* 受信済データ長 */ 42 43 for (;;) { 44 /* 空き容量を算出 */ 45 int rest_len = recv_len_max - data_len; 46 if (rest_len <= 0) { 47 /* 空き無し */ 48 break; 49 } 50 51 /* 格納先の位置を求める */ 52 char* recv_top = result + data_len; 53 int recv_len = recv(sock, recv_top, rest_len, 0); 54 if (recv_len > 0) { 55 /* 受信した */ 56 data_len += recv_len; 57 printf("今回の受信データバイト長=%d, 合計=%d\n", recv_len, data_len); 58 } 59 else if (recv_len == 0) { 60 printf("コネクションが切断\n"); 61 break; 62 } 63 else { 64 int err = WSAGetLastError(); 65 printf("WSAGetLastError=%d\n", err); 66 break; 67 } 68 } 69 70 /* 末端に '\0' を挿入 */ 71 *(result + data_len) = '\0'; 72 printf("受信データ=[%s]\n", result); 73 74 free(result); 75 76 closesocket(sock); 77 78 // NOTE: カッコが抜けてました。 79 // WSACleanup; 80 81 WSACleanup(); 82 83 return 0; 84}

投稿2020/08/12 18:46

編集2020/08/12 19:07
dodox86

総合スコア9183

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

rubato6809

2020/08/13 08:48 編集

for (;;) ループ中の recv() 後、 break やら else やらがゴチャゴチャしているのに引っかかったので。 break の所はループを脱出することに専念し、エラー表示はループを抜けた後で十分だと思います。また、そうしたほうがループ中でやるべきことを明確に示せると思います。
dodox86

2020/08/13 09:01

ご指摘どうもありがとうございます。低評価の理由、と言うことでよろしいでしょうか。エラー出力の位置に関しては、最終的なコードの整然さよりもサンプルによくある冗長さを優先させているつもりですので、このままとさせていただこうと思います。recv_len==0でもrecv_len < 0 でも結局break している違和感は理解しています。そこは質問者さんで判断していただいて、最終的な製品レベルのコードにしていただければと思っています。それを踏まえた上で見本とすべきサンプルで悪いコードをわざわざ提示することは無いだろう、と言うのであれば、低評価も止む無しです。
lushimo

2020/08/13 13:08 編集

ソースコードまで提示していただきありがとうございます。頂いた解説と合わせてよく読んで参考にさせていただきます。 すみません、一点だけ確認なんですが size_t send_len = strlen(buf) ; こちら終端文字?を含めて +1 するのが正解ですかね? このままだとサーバー側に渡される文字に余計な文字が付加されてしまいました
dodox86

2020/08/13 16:21

> こちら終端文字?を含めて +1 するのが正解ですかね? サーバー側で終端文字'\0'を付与しないのであれば、そうですね。確かに今のサーバー側のコードDBServer.cを読むと、クライアント側からは終端文字'\0'まで送られてくることを期待するコードになっていました。サンプルの意図としては、send(sock, buf, sizeof(buf), 0); で、不要なデータまで送る必要はないだろうとの思いで修正したものでした。すみませんが質問者さんのサーバー、クライアント間通信インターフェースに合わせて修正してください。 > このままだとサーバー側に渡される文字に余計な文字が付加されてしまいました クライアント側が終端の'\0'を送っていない為に、サーバー側での受信データを扱うときに、ゴミ(つまり余計な文字)まで使ってしまっている、ということでしょう。「終端の'\0'まで送信側が送る」と通信インターフェース仕様、つまりプロトコルを決めたのであれば、「’\0'の受信をもってデータの終端とする」と捉えることもできます。
dodox86

2020/08/13 16:34

> 「’\0'の受信をもってデータの終端とする」と捉えることもできます。 本来、0x00を含むバイナリーデータをsendできることを考えると、ちょっと頼りない仕様に思えなくもないですが、その辺りは適時、ご検討ください。
guest

0

recvの第3引数は受信したデータを格納するメモリの先頭アドレスを渡す必要があります。データが格納されたメモリの先頭アドレスを格納するわけではありません。なのでchar[3000]を渡した時は成功し、不定アドレスを指しているポインタではエラーになります。

gccをお使いのようですからerrno.hをincludeしてerrnoの値を確認すればエラーの内容を知ることができます。strerror()を併用するとより分かりやすいのでオススメです。

ポインタによる解決を望まれるなら既に解答例が他の方から示されているので参考にしてください。

投稿2020/08/10 17:03

Shuty_dd

総合スコア4

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問