当前位置: 首页 > news >正文

Socket编程TCP

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.dtcms.com/a/113531.html

相关文章:

  • HarmonyOS:WebView 控制及 H5 原生交互实现
  • 硬件学习之器件篇-蜂鸣器
  • 第三章 react redux的学习之redux和react-redux,@reduxjs/toolkit依赖结合使用
  • use_tempaddr 笔记250405
  • setj集合
  • 1.5 基于改进蛇优化VGG13SE故障诊断方法的有效性分析
  • Python实现链接KS3,并将文件数据上传到KS3
  • 【spring Cloud Netflix】OpenFeign组件
  • 第二十九章:Python-mahotas库:图像处理的高效工具
  • 使用 pytest-xdist 进行高效并行自化测试
  • PHP的垃圾回收机制
  • 我的创作历程:从不情愿到主动分享的成长
  • 用北太天元脚本解决了关于双曲线的求离心率对应的参数、等腰三角形条件下的点坐标和向量点积条件下的参数范围
  • 如何判断栈生长的方向
  • SDL显示YUV视频
  • 快速从零部署一个DeepSeek-R1服务
  • NAS原理与技术详解:从基础概念到实践应用
  • 基础知识补充篇:关于数据不可修改
  • 功能测试和性能测试的区别有哪些?
  • 使用Geotools中的原始方法来操作PostGIS空间数据库
  • java高并发------守护线程Daemon Thread
  • Redis数据结构之ZSet
  • P3654 First Step (ファーストステップ)
  • Linux:(五种IO模型)
  • 基于SSM的高校宿舍水电管理系统
  • 0201线性回归-机器学习-人工智能
  • 开篇 - 配置Unlua+VsCode的智能提示、调试以及学习方法
  • 【LeetCode 热题100】23:合并 K 个升序链表(详细解析)(Go语言版)
  • 《UNIX网络编程卷1:套接字联网API》第7章:套接字选项深度解析
  • 如何理解分类(Category)?Kotlin 扩展是何方神圣?C/C++编译器的C/C++扩展