Linux网络之TCP

Socket编程--TCP

TCP与UDP协议使用的套接字接口比较相似, 但TCP需要使用的接口更多, 细节也会更多.

接口

socket和bind不仅udp需要用到, tcp也需要. 此外还要用到三个函数:

服务端

1. int listen(int sockfd, int backlog);

头文件#include <sys/socket.h>

功能: 将套接字设置为被动监听模式, 等待连接请求

参数:

  • sockfd:指向一个已绑定并且是 SOCK_STREAM 类型的套接字描述符。
  • backlog:内核为此套接字排队的最大连接请求数量

返回值:成功, 返回 0; 失败, 返回 -1,并设置 errno 来指示错误类型

2. int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

功能: 从监听套接字中提取第一个待处理的连接请求, 并为新连接创建一个新的套接字, 用于与客户端通信.

参数:

  • sockfd:用于监听的套接字描述符(通过 listen() 激活)。
  • addr:用于存储客户端地址信息的指针,可为 NULL,不获取客户端信息。
  • addrlen:指向 socklen_t 类型变量的指针,用于指定和接收 addr 的大小。

返回值: 成功, 返回新的套接字描述符, 用于与客户端通信; 失败, 返回 -1, 并设置 errno 来指示错误类型

客户端

3. int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

功能: 主动向指定地址的服务器发起连接请求。

参数:

  • sockfd:已创建的套接字描述符(通常通过 socket() 创建)
  • addr:指向服务器地址的结构体指针(通常是 struct sockaddr_in 或 struct sockaddr_in6)
  • addrlen:addr 结构的大小(以字节为单位)

返回值:成功, 返回 0; 失败, 返回 -1并设置 errno 来指示错误类型

实现TCP通信

下面来实现一个TCP通信的服务端和客户端.

服务端

(1) 准备工作

包括自定义错误码, 设置不可拷贝的类的基类 和 InetAddr用于封装ip和port

/**
 * @file commond.h
 * @brief 自定义错误码
 */
#pragma once
enum UdpError
{
    Usage_Err = 0,
    Socket_Err,
    Bind_Err,
    Recv_Err,
    Sendto_Err,
    Listen_Err,
    Connect_Err
};


/**
 * @file InetAddr.hpp
 * @brief 封装网络地址,包括IP和端口
 */
#pragma once
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>

class InetAddr
{
public:
    InetAddr(const sockaddr_in& sock)
    :_sock(sock)
    {
        sockaddr_in* temp = (sockaddr_in*)&_sock;
        _port = ntohs(temp->sin_port);
        _ip = inet_ntoa(temp->sin_addr);
}

    uint16_t Port() const
    {
        return _port;
    }

    std::string Ip() const
    {
        return _ip;
    }

    std::string Debug()
    {
        std::string temp = "[";
        temp += (_ip + ":" + std::to_string(_port) + "]");
        return temp;
    }
private:
    std::string _ip;
    uint16_t _port;
    sockaddr_in _sock;
};


/**
 * @file NotCopable.hpp
 * @brief 不可拷贝的类
 */
#pragma once
class NotCopable
{
public:
    NotCopable()
    {}
private:
    NotCopable(const NotCopable&) = delete;
    NotCopable& operator=(const NotCopable&) = delete;
};

 (2) 服务端类

注意这里成员我们把tcp服务端维护的socket命名为_listenfd, 这是和udp不同的一部分.

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

#include "NotCopable.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "commond.h"

const int default_backlog = 1000;
class TcpServer : NotCopable
{
public:
    TcpServer(uint16_t port)
        : _port(port),
        _isrunning(false)
    {}

    void Init()
    {}

    void Start()
    {}

    ~TcpServer()
    {}

private:
    int _listenfd;
    uint16_t _port;
    bool _isrunning;
};

(3) 初始化Init

由于TCP属于可靠传输, 它对于网络连接的可靠性比UDP强, 所以它的服务端初始化步骤也会多一些:

  • UDP: socket() -> bind()
  • TCP:socket() -> bind() -> listen()->accept()

socket, bind 和 listen步骤都是一样的, accept是服务器启动(Start)需要的工作

 注意这里多了setsockopt的步骤, 作用是为套接字 _listenfd 启用:

  • 地址复用(SO_REUSEADDR): 服务程序崩溃或正常退出后, 端口可能因 TIME_WAIT 状态被占用.启用后,服务器可以立即重新绑定并启动
  • 端口复用(SO_REUSEPORT):用于多进程/多线程服务共享同一个端口, 提高并发性能.
void Init()
{
    // 1. socket
    _listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_listenfd < 0)
    {
        lg.LogMessage(Fatal, "sock fail %d:%s", errno, strerror(errno));
        exit(Socket_Err);
    }
    lg.LogMessage(Debug, "socket socket success, sockfd: %d\n", _listenfd);
    //解决服务端重启后bind失败问题
    int opt = 1;
    setsockopt(_listenfd, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
    // 2. bind
    struct sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(_port);
    inet_pton(AF_INET, "0.0.0.0", &serverAddr.sin_addr);
    if (bind(_listenfd, (sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
    {
        lg.LogMessage(Fatal, "bind fail %d:%s", errno, strerror(errno));
        exit(Bind_Err);
    }
    lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listenfd);
    // 3. listen
    if (listen(_listenfd, default_backlog) < 0)
    {
        lg.LogMessage(Fatal, "bind fail %d:%s", errno, strerror(errno));
        exit(Listen_Err);
    }
    lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listenfd);
}

 (4) 启动服务器

启动服务器需要使用accept函数等待客户端的连接, 没有连接请求会阻塞等待, 出现连接请求则会返回一个用于通信的fd. 然后在这个fd下进行服务, 服务完成后记得关闭socket.

举个例子, 就像是一些饭店门口的招待一样, 它(listenfd)时刻等待(listen)着客户上门, 一旦有一个客户来了(connect), 它就把客人领进来(accpet)然后交给一个新的服务员(accept的返回值)为它提供服务.

void Start()
{
    _isrunning = true;
    while (_isrunning)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int accfd = accept(_listenfd, (sockaddr *)&client, &len);
        if (accfd < 0)
        {
            lg.LogMessage(Info, "accept fail %d:%s", errno, strerror(errno));
            continue;
        }
        lg.LogMessage(Debug, "accept socket success, accfd: %d\n", accfd);

        //提供服务
        //版本1 -- 单进程
        InetAddr cilentIA(client);
        Service(accfd, cilentIA);
        close(accfd);
    }
    _isrunning = false;
}

