Linux システム アプリケーション プログラミング (5) Linux ネットワーク プログラミング (その 1)

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 つの部分が含まれます。

    1. メッセージ ヘッダー: データ パケットの種類、データ パケットの長さなど、データ パケットの基本情報を記述するために使用されます。

      例如:<文件传输>头部可以包括文件类型、文件的md5值、文件的大小等

    2. メッセージ本文: テキスト、画像、音声などの特定のデータを保存するために使用されます。

  • カスタム プロトコルを設計するときは、次の原則に従う必要があります。

    1. 新しいメッセージ タイプやフィールドを簡単に追加できるように、プロトコルは拡張可能である必要があります。
    2. メッセージの形式は明確で仕様に準拠している必要があり、固定長、区切り文字、マーカーなどを使用してメッセージの始まりと終わりを識別できます。
    3. 受信者がメッセージを正しく処理できるように、メッセージ ヘッダーには十分なメタ情報が含まれている必要があります。
    4. データ漏洩や情報改ざんなどのリスクを回避するために、プロトコル設計ではネットワーク上のセキュリティ問題を考慮する必要があります。
  • カスタム プロトコルは通常、ゲーム開発、組み込みシステム、金融取引、その他のシナリオなど、特定の分野のアプリケーションで使用されます。カスタム プロトコルの設計と実装は、特定のシナリオに基づいて検討する必要があり、ネットワーク プロトコルについて一定の理解を必要とし、プロトコルの信頼性、スケーラビリティ、セキュリティなどの問題に注意を払う必要があります。

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");
    }
}

ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/weixin_54429787/article/details/130352887