socket编程TCP

目录

[Echo Server](#Echo Server)

TCP新增函数

基本框架

单进程版本

多进程版本

多线程版本

线程池版本

远程命令执行服务


有了前面的基础,讲解TCP更加简单了。

我们直接讲解代码。

Echo Server

我们先讲解基本框架和思路,再来改进版本。

还是和先前UDP一样,服务端只需要拿着需要绑定的端口号,直接将服务端跑起来即可,而客户端需要拿着目标服务端的ip和绑定的端口号访问即可。

TCP新增函数

我们只是简单讲,主要是代码为主。

listen函数:

  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客⼾端处于连接等待状态,如果接收 到更多的连接请求就忽略,这⾥设置不会太⼤(⼀般是5)
  • listen()成功返回0,失败返回-1;

accept函数:

  • 三次握⼿完成后,服务器调⽤accept()接受连接;
  • 如果服务器调⽤accept()时还没有客⼾端的连接请求,就阻塞等待直到有客⼾端连接上来
  • addr是⼀个传出参数,accept()返回时传出客⼾端的地址和端⼝号
  • 如果给addr参数传NULL,表⽰不关⼼客⼾端的地址;
  • addrlen参数是⼀个传⼊传出参数(value-resultargument),传⼊的是调⽤者提供的,缓冲区addr 的⻓度以避免缓冲区溢出问题,传出的是客⼾端地址结构体的实际⻓度(有可能没有占满调⽤者提 供的缓冲区);

connect函数:

  • 客⼾端需要调⽤connect()连接服务器;
  • connect和bind的参数形式⼀致,区别在于bind的参数是⾃⼰的地址,⽽connect的参数是对⽅的 地址;
  • connect()成功返回0,出错返回-1;

基本框架

TcpServer.hpp:

1. 初始化需要做创建套接字,绑定端口号,以及将套接字设置listen状态。

2.服务端运行起来,接受连接,当有客户端连接上来时,执行相关服务,当然可以回调函数,执行我们自定义的函数。

3.服务端回调函数,是可以灵活的实现各种服务,比如我们依然写之前的翻译系统服务。

细节:

一个服务端,肯定是不允许拷贝的,也就是说有且只有一个服务端对象处理客户端信息,而不能有多个服务端对象,所以服务端类是不允许拷贝和赋值重载的,怎么解决?

直接将它的拷贝和赋值重载删除或者私有化,不优雅,我们可以这样:

这样子类要拷贝的时候,也需要拷贝父类,而父类是不允许拷贝的,所以这个服务端类不能进行拷贝和赋值重载。

单进程版本

单进程版它是有BUG的,它一次只能服务一个客户端,也就是说当其他客户端连接服务端,它只能服务第一个客户端。

我们来看看:

服务端跑起来,服务端进程就会不断地接收,当有一个客户端连接成功,服务端就会调用Service函数,然后死循环读,直到这个客户端不写了,退出了,服务端这个进程才会再次阻塞着接收,也就是说只要这个客户端一个不退出,那服务端就不会处理其他客户端信息。

所以,单进程是一定不会被允许存在的。

多进程版本

多进程可以解决这个问题。

父进程阻塞接收accept,当接收成功,创建子进程,父进程阻塞等待回收子进程,子进程创建孙子进程,然后子进程直接退出,父进程等待回收子进程成功,父进程再次循环等待接收accept,而孙子进程就会变成孤儿进程,这个孙子进程会去调用Service函数(也就是说我们让孙子进程跑服务),跑完退出,系统1号进程会回收这个孤儿进程。

当有多个客户端连接时,服务端就会有多个孙子进程跑服务(一个客户端对应一个孙子进程),父进程只负责接收,没有子进程。

一张图了解:

多线程版本

主线程不断地阻塞接收accept,当接收成功,创建线程,创建的线程跑Routine函数,将线程设置成分离状态,这样主线程就不用等待阻塞回收子线程而是执行完系统回收,子线程回调Service函数,也就是说子线程是来处理客户端信息的(做服务的),而主线程是来接收accept的。

当客户端越来越多时,服务端只会有一个主线程,多个子线程,一个子线程处理一个客户的信息。

线程池版本

主线程不断地阻塞接收accept,当接收成功,创建线程池,线程池中会创建固定多个子线程,然后将Service函数插入这些线程中,然后线程池底层唤醒子线程,转而这些线程就会处理这个Service服务。

当客户端越来越多时,服务端只会有一个主线程,固定的子线程,但依然是一个子线程对应一个客户端,按理来说当客户端达到一定数量,会不够线程去处理客户端信息,当然线程池中的线程多少可以自行设置。

我们可以结合一些服务来应用这些方案。

远程命令执行服务

客户端向服务端发送指令,服务端执行指令,然后将指令执行完成后的信息回显给客户端。

popen函数:

当前父进程poen读到某个指令,会创建子进程执行指令,然后将指令执行完后的回显信息会写到特定的FILE文件中(文件),我们只需要从这个特定的文件中拿数据就能拿到回显信息。

代码:

Command.hpp:

复制代码
class Command
{
public:
    // 严格匹配
    Command()
    {
        _whiteListCommands.insert("ls");
        _whiteListCommands.insert("which");
        _whiteListCommands.insert("ls -l");
        _whiteListCommands.insert("who");
    }
    bool IsSafeCommand(const std::string &cmd)
    {
        auto iter = _whiteListCommands.find(cmd);
        return iter != _whiteListCommands.end();
    }
    std::string Execute(const std::string &cmd, InetAddr &addr)
    {
        if (!IsSafeCommand(cmd))
            return "坏人";
        std::string who = addr.StringAddr();
        // 执行命令
        FILE *fp = popen(cmd.c_str(), "r");
        if (fp == nullptr)
            return "你的命令不存在" + cmd;
        std::string res;
        char line[1024];
        while (fgets(line, sizeof(line), fp))
            res += line;
        pclose(fp);
        std::string result = who + "execute done,result is:\n" + res;
        return result;
    }
private:
    std::set<std::string> _whiteListCommands;
};

Common.hpp:

复制代码
#define CONV(addr) ((struct sockaddr *)&addr)
enum ExitCode
{
    OK = 0,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERROR,
    CONNECT_ERROR,
    FORK_ERROR,
};
// 不能拷贝
class NoCopy
{
public:
    NoCopy() = default;
    // 拷贝
    NoCopy(const NoCopy &) = delete;
    // 赋值构造
    const NoCopy &operator=(const NoCopy &) = delete;
};

TcpServer.hpp:

复制代码
using task_t = std::function<void()>;
const static int backlog = 8;
using func_t = std::function<std::string(const std::string &, InetAddr &)>;
class TcpServer : public NoCopy
{
public:
    TcpServer(uint16_t port, func_t func)
        : _port(port), _isrunning(false),
          _listensockfd(-1), _func(func) {}
    void Init()
    {
        // 1.创建套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error";
            exit(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "socket success:" << _listensockfd; // 3
        // 2.绑定端口号
        InetAddr local(_port);
        int n = bind(_listensockfd, local.NetAddrPtr(), local.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success:" << _listensockfd; // 3
        // 3.设置socket状态为listen监听
        n = listen(_listensockfd, backlog);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error" << _listensockfd; // 3
            exit(LISTEN_ERROR);
        }
        LOG(LogLevel::INFO) << "listen success:" << _listensockfd; // 3
    }
    void Service(int sockfd, InetAddr &peer)
    {
        char buffer[1024];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
            if (n > 0) // 读取成功
            {
                buffer[n] = 0;
                LOG(LogLevel::INFO) << peer.StringAddr() << "#" << buffer;
                // 执行外部回调函数
                std::string echo_string = _func(buffer, peer);
                // 写回数据
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0) // 读取失败
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "退出了...";
                close(sockfd); // 关闭文件描述符
                break;
            }
            else // n==0的情况 表示对端把链接关闭了,读到了文件结尾
            {
                LOG(LogLevel::DEBUG) << peer.StringAddr() << "异常...";
                close(sockfd); // 关闭文件描述符
                break;
            }
        }
    }
    class ThreadData
    {
    public:
        ThreadData(int fd, InetAddr &ar, TcpServer *t)
            : sockfd(fd), addr(ar), ts(t) {}
        int sockfd;
        InetAddr addr;
        TcpServer *ts;
    };
    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(args);
        td->ts->Service(td->sockfd, td->addr);
        delete td;
        return nullptr;
    }
    void Run()
    {
        _isrunning = true;
        while (_isrunning)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 如果没有连接,accept就会阻塞
            int sockfd = accept(_listensockfd, CONV(peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error";
                continue;
            }
            InetAddr addr(peer); // 网络转主机
            LOG(LogLevel::INFO) << "accept success,peer addr:" << addr.StringAddr();
            // // version0--单进程程序:不会存在
            // Service(sockfd, addr); // 连接成功执行服务
            // version1--多进程版本
            // pid_t id = fork();
            // if (id < 0) // 创建失败
            // {
            //     LOG(LogLevel::FATAL) << "fork error";
            //     exit(FORK_ERROR);
            // }
            // else if (id == 0)
            // {
            //     close(_listensockfd);
            //     if (fork() > 0)
            //         exit(OK);
            //     Service(sockfd, addr);//孤儿进程,系统1号进程回收我
            //     exit(OK);
            // }
            // else
            // {
            //     close(sockfd);
            //     waitpid(id, nullptr, 0);//不会阻塞了
            // }
            // version2--多线程版本
            ThreadData *td = new ThreadData(sockfd, addr, this);
            pthread_t pid;
            pthread_create(&pid, nullptr, Routine, td);
            // //version3--线程池版本
            // ThreadPool<task_t>::GetInstance()->Enqueue([this,sockfd,&addr]() {
            //     Service(sockfd,addr);
            // });
        }
        _isrunning = false;
    }

