【Linux网络篇】:简单的TCP网络程序编写以及相关内容的扩展

news2025/6/2 3:15:06

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:Linux篇–CSDN博客

在这里插入图片描述

文章目录

  • 一.简单的TCP网络程序
    • 相关接口
    • 代码实现
      • 服务器单进程版
      • 服务器多进程版
      • 服务器多线程版
      • 服务器线程池版
    • 应用场景---英译汉
    • 守护进程化
  • 二.补充内容
    • TCP建立连接(三次握手)
    • TCP断开连接(四次挥手)
    • TCP通信的全双工特性

一.简单的TCP网络程序

相关接口

1.socket函数

int socket(int domain, int type, int protocol);
  • 功能:创建套接字
  • 参数
    • domain:协议族,比如AF_INET(IPv4);
    • type:套接字类型,SOCK_DGRAM(UDP),SOCK_STREAM(TCP);
    • protocol:协议,通常为0;
  • 返回值:成功返回套接字描述符sockfd(类似于文件描述符),后续所有的操作都依赖这个描述符;失败返回-1。

2.bind函数

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 功能:将套接字与特定的IP地址和端口绑定

  • 参数

    • sockfd:套接字描述符
    • addr:地址结构体指针
    • addrlen:地址结构体长度
  • 返回值:成功返回0,失败返回-1

  • 使用示例

    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(8080);  // 端口号
    local.sin_addr.s_addr = htonl(INADDR_ANY);  // 任意IP
    
    if (bind(sockfd, (struct sockaddr*)&local, sizeof(local)) < 0) {
        perror("bind error");
        return -1;
    }
    

3.listen函数

int listen(int sockfd, int backlog);
  • 作用:服务端将套接字设置为监听状态,等待客户端连接
  • 参数
    • sockfd:套接字描述符
    • backlog:等待连接队列的最大长度
  • 返回值:成功返回0,失败返回-1。

4.accept函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 作用:服务端接受客户端的连接请求
  • 参数
    • sockfd:监听套接字描述符
    • addr:用于存储客户端地址信息的结构体指针
    • addrlen:地址结构体的长度
  • 返回值:成功返回新的套接字描述符,失败返回-1。

5.connect函数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 作用:客户端用于连接服务器
  • 参数
    • sockfd:套接字描述符
    • addr:服务器地址信息
    • addrlen:地址结构体的长度
  • 返回值:成功返回0,失败返回-1

7.write函数

ssize_t write(int fd, const void *buf, size_t count);
  • 作用:向文件描述符写入数据(因为TCP是面向字节流的,所以可以用该函数发送数据)。
  • 参数
    • fd:文件描述符(套接字描述符)
    • buf:要发送的数据缓冲区
    • count:要发送的字节数
  • 返回值:成功返回实际写入的字节数;失败返回-1,并设置errno

8.read函数

ssize_t read(int fa, void *buf, size_t count);
  • 作用:从文件描述符读取数据(因为TCP是面向字节流的,所以可以用该函数接收数据)。
  • 参数
    • fd:文件描述符(套接字描述符)
    • buf:接收数据的缓冲区
    • count:缓冲区的大小
  • 返回值:成功返回实际读取的字节数;失败返回-1,并设置errno

9.close函数

int close(int sockfd);
  • 功能:关闭套接字
  • 参数
    • sockfd:要关闭的套接字描述符
  • 返回值:成功返回0;失败返回-1。

使用这些函数的基本流程

服务端

1.创建套接字(socket)

2.绑定套接字(bind)

3.开始监听(listen)

4.接受连接(accept)

5.接收数据(read)

6.发送数据(write)

7.关闭连接(close)

客户端

1.创建套接字(socket)

2.连接服务器(connect)

3.发送数据(write)

4.接收数据(read)

5.关闭连接(close)

注意点:客户端并没有绑定套接字的步骤,不代表客户端不用绑定,只是不需要用户来绑定而已,这一过程是由系统来完成的。因为客户端的主要目的是连接服务器,而不是被其他的程序连接,不需要一个固定的,众所周知的端口号,系统会自定分配一个可用的临时端口号。

代码实现

主程序

用来启动服务器

#include "tcpserver.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc){
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n"
              << std::endl;
}

int main(int argc, char *argv[]){
    if(argc != 2){
        Usage(argv[0]);
        exit(0);
    }
    uint16_t serverport = std::stoi(argv[1]);
    std::unique_ptr<TCPServer> tcp_svr(new TCPServer(serverport));

    tcp_svr->InitServer();
    tcp_svr->Run();

    return 0;
}

客户端

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 4096
enum{
    SOCK_ERR=1,
    CONNECT_ERR,
};

void Usage(std::string proc){
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

int main(int argc, char *argv[]){
    if(argc != 3){
        Usage(argv[0]);
        exit(0);
    }
    // 获取服务端的IP地址和端口号
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 填充服务端的网络地址结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    // 创建client socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0){
        std::cerr << "client socket create error..." << std::endl;
        exit(SOCK_ERR);
    }

    // 连接bind client socket 由系统完成bind 随机端口
    // 客户端发起connect的时候,进行自动随机bind
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if(n < 0){
        std::cerr << "client connect error..." << std::endl;
        exit(CONNECT_ERR);
    }

    // 进行通信
    std::string message;
    while(true){
        std::cout << "Please Enter@ ";
        getline(std::cin, message);

        // 发送信息到服务端
        write(sockfd, message.c_str(), message.size());

        // 接收处理后的信息
        char buffer[SIZE];
        ssize_t k = read(sockfd, buffer, sizeof(buffer));
        if(k > 0){
            buffer[k] = 0;
            std::cout << buffer << std::endl;
        }
        else if(k == 0){
            std::cout << "client quit!" << std::endl;
            //break;
        }
        else{
            std::cout << "client read error!" << std::endl;
            //break;
        }
    }
    
    // 关闭套接字描述符
    close(sockfd);
    return 0;
}