这里要注意的是TCP和UDP的工作方式不同:

在 TCP 协议中, 响应连接和提供服务的过程是分开的. 首先, 服务器通过监听套接字等待客户端的连接请求; 当有连接请求时, 服务器通过 accept() 系统调用接受该连接, 并为该连接创建一个新的套接字来进行后续的数据交换. 这意味着每个连接都有一个独立的套接字, 而不是使用同一个套接字进行所有通信. 这种机制确保了每个连接的状态被单独维护, 避免了不同连接之间的干扰.

相比之下, UDP 使用同一个套接字进行数据的发送和接收, 没有连接建立的过程, 数据包是独立的. 因此, UDP 不需要像 TCP 那样为每个连接分配独立的套接字, 适合用于短小、高效的数据传输, 但不保证数据传输的可靠性和顺序.

(5) 提供服务(Service)

这里的服务的具体实现有多种类型可以选择, 先写最简单的版本v1.

1. 与 UDP 不同, TCP 是面向连接的, 不再使用recvfrom(), 而是直接通过连接套接字进行read()操作来接收数据

2. read的返回值

  • 返回值 > 0: 表示成功读取到数据,返回的数值表示实际读取的字节数
  • 返回值 == 0: 表示客户端关闭了连接. 此时, 服务器端可以根据这个信号关闭与客户端的连接, 清理相关资源.
  • 返回值 < 0: 表示发生错误, errno 会提供详细错误信息.

值得注意的是, 类似于管道, read返回值等于0时表示写端关闭; 这里对socket的 read 返回值为0表示客户端关闭连接.

void Service(int sockfd, InetAddr addr)
{
    char buffer[1024];
    while(true)
    {
        ssize_t read_ret = read(sockfd, buffer, sizeof(buffer) - 1);
        if (read_ret > 0)
        {
            buffer[read_ret] = '\0';
            std::cout << addr.Debug() << ":" <<buffer << std::endl;

            std::string echo_string = "server echo# ";
            echo_string += buffer;
            write(sockfd, echo_string.c_str(), echo_string.size());
        }
        else if(read_ret == 0)
        {
            lg.LogMessage(Info, "client quit...\n");
            break;
        }
        else
        {
            lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
            break;
        }
    }
}

 客户端

(1) 主体思路

1. 这里添加了断线重连机制, 默认重连次数为5.

2. 对于服务器的访问封装为了一个函数visitServer.

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include "InetAddr.hpp"
#include "commond.h"

const int reconnect_cnt = 5;

void Usage(char *proc)
{
    std::cout << "Usage: \n\t" << proc << "proc_name serverIp serverPort" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    char* ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    int cnt = 1;
    while(cnt <= reconnect_cnt)
    {
        bool ret =  visitServer(port, ip, &cnt);
        if(ret == false)
        {
            sleep(1);
            std::cout << "client reconnect, cnt = " << cnt++ << std::endl;
        }
        else
        {
            break;
        }
    }
    
    if(cnt > reconnect_cnt)
    {
        std::cout << "server offline" << std::endl;
    }
    return 0;
}

(2) 访问服务器

1. socket和bind是固定, 必不可少的步骤, 注意客户端是自动bind

2. TCP在正式通信之前需要先建立连接, 通过connect与服务端建立连接.

3. 为了契合断线重连的逻辑, 我们把重连次数(pcnt)传递进来, 在每次连接成功后就把它刷新为1, 以实现下次重连时依然是从 1 开始计数.

4. 通信的过程不使用sendto, 而使用 write 直接通过套接字写数据, 客户端直接用socket创建的套接字通信即可.

bool visitServer(uint16_t port, char* ip, int* pcnt)
{
    bool ret = true;
    // 1.socket
    int clientSock = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSock < 0)
    {
        std::cerr << "sock error" << std::endl;
        return false;
    }
    //2. 自动bind

    //3. connect
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &serverAddr.sin_addr);
    int connect_ret = connect(clientSock, (sockaddr *)&serverAddr, sizeof(serverAddr));
    if(connect_ret < 0)
    {
        std::cerr << "connect error" << std::endl;
        ret = false;
        goto END;
    }
    *pcnt = 1;

    //4. 通信
    char buffer[1024];
    while (true)
    {
        std::string msg;
        std::cout << "please Enter: ";
        getline(std::cin, msg);
        ssize_t write_ret = write(clientSock, msg.c_str(), msg.size());
        if (write_ret > 0)
        {
            ssize_t read_ret = read(clientSock, buffer, sizeof(buffer) - 1);
            if (read_ret > 0)
            {
                buffer[read_ret] = '\0';
                std::cout << buffer << std::endl;
            }
            else if (read_ret == 0)
            {
                //server close
                //服务端关闭认为是正常的, 可能本身协议如此
                break;
            }
            else
            {
                //error
                ret = false;
                break;
            }
        }
        else
        {
            //error
            std::cerr << "server may close" << std::endl;
            ret = false;
            break;
        }
    }

