Socket编程TCP

news2025/4/9 21:34:00

Socket编程TCP

  • 1、V1——EchoServer单进程版
  • 2、V2——EchoServer多进程版
  • 3、V3——EchoServer多线程版
  • 4、V4——EchoServer线程池版
  • 5、V5——多线程远程命令执行
  • 6、验证TCP——Windows作为client访问Linux
  • 7、connect的断线重连

1、V1——EchoServer单进程版

在这里插入图片描述

在TcpServer.hpp中实现服务器逻辑,然后TcpServer.cc中启动,客户端我们就不封装了,直接在TcpClient.cc中实现。Common.hpp和日志都是直接写过的,直接拿过来用。

下面先写出基本框架:

#pragma once

#include <iostream>
#include <memory>

const static uint16_t gport = 8080;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
    :_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {

    }

    void Start()
    {

    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {

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

需要保存sockfd,然后还需要端口号,同时使用bool类型的变量来表示服务端是否运行,通过Stop函数可以停止。


1、创建套接字socket
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
使用socket函数创建套接字,第一个参数domain表示域或协议家族,使用AF_INET表示网络通信,使用AF_UNIX表示本地通信,我们设置为AF_INET。第二个参数表示套接字类型,在UDP我们使用SOCK_DGRAM表示数据报,在TCP这里我们使用SOCK_STREAM,表示面向字节流。第三个参数设置为0即可。
socket成功返回文件描述符,失败返回-1错误码被设置。

#pragma once

#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const static uint16_t gport = 8080;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
    :_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;
    }

    void Start()
    {

    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {

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

2、填充网络信息并绑定。
在这里插入图片描述
在这里插入图片描述
bind函数将创建套接字返回的sockfd和传入的网络信息结构体对象绑定。由于UDP介绍过,不再赘述。
成功返回0,失败返回-1错误码被设置。

#pragma once

#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const static uint16_t gport = 8080;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
    :_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;

        // 2.填充网络信息并绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = ::htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); 
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);
            Die(BIND_ERR);
        }
    }

    void Start()
    {

    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {

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

3、将套接字设置为监听状态。
由于TCP是面向连接的,所以要求TCP要随时随地的等待被连接,因此需要将socket设置为监听状态。
在这里插入图片描述
在这里插入图片描述
第一个参数表示要监听的套接字,就是上面socket的返回值。第二参数表示全连接数量,我们设置为8即可,这个等后面讲TCP原理再说。
成功返回0,失败返回-1,错误码设置。

#pragma once

#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Common.hpp"

#define BACKLOG 8

using namespace LogModule;

const static uint16_t gport = 8080;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
    :_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;

        // 2.填充网络信息并绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = ::htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); 
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;

        // 3.设置套接字为监听状态
        n = ::listen(_listensockfd, BACKLOG);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error: " << strerror(errno);
            Die(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;
    }

    void Start()
    {

    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {

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

4、获取新连接,接收客户端发送的数据
下面我们在Start函数中实现服务器获取客户端连接,并将客户端发送过来的字符串添加echo#发送回去。
在这里插入图片描述
在这里插入图片描述
使用accept函数来获取连接,第一个参数sockfd就是前面socket的返回值,第二个参数和第三个参数相当于输出型参数,因为我们需要知道是谁跟服务器建立了连接。
成功该函数返回一个文件描述符,失败返回-1,错误码被设置。
那么为什么socket已经返回一个文件描述符了,这个accept又返回一个文件描述符呢?——这是因为socket返回文件描述符是专门用来获取新连接的,而accept返回值是用来提供服务的。
另外,如果没有人连接,那么就会阻塞在accept这里。

#pragma once

#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"

#define BACKLOG 8

using namespace LogModule;

const static uint16_t gport = 8080;

class TcpServer
{
public:
    TcpServer(uint16_t port = gport)
    :_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;

        // 2.填充网络信息并绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = ::htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); 
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;

        // 3.设置套接字为监听状态
        n = ::listen(_listensockfd, BACKLOG);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error: " << strerror(errno);
            Die(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;
    }

    void HandlerRequest(int sockfd)
    {
        LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;
        char inbuffer[4096];
        while (true)
        {
            int n = ::read(sockfd, inbuffer, sizeof(inbuffer) - 1);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::string echo_string = "echo# ";
                echo_string += inbuffer;
                ::write(sockfd, echo_string.c_str(), echo_string.size());
            }
        }
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            LOG(LogLevel::DEBUG) << "accept ing...";
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensockfd, CONV(&peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);
                continue;
            } 
            LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "client info: " << "[" << addr.Addr() << "]";

            HandlerRequest(sockfd);
        }
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {

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

如上,我们引入UDP写的InetAddr.hpp,获取新连接后将客户端信息打印出来。同时将返回的sockfd传给HandlerRequest函数处理,在HandlerRequest我们读取客户端发送的数据,添加echo#发送回客户端。
这里读写数据可以直接使用read和write,因为TCP是面向字节流的。

接着实现一下TcpServer.cc,然后进行一下测试:

// TcpServer.cc
#include "TcpServer.hpp"


int main()
{
    std::unique_ptr<TcpServer> svr_uptr = std::make_unique<TcpServer>();
    svr_uptr->InitServer();
    svr_uptr->Start();

    return 0;
}

编译后进行测试:
在这里插入图片描述
使用netstat -tlnp查看tcp服务,t表示tcp,l表示之查看listen状态的,n表示将能显示数字的都显示成数字,p表示显示最后一列PID/Program name。
可以看到我们服务启动起来了,端口号8080,并且当前状态处于监听状态。


5、使用telnet进行测试
在这里插入图片描述
使用telnet访问百度80端口,连接后输入ctrl ],然后回车,输入GET / HTTP/1.1回车再回车,可以获取百度的网页信息。

下面我们使用telnet测试我们写的客户端:
在这里插入图片描述

我们也可以通过浏览器,输入IP:端口号也可以:
在这里插入图片描述


6、实现客户端

在这里插入图片描述在这里插入图片描述
客户端也是创建套接字,但是并不需要主动bind,由于TCP是面向连接的,所以客户端需要使用connect来和服务器建立连接,第一个参数就是socket的返回值,第二个参数就是服务端的信息。connect底层会自动进行bind。
成功返回0,失败返回-1,错误码被设置。

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

// ./client_tcp serverip serverport
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        return 1;
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);   
     
    int sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket failed" << std::endl;
        return 2;
    } 

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = ::htons(serverport);
    server.sin_addr.s_addr = ::inet_addr(serverip.c_str());

    int n = ::connect(sockfd, (const struct sockaddr*)&server, sizeof(server));
    if (n < 0)
    {
        std::cout << "connect failed" << std::endl;
        return 3;
    }
    
    while (1)
    {
        std::cout << "Please Enter@ ";
        std::string message;
        std::getline(std::cin, message);
        n = ::write(sockfd, message.c_str(), message.size());
        if (n > 0)
        {
            char buffer[4096];
            int m = ::read(sockfd, buffer, sizeof(buffer) - 1);
            if (m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else break;
        }
        else break;
    }

    return 0;
}

接着使用我们写的客户端进行测试:
在这里插入图片描述
我们可以看到,客户端发送消息能够接受服务器的echo消息。但是当我们退出客户端,再次运行的时候,我们发消息就没响应了。这是因为我们服务端进入HandlerRequest之后死循环了。

继续修改HandlerRequest,当服务端read的返回值为0,说明客户端退出了。当read返回值小于0,说明读取失败,直接退出即可。
在这里插入图片描述
在这里插入图片描述
现在我们进程退出后读到0就会退出,但是还有个问题,就是文件描述符一直增加,这是因为我们服务端在读取到0,表明客户端退出,服务端退出HandlerRequest逻辑的时候并没有把文件关掉,所以我们在HandlerRequest函数最后添加close函数。
在这里插入图片描述

如果不关闭文件,那么fd的值就会一直增加,而fd属于有用的、有限的资源,如果不关闭就会导致fd泄漏问题。
另外如果客户端很多的话那fd不是会一直增加吗,如果fd只有32、64那不就不够用了吗?确实如此,Linux是支持对文件描述符个数进行扩展的,默认云服务器的fd数量是比较多的。可以使用ulimit -a查看:

在这里插入图片描述
如上图,open files就是fd的数量。


2、V2——EchoServer多进程版

上面的代码我们已经实现了单进程版,当服务端获取新连接,就会去执行HandlerRequest,这时候如果再来一个客户端就无法处理了,只能处理完当前客户端才能回到Start中再次获取新连接继续处理。也就是服务端当前只能处理一个客户端请求。因此我们需要将代码改成多进程版本,让服务端支持处理多个客户端。
在这里插入图片描述
原来是直接去执行HandlerRequest函数,现在我们创建子进程来执行HandlderRequest。由于创建子进程后,子进程拷贝父进程的文件描述符表,它们是各自有一份的,所以将子进程的listensockfd关闭,将父进程的sockfd关闭。但是这样还有个问题,就是父进程需要对子进程进行回收,否则子进程退出后就会僵尸,导致内存泄漏。而父进程如果等待子进程就会阻塞住,这样就跟单进程版没啥区别了。
下面有两种解决办法:
1、父进程直接使用signal函数将17号信号SIGCHLD主动设置为忽略。这样子进程退出后由操作系统自动回收。
2、父进程创建子进程,子进程中继续创建子进程,由孙子进程去执行HandlerRequest,子进程直接退出。这样孙子进程就会变成孤儿进程,孙子进程会被1号进程——操作系统领养,等将来孙子进程退出时由操作系统回收释放。然后父进程直接waitpid,由于子进程创建进程后直接退出,所以父进程waitpid不会阻塞,直接回收子进程,然后继续获取新连接。

我们使用第二种办法:
在这里插入图片描述

下面进行测试:
在这里插入图片描述
可以看到现在已经可以处理多个客户端了,并且每次获取新连接返回的文件描述符都是4。

在这里插入图片描述
可以看到有两个孙子进程,它们的父进程都是1号进程,将来客户端退出,这两个进程读到0退出就会由操作系统回收释放。


3、V3——EchoServer多线程版

创建进程还是一个比较重的工作,需要创建地址空间、页表等。所以接下来我们实现一个多线程版本。

在这里插入图片描述
创建线程执行ThreadEntry,而由于回调函数必须是返回值为void*,参数为void*的函数,因此不能直接执行HandlerRequest函数。现在又有很多问题:
1、ThreadEntry是类内函数,所以有一个隐含的this指针。因此我们需要将ThreadEntry设置static。
2、设置为static后线程可以执行ThreadEntry函数了,但是需要在ThreadEntry里面继续调用HandlerRequest函数,而调用HandlerRequest函数还是需要this指针,因此我们可以将this指针和sockfd封装在一个结构体里面传给ThreadEntry函数。
3、线程也需要等待,否则会有类似僵尸进程的问题。我们可以直接在ThreadEntry让线程自己分离。

在这里插入图片描述
在这里插入图片描述
如图多线程这里线程共享文件描述符表,因此主线程不敢随便close。可以看到sockfd会一直增加。

在这里插入图片描述


4、V4——EchoServer线程池版

在线程互斥与同步我们写过一个线程池,我们可以拿过来用。主线程将获取的新连接通过lambda或bind加入任务队列中。
在这里插入图片描述
定义一个task_t类型,然后加入到线程池的任务队列中。

HandlerRequest获取和发送数据我们使用的是read和write。接下来介绍两个接口:recv和send
在这里插入图片描述
在这里我们还可以使用recv读取,flags设置为0即可,阻塞读取。

在这里插入图片描述
还可以使用send发送,flag设置为0即可,阻塞发送。

// TcpServer.hpp
#pragma once

#include <iostream>
#include <cstring>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/wait.h>
#include "Log.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

#define BACKLOG 8

using namespace LogModule;
using namespace ThreadPoolModule;

const static uint16_t gport = 8080;

class TcpServer
{
    using task_t = std::function<void()>;
    struct ThreadData
    {
        int sockfd;
        TcpServer* self;
    };
public:
    TcpServer(uint16_t port = gport)
    :_port(port)
    ,_isrunning(false)
    {}

    void InitServer()
    {
        // 1.创建套接字
        _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
        if (_listensockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error: " << strerror(errno);
            Die(SOCKET_ERR);
        }
        LOG(LogLevel::INFO) << "create socket success, sockfd is: " << _listensockfd;

        // 2.填充网络信息并绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = ::htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        int n = ::bind(_listensockfd, CONV(&local), sizeof(local)); 
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error: " << strerror(errno);
            Die(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd is: " << _listensockfd;

        // 3.设置套接字为监听状态
        n = ::listen(_listensockfd, BACKLOG);
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "listen error: " << strerror(errno);
            Die(LISTEN_ERR);
        }
        LOG(LogLevel::INFO) << "listen success, sockfd is: " << _listensockfd;
    }

    void HandlerRequest(int sockfd)
    {
        LOG(LogLevel::INFO) << "HandlerRequest, sockfd is: " << sockfd;
        char inbuffer[4096];
        // 长任务
        while (true)
        {
            int n = ::recv(sockfd, inbuffer, sizeof(inbuffer) - 1, 0);
            if (n > 0)
            {
                inbuffer[n] = 0;
                std::string echo_string = "echo# ";
                echo_string += inbuffer;
                ::send(sockfd, echo_string.c_str(), echo_string.size(), 0);
            }
            else if (n == 0)
            {
                // 当读到0,表明客户端退出了。
                LOG(LogLevel::INFO) << "client quit: " << sockfd;
                break;
            }
            else
            {
                // n < 0说明读取失败
                break;
            }
        }
        ::close(sockfd);
    }

    static void* ThreadEntry(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData* td = static_cast<ThreadData*>(args);
        td->self->HandlerRequest(td->sockfd);
        delete td;        
        return nullptr;
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            LOG(LogLevel::DEBUG) << "accept ing...";
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = ::accept(_listensockfd, CONV(&peer), &len);
            if (sockfd < 0)
            {
                LOG(LogLevel::WARNING) << "accept error: " << strerror(errno);
                continue;
            } 
            LOG(LogLevel::INFO) << "accept success, sockfd is: " << sockfd;
            InetAddr addr(peer);
            LOG(LogLevel::INFO) << "client info: " << "[" << addr.Addr() << "]";

            // version-0 单进程版
            // HandlerRequest(sockfd);
            
            // version-1 多进程版
            // pid_t id = fork();
            // if (id == 0)
            // {
            //     ::close(_listensockfd);
            //     if (fork() > 0) exit(0);
            //     HandlerRequest(sockfd);
            //     exit(0);
            // }
            // ::close(sockfd);
            // int rid = waitpid(id, nullptr, 0);  // 不会阻塞.
            // if (rid < 0)
            // {
            //     LOG(LogLevel::WARNING) << "waitpid error";
            // }

            // version-3 多线程版
            // pthread_t tid;
            // ThreadData* td = new ThreadData;
            // td->sockfd = sockfd;
            // td->self = this;
            // pthread_create(&tid, nullptr, ThreadEntry, td);

            // version-4 线程池版-比较适合处理短任务,或者是用户量少的情况
            // ThreadPool<task_t>::GetInstance()->Equeue(std::bind(&TcpServer::HandlerRequest, this, sockfd));
            ThreadPool<task_t>::GetInstance()->Equeue([this, sockfd](){
                this->HandlerRequest(sockfd);
            });
        }
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~TcpServer()
    {

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

下面进行测试:
在这里插入图片描述
在这里插入图片描述
但是我们今天这里的HandlerRequest是长任务,所以并不适合线程池。线程池比较适合短任务,用户量少的情况。


5、V5——多线程远程命令执行

我们可以让客户端输入命令,然后服务端接受数据做处理,将命令执行的结果返回给客户端。

在这里插入图片描述
添加一个handler_t类型,然后添加为TcpServer的成员变量,将来上层通过构造函数传入回调函数,在HandlerRequest中调用回调函数获取命令执行结构,然后将命令执行结果返回给客户端。

下面就需要实现CommonExec.hpp:
在这里插入图片描述
Execute就是将来上层要将传入回调函数执行的方法。首先需要fork创建子进程,可以让子进程重定向,将输出重定向到管道的写端,然后执行exec*程序替换,接着将结果写到管道里面,父进程再从管道读取结果。今天我们就不这么写了,介绍两个函数:
在这里插入图片描述
popen会创建管道,创建子进程进行程序替换执行命令,参数command就是命令字符串,type表示读写,我们设置为读就可以。返回值是FILE*,将来通过返回值可以读取命令执行的结果。然后使用pclose关闭FILE。

#pragma once

#include <iostream>
#include <cstdio>
#include <string>
#include <set>

const int line_size = 1024;

class Command
{
public:
    Command()
    {
        _white_list.insert("ls");
        _white_list.insert("pwd");
        _white_list.insert("ls -l");
        _white_list.insert("ll");
        _white_list.insert("ls -a -l");
        _white_list.insert("who");
        _white_list.insert("whoami");
    }

    bool SafeCheck(const std::string& cmdstr)
    {
        auto iter = _white_list.find(cmdstr);
        return iter != _white_list.end();
    }

    std::string Execute(std::string cmdstr)
    {
        // 1.pipe
        // 2.fork + dup2(pipe[1], 1) + exec*
        // 3.父进程读取
        if (!SafeCheck(cmdstr)) return cmdstr + " 不支持!";

        FILE* fp = popen(cmdstr.c_str(), "r");
        if (fp == nullptr)
        {
            return "Failed";
        }
        std::string result;
        char buffer[line_size];
        while (true)
        {
            char* p = ::fgets(buffer, sizeof(buffer), fp);
            if (p == nullptr) break;
            result += buffer;
        }
        pclose(fp);
        return result.empty() ? "Done" : result;
    }
private:
    std::set<std::string> _white_list;
};

使用set来保存运行执行的命令,Execute函数内部调用SafeCheck进行判断,如果不合法直接返回。

下面在TcpServer.cc传入回调函数:
在这里插入图片描述
在这里插入图片描述


6、验证TCP——Windows作为client访问Linux

下面这份代码在windows的vs2022下运行,windows作为客户端访问Linux,客户端发送消息,服务端返回echo# 消息。

#include <iostream>
#include <string>
#include <cstring>
#include <WinSock2.h>
#include <Windows.h>

#pragma warning(disable : 4996)     // 去除使用inet_addr的警告
#pragma comment(lib, "ws2_32.lib")  // 指定要链接的库

std::string serverip = "47.117.157.14";  // 服务器IP
uint16_t serverport = 8080;				 // 服务器端口号

int main()
{
	WSADATA wsd;  // 定义winsock初始化信息结构体
	WSAStartup(MAKEWORD(2, 2), &wsd);  // 初始化winsock库

	SOCKET sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == INVALID_SOCKET)
	{
		std::cout << "create socket error" << std::endl;
		WSACleanup(); // 清理并释放winsock资源
		return 1;
	}

	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_port = ::htons(serverport);
	server.sin_addr.s_addr = ::inet_addr(serverip.c_str());

	int n = ::connect(sockfd, (const sockaddr*)&server, sizeof(server));
	if (n == SOCKET_ERROR)
	{
		std::cout << "connect error" << std::endl;
		closesocket(sockfd);
		WSACleanup(); // 清理并释放winsock资源
	}

	char buffer[4096];
	while (true)
	{
		std::cout << "Please Enter@ ";
		std::string line;
		std::getline(std::cin, line);
		n = ::send(sockfd, line.c_str(), line.size(), 0);
		if (n > 0)
		{
			n = ::recv(sockfd, buffer, sizeof(buffer), 0);
			if (n > 0)
			{

				buffer[n] = 0;
				std::cout << buffer << std::endl;
			}
		}
	}

	closesocket(sockfd);
	WSACleanup(); // 清理并释放winsock资源
	return 0;
}

在这里插入图片描述


7、connect的断线重连

客户端会面临服务器崩溃的情况,我们可以试着写一个客户端重连的代码,模拟并理解一些客户端行为,比如游戏客户端断线重连。
采用状态机,实现一个简单的tcp client可以实现重连效果。

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

enum class Status
{
    NEW,          // 新建状态
    CONNECTING,   // 正在连接,仅方便查看conn状态
    CONNECTED,    // 连接成功
    DISCONNECTED, // 连接失败或重连失败
    CLOSED        // 连接失败,经过重连后还是失败。
};

enum ExitCode
{
    USAGE_ERR = 1,
    SOCKET_ERR,
};

const static int defaultsockfd = -1;
const static int defaultretryinterval = 1;
const static int defaultmaxretries = 5;

class ClientConnection
{
public:
    ClientConnection(const std::string &serverip, uint16_t serverport)
        : _sockfd(defaultsockfd), _serverip(serverip), _serverport(serverport), _status(Status::NEW), _retry_interval(defaultretryinterval), _max_retries(defaultmaxretries)
    {
    }

    void Connect()
    {
    }

    void Reconnect()
    {
    }

    void Process()
    {
    }

    void Disconnect()
    {
    }

    Status GetStatus() { return _status; }

    ~ClientConnection()
    {
    }

private:
    int _sockfd;
    std::string _serverip; // 服务器IP
    uint16_t _serverport;  // 服务器端口
    Status _status;        // 当前连接状态
    int _retry_interval;   // 重连时间间隔
    int _max_retries;      // 最大重连次数
};

class TcpClient
{
public:
    TcpClient(const std::string &serverip, uint16_t serverport)
        : _connection(serverip, serverport)
    {
    }

    void Execute()
    {
        while (true)
        {
            switch (_connection.GetStatus())
            {
            case Status::NEW:
                _connection.Connect();
                break;
            case Status::CONNECTED:
                _connection.Process();
                break;
            case Status::DISCONNECTED:
                _connection.Reconnect();
                break;
            case Status::CLOSED:
                _connection.Disconnect();
                return;
            default:
                break;
            }
        }
    }

    ~TcpClient()
    {
    }

private:
    ClientConnection _connection;
};

// ./client_tcp serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    TcpClient client(serverip, serverport);
    client.Execute();
    return 0;
}

如上,我们通过命令行参数将服务端ip和端口号传给进程,然后调用TcpServer构造函数传入,创建出TcpServer对象后就去执行Execute函数,该函数通过对ClientConnection对象中状态判断执行哪个函数。
如果当前状态为NEW,表示处于新建状态,就执行Connect函数建立连接。
如果当前状态为CONNECTED,表示已建立连接,就执行Process发送数据给服务端并接收服务端返回的数据。
如果当前状态为DISCONNECTED,表示建立连接失败,执行Reconnect重新连接服务端。
如果当前状态为CLOSED,表示经过重连后还是失败,所以直接关闭sockfd退出。

那么ClientConnection中就需要文件描述符sockfd,服务端IP和端口号,当前ClientConnection状态,重连时间间隔和最大重连次数。


实现Connect函数:

void Connect()
{
    _sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (_sockfd < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_ERR);
    }

    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.s_addr);

    int n = connect(_sockfd, (const sockaddr*)&server, sizeof(server));
    if (n < 0)
    {
        Disconnect();   // 关闭sockfd
        _status = Status::DISCONNECTED; // 连接失败
        return;
    }
    // 连接成功
    _status = Status::CONNECTED;
}

