【Linux网络】Socket编程TCP-建立Echo Server(下)

上篇:【Linux网络】Socket编程TCP-实现Echo Server(上)https://blog.csdn.net/2402_82757055/article/details/154367478?spm=1001.2014.3001.5501

1.实现客户端

1.1 创建套接字

首先就是创建套接字。

// TcpClient.cc文件
#include 
#include "Common.hpp"
#include "InetAddr.hpp"
// Usage: ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        exit(ExitCode::USAGE_ERR);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "创建socket失败" << std::endl;
        exit(ExitCode::SOCKET_ERR);
    }
    return 0;
}

1.2 connect

这里和UDP的客户端一样不需要显式的bind,采用随机端口号的方式,这里也不需要listen也不需要accept,这都是服务器应该做的,客户端只需要直接向目标服务器发起建立连接的请求

这里要用到函数connect,成功返回0,失败返回-1。

这些参数都很眼熟了,不做过多解释。

connect可能会出错,所以在这个退出码里再增加一个CONNECT_ERR。

// Common.hpp文件
enum ExitCode
{
    normal = 0,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    USAGE_ERR,
    CONNECT_ERR
};
// TcpClient.cc文件
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        exit(ExitCode::USAGE_ERR);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "创建socket失败" << std::endl;
        exit(ExitCode::SOCKET_ERR);
    }
    // 2.直接向目标服务器发起建立连接的请求
    InetAddr client(server_ip, server_port); // 这里要主机转网络
    int n = connect(sockfd, client.NetAddrPtr(), client.NetAddrLen());
    if(n < 0)
    {
        std::cout << "connect error" << std::endl;
        exit(ExitCode::CONNECT_ERR);
    }
    return 0;
}

1.3 发收消息

// TcpClient.cc文件
#include 
#include "Common.hpp"
#include "InetAddr.hpp"
#include 
// Usage: ./tcpclient server_ip server_port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        exit(ExitCode::USAGE_ERR);
    }
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "创建socket失败" << std::endl;
        exit(ExitCode::SOCKET_ERR);
    }
    // 2.直接向目标服务器发起建立连接的请求
    InetAddr client(server_ip, server_port); // 这里要主机转网络
    int n = connect(sockfd, client.NetAddrPtr(), client.NetAddrLen());
    if (n < 0)
    {
        std::cout << "connect error" << std::endl;
        exit(ExitCode::CONNECT_ERR);
    }
    while (true)
    {
        // 3.发消息
        std::cout << "Please Enter# ";
        std::string line;
        std::getline(std::cin, line);
        write(sockfd, line.c_str(), line.size());
        // 4.收消息
        char buffer[1024];
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }
    close(sockfd);
    return 0;
}

然后先启动服务端,在启动客户端,启动客户端的瞬间就会显示accept success,如果客户端退出,服务端也会有相应的显示。

2.实现多进程版

2.1 创建子进程

首先就是要把子进程创建出来,用fork函数。

// TcpServer.hpp文件
    void Run()
    {
        if (_isrunning) // 不能重复启动
            return;
        _isrunning = true;
        // a.获取链接
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 没有链接时,accept会被阻塞
            int sockfd = accept(_listen_sockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr local(peer); // 这里需要网络转主机
            LOG(LogLevel::INFO) << "accept success " << local.StringAddr();
            // 执行任务
            // Service(sockfd, local); // 单进程版-只是为了测试
            // 多进程版
            pid_t id = fork(); // 创建子进程
            if (id < 0)
            {
                LOG(LogLevel::FATAL) << "fork error";
                exit(ExitCode::FORK_ERR);
            }
            else if (id == 0) // 子进程
            {
            }
            else // 父进程
            {
            }
        }
        _isrunning = false;
    }

通过之前的学习,我们知道子进程会得到父进程打开的那些文件描述符,具体详解在:【Linux】匿名管道和进程池