END:
    close(clientSock);
    return ret;
}

 测试结果

1. 可以正常的通信了:

 2. 测试断线重连:


改善Service

当前我们的服务器只能同时建立一个客户端的连接, 只有在处理完当前客户端的请求后, 才能去 accept() 下一个连接. 这意味着客户端的请求是串行处理的, 只有前一个请求处理完毕, 后续的客户端才能接入, 从而导致客户端必须排队等待服务器处理, 这会影响服务器的响应效率和吞吐量.

所以我们并不希望让客户端串行等待服务器, 而是希望服务器能够并发地处理多个客户端的请求.

所以这里把服务器改为: 多进程/进程池 和多线程/线程池 的模型

多进程

 每建立一个连接就创建一个子进程, 让父进程只负责accpt接受下一个连接, 子进程只负责去提供服务. 有几个需要注意的地方:

1. 因此父进程就不再需要accfd, 子进程就不再需要listenfd, 推荐的做法是把父子进程都用不到的文件描述符关闭掉, 以防止文件描述符泄漏 : 假如父进程不关闭掉accfd, 随着连接的增多文件描述符会越来越多, 而文件描述符是有限的(由系统参数 ulimit -n 限制).如果长时间不关闭, 文件描述符表可能会耗尽, 导致后续的连接无法建立.

2. 父进程除了建立连接外, 还需要等待子进程回收. 

  • 阻塞式的等待会导致在子进程服务完成之前父进程都处于阻塞状态, 后续的连接无法建立, 程序依然是串行的.
  • 而非阻塞式的等待

解决方案就两种:

1. 在子进程代码中创建一个孙子进程, 子进程直接退出让父进程wait成功, 此时只剩下父进程和孙子进程, 而孙子进程由于父进程退出导致其变成孤儿进程, 被系统领养, 孙子进程退出时资源会被系统回收.

void Start()
{
    _isrunning = true;
    while (_isrunning)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int accfd = accept(_listenfd, (sockaddr *)&client, &len);
        if (accfd < 0)
        {
            lg.LogMessage(Info, "accept fail %d:%s", errno, strerror(errno));
            continue;
        }
        lg.LogMessage(Debug, "accept socket success, accfd: %d\n", accfd);

        //版本2 -- 多进程(父进程等待), 可以并发处理多个客户端
        pid_t id = fork();
        if(id < 0)
        {
            close(accfd);
            continue;
        }
        else if(id == 0)
        {
            //child
            close(_listenfd);
            if(fork() > 0) exit(0);//子进程直接退出
            
            InetAddr cilentIA(client);
            Service(accfd, cilentIA);
            close(accfd);
            exit(0);
        }
        else
        {
            //father
            close(accfd);
            if(waitpid(id, nullptr, 0) < 0)
            {
                std::cerr << "wait fail" << std::endl;
            }
        }
    }
    _isrunning = false;
}

2. 将SIGCHILD信号忽略. Linux下, 将SIGCHILD信号忽略, 子进程退出后自动释放资源, 因此就不涉及父进程的等待了.

void Start()
    {
        _isrunning = true;
        signal(SIGCHLD, SIG_IGN);//Linux下, 将SIGCHILD信号忽略, 子进程退出后自动释放资源
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int accfd = accept(_listenfd, (sockaddr *)&client, &len);
            if (accfd < 0)
            {
                lg.LogMessage(Info, "accept fail %d:%s", errno, strerror(errno));
                continue;
            }
            lg.LogMessage(Debug, "accept socket success, accfd: %d\n", accfd);

            //版本2.2 -- 多进程(信号版), 可以并发处理多个客户端
            pid_t id = fork();
            if(id < 0)
            {
                close(accfd);
                continue;
            }
            else if(id == 0)
            {
                //child
                close(_listenfd);
                //提供服务                
                InetAddr cilentIA(client);
                Service(accfd, cilentIA);
                close(accfd);
                exit(0);
            }
            else
            {
                //father
                close(accfd);
                //父进程不等待
            }
        }
        _isrunning = false;
    }

进程池

为了避免频繁创建和销毁子进程占用系统资源, 可以使用进程池, 将使用固定数量的子进程提前创建好, 等待任务的分配即可.

有bug暂略

多线程

多线程比多进程/进程池的实现更简单, 因为多线程可以共享进程资源, 比如文件描述符表,

1. 由于文件描述符表是共享的, 所以主线程和新线程就不能关闭对应文件描述符, 因为使用的都是同一份资源, 主线程关闭了新线程就无法使用.

2. 为了避免主线程join等待, 将新线程设置为分离状态, 任务处理完就自动释放.

class TcpServer;
class ThreadData
{
public:
    ThreadData(int sockfd, const InetAddr& addr, TcpServer* ts)
    :_sockfd(sockfd)
    ,_addr(addr)
    ,_ts(ts)
    {}

    InetAddr getAddr()
    {
        return _addr;
    }

    int getSock()
    {
        return _sockfd;
    }

    TcpServer* getTcpServer()
    {
        return _ts;
    }

private:
    int _sockfd;
    InetAddr _addr;
    TcpServer* _ts;
};

static void* handlerRequest(void* args)
{
    //1. 初始化
    pthread_detach(pthread_self());
    ThreadData* td = static_cast<ThreadData*>(args);
    TcpServer* ts= td->getTcpServer();
    InetAddr clientAD= td->getAddr();
    int sockfd = td->getSock();
    //2. 提供服务
    ts->Service(sockfd, clientAD);
    //3. 释放资源
    close(sockfd);
    return nullptr;
}