创建套接字,然后进行连接,对连接返回值进行判断,如果小于说明连接失败。调用Disconnect关闭之前打开的sockfd。然后将状态设置为DISCONNECTED直接返回。那么在Execute函数中下一次循环就会去执行Reconnect进行重连。


实现Reconnect函数:

void Reconnect()
{
    _status = Status::CONNECTING;
    int count = 0;
    while (count < _max_retries)
    {
        // _status = Status::CONNECTING;
        Connect();
        if (_status == Status::CONNECTED)
            return;
        ++count;
        std::cout << "正在重连..., 重连次数: " << count << std::endl;
        sleep(_retry_interval);
    }
    _status = Status::CLOSED;
    std::cout << "重连失败,请检查你的网络..." << std:: endl;
}

将状态设置为CONNECTING表示正在连接,然后循环调用Connect,调用后对状态进行判断,如果为CONNECTED表示连接成功直接返回。如果最后达到最大连接次数还是失败,设置状态为CLOSED。


实现Process函数:

void Process()
{
    while (true)
    {
        std::string line;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, line);
        int n = send(_sockfd, line.c_str(), line.size(), 0);
        if (n > 0)
        {
            char buffer[1024];
            int m = recv(_sockfd, buffer, sizeof(buffer)-1, 0);
            if (m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;
            }
            else // 读取失败或断开连接
            {
                _status = Status::DISCONNECTED;
                break;
            }
        }
        else
        {
            std::cerr << "send error" << std::endl;
            _status = Status::CLOSED;
            // _status = Status::DISCONNECTED;
            break;
        }
    }
}