子进程除了能看到sockfd,还可以看到listen_sockfd,但是在子进程里我们不想让他访问listen_sockfd,在父进程里,也不需要访问子进程的sockfd,所以这里要关掉(也可以不关,关了更严谨)。

// 多进程版
pid_t id = fork();
if (id < 0)
{
    LOG(LogLevel::FATAL) << "fork error";
    exit(ExitCode::FORK_ERR);
}
else if (id == 0) // 子进程
{
    close(_listen_sockfd);
}
else // 父进程
{
    close(sockfd);
}

子进程就直接执行之前的Service,执行完了就退出。

// 多进程版
pid_t id = fork();
if (id < 0)
{
    LOG(LogLevel::FATAL) << "fork error";
    exit(ExitCode::FORK_ERR);
}
else if (id == 0) // 子进程
{
    close(_listen_sockfd);
    Service(sockfd, local); // 执行任务
    exit(ExitCode::normal);
}
else // 父进程
{
    close(sockfd);
}

2.2 解决僵尸进程和父进程被阻塞问题

父进程应该要等待子进程,否则子进程会变成僵尸进程

else // 父进程
{
    close(sockfd);
    waitpid(id, nullptr,0); // ?
}

但是这里直接用waitpid函数进行等待的话,子进程不退出父进程就会一直阻塞住,这不又变成了单进程了,创建子进程意义何在?

最推荐的解决方法有两个。

方法一:对子进程信号做处理。

子进程会向父进程发送SIGCHLD信号,我们在初始化的时候,让父进程对这个信号进行忽略。

忽略了之后父进程就不会等待子进程了,父进程就一直获取连接,子进程就执行任务,两者就可以并发运行。

方法二:子进程里再创子进程。

// 多进程版
pid_t id = fork();
if (id < 0)
{
    LOG(LogLevel::FATAL) << "fork error";
    exit(ExitCode::FORK_ERR);
}
else if (id == 0) // 子进程
{
    close(_listen_sockfd);
    if(fork() > 0) // 子进程又创建子进程,就是孙子进程
        exit(ExitCode::normal); // 子进程创建好孙子进程自己退出
    Service(sockfd, local); // 执行任务的是孙子进程
    exit(ExitCode::normal);
}
else // 父进程
{
    close(sockfd);
    waitpid(id, nullptr,0); // 回收子进程
}
  • 子进程又创建了自己的子进程,也就是孙子进程,然后子进程自己退出(fork() > 0的情况),子进程退出后,父进程waitpid就会回收子进程,此时就不会出现僵尸进程的情况,waitpid出也不会被阻塞了。
  • 执行任务的是子进程fork出来的进程,也就是孙子进程。
  • 对于孙子进程来说,孙子进程的父进程是子进程,子进程先退出,也就是孙子进程的父进程退出了,相当于孙子进程变成了孤儿进程,孤儿进程会被1号进程“领养”,处理完任务后系统会回收这个孙子进程

先把server启动,再启动两个或者更多client,我这里就启动两个。

我们查看进程状态的时候,就会查到两个孤儿进程,他们父进程PID是1

ps axj | head -1 && ps ajx | grep tcpserver

3.实现多线程版

线程相关讲解在【Linux】线程控制

子线程可以拿到主线程sockfd之类的文件描述符,但是新的线程并没有重新弄一个文件描述符表出来,用的就是进程的,所有线程共享一个文件描述符表,所以线程不可以像进程那样关闭自己不需要的文件。

新线程执行完之后,主线程也要等待这些线程,但这里同样是阻塞式等待,解决方法就是设置线程分离

因为pthread_create的第三个参数Routine函数的参数只能是void* ,但是放在类内会有隐含的this指针,所以Routine函数要设为static,这一点在【Linux】多线程创建及封装 中有详细讲解。