服务器单进程版

#pragma once

#include "log.hpp"
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

#define MAXSIZE 4096

Log log;

enum
{
    PORT_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
    LISTER_ERR
};

int backlog = 10;

const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";

class TCPServer{
public:
    TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip)
    : _listensockfd(0), _port(serverport), _ip(serverip)
    {
        if(_port < 1024){
            log(Warning, "Port number %d is too low, please use a port number > 1024", _port);
            exit(PORT_ERR);
        }
    }

    void InitServer(){
        // 1.创建tcp socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0){
            log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));
            exit(SOCKET_ERR);
        }
        log(INFO, "server socket create success, listensockfd: %d", _listensockfd);

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 2.连接tcp socket
        if(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){
            log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);

        // 3.监听listen socket
        // TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensockfd, backlog) < 0){
            log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(LISTER_ERR);
        }
        log(INFO, "server listen success, listensockfd: %d", _listensockfd);
    }

    void Run(){
        log(INFO, "TCPServer is running...");
        while(true){
            // 1.获取新链接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _listensockfd是用来监听的 后续的通信都是使用新的sockfd
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if(sockfd < 0){
                log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);
            
            // 2.根据新链接建立通信
            // version 1 --- 单进程版
            Service(sockfd, clientip, clientport);
            close(sockfd);
        }
    }

    ~TCPServer(){
        if(_listensockfd > 0){
            close(_listensockfd);
        }
    }

