Tcp Socket

目录

[V1版本 - EchoServer](#V1版本 - EchoServer)

[V2 版本 - CommandServer](#V2 版本 - CommandServer)

[inet_ntoa 的线程安全问题](#inet_ntoa 的线程安全问题)

[inet_addr 的设计缺陷](#inet_addr 的设计缺陷)


V1版本 - EchoServer

Tcp Socket 和 Udp Socket 很多地方是类似的,我们重点介绍不同的地方!

第一步 调用 socket 创建套接字(第二个参数选择 SOCK_STREAM 流式服务),第二步 bind,依旧采用 绑定本主机任意 ip 地址的方式,由于 Tcp 是面向连接的,因此在正式通信前要先建立连接,这就要求 服务器先处于 listen(监听) 状态

cpp 复制代码
#include <sys/socket.h>
int listen(int sockfd, int backlog);

参数: sockfd 为 socket 返回的套接字描述符,backlog 表示 套接字内核连接队列的最大长度,这个参数后续会详细介绍

返回值:成功返回0,失败返回-1,错误码被设置

设置完监听状态后,这就意味着如果有客户端发出了连接请求,服务器是能收到的,下来就是 调用 accept 接收请求了!

cpp 复制代码
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

参数: sockfd 为 socket 返回的套接字描述符,addr 是输出型参数,addrlen 是输入输出型参数,这两个参数用于获取连接服务器的当前客户端的地址信息,类比 Udp 中的 recvfrom

返回值:成功返回一个新的套接字文件描述符,专门用于和客户端进行通信,失败返回 -1,如果 accept 失败了,说明接受当前的一个客户端连接失败了,直接继续循环,去 accept 下一个客户端连接即可!

注意,现在出现了两个套接字文件描述符,第一个套接字文件描述符是 socket 的返回值、bind 和 listen 的参数,这个套接字文件描述符应该命名为 _listensockfd,专门用来通信前建立连接的工作,而第二个套接字文件描述符是 accept 的返回值,用于后续服务器和客户端之间的通信!

举个例子:饭店门口有个招揽客人的服务员(张三),张三只负责揽客,在店门外将客人拉拢进饭店里,然后叫专门给客人提供吃饭服务的服务员(李四),接着张三就继续去拉拢客人了,张三就是监听套接字,而李四是后续用来服务器和客户端通信的套接字!

accept 接收请求之后就可以开始处理请求了,我们这一版服务器实现的是简单的消息回显功能!

TcpEchoServer.hpp

我们直接使用上一篇 Udp Socket 封装好的 InetAddr 类

cpp 复制代码
#ifndef __TCP_ECHO_SERVER_HPP__ 
#define __TCP_ECHO_SERVER_HPP__ 

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;

class TcpEchoServer
{
private:
    void HandlerIO(int sockfd, InetAddr client)
    {
        char buffer[1024];
        while(true)
        {
            buffer[0] = 0;
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if(n > 0)
            {
                buffer[n] = 0;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                LOG(LogLevel::INFO) << client.ToString() << "say: " << buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, me too!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error";
                break;
            }
        }
    }

public:
    TcpEchoServer(uint16_t port = 8080)
        :_listensockfd(gdefaultfd), _port(port)
    {}

    void Init()
    {
        //1.create socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        //2.bind socket
        InetAddr local(_port);
        if(bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
        
        //3.set socket listen
        if(listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
    }

    void Start()
    {
        //服务器是一个死循环
        while(true)
        {
            //1. accept socket 
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);      
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept socket error";
                continue; //让服务器继续去accept其他链接
            }
            //2.echo server
            InetAddr client(peer);
            LOG(LogLevel::INFO) << "accept socket success: " << sockfd << ", client addr: " << client.ToString();
            HandlerIO(sockfd, client);
        }
    }

    ~TcpEchoServer(){}
private:
    int _listensockfd;
    uint16_t _port;
};

#endif

HandlerIO 函数就用于读取客户端发送来的消息,并将消息发送回客户端,Tcp 读写消息也有自己专门的接口,我们此处先不做介绍,先使用 read / write 接口就可以完成字节流的读写!

cpp 复制代码
void HandlerIO(int sockfd, InetAddr client)
{
    char buffer[1024];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            std::string echo_string = "server echo# ";
            echo_string += buffer;
            LOG(LogLevel::INFO) << client.ToString() << "say: " << buffer;
            write(sockfd, echo_string.c_str(), echo_string.size());
        }
        else if(n == 0)
        {
            LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, me too!";
            break;
        }
        else
        {
            LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error";
            break;
        }
    }
    close(sockfd); //必须手动关闭文件描述符!
} 

注意,HandlerIO 结束时必须手动关闭文件描述符,因为 Tcp Socket 每次处理一个客户端请求就要调用 一次 accept,得到一个新的文件描述符,如果不关闭历史打开的文件描述符,就会有严重的文件描述符泄漏问题,客户端比较多的话,后续就没有文件描述符可以分配了,而 Udp Socket 中,全程使用的都是同一个 文件描述符,不会存在不够用的情况,但最佳实践还是用完关掉!

必修手动

Tcp客户端依旧是先创建套接字,依旧不需要显式bind,下来就是客户端主动发起连接请求了!

cpp 复制代码
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数: sockfd 为 socket 返回的套接字描述符,addr 和 addrlen 输入型参数,这两个参数用于指定连接服务器的地址信息,类比 Udp 中的 sendto 接口

返回值:成功返回0,失败返回-1,错误码被设置

接下来就是调用 write 和 read 发送和读取消息了!

TcpClient.cc

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "InetAddr.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usgae: " << proc << " serverip serverport" << std::endl;
}

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

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    // 1.create socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_CREATE_ERROR);
    }
    std::cout << "create socket success: " << sockfd << std::endl;

    //2.connect socket
    InetAddr peer(serverport, serverip);
    int n = connect(sockfd, peer.Addr(), peer.Length());
    if(n < 0)
    {
        std::cerr << "connect socket error" << std::endl;
        exit(SOCKET_CONNECT_ERROR);
    }
    std::cout << "connect socket success: " << sockfd << std::endl;

    //3. send messgae
    while(true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line);
        ssize_t n = write(sockfd, line.c_str(), line.size());
        if(n >= 0)
        {
            char buffer[1024];
            ssize_t m = read(sockfd, buffer, sizeof(buffer) - 1);
            if(m > 0)
            {
                buffer[m] = 0;  
                std::cout << buffer << std::endl;
            }
        }
    }
    return 0;
}