void Start()
{
    _isrunning = true;
    while (_isrunning)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);
        int accfd = accept(_listenfd, (sockaddr *)&client, &len);
        if (accfd < 0)
        {
            lg.LogMessage(Info, "accept fail %d:%s", errno, strerror(errno));
            continue;
        }
        lg.LogMessage(Debug, "accept socket success, accfd: %d\n", accfd);

        //v4 多线程
        InetAddr clientAD(client);
        ThreadData td(accfd, clientAD, this);
        pthread_t tid;
        pthread_create(&tid, nullptr, handlerRequest, &td);
    }
    _isrunning = false;
}

线程池

线程池和进程池要解决的问题类似, 避免线程频繁创建和销毁占用系统资源.

此外在线程池中还要更改一下工作的模式, 实际我们并不能为每一个服务都创建一个死循环去处理, 这样当线程池容量满时后续的连接请求就无法处理, 因此改善为服务端为客户端提供几种可选的服务, 比如 "心跳服务, 英译汉服务, 字符转大写服务 " 供客户端去选择, 服务提供完毕后此连接就被释放了.

1. 服务端可以设置一个注册机制将几个函数保存在一个unordered_map中以供回调.

2. 服务端要提供一个"展示服务列表"的服务提示客户端.

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <cstring>
#include <string>
#include <unordered_map>

#include "NotCopable.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "commond.h"
#include "threadPool.hpp"

class TcpServer;

const int default_backlog = 1000;
class TcpServer : NotCopable
{
    using task_t = std::function<void()>;
    using callback_t  = std::function<void(int, InetAddr)>;
public:
    TcpServer(uint16_t port)
        : _port(port),
          _isrunning(false)
    {}

    void Init()
    {
        // 1. socket
        _listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenfd < 0)
        {
            lg.LogMessage(Fatal, "sock fail %d:%s", errno, strerror(errno));
            exit(Socket_Err);
        }
        lg.LogMessage(Debug, "socket socket success, sockfd: %d\n", _listenfd);
        int opt = 1;
        setsockopt(_listenfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt)); // 解决服务端重启后bind失败问题
        // 2. bind
        struct sockaddr_in serverAddr;
        memset(&serverAddr, 0, sizeof(serverAddr));
        serverAddr.sin_family = AF_INET;
        serverAddr.sin_port = htons(_port);
        inet_pton(AF_INET, "0.0.0.0", &serverAddr.sin_addr);
        if (bind(_listenfd, (sockaddr *)&serverAddr, sizeof(serverAddr)) < 0)
        {
            lg.LogMessage(Fatal, "bind fail %d:%s", errno, strerror(errno));
            exit(Bind_Err);
        }
        lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listenfd);
        // 3. listen
        if (listen(_listenfd, default_backlog) < 0)
        {
            lg.LogMessage(Fatal, "bind fail %d:%s", errno, strerror(errno));
            exit(Listen_Err);
        }
        lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listenfd);
        // 4. 线程池
        ThreadPool<task_t>::GetInstance()->Start();
        // 5. 服务列表注册
        callback_t showService_bind  = std::bind(&TcpServer::showService, this, std::placeholders::_1, std::placeholders::_2);
        Register("default", showService_bind);
    }

    void Service(int sockfd, InetAddr addr)
    {
        //展示服务列表
        _services["default"](sockfd, addr);
        //读取服务
        char buffer[128];
        ssize_t read_ret = read(sockfd, buffer, sizeof(buffer) - 1);
        if (read_ret > 0)
        {
            buffer[read_ret] = '\0';
            string option = buffer;
            lg.LogMessage(Debug, "%s selected %s", addr.Debug().c_str(), option.c_str());
            if(_services.find(option) != _services.end())
                _services[option](sockfd, addr);
            else
            {
                char msg[] = "error option";
                if(write(sockfd, msg, sizeof(msg)) <= 0)
                {
                    lg.LogMessage(Debug, "write error, sockfd: %d\n", _listenfd);
                }
            }
        }
        else if (read_ret == 0)
        {
            lg.LogMessage(Info, "client quit...\n");
            close(sockfd);
        }
        else
        {
            lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
        }
    }

    void showService(int sockfd, InetAddr addr)
    {
        std::string msg = "Service list: ";
        for(const auto& service: _services)
        {
            if(service.first != "default")
            {
                 msg += "| ";
                msg += service.first;
            }
        }
        if(write(sockfd, msg.c_str(), msg.size()) <= 0)
        {
            lg.LogMessage(Debug, "write error");
        }
    }
    
    void Register(const string& name, callback_t func)
    {
        _services[name] = func;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int accfd = accept(_listenfd, (sockaddr *)&client, &len);
            if (accfd < 0)
            {
                lg.LogMessage(Info, "accept fail %d:%s", errno, strerror(errno));
                continue;
            }
            lg.LogMessage(Debug, "accept socket success, accfd: %d\n", accfd);

            // v5 线程池
            InetAddr clientAD(client);
            task_t task = std::bind(&TcpServer::Service, this, accfd, clientAD);
            ThreadPool<task_t>::GetInstance()->Push(task);
        }
        _isrunning = false;
    }

    ~TcpServer()
    {
    }

private:
    int _listenfd;
    uint16_t _port;
    bool _isrunning;
    std::unordered_map<std::string, callback_t> _services;
};

 客户端:

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string>
#include "InetAddr.hpp"
#include "commond.h"

const int reconnect_cnt = 5;

void Usage(char *proc)
{
    std::cout << "Usage: \n\t" << proc << "proc_name serverIp serverPort" << std::endl;
}

