ソケットとファイルの送受信が可能なC/C++言語を使用して、Linux上で実行されるクライアント/サーバーアーキテクチャを実装したいと思います。このタスクを簡単にするライブラリはありますか?誰でも例を提供できますか?
最も移植性の高いソリューションは、ファイルをチャンク単位で読み取り、ループ内でソケットにデータを書き込むことです(同様に、ファイルを受信する場合も同様です)。バッファ、 read
をそのバッファに割り当て、 write
をそのバッファからソケットに割り当てます(send
およびrecv
は、ソケット固有のデータの書き込みおよび読み取り方法です)。アウトラインは次のようになります。
while (1) {
// Read data into buffer. We may not have enough to fill up buffer, so we
// store how many bytes were actually read in bytes_read.
int bytes_read = read(input_file, buffer, sizeof(buffer));
if (bytes_read == 0) // We're done reading from the file
break;
if (bytes_read < 0) {
// handle errors
}
// You need a loop for the write, because not all of the data may be written
// in one call; write will return how many bytes were written. p keeps
// track of where in the buffer we are, while we decrement bytes_read
// to keep track of how many bytes are left to write.
void *p = buffer;
while (bytes_read > 0) {
int bytes_written = write(output_socket, p, bytes_read);
if (bytes_written <= 0) {
// handle errors
}
bytes_read -= bytes_written;
p += bytes_written;
}
}
特にエラーを処理する場合は、read
およびwrite
のドキュメントを注意深く読んでください。一部のエラーコードは、たとえばcontinue
ステートメントでもう一度ループするなど、再試行する必要があることを意味しますが、他のエラーコードは何かが壊れているため停止する必要があることを意味します。
ファイルをソケットに送信するために、システムコール sendfile
があります。あるファイル記述子から別のファイル記述子にファイルを送信するようにカーネルに指示すると、カーネルが残りを処理できます。ソースファイル記述子はmmap
(たとえば、ソケットではなく実際のファイルである)をサポートする必要があり、宛先はソケットである必要があるという注意事項があります(したがって、ファイルをコピーするために使用できません。または、1つのソケットから別のソケットに直接データを送信します);ファイルをソケットに送信するという、あなたが説明する使用法をサポートするように設計されています。ただし、ファイルの受信には役立ちません。そのためには、ループを自分で行う必要があります。 sendfile
呼び出しがあるのはわかりませんが、類似のrecvfile
呼び出しはありません。
sendfile
はLinux固有であることに注意してください。他のシステムには移植できません。他のシステムには、独自のバージョンのsendfile
が頻繁にありますが、正確なインターフェースは異なる場合があります( FreeBSD 、 Mac OS X 、 Solaris )。
Linux 2.6.17では、 splice
システムコールは 導入済み でしたが、2.6.23では sendfile
。 splice
は、sendfile
よりも汎用的なAPIです。 splice
およびtee
の適切な説明については、かなり良い Linus自身の説明 を参照してください。彼は、splice
の使用が基本的に上記のループと同じであり、read
とwrite
を使用する方法を指摘していますが、バッファはカーネル内にあるため、データにはありませんカーネルとユーザー空間の間で転送されるか、CPUを通過することさえありません(「ゼロコピーI/O」として知られています)。
Aman 2 sendfile
。クライアントでソースファイルを開き、サーバーで宛先ファイルを開くだけで、sendfileを呼び出すと、カーネルはデータを切り刻んで移動します。
最小実行可能POSIX read
+ write
の例
使用法:
[〜#〜] lan [〜#〜] で2台のコンピューターを取得します。
たとえば、ほとんどの場合、両方のコンピューターがホームルーターに接続されている場合、これは機能します。これが私がテストした方法です。
サーバーコンピューターで:
ifconfig
を使用してサーバーのローカルIPを見つけます。 192.168.0.10
実行:
./server output.tmp 12345
クライアントコンピューターで:
printf 'ab\ncd\n' > input.tmp
./client input.tmp 192.168.0.10 12345
結果:output.tmp
を含むサーバーコンピューター上に'ab\ncd\n'
ファイルが作成されます!
server.c
/*
Receive a file over a socket.
Saves it to output.tmp by default.
Interface:
./executable [<output_file> [<port>]]
Defaults:
- output_file: output.tmp
- port: 12345
*/
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
char *file_path = "output.tmp";
char buffer[BUFSIZ];
char protoname[] = "tcp";
int client_sockfd;
int enable = 1;
int filefd;
int i;
int server_sockfd;
socklen_t client_len;
ssize_t read_return;
struct protoent *protoent;
struct sockaddr_in client_address, server_address;
unsigned short server_port = 12345u;
if (argc > 1) {
file_path = argv[1];
if (argc > 2) {
server_port = strtol(argv[2], NULL, 10);
}
}
/* Create a socket and listen to it.. */
protoent = getprotobyname(protoname);
if (protoent == NULL) {
perror("getprotobyname");
exit(EXIT_FAILURE);
}
server_sockfd = socket(
AF_INET,
SOCK_STREAM,
protoent->p_proto
);
if (server_sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
if (setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) < 0) {
perror("setsockopt(SO_REUSEADDR) failed");
exit(EXIT_FAILURE);
}
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = htonl(INADDR_ANY);
server_address.sin_port = htons(server_port);
if (bind(
server_sockfd,
(struct sockaddr*)&server_address,
sizeof(server_address)
) == -1
) {
perror("bind");
exit(EXIT_FAILURE);
}
if (listen(server_sockfd, 5) == -1) {
perror("listen");
exit(EXIT_FAILURE);
}
fprintf(stderr, "listening on port %d\n", server_port);
while (1) {
client_len = sizeof(client_address);
puts("waiting for client");
client_sockfd = accept(
server_sockfd,
(struct sockaddr*)&client_address,
&client_len
);
filefd = open(file_path,
O_WRONLY | O_CREAT | O_TRUNC,
S_IRUSR | S_IWUSR);
if (filefd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
do {
read_return = read(client_sockfd, buffer, BUFSIZ);
if (read_return == -1) {
perror("read");
exit(EXIT_FAILURE);
}
if (write(filefd, buffer, read_return) == -1) {
perror("write");
exit(EXIT_FAILURE);
}
} while (read_return > 0);
close(filefd);
close(client_sockfd);
}
return EXIT_SUCCESS;
}
client.c
/*
Send a file over a socket.
Interface:
./executable [<input_path> [<sever_hostname> [<port>]]]
Defaults:
- input_path: input.tmp
- server_hostname: 127.0.0.1
- port: 12345
*/
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <netdb.h> /* getprotobyname */
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
int main(int argc, char **argv) {
char protoname[] = "tcp";
struct protoent *protoent;
char *file_path = "input.tmp";
char *server_hostname = "127.0.0.1";
char *server_reply = NULL;
char *user_input = NULL;
char buffer[BUFSIZ];
in_addr_t in_addr;
in_addr_t server_addr;
int filefd;
int sockfd;
ssize_t i;
ssize_t read_return;
struct hostent *hostent;
struct sockaddr_in sockaddr_in;
unsigned short server_port = 12345;
if (argc > 1) {
file_path = argv[1];
if (argc > 2) {
server_hostname = argv[2];
if (argc > 3) {
server_port = strtol(argv[3], NULL, 10);
}
}
}
filefd = open(file_path, O_RDONLY);
if (filefd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
/* Get socket. */
protoent = getprotobyname(protoname);
if (protoent == NULL) {
perror("getprotobyname");
exit(EXIT_FAILURE);
}
sockfd = socket(AF_INET, SOCK_STREAM, protoent->p_proto);
if (sockfd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* Prepare sockaddr_in. */
hostent = gethostbyname(server_hostname);
if (hostent == NULL) {
fprintf(stderr, "error: gethostbyname(\"%s\")\n", server_hostname);
exit(EXIT_FAILURE);
}
in_addr = inet_addr(inet_ntoa(*(struct in_addr*)*(hostent->h_addr_list)));
if (in_addr == (in_addr_t)-1) {
fprintf(stderr, "error: inet_addr(\"%s\")\n", *(hostent->h_addr_list));
exit(EXIT_FAILURE);
}
sockaddr_in.sin_addr.s_addr = in_addr;
sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(server_port);
/* Do the actual connection. */
if (connect(sockfd, (struct sockaddr*)&sockaddr_in, sizeof(sockaddr_in)) == -1) {
perror("connect");
return EXIT_FAILURE;
}
while (1) {
read_return = read(filefd, buffer, BUFSIZ);
if (read_return == 0)
break;
if (read_return == -1) {
perror("read");
exit(EXIT_FAILURE);
}
/* TODO use write loop: https://stackoverflow.com/questions/24259640/writing-a-full-buffer-using-write-system-call */
if (write(sockfd, buffer, read_return) == -1) {
perror("write");
exit(EXIT_FAILURE);
}
}
free(user_input);
free(server_reply);
close(filefd);
exit(EXIT_SUCCESS);
}
さらなるコメント
可能な改善:
現在、output.tmp
は送信が完了するたびに上書きされます。
これは、複数のファイルをアップロードできるようにファイル名を渡すことができる単純なプロトコルの作成を請います。もちろん、それは path transversal脆弱性 を避けるために衛生を必要とします。
あるいは、ファイルをハッシュしてファイル名を見つけるサーバーを作成し、元のパスからディスク(データベース上の)ハッシュへのマップを保持することもできます。
一度に接続できるクライアントは1つだけです。
これは、接続が長時間続く低速クライアントがある場合に特に有害です。低速接続は全員を停止させます。
これを回避する1つの方法は、accept
ごとにプロセス/スレッドをフォークし、すぐにリッスンを再開し、ファイルでファイルロック同期を使用することです。
タイムアウトを追加し、時間がかかりすぎる場合はクライアントを閉じます。または、DoSを実行するのは簡単です。
poll
またはselect
はいくつかのオプションです: 読み取り関数呼び出しでタイムアウトを実装する方法?
単純なHTTP wget
実装を以下に示します。 libcurlを使用せずにCでHTTP get要求を作成する方法
Ubuntu 15.10。でテスト済み。
このファイルは、適切なsendfile
の例として役立ちます。 http://tldp.org/LDP/LGNET/91/misc/tranter/server.c.txt