上图展示了服务器和客户端的通信功能,确实完成了 echo_server 功能,但是当我们再启动一个客户端,服务器没有打印出任何接受新连接成功的消息,第二个客户端发消息也没有任何反应!

但是当我们终止掉第一个客户端后,服务器立马刷出了 accept 连接成功的消息,并且第二个客户端发送的消息也收到了,第二个客户端也立马收到了服务器回显的消息!

因为目前我们的服务器是单进程/单线程的,处理请求的逻辑是这样的: while(true) { accept;HandlerIO},HandlerIO 是在一个循环中的,客户端1 connect,服务器 accept 了该请求,然后去和客户端1进行收发消息了,只要客户端1没有退,服务器就在 HandlerIO 中,无法再 accept其他请求了,只有客户端1退出了,HandlerIO 中 read 读到0,才会跳出循环,然后继续去 accept 客户端2的连接,才能和客户端2收发消息!

那为啥 Udp Socket 中 也是 单进程 / 单线程 却没有问题呢??因为 Udp 通信不需要建立连接,上来直接 recvfrom 读取消息,然后 sento 发送消息,有消息就读取,没有消息就阻塞在 recvfrom 处,有客户端发来消息,就立马可以读到,而 Tcp 需要先 accept 连接,accept 成功之后处理当前客户端的请求了,就无法再 accept 其他客户端了,其他客户端只能等了!

为了解决这个问题,我们需要引入多进程/多线程

多进程版本:TcpEchoServer.hpp

cpp 复制代码
#ifndef __TCP_ECHO_SERVER_HPP__ 
#define __TCP_ECHO_SERVER_HPP__ 

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;

