この記事の主な内容:
Linux システム アプリケーション プログラミング (5) Linux ネットワーク プログラミング (その 1)
1. ネットワークの基本
1. 2つのネットワークモデルと共通プロトコル
(1) OSI 7層モデル(データネットワーク伝送表現)
- 物理層、データリンク層、ネットワーク層、トランスポート層、セッション層、プレゼンテーション層、アプリケーション層(下から上)
(2) TCP/IP 4層モデル(ネットワーク間通信)
- ネットワークインターフェース層(リンク層)、ネットワーク層、トランスポート層、アプリケーション層
(3) 共通ネットワークプロトコル層
2. エンディアンネス
(1) 2バイトオーダー
(2) バイトオーダー変換機能
3.TCP通信タイミング(ハンドシェイク3回、ウェーブ4回)
以下均为简述,仅针对面试时能够有东西掰扯
(1)「3回の握手」と「4回のウェーブ」とは
- 「スリーウェイ ハンドシェイク」とは、TCP クライアントとサーバーが接続を確立するために 3 回の通信を必要とすることを意味します。
- 「4 波」とは、TCP クライアントとサーバーの接続を切断するのに 4 回の通信が必要であることを意味します。
(2)「3回の握手」と「4回のウェーブ」のプロセス
-
「スリーウェイ ハンドシェイク」: クライアントはサーバーへの接続リクエストを積極的に開始します。つまり、接続を確立するために SYN フラグを送信します。サーバーはリクエストを受信した後、SYN と ACK (応答フラグ) で応答し、同意します。サーバーがクライアントの接続要求を受信したことを示します。クライアントはサーバーの SYN+ACK を受信した後、ACK 応答フラグをサーバーに送信します。サーバーはそれを受信した後、3 ウェイ ハンドシェイクを完了して接続を確立します。
-
「4 つの波」: 一般に、クライアントは積極的に切断します。FIN フラグをサーバーに送信した後、クライアントは半クローズ状態になります (つまり、サーバー データの受信のみが可能ですが、データの送信はできません)。サーバーは応答します。 FIN.end ACK 応答を受信した後、サーバーもクライアントに FIN を送信し、クライアントがサーバーに ACK を応答して接続が切断されるまで、サーバーもセミクローズ状態になります。
实际上,套接字在内核中实现了读、写两个缓冲区,半关闭就是关闭了写缓冲区
-
【補足】 上記の通り、クライアントはハーフクローズ状態ですが、なぜ4波目でサーバーにACKを返信できるのでしょうか?
ハーフクローズは、ソケット内の書き込みバッファをクローズするだけです。この時点では、クライアントとサーバー間のソケット接続はクローズされていません。したがって、ハーフクローズ状態でも、クライアントはサーバーに ACK 確認で応答できます。確立された TCP 接続を通じてパケットを送信し、4 回手を振るプロセスを完了します。
(3) 切断するにはなぜ「4 波」必要なのでしょうか?
- 閉じるのに 4 つのウェーブが必要な TCP 接続の直接の原因: ハーフクローズ
- 理由: 接続を閉じる前に双方が必要な操作を確実に完了できるようにし、ネットワークの不安定による影響を最小限に抑えてデータの信頼性を確保するためです。
2. ソケットネットワークプログラミング
1. ネットワークアドレス構造
2.ソケットプログラミングAPI
(1) ソケットを作成します。
(2) バインディングアドレスbind()
(3) listen()の設定
(4) 接続待ち accept()
(5) 接続を開始する connect()
(6) アドレス多重化setsockopt()の設定
3. 訴訟手続き
本案例参考于抖音up@小飞有点东西《python全栈高级篇》,up的python视频很nb;以下为笔者学习后用C语言描述的版本
1. シンプルな「模擬 Linux ターミナル」v1.0
【開発環境】 ubuntu22.04、CLion
[コアテクノロジー] TCPネットワークプログラミング、サーバーマルチプロセス/マルチスレッド同時実行、スティッキーパケット問題の解決
【事例説明】クライアントはサーバーに接続後、コマンドラインから Linux コマンドを入力し、サーバーによる実行結果がクライアントに送信されます。
[v1.0 コード]複数のプロセスがサーバーの同時実行を実現し、親プロセスが子プロセスをリサイクルしてゾンビ プロセスを回避し、子プロセスがクライアントと通信します。
至此,程序还有BUG未解决——粘包问题
#include "temp.h" //many head files in it
/* 服务器socket结构体 */
struct ServerSocket{
int sockfd; //服务器socket文件描述符
void (* socketBind)(int ,char *,int); //给sockfd绑定地址函数
void (* serverListen)(int , int); //监听sockfd函数
struct ClientSocket (* serverAccept)(int); //建立连接函数
};
/* 客户端socket结构体 */
struct ClientSocket{
int cfd; //建立连接的socket文件描述符
char ip[32]; //客户端IP
int port; //客户端Port
};
/* 服务器socket绑定地址信息函数实现 */
void socketBind(int sockfd,char *ip,int port){
int retn;
/* 初始化地址结构体sockaddr_in */
struct sockaddr_in serAddr = {
.sin_port = htons(port),
.sin_family = AF_INET
};
inet_pton(AF_INET,ip,&serAddr.sin_addr.s_addr);
/* 调用bind()绑定地址 */
retn = bind(sockfd,(struct sockaddr *)&serAddr,sizeof(serAddr));
if(retn == -1){
perror("bind");
exit(-1);
}
printf("<Server> bind address: %s:%d\n",ip,port);
}
/* 服务器socket监听函数实现 */
void serverListen(int sockfd,int n){
int retn;
retn = listen(sockfd,n);
if(retn == -1){
perror("listen");
exit(-1);
}
printf("<Server> listening...\n");
}
/* 服务器建立连接函数实现,返回值为struct ClientSocket结构体 *
* (包括建立连接的socket文件描述符、客户端信息) */
struct ClientSocket serverAccept(int sockfd){
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
struct ClientSocket c_socket;
c_socket.cfd = accept(sockfd,(struct sockaddr *)&clientAddr,&addrLen);
if(c_socket.cfd == -1){
perror("accept");
exit(-1);
}else{
c_socket.port = ntohs(clientAddr.sin_port);
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,c_socket.ip,sizeof(clientAddr));
return c_socket;
}
}
/* 信号处理函数:回收子进程 */
void waitChild(int signum){
wait(NULL);
}
int main(){
/* 初始化服务器socket */
struct ServerSocket ss = {
.serverAccept = serverAccept,
.socketBind = socketBind,
.serverListen = serverListen
};
/* 设置端口复用 */
int optval = 1;
setsockopt(ss.sockfd,SOL_SOCKET,SO_REUSEPORT,&optval,sizeof(optval));
ss.sockfd = socket(AF_INET,SOCK_STREAM,0);
ss.socketBind(ss.sockfd,"192.168.35.128",8880);
ss.serverListen(ss.sockfd,128);
/* 多进程实现服务器并发 */
struct ClientSocket cs; //客户端socket
pid_t pid = 1;
int nread;
while(1){
//循环等待客户端接入
cs = ss.serverAccept(ss.sockfd);
printf("<Server> client connected.(%s:%d)\n",cs.ip,cs.port);
pid = fork(); //创建父子进程
if(pid > 0){
//父进程
close(cs.cfd); //关闭通信的套接字
signal(SIGCHLD,waitChild); //注册信号
continue;
}else if(pid == 0){
//子进程
close(ss.sockfd); //关闭建立连接的socket
while(1){
char *writeBuff = (char *) malloc(2048); //写buff
char *readBuff = (char *) malloc(128); //读buff
FILE *buffFile = NULL; //文件流
while(1) {
nread = read(cs.cfd, readBuff, 128); //读取客户端发过来的命令
/* 对read判空,防止客户端退出后一直收空数据的死循环 */
if (nread == 0) {
printf("<server> client disconnected (%s:%d)\n",cs.ip,cs.port);
break;
}
/* 执行客户端发过来的命令 */
buffFile = popen(readBuff, "r");
fread(writeBuff, 2048, 1, buffFile); //命令执行成功结果读取到writeBuff
if (strlen(writeBuff) == 0) {
write(cs.cfd, "\n", 1);
}else{
write(cs.cfd, writeBuff, strlen(writeBuff)); //结果写回给客户端
}
/* 清空缓存数据,关闭流 */
memset(writeBuff, '\0', strlen(writeBuff));
memset(readBuff, '\0', strlen(readBuff));
pclose(buffFile);
}
return 0;
}
}else{
perror("fork");
exit(-1);
}
}
}
2.TCPスティッキーパケット問題
(1) 粘着パッケージ問題の導入
- v1.0 のサーバーコードは、実行結果が短い ls と dir コマンドのみを実行しており、バグはないようですが、より長い結果で ps -aux コマンドを実行すると、返される結果がさらに長くなっていることがわかります。 . 、クライアントは 1 回の読み取りで読み取りを完了しません (または読み取りが速すぎて、キャッシュが小さすぎるため)。次のコマンドが実行された後、結果は前のコマンドの未完了の内容に接続されます。図に示すように:
- クライアントのデータの読み込みが速すぎる場合や、クライアントが設定したキャッシュが小さすぎる場合は、コードの遅延を利用してデータの読み込みが速すぎることを避け、より大きなキャッシュ領域を設定することで、ある程度スティッキングの問題を回避できます。 、しかし、この解決策は良くありません、遅延は必然的にユーザーエクスペリエンスに影響を与えます、そして過度に大きなキャッシュ領域も現実的ではありません。したがって、TCP スティッキー問題を別の角度から解決する必要があります。
(2) TCPスティッキーパケットの原因
- TCP プロトコルはメッセージではなくバイト ストリームに基づいてデータを送信します。データは水の流れのように送信され、データを区別することが難しいため、複数の独立したデータ パケットが 1 つのデータ パケットに結合されることは避けられません。
- TCP の基礎となる最適化アルゴリズムである Nagle アルゴリズムは、ネットワークの輻輳を回避し、ネットワーク負荷を軽減するように設計されており、複数の小さなデータ パケットを 1 つの大きなデータ パケットに結合して送信することで、ネットワーク トラフィックと送信遅延を軽減します。送信する必要がある小さなデータ パケットが多数ある場合、Nagle アルゴリズムはこれらのデータ パケットを最初にキャッシュし、送信する前にキャッシュ領域で大きなデータ パケットに組み立てようとします。したがって、受信側が受信したデータ パケットを時間内に処理できない場合、または送信側のバッファ領域がいっぱいになっていない場合、TCP パケットのスタック問題が発生します。
(3) 袋のベタつき問題を解決
- 固定パケット長: 送信および読み取りごとに固定サイズ
- データの全長をデータ ヘッダーに追加します。受信側はまずメッセージ ヘッダー内の長さ情報を読み取り、次に長さ情報に基づいて対応する長さのデータを読み取ります。
(实际上也就是<自定义协议>)
- 特別な区切り文字: 特別な区切り文字 (\n や \r\n など) を使用して各データを区切ります
(4) カスタムプロトコル
-
カスタム プロトコルには通常、次の 2 つの部分が含まれます。
-
メッセージ ヘッダー: データ パケットの種類、データ パケットの長さなど、データ パケットの基本情報を記述するために使用されます。
例如:<文件传输>头部可以包括文件类型、文件的md5值、文件的大小等
-
メッセージ本文: テキスト、画像、音声などの特定のデータを保存するために使用されます。
-
-
カスタム プロトコルを設計するときは、次の原則に従う必要があります。
- 新しいメッセージ タイプやフィールドを簡単に追加できるように、プロトコルは拡張可能である必要があります。
- メッセージの形式は明確で仕様に準拠している必要があり、固定長、区切り文字、マーカーなどを使用してメッセージの始まりと終わりを識別できます。
- 受信者がメッセージを正しく処理できるように、メッセージ ヘッダーには十分なメタ情報が含まれている必要があります。
- データ漏洩や情報改ざんなどのリスクを回避するために、プロトコル設計ではネットワーク上のセキュリティ問題を考慮する必要があります。
-
カスタム プロトコルは通常、ゲーム開発、組み込みシステム、金融取引、その他のシナリオなど、特定の分野のアプリケーションで使用されます。カスタム プロトコルの設計と実装は、特定のシナリオに基づいて検討する必要があり、ネットワーク プロトコルについて一定の理解を必要とし、プロトコルの信頼性、スケーラビリティ、セキュリティなどの問題に注意を払う必要があります。
3. シンプルな「模擬 Linux ターミナル」v2.0
[サーバー v2.0]データの全長をデータ ヘッダーに追加することにより、クライアントはまずデータの全長を読み取り、この読み取りのサイズを決定し、スティッキー パケットの問題を解決します。
#include "temp.h" //many head files in it
/* 服务器socket结构体 */
struct ServerSocket{
int sockfd; //服务器socket文件描述符
void (* socketBind)(int ,char *,int); //给sockfd绑定地址函数
void (* serverListen)(int , int); //监听sockfd函数
struct ClientSocket (* serverAccept)(int); //建立连接函数
};
/* 客户端socket结构体 */
struct ClientSocket{
int cfd; //建立连接的socket文件描述符
char ip[32]; //客户端IP
int port; //客户端Port
};
/* 数据结构体 */
struct Data{
int headerLenth; //数据头部长度
long dataLenth; //数据长度(命令执行成功的结果长度)
char *dataBody; //数据正文(命令执行成功的结果)
};
/* 服务器socket绑定地址信息函数实现 */
void socketBind(int sockfd,char *ip,int port){
int retn;
/* 初始化地址结构体sockaddr_in */
struct sockaddr_in serAddr = {
.sin_port = htons(port),
.sin_family = AF_INET
};
inet_pton(AF_INET,ip,&serAddr.sin_addr.s_addr);
/* 调用bind()绑定地址 */
retn = bind(sockfd,(struct sockaddr *)&serAddr,sizeof(serAddr));
if(retn == -1){
perror("bind");
exit(-1);
}
printf("<Server> bind address: %s:%d\n",ip,port);
}
/* 服务器socket监听函数实现 */
void serverListen(int sockfd,int n){
int retn;
retn = listen(sockfd,n);
if(retn == -1){
perror("listen");
exit(-1);
}
printf("<Server> listening...\n");
}
/* 服务器建立连接函数实现,返回值为struct ClientSocket结构体 *
* (包括建立连接的socket文件描述符、客户端信息) */
struct ClientSocket serverAccept(int sockfd){
struct sockaddr_in clientAddr;
socklen_t addrLen = sizeof(clientAddr);
struct ClientSocket c_socket;
c_socket.cfd = accept(sockfd,(struct sockaddr *)&clientAddr,&addrLen);
if(c_socket.cfd == -1){
perror("accept");
exit(-1);
}else{
c_socket.port = ntohs(clientAddr.sin_port);
inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,c_socket.ip,sizeof(clientAddr));
return c_socket;
}
}
/* 信号处理函数:回收子进程 */
void waitChild(int signum){
wait(NULL);
}
/* 处理数据的函数,返回值为struct Data */
struct Data dataDealWith(FILE *file){
char *tempBuff = (char *)malloc(8192); //临时buff
long readBytes = 0; //读取的字节数
struct Data data = {
.dataLenth = 0,
.dataBody = NULL
};
/* 处理数据:计算数据正文大小,并保留管道中的数据到data.dataBody(需要动态调整大小) */
while(fread(tempBuff,sizeof(char),8192,file) > 0){
readBytes = strlen(tempBuff)+1; //读到临时buff的字节数
data.dataLenth += readBytes; //数据长度累加readBytes
if(data.dataLenth <= readBytes){
//如果数据长度小于设置的tempBuff大小,直接拷贝
data.dataBody = (char *)malloc(readBytes);
strcpy(data.dataBody,tempBuff);
}else if(data.dataLenth > readBytes){
//如果数据长度大于设置的tempBuff大小,扩容后拼接到后面
data.dataBody = realloc(data.dataBody,data.dataLenth);
strcat(data.dataBody,tempBuff);
}
data.dataBody[strlen(data.dataBody)+1] = '\0';
memset(tempBuff,'\0',8192);
}
free(tempBuff); //释放临时buff
return data;
}
int main(){
/* 初始化服务器socket */
struct ServerSocket ss = {
.serverAccept = serverAccept,
.socketBind = socketBind,
.serverListen = serverListen
};
/* 设置端口复用 */
int optval = 1;
setsockopt(ss.sockfd,SOL_SOCKET,SO_REUSEPORT,&optval,sizeof(optval));
ss.sockfd = socket(AF_INET,SOCK_STREAM,0);
ss.socketBind(ss.sockfd,"192.168.35.128",8880);
ss.serverListen(ss.sockfd,128);
/* 多进程实现服务器并发 */
struct ClientSocket cs; //客户端socket
pid_t pid = 1;
int nread;
while(1){
//循环等待客户端接入
cs = ss.serverAccept(ss.sockfd);
printf("<Server> client connected.(%s:%d)\n",cs.ip,cs.port);
pid = fork(); //创建父子进程
if(pid > 0){
//父进程
close(cs.cfd); //关闭通信的套接字
signal(SIGCHLD,waitChild); //注册信号
continue;
}else if(pid == 0){
//子进程
close(ss.sockfd); //关闭建立连接的socket
while(1){
char *readBuff = (char *) malloc(128); //读buff
FILE *buffFile = NULL; //文件流
struct Data data;
char head[8];
while(1) {
nread = read(cs.cfd, readBuff, 128); //读取客户端发过来的命令
/* 对read判空,防止客户端退出后一直收空数据的死循环 */
if (nread == 0) {
printf("<server> client disconnected (%s:%d)\n",cs.ip,cs.port);
break;
}
/* 执行客户端发过来的命令 */
buffFile = popen(readBuff, "r"); //命令执行成功结果读取到writeBuff
data = dataDealWith(buffFile);
sprintf(head,"%ld",data.dataLenth);
write(cs.cfd,head, 8);
write(cs.cfd,data.dataBody,data.dataLenth);
memset(readBuff, '\0', strlen(readBuff));
memset(&data,0,sizeof(data));
pclose(buffFile);
}
exit(1);
}
}else{
perror("fork");
exit(-1);
}
}
}
【クライアントv2.0】
#include "temp.h"
int main(){
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd == -1){
perror("socket");
exit(-1);
}
struct sockaddr_in serAddr = {
.sin_family = AF_INET,
.sin_port = htons(8880)
};
inet_pton(AF_INET,"192.168.35.128",&serAddr.sin_addr.s_addr);
int retn = connect(fd,(struct sockaddr *)&serAddr,sizeof(serAddr) );
if(retn == -1){
perror("connect");
exit(-1);
}
char *writeBuff = (char *)malloc(128);
char *readBuff = (char *)malloc(1024);
char *header = (char *)malloc(8);
int nread = 0;
int dataLength = 0;
while(1){
printf("[email protected]:");
fgets(writeBuff,128,stdin);
if(*writeBuff == ' ' || *writeBuff == '\n'){
continue;
}
write(fd,writeBuff, strlen(writeBuff));
read(fd,header,8);
if(atol(header) == 0)continue;
printf("header:%ld\n", atol(header));
while(dataLength <= atol(header)){
read(fd,readBuff,1024);
dataLength += strlen(readBuff)+1;
printf("%s",readBuff);
memset(readBuff,'\0', 1024);
if(dataLength >= atol(header)){
dataLength = 0;
break;
}
}
memset(header,'\0', strlen(header));
memset(writeBuff,'\0', strlen(writeBuff));
printf("done\n");
}
}