private:
    uint16_t _port;
    bool _isrunning;
    int _listensockfd;
    func_t _func;
};

TcpServer.cc:

复制代码
//./TcpServer 8080
int main(int agrc, char *agrv[])
{
    if (agrc != 2)
    {
        LOG(LogLevel::FATAL) << "Usage error";
        exit(USAGE_ERR);
    }
    Enable_Console_Log_Strategy();
    uint16_t port = std::stoi(agrv[1]);
    //翻译服务
    // Dict d;
    // d.LoadDict();
    // std::unique_ptr<TcpServer> up = std::make_unique<TcpServer>(port,
    //     [&d](const std::string & word, InetAddr & addr){
    //         return d.Translate(word,addr);
    // });
    Command d;
    std::unique_ptr<TcpServer> up = std::make_unique<TcpServer>(port,
        std::bind(&Command::Execute,&d,std::placeholders::_1,std::placeholders::_2)
    );
    //也可以这样
    // std::unique_ptr<TcpServer> up = std::make_unique<TcpServer>(port,
    //     [&d](const std::string & cmd, InetAddr & addr){
    //         return d.Execute(cmd,addr);
    // });
    up->Init();
    up->Run();
    return 0;
}

TcpClient.cc:

复制代码
// ./TcpClient 127.0.0.1 8080
int main(int agrc, char *agrv[])
{
    if (agrc != 3)
    {
        std::cerr << "Usage error" << std::endl;
        exit(USAGE_ERR);
    }
    std::string server_ip = agrv[1];
    uint16_t server_port = std::stoi(agrv[2]);
    // 1.创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "socket error" << std::endl;
        exit(SOCKET_ERR);
    }
    // 2.发送连接请求
    // 将服务端的ip和端口号转化成网络序列
    InetAddr serveraddr(server_ip, server_port);
    // 请求连接
    int n = connect(sockfd, serveraddr.NetAddrPtr(), serveraddr.NetAddrLen());
    if (n < 0)
    {
        std::cerr << "connect error" << std::endl;
        exit(CONNECT_ERROR);
    }
    while (true)
    {
        std::string line;
        std::cout << "Please Enter@";
        std::getline(std::cin, line);
        write(sockfd, line.c_str(), line.size());
        char buffer[1024];
        ssize_t n = read(sockfd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n]=0;
            std::cout<<"Server say#"<<buffer<<std::endl;
        }
    }
    close(sockfd);
    return 0;
}

好了,我们下期见!!!

相关推荐
北极糊的狐2 小时前
IDEA中安装 CamelCase 插件实现变量快速生成驼峰命名
开发语言·php
【蜡笔小新】2 小时前
《筑基篇》C语言基础2
c语言·开发语言
CILMY232 小时前
【Linux】进度条实践教程:使用Makefile构建项目
linux·进度条·make和makefile
gaize12132 小时前
腾讯云锐驰和蜂驰的区别
服务器·腾讯云
沉在嵌入式的鱼2 小时前
linux串口对0X0D、0X0A等特殊字符的处理
linux·stm32·单片机·特殊字符·串口配置
gxh19922 小时前
4步将HTTP请求升级为HTTPS
运维·服务器·网络协议·http·https
BullSmall2 小时前
日志打印IP:安全与合规的最佳实践
网络·tcp/ip·安全
洛阳泰山2 小时前
Java实现周易六爻自动排盘:根据起卦的公历时间换算农和干支时间,推算日柱空亡(旬空)
java·开发语言·周易·六爻·算卦
云和数据.ChenGuang3 小时前
运维工程师技术之nfs共享文件系统
运维·服务器·运维技术·数据库运维工程师·运维教程