class TcpEchoServer
{
private:
    void HandlerIO(int sockfd, InetAddr client)
    {
        char buffer[1024];
        while(true)
        {
            buffer[0] = 0;
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if(n > 0)
            {
                buffer[n] = 0;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                LOG(LogLevel::INFO) << client.ToString() << "say: " << buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, me too!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error";
                break;
            }
        }
        close(sockfd); //必须手动关闭文件描述符!
    } 

public:
    TcpEchoServer(uint16_t port = 8080)
        :_listensockfd(gdefaultfd), _port(port)
    {}

    void Init()
    {
        //1.create socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        //2.bind socket
        InetAddr local(_port);
        if(bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
        
        //3.set socket listen
        if(listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
    }

    void Start()
    {
        while(true)
        {
            //1. accept socket 
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);      
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept socket error";
                continue; //让服务器继续去accept其他链接
            }
            //2.echo server
            InetAddr client(peer);
            LOG(LogLevel::INFO) << "accept socket success: " << sockfd << ", client addr: " << client.ToString();
            pid_t pid = fork();
            if(pid < 0)
            {
                LOG(LogLevel::ERROR) << "resource not enough, create child process error";
                exit(FORK_ERROR);
            }
            else if(pid == 0)
            {
                close(_listensockfd); //子进程不再需要_listensockfd, 就关掉
                if(fork() > 0) exit(OK); //子进程再创建孙子进程, 创建成功, 子进程直接退出, waitpid直接返回, 孙子进程被托管!
                HandlerIO(sockfd, client); //孙子进程
                exit(OK);
            }
            else
            {
                //father
                close(sockfd); //父进程不再需要sockfd, 就关掉, 并且防止fd泄漏
                pid_t rid = waitpid(pid, nullptr, 0); 
                (void)rid;
            }
        }
    }

    ~TcpEchoServer(){}
private:
    int _listensockfd;
    uint16_t _port;
};

#endif

多线程版本:TcpEchoServer.hpp

cpp 复制代码
#ifndef __TCP_ECHO_SERVER_HPP__ 
#define __TCP_ECHO_SERVER_HPP__ 

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;
class TcpEchoServer
{
private:
    void HandlerIO(int sockfd, InetAddr client)
    {
        char buffer[1024];
        while(true)
        {
            buffer[0] = 0;
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if(n > 0)
            {
                buffer[n] = 0; 
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                LOG(LogLevel::INFO) << client.ToString() << "say: " << buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, me too!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error";
                break;
            }
        }
        close(sockfd); //必须手动关闭文件描述符!
    } 

public:
    TcpEchoServer(uint16_t port = 8080)
        :_listensockfd(gdefaultfd), _port(port)
    {}

    void Init()
    {
        //1.create socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        //2.bind socket
        InetAddr local(_port);
        if(bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
        
        //3.set socket listen
        if(listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
    }

    struct ThreadData
    {
        ThreadData(int sockfd, TcpEchoServer* self, InetAddr addr)
            :_sockfd(sockfd), _self(self), _addr(addr)
        {}
        int _sockfd;
        TcpEchoServer* _self;
        InetAddr _addr;
    };

    static void* Routine(void* args)
    {
        ThreadData* td = static_cast<ThreadData*>(args);
        pthread_detach(pthread_self()); //将自己设置为分离状态, 主线程就不用等待了, 直接去获取新链接
        td->_self->HandlerIO(td->_sockfd, td->_addr);
        return nullptr;
    }

    void Start()
    {
        while(true)
        {
            //1. accept socket 
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);      
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept socket error";
                continue; //让服务器继续去accept其他链接
            }
            //2.echo server
            InetAddr client(peer);
            LOG(LogLevel::INFO) << "accept socket success: " << sockfd << ", client addr: " << client.ToString();
            
            pthread_t tid;
            ThreadData* td = new ThreadData(sockfd, this, client);
            pthread_create(&tid, nullptr, Routine, td);
        }
    }

    ~TcpEchoServer(){}
private:
    int _listensockfd;
    uint16_t _port;
};

#endif

