Linux系统应用编程(五)Linux网络编程(上篇)

Linux系统应用编程(五)Linux网络编程(上篇)

一、网络基础

1.两个网络模型和常见协议

(1)OSI七层模型(物数网传会表应)
  • 物理层、数据链路层、网络层、传输层、会话层、表示层、应用层(自下到上)
(2)TCP/IP四层模型(网网传应)
  • 网络接口层(链路层)、网络层、传输层、应用层
(3)常见网络协议所属层

在这里插入图片描述

2.字节序

(1)两种字节序

在这里插入图片描述

(2)字节序转换函数

在这里插入图片描述

3.TCP通信时序(三次握手、四次挥手)

以下均为简述,仅针对面试时能够有东西掰扯

(1)什么是"三次握手"和"四次挥手"
  • "三次握手"意思是TCP客户端和服务器建立连接需要3次通信的过程;
  • "四次挥手"意思是TCP客户端和服务器断开连接需要4次通信的过程。
(2)"三次握手"和"四次挥手"的过程
  • “三次握手”:客户端主动向服务器发起连接请求,也就是发送建立连接的标志位SYN,服务器收到该请求同意后回复一个SYN和ACK(应答标志位),表示服务器收到客户端的连接请求,客户端收到服务器SYN+ACK后,再向服务器发送ACK应答标志位,等到服务器收到后就完成了三次握手建立连接。

  • “四次挥手”:一般由客户端主动断开,发送FIN标志位给服务器后,客户端处于半关闭状态(也就是只能接收服务器数据,而不能发送数据);服务器接收到FIN后回复客户端ACK应答;接着服务器也会发送FIN给客户端,同时服务器也进入半关闭状态,直到客户端回复ACK给到服务器,连接断开。

    实际上,套接字在内核中实现了读、写两个缓冲区,半关闭就是关闭了写缓冲区

  • 【补充】上面说到客户端处于半关闭,为什么可以在第四挥手时给服务器回复ACK?

    半关闭只是关闭socket中的写缓冲区,此时客户端和服务器的socket连接并没有关闭,因此,在半关闭状态下,客户端仍然可以通过已经建立好的TCP连接给服务器回复ACK确认包来完成四次挥手的过程。

(3)为什么断开连接需要"四次挥手"
  • 导致TCP连接关闭需要四次挥手的直接原因:半关闭
  • 为什么:为了确保双方在关闭连接之前都能够完成必要的操作,并尽可能地减少因网络不稳定性造成的影响,以保证数据的可靠性。

二、Socket网络编程

1.网络地址结构体

在这里插入图片描述

2.Socket编程API

(1)创建套接字socket( )

在这里插入图片描述

(2)绑定地址bind( )

在这里插入图片描述

(3)设置监听listen( )

在这里插入图片描述

(4)等待连接accept( )

在这里插入图片描述

(5)发起连接connect( )

在这里插入图片描述

(6)设置地址复用setsockopt( )

在这里插入图片描述

三、案例程序

本案例参考于抖音up@小飞有点东西《python全栈高级篇》,up的python视频很nb;以下为笔者学习后用C语言描述的版本

1.简易"模拟Linux终端"v1.0

【开发环境】 ubuntu22.04、CLion

【核心技术】 TCP网络编程、服务器多进程/多线程并发、解决粘包问题

【案例描述】 client接入server后,通过命令行输入Linux命令,由server执行后的结果发送给client。

【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执行结果较短的命令,看似没有BUG,但是如果执行的是像ps -aux命令结果较长的,就可以发现,由于返回的结果较长,客户端一次读取并没有读取完(或者读取太快、缓存太小),当下一条命令执行后,结果就会和上一条命令没有读取完的内容连在一起。如图:

在这里插入图片描述

  • 针对客户端读取数据太快,或客户端设置的缓存太小,虽然我们在代码中,用延时避免读取数据太快、设置较大的缓存区可以一定程度避免粘包问题,但是这种解决方法并不好,延时难免影响用户体验,过大的缓存区也不切实际。所以,需要从其他角度解决TCP的粘包问题。
(2)TCP粘包产生原因
  • TCP协议基于字节流传输数据,并不是基于消息,数据类似水流传输着,数据之间难以区分,所以不可避免出现将多个独立的数据包粘成一个数据包的情况;
  • TCP为了避免网络拥塞,减少网络负载而设计的底层优化算法Nagle算法,通过将多个小数据包合并成一个大数据包进行发送,以减少网络流量和传输延迟。当有大量小数据包需要发送时,Nagle算法会将这些数据包先缓存起来,并在缓存区中尝试组装成一个更大的数据包再进行发送。所以如果接收方不能及时地处理接收到的数据包,或者发送方的缓存区未被填满,那么就会导致TCP粘包问题的产生。
(3)解决粘包问题
  • 固定数据包的长度:每次发送读取都固定大小
  • 在数据头部加入数据的总长度:接收方先读取消息头中的长度信息,再根据长度信息读取对应长度的数据(实际上也就是<自定义协议>)
  • 特殊分割符:使用特殊的分割符(如\n或者\r\n)来分割每条数据
(4)自定义协议
  • 自定义协议通常包含两部分内容:

    1. 消息头:用于描述数据包的基本信息,如数据包类型、数据包长度等。

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

    2. 消息体:用于存储具体的数据,如文本、图片、音频等。

  • 设计自定义协议时,需要遵循以下几个原则:

    1. 协议必须是可扩展的,能够容易地添加新的消息类型或字段。
    2. 消息的格式必须明确并符合规范,可以使用固定长度、分隔符、标记等方式来辨别消息的开始和结束。
    3. 在消息头中要包含足够的元信息,能够让接收方对消息进行正确的处理。
    4. 协议设计必须考虑网络上的安全问题,避免数据泄露和信息篡改等风险。
  • 自定义协议通常用于特定领域的应用,如游戏开发、嵌入式系统、金融交易等场景。自定义协议的设计和实现需要结合具体场景进行考虑,需要对网络协议有一定的了解,并且需要注意协议的可靠性、可扩展性和安全性等问题。

3.简易"模拟Linux终端"v2.0

【Server 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);
        }
    }
}

【Client 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