bool Read(int sockfd, std::string& out)
{
    bool ret = true;
    char buffer[1024];
    ssize_t read_ret = read(sockfd, buffer, sizeof(buffer) - 1);
    if (read_ret > 0)
    {
        buffer[read_ret] = '\0';
        // std::cout << buffer << std::endl;
        out = std::string(buffer);
    }
    else if (read_ret == 0)
    {
        // server close
        // 服务端关闭认为是正常的, 可能本身协议如此
    }
    else
    {
        // error
        ret = false;
    }
    return ret;
}

bool visitServer(uint16_t port, char *ip, int *pcnt)
{
    // 1.socket
    int clientSock = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSock < 0)
    {
        std::cerr << "sock error" << std::endl;
        close(clientSock);
        return false;
    }
    // 2. 自动bind

    // 3. connect
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &serverAddr.sin_addr);
    int connect_ret = connect(clientSock, (sockaddr *)&serverAddr, sizeof(serverAddr));
    if (connect_ret < 0)
    {
        std::cerr << "connect error" << std::endl;
        close(clientSock);
        return false;
    }
    *pcnt = 1;

    // 4. 通信
    std::string server_list;
    bool ret1 = Read(clientSock, server_list);//读取服务列表
    std::cout << server_list << std::endl;
    //选择服务
    std::string msg;
    std::cout << "Please select service: ";
    getline(std::cin, msg);
    ssize_t write_ret1 = write(clientSock, msg.c_str(), msg.size());//发送服务

    bool ret2 = true, ret3 = true;
    if(write_ret1 > 0)
    {
        //服务发送成功
        std::string warnning; 
        ret2 = Read(clientSock, warnning);//读取服务器的提示信息
        if(warnning == "error option")
        {
            std::cout << warnning << std::endl;
            exit(0);
        }
        else
        {
            std::cout << warnning;
            //发送内容
            std::string content;
            getline(std::cin, content);
            ssize_t write_ret2 = write(clientSock, content.c_str(), content.size());
            if (write_ret2 > 0)
            {
                std::string answer;
                ret3 = Read(clientSock, answer);
                std::cout << answer << std::endl;
            }
            else
            {
                // error
                std::cerr << "server may close" << std::endl;
                ret3 = false;
            }
        }
    }
    else
    {
        std::cerr << "server may close" << std::endl;
        close(clientSock);
        return false;
    }

    close(clientSock);
    return ret1 && ret2 && ret3;
}

int main(int argc, char *argv[])
{
    // 参数
    if (argc != 3)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    char *ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    // 默认短线重连5次
    int cnt = 1;
    while (cnt <= reconnect_cnt)
    {
        bool ret = visitServer(port, ip, &cnt);
        if (ret == false)
        {
            sleep(1);
            std::cout << "client reconnect, cnt = " << cnt++ << std::endl;
        }
        else
        {
            break;
        }
    }

    // 重连失败
    if (cnt > reconnect_cnt)
    {
        std::cout << "server offline" << std::endl;
    }
    return 0;
}


补充问题:

1. IO函数的字节序列问题

我们知道网络字节序列规定都是大端的, 所以我们需要对通过网络传输的数据进行大小端的转换, 比如我们在初始化 socket_in 时都调用了 htons 和 inet_addr(内部自动处理) 对port和ip进行了转换.

但是我们在写Service时, write 和 read 的IO操作时并没有手动转换字节序列?

这是因为IO类函数 (read/write) 内部自动进行了大小端转换.

2. 面向数据报/字节流

 我们知道 UDP是面向数据报的, TCP是面向字节流的. 所以之前的UDP代码是正确的, 但是目前的TCP代码是有bug的.

  • 面向数据报(UDP)的特点是: 数据与数据之间是有边界的, 比如sendto发送了一次, 就一定对应recvfrom接收了一次. 数据之间是很明确的.
  • 面向字节流(TCP): read收的次数和write的次数无关, 读写的步调不一致. 比如write了多次的数据可能read1次就接收完了. 这和管道很类似, 数据的处理必须由用户解析.  

关于面向字节流, 举个例子, 当我们读取一个文件时, 想要划分出其中的单词, 我们对其的操作无非有两种:

  1. 成块的读取整个文件然后手动根据空格分隔单词.
  2. 单字节的读取, 直到读到空格才明确读到了一个单词.

所以我们其实是默认按照文件中对于单词划分的协议(以空格分隔)去处理字节流的.

结论: 我们之前的read操作是自定义了一个缓冲区, 默认读取上来的所有字节流是一次处理, 但我们其实并不知道客户端想要传来多少字节的数据, 可能先发一部分, 再发剩余的部分. 所以TCP中要正确的处理数据, 必须结合用户协议.


守护进程

我们的网络服务器, 不能在 bash 中以前台进程的方式运行, 真正的服务器必须在Linux后台以守护进程(精灵进程)的方式运行.

进程组, 作业与会话

1. 进程组

一个进程自成一个进程组:

1. 当前我启动了一个进程 sleep 100000, 通过ps查看其属性,  发现 进程ID 进程组ID 相同, 这是因为一个进程自成一个进程组

2. 现在我创建了一个新的连接pts/1, 通过管道|, 同时启动两个进程: sleep 10000 | sleep 20000, 通过ps可以看到:

 3. 每次我们登录linux, OS会给登录的用户提供1个shell(通常是bash)和1个terminal, 用于给用户提供命令行解析服务. 把 shell 和 terminal 可以打包为一个会话(session). 

现在启动了三个连接, 就会有三个bash. 由于每个bash是一个进程, 所以每个bash自成一个进程组, 我们还可以发现每个bash自成了一个会话.

4.  所以当前linux服务器上, 任何时刻, 任何一个会话内部, 可以存在多个进程组(用户级任务). 

 5. 但是默认任何时刻, 只允许一个进程组在前台(前台进程组), 前台是和键盘与终端相关, 可以接受IO(主要是input)的就是前台.