上述多进程/多线程的代码作为日常 demo 当然没有问题,但创建进程和线程都是有成本的,当客户端都发来消息了,再去创建多进程/多线程有点慢,并且当前代码中进程/线程个数是没有上限限制的,来一个客户端创建一个进程/线程,客户端太多,最后服务器可能就撑不住了,因此我们可以引入线程池,预先创建一批线程,并且线程个数是有上限的,当线程个数到达上限时,客户端发来的消息作为任务就在任务队列中等待,线程可以暂时不处理!

线程池版本:TcpEchoServer.hpp

cpp 复制代码
#ifndef __TCP_ECHO_SERVER_HPP__ 
#define __TCP_ECHO_SERVER_HPP__ 

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;

using task_t = std::function<void()>;

class TcpEchoServer
{
private:
    void HandlerIO(int sockfd, InetAddr client)
    {
        char buffer[1024];
        while(true)
        {
            buffer[0] = 0;
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if(n > 0)
            {
                buffer[n] = 0; 
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                LOG(LogLevel::INFO) << client.ToString() << "say: " << buffer;
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, me too!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error";
                break;
            }
        }
        close(sockfd); //必须手动关闭文件描述符!
    } 

public:
    TcpEchoServer(uint16_t port = 8080)
        :_listensockfd(gdefaultfd), _port(port)
    {}

    void Init()
    {
        //1.create socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        //2.bind socket
        InetAddr local(_port);
        if(bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
        
        //3.set socket listen
        if(listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
    }

    void Start()
    {
        while(true)
        {
            //1. accept socket 
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);      
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept socket error";
                continue; //让服务器继续去accept其他链接
            }
            //2.echo server
            InetAddr client(peer);
            LOG(LogLevel::INFO) << "accept socket success: " << sockfd << ", client addr: " << client.ToString();

            //接入线程池
            ThreadPool<task_t>::GetInstance()->Push([sockfd, this, client](){
                this->HandlerIO(sockfd, client);
            });
        }
    }
    
    ~TcpEchoServer(){}
private:
    int _listensockfd;
    uint16_t _port;
};

#endif

上述的 HanderIO 本质是长任务,因为内部是 while 死循环,来一个客户端请求,线程池中就会有一个线程去处理,但只要客户端不退出,这个线程就要一直处理这个客户端请求,这就叫做长任务,长任务意味着要维持长连接,这种任务其实是不适合用线程池来处理的,后续会有更合适的做法,此处仅仅是为了 demo 演示 !

V2 版本 - CommandServer

这一版的服务器我们实现的功能是用户发来一个指令,服务器将执行完指令的结果返回给客户端

CommandServer.hpp 中,我们在类中加入回调函数对象成员,参数为 string 表示客户端发来的命令,返回值为 string 表示命令执行后的结果,在 read 之后,调用回调函数,将返回值 write 给客户端即可完成命令集解析!

cpp 复制代码
#ifndef __TCP_ECHO_SERVER_HPP__ 
#define __TCP_ECHO_SERVER_HPP__ 

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Comm.hpp"
#include "logger.hpp"
#include "InetAddr.hpp"

static const int gdefaultfd = -1;
static const int gbacklog = 8;

using callback_t = std::function<std::string (const std::string&)>;

class CommandServer
{
private:
    void HandlerIO(int sockfd, InetAddr client)
    {
        char buffer[1024];
        while(true)
        {
            buffer[0] = 0;
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if(n > 0)
            {
                buffer[n] = 0; 
                LOG(LogLevel::INFO) << client.ToString() << "say: " << buffer;
                std::string result = _cb(buffer); //_cb回调负责将命令执行的结果返回
                write(sockfd, result.c_str(), result.size());
            }
            else if(n == 0)
            {
                LOG(LogLevel::INFO) << "client " << client.ToString() << " quit, me too!";
                break;
            }
            else
            {
                LOG(LogLevel::WARNING) << "read client " << client.ToString() << " error";
                break;
            }
        }
        close(sockfd); //必须手动关闭文件描述符!
    } 

public:
    CommandServer(callback_t cb, uint16_t port = 8080)
        :_cb(cb), _listensockfd(gdefaultfd), _port(port)
    {}

    void Init()
    {
        //1.create socket
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "create socket error";
            exit(SOCKET_CREATE_ERROR);
        }
        LOG(LogLevel::INFO) << "create socket success: " << _listensockfd;