Process就是进行简单的IO操作,当recv读取数据m==0,说明服务端关闭连接或掉线了,我们进行重连。

最后Disconnect就是关闭sockfd:

void Disconnect()
{
    if (_sockfd > defaultsockfd)
    {
        close(_sockfd);
        _sockfd = -1;
    }
}

完整代码如下:

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

enum class Status
{
    NEW,          // 新建状态
    CONNECTING,   // 正在连接,仅方便查看conn状态
    CONNECTED,    // 连接成功
    DISCONNECTED, // 连接失败或重连失败
    CLOSED        // 经过重连后还是失败。
};

enum ExitCode
{
    USAGE_ERR = 1,
    SOCKET_ERR,
};

const static int defaultsockfd = -1;
const static int defaultretryinterval = 1;
const static int defaultmaxretries = 5;

class ClientConnection
{
public:
    ClientConnection(const std::string &serverip, uint16_t serverport)
        : _sockfd(defaultsockfd), _serverip(serverip), _serverport(serverport), _status(Status::NEW), _retry_interval(defaultretryinterval), _max_retries(defaultmaxretries)
    {
    }

    void Connect()
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "create socket error" << std::endl;
            exit(SOCKET_ERR);
        }

        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);

        int n = connect(_sockfd, (const sockaddr*)&server, sizeof(server));
        if (n < 0)
        {
            Disconnect();  // 关闭sockfd
            _status = Status::DISCONNECTED; // 连接失败
            return;
        }
        // 连接成功
        _status = Status::CONNECTED;
    }

    void Reconnect()
    {
        _status = Status::CONNECTING;
        int count = 0;
        while (count < _max_retries)
        {
            // _status = Status::CONNECTING;
            Connect();
            if (_status == Status::CONNECTED)
                return;
            ++count;
            std::cout << "正在重连..., 重连次数: " << count << std::endl;
            sleep(_retry_interval);
        }
        _status = Status::CLOSED;
        std::cout << "重连失败,请检查你的网络..." << std:: endl;
    }

    void Process()
    {
        while (true)
        {
            std::string message = "hello server";
            int n = send(_sockfd, message.c_str(), message.size(), 0);
            if (n > 0)
            {
                char buffer[1024];
                int m = recv(_sockfd, buffer, sizeof(buffer)-1, 0);
                if (m > 0)
                {
                    buffer[m] = 0;
                    std::cout << buffer << std::endl;
                }
                else // 读取失败或断开连接
                {
                    _status = Status::DISCONNECTED;
                    break;
                }
            }
            else
            {
                std::cerr << "send error" << std::endl;
                _status = Status::CLOSED;
                // _status = Status::DISCONNECTED;
                break;
            }
            sleep(1);
        }
    }

    void Disconnect()
    {
        if (_sockfd > defaultsockfd)
        {
            close(_sockfd);
            _sockfd = -1;
        }
    }

    Status GetStatus() { return _status; }

    ~ClientConnection()
    {
    }

