Linux system application programming (5) Linux network programming (Part 1)

Linux system application programming (5) Linux network programming (Part 1)

1. Network basics

1. Two network models and common protocols

(1) OSI seven-layer model (data network transmission representation)
  • Physical layer, data link layer, network layer, transport layer, session layer, presentation layer, application layer (bottom to top)
(2) TCP/IP four-layer model (network-to-network communication)
  • Network interface layer (link layer), network layer, transport layer, application layer
(3) Common network protocol layers

Insert image description here

2. Endianness

(1) Two byte orders

Insert image description here

(2) Byte order conversion function

Insert image description here

3.TCP communication timing (three handshakes, four waves)

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

(1) What are "three handshakes" and "four waves"
  • "Three-way handshake" means that the TCP client and server require three communications to establish a connection;
  • "Four waves" means that it takes 4 communications to disconnect the TCP client and server.
(2) The process of "three handshakes" and "four waves"
  • "Three-way handshake": The client actively initiates a connection request to the server, that is, it sends the SYN flag to establish the connection. The server responds with a SYN and ACK (response flag) after receiving the request and agrees, indicating that the server has received the client's connection request. , after the client receives the server SYN+ACK, it sends the ACK response flag to the server. After the server receives it, it completes the three-way handshake to establish the connection.

  • "Four Waves": Generally, the client actively disconnects. After sending the FIN flag to the server, the client is in a semi-closed state (that is, it can only receive server data, but cannot send data); the server replies to the client after receiving the FIN. end ACK response; then the server will also send FIN to the client, and the server will also enter a semi-closed state until the client replies ACK to the server and the connection is disconnected.

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

  • [Supplement] As mentioned above, the client is half-closed. Why can it reply ACK to the server on the fourth wave?

    Half-close only closes the write buffer in the socket. At this time, the socket connection between the client and the server is not closed. Therefore, in the half-closed state, the client can still reply to the server with an ACK confirmation packet through the established TCP connection. Complete the process of waving four times.

(3) Why does it require "four waves" to disconnect?
  • The direct cause of the TCP connection requiring four waves to close: half-closed
  • Why: To ensure that both parties can complete necessary operations before closing the connection, and to minimize the impact caused by network instability to ensure data reliability.

2. Socket network programming

1. Network address structure

Insert image description here

2.Socket programming API

(1) Create a socket socket()

Insert image description here

(2) Binding address bind()

Insert image description here

(3) Set up listening()

Insert image description here

(4) Waiting for connection accept()

Insert image description here

(5) Initiate a connection connect()

Insert image description here

(6) Set address multiplexing setsockopt()

Insert image description here

3. Case procedures

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

1. Simple "Simulated Linux Terminal" v1.0

[Development environment] ubuntu22.04, CLion

[Core Technology] TCP network programming, server multi-process/multi-thread concurrency, solving sticky packet problems

[Case description] After the client connects to the server, it enters a Linux command through the command line, and the result of execution by the server is sent to the client.

[v1.0 code] Multiple processes realize server concurrency, the parent process recycles the child process to avoid zombie processes, and the child process communicates with the client.

至此,程序还有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);
        }
    }
}

Insert image description here

Insert image description here

2.TCP sticky packet problem

(1) Introduction of sticky package problem
  • The server code of v1.0 only executes the ls and dir commands with shorter execution results. It seems that there are no bugs. However, if the ps -aux command is executed with longer results, you can find that the returned results are longer. , the client does not finish reading in one read (or reads too fast, and the cache is too small). After the next command is executed, the result will be connected with the unfinished content of the previous command. As shown in the picture:

Insert image description here

  • For situations where the client reads data too fast or the cache set by the client is too small, although we use delays in the code to avoid reading data too fast and set a larger cache area, we can avoid the sticking problem to a certain extent, but this This solution is not good, the delay will inevitably affect the user experience, and an overly large cache area is also impractical. Therefore, it is necessary to solve the TCP sticky problem from other angles.
(2) Causes of TCP sticky packets
  • The TCP protocol transmits data based on byte streams, not messages. Data is transmitted like water flow, and it is difficult to distinguish between data, so it is inevitable that multiple independent data packets will be glued into one data packet;
  • TCP's underlying optimization algorithm, the Nagle algorithm, is designed to avoid network congestion and reduce network load. It reduces network traffic and transmission delays by merging multiple small data packets into one large data packet for transmission. When there are a large number of small data packets that need to be sent, the Nagle algorithm will cache these data packets first, and try to assemble them into a larger data packet in the cache area before sending. Therefore, if the receiver cannot process the received data packet in time, or the sender's buffer area is not filled, it will cause TCP packet sticking problem.
(3) Solve the problem of sticky bags
  • Fixed packet length: fixed size for each send and read
  • Add the total length of the data to the data header: the receiver first reads the length information in the message header, and then reads the data of the corresponding length based on the length information.(实际上也就是<自定义协议>)
  • Special separator: use special separator (such as \n or \r\n) to separate each piece of data
(4) Custom protocol
  • Custom protocols usually contain two parts:

    1. Message header: used to describe the basic information of the data packet, such as data packet type, data packet length, etc.

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

    2. Message body: used to store specific data, such as text, pictures, audio, etc.

  • When designing a custom protocol, you need to follow the following principles:

    1. The protocol must be extensible so that new message types or fields can be easily added.
    2. The format of the message must be clear and conform to the specification. Fixed length, delimiters, markers, etc. can be used to identify the beginning and end of the message.
    3. Sufficient meta-information should be included in the message header to allow the receiver to process the message correctly.
    4. Protocol design must consider security issues on the network to avoid risks such as data leakage and information tampering.
  • Custom protocols are usually used in applications in specific fields, such as game development, embedded systems, financial transactions and other scenarios. The design and implementation of custom protocols need to be considered based on specific scenarios, require a certain understanding of network protocols, and pay attention to issues such as the reliability, scalability, and security of the protocols.

3. Simple "Simulated Linux Terminal" v2.0

[Server v2.0] By adding the total length of the data to the data header, the client first reads the total length of the data, determines the size of this read, and solves the problem of sticky packets.

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

Insert image description here
Insert image description here
Insert image description here

Guess you like

Origin blog.csdn.net/weixin_54429787/article/details/130352887