a. 现在创建一个后台进程组(作业): sleep 10000 | sleep 20000 | sleep 30000 &, 其中[1]是进程组的编号

注意看此时的bash仍是前台进程组:Ss+ 

b. 使用 fg 进程组编号, 可以把进程变成前台进程. 由于任何时刻, 只允许一个进程组在前台, 所以bash自动被设为后台进程.

 此时bash的状态是Ss, +没了, 表示其不是一个前台进程.

c. Ctrl+Z (SIGTSTP, 20信号)可将该进程组暂停, 此时进程组状态不是Running而是Stopped, 且进程组名后无"&":

 bash又自动恢复成前台进程:

d. bg 进程组编号, 可以将进程组设置为后台进程, 发现"&"添加了进去:

6. 用户级任务VS进程组: 进程组是技术层面的概念, 用户级任务是用户级的概念.

服务端守护进程化

我们在服务器运行的服务不应该受终端窗口的连接断开而关闭, 也就是说, 服务进程不应该依赖于当前终端会话的生命周期, 而应该在后台持续运行. 这种情况下, 服务应该能够独立于终端会话运行, 即使终端关闭或用户退出, 服务进程仍然能够继续运行. 为了实现这一点, 通常会将服务程序设计为守护进程(Daemon), 也叫精灵进程.

守护进程是一个独立的会话, 它不隶属于任何终端会话. 如何把进程设置为守护进程?

通过系统调用setsid, 调用者创建一个新会话, 且此进程组成为该会话的会话领导进程, 同时, 该进程也成为新的进程组的组长.

pid_t setsid(void);

头文件: <sys/types.h> <unistd.h>

参数: 无

返回值:

  • 成功时, 返回新的会话 ID(即会话领导进程的进程PID)
  • 失败时, 返回 -1, 并设置 errno.  如果调用进程已经是某个进程组的组长 或 当前会话的会话领导进程, 调用会失败.

但设置守护进程还有一些其它的步骤:

1. 忽略可能导致进程退出的信号

守护进程隶属于另一个独立的会话, 它的具体运行情况我们这个会话是无从得知的. 此时它就有可能受到异常信号的干扰而退出.

例如,网络服务程序通常需要与多个客户端保持连接. 如果客户端关闭了连接, 而守护进程继续尝试向这个已经关闭的连接写数据, 操作系统就会向守护进程发送 SIGPIPE 信号, 表示它无法继续写入数据. 默认情况下, 进程接收到 SIGPIPE 信号时会退出, 但在守护进程的情况下, 退出进程并非期望的行为. 守护进程应该能够继续运行,即使它与某些进程或客户端的连接已经关闭.

2. 确保进程脱离当前进程组和控制终端

我们就可以fork一个子进程, 然后父进程退出, 此时子进程是一个孤儿进程, 被1号进程领养, 它也就不是组长了. 所以我们也可以认为, 守护进程本质上就是一个孤儿进程

3. 创建一个新的会话并成为会话领导进程

调用setsid

4. 将当前工作目录(CWD)改为根目录以避免影响文件系统

默认情况下, 进程所在的路径是当前目录, 可以通过命令: ll /proc/进程pid/cwd 查看

5. 关闭或重定向标准输入输出和标准错误流

在Linux中存在一个文件: /dev/null, 向该文件中写入和读取的内容会被全部丢弃. 因为守护进程是脱离终端的, 并没有显示器, 键盘等设备文件, 所以可以把0,1,2 重定向到/dev/null. 如果无法重定向, 关闭这三个文件也行. 但推荐重定向

#pragma once
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>
#include <sys/stat.h>
#include <fcntl.h>

const char* root_path = "/";
const char* dev_null = "/dev/null";
void Daemon(bool is_chg_cwd, bool is_close_fd)
{
    //1. 忽略可能引起程序异常退出的信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);
    //2. 让自己不要成为进程组组长
    if(fork() > 0) exit(0);
    //3. 设置让自己成为一个会话, 后面的代码实际上是子进程在执行
    setsid();
    //4. 每一个进程都有自己的CWD, 是否把CWD改为根目录
    if(is_chg_cwd)
        chdir(root_path);
    //5. 已经变为守护进程, 不需要和用户输入输出进行管理了
    if(is_close_fd)
    {
        close(0);
        close(1);
        close(2);
    }
    else
    {
        int fd = open(dev_null, O_RDWR);
        if(fd > 0)
        {
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            close(fd);
        }
    }
}

查看现象:

 1. 可以看到守护进程的父进程是1号进程:

2. ll /proc/820980/fd: 

发现0, 1, 2已经被重定向到/dev/null:

3. ll /proc/820980/cwd, 由于server里设置为false, 所以没有变动.


简单了解TCP

简单了解TCP三次握手 

TCP是面向连接的, 在通信之前需要先连接, TCP的服务端和客户端建立连接采用了"三次握手"的策略. socket中的 connect 是发起了第一次握手(SYN), 剩下的两次握手是双方OS自动进行. connect和accept都会阻塞等待三次握手完成, 握手完成后, connect才能返回从而客户端得到一个建立好连接的socket文件描述符; accept成功返回时, 服务端得到一个新的connfd. 

三次握手的目的是达成共识, 没有三次握手就没有后续的通信行为:

  • 1. 在吗? 我要建立连接
  • 2. 我在, 什么时候建立?
  • 3. 现在建立

TCP四次挥手

1. 客户端和服务端在需要断开连接时需要关闭close文件描述符, 而每次close对应两次挥手, 两次close就对应了四次挥手. 

2. 因为TCP是全双工的, 客户端能向服务端发消息, 反之依成立. 所以客户端对服务端关闭连接并不代表服务端也关闭了对客户端的链接, 所以关闭连接必须要把 "客户端->服务端" 和 "服务端->客户端" 两个朝向的连接都关闭.