private:
    int _sockfd;
    std::string _serverip; // 服务器IP
    uint16_t _serverport;  // 服务器端口
    Status _status;        // 当前连接状态
    int _retry_interval;   // 重连时间间隔
    int _max_retries;      // 最大重连次数
};

class TcpClient
{
public:
    TcpClient(const std::string &serverip, uint16_t serverport)
        : _connection(serverip, serverport)
    {
    }

    void Execute()
    {
        while (true)
        {
            switch (_connection.GetStatus())
            {
            case Status::NEW:
                _connection.Connect();
                break;
            case Status::CONNECTED:
                _connection.Process();
                break;
            case Status::DISCONNECTED:
                _connection.Reconnect();
                break;
            case Status::CLOSED:
                _connection.Disconnect();
                return;
            default:
                break;
            }
        }
    }

    ~TcpClient()
    {
    }

private:
    ClientConnection _connection;
};

// ./client_tcp serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    TcpClient client(serverip, serverport);
    client.Execute();
    return 0;
}

下面进行测试:
在这里插入图片描述

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

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

相关文章

文件映射mmap与管道文件

在用户态申请内存&#xff0c;内存内容和磁盘内容建立一一映射 读写内存等价于读写磁盘 支持随机访问 简单来说&#xff0c;把磁盘里的数据与内存的用户态建立一一映射关系&#xff0c;让读写内存等价于读写磁盘&#xff0c;支持随机访问。 管道文件&#xff1a;进程间通信机…