        //2.bind socket
        InetAddr local(_port);
        if(bind(_listensockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(SOCKET_BIND_ERROR);
        }
        LOG(LogLevel::INFO) << "bind socket success: " << _listensockfd;
        
        //3.set socket listen
        if(listen(_listensockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(SOCKET_LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen socket success: " << _listensockfd;
    }

    struct ThreadData
    {
        ThreadData(int sockfd, CommandServer* self, InetAddr addr)
            :_sockfd(sockfd), _self(self), _addr(addr)
        {}
        int _sockfd;
        CommandServer* _self;
        InetAddr _addr;
    };

    static void* Routine(void* args)
    {
        ThreadData* td = static_cast<ThreadData*>(args);
        pthread_detach(pthread_self()); //将自己设置为分离状态, 主线程就不用等待了, 直接去获取新链接
        td->_self->HandlerIO(td->_sockfd, td->_addr);
        return nullptr;
    }

    void Start()
    {
        while(true)
        {
            //1. accept socket 
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr*)&peer, &len);      
            if(sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept socket error";
                continue; //让服务器继续去accept其他链接
            }
            //2.echo server
            InetAddr client(peer);
            LOG(LogLevel::INFO) << "accept socket success: " << sockfd << ", client addr: " << client.ToString();
            
            pthread_t tid;
            ThreadData* td = new ThreadData(sockfd, this, client);
            pthread_create(&tid, nullptr, Routine, td);
        }
    }

    ~CommandServer(){}
private:
    int _listensockfd;
    uint16_t _port;
    callback_t _cb;
};

#endif

我们将命令解析封装成一个类:Command.hpp

Command.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include "logger.hpp"

class Command
{
private:
    bool isSafe(const std::string& cmd)
    {
        for(auto& t : _cmd_black_list)
        {
            if(strstr(cmd.c_str(), t.c_str())) //判断t是否是cmd的子串
            {
                return false;
            }
        }
        return true; 
    }
public:
    Command(){
        _cmd_black_list.push_back("rm");
        _cmd_black_list.push_back("shutdown");
        _cmd_black_list.push_back("kill");
        _cmd_black_list.push_back("userdel");
    }
    
std::string Exec(const std::string& cmd)
    {
        if(!isSafe(cmd))  return "坏人";
        
        std::string safe_cmd = cmd + " 2>&1"; 
        FILE* fp = popen(safe_cmd.c_str(), "r"); 
        
        std::string result;
        if(fp == NULL)
        {
            result = cmd + "exec error";
        }
        else
        {
            char buffer[1024];
            while(fgets(buffer, sizeof(buffer), fp) != nullptr) 
            {
                result += buffer; 
            }
            pclose(fp);
        }
        return result;
    }

    ~Command(){}
private:
    std::vector<std::string> _cmd_black_list; //指令黑名单
};

ServerMain.cc

cpp 复制代码
#include "CommandServer.hpp"
#include <memory>
#include "Command.hpp"

void Usage(std::string proc)
{
    std::cerr << "Usgae: " << proc << " localport" << std::endl;
}

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

    uint16_t port = std::stoi(argv[1]);

    Command cmdObj;
    std::unique_ptr<CommandServer> tsvr = std::make_unique<CommandServer>(
        [&cmdObj](const std::string& cmdstr) {
            return cmdObj.Exec(cmdstr); 
        } ,port
    );
    tsvr->Init();
    tsvr->Start();
    return 0;
}

inet_ntoa 的线程安全问题

我们在学习 Udp Socket 和 Tcp Socket 时,需要将 4字节 ip 转为字符串 ip 时,使用的函数都是 inet_ntoa,但其实 inet_ntoa 并不是线程安全的,inet_ntoa 的内部简化实现如下:

cpp 复制代码
char *inet_ntoa(struct in_addr in) {
    static char buf[16]; // 这是一个静态缓冲区,所有线程共享!
    unsigned char *bytes = (unsigned char *)&in.s_addr;
    snprintf(buf, sizeof(buf), "%d.%d.%d.%d", bytes[0], bytes[1], bytes[2], bytes[3]);
    return buf; // 返回指向共享缓冲区的指针
}

