Linux 開発 - ネットワーク プログラミング
- ネットワーク プログラミング。ネットワークに接続された 2 台以上のコンピュータ間でデータを交換するプログラムを作成します。
1. ネットワーク - 概要
ctontrol + C: プロセスを強制的に終了します
1. ネットワークの物理構造
1. 通常のネットワーク
2. 光ファイバーギガビットネットワーク
- 市場には多くのギガビット ネットワークが存在しますが、通常のネットワーク ケーブルの最大スループットは 100 メガビットにすぎず、ギガビットに到達したい場合は、ギガビット光ファイバー ネットワーク ケーブルを交換する必要があり、ギガビット光ファイバー アダプターも必要です。
- ギガビット ネットワーク ケーブルは平らで、USB に似た小さなインターフェイスを備えています。
2. ネットワーク - IP アドレス
并不是所有的地址都是可以用的
-
1. ネットワークアドレスは 4 バイト 32 ビットで構成されます。
-
2. 0 で始まるアドレスは使用できません。
-
3. 0 で終わるアドレスは、特定のアドレスではなく、ネットワーク セグメントを表します。
-
4. 224 ~ 239 で始まるアドレスは
组播
アドレスであり、ポイントツーポイント伝送には使用できません。- マルチキャスト: TCP/UDP を介したブロードキャストを理解します。
- 利点: マルチキャストにより帯域幅を大幅に節約できる
- 短所: ネットワーク ストームが発生しやすい
-
5. 240 ~ 255 で始まるアドレスは実験用に予約されており、通常はサーバーまたは端末のアドレスとしては使用されません。
- 127.0.0.1 は予約されたループバック ネットワーク アドレスです。このネットワークに送信されたデータはすべて送り返されます。通常、学習とテストに使用されます。
- 0.0.0.0 は予約されており、ネットワーク セグメント アドレス全体で、通常はサーバーの監視に使用されます。
-
カテゴリ A (大規模ネットワーク)、B (中規模ネットワーク)、C (小規模ネットワーク) は、さまざまなサイズのネットワークに使用されます。
-
クラス D はマルチキャスト (ブロードキャストと同様) に使用されます。
-
クラス E は実験的なプライベート ネットワーク用に予約されています
3. ネットワークポート
このポートはサーバーだけでなくクライアントによっても使用されます。
-
Well Known Ports : 一般的に使用されるポートとも呼ばれ、0 から 1024 までのポートで、特定のサービスに緊密にバインドされています。通常、これらのポートの通信は特定のサービスのプロトコルを明確に示しており、この種のポートはその役割を再定義できません。
netstat -an
- 443: httpsサービス
- 80:http通信
- 23: Telnetサービス
-
登録済みポート: ポート番号の範囲は 1025 ~ 49151 です。これらのポートのほとんどは、サービス オブジェクトを明確に定義していません。実際のニーズに応じてさまざまなプログラムをカスタマイズできます。リモート コントロール ソフトウェアとトロイの木馬プログラムは、これらのポートによって定義されます。
-
動的ポートおよび/またはプライベート ポート: ポート番号の範囲は 49152 ~ 65535 (サーバーのリスニング ポートとして簡単に使用しないでください)。特に、一部のトロイの木馬プログラムはこれらのポートを好んで使用しますが、これらのポートは気づかれないことが多く、簡単に隠すことができます。
4. ネットワークプロトコル
合意内容は、 です一种网络交互中数据格式和交互流程的约定
。このプロトコルを通じて、リモート デバイスとデータをやり取りしたり、サービスを要求したり、相手のサービスを完了したりできます。
1. TCP プロトコルのヘッダー構造:
48バイト
2. HTTP プロトコルのヘッダー構造:
テキスト ストリーム
GET リクエスト、バージョン 1.1、ホスト: Web サイトにデータを送信、UA: クライアント ブラウザー情報、
受け入れ: 情報、形式、言語タイプを受信
3. SSHパケットヘッダ構造
6バイト、バイトストリーム
5.TCPプロトコル
伝送制御プロトコル (TCP) は、コネクション型で信頼性の高いバイト ストリーム (バイト単位) トランスポート層通信プロトコルです。安全で信頼性がありますが、パフォーマンスが犠牲になります。
-
インタラクションプロセス:
- 物理的な接続の中断を解決するにはタイムアウトが必要で、デフォルトのタイムアウトは 2 時間に及ぶ場合があります。
- タイムアウトの解決策: ハートビート パケット メカニズム。双方がオンラインであるかどうかを強制的に問い合わせ、異常が一定回数以上継続した場合、および通常 3 回のハートビートを送受信できない場合、接続を強制的に切断します。
2. ソケット - 概要
- Windows ネットワーク プログラミングのネットワーク部分に似ています: Windows ネットワーク プログラミング
- ソケット: ネットワーク データ送信に使用されるソフトウェア デバイス。オペレーティング システムはソケット サポートを提供します。
1. ソケットの理解
2. ソケットの作成
ソケットにはさまざまな種類がありますが、最も一般的に使用されるのは TCP ソケットと UDP ソケットです。
#include <sys/socket.h>
/* socket创建
- domain:使用的协议族(Protocol Family)信息
- type:数据传输类型信息
- protocol:计算机之间通信种使用的协议信息
*/
int socket(int domain, int type, int protocol);
3. ソケット機能
- コネクション型ソケット TCP : 信頼性が高く、順序どおりに配信され、バイトベースのコネクション型データ送信方式です。
- メッセージ指向ソケット UDP : 信頼性が低く、順序どおりに配信されず、高速データ送信用に設計されています。
4.バインド機能
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
/* socket绑定
- sockfd:socket套接字文件描述符
- myaddr:结构体,存放地址信息的结构体变量地址值,IPv4,IPV6
- addrlen:
*/
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);//绑定成功返回0,失败返回-1
ntohs は、短いデータをネットワーク バイト オーダーからホスト バイト オーダーに変換するものとして理解できます。
struct sockaddr_in addr;
char *serv_ip = "221.214.168.13";//声明IP地址字符串
char *serv_port = "9190";//声明端口号
memset(&add, e,sizeof(addr));//结构体变量addr的所有成员初始化为0
addr.sin_fimily = AF_INET;//指定地址族
/*
每次创建服务器端套接字都要输入IP地址,会很繁琐,此时可初始化地址信息为INADDRANY。
addr.sin_addr.s_addr = htonl(INADDRANY);
*/
add.sin_addr.s_addr = inet_addr(serv_ip);//基于字符串的IP地址初始化
add.sin_port=htons(atoi(serv_port));//基于字符串的端口号初始化
5.リスニング機能
#include <sys/socket.h>
/*
- sock:套接字文件描述符
- backlog:连接请求等待队列的长度,若为5,则队列长度为5,最多使5个连接请求进入队列
*/
int listen(int sock, int backlog);//成功返回0,失败返回-1.
4.受け入れ機能
#include <sys/socket.h>
/*
- sock:套接字文件描述符
- addr:保存发起连接请求的客户端地址信息的变量地址值,调用函数后传递来的地址变量参数天才客户端地址信息
- addrlen: addr结构体参数的长度。
*/
int accept(int sock, struct sockaddr*addr, socklen_t *addrlen);//成功返回0,失败返回-1.
3. TCP プログラミング - エコー サーバー
- エコーサーバー: クライアントから受け取ったデータをそのままクライアントに返す、つまり「echo」
1. TCP/IPプロトコルスタック
TCP/IP ネットワーク プロトコルに基づくソケットは、TCP ソケットと UDP ソケットに分類されます。
2.TCPサーバー:
void server_func()
{
printf("%s(%d):%s\n", __FILE__, __LINE__, __FUNCTION__);
int server_sock, client_sock;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
socklen_t client_addr_size;
//此处省略了一些,socket的配置认证,eg:版本号等
server_sock = socket(PF_INET, SOCK_STREAM, 0);
if (server_sock < 0) {
//error_handling("create socket error!");
std::cout << "create socket error!\n";
return;
}
memset(&server_addr, 0 , sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("0.0.0.0");// htonl(INADDR_ANY); 0.0.0.0 表示监听全网段,包括内外网
server_addr.sin_port = htons(9444);
int ret = bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (ret == -1) {
//error_handling("bind socket error!");
std::cout << "server bind socket error!\n";
close(server_sock);
return;
};
client_addr_size = sizeof(client_addr);
ret = listen(server_sock, 3);
if (ret == -1) {
//error_handling("listen socket error!");
std::cout << "server listen socket error!\n";
close(server_sock);
return;
};
//回声服务器-原理:服务端循环接收客户端的消息
char buffer[1024];
while (1)
{
memset(buffer, 0, sizeof(buffer));
client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_addr_size);
if (client_sock == -1) {
//error_handling("accept server socket error!");
std::cout << "server accept socket error!\n";
close(server_sock);
return;
}
ssize_t len = 0;
while ((len = read(client_sock, buffer, sizeof(buffer))) > 0)
{
len = write(client_sock, buffer, strlen(buffer));
if (len != (ssize_t)strlen(buffer)) {
//error_handling("write message failed!");
std::cout << "server write message failed!\n";
close(server_sock);
return;
}
std::cout << "server read & write success!, buffer:" << buffer <<"__len:"<< len << std::endl;
memset(buffer, 0, len);//清理
}
close(client_sock);//服务端关闭的时候,客户端会自动关闭
};
close(server_sock);
}
3. TCP クライアント:
void client_func()
{
int client = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(9444);
int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret == -1) {
std::cout << "client connect failed!\n";
close(client);
return;
}
std::cout << "client connect server is success!\n";
char buffer[256] = "";
while (1)
{
fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
break;
}
size_t len = strlen(buffer);
size_t send_len = 0;
//当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
while (send_len < len)
{
ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
if (ret <= 0) {
//连接出了问题
fputs("may be connect newwork failed,make client write failed!\n", stdout);
close(client);
return;
}
send_len += (size_t)ret;
std::cout << "client write success, msg:" << buffer << std::endl;
}
memset(buffer, 0, sizeof(buffer));
//当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
size_t read_len = 0;
while (read_len < len)
{
size_t ret = read(client, buffer + read_len, len - read_len);
if (ret <= 0) {
//连接出了问题
fputs("may be connect newwork failed, make client read failed!\n", stdout);
close(client);
return;
}
read_len += (size_t)ret;
}
std::cout << "from server:" << buffer << std::endl;
};
close(client);
std::cout << "client done!" << std::endl;
}
4. サーバーとクライアント間の異なるプロセス間の対話
#include <sys/wait.h>
void server_client_connect()
{
//创建进程
pid_t pid = fork();
if (pid == 0) {
//为0时表示在子进程
sleep(1);//为了让服务端先执行
client_func();
}
else if (pid > 0) {
//主进程为服务端进程
server_func();
int status = 0;
wait(&status);
}
else
{
std::cout << "fork failed!" << pid << std::endl;
}
}
5. エコーサーバー実践プロジェクト(ネットワーク電卓)—クラウドコンピューティング
- ソースコードが必要な場合は、メッセージを残してください。
- プロジェクトの要件:
/*
* 云计算器-需求:
1.客户端连接到服务器端后以1字节整数形式传递待算数字个数。 (个数是1字节)
2.客户端向服务器端传递的每个整数型数据占用4字节。 (每个数是4字节)
3.传递整数型数据后接着传递运算符。运算符信息占用1字节。 (运算符信息1字节)
4.选择字符+、-、*之一传递。
5. 服务器端以4字节整数型向客户端传回运算结果。
6. 客户端得到运算结果后终止与服务器端的连接。
*/
- 最終出力:
4. TCP の基本原理
1. TCPソケットI/Oバッファリング
-
TCP ソケットのデータ送受信には境界がないことがわかっています。サーバーが 40 バイトのデータを転送するために書き込み関数を 1 回呼び出したとしても、クライアントは 4 回の読み取り関数呼び出しを通じて毎回 10 バイトを読み取る可能性があります。ただし、ここにも疑問があり、サーバーは一度に 40 バイトを送信しますが、クライアントはそれをバッチでゆっくり受信できます。クライアントが 10 バイトを受信した後、残りの 30 バイトはどこで待機していますか? それは、着陸を待って上空でホバリングしている飛行機のようなもので、残りの 30 バイトも受信を待ってネットワーク内をさまよっているのでしょうか?
-
実際には、write 関数が呼び出された直後にデータが送信されるわけではありません。また、read 関数が呼び出された直後にデータが受信されるわけでもありません。より正確には、次の図に示すように、書き込み関数が呼び出された瞬間にデータが出力バッファに移動され、読み取り関数が呼び出された瞬間にデータが入力バッファから読み取られます。
- 1. データの送信から受信までの間に物理的なケーブルや入出力バッファがあるため、速度が遅くなります。
- 2. サーバーはデータを送信するときに、まずクライアントにバッファがあるかどうかを確認します (3 ウェイ ハンドシェイク プロトコル)。バッファがある場合は、送信を続行できます。
-
write 関数が呼び出されると、データは出力バッファに移動され、適切なタイミングで (個別に送信されるか一度に送信されるかに関係なく) 相手の入力バッファに転送されます。このとき、相手はread関数を呼び出して入力バッファからデータを読み出します。これらの I/O バッファリングの特性は次のように要約できます。
A: I/O バッファリングは各 TCP ソケット内に個別に存在します。
B: I/O バッファリングはソケットの作成時に自動的に生成されます。
C:出力バッファーに残ったデータは、ソケットが閉じられた場合でも配信され続けます。
D: ソケットを閉じると、入力バッファ内のデータが失われます。
それでは、次の状況では何が起こるでしょうか? I/O バッファリングを理解した後、「
クライアントの入力バッファは 50 バイトで、サーバーは 100 バイトを送信します。」
これは確かに問題です。入力バッファは 50 バイトしかありませんが、100 バイトのデータが受信されました。次の解決策が提案できます:
入力バッファがいっぱいになる前に、すぐに read 関数を呼び出してデータを読み取ります。これにより、スペースが解放され、問題は解決されます。 -
実際には、TCP がデータ フローを制御するため、そのような問題はまったく発生しません。
TCP にはスライディング ウィンドウ (Sliding Window) プロトコルがあり、会話モードでは次のように表示されます。
ソケット A: 「こんにちは、最大 50 バイトまで送信できます。」
ソケット B: 「OK!」
ソケット A: 「20 バイトのスペースを解放しました。最大 70 バイトまで受信できます。」 .ソケット B :
「OK!」
データの送受信も同様で、TCP ではバッファオーバーフローによるデータの消失はありませんが、
バッファリングにより伝送効率が低下します。
2. TCP の内部原則
- TCP 通信における 3 つの主要なステップ:
- 1. 接続を確立するための 3 ウェイ ハンドシェイク。
- 2. 通信を開始し、データを交換します。
- 3. 4 回手を振って接続を解除します。
1. スリーウェイハンドシェイク
(クライアントが開始 - サーバーが応答)
- [1回目のハンドシェイク] ソケットA: 「こんにちは、ソケットB。送信したいデータがあるので、接続を確立してください。」 [2回目のハンド
シェイク] ソケットB: 「わかりました、こちら側の準備ができました。」
[3回目のハンドシェイク] ソケットA :「リクエストを受け入れていただき、ありがとうございます。」
まず、接続を要求するホスト A は次の情報をホスト B に送信します:
[SYN] SEQ:1000, ACK: -
このメッセージの SEQ は 1000、ACK は空で、SEQ 1000 の意味は次のとおりです
。 「現在送信中のデータパケットのシリアル番号は1000です。受信が正しければ、データパケット番号1001をあなたに届けるように通知してください。」 これは初めて接続を要求するときに使用されるメッセージであり、SYNとも呼ばれます。 。SYN は Synchronization の略で、データの送受信の前に送信される同期メッセージを表します。
次に、ホスト B は A にメッセージ
[SYN+ACK]SEQ:2000, ACK:1001 を送信します。
このとき、SEQ は 2000、ACK は 1001 であり、SEQ 2000 の意味は次のとおりです
。送信されているデータ パケットの番号は 2000 です。正しく受信された場合は、データ パケット番号 2001 をあなたに届けるように通知してください。」
ACK1001 の意味は次のとおりです。
「送信された SEQ 1000 のデータ パケットは正しく受信されました。次に、SEQ 1001 のデータ パケットを渡してください。」
ホストAが初めて送信するデータパケットに対する確認メッセージ(ACK1001)と、ホストBがデータ送信を準備するための同期メッセージ(SEQ2000)をまとめて送信するため、このタイプのメッセージはSYN+とも呼ばれます。応答します。
データの送受信を行う前に、データパケットにシーケンス番号を付与し、相手にシーケンス番号を通知することで、データの損失を防ぐための準備が行われます。パケットにシーケンス番号を割り当てて確認応答することにより、データ損失が発生した場合でも、失われたパケットをすぐに確認して再送信できます。したがって、TCP は信頼性の高いデータ送信を保証できます。最後に、ホスト A からホスト B に送信されたメッセージを確認します:
[ACK]SEQ:1001、ACK:2001
TCP 接続中にデータ パケットを送信する場合は、シーケンス番号を割り当てる必要があります。
前のシリアル番号 1000 に 1 を加えて、1001 を割り当てます。このとき、データパケットは
「送信されたSEQ 2000のデータパケットが正しく受信されましたので、SEQ 2001のデータパケットを送信できるようになりました。」というメッセージを伝えます。
これにより、ACKメッセージにACK2001が付加されて送信される。この時点で、ホスト A とホスト B はお互いの準備が整っていることを確認しました。
- くそー、文章が複雑すぎるので、わかりやすく説明しましょう。
TCP の 3 ウェイ ハンドシェイクは、暗く風が強い夜にコミュニティを一人で歩いているようなものです。遠くないところで、コミュニティの美しい女の子がこちらに近づいてくるのが見えます。ただし、街路灯は光であるため、100% 確実であるとは言えません。少し薄暗いので、手を振って相手があなたを認識しているかどうかを判断する必要があります。
まずあなたは女の子に手を振ります (syn)、あなたが手を振っているのを見た女の子はあなたにうなずき、笑顔を絞り出します (ack)。女の子の笑顔を見た後、女の子があなたを正常に識別したことを確認します (確立された状態に入ります)。
しかし、女の子は少し恥ずかしがって、あなたが他の人を見ていないかどうかを確認するために周りを見回しました。女の子もあなたに手を振ってくれました(syn) 女の子が手を振っているのを見て、相手が確認を求めているのが分かり、うなずいて笑顔になりました(ack) 女の子は相手の笑顔を見て確認しました あなた自分自身に挨拶しています(確立された状態に入ります)。
TCP 3 ウェイ ハンドシェイクを使用した攻撃:
クライアントは最初のハンドシェイクのみを実行し続けるため、サーバーは接続の作成とこのハンドシェイクに基づいたシーケンス番号の割り当てを続けますが、これらの作成された接続には約 2 時間のタイムアウトが必要なため、より多くのサーバーが使用されます。リソースが不足し、過剰なデータ量がサーバーをブロックします。
2. データ送信
- データ送信中は再送信と重複排除に注意する必要があり、オペレーティング システムのネットワーク カーネル モジュールがこれら 2 つのタスクをすでに処理しています。
- TCPのデータ通信は、二人の人が空気を越えて通信することを意味し、一定の距離があり、相手は自分の言ったことを聞いているかを繰り返し確認する必要があります。
あなたが文章を叫び(seq)、女の子がそれを聞いた後、彼女はそれを聞いたとあなたに答えます(ack)。
叫んだのに女の子から長い間返事がなかったら、とても落ち込んでしまいますが、恋をしているときと同じように、あなたは熱意に満ちていますが、女の子は暑かったり冷たかったりするので、我慢します。 1回ダメなら2回、2回ダメなら2回、3回、これがtcp再送信です。再送信するので、女の子は同じ文章を二度聞いている可能性があり、これが重複の排除です。
オペレーティング システムのネットワーク カーネル モジュールは、再送信と重複排除という 2 つのタスクをすでに処理しています。
それが終わったら、お姉さんとあなたはしぶしぶ別れるでしょう。
それなら別れなければなりません。
つまり、TCP 通信には 4 つの波が来ています。
3. 4回手を振る
5. UDPプログラミング
UDP は DDOS 攻撃 (ネットワーク分散型サービス拒否攻撃) の主な形式です。送信者アドレスは偽造でき、データは拒否できません。データ パケットを受信する必要がありますが、送信者アドレスが偽造されているため、送信者を拒否することはできません。送信者アドレスが見つかり拒否できません。
1. UDPの基本原則
UDP の仕組みを文字で説明します。これは UDP を説明するときに使用される伝統的な例であり、UDP の特性と完全に一致しています。手紙を送る前に、封筒に差出人と受取人の住所を記入し、切手を貼ってポストに投函します。もちろん、手紙の性質上、相手が受け取ったかどうかを確認することはできません。また、郵送中に手紙が紛失してしまう可能性もあります。とはいえ、手紙は信頼性の低い伝達方法です。同様に、UDP も信頼性の低いデータ送信サービスを提供します。
「この場合、TCP の方が良いプロトコルですよね?」
信頼性だけを考慮すると、確かに TCP の方が UDP よりも優れています。ただし、UDP は TCP よりも構造が単純です。UDP は、ACK のような応答メッセージを送信しません。また、SEQ のようなデータ パケットにシーケンス番号を割り当てません。したがって、UDP のパフォーマンスが TCP のパフォーマンスよりもはるかに高い場合があります。プログラミングでの UDP の実装も、TCP よりも簡単です。さらに、UDP は TCP ほど信頼性は高くありませんが、データ破損は想像されているほど頻繁には発生しません。したがって、信頼性よりもパフォーマンスが重要な場合には、UDP が適切な選択となります。
この場合、UDP の役割は何でしょうか? TCP は信頼性の高いデータ伝送サービスを提供するために、信頼性の低い IP 層でフロー制御を実行しますが、UDP にはこのフロー制御機構がありません。
フロー制御は、UDP と TCP を区別する最も重要な記号です。しかし、TCP からフロー制御を削除すると、ほんの一握りのコンテンツしか残りません。言い換えれば、TCP の命はフロー制御にあります。
TCP を電話にたとえると、UDP は手紙にたとえられます。ただし、これはプロトコルがどのように機能するかを説明しているだけであり、データ交換速度は含まれていません。「電話は文字より速いから、TCPのデータ送受信速度もUDPより速い」と誤解しないでください。実際にはまったく逆です。TCP は UDP よりも高速であることはできませんが、特定の種類のデータの送受信では UDP に近い速度になる可能性があります。たとえば、毎回交換されるデータ量が大きくなるほど、TCP 伝送速度は UDP 伝送速度に近づきます。
TCP は UDP と組み合わせて使用されます。TCP はフロー制御に使用され、UDP はデータ送信に使用されます。
UDP パケット データ送信の無秩序な性質により、ルーターのステータス変化により、パケットが最初に送信され、後で到着する可能性があります。
- 上の図からわかるように、IP の役割は、ホスト B から送信される UDP データ パケットをホストAに正確に配信することです。しかし、最終的に UDP パケットをホスト A の特定の UDP ソケットに届けるプロセスは、UDP によって完了します。UDP の最も重要な役割は、ホストに送信されたデータ パケットをポート番号に従って最終の UDP ソケットに配信することです。
実際、実際のアプリケーション シナリオでは、UDP もある程度の信頼性を持っています。ネットワーク伝送の特性上、情報の損失が頻繁に起こりますが、圧縮ファイルを伝送する場合(10,000 個のデータ パケットを送信する場合、1 つの損失だけで問題が発生する)、圧縮ファイルの一部が失われる限り、TCP を使用する必要があります。解凍するのが難しいです。しかし、ネットワーク上でビデオやオーディオをリアルタイムに送信する場合は状況が異なります。マルチメディア データの場合、データの一部が失われることは大きな問題ではなく、短期的な画像のジッターや微妙なノイズが発生するだけです。ただし、リアルタイムのサービスを提供する必要があるため、速度が非常に重要な要素となり、現時点では UDP を考慮する必要があります。ただし、必ずしも UDP が TCP より速いわけではなく、TCP が UDP より遅い理由としては、次の 2 点が考えられます。- 1 データの送受信前後の接続設定と解除の処理。
- 2. データ送受信処理の信頼性を確保するためにフロー制御を追加
特に送受信データ量が少ないが頻繁な接続が必要な場合は、TCP よりも UDP の方が効率的です。
2. UDPソケット
-
UDP と TCP の違い:
- 1. UDP にはリッスンとアクセプトがありません
- 2. UDP はソケット上でデータを送受信します。ソケットは、from と recvfrom の送信者アドレスと受信者アドレスによって区別されます。
-
UDP はサーバーとクライアントが接続されていない
UDP サーバー/クライアントは、TCP のように接続された状態でデータのやり取りをしないため、TCP のように接続処理を行う必要がありません。つまり、TCP 接続プロセス中に呼び出される listen 関数と accept 関数を呼び出す必要はありません。UDPではソケットの作成処理とデータ交換の処理のみです。 -
UDP サーバーとクライアントの両方に必要な
TCP ソケットは 1 つだけであり、ソケット間には 1 対 1 の関係が必要です。10 個のクライアントにサービスを提供するには、ゲートキーパーのサーバー ソケットに加えて、サーバー側のソケットが 10 個必要です。しかし、UDP では、サーバーとクライアントの両方に必要なソケットは 1 つだけです。以前 UDP の原理を説明した際に手紙の例を挙げましたが、手紙を送受信する際に使用されるメールボックスは UDP ソケットにたとえられます。近くに郵便ポストがあれば、そこから任意の住所に手紙を送ることができます。同様に、任意のホストにデータを送信するために必要な UDP ソケットは 1 つだけです
- 上の図は、UDP ソケットと 2 つの異なるホスト間でデータを交換するプロセスを示しています。言い換えれば、複数のホストと通信できるのは 1 つの UDP ソケットだけです。
- TCP ソケットを作成した後は、データ送信時にアドレス情報を追加する必要はありません。TCP ソケットは相手のソケットに接続されたままになるためです。言い換えれば、TCP ソケットは宛先アドレス情報を知っています。ただし、UDP ソケットは接続状態を保持しないため(UDP ソケットには簡易的なメールボックス機能しかありません)、データを送信するたびにターゲットアドレス情報を追加する必要があります。これは、手紙を送る前に住所を記入するのと同じです。以下は、アドレスを入力してデータを送信するときに呼び出される UDP 関連の関数です。
1. sendto() 関数 - 送信者
#include<sys/socket.h>
→成功时返回传输的字节数,失败时返回-1。
ssize_t sendto(int sock,void*buff,size_t nbytes,int flags,struct sockaddr *to, socklen_t addrlen);
● sock は
、データの送信に使用される UDP ソケット ファイル記述子です。
● buff は
送信するデータのバッファアドレス値を保存します。
● nbytes
送信されるデータの長さ (バイト単位)。
● flags
オプションのパラメータが使用できない場合は、0 を渡します。
●
ターゲットアドレス情報を格納する sockaddr 構造体変数のアドレス値。
● addrlen は、
パラメータ to に渡されるアドレス値構造体変数の長さです。
sendto 関数と以前の TCP 出力関数の最大の違いは、この関数がターゲット アドレス情報を渡す必要があることです。次にUDPデータを受信する機能を紹介します。UDPデータの送信元は固定されていないため、この関数は送信元情報を受け取ることができる形式、つまりUDPデータパケット内の送信元情報も同時に返す形で定義されています。
2.recvfrom() 関数 - 受信者
#include<sys/socket.h>
//→成功时返回接收的字节数,失败时返回-1。
ssize_t recvfrom(int sock, void *buff,size_t nbytes, int flags,
struct sockaddr*from, socklen_t*addrlen);
●sock は、データを受信するために使用される UDP ソケット ファイル記述子です。
●buff は受信データのキャッシュアドレス値を保存します。
●nbytes 受信できる最大バイト数です。パラメータ buf が指すバッファサイズを超えることはできません。
●flags オプションのパラメータが使用できない場合は、0 を渡します。
●from送信者のアドレス情報を格納するsockaddr構造体変数のアドレス値。
●addrlenはパラメータfromの可変長構造体の可変アドレス値を保持します。
UDP プログラムを作成する際の中核部分は上記 2 つの関数にあり、UDP データ送信におけるそれらのステータスも示しています。
3. UDP プログラミング - エコー サーバー
1.サーバー
- 1. ソケットの作成
- 2. アドレス ファミリ (IP、ポート) を構成します。
- 3.バインドバインド
- 4.データ受信recvfromとデータ送信sendto
- 5. ソケットを閉じます
2. クライアント
- 1. ソケットの作成
- 2. アドレス ファミリ (IP、ポート) を構成します。
- 3. データを送信する(sendto)およびデータを受信する(recvfrom)
- 4. ソケットを閉じます
3. コアソースコード
//UDP回声服务器
#define BUF_SIZE 30 //buffer字节大小
//需要控制台 输入端口
void udp_server(int argc, char* argv[])
{
if (argc != 2) {
//校验端口参数,
printf("Usage : %s <port>\n", argv[0]);
error_handling("argement is error: 端口");
}
int serv_sock;
char message[BUF_SIZE];
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
//创建socket
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);//UDP的socket SOCK_DGRAM: 报文类型
if (serv_sock == -1)
error_handling("UDP socket creation error");
memset(&serv_adr, 0, sizeof(serv_adr));
//配置socket的地址族
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY->0.0.0.0 htonl:主机字节序 转网络字节序 l:long型 4或8字节
serv_adr.sin_port = htons((uint16_t)atoi(argv[1]));//需要输入端口 htons:主机字节序 转网络字节序 s:short型,2字节
//绑定
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
//收发数据
ssize_t str_len;
while (1)
{
clnt_adr_sz = sizeof(clnt_adr);
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);//利用分配的地址接收数据。不限制数据传输对象
sendto(serv_sock, message, str_len, 0, (struct sockaddr*)&clnt_adr, clnt_adr_sz);//函数调用同时获取数据传输端的地址。正是利用该地址将接收的数据逆向重传。
}
close(serv_sock);
}
void udp_client(int argc, char* argv[])
{
if (argc != 3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
error_handling("argement is error: IP");
}
int sock;
char message[BUF_SIZE];
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
//创建socket
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));//清理,防止默认值
//配置socket的地址族
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);//控制台输入ip地址,eg:127.0.0.1
serv_adr.sin_port = htons((uint16_t)atoi(argv[2]));//控制台输入端口
//收发数据
ssize_t str_len;
while (1)
{
fputs("Insert message(q to quit): ", stdout);
fgets(message, sizeof(message), stdin);
if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))//字符串比较
break;
ssize_t len = sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
memset(message, 0, len);
adr_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);
printf("Message from server: %s", message);
message[str_len] = 0;
}
close(sock);
}
//主进程开启客户端,再开启服务端,是为了在主进程调试方便
void udp_server_client_connect(int argc, char* argv[])
{
char* argv0 = "";
pid_t pid = fork();
if (pid < 0)
std::cout << "fork failed!" << pid << std::endl;
if (pid == 0)//开启新进程,子进程(服务端进程)
{
int argc = 2;
char* argv[] = {
argv0, (char*)"9555" };
udp_server(argc, argv);
}
else//pid > 0 , 父进程(客户端进程)
{
int argc = 3;
char* argv[] = {
argv0, (char*)"127.0.0.1", (char*)"9555" };
udp_client(argc, argv);
int status = 0;
wait(&status);
}
}
3. UDPの伝送特性と呼び出し
- UDPサーバー/クライアントの実装方法は前述しました。しかし、UDP クライアントをよく見ると、IP とポートをソケットに割り当てるプロセスが欠けていることがわかります。
- TCP クライアントは connect 関数を呼び出してこのプロセスを自動的に完了しますが、UDP には同じ機能を引き受けることができる関数呼び出しステートメントさえありません。IP 番号とポート番号は正確にいつ割り当てられますか?
- UDP プログラムでは、sendto 関数を呼び出してデータを送信する前にソケット アドレスの割り当てが完了している必要があるため、bind 関数が呼び出されます。
- もちろん、bind 関数は TCP プログラムにも登場していますが、bind 関数は TCP と UDP を区別せず、つまり UDP プログラムからも呼び出すことができます。
- また、sendto関数呼び出し時にアドレス情報が割り当てられていないことが判明した場合、初めてsendto関数を呼び出したときに、対応するソケットにIPとポートが自動的に割り当てられます。
- また、このときに割り当てられたアドレスはプログラムの終了(ソケットを閉じる前)まで保持されるため、他のUDPソケットとのデータ交換にも使用できます。
- もちろん、IP としてホスト IP を使用し、ポート番号として未使用のポート番号を選択します。
要約すると、sendto 関数が呼び出されるときに IP とポート番号が自動的に割り当てられるため、通常は UDP クライアントで追加のアドレス割り当てプロセスを行う必要はありません。
6.ソケットの各種オプション
1. I/Oバッファサイズ
1. 理解:
2,getsockopt & setsockopt 関数数
上の表のほぼすべてのオプションを読み取り (Get) および設定 (Set)できます(もちろん、一部のオプションは 1 つの操作しか実行できません)。オプションの読み込みと設定は、以下の 2 つの機能で完了します。
#include<sys/socket.h>
//→成功时返回0,失败时返回-1。
int getsockopt(int sock, int level,int optname, void *optval, socklen_t *optlen);
●sock: オプションソケットファイル記述子を表示するために使用されます。
●level 表示するオプションのプロトコル層。
●optname 表示するオプションの名前。
●optvalは閲覧結果のバッファアドレス値を保存します。
●optlen によって第 4 パラメータ optval に渡されるバッファ サイズ。関数を呼び出した後、この変数には 4 番目のパラメーターを通じて返されたオプション情報のバイト数が格納されます。
#include<sys/socket.h>
//→成功时返回0,失败时返回-1。
int setsockopt(int sock, int level, int optname, const void*optval, socklen_t optlen);
●sock は、オプションのソケットファイル記述子を変更するために使用されます。
●level 変更するオプションのプロトコル層。
●optname 変更するオプションの名前。
●optvalは、変更するオプション情報のバッファアドレス値を保持します。
●optlen 第4パラメータoptvalに渡すオプション情報のバイト数。
3,SO_SNDBUF &SO_RCVBUF
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
int snd_buf=1024*3, rcv_buf=1024*3;
int state;
socklen_t len;
sock=socket(PF_INET, SOCK_STREAM, 0);
state=setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
if(state)
error_handling("setsockopt() error!");
state=setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
if(state)
error_handling("setsockopt() error!");
len=sizeof(snd_buf);
state=getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
if(state)
error_handling("getsockopt() error!");
len=sizeof(rcv_buf);
state=getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
if(state)
error_handling("getsockopt() error!");
printf("Input buffer size: %d \n", rcv_buf);
printf("Output buffer size: %d \n", snd_buf);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
2,SO_REUSEADDR
- アドレス割り当てエラー (Binding Error)アドレス再割り当てが発生しました
サーバーは応答の遅延により Time-wait 待機状態になっています。
- 時間待機の解決策は、ソケット オプションの SO_REUSEADDR のステータスを変更することです。このパラメータを適切に調整することで、Time-wait 状態のソケット ポート番号を新しいソケットに再割り当てできます。SO_REUSEADDR のデフォルト値は 0 (false) で、時間待ち状態のソケット ポート番号を割り当てることができないことを意味します。したがって、この値を 1 (true) に変更する必要があります。
//解决Time-wait的问题
getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);
printf("SO_REUSEADDR = %d\n", optval);
//设置optval
if (optval == 0)
optval = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, addrlen);
- デモプロジェクト:
//------------------------ Time-wait--------------------------
#define TRUE 1
#define FALSE 0
void tw_tcp_server()
{
int sock, client, optval = 0;
struct sockaddr_in addr, cliAddr;
socklen_t addrlen = sizeof(addr);
char buffer[256] = "";
sock = socket(PF_INET, SOCK_STREAM, 0);
//解决Time-wait的问题
getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);
printf("SO_REUSEADDR = %d\n", optval);
//设置optval
if (optval == 0)
optval = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, addrlen);
//查看新的optval
getsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &optval, &addrlen);
printf("SO_REUSEADDR = %d\n", optval);
memset(&addr, 0, addrlen);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(9526);
addrlen = sizeof(addr);
if (bind(sock, (struct sockaddr*) & addr, addrlen) == -1)
{
error_handling("tw_tcp_server bind failed");
}
listen(sock, 3);
client = accept(sock, (struct sockaddr*)&cliAddr, &addrlen);
read(client, buffer, sizeof(buffer));
close(client);
close(sock);
}
void tw_tcp_client()
{
int client = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(9526);
int ret = connect(client, (struct sockaddr*)&servaddr, sizeof(servaddr));
if (ret == -1) {
std::cout << "client connect failed!\n";
close(client);
return;
}
std::cout << "client connect server is success!\n";
char buffer[256] = "";
while (ret == 0)
{
fputs("Input message(Q to quit):", stdout);//提示语句,输入Q结束
fgets(buffer, sizeof(buffer), stdin);//对文件的标准输入流操作 读取buffer的256字节
if (strcmp(buffer, "q\n") == 0 || (strcmp(buffer, "Q\n") == 0)) {
break;
}
size_t len = strlen(buffer);
size_t send_len = 0;
//当数据量很大时,并不能一次把所有数据全部发送完,因此需要分包发送
while (send_len < len)
{
ssize_t ret = write(client, buffer + send_len, len - send_len);//send_len 记录分包的标记
if (ret <= 0) {
//连接出了问题
fputs("may be connect newwork failed,make client write failed!\n", stdout);
close(client);
return;
}
send_len += (size_t)ret;
std::cout << "client write success, msg:" << buffer << std::endl;
}
memset(buffer, 0, sizeof(buffer));
//当数据量很大时,并不能一次把所有数据全部读取完,因此需要分包读取
size_t read_len = 0;
while (read_len < len)
{
size_t ret = read(client, buffer + read_len, len - read_len);
if (ret <= 0) {
//连接出了问题
fputs("may be connect newwork failed, make client read failed!\n", stdout);
close(client);
return;
}
read_len += (size_t)ret;
}
std::cout << "from server:" << buffer << std::endl;
};
close(client);
std::cout << "client done!" << std::endl;
}
void tw_func(char* option)
{
if (strcmp(option, "1") == 0)
{
tw_tcp_server();
tw_tcp_server();
}
else {
tw_tcp_client();
}
}
- メイン関数で呼び出されます:
tw_func(argv[1]);//Time-wait超时等待
- 操作結果:
3,TCP_NODELAY
ノードレイ: 遅延はありません。
ネーグルアルゴリズム
発明者である John Nagle にちなんで名付けられたNagle のアルゴリズムは、多数の小さなバッファ メッセージを自動的に連結するために使用されます。このプロセス (nagling と呼ばれます) は、送信する必要があるパケットの数を減らすことによってネットワーク ソフトウェア システムの効率を高めます。
上の図から次の結論が導き出されます。
- 「Nagle アルゴリズムは、前のデータに対する ACK メッセージが受信された場合にのみ、次のデータを送信します。」
TCP ソケットは、デフォルトで Nagle アルゴリズムを使用してデータを交換するため、ACK を受信するまでバッファリングが最大化されます。上の写真の左側がまさにこれです。文字列「Nagle」を送信するには、それを出力バッファに渡します。この時点ではヘッダ文字「N」より前にデータがない(受信するACKがない)ため、すぐに送信されます。その後、文字「N」の ACK メッセージの待ちが開始され、待機中に残りの「agle」が出力バッファに埋められます。次に、文字「N」の ACK メッセージを受信した後、出力バッファリングされた「agle」をデータ パケットにロードして送信します。つまり、1 つのストリングを送信するには、合計 4 つのデータ パケットを渡す必要があります。 - 次に、Nagle アルゴリズムを使用しない場合に文字列「Nagle」を送信するプロセスを分析します。「N」から「e」までの文字が順番に出力バッファに渡されるとします。このときの送信処理はACKの受信の有無とは関係がないため、データは出力バッファに到達したらすぐに送信されます。上図の右側からわかるように、文字列「Nagle」を送信するには合計 10 パケットが必要です。Nagle アルゴリズムを使用しないと、ネットワーク トラフィックに悪影響を及ぼすことがわかります。たとえ1バイトのデータを送信したとしても、ヘッダ情報は数十バイトになる場合があります。したがって、ネットワークの伝送効率を向上させるには、Nagle アルゴリズムを使用する必要があります。
- プログラム内で文字列が出力バッファに渡されるとき、文字列はそのまま渡されるわけではないため、文字列「Nagle」が送信される実際の状況は上図のようになりません。ただし、文字列を構成する文字が一定期間後に出力バッファに転送される場合 (そのようなデータ転送が存在する場合)、上の図と同様の状況が発生する可能性があります。上の図では、送信されるデータが一定間隔で出力バッファに転送されます。
- しかし、Nagle のアルゴリズムは常に適用できるわけではありません。送信されるデータの特性により、ネットワークトラフィックに大きな影響を受けない場合には、Nagleアルゴリズムを使用しない場合の方が、使用する場合よりも送信速度が速くなります。最も代表的なのは「大容量ファイルデータの送信」です。ファイル データを出力バッファに取り込むのにそれほど時間はかからないため、Nagle のアルゴリズムを使用しなくても、出力バッファがいっぱいになったときにパケットが送信されます。データパケット数が増加しないだけでなく、ACKを待たずに連続送信するため、通信速度が大幅に向上します。
- 一般に、Nagle アルゴリズムを適用しないと、伝送速度が向上します。ただし、Nagle アルゴリズムの使用を無条件に放棄すると、過剰なネットワーク トラフィックが増加し、伝送に影響を及ぼします。したがって、データの特性が正確に判断できない場合には、Nagle アルゴリズムを無効にするべきではありません。
- Nagle アルゴリズムは、先ほど述べた「大きなファイル データ」に対して無効にする必要があります。つまり、必要に応じて Nagle アルゴリズムを無効にする必要があります。「Nagle アルゴリズムを使用するかどうかで、ネットワーク トラフィックにほとんど差はありません。Nagle アルゴリズムを使用した場合、通信速度は遅くなります。」 無効化する方法は非常に簡単です。
- 次のコードからもわかるように、ソケット オプション TCP_NODELAY を 1 (true) に変更するだけです。
int opt_val=1;
setsockopt(sock, IPPROTO_TCP,TCP_NODELAY,(void*)&opt_val, sizeof(opt_val));
- Nagle アルゴリズムの設定状況は TCP_NODELAY の値で確認できます。
int opt_val;socklen_t opt_len;
opt_len=sizeof(opt_val);
getsockopt(sock,IPPROTO_TCP,TCP_NODELAY,(void*)&opt_val,&opt_len);
- Nagle アルゴリズムが使用されている場合は、opt val 変数に 0 が保存され、
Nagle アルゴリズムが無効な場合は 1 が保存されます。