代码随想录回溯算法03

93.复原IP地址 本期本来是很有难度的&#xff0c;不过 大家做完 分割回文串 之后&#xff0c;本题就容易很多了 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;回溯算法如何分割字符串并判断是合法IP&#xff1f;| LeetCode&#xff1a;93.复原IP地址_哔哩哔…

批量改CAD图层颜色——CAD c#二次开发

一个文件夹下大量图纸&#xff08;几百甚至几千个文件&#xff09;需要改图层颜色时&#xff0c;可采用插件实现&#xff0c;效果如下&#xff1a; 转换前&#xff1a; 转换后&#xff1a; 使用方式如下&#xff1a;netload加载此dll插件&#xff0c;输入xx运行。 附部分代码如…

【内网安全】DHCP 饿死攻击和防护

正常情况&#xff1a;PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包&#xff1a;大量的DHCP Discover包 此时模…

10种电阻综合对比——《器件手册--电阻》

二、电阻 前言 10种电阻对比数据表 电阻类型 原理 特点 应用 贴片电阻 贴片电阻是表面贴装元件&#xff0c;通过将电阻体直接贴在电路板上实现电路连接 体积小、重量轻&#xff0c;适合高密度电路板&#xff1b;精度高、稳定性好&#xff0c;便于自动化生产 广泛应用于…