private:
    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){
        // 测试代码
        char buffer[MAXSIZE];
        while(true){
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0){
                buffer[n] = 0;
                // 信息处理
                std::cout << "server get a message: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                // 信息发送
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0){
                log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else{
                log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }

private:
    int _listensockfd;
    uint16_t _port;
    std::string _ip;
};

编译后启动服务器和客户端进行通信测试

在这里插入图片描述

在这里插入图片描述

测试现象

先启动右上角第一个客户端进行测试,服务端以及客户端都能正常收到信息;但是当打开右下角第二个客户端进行测试时,发送两条信息后,服务端并没有正常收到信息,以及客户端也没有收到处理后的信息,而一旦把第一个客户端关闭后,第二个客户端之前发送的信息,服务端以及第二个客户端就会立即收到。

原因

目前服务端的根据新链接建立通信这一块使用的是单进程实现的,并且是在一个循环中,只有第一个启动的客户端退出后,才能继续进入下一次的循环,重新获取新链接建立通信。所以这就是为什么第二个启动的客户端发送的信息,刚开始时服务端并没有收到,而是等到第一个客户端退出后才收到信息。

解决方法也有很多种,有多进程,多线程等方法实现。

服务器多进程版

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"

#define MAXSIZE 4096

extern Log log;

enum
{
    PORT_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
    LISTER_ERR
};

int backlog = 10;

const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";

class TCPServer{
public:
    TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip)
    : _listensockfd(0), _port(serverport), _ip(serverip)
    {
        if(_port < 1024){
            log(Warning, "Port number %d is too low, please use a port number > 1024", _port);
            exit(PORT_ERR);
        }
    }

    void InitServer(){
        // 1.创建tcp socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0){
            log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));
            exit(SOCKET_ERR);
        }
        log(INFO, "server socket create success, listensockfd: %d", _listensockfd);

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 2.连接tcp socket
        if(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){
            log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);

        // 3.监听listen socket
        // TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensockfd, backlog) < 0){
            log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(LISTER_ERR);
        }
        log(INFO, "server listen success, listensockfd: %d", _listensockfd);
    }

    void Run(){
        log(INFO, "TCPServer is running...");

        // 启动线程池
        ThreadPool<Task>::GetInstance()->start();

        while (true)
        {
            // 1.获取新链接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _listensockfd是用来监听的 后续的通信都是使用新的sockfd
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if(sockfd < 0){
                log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            // 2.根据新链接建立通信
            // version 1 --- 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);


            version 2 --- 多进程版
            pid_t id = fork();
            if(id == 0){
                 // child
                 close(_listensockfd);
                 // 子进程再创建一个新的孙子进程 然后子进程立即退出
                 if(fork() > 0){
                     exit(0);
                 }

                 // grandson
                 // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收
                 Service(sockfd, clientip, clientport);
                 close(sockfd);
                 exit(0);
             }
             // father
             // 父进程关闭sockfd 然后就只剩下子进程使用sockfd
             close(sockfd);    
             // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...
             pid_t rid = waitpid(id, nullptr, 0); 

        }   
    }

    ~TCPServer(){
        if(_listensockfd > 0){
            close(_listensockfd);
        }
    }

private:
    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){
        // 测试代码
        char buffer[MAXSIZE];
        while(true){
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0){
                buffer[n] = 0;
                // 信息处理
                std::cout << "server get a message: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                // 信息发送
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0){
                log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else{
                log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }

private:
    int _listensockfd;
    uint16_t _port;
    std::string _ip;
};

测试

在这里插入图片描述

服务器多线程版

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"

#define MAXSIZE 4096

extern Log log;

enum
{
    PORT_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
    LISTER_ERR
};

int backlog = 10;

const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";

class TCPServer;

class _ThreadData{
public:
    _ThreadData(const int sockfd, const std::string ip, const uint16_t port, TCPServer *t)
    :sockfd_(sockfd), ip_(ip), port_(port), tsvr_(t)
    {}

public:
    int sockfd_;
    std::string ip_;
    uint16_t port_;
    TCPServer *tsvr_;
};

class TCPServer{
public:
    TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip)
    : _listensockfd(0), _port(serverport), _ip(serverip)
    {
        if(_port < 1024){
            log(Warning, "Port number %d is too low, please use a port number > 1024", _port);
            exit(PORT_ERR);
        }
    }

    void InitServer(){
        // 1.创建tcp socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0){
            log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));
            exit(SOCKET_ERR);
        }
        log(INFO, "server socket create success, listensockfd: %d", _listensockfd);

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 2.连接tcp socket
        if(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){
            log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);

        // 3.监听listen socket
        // TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensockfd, backlog) < 0){
            log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(LISTER_ERR);
        }
        log(INFO, "server listen success, listensockfd: %d", _listensockfd);
    }

    static void *Routine(void *args){
        // 新线程使用线程分离将自己分离出去 主线程就不用再回收 直接继续创建其他的新线程
        pthread_detach(pthread_self());
        _ThreadData *td = static_cast<_ThreadData *>(args);

        // 线程的执行函数是静态的并不能直接访问类内方法 所以将this指针作为成员属性存放到线程信息对象中
        td->tsvr_->Service(td->sockfd_, td->ip_, td->port_);

        // 释放
        delete td;
        return nullptr;
    }

    void Run(){
        log(INFO, "TCPServer is running...");

        // 启动线程池
        ThreadPool<Task>::GetInstance()->start();

        while (true)
        {
            // 1.获取新链接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _listensockfd是用来监听的 后续的通信都是使用新的sockfd
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if(sockfd < 0){
                log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            // 2.根据新链接建立通信
            // version 1 --- 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);


            // version 2 --- 多进程版
            // pid_t id = fork();
            // if(id == 0){
            //     // child
            //     close(_listensockfd);
            //     // 子进程再创建一个新的孙子进程 然后子进程立即退出
            //     if(fork() > 0){
            //         exit(0);
            //     }

            //     // grandson
            //     // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收
            //     Service(sockfd, clientip, clientport);
            //     close(sockfd);
            //     exit(0);
            // }
            // // father
            // // 父进程关闭sockfd 然后就只剩下子进程使用sockfd
            // close(sockfd);    
            // // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...
            // pid_t rid = waitpid(id, nullptr, 0); 


            // version 3 --- 多线程版本
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
            pthread_create(&tid, nullptr, Routine, td);

        }   
    }

    ~TCPServer(){
        if(_listensockfd > 0){
            close(_listensockfd);
        }
    }

private:
    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){
        // 测试代码
        char buffer[MAXSIZE];
        while(true){
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0){
                buffer[n] = 0;
                // 信息处理
                std::cout << "server get a message: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                // 信息发送
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0){
                log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else{
                log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }

private:
    int _listensockfd;
    uint16_t _port;
    std::string _ip;
};

测试

在这里插入图片描述

使用多线程实现,相当于每有一个新的链接,就要创建一个新的线程,所以新线程可能会越来越多

在这里插入图片描述

服务器线程池版

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cstring>
#include <signal.h>
#include "log.hpp"
#include "threadpool.hpp"
#include "Task.hpp"

#define MAXSIZE 4096

extern Log log;

enum
{
    PORT_ERR=1,
    SOCKET_ERR,
    BIND_ERR,
    LISTER_ERR
};

int backlog = 10;

const uint16_t defaultport = 8080;
const std::string defaultip = "0.0.0.0";

class TCPServer;

class _ThreadData{
public:
    _ThreadData(const int sockfd, const std::string ip, const uint16_t port, TCPServer *t)
    :sockfd_(sockfd), ip_(ip), port_(port), tsvr_(t)
    {}

public:
    int sockfd_;
    std::string ip_;
    uint16_t port_;
    TCPServer *tsvr_;
};

class TCPServer{
public:
    TCPServer(const uint16_t serverport = defaultport, const std::string serverip = defaultip)
    : _listensockfd(0), _port(serverport), _ip(serverip)
    {
        if(_port < 1024){
            log(Warning, "Port number %d is too low, please use a port number > 1024", _port);
            exit(PORT_ERR);
        }
    }

    void InitServer(){
        // 1.创建tcp socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0){
            log(Fatal, "server socket create error, listensockfd: %d, strerror: %s", _listensockfd, strerror(errno));
            exit(SOCKET_ERR);
        }
        log(INFO, "server socket create success, listensockfd: %d", _listensockfd);

        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));

        // 2.连接tcp socket
        if(bind(_listensockfd, (const struct sockaddr *)&local, sizeof(local)) < 0){
            log(Fatal, "server bind socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "server bind socket success, listensockfd: %d", _listensockfd);

        // 3.监听listen socket
        // TCP是面向链接的,服务器一般是比较"被动的",服务器一直处于一种,一直在等待连接到来的状态
        if(listen(_listensockfd, backlog) < 0){
            log(Fatal, "server listen socket error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(LISTER_ERR);
        }
        log(INFO, "server listen success, listensockfd: %d", _listensockfd);
    }

    static void *Routine(void *args){
        // 新线程使用线程分离将自己分离出去 主线程就不用再回收 直接继续创建其他的新线程
        pthread_detach(pthread_self());
        _ThreadData *td = static_cast<_ThreadData *>(args);

        // 线程的执行函数是静态的并不能直接访问类内方法 所以将this指针作为成员属性存放到线程信息对象中
        td->tsvr_->Service(td->sockfd_, td->ip_, td->port_);

        // 释放
        delete td;
        return nullptr;
    }

    void Run(){
        log(INFO, "TCPServer is running...");

        // 启动线程池
        ThreadPool<Task>::GetInstance()->start();

        while (true)
        {
            // 1.获取新链接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // _listensockfd是用来监听的 后续的通信都是使用新的sockfd
            int sockfd = accept(_listensockfd, (struct sockaddr *)&client, &len);
            if(sockfd < 0){
                log(Warning, "server accept error, errno: %s, strerror: %s", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            log(INFO, "server get a new link..., sockfd: %d, clientport: %d, clientip: %s", sockfd, clientport, clientip);

            // 2.根据新链接建立通信
            // version 1 --- 单进程版
            // Service(sockfd, clientip, clientport);
            // close(sockfd);


            // version 2 --- 多进程版
            // pid_t id = fork();
            // if(id == 0){
            //     // child
            //     close(_listensockfd);
            //     // 子进程再创建一个新的孙子进程 然后子进程立即退出
            //     if(fork() > 0){
            //         exit(0);
            //     }

            //     // grandson
            //     // 子进程退出后 孙子进程就没有了父进程 被系统领养 由系统进行回收
            //     Service(sockfd, clientip, clientport);
            //     close(sockfd);
            //     exit(0);
            // }
            // // father
            // // 父进程关闭sockfd 然后就只剩下子进程使用sockfd
            // close(sockfd);    
            // // 子进程创建孙子进程退出后,父进程立即回收子进程,然后继续进行下一次循环...
            // pid_t rid = waitpid(id, nullptr, 0); 


            // version 3 --- 多线程版本
            // pthread_t tid;
            // ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
            // pthread_create(&tid, nullptr, Routine, td);


            // version 4 --- 线程池版本
            Task t(sockfd, clientip, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);

        }   
    }

    ~TCPServer(){
        if(_listensockfd > 0){
            close(_listensockfd);
        }
    }

private:
    void Service(int sockfd, const std::string &clientip, const uint16_t &clientport){
        // 测试代码
        char buffer[MAXSIZE];
        while(true){
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0){
                buffer[n] = 0;
                // 信息处理
                std::cout << "server get a message: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                // 信息发送
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0){
                log(INFO, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else{
                log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }

private:
    int _listensockfd;
    uint16_t _port;
    std::string _ip;
};

线程池封装

#pragma once 

#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <queue>
#include <unistd.h>


struct ThreadData{
    pthread_t _tid;
    std::string _threadname;
};

template<class T>
class ThreadPool{
    static const int defaultnum = 5;

private:
    // 申请锁封装
    void Lock(){
        pthread_mutex_lock(&_mutex);
    }

    // 释放锁封装
    void UnLock(){
        pthread_mutex_unlock(&_mutex);
    }

    // 唤醒封装
    void WakeUp(){
        pthread_cond_signal(&_cond);
    }

    // 条件等待封装
    void ThreadSleep(){
        pthread_cond_wait(&_cond, &_mutex);
    }

    // 判断任务队列是否为空封装
    bool IsTaskEmpty(){
        return _tasks.empty();
    }

    std::string GetThreadName(pthread_t tid){
        for(auto td : _threads){
            if(td._tid == tid){
                return td._threadname;
            }
        }
        return "NONE";
    }

public:
    // 线程的执行函数在类内要设置成静态成员函数
    static void *HandlerTask(void *args){
        // 错误写法:线程的执行函数设置成静态成员函数后,就不能再直接访问类内的成员变量和成员函数
        // while(true){
        //     Lock();
        //     while(_task.empty()){
        //         ThreadSleep();
        //     }
        //     T t = _task.front();
        //     _task.pop();
        //     UnLock();
        //     t.run(); // 每个任务对象在类内有自己的执行方法,所以不用在临界区执行
        // }

        // 正确写法:
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());

        while(true){
            tp->Lock();
            // 循环判断条件,防止伪唤醒
            while(tp->IsTaskEmpty()){
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->UnLock();
            t.Run();
        }
    }

    void start(){
        int size = _threads.size();
        for (int i = 0; i < size; i++){
            _threads[i]._threadname = "thread-" + std::to_string(i + 1);
            //pthread_create(&(_threads[i].tid), nullptr, HandlerTask, nullptr);
            // 为了方便线程执行函数直接访问成员变量和成员函数,这里将this指针作为参数传过去
            pthread_create(&(_threads[i]._tid), nullptr, HandlerTask, this);
        }
    }

    // 存放任务到任务队列中
    void Push(const T&t){
        Lock();
        _tasks.push(t);
        WakeUp();
        UnLock();
    }

    // 从任务队列中获取任务
    T Pop(){
        T t = _tasks.front();
        _tasks.pop();
        return t;
    }

    // 懒汉方式单例模式
    // 静态成员函数只能访问静态成员变量 静态成员变量只有一份,在多线程情况下,就变成了共享资源
    // 需要加上互斥机制进行保护
    static ThreadPool<T>* GetInstance(){
        // 因为多线程只会在初次创建单例时才会竞争_tp指针 如果直接在判断外面加锁
        // 会导致创建之后每次使用单例时都要申请锁释放锁 所以采用双层判断空指针 降低锁冲突的概率 提高性能
        if (_tp == nullptr){
            pthread_mutex_lock(&_s_mutex);
            if (_tp == nullptr){
                _tp = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&_s_mutex);
        }

        return _tp;
    }

private:
    ThreadPool(int num = defaultnum)
    :_threads(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }

    // 禁用拷贝构造函数和赋值运算符
    // 禁止通过拷贝构造函数创建新的线程池对象
    ThreadPool(const ThreadPool<T> &) = delete;
    // 禁止通过赋值运算符将一个线程池对象那个赋值给另一个
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;

private:
    std::vector<ThreadData> _threads;  // 线程数组
    std::queue<T> _tasks;              // 任务队列
 
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static ThreadPool<T> *_tp;         // 静态成员变量
    static pthread_mutex_t _s_mutex;   
};

template <class T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;    // 静态成员变量只能在类外初始化

template <class T>
pthread_mutex_t ThreadPool<T>::_s_mutex = PTHREAD_MUTEX_INITIALIZER;

任务封装

#pragma once

#include <iostream>
#include <string>
#include "log.hpp"

extern Log log;

class Task{
public:
    Task(const int sockfd, const std::string ip, const uint16_t port)
        : _sockfd(sockfd), _ip(ip), _port(port)
    {}

    void Run(){
        char buffer[4096];
        while(true){
            ssize_t n = read(_sockfd, buffer, sizeof(buffer));
            if (n > 0){
                buffer[n] = 0;
                // 信息处理
                std::cout << "server get a message: " << buffer << std::endl;
                std::string echo_string = "tcpserver echo# ";
                echo_string += buffer;

                // 信息发送
                n = write(_sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0){
                log(INFO, "%s:%d quit, server close sockfd: %d", _ip.c_str(), _port, _sockfd);
            }
            else{
                log(Warning, "read error, sockfd: %d, clientip: %s, clientport: %d", _sockfd, _ip.c_str(), _port);
            }
        }
    }

    ~Task()
    {}
private:
    int _sockfd;
    std::string _ip;
    uint16_t _port;
};

效果和多线程实现的一样,这里就不再展示。

应用场景—英译汉

直接使用线程池版的服务器来实现:

dict.txt文件

该文件用来存放进行翻译的单词(可以自己添加对应的单词)

apple:苹果
banana:香蕉
red:红色

Init.hpp

用来封装英译汉功能:

#pragma once 
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "log.hpp"

const std::string dictname = "./dict.txt";
const std::string sep = ":";

static bool Split(std::string &line, std::string *part1, std::string *part2){
    auto pos = line.find(sep);
    if(pos == std::string::npos){
        return false;
    }
    // 输出型参数
    *part1 = line.substr(0, pos);
    *part2 = line.substr(pos + 1);
    return true;
}

class Init{
public:
    Init(){
        std::ifstream in(dictname);
        if(!in.is_open()){
            log(Fatal, "ifstream open %s error", dictname.c_str());
            exit(1);
        }
        std::string line;
        while (std::getline(in, line)){
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
        }

        in.close();
    }

    std::string translation(const std::string &key){
        auto iter = dict.find(key);
        if (iter == dict.end()){
            return "UnKnow";
        }
        else{
            return iter->second;
        }
    }

private:
    std::unordered_map<std::string, std::string> dict;
};

修改Task.hpp

将服务端对信息的处理转变为翻译:

在这里插入图片描述

测试

在这里插入图片描述

注意点

如果服务器处理完信息后正在向客户端写入时,此时客户端正好关闭,可能会导致服务器写入失败问题,所以在Task.hpp中需要对服务器写入做异常处理。

在这里插入图片描述

如果服务器写入失败,可能会导致服务器的崩溃以及被系统杀掉,所以在服务器中需要对异常信号做忽略处理

在这里插入图片描述

模拟实现客户端重连现象

为客户端添加一个重连模块:

如果客户端正在通信时,服务器关闭,客户端会重新进行链接,如果链接一定次数后还是失败,就会终止退出。

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 4096
enum{
    SOCK_ERR=1,
    CONNECT_ERR,
};

void Usage(std::string proc){
    std::cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << std::endl;
}

int main(int argc, char *argv[]){
    if(argc != 3){
        Usage(argv[0]);
        exit(0);
    }
    // 获取服务端的IP地址和端口号
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 填充服务端的网络地址结构体
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    while (true)
    {
        // 创建client socket
        int sockfd = 0;
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            std::cerr << "client socket create error..." << std::endl;
            exit(SOCK_ERR);
        }

        int cnt = 5;
        int isreconnect = false;

        // 连接bind client socket 由系统完成bind 随机端口

        do
        {
            // 客户端发起connect的时候,进行自动随机bind
            int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
            if (n < 0)
            {
                isreconnect = true;
                cnt--;
                std::cerr << "client connect error..., reconnect: "<< cnt << std::endl;
                sleep(2);
            }
            else{
                break;
            }
        } while (cnt && isreconnect);

        if(cnt == 0){
            std::cerr << "user ofline..." << std::endl;
            break;
        }

        // 进行通信
        std::string message;
        while (true){
            std::cout << "Please Enter@ ";
            getline(std::cin, message);

            // 发送信息到服务端
            ssize_t s = write(sockfd, message.c_str(), message.size());
            if (s < 0)
            {
                std::cout << "client write error!" << std::endl;
                continue;
            }

            // 接收处理后的信息
            char buffer[SIZE];
            ssize_t k = read(sockfd, buffer, sizeof(buffer));
            if (k > 0)
            {
                buffer[k] = 0;
                std::cerr << buffer << std::endl;
            }
            else if (k == 0)
            {
                std::cout << "client reconnect!" << std::endl;
                break;
            }
            else
            {
                std::cerr << "client read error!" << std::endl;
                break;
            }
        }

        // 关闭套接字描述符
        close(sockfd);
    }

    return 0;
}

在这里插入图片描述

守护进程化

1.前台进程和后台进程

在Linux中每个用户登录时,都会创建一个会话框session,每个session都包含一个bash进程(命令行解释器),刚开始时,bash进程就是前台进程。一个session只能有一个前台进程在运行,键盘信号只能发给前台进程;至于后台进程无法从标准输入获取数据。

谁拥有键盘文件,谁就是前台进程;而前台进程和后台进程都可以向显示器文件打印

  • 以前台方式执行
./process

在这里插入图片描述

可以被键盘输入的ctrl+c终止掉。

  • 以后台方式执行
./process &

后台进程也是向显示器文件打印,为了防止干扰,将打印内容重定向到文件中

在这里插入图片描述

启动时前面的[1]表示后台任务号

  • 查看所有的后台进程
jobs

在这里插入图片描述

  • 将后台进程转变为前台进程
fg 后台任务号

在这里插入图片描述

  • 暂停后台进程

先将后台进程变为前台进程,然后键盘输入ctrl+z

在这里插入图片描述

  • 取消暂停
bg 后台任务号

在这里插入图片描述

2.Linux的进程间关系

在这里插入图片描述

在上面图中,启动两个后台进程,第二个是通过管道启动三个进程,打印对应的进程信息。

其中PGID表示当前进程所属的进程组ID;SID表示当前进程所属的session对话框的ID。

对于./process进程,一个进程就是一个进程组;而对于管道方式启动的三个sleep进程,属于同一个进程组,并且他们的进程组ID还是第一个进程的PID

什么是进程组

  • 一个或多个进程的集合;
  • 每个进程组都有一个唯一的进程ID;
  • 进程组中的第一个进程成为组长进程;
  • 组长进程的PID等于进程组的PGID;

3.进程组和任务之间的关系

在Linux中,任务和进程是同一个概念。

一个任务可以有多个进程共同完成,也可以是一个进程独立完成;而进程组既可以包含一个进程也可以包含多个进程。

一个任务只能属于一个进程组,而一个进程组可以包含多个任务

纠正之前的概念:前台进程,后台进程实际上应该叫做前台任务和后台任务

4.守护进程化

在同一个session会话中执行的指令转变为进程后,每个进程的父进程都是bash进程:

在这里插入图片描述

一旦关闭bash进程后,重新登陆建立一个新的session会话启动一个新的bash进程,再次查看之前的后台任务:

在这里插入图片描述

这些后台进程并没有因为父进程bash的退出而终止掉,而是继续执行,并且失去父进程后,被系统领养变成孤儿进程。

结论就是:这些后台进程会受到用户登录和退出的影响!因为父进程改变!

如果想要创建一个不受到任何用户登录和退出影响的进程—需要守护进程化!

守护进程是一种特殊的后台进程,具有以下特征:

  • 在后台运行
  • 没有控制终端(自成进程组自成会话,不受到任何用户登录和退出的影响)
  • 生命周期长
  • 系统启动时运行,系统关闭时终止

因为守护进程需要创建新的会话,自成一个会话,所以父进程也是系统。

实际上守护进程也是孤儿进程,只不过这种“孤儿化”是有意为之的,目的就是让进程完全脱离控制终端,在后台长期运行

创建新的会话需要用到setsid函数:

pid_t setsid(void);
  • 主要功能
    • 创建一个新的会话
    • 调用进程成为新会话的会话首进程
    • 调用进程成为新进程组的组长进程
    • 调用进程没有控制终端
  • 调用规则
    • 调用进程不能是进程组的组长
    • 如果调用进程是进程组的组长,调用会失败
    • 所以通常要先fork创建子进程,在子进程中调用该函数创建新的会话
  • 作用
    • 使进程完全脱离控制终端
    • 使进程成为新会话的领导者
    • 使进程成为新进程组的组长
    • 确保进程在后台运行
  • 返回值
    • 成功:返回新会话的ID
    • 失败:返回-1,并设置errno
  • 常见用途
    • 创建守护进程
    • 实现后台服务
    • 脱离终端控制
    • 创建独立进程组

将服务器守护进程化

daemon.hpp

#pragma once

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const std::string nullfile = "/dev/null";

void Daemon(const std::string &cwd = ""){
    // 1. 创建子进程
    pid_t id = fork();
    if(id > 0){
        // 父进程直接终止退出
        exit(0);
    }

    // 2. 子进程创建新的会话
    setsid();

    // 3. 更改当前调用进程的工作目录
    if (!cwd.empty()){
        chdir(cwd.c_str());
    }

    // 4. 标准输入 标准输出 标准错误重定向到/dev/null
    int fd = open(nullfile.c_str(), O_RDWR);
    if(fd > 0){
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }

    // 5. 忽略异常信号
    signal(SIGCLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGSTOP, SIG_IGN);
}

tcpserver.hpp

在这里插入图片描述

在这里插入图片描述

重新编译执行:

在这里插入图片描述

启动后的服务器的PID,进程组PGID,会话SID都是同一个4725,说明当前的服务器是自成进程组自成会话的一个后台进程。

因为这里启动时并没有设置指定的工作路径,所以启动后还是在当前调用进程的工作目录下。

在这里插入图片描述

查看一下服务器的三个标准流是否重定向到目标文件/dev/null下;该文件是一个特殊的设备文件,可以理解为一个垃圾站,会自动丢弃写入的数据。

测试时,左边终端启动完服务器后关闭,然后在右边打开一个新的终端,启动客户端然后进行测试:

在这里插入图片描述

再通过指令查看之前的服务器:

在这里插入图片描述

通过信号直接杀掉服务器后再次查看,就找不到之前启动的服务器了:

在这里插入图片描述

如果不想手写守护进程化的代码,可以直接使用系统库中的daemon函数:

函数原型

int daemon(int nochdir, int noclose);

参数说明

  • nochdir
    • 0:将工作目录改为根目录"/"
    • 1:保持当前工作目录不变;
  • noclose
    • 0:将标准输入,输出,错误重定向到/dev/null
    • 1:保持标准输入输出,错误不变;

二.补充内容

TCP建立连接(三次握手)

目的:让客户端和服务器双方都确认彼此的收发功能正常,建立可靠的连接。

步骤

  • 1.第一次握手:客户端—>服务器

    客户端发送SYN(同步)包,告诉服务器“我要连接你”。

  • 2.第二次握手:服务器—>客户端

    服务器收到后,回复SYN+ACK包,表示“我收到了你的请求,我也准备好了”。

  • 第三次握手:客户端—>服务器

    客户端收到SYN+ACK后,再发一个ACK包,表示“我也准备好了”。

结果:连接建立,双方可以开始通信了。

形象比喻

  • 客户端敲门(SYN)
  • 服务器回应“我在家,你是谁?”(SYN+ACK)
  • 客户端说“我是XX,咱们聊聊吧!”(ACK)

TCP断开连接(四次挥手)

目的:让双方都能优雅的关闭连接,确保数据都传完。

步骤

  • 1.第一次挥手:客户端—>服务器

    客户端发送FIN包,表示“我没有数据要发了,可以断开了”。

  • 2.第二次挥手:服务器—>客户端

    服务器收到后,回复ACK包,表示“我知道了”。

  • 3.第三次挥手:服务器—>客户端

    服务器处理完自己的数据后,也发送FIN包,表示“我也没有数据要发了”。

  • 4.第四次挥手:客户端—>服务器

    客户端收到FIN后,回复ACK包,表示“我知道了”。

结果:连接彻底关闭。

形象比喻

  • 客户端说”我说完了“(FIN
  • 服务器点头“我知道了”(ACK
  • 服务器说“我也说完了”(FIN
  • 客户端点头“我知道了”(ACK

总结口诀

  • 三次握手:你来——我应——你再确认
  • 四次挥手:你说完——我知道——我也说完——你知道

TCP通信的全双工特性

先用一个简单的类比来理解什么是TCP全双工通信:

想象两个人通过电话通话:

1.双方可以同时说话和听对方说话

2.不需要等待对方说完才能说话

3.双方都有独立的“说话通道”和“听通道”

在TCP通信中:

1.每个TCP连接都有两个独立的数据流

  • 一个从客户端到服务器
  • 一个从服务器到客户端

2.这两个数据流可以同时工作,互不影响

3.不需要等待一个方向的数据传输完成才能开始另一个方向

而TCP通信的全双工特性与缓冲区机制密切相关:

1.每个TCP连接都有四个缓冲区

  • 发送方的发送缓冲区
  • 发送方的接收缓冲区
  • 接收方的发送缓冲区
  • 接收方的接收缓冲区

2.缓冲区的作用

  • 发送缓冲区:存储待发送的数据
  • 接收缓冲区:存储已接收但还未被应用程序读取的数据

3.为什么能实现全双工

  • 发送和接收使用不同的缓冲区
  • 发送操作不会阻塞接受操作
  • 接受操作不会阻塞发送操作

明白了上面的特性之后,再来思考一个问题:

TCP是面向字节流的,那如何保证,读取上来的数据,是“一个完整”的报文呢

首先,我们使用的readwrite函数只是拷贝函数。包括sendrecv函数也是如此,这些函数并不是网络数据的收发函数,只是实现数据从用户到内核或者从内核到用户的拷贝。

先来理解发送数据:

write发送数据实际上是将数据拷贝到发送方的发送缓冲区,具体决定网络收发的(什么时候发,发多少,出错了怎么办)是TCP协议决定的

因为只有TCP协议了解当前网络的健康状态和接收方的接受能力(之后会讲解),所以必须由TCP协议决定网络数据的收发

而TCP协议属于操作系统的网络模块部分,所以用户把数据交给TCP,本质上就是把数据交给操作系统。就和之前学习文件时的把数据拷贝到struct *file指向的文件缓冲区里一样;把数据发出去,发送到对应端的网络里,这个过程也如同当初学习文件时,把数据从文件缓冲区刷新到磁盘一样;作用都是一样的。

再来理解接收数据:

read接收数据实际上是将数据从接收方的接收缓冲区拷贝到用户自定义的缓冲区,而对方发送多少数据,什么时候发的,有没有一次性全部发送过来这些都是由操作系统(TCP协议)来决定的;至于接收方从接收缓冲区中读取时,读取多少,什么时候读取,一次性读取完还是分批次读取这些都是不确定的了!

所以在接收方的用户应用层中就必须保证把协议定好,这样才能更好的定义读取到的数据分析,保证读取到的是“一个完整”的报文!

所以接下来的学习内容就是用户应用层的协议!

以上就是关于TCP网络程序编写的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2393205.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Scratch节日 | 粽子收集

端午节怎么过&#xff1f;当然是收粽子啦&#xff01;这款 粽子收集 小游戏&#xff0c;让你一秒沉浸节日氛围&#xff0c;轻松收集粽子&#xff0c;收获满满快乐&#xff01; &#x1f3ae; 玩法介绍f 开始游戏&#xff1a;点击开始按钮&#xff0c;游戏正式开始&#xff01;…

stl三角面元文件转颗粒VTK文件

效果展示&#xff1a; import os import sys import json import argparse import numpy as np import pandas as pd import open3d as o3d from glob import globPARTICLE_RADIUS 0.025def stl_to_particles(objpath, radiusNone):if radius is None:radius PARTICLE_RADIU…

Java String的使用续 -- StringBuilder类和StringBuffer

文章目录 字符串的不可变性StringBuilder和StringBuffer函数使用 字符串的不可变性 字符串不可变是因为有private修饰&#xff0c;只能在类的内部使用不可以在类外使用&#xff0c;因此使用时是不可以修改字符串的 public class test {public static void main(String[] args…

基于python+Django+Mysql的校园二手交易市场

文章目录 基于pythonDjangoMysql的校园二手交易市场运行步骤系统设计功能设计任务目标用户特点参与者列表基本要求功能模块图 数据库设计会员用户信息表&#xff08;user_userinfo&#xff09;商品信息表&#xff08;goods_goodsinfo&#xff09;管理员用户信息表&#xff08;a…

从零打造算法题刷题助手:Agent搭建保姆级攻略

我用Trae 做了一个有意思的Agent 「大厂机试助手」。 点击 https://s.trae.com.cn/a/d2a596 立即复刻&#xff0c;一起来玩吧&#xff01; Agent 简介 Agent名称为大厂机试助手&#xff0c;主要功能有以下三点。 解题&#xff1a; 根据用户给出的题目给出具体的解题思路引导做…

懒人云电脑方案:飞牛NAS远程唤醒 + 节点小宝一键唤醒、远程控制Windows!

后台高频问题解答&#xff1a; “博主&#xff0c;飞牛NAS能定时开关机了&#xff0c;能不能让它顺便把家里Windows电脑也远程唤醒控制&#xff1f;最好点一下就能连&#xff0c;不用记IP端口那种&#xff01;” 安排&#xff01;今天这套方案完美实现&#xff1a; ✅ 飞牛NAS…

【Python】第一弹:对 Python 的认知

目录 一、Python 的背景 1.1. Python 的由来 1.2 Python 的作用 1.3 Python 的优缺点 1.4 Python 的开发工具 一、Python 的背景 1.1. Python 的由来 Python 由荷兰数学和计算机科学研究学会的吉多・范罗苏姆 &#xff08;Guido van Rossum&#xff09;在 20 世纪 80 年代…

直播预告 | 聚焦芯必达|打造可靠高效的国产 MCU 与智能 SBC 汽车解决方案

随着汽车电子国产化快速推进&#xff0c;车规级 MCU 与 CAN/LIN SBC 作为车身控制的核心组件&#xff0c;正面临更高的安全与可靠性挑战。品佳集团将携手芯必达微电子&#xff0c;深入剖析国产 MCU/SBC/智能 SBC 的最新技术与应用&#xff0c;助力企业打造高性能、可量产的国产…

Java源码中有哪些细节可以参考?(持续更新)

欢迎来到啾啾的博客&#x1f431;。 记录学习点滴。分享工作思考和实用技巧&#xff0c;偶尔也分享一些杂谈&#x1f4ac;。 有很多很多不足的地方&#xff0c;欢迎评论交流&#xff0c;感谢您的阅读和评论&#x1f604;。 目录 String的比较final的使用transient避免序列化 St…

GelSight Mini触觉传感器:7μm精度+3D 映射,赋能具身智能精密操作

GelSight Mini 高分辨率视触觉传感器采用先进的光学成像与触觉感知技术&#xff0c;赋予机器人接近人类的触觉能力。该设备可捕捉物体表面微观细节&#xff0c;并生成高精度的2D/3D数字映射&#xff0c;帮助机器人识别形状、纹理及接触力&#xff0c;从而执行更复杂、精准的操作…

day 23 机器学习管道(pipeline)

在机器学习领域&#xff0c;“pipeline” 常被翻译为 “管道” 或 “流水线”&#xff0c;它是机器学习中极为重要的概念。在构建机器学习模型时&#xff0c;通常需按特定顺序对数据执行预处理、特征提取、模型训练以及模型评估等步骤&#xff0c;而使用 “pipeline” 能有效管…

鸿蒙仓颉开发语言实战教程:自定义组件

关于仓颉开发语言我们已经连续分享了很多天&#xff0c;相信大家对于仓颉开发语言已经有了一定的了解。今天我们继续进阶&#xff0c;分享一个仓颉开发语言中的自定义组件知识。 本文案例就以上一篇文章中的自定义tabbar为例&#xff0c;因为我们自己开发的tabbar一直放在inde…

基于Spring Boot+Vue 网上书城管理系统设计与实现(源码+文档+部署讲解)

技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…

opencvsharp usb摄像头录像 c# H264编码

1.首先创建ConsoleApp&#xff0c;.Net 9.0&#xff0c;不要创建WinForm 。WInForm帧率和实际对不上&#xff0c;有延时。 2.下载opencvsharp。 3.下载openh264-1.8.0-win32.dll , openh264-1.8.0-win64.dll .放在根目录。 https://github.com/cisco/openh264 using OpenCv…

JavaScript 性能优化按层次逐步分析

JavaScript 性能优化实战 &#x1f4a1; 本文数据基于Chrome 136实测验证&#xff0c;涵盖12项核心优化指标&#xff0c;通过20代码案例演示性能提升300%的实战技巧。 一、代码层深度优化 1. 高效数据操作&#xff08;百万级数据处理&#xff09; // 不良实践&#xff1a;频繁…

【Linux网络篇】:初步理解应用层协议以及何为序列化和反序列化

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;Linux篇–CSDN博客 文章目录 一.序列化和反序列化为什么需要序列化和反序列化为什么应用层…

特伦斯 S75 电钢琴:奏响极致音乐体验的华丽乐章

在音乐爱好者增多、音乐教育普及&#xff0c;以及科技进步的推动下&#xff0c;电钢琴市场蓬勃发展。其在技术、品质和应用场景上变化巨大&#xff0c;高端化、个性化产品受青睐&#xff0c;应用场景愈发多元。在此背景下&#xff0c;特伦斯 S75 电钢琴以卓越性能和独特设计&am…

硬件学习笔记--64 MCU的ARM核架构发展及特点

MCU&#xff08;微控制器&#xff09;的ARM核架构是当前嵌入式系统的主流选择&#xff0c;其基于ARM Cortex-M系列处理器内核&#xff0c;具有高性能、低功耗、丰富外设支持等特点。以下是ARM核MCU的主要架构及其发展&#xff1a; 1. ARM Cortex-M系列内核概览 ARM Cortex-M系…

USB充电检测仪-2.USB充电检测仪硬件设计

本系列文章的最终目标是制作一个USB充电检测仪&#xff0c;支持的功能&#xff1a; 显示USB充电电压、电流、功率、充电量&#xff08;单位WH&#xff09;&#xff1b;实现Typec口和USB-A口的相互转换&#xff08;仅支持USB 2.0&#xff09;&#xff1b; 当然网上有很多卖这种…

AU6815集成音频DSP的2x25W数字型ClaSS D音频功率放大器(替代TAS5805)

1.特性 ● 输出配置 - 立体声 2.0: 2x25W (8Ω,21V,THD N 1%) - 立体声 2.0: 2x23W (6Ω, 18V,THD N 1%) ● 供电电压范围 - PVDD:4.5V-21V - DVDD: 1.8V 或者 3.3V ● 静态功耗 - 31.5mA at PVDD12V,BD - 18.5mA at PVDD12V,1SPW ● 音频性能指标 - Noise: ≤38uVrms - TH…