// TcpServer.hpp文件
#pragma once
#include "Common.hpp"
#include "InetAddr.hpp"
#include "MyLog.hpp"
#include 
#include 
#include 
using namespace MyLog;
// const static int backlog = 8;
class TcpServer : public NoCope // TcpServer类public继承NoCope类
{
public:
    TcpServer(uint16_t port)
        : _port(port),
          _listen_sockfd(-1),
          _isrunning(false)
    {
    }
    void Init()
    {
        // 1.创建套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "创建socket失败";
            exit(ExitCode::SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create listen socket success, sockfd: " << _listen_sockfd;
        // 2.bind
        InetAddr local(_port);
        int n = bind(_listen_sockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind 失败";
            exit(ExitCode::BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind succes, sockfd: " << _listen_sockfd;
        // 3.设置listen状态
        n = listen(_listen_sockfd, 8);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen 失败";
            exit(ExitCode::LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen succes";
    }
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            // b.收消息
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                LOG(LogLevel::DEBUG) << "Read message, " << peer.StringAddr() << "# " << buffer;
                // c.发消息
                std::string echo_string = "Server echo$ ";
                echo_string += buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出...";
                close(sockfd);
                break;
            }
            else
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "读取异常";
                close(sockfd);
                break;
            }
        }
    }
    class ThreadData
    {
    public:
        ThreadData(int fd, InetAddr &p, TcpServer *s)
            : sfd(fd), peer(p), self(s)
        {
        }
        int sfd;
        InetAddr &peer; //peer这里是无参构造,要在InetAddr构造函数里加上他的无参构造
        TcpServer *self;
    };
    static void *Routine(void *args)
    {
        pthread_detach(pthread_self()); // 设置线程分离
        ThreadData *td = static_cast(args);
        td->self->Service(td->sfd, td->peer); // 执行任务
        delete td;
        return nullptr;
    }
    void Run()
    {
        if (_isrunning) // 不能重复启动
            return;
        _isrunning = true;
        // a.获取链接
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 没有链接时,accept会被阻塞
            int sockfd = accept(_listen_sockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr local(peer); // 这里需要网络转主机
            LOG(LogLevel::INFO) << "accept success " << local.StringAddr();
            // 多线程版
            pthread_t tid;
            ThreadData *td = new ThreadData(sockfd, local, this);
            int n = pthread_create(&tid, nullptr, Routine, td);
        }
        _isrunning = false;
    }
    ~TcpServer() {}
private:
    uint16_t _port; // 端口号
    int _listen_sockfd;
    bool _isrunning;
};

同样先把server启动,再启动两个或者更多client,我这里就启动两个。

4.实现线程池版

用到的线程池是之前自己实现的:【Linux】线程池

// TcpServer.hpp文件
#pragma once
#include "Common.hpp"
#include "InetAddr.hpp"
#include "MyLog.hpp"
#include "ThreadPool.hpp"
#include 
#include 
#include 
#include 
using namespace MyLog;
using namespace MyThreadPool;
using task_t = std::function;
class TcpServer : public NoCope // TcpServer类public继承NoCope类
{
public:
    TcpServer(uint16_t port)
        : _port(port),
          _listen_sockfd(-1),
          _isrunning(false)
    {
    }
    void Init()
    {
        //...
    }
    void Service(int sockfd, InetAddr &peer)
    {
        //...
    }
    void Run()
    {
        if (_isrunning) // 不能重复启动
            return;
        _isrunning = true;
        // a.获取链接
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(sockaddr_in);
            // 没有链接时,accept会被阻塞
            int sockfd = accept(_listen_sockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr local(peer); // 这里需要网络转主机
            LOG(LogLevel::INFO) << "accept success " << local.StringAddr();
            // 线程池版
            ThreadPool::GetInstance()->Equeue([this, sockfd, &local](){
                this->Service(sockfd, local);
            });
        }
        _isrunning = false;
    }
    ~TcpServer() {}
private:
    uint16_t _port; // 端口号
    int _listen_sockfd;
    bool _isrunning;
};

本篇分享就到这里,拜拜~

posted on 2025-12-09 16:43  ljbguanli  阅读(0)  评论(0)    收藏  举报