剑指Offer(数据结构与算法面试题精讲)C++版——day6

剑指Offer&#xff08;数据结构与算法面试题精讲&#xff09;C版——day6 题目一&#xff1a;不含重复字符的最长子字符串题目二&#xff1a;包含所有字符的最短字符串题目三&#xff1a;有效的回文 题目一&#xff1a;不含重复字符的最长子字符串 这里还是可以使用前面&#x…

freertos韦东山---事件组以及实验

事件组的原理是什么&#xff0c;有哪些优点&#xff0c;为啥要创造出这个概念 在实时操作系统&#xff08;如 FreeRTOS&#xff09;中&#xff0c;事件组是一种用于任务间同步和通信的机制&#xff0c;它的原理、优点及存在意义如下&#xff1a; 事件组原理 数据结构&#xf…

架构师面试(二十六):系统拆分

问题 今天我们聊电商系统实际业务场景的问题&#xff0c;考查对业务系统问题的分析能力、解决问题的能力和对系统长期发展的整体规划能力。 一电商平台在早期阶段业务发展迅速&#xff0c;DAU在 10W&#xff1b;整个电商系统按水平分层架构进行设计&#xff0c;包括【入口网关…

Java中的同步和异步

一、前言 在Java中&#xff0c;同步&#xff08;Synchronous&#xff09;和异步&#xff08;Asynchronous&#xff09;是两种不同的任务处理模式。核心区别在任务执行的顺序控制和线程阻塞行为。 二、同步&#xff08;Synchronous&#xff09; 定义&#xff1a;任务按顺序执行…