3. 四次挥手的目的同样也是建立共识, 建立双方都要断开连接的共识.

系统层面简单理解TCP

1. 既然在操作系统中建立了连接, OS就要对其进行管理. 而"建立连接"是抽象的说法, 实际在系统层面本质是双方操作系统为了维护这条连接, 创建了对应的 "连接" 结构体字段. 所以客观上对于连接的描述, 在OS内部实际是对结构体字段的修改, 多个客户端创建的多个连接实际上是OS创建的多个结构体, 通过链表等方式组织起来. 服务器对于连接的管理变为对链表的增删查改. 

2. 用户层的一个系统调用, OS都为我们做了很多事情: 一个connect触发了三次挥手; 两次close触发了四次挥手. 这些底层工作都不用用户参与.


有了客户端和服务器这个概念, 未来在 TCP通信时, 首先CS双方一定要先建立链接,而TCP通信是双方的地位是对等的, 一旦建立好TCP链接, 客户端服务器可以互发消息. 为什么?

因为客户端和服务端都使用TCP协议, 它们底部会存在一个发送缓冲区和接收缓冲区. 用来发送和接收数据. 而在通信前我们还都会建立一个用户级缓冲区, 比如最常见的一个char类型的buffer. 

所以当我们把数据通过网络发送给server的时候, 它本质上并没有把数据发送到网络中, 而是把数据从用户空间先通过 write/send 拷贝到内核的发送缓冲区中, 拷贝完成之后, write/send 这样的系统调用接口的工作就结束了.

1. write/send只是拷贝函数而已, 发送缓冲区中的数据的行为(什么时候发?发多少?出错怎么办?)都是由内核TCP协议决定. 所以TCP叫做传输控制协议.  

其效果等同于本地对于文件的IO操作, 我们把数据通过系统调用写到文件的内核级缓冲区. 然后OS定期把数据给我们通过一定策略刷新到磁盘.

2. TCP通信的时候实际上是双方的OS在通信, 是把本地的发送缓冲区通过网络拷贝到对方的接收缓冲区. 而read/recv系统调用的功能是把接收缓冲区的数据拷贝到用户空间.

3. read/recv和write/send等系统调用的策略和本地IO一样, 如果read/recv时接受缓冲区内容为空, 则OS会把进程阻塞; 而write/send时发送缓冲区满则OS也会把进程阻塞. 

4. 用户把数据拷贝到发送缓冲区, OS有时间时通过TCP的策略把数据通过网络发送到对方的接收缓冲区. 对于通信双方来说, 数据有人写就有人拿, 这种通信方式很像生产者消费者模型.

5. TCP是面向字节流的, 我在用户层空间以为自己发送了N字节, 但是并不代表接收端一定要一次接收完这N个字节, TCP并不关心我发送的是否是完整的报文, 而只关心我发送了多少字节. 如果想要确定报文是否完整交付, 就要自己定制协议明确报文之间的边界.

UDP是面向数据报的, 它的报文是完整的, 要么就不发, 要发就必须把N个字节全部发给你, 它的内核层没有缓冲区, 我发送的是一个数据报, 接收到的就也要是数据报. 所以UDP关心的是完整的数据报, 它对于数据之间的边界是清晰的, 内核中就已经划清了报文的边界.

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

如何设计浪漫风格的壁纸

一、选择浪漫的色彩 柔和色调&#xff1a; 粉色系&#xff1a;粉色是浪漫的经典色彩&#xff0c;包括淡粉色、玫瑰粉、樱花粉等&#xff0c;能够营造出温馨和甜蜜的氛围。 紫色系&#xff1a;紫色带有神秘和高贵的感觉&#xff0c;如薰衣草紫、淡紫色等&#xff0c;适合营造浪…

PBFT算法

在我的博客中对于RAFT算法也有详细的介绍&#xff0c;raft算法包含三种角色&#xff0c;分别是&#xff1a;跟随者&#xff08; follower &#xff09;&#xff0c;候选人&#xff08;candidate &#xff09;和领导者&#xff08; leader &#xff09;。集群中的一个节点在某一…

第24篇 基于ARM A9处理器用汇编语言实现中断<六>

Q&#xff1a;怎样设计ARM处理器汇编语言程序使用定时器中断实现实时时钟&#xff1f; A&#xff1a;此前我们曾使用轮询定时器I/O的方式实现实时时钟&#xff0c;而在本实验中将采用定时器中断的方式。新增第三个中断源A9 Private Timer&#xff0c;对该定时器进行配置&#…

深度学习笔记——循环神经网络之LSTM

大家好&#xff0c;这里是好评笔记&#xff0c;公主号&#xff1a;Goodnote&#xff0c;专栏文章私信限时Free。本文详细介绍面试过程中可能遇到的循环神经网络LSTM知识点。 文章目录 文本特征提取的方法1. 基础方法1.1 词袋模型&#xff08;Bag of Words, BOW&#xff09;工作…

Linux(Centos、Ubuntu) 系统安装jenkins服务

该文章手把手演示在Linux系统下如何安装jenkins服务、并自定义jenkins数据文件位置、以及jenkins如何设置国内镜像源加速&#xff0c;解决插件下载失败问题 安装方式&#xff1a;war包安装 阿里云提供的war下载源地址&#xff1a;https://mirrors.aliyun.com/jenkins/war/?s…

SQL Server 建立每日自动log备份的维护计划

SQLServer数据库可以使用维护计划完成数据库的自动备份&#xff0c;下面以在SQL Server 2012为例说明具体配置方法。 1.启动SQL Server Management Studio&#xff0c;在【对象资源管理器】窗格中选择数据库实例&#xff0c;然后依次选择【管理】→【维护计划】选项&#xff0…