核心问题 :使用一个静态内部缓冲区来存储转换后的IP地址字符串,并返回指向该缓冲区的指针。

后果:该缓冲区被进程中所有线程共享。当多个线程几乎同时调用此函数时,后调用的线程会覆盖前一个线程存放在缓冲区中的数据,导致所有线程最终都可能得到错误的结果。

下面这段代码可以验证 inet_ntoa 确实不是线程安全的!

cpp 复制代码
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main() {
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);
    printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2);
    return 0;
}

因此,以后需要将网络序列转主机序列时,不要再用 inet_ntoa 函数了,直接使用 inet_ntop 函数,该函数要求用户提供存放转换后的字符串ip的缓冲区,因此该函数是线程安全的!

参数:

af:协议族,使用 AF_INET 表示 IPV4

src:指向二进制地址的指针(网络地址),如 struct sockaddr_in 的地址

dst:指向输出字符串缓冲区的指针(由调用者提供)

size:缓冲区大小

**返回值:**成功返回 dst 指针,失败返回 nullptr,错误码被设置

cpp 复制代码
void NetToHost()
{
    _port = ntohs(_addr.sin_port);
    // _ip = inet_ntoa(_addr.sin_addr); //不是线程安全的!

    //最佳实践, 是线程安全的!
    char buffer[1024];
    inet_ntop(AF_INET, &_addr.sin_addr, buffer, sizeof(buffer));
    _ip = buffer;
}

inet_addr 的设计缺陷

inet_addr 的作用是把点分十进制的字符串变成网络整数 IP,它是线程安全的,因为它只是单纯地解析你传进去的字符串,然后直接把算出来的 32 位整数(传值)返回给你,不需要依赖任何全局静态变量。因为它在设计上有一个无法填补的逻辑漏洞:

如果转换失败(比如你传了个 "hello"),它会返回 INADDR_NONE(也就是 -1 的补码形式 0xFFFFFFFF)。

但是!有一个完全合法且非常有用的 IP 地址叫做 255.255.255.255(局域网全网广播地址)。当你试图转换这个合法地址时,它转换出来的整数刚好也是 0xFFFFFFFF!

这就导致你根本无法分辨,它返回 -1 到底是因为报错,还是因为你传的就是 255.255.255.255。

参数:

af:协议族,使用 AF_INET 表示 IPV4

src:指向待转换的点分十进制字符串

dst:指向存储二进制结果的指针

**返回值:**1表示转换成功,0表示格式错误(如src不是有效的地址字符串),-1表示系统错误(如 af 不支持),错误码被设置

cpp 复制代码
void HostToNet()
{
    memset(&_addr, 0, sizeof(_addr));
    _addr.sin_family = AF_INET;
    _addr.sin_port = htons(_port);
    // _addr.sin_addr.s_addr = inet_addr(_ip.c_str()); //有设计上的缺陷

    //最佳实践, inet_pton
    inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
}
相关推荐
阿米亚波3 天前
【EVE-NG 实战】防火墙基础(VLAN · VRRP · NAT · ACL · 静态路由)
运维·网络·笔记·网络协议·计算机网络·网络安全·运维开发
liulilittle4 天前
Exhaustive drift-fix simulation V2 — KCC on shared-bottleneck wired path.
开发语言·网络·python·tcp/ip·计算机网络·信息与通信·通信
2401_868534785 天前
案例介绍-小型网络规划及搭
python·计算机网络
ShineWinsu6 天前
对于Linux:网络基础的解析
linux·网络·面试·udp·笔试·ip·tcp
2401_868534786 天前
系统分析师案例分析题常考知识点
python·计算机网络
从零开始的代码生活_7 天前
Linux epoll 多路转接详解
linux·运维·网络·后端·tcp/ip·计算机网络·php
liulilittle8 天前
KCC 拥塞控制算法缺陷修复记录(26/07/05)
网络·tcp/ip·计算机网络·c·信息与通信·tcp·通信
liulilittle8 天前
KCC 边界缺陷深度分析:六问题根源与解决方案
网络·网络协议·tcp/ip·计算机网络·信息与通信·ip·通信
Thomas.Chan8 天前
数据中心动环监控系统架构详解
运维·网络·计算机网络·其他·系统架构·数据中心