在 Ubuntu24.04 LTS 上 Docker Compose 部署基于 Dify 重构二开的开源项目 Dify-Plus

一、安装环境信息说明 硬件资源&#xff08;GB 和 GiB 的主要区别在于它们的换算基数不同&#xff0c;GB 使用十进制&#xff0c;GiB 使用二进制&#xff0c;导致相同数值下 GiB 表示的容量略大于 GB&#xff1b;换算关系&#xff1a;1 GiB ≈ 1.07374 GB &#xff1b;1 GB ≈ …

NO.64十六届蓝桥杯备战|基础算法-简单贪心|货仓选址|最大子段和|纪念品分组|排座椅|矩阵消除(C++)

贪⼼算法是两极分化很严重的算法。简单的问题会让你觉得理所应当&#xff0c;难⼀点的问题会让你怀疑⼈⽣ 什么是贪⼼算法&#xff1f; 贪⼼算法&#xff0c;或者说是贪⼼策略&#xff1a;企图⽤局部最优找出全局最优。 把解决问题的过程分成若⼲步&#xff1b;解决每⼀步时…

瑞萨RA4M2使用心得-KEIL5的第一次编译

目录 前言 环境&#xff1a; 开发板&#xff1a;RA-Eco-RA4M2-100PIN-V1.0 IDE&#xff1a;keil5.35 一、软件的下载 编辑瑞萨的芯片&#xff0c;除了keil5 外还需要一个软件&#xff1a;RASC 路径&#xff1a;Releases renesas/fsp (github.com) 向下找到&#xff1a; …