PHP防伪溯源一体化管理系统小程序

&#x1f50d; 防伪溯源一体化管理系统&#xff0c;品质之光&#xff0c;根源之锁 &#x1f680; 引领防伪技术革命&#xff0c;重塑品牌信任基石 我们自豪地站在防伪技术的前沿&#xff0c;为您呈现基于ThinkPHP和Uniapp精心锻造的多平台&#xff08;微信小程序、H5网页&…

vim如何设置制表符表示的空格数量

:set tabstop4 设置制表符表示的空格数量 制表符就是tab键&#xff0c;一般默认是四个空格的数量 示例&#xff1a; &#xff08;vim如何使设置制表符表示的空格数量永久生效&#xff1a;vim如何使相关设置永久生效-CSDN博客&#xff09;

企业级流程架构设计思路-基于价值链的流程架构

获取更多企业流程资料 纸上得来终觉浅&#xff0c;绝知此事要躬行 一.企业流程分级规则定义 1.流程分类分级的总体原则 2.完整的流程体系需要体现出流程的分类分级 03.通用的流程分级方法 04.流程分级的标准 二.企业流程架构设计原则 1.流程架构设计原则 流程框架是流程体…

刷题总结 回溯算法

为了方便复习并且在把算法忘掉的时候能尽量快速的捡起来 刷完回溯算法这里需要做个总结 回溯算法的适用范围 回溯算法是深度优先搜索&#xff08;DFS&#xff09;的一种特定应用&#xff0c;在DFS的基础上引入了约束检查和回退机制。 相比于普通的DFS&#xff0c;回溯法的优…

【博客之星】年度总结:在云影与墨香中探寻成长的足迹

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、年度回顾 1、创作历程 2、个人成长 3、个人生活与博客事业 二、技术总结 1、赛道选择 2、技术工具 3、实战项目 三、前景与展望 1、云原生未来…

Adobe的AI生成3D数字人框架:从自拍到生动的3D化身

一、引言 随着人工智能技术的发展,我们见证了越来越多创新工具的出现,这些工具使得图像处理和视频编辑变得更加智能与高效。Adobe作为全球领先的创意软件公司,最近推出了一项令人瞩目的新技术——一个能够将普通的二维自拍照转换成栩栩如生的三维(3D)数字人的框架。这项技…

【Nacos】负载均衡

目录 前言 一、服务下线二、权重配置三、同一个集群优先访问四、环境隔离 前言 我们的生产环境相对是比较恶劣的&#xff0c;我们需要对服务的流量进行更加精细的控制.Nacos支持多种负载均衡策略&#xff0c;包括配置权重&#xff0c;同机房&#xff0c;同地域&#xff0c;同环…

回首2024,展望2025

2024年&#xff0c;是个充满挑战与惊喜的年份。在这366个日夜里&#xff0c;我站在编程与博客的交汇点&#xff0c;穿越了无数的风景与挑战&#xff0c;也迎来了自我成长的丰收时刻。作为开发者的第十年&#xff0c;我依然步伐坚定&#xff0c;心中始终带着对知识的渴望与对自我…

net Core Ocelot(1)单地址,多地址

Ocelot 网关技术 》》》配置文件 》》》单地址 {"Routes": [{// 上游 》》 接受的请求//上游请求方法,可以设置特定的 HTTP 方法列表或设置空列表以允许其中任何方法"UpstreamHttpMethod": [ "Get", "Post" ],"UpstreamPathTe…

计算机图形学:实验三 光照与阴影

一、程序功能设计 设置了一个3D渲染场景&#xff0c;支持通过键盘和鼠标控制交互&#xff0c;能够动态调整光源位置、物体材质参数等&#xff0c;具有光照、阴影和材质效果的场景渲染。 OpenGL物体渲染和设置 创建3D物体&#xff1a;代码中通过 openGLObject 结构体表示一个…

22_解析XML配置文件_List列表

解析XML文件 需要先 1.【加载XML文件】 而 【加载XML】文件有两种方式 【第一种 —— 使用Unity资源系统加载文件】 TextAsset xml Resources.Load<TextAsset>(filePath); XmlDocument doc new XmlDocument(); doc.LoadXml(xml.text); 【第二种 —— 在C#文件IO…

JavaScript 数组的map和join方法、延迟函数、location对象、本地存储、正则表达式、箭头函数

数组处理方法 map方法 map方法的作用是遍历数组所有元素&#xff0c;然后执行处理操作&#xff0c;最后返回一个新的数组 语法格式&#xff1a;新数组 原来数组.map(function(ele,index){ ele是数组元素&#xff0c;index是下标 执行完操作之后使用return 返回一个…

物联网网关Web服务器--CGI开发实例BMI计算

本例子通一个计算体重指数的程序来演示Web服务器CGI开发。 硬件环境&#xff1a;飞腾派开发板&#xff08;国产E2000处理器&#xff09; 软件环境&#xff1a;飞腾派OS&#xff08;Phytium Pi OS&#xff09; 硬件平台参考另一篇博客&#xff1a;国产化ARM平台-飞腾派开发板…

【论文+源码】diffuseq使用扩散模型和diffuseq-v2的序列文本生成序列,并且桥接离散和连续的文本空间,用于加速SEQ2SEQ扩散模型。

这篇论文介绍了一种名为DIFFUSEQ的新型扩散模型&#xff0c;专门针对序列到序列&#xff08;SEQ2SEQ&#xff09;文本生成任务进行设计。尽管扩散模型在视觉和音频等连续信号领域取得了成功&#xff0c;但在自然语言处理特别是条件生成方面的适应仍然未被广泛探索。通过广泛的评…