前章ではソケットとTCP/UDPプロトコルについて説明しました。本章ではUDPプロトコルに基づくネットワークサービスをいくつか実装します。サーバーとクライアントの両方が必要です。
- サーバーの実装
1.1 socket関数
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
引数の説明:
- domain:ドメイン。通信タイプ(ネットワーク通信/ローカル通信)を示します。これはsockaddr構造体の最初の16ビットに該当します。ローカル通信の場合はAF_UNIX、ネットワーク通信の場合はAF_INETを使用します。
- type:ソケットが提供するサービスの種類。一般的にはSOCK_STREAMとSOCK_DGRAMがあります。TCPプロトコルを使用する通信ならSOCK_STREAMを、UDPプロトコルを使用する通信ならSOCK_DGRAMを指定します。
- protocol:ソケットの作成タイプ(TCP/UDP)。ただし、このパラメータは上記2つのパラメータで決定されるため、通常は0に設定します。
戻り値:
成功時はファイルディスクリプタを返し、失敗時は-1を返します。エラーコードは設定されます。
システムのファイル操作でもファイルディスクリプタを返しますが、普通のファイルのファイルバッファはディスクに対応しているのに対し、ソケットのファイルディスクリプタのバッファはネットワークカードに対応しています。ユーザーがデータをバッファに書き込むと、オペレーティングシステムが自動的にバッファからネットワークカードにデータを転送し、ネットワークカードがそのデータをリモートホストに送信します。
class UdpServer
{
public:
UdpServer()
{
// ソケットを作成
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket < 0)
{
std::cerr << "ソケット作成エラー" << std::endl;
exit(1);
}
std::cout << "ソケット作成成功、ソケット番号: " << _socket << std::endl;
}
~UdpServer()
{
close(_socket);
}
private:
int _socket;
};
socket関数を呼び出すことで、ソケットを作成できます。最初の引数はAF_INETとしてネットワーク通信を指定し、2番目の引数はSOCK_DGRAMとしてUDPサービス(データグラム)を指定します。プロセス起動時、標準入力、標準出力、標準エラーの3つのファイルディスクリプタが既に開かれています。ファイルディスクリプタの生成規則は0から順に未使用の番号を探して割り当てることなので、_socketの値は3になると考えられます。
1.2 bind関数
ソケットを作成した後はまだファイルを開いただけであり、ネットワークと関連付けられていないため、bind関数を使ってIP+ポートとファイルを結合する必要があります。
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
引数の説明:
- sockfd:socket関数の戻り値。
- addr:汎用構造体。プロトコルファミリー、IPアドレス、ポート番号を含みます。
- addrlen:addrの長さ。
戻り値:
成功時は0、失敗時は-1を返します。
sockaddr_in構造体
struct sockaddr_in
{
short int sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
- sin_family:プロトコルファミリー。通信タイプ(AF_INETネットワーク通信、PF_INETローカル通信)を示します。
- sin_port:ポート番号(ネットワークバイトオーダー)。
- sin_addr:IPアドレス。
- sin_zero:埋め込みフィールド。sizeof(sockaddr_in) = 16になるようにします。
次のステップではIPとポート番号を追加する必要があるため、コードを修正してUDPコンストラクタにポートと文字列(IPのドット区切り表現)を追加します。
class UdpServer
{
public:
UdpServer(const uint16_t &port, const std::string &str = "")
{
// ソケットを作成
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket < 0)
{
std::cerr << "ソケット作成エラー" << std::endl;
exit(1);
}
std::cout << "ソケット作成成功、ソケット番号: " << _socket << std::endl;
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(str.c_str());
int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
if (n < 0)
{
std::cout << "bindエラー" << std::endl;
exit(2);
}
std::cout << "bind成功" << std::endl;
}
~UdpServer()
{
close(_socket);
}
private:
int _socket;
};
sockaddr_inパラメータを設定する際には以下の点に注意してください:
- sin_familyはsocket関数で作成したドメインと一致させる必要があります。
- ポート番号とIPはネットワークに送信されるため、ネットワークデータはビッグエンディアン形式で統一されているため、コードの移植性を考慮してhton関数を使用してビッグエンディアンに変換します。
- IPについては、ドット区切り形式(例:192.168.12.80)を直接送信すると多くのバイトを消費するため、32ビット整数でIPを表現し、ネットワーク伝送時にはドット区切りのIPを整数に変換して送信します。
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp); // ドット区切りから整数へ変換
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in); // 整数からドット区切りへ変換
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);
ここではinet_addrとinet_ntoaの2つを重点的に学習します。
1.3 サーバーの実行
まず、サーバーは無限ループで動作し、起動後に終了しないことを理解する必要があります。
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
void Usage()
{
std::cout << "使用方法: ./UdpServer [ip] port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage();
return 3;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[argc - 1]);
std::unique_ptr<UdpServer> p(new UdpServer(port, ip));
p->Start();
return 0;
}
コマンドライン引数を使用して、プログラム起動時にIPとポート番号を渡すことができます。
1.4 IPのバインド
ifconfigはネットワークインターフェースパラメータを設定・表示するためのコマンドラインツールです。イーサネットやWi-Fiなどのネットワークインターフェースの詳細情報を表示・設定するために使用され、IPアドレス、サブネットマスク、ブロードキャストアドレス、MACアドレスなどが含まれます。
127.0.0.1はローカルループバックアドレスで、ローカルでのテストに使用されます。ローカルテストが成功すれば、ネットワークテストで問題が発生した場合、それはネットワークの問題である可能性が高いです。ローカルループバックとは、データがネットワークに送信されることなくローカル内で処理されることを意味します。
上記プログラムを実行すると、netstat関数を使用してネットワーク状況を確認できます。
- -aまたは--all:すべてのソケット接続を表示します。
- -lまたは--listening:監視中のサーバーソケットを表示します。
- -nまたは--numeric:ドメイン名ではなくIPアドレス(数値)を使用します。
- -pまたは--programs:使用中のソケットのプログラム識別子とプログラム名を表示します。
- -tまたは--tcp:TCP通信プロトコルの接続状況を表示します。
- -uまたは--udp:UDP通信プロトコルの接続状況を表示します。
実際に実行したプログラムがIPとポート番号をbindしたことが確認できます。
しかし実際には、サーバーは固定IPをバインドすることをお勧めしません。
- セキュリティ:攻撃者がこの固定IPに対して攻撃(DDoSなど)を行う可能性があり、動的IPや負荷分散技術を使用することで攻撃を分散し、システムの安全性を高めることができます。
- 利用可能性:特定の状況では固定IPが常に利用できないことがあります。クラウドサービス環境では、サーバー移行や再デプロイによりIPアドレスが変更されることがあります。固定IPにバインドしていた場合、IPアドレス変更時にサービスが停止する可能性があります。
- サーバーは複数のIPを持つ可能性があります(複数のネットワークカード)。クライアントがデータを送信すると、各ネットワークカードがデータを受信します。特定のIPを指定して8080ポートを受信したい場合、その指定されたIPのみがデータを受け取りますが、任意のIPにバインドすれば、8080ポートへの送信はどのネットワークカードでも受け取り、それをサーバーに渡します。
addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());
そのため修正します。サービス開始時にIPが渡された場合はそれを使い、渡されなかった場合はINADDR_ANY(つまり0.0.0.0)を使用します。バインド後は、このホスト上の任意のIPから送信された、ポート番号がXXX(ここでは8080)のデータはすべてこのプロセスに渡されます。
1.5 データの読み取りrecvfrom
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
引数の説明:
- sockfd:どのソケットから読み込むか。
- buf:読み取ったデータを格納するバッファ。
- len:lenバイトを読む。
- flags:読み取り方法。0はブロック読み取りを意味します。
- src_addr:送信元の情報。
- addrlen:出力型引数。src_addrの長さ(sizeof(src_addr)に初期化する必要があります)。
void Start()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
printf("[%s:%d]# %s", ip.c_str(), port, temp);
}
}
}
src_addrは送信元の情報を保存するために使用します。前述のbind関数でsockaddr構造体について説明しました。送信元のIPとポート番号が含まれていますが、ネットワークシーケンスなのでホストシーケンスに変換する必要があります。ntoh関数を使用して変換します。IPは32ビット整数で、見やすくするためドット区切り形式に変換するためにinet_ntoa関数を使用します。
1.6 サーバー全体のコード
#pragma once
#include <iostream>
#include <string>
#include <unordered_set>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class UdpServer
{
public:
UdpServer(const uint16_t &port, const std::string &str = "")
{
// ソケットを作成
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket < 0)
{
std::cerr << "ソケット作成エラー" << std::endl;
exit(1);
}
std::cout << "ソケット作成成功、ソケット番号: " << _socket << std::endl;
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());
int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
if (n < 0)
{
std::cout << "bindエラー" << std::endl;
exit(2);
}
std::cout << "bind成功" << std::endl;
}
void Start()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
printf("[%s:%d]# %s", ip.c_str(), port, temp);
}
}
}
~UdpServer()
{
close(_socket);
}
private:
int _socket;
};
- クライアントの実装
サーバーの作成とクライアントも似ています。ソケットの作成が必要です。
2.1 ソケットの作成
void Usage()
{
std::cout << "使用方法: ./Client [ip] port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return 3;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[argc - 1]);
// ソケットを作成
int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);
if (clientsocket < 0)
{
std::cerr << "ソケット作成エラー" << std::endl;
exit(1);
}
std::cout << "ソケット作成成功、ソケット番号: " << clientsocket << std::endl;
return 0;
}
2.2 バインドに関する問題
クライアントはIPとポート番号をバインドする必要がありますが、明示的なbindは不要です。
これはネットワークカードのファイルとIP+ポート番号を結合する必要があることを意味しますが、手動でバインドする必要はありません。オペレーティングシステムが自動的にポート番号を割り当てます。
なぜ明示的にバインドできないのか?まず、1つのポートは1つのプロセスのみに割り当てられることを理解する必要があります。2社がそれぞれクライアントソフトウェアをリリースした場合、互いのクライアントが同じポート番号を使用しないようにする必要があります。インターネット上には多くの企業があり、すべてのポート使用を協議することは現実的ではありません。そのため、オペレーティングシステムが使用されていないポート番号を自動的に割り当てるようにします。
sendto(メッセージ送信)のような関数を呼び出すと、オペレーティングシステムが自動的にポート番号を割り当てます。つまり、クライアントが起動するたびに異なるポート番号が割り当てられる可能性があります。
- サーバーのポート番号が固定な理由
サーバーのポート番号は広く知られています。もし毎回変更されると、クライアントがサーバーを見つけられなくなります。
2.3 メッセージの送信sendto
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
引数の説明:
- sockfd:どのソケットから送信するか。
- buf:バッファのデータを相手に送信します。
- len:送信するバイト数。
- flags:書き込み方法。0はブロック書き込みを意味します。
- dest_addr:相手ホストの情報(プロトコルファミリー、IP、ポート)。
- addrlen:dest_addrの長さ。
recvfromと同様の引数です。
// サーバーに直接送信。システムが自動的にbindします
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
while (true)
{
std::string buffer;
std::cout << "入力してください# ";
std::getline(std::cin, buffer);
sendto(clientsocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
}
クライアントのメッセージ送信ロジックを完了すると、通信を開始できます。
まずサーバーを起動し、次にクライアントを起動します(異なるプロセス)。
コマンドラインで入力が促されます。
クライアント側でメッセージを入力すると、サーバーに正常に送信されます。再度netstatでネットワーク状況を確認すると、クライアントとサーバーが実行中で、それぞれのポート番号が表示されます。
これでシンプルなサーバークライアントモデルが実装されました。以下はソースコードです。
2.4 ソースコード
UdpServer.hpp
//Server.hpp
#pragma once
#include <iostream>
#include <string>
#include <unordered_set>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class UdpServer
{
public:
UdpServer(const uint16_t &port, const std::string &str = "")
{
// ソケットを作成
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket < 0)
{
std::cerr << "ソケット作成エラー" << std::endl;
exit(1);
}
std::cout << "ソケット作成成功、ソケット番号: " << _socket << std::endl;
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());
int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
if (n < 0)
{
std::cout << "bindエラー" << std::endl;
exit(2);
}
std::cout << "bind成功" << std::endl;
}
void Start()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
temp[n] = 0;
printf("[%s:%d]# %s\n", ip.c_str(), port, temp);
}
}
}
~UdpServer()
{
close(_socket);
}
private:
int _socket;
};
Server.cc
#include <iostream>
#include <memory>
#include "UdpServer.hpp"
void Usage()
{
std::cout << "使用方法: ./UdpServer [ip] port" << std::endl;
}
int main(int argc, char* argv[])
{
if (argc != 3 && argc != 2)
{
Usage();
return 3;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[argc - 1]);
if (argc == 2)
ip = "";
std::unique_ptr<UdpServer> p(new UdpServer(port, ip));
p->Start();
return 0;
}
Client.cc
#include <iostream>
#include <memory>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage()
{
std::cout << "使用方法: ./Client [ip] port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return 3;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[argc - 1]);
// ソケットを作成
int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);
if (clientsocket < 0)
{
std::cerr << "ソケット作成エラー" << std::endl;
exit(1);
}
std::cout << "ソケット作成成功、ソケット番号: " << clientsocket << std::endl;
// サーバーに直接送信。システムが自動的にbindします
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
while (true)
{
std::string buffer;
std::cout << "入力してください# ";
std::getline(std::cin, buffer);
sendto(clientsocket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
}
return 0;
}
- その他の機能
以前学んだ基本的なサーバークライアントの作成方法で、クライアントからサーバーにメッセージを送信し、サーバーがそのまま表示することができました。サーバーがデータを受け取った後、特別な処理を行うようにできます。
void ExecuteCommand()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
// データを受信
temp[n] = '\0';
std::string buffer = temp;
// データ処理
// データを戻す
sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
}
}
}
このような関数を作成し、データを受信した後処理を行い、クライアントに送信します。次にデータ処理関数を実装します。
3.1 エコー機能
簡単です。ユーザーのデータを受信した後、処理せずにそのまま返します。
void ExecuteCommand()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
// データを受信
temp[n] = '\0';
std::string buffer = temp;
// データ処理
EchoMessage(buffer);
// データを戻す
sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
}
}
}
void EchoMessage(std::string &buffer)
{
return;
}
EchoMessage関数は何もしていませんが、ユーザーのメッセージが処理されたことを強調するために関数を追加しました。
正常に実行できます。
3.2 大文字変換
クライアントから送信された小文字を大文字に変換します。
void ExecuteCommand()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
// データを受信
temp[n] = '\0';
std::string buffer = temp;
// データ処理
//EchoMessage(buffer);
Transformed(buffer);
// データを戻す
sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
}
}
}
void Transformed(std::string &buffer)
{
for (auto &ch : buffer)
{
if ('a' <= ch && ch <= 'z')
{
ch -= 32;
}
}
return;
}
呼び出す関数を変更するだけで、さまざまな業務を実現できます。
3.3 英語辞書(英語から中国語)
ユーザーが送信した英語を中国語に変換します。
まず英語単語と対応する意味を保持するハッシュテーブルを作成します。
static std::unordered_map<std::string, std::string> Dict;
void TranslationInit()
{
Dict.insert({"apple", "苹果"});
Dict.insert({"pear", "梨子"});
Dict.insert({"banana", "香蕉"});
Dict.insert({"orange", "橘子"});
Dict.insert({"left", "左"});
Dict.insert({"right", "右"});
Dict.insert({"sun", "太阳"});
Dict.insert({"moon", "月亮"});
}
void ExecuteCommand()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
std::string ip = inet_ntoa(addr.sin_addr);
uint16_t port = ntohs(addr.sin_port);
if (n > 0)
{
// データを受信
temp[n] = '\0';
std::string buffer = temp;
// データ処理
//EchoMessage(buffer);
//Transformed(buffer);
Translation(buffer);
// データを戻す
sendto(_socket, buffer.c_str(), buffer.size(), 0, (sockaddr *)&addr, sizeof(addr));
}
}
}
void Translation(std::string &buffer)
{
auto it = Dict.find(buffer);
if (it == Dict.end())
{
buffer = "not find";
}
else
{
buffer = it->second;
}
}
3.4 コマンド実行
ユーザーが送信したシェルコマンド(例:pwd、lsなど)を実行します。
以下の関数を紹介します。
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
popen関数は子プロセスを作成し、子プロセスでcommandコマンドを実行し、結果をファイルに書き込みます。typeはそのファイルのオープン方法(w/r/a)を指定します。
std::unordered_set<std::string> forbid = {
"rm",
"mv",
"kill",
"cp"
};
void ExecuteCommand(std::string &buffer)
{
// 禁止コマンドを検索
if (!sercharforbid(buffer))
{
buffer = "you can't do that\n";
return;
}
// popenを使用してユーザーのコマンドを実行し、結果をfpに書き込みます
if (buffer == "ll")
{
buffer = "ls -l --color=auto";
}
FILE *fp = popen(buffer.c_str(), "r");
if (fp == nullptr)
{
buffer = "command is unknow";
return;
}
// 情報を読み取り
buffer.clear();
char temp[1024];
while (fgets(temp, sizeof(temp), fp) != NULL)
{
buffer += temp;
}
pclose(fp);
}
bool sercharforbid(const std::string &buffer)
{
for (auto com : forbid)
{
int pos = buffer.find(com);
if (pos != std::string::npos)
{
return false;
}
}
return true;
}
いくつかのコマンドをハッシュテーブルに保存し、ユーザーがこれらのコマンド(例:rm削除)を実行しないようにします。
3.5 ネットワークチャットルーム
多くのユーザーがこのチャットルームに参加し、1人のユーザーがメッセージを送信すると、他のすべてのユーザーがそのメッセージを見ることができます。
まずsockaddr_inをカプセル化するクラスを作成します。
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class InetAddr
{
public:
InetAddr(const sockaddr_in addr)
: _addr(addr)
{
_ip = inet_ntoa(addr.sin_addr);
_port = ntohs(addr.sin_port);
}
std::string GetUser()
{
std::string temp;
temp += _ip;
temp += " : ";
temp += std::to_string(_port);
return temp;
}
std::string& GetIp()
{
return _ip;
}
uint16_t GetPort()
{
return _port;
}
sockaddr_in& GetAddr()
{
return _addr;
}
private:
std::string _ip;
uint16_t _port;
sockaddr_in _addr;
};
このクラスはsockaddr_inからIPとポート番号を抽出して保存し、GetUser関数はIP+ポート番号の文字列を返します。
次にユーザー情報を保存するハッシュテーブルを作成します。
std::unordered_map<std::string, InetAddr> _users;
void AddUser(InetAddr &user)
{
std::string userMessage = user.GetUser();
if (_users.find(userMessage) != _users.end())
{
return;
}
_users.insert({userMessage, user});
}
ユーザーがメッセージを送信するたびに、sockaddr_inからユーザーのIP+ポート番号を抽出し、初めてメッセージを送信するユーザーであれば情報を保存します。
void Route(size_t sock, std::string message)
{
// messageをすべてのユーザーに送信
for (auto user : _users)
{
sockaddr_in addr = user.second.GetAddr();
sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&addr, sizeof(addr));
}
}
最後に、保存されたユーザー情報に基づいてデータをすべてのユーザーに送信します。
これでシンプルなネットワークチャットルームが完成しましたが、テストで大きな問題があることがわかります。サーバーは単一スレッドでブロッキング読み取りなので、他の人がデータを送信する際にブロッキングされてデータが表示されず、書き込み後にデータが再び表示されます。
私たちのテストでもこの点が反映されています。実行順序は上から下、左から右で、一度に1つのメッセージを送信し、合計2回です。2回目の左上のプロセスが右上の最初のメッセージを受信したことがわかります。これは実際のネットワーク通信ではあり得ません。
そのため、サーバーをスレッドプールバージョンに変更し、サーバーを2つのスレッド(1つは読み取り、1つは書き込み)にします。
#pragma once
#include <iostream>
#include <unordered_set>
#include <unordered_map>
#include <functional>
#include <unistd.h>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "LogMessage.hpp"
#include "ExistReason.hpp"
#include "ThreadPool.hpp"
#include "LocalGuard.hpp"
#include "Pthread.hpp"
#include "InetAddr.hpp"
using task_t = std::function<void()>;
class ChatServer
{
public:
ChatServer(const uint16_t &port, const std::string &str = "")
{
// ソケットを作成
_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (_socket < 0)
{
Log::LogMessage(Error, "ソケット作成エラー");
exit(CREATE_SOCKET_ERROR);
}
Log::LogMessage(Debug, "ソケット作成成功、ソケット番号: %d", _socket);
// bindを実行
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = str.empty() ? INADDR_ANY : inet_addr(str.c_str());
int n = bind(_socket, (sockaddr *)&addr, sizeof(addr));
if (n < 0)
{
Log::LogMessage(Error, "bindエラー");
exit(BIND_ERROR);
}
Log::LogMessage(Debug, "bind成功");
pthread_mutex_init(&_user_mutex, nullptr);
ThreadPool<task_t>::GetInstance()->Start();
}
~ChatServer()
{
pthread_mutex_destroy(&_user_mutex);
close(_socket);
}
void Start()
{
while (true)
{
char temp[1024];
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
socklen_t addrlen = sizeof(addr);
int n = recvfrom(_socket, temp, sizeof(temp) - 1, 0, (sockaddr *)&addr, &addrlen);
temp[n] = 0;
InetAddr user(addr);
if (n > 0)
{
// ユーザー情報を保存
AddUser(user);
std::string message = "[";
message += user.GetUser();
message += "] ";
message += temp;
// タスクをキューに追加
task_t task = std::bind(&ChatServer::Route, this, _socket, message);
ThreadPool<task_t>::GetInstance()->Push(task);
}
else if (n == 0)
{
// 相手が接続を閉じた
Log::LogMessage(Debug, "接続を閉じる");
}
else
{
Log::LogMessage(Warning, "サーバーrecvfrom警告");
}
}
}
private:
void AddUser(InetAddr &user)
{
LockGuard lock(&_user_mutex);
std::string userMessage = user.GetUser();
if (_users.find(userMessage) != _users.end())
{
return;
}
_users.insert({userMessage, user});
}
void Route(size_t sock, std::string message)
{
// messageをすべてのユーザーに送信
LockGuard lock(&_user_mutex);
for (auto user : _users)
{
sockaddr_in addr = user.second.GetAddr();
sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&addr, sizeof(addr));
}
}
private:
int _socket;
std::unordered_map<std::string, InetAddr> _users;
pthread_mutex_t _user_mutex;
};
スレッドプールを使用して事前に複数のスレッドを作成し、ユーザーがメッセージを送信すると、スレッドを割り当ててデータを処理(他のユーザーに情報を送信)します。
クライアント
#include <iostream>
#include <memory>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "LogMessage.hpp"
#include "ExistReason.hpp"
#include "Pthread.hpp"
void Usage()
{
std::cout << "使用方法: ./Client [ip] port" << std::endl;
}
struct ThreadDate
{
ThreadDate(int sock, sockaddr_in addr)
: _sock(sock), _addr(addr)
{
}
int _sock;
sockaddr_in _addr;
};
void Sender(ThreadDate date)
{
while (true)
{
std::string buffer;
std::cout << "入力してください# ";
std::getline(std::cin, buffer);
sendto(date._sock, buffer.c_str(), buffer.size(), 0, (sockaddr *)&date._addr, sizeof(date._addr));
if (sendto <= 0)
{
std::cerr << "送信エラー" << std::endl;
}
}
}
void Recver(ThreadDate date)
{
char mes[1024];
while (true)
{
sockaddr_in add;
socklen_t addlen = sizeof(add);
int n = recvfrom(date._sock, mes, sizeof(mes) - 1, 0, (sockaddr *)&add, &addlen);
if (n > 0)
{
mes[n] = '\0';
std::cerr << mes << std::endl;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage();
return USE_ERROR_MANUAL;
}
std::string ip = argv[1];
uint16_t port = atoi(argv[argc - 1]);
// ソケットを作成
int clientsocket = socket(AF_INET, SOCK_DGRAM, 0);
if (clientsocket < 0)
{
exit(1);
}
// サーバーに直接送信。システムが自動的にbindします
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
ThreadDate date(clientsocket, addr);
Thread<void, ThreadDate> sender(Sender, date);
Thread<void, ThreadDate> recver(Recver, date);
sender.Create();
recver.Create();
sender.Jion();
recver.Jion();
return 0;
}
メインプロセスは2つのスレッドを作成します。1つは待機し、もう1つはデータを送信します。
最終的に以前の問題が解決されましたが、新しい問題が発生します。マルチプロセスであるため、画面への出力時にエラーが発生します。パイプを使用することで解決できます。重要な点は、クライアントのメッセージ受信スレッドがメッセージを受信した際にcerrを使用して出力していることです。標準エラー(ファイル記述子2)をパイプにリダイレクトすることで、読み書きを分離できます。