数据分析-Excel-学习笔记

Day1 复现报表聚合函数&#xff1a;日期联动快速定位区域SUMIF函数SUMIFS函数环比、同比计算IFERROR函数混合引用单元格格式总结汇报 拿到一个Excel表格&#xff0c;首先要看这个表格个构成&#xff08;包含了哪些数据&#xff09;&#xff0c;几行几列&#xff0c;每一列的名称…

整车CAN网络和CANoe

车载网络中主要包含有Can网络,Lin网络,FlexRay,Most,以太网。 500kbps:500波特率,表示的数据传输的速度。表示的是最大的网速传输速度。也就是每秒 500kb BodyCan车身Can InfoCan娱乐信息Can 车身CAN主要连接的是ESB电动安全带 ADB自适应远光灯等 PTCan动力Can 底盘Can

ChatGPT 的新图像生成器非常擅长伪造收据

本月&#xff0c;ChatGPT 推出了一种新的图像生成器&#xff0c;作为其 4o 模型的一部分&#xff0c;该模型在生成图像内的文本方面做得更好。 人们已经在利用它来生成假的餐厅收据&#xff0c;这可能会为欺诈者使用的已经很广泛的 AI 深度伪造工具包添加另一种工具。 多产的…

JS页面尺寸事件

元素位置 在这里插入图片描述 父元素带有定位时输出相对于父亲元素的距离值

网络协议之基础介绍

写在前面 本文看下网络协议相关基础内容。 1&#xff1a;为什么要有网络协议 为了实现世界各地的不同主机的互联互通。 2&#xff1a;协议的三要素 协议存在的目的就是立规矩&#xff0c;无规矩不成方圆嘛&#xff01;但是这个规矩也不是想怎么立就怎么立的&#xff0c;也…

初识数据结构——Java集合框架解析:List与ArrayList的完美结合

&#x1f4da; Java集合框架解析&#xff1a;List与ArrayList的完美结合 &#x1f31f; 前言&#xff1a;为什么我们需要List和ArrayList&#xff1f; 在日常开发中&#xff0c;我们经常需要处理一组数据。想象一下&#xff0c;如果你要管理一个班级的学生名单&#xff0c;或…

uniapp微信小程序引入vant组件库

1、首先要有uniapp项目&#xff0c;根据vant官方文档使用yarn或npm安装依赖&#xff1a; 1、 yarn init 或 npm init2、 # 通过 npm 安装npm i vant/weapp -S --production# 通过 yarn 安装yarn add vant/weapp --production# 安装 0.x 版本npm i vant-weapp -S --production …

贪心进阶学习笔记

反悔贪心 贪心是指直接选择局部最优解&#xff0c;不需要考虑之后的影响。 而反悔贪心是在贪心上面加了一个“反悔”的操作&#xff0c;于是又可以撤销之前的“鲁莽行动”&#xff0c;让整个的选择稍微变得“理智一些”。 于是&#xff0c;我个人理解&#xff0c;反悔贪心是…