TCP网络编程

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

Tcp网络程序

1.listen

2.accept​

版本①转换大小写

3.connect​

版本②多进程​编辑

版本③多线程

版本④线程池

版本⑥守护进程

版本⑦网络计算器


Tcp网络程序

1.listen

        注意本文的所有代码已上传gitee如果想要更好的体验请配合整体代码观看本篇文章。

        其中已经用到过的知识就不细讲了只细谈新接触到的。

        上面我们提到过Tcp面向的是字节流而udp面向的是数据报所以在创建socket时就有区别。  

        将普通的套接字文件变为监听套接字。

        为什么要监听呢因为tcp是面向连接的。何为面向可以理解为面向对象就是在写代码之前将对象先写出来。那面向连接呢此时还没有建立连接所以要将套接字文件变为监听套接字一直处于监听状态等待他人连接。 

代码


static void Usage(const string proc)
{
    cout << "Usage:\n\t" << proc << " port [ip]" << endl;
}

class Tcpserver
{
public:
    Tcpserver(uint16_t port, const string &ip = "")
        : _sock(-1), _port(port), _ip(ip)
    {
    }
    ~Tcpserver()
    {
    }

public:
    void init()
    {
        // 1 create cosk
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            logMessage(FATAL, "socket: %s%d", strerror(errno), _sock);
            exit(1);
        }
        logMessage(DEBUG, "socket create success : %d", _sock);

        // 2 bind
        // 2.1 填充网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        // 这里的inet_aton 和 inet_addr的用途一样
        _ip.empty() ? INADDR_ANY : (inet_aton(_ip.c_str(), &local.sin_addr));
        // 2.2 bind网络信息
        if (bind(_sock, (const sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind: %s%d", strerror(errno), _sock);
            exit(2);
        }
        logMessage(DEBUG, "bind success: %d", _sock);

        // 3 listen
        if (listen(_sock, 5) < 0) // 为什么填5我们后面讲Tcp协议时讲
        {
            logMessage(FATAL, "listen: %s%d", strerror(errno), _sock);
            exit(3);
        }
        logMessage(DEBUG, "listen success: %d", _sock);
    }
    void start()
    {
        while(1)
        {
            logMessage(DEBUG,"Server run ...");
            sleep(1);
        }
    }

private:
    int _sock;
    uint16_t _port;
    string _ip;
};

int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3)
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port = atoi(argv[1]);
    string ip;
    if (argc == 3)
    {
        ip = argv[2];
    }

    Tcpserver tcp(port);
    tcp.init();
    tcp.start();
    return 0;
}

结果 

2.accept

        返回值  成功返回套接字也是一个文件描述符失败-1。第一个参数为调用socket返回的文件描述符返回值与第一参数都是文件描述符。返回值主要是为用户提供网络服务的socket主要是IO。第一个参数主要是为了获得新的连接监听socket。后面两个参数的意义与recevfrom后两个参数一样。

代码

    void start()
    {
        while (1)
        {
            // 4 获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int servicesock = accept(_sock, (struct sockaddr *)&peer, &len);
            if (servicesock < 0)
            {
                logMessage(WARNING, "accept: %s%d", strerror(errno), servicesock);
                continue;
            }
            logMessage(DEBUG, "accept success: %d", _sock);
            // 4.1 获取客户端信息
            int clientport = ntohs(peer.sin_port);
            string clientip = inet_ntoa(peer.sin_addr);
            // logMessage(DEBUG,"Server run ...");
            // sleep(1);
        }
    }

版本①转换大小写

        我们先尝试下简单的服务将小写转换为大写。

        这里接收信息为何不用recevfrom因为recevfrom和sendto是配套提供给udp使用的在tcp中我们使用read和write通过文件描述符来对网卡操作毕竟linux下一切皆文件。

代码

    start
    // 5 提供服务
    // 5.1 转换大小写
    tranfrom(servicesock, clientip, clientport);

    void tranfrom(int sock, const string &ip, int port)
    {
        assert(sock >= 0);
        assert(!ip.empty());
        char inbuffer[1024];
        while (1)
        {
            // 首先要接受信息      我们认为读到的是字符串因为字符串后面自动会加'/0',我们选择不读'/0'
            ssize_t s = read(sock, inbuffer, sizeof(inbuffer) - 1);
            if (s > 0)
            {
                inbuffer[s] = '\0';
                if (strcasecmp(inbuffer, "quit") == 0)
                {
                    logMessage(DEBUG, "clinet quit %s %d", ip, port);
                    break;
                }
                logMessage(DEBUG, "Infor : %s %d >>> %s", ip, port, inbuffer);
                
                for (int i = 0; i < s; i++)
                {
                    if (isalpha(inbuffer[i]) && islower(inbuffer[i]))
                    {
                        inbuffer[i] = toupper(inbuffer[i]);
                    }
                }
                write(sock, inbuffer, strlen(inbuffer));
            }
            else if (s == 0)
            {
                // 代表对端关闭client退出
                logMessage(DEBUG, "clinet quit %s %d", ip, port);
                break;
            }
            else
            {
                logMessage(DEBUG, "%s %d read error: %s", ip, port, strerror(errno));
                break;
            }
        }

        写到这里我们将client来写下要不不能验证server中写的成果。

3.connect

        tcpclient需要connect来连接server因为write没有这个功能而udpclient中直接使用sendto兼含连接与发送。

static void Usage(const string proc)
{
    cout << "Usage:\n\t"
         << "server IP ,server port" << endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    // 获取服务端
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    // 1 创建socket
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        cerr << "socket error : " << strerror(errno) << endl;
        exit(1);
    }

    // 2 connect
    // 2.1 填充
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    server.sin_port = htons(serverport);

    // 3 连接
    if (connect(sock, (const struct sockaddr *)&server, sizeof(server)) != 0)
    {
        cerr << "connect error : " << strerror(errno) << endl;
        ;
        exit(2);
    }
    cout << "connect sueccess : " << sock << endl;

    string message;
    while (!quit)
    {
        message.clear();
        cout << "Plz entry : ";
        getline(cin, message);
        if (strcasecmp(message.c_str(), "quit") == 0)
        {
            quit = true;
        }

        ssize_t s = write(sock, message.c_str(), message.size());
        if (s > 0)
        {
            message.resize(1024);
            ssize_t s = read(sock, (char *)message.c_str(), 1024);
            if (s > 0)
            {
                message[s] = '\0';
            }
            cout << "server transfer"
                 << ">>> " << message << endl;
        }
        else if (s <= 0)
        {
            break;
        }
    }
    close(sock);
    return 0;
}

        我们来调试一下看看结果如何

版本②多进程

         为什么会出现这种情况两个client同时去访问server其中一个client正常发送与接受另一个缺阻塞住了必须等待另一个client结束才能正常输入与接受呢

        因为我们当前的server是单进程的其中transfer的while循环一直在提供服务进程不结束单进程并不会给另一个client提供服务。所以我们要写一个多进程版本。

代码

            // 5.2 多进程版本
            pid_t pid = fork();
            assert(pid != -1);
            if(pid == 0)
            {
                // child 注意子进程会继承父进程的文件描述符
                tranfrom(servicesock, clientip, clientport);
                exit(0);
            }
            // parent
            // 一定要关闭不然文件描述符会越来越少
            close(servicesock);
            // 方案一 waitpid -1 WNOHANG 具体的进程细节可以看我前面的博客
            // 方案二 signal
            signal(SIGCHLD,SIG_IGN);

结果

         我们查看会发现有三个进程在跑证明多进程版本成功。

        进阶版本

代码

            // 5.3 多进程进阶版
            // 爷爷进程
            pid_t pid = fork();
            if (pid == 0)
            {
                // 爸爸进程
                if (fork() > 0)
                {
                    // 爸爸进程退出
                    exit(0);
                }
                // 走到这里的是由爸爸进程衍生的儿子进程
                // 儿子进程此时属于孤儿进程不用管他可以交给操作系统去回收
                tranfrom(servicesock, clientip, clientport);
            }
            close(servicesock);
            // 回收爸爸进程阻塞式回收并不会阻塞等待父进程退出因为父进程直接会退出
            pid_t pd = waitpid(pid, nullptr, 0);
            assert(pd > 0);

结果

        在多开几个也没问题       

版本③多线程

代码

class Tcpserver; // 声明一下tcp

class ThreadData
{
public:
    ThreadData(uint16_t clientport, string clientip, int sock, Tcpserver *ts)
        : _clientport(clientport), _clientip(clientip), _sock(sock), _this(ts)
    {
    }

public:
    uint16_t _clientport;
    string _clientip;
    int _sock;
    Tcpserver *_this;
};    

start:
        // 5.4 多线程版本
        ThreadData *td = new ThreadData(clientport, clientip, servicesock,this);
        pthread_t t;
        pthread_create(&t, nullptr, fuc, (void *)td);

    static void *fuc(void *argc)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData *>(argc);
        td->_this->tranfrom(td->_sock, td->_clientip, td->_clientport);
        delete td;
        return nullptr;
    }

结果

         我们多开几个client去连接server

版本④线程池

        我们把提供服务的任务交给线程池来做。我们将前几次写的lock.hpp因为线程池的关系需要锁和线程池这个线程池是单例模式拿过来并写一个Task.hpp其中包含了回调函数的细节。

threadpool.hpp
#include "util.hpp"
#include "lock.hpp"

using namespace std;

int gThreadNum = 15;

template <class T>
class ThreadPool
{
private:
    ThreadPool(int threadNum = gThreadNum) : threadNum_(threadNum), isStart_(false)
    {
        assert(threadNum_ > 0);
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    void operator=(const ThreadPool<T>&) = delete;

public:
    static ThreadPool<T> *getInstance()
    {
        static Mutex mutex;
        if (nullptr == instance) //仅仅是过滤重复的判断
        {
            LockGuard lockguard(&mutex); //进入代码块加锁。退出代码块自动解锁
            if (nullptr == instance)
            {
                instance = new ThreadPool<T>();
            }
        }

        return instance;
    }
    //类内成员, 成员函数都有默认参数this
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        // prctl(PR_SET_NAME, "follower"); // 更改线程名称
        while (1)
        {
            tp->lockQueue();
            while (!tp->haveTask())
            {
                tp->waitForTask();
            }
            //这个任务就被拿到了线程的上下文中
            T t = tp->pop();
            tp->unlockQueue();
            t(); // 让指定的先处理这个任务
        }
    }
    void start()
    {
        assert(!isStart_);
        for (int i = 0; i < threadNum_; i++)
        {
            pthread_t temp;
            pthread_create(&temp, nullptr, threadRoutine, this);
        }
        isStart_ = true;
    }
    void push(const T &in)
    {
        lockQueue();
        taskQueue_.push(in);
        choiceThreadForHandler();
        unlockQueue();
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    int threadNum()
    {
        return threadNum_;
    }

private:
    void lockQueue() { pthread_mutex_lock(&mutex_); }
    void unlockQueue() { pthread_mutex_unlock(&mutex_); }
    bool haveTask() { return !taskQueue_.empty(); }
    void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
    void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
    T pop()
    {
        T temp = taskQueue_.front();
        taskQueue_.pop();
        return temp;
    }

private:
    bool isStart_;
    int threadNum_;
    queue<T> taskQueue_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *instance;
    // const static int a = 100;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr;

        

Task.hpp
class Task
{
public:
    //等价于
    // typedef std::function<void (int, std::string, uint16_t)> callback_t;
    using callback_t = std::function<void (int, std::string, uint16_t)>;
private:
    int sock_; // 给用户提供IO服务的sock
    uint16_t port_;  // client port
    std::string ip_; // client ip
    callback_t func_;  // 回调方法
public:
    Task():sock_(-1), port_(-1)
    {}
    Task(int sock, std::string ip, uint16_t port, callback_t func)
    : sock_(sock), ip_(ip), port_(port), func_(func)
    {}
    void operator () ()
    {
        logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求正在进行中......",\
            pthread_self(), ip_.c_str(), port_);

        func_(sock_, ip_, port_);

        logMessage(DEBUG, "线程ID[%p] 处理%s:%d的请求已经结束了......",\
            pthread_self(), ip_.c_str(), port_);
    }
    ~Task()
    {}
};

          注意现在tranfrom函数放在了类的外面。

    // 在init函数中初始化线程池  
    init:
    // 4 加载线程池
    // 当前的线程池是单例模式
    _tp = ThreadPool<Task>::getInstance();
    // 在start函数中运行线程池
    start:
    // 启动线程池
    _tp->start();
    // 5.5 线程池版本
    Task t(servicesock, clientip, clientport, tranfrom);
    _tp->push(t);
        
    

        这几步的目的是初始化线程池运行线程池将Task任务传入线程池中调用Task任务中的回调函数。 

结果

         使用线程池的结果就是每一个处理任务的线程都是独立不同的线程但是现在还是具有缺点因为线程池中的线程个数有限并且这次的任务tranfrom是个死循环如果一直申请任务任务不退出最后就不能申请任务了。

版本⑤远程获取服务端shell信息版本

        这次我们要用到一个新的函数。

        作用是输入命令将命令执行完的结果保存在文件中。 

代码

    start
    // 5.6 远程获取服务端shell信息版本
    Task t(servicesock, clientip, clientport, execCommand);
    _tp->push(t);

void execCommand(int sock, string ip, uint16_t port)
{
    assert(sock >= 0);
    assert(!ip.empty());

    char command[1024];
    while (1)
    {
        // 首先要接受信息
        ssize_t s = read(sock, command, sizeof(command) - 1);
        if (s > 0)
        {
            command[s] = '\0';
            string safe = command;
            // 防止恶意操作
            if ((string::npos != safe.find("rm")) || (string::npos != safe.find("unlink")))
            {
                break;
            }

            FILE *fp = popen(command, "r");
            if (fp == nullptr)
            {
                logMessage(FATAL, "popen fail , command is : %s ,because : %s ", command, strerror(errno));
                break;
            }
            logMessage(DEBUG, "%s %d 执行的任务是[%s] ", ip.c_str(), port, command);

            char line[1024];
            while (fgets(line, sizeof(line) - 1, fp) != nullptr)
            {
                // 将文件的内容写到line中再从line中写到servicesock中
                write(sock, line, strlen(line));
            }
            pclose(fp);
            logMessage(DEBUG, "%s %d 的[%s]任务执行完毕 ", ip.c_str(), port, command);
        }
        else if (s == 0)
        {
            // 代表对端关闭client退出
            logMessage(DEBUG, "clinet quit %s %d", ip.c_str(), port);
            break;
        }
        else
        {
            logMessage(DEBUG, "%s %d read error: %s", ip.c_str(), port, strerror(errno));
            break;
        }
    }

    // client退出会走到这里
    // 将提供服务的文件描述符关掉
    close(sock);
}

 结果

版本⑥守护进程

        一般而言对外提供的服务器都是以守护进程(精灵进程)的方式在工作除非用户手动关闭否则一直会在运行。

        这里的PPID是父进程idPID是这个进程的idPGID是进程组的id此次重要的是SIDSID为当前进程的会话id。

        PGID是运行的一组进程的第一个进程的PID。

        当前这几个进程都属于PGID为28753的这个进程组。

        那什么又是会话呢 

        在我们登陆linux或者windows时linux服务器就会形成一个叫会话的东西它是由一个前台进程组必须要有和0个或多个后台进程组构成。

        有时当window特别卡的时候我们可以选择注销再登陆这样电脑就不卡了。背后的原因有注销相当于将当前会话中的进程都删掉这样就不会卡了。

        一般而言一个会话中的初始进程都是bash这个会话的id就是bash的pid。

         最后的进程为bash进程sleep这几个进程属于bash这个会话中的进程所以sid与bash的pid相同。

        我们今天要做的是将我们的服务器单独形成一个新的会话不然在原先的会话中会受到用户登陆 注销对会话的影响。

        像这种自成进程组自成新会话周而复始的去运行的进程就叫做守护进程或者精灵进程。

        如何让进程变为守护进程呢我们要使用setsid并做其他手段。

         哪一个进程调用这个函数就会变成守护进程返回值为该进程的pid前提是该进程不能是进程组的组长就是该进程的进程组id不能与该进程的id相同。

        在使用setsid之前我们要做到

必做

        ①用子进程调用setsid

        ②close012将标准输出标准输入标准错误对应的文件描述符都关闭一旦成为守护进程就跟键盘显示屏输入无关了因为输入和输出都是从网络中写入或获取的。但这种方法很少有人做。

        ③打开/dev/null并且进行对012的重定向。/dev/null是一个linux下的垃圾桶凡是从/dev/null里面读写一概被丢弃。

选做

        ①忽略SIGPIPI信号因为server在作为写端写的时候如果client关闭管道会导致client发送SIGPIPE信号向server导致server关闭我们要想server不被影响可以用signal来使server忽略SIGPIPE。
        ②更改进程的工作目录chdir。

代码 在main函数中调用init和start之前调用daemonize                             

void daemonize()
{
    // 1. 忽略SIGPIPE信号
    signal(SIGPIPE, SIG_IGN);
    // 2. 改变当前工作目录
    // chdir() 这次就不做了
    // 3. 让自己成为进程组组长
    if (fork() > 0)
        exit(1);
    // 4. 调用setsid
    setsid();
    // 5. 重定向012
    int fd = 0;
    if ((fd = open("/dev/null", O_RDWR)) != -1)
    {
        dup2(fd,STDIN_FILENO);
        dup2(fd,STDOUT_FILENO);
        dup2(fd,STDERR_FILENO);
        // 6. 关闭fd
        if(fd > STDERR_FILENO) close(fd);
    }
}

结果

        我们的tcpserver犹如sshd一样成了一个守护进程。再次运行clinet也是可以的。

        现在会有问题我们的日志呢全都到垃圾桶去了我们需要将日志输入到日志文件中。我们需要将打印日志的函数修改一下。

代码 

#define LOGFIFE "tcpserver.log"

const char *log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};

void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);
    char logInfor[1024];
    char *name = getenv("USER");
    va_list ap;
    va_start(ap, format);

    vsnprintf(logInfor, sizeof(logInfor) - 1, format, ap);

    va_end(ap);
    
    umask(0);
    int fd = open(LOGFIFE, O_WRONLY | O_CREAT | O_APPEND,0666);

    FILE * out = (level == FATAL) ? stderr : stdout;

    dup2(fd,1);
    dup2(fd,2);

    fprintf(out,"%s | %u | %s | %s\n",\
        log_level[level],\
        (unsigned int)time(nullptr),\
        name == nullptr ? "Unkown" : name,\
        logInfor
    );

    // 将C缓冲区中的内容刷新到os中
    fflush(out);
    // 将os中的数据尽快刷到磁盘中
    fsync(fd);

}

  结果

      

        如何证明它是守护进程

        像其他与会话紧密相连的进程不是守护进程的进程。 在断开连接之后重新登陆在未断开连接之前运行的进程就无了。

         而守护进程只要开始运行就与世界无关了独立的会话独立的进程组除非用kill -9将该进程删除不让会一直运行。

        如何不使用系统提供的daemon和自己手动去让进程变为守护进程的方法来让一个进程变为守护进程可以使用

        可以发现当前的nohup.out所占的空间在不断变大打开就是a.out输出到stdout的信息。 

         查看它的sid发现还是和bash是同一个会话组的。

        再次重启之后发现a.out还在运行并且自成一个会话。 

      

        至于一些关于打印与服务器安全退出的杂项这就不讲了有兴趣的可以看下我的gitiee。

问题一

        如果将服务器退出客户端还在链接再次将服务器重新启动会发现客户端链接不上重新启动之后的服务器。

问题二

        listen的第二个参数在本片文章中没有讲。

问题三
        popen在输入不识别的命令时不会返回NULL还是会打开文件但是不会文件结束标识会导致客户端阻塞。

        这三个问题后续都会讲到请耐心等待。

版本⑦网络计算器

        先前的版本信息之间的传输都是按“字符串”的方式来接收与发送的如果接收与发送的信息是结构化的数据怎么办呢

        可不可以这样以计算数字为例

        这样不挺好的吗

        弊端就是每一个系统之间的结构体对齐规则不一样当前对象所占的大小为12字节如果客户端将数据解释为10字节就会造成数据丢失。

        当前我们可以将这个结构体转换成“字符串”并制定规则由对方接收。

        将结构化的数据转化为字符串或者字节流的方式叫做序列化。

        相反则叫做反序列化。

        除了将结构化的数据转化为字符串我们还要将字符串的长度加在序列化之后的字符串的首字母之前。至于为什么我如果连续向通信管道之中发送数据一堆信息都被对面读取但是在反序列化时不知道将多少数据作为一条消息还是所有的信息都是一条在每一条消息发送之前带上信息大小便于更好地反序列化。这些过程就是在制定协议。

        至于要做网络计算器我们首先搭出部分框架。

客户端main函数中建立连接之后
    // 4 计算
    string message;
    while (!quit)
    {
        message.clear();
        cout << "请输入表达式: ";
        getline(cin, message);
        if (strcasecmp(message.c_str(), "quit") == 0)
        {
            quit = true;
            continue;
        }

        Request req;
        if (!buyRequest(message, &req))
        {
            continue;
        }

        string str;
        req.serialize(&str);
        cout << "message->serialize: " << str << endl;

        str = encode(str, str.size());
        cout << "str->encode: " << str << endl;

        ssize_t s = write(sock, str.c_str(), str.size());
        if (s > 0)
        {
            char buff[1024];
            size_t s = read(sock, buff, sizeof(buff) - 1);
            if (s > 0)
            {
                buff[s] = 0;
                string package = buff;
                cout << "Debug->getmessage: " << package << endl;

                Responce resp;
                uint32_t len = 0;

                string str = decode(package, &len);
                if (len > 0)
                {
                    package = str;
                    cout << "Debug->decode: " << package << endl;
                  
                    resp.Deserialize(package);
                    cout << " result : " << resp._result << " "
                         << " resultcode : " << resp._resultcode << endl;
                }
            }
        }
        else if (s <= 0)
        {
            break;
        }
    }

服务端在主函数中依据前面线程池调用回调函数的方法

void netcalculate(int sock, string ip, uint16_t port)
{
    assert(sock >= 0);
    assert(!ip.empty());

    Request req;
    string inbuff;
    while (1)
    {
        char buff[128];
        int sz = read(sock, buff, sizeof(buff));
        if (sz == 0)
        {
            // 代表对端关闭client退出
            logMessage(DEBUG, "clinet quit %s %d", ip.c_str(), port);
            break;
        }
        else if (sz < 0)
        {
            logMessage(DEBUG, "%s %d read error: %s", ip.c_str(), port, strerror(errno));
            break;
        }
        buff[sz] = 0;
        inbuff += buff;

        // 例如9\r\n888 + 666\r\n
        // 1 检查是否接收到有一个完整的序列化后的字符串
        uint32_t packagelen = 0;
        string package = decode(inbuff, &packagelen);
        // 无法读取一个完整的序列化后的字符串重新读取
        if (packagelen == 0)
            continue;

        // 上一步得到了len将字符串最前端的数字去掉
        // 2 反序列化
        if (req.Deserialize(package))
        {
            req.debug();
            // 3 逻辑处理 得出算数结果
            Responce respon = calculate(req);
            
            // 4 序列化得到的结果
            string respPackage;
            respon.serialize(&respPackage);

            // 5 encode序列化后的字符串
            respPackage = encode(respPackage,respPackage.size());

            // 6 发送
            write(sock,respPackage.c_str(),respPackage.size());  
        }
    }
}

        下面是序列化反序列化等函数的实现


#define CRLF "\r\n"
#define CRLFLEN strlen(CRLF)
#define SPACE " "
#define SPACELEN strlen(SPACE)

#define OPS "+-*/%"

static Responce calculate(const Request &req)
{
    Responce resp;
    switch (req._opr)
    {
    case '+':
        resp._result = req._x + req._y;
        break;
    case '-':
        resp._result = req._x - req._y;
        break;
    case '*':
        resp._result = req._x * req._y;
        break;
    case '/':
    {
        if (req._y == 0)
        {
            resp._resultcode = 1; // 1 除零错误
            resp._result = INT32_MAX;
        }
        else
            resp._result = req._x / req._y;
        break;
    }
    case '%':
       {
        if (req._y == 0)
        {
            resp._resultcode = 2; // 2 模零错误
            resp._result = INT32_MAX;
        }
        else
            resp._result = req._x % req._y;
        break;
    }
    default:
        resp._resultcode = 3;  // 非法输入
        break;
    }
    return resp;
}
// encode 为整个序列化之后的字符串添加长度
string encode(const string &in, uint32_t len)
{
    // "_resultcode _result" -> "len\r\n_resultcode _result\r\n"
    string ret = to_string(len);
    ret += CRLF;
    ret += in;
    ret += CRLF;
    return ret;
}
// 1 可以作为检查函数检查有没有读取到完整序列化并encode过的字符串
//  必须有完整长度  具有与长度相符的有效载荷
// 2 由上述条件可以返回有效载荷和有效长度
// decode 为整个序列化之后的字符串提取长度
//   9\r\n888 + 666\r\n \r\n88981\n
string decode(string &in, uint32_t *len)
{
    assert(len);
    // 初始检查
    *len = 0;
    size_t pos = in.find(CRLF);
    if (pos == string::npos)
        return "";

    // 1 提取长度
    string slen = in.substr(0, pos);
    int ilen = atoi(slen.c_str());

    // 2 确认有效载荷是否符合要求
    int sz = in.size() - 2 * CRLFLEN - pos;
    if (sz < ilen)
        return "";

    // 3 提取888 + 666
    string package = in.substr(pos + CRLFLEN, ilen);
    *len = ilen;

    // 4 将提取完毕的字符串从读取的字符串删除便于下次decode
    int remveLen = slen.size() + 2 * CRLFLEN + package.size();
    in.erase(0, remveLen);

    // 5 返回
    return package;
}

// 定制请求
class Request
{
public:
    Request()
    {
    }
    ~Request()
    {
    }
    // 序列化
    void serialize(string *out)
    {
        string x = to_string(_x);
        string y = to_string(_y);

        *out = x;
        *out += SPACE;
        *out += _opr;
        *out += SPACE;
        *out += y;
    }
    // 反序列化
    bool Deserialize(const string &in)
    {
        // 888 + 666
        size_t firpos = in.find(SPACE);
        if (firpos == string::npos)
            return false;
        size_t secpos = in.rfind(SPACE);
        if (secpos == string::npos)
            return false;

        string date1 = in.substr(0, firpos);
        string date2 = in.substr(secpos + SPACELEN);
        string opr = in.substr(firpos + SPACELEN, secpos - (firpos + SPACELEN));
        if (opr.size() != 1)
            return false;

        _x = atoi(date1.c_str());
        _y = atoi(date2.c_str());
        _opr = opr[0];
        return true;
    }
    void debug()
    {
        cout << "---------------------------------------------" << endl;
        cout << " _x: " << _x << " _opr: " << _opr << " _y: " << _y<<endl;
        cout << "---------------------------------------------" << endl;
    }

public:
    int _x;
    char _opr;
    int _y;
};

// 定制响应
class Responce
{
public:
    Responce() : _resultcode(0), _result(0)
    {
    }
    ~Responce()
    {
    }
    // 序列化
    void serialize(string *out)
    {
        // "_resultcode _result"
        string code = to_string(_resultcode);
        string res = to_string(_result);

        *out = code;
        *out += SPACE;
        *out += res;
    }
    // 反序列化
    bool Deserialize(const string &in)
    {
        // "_resultcode _result"
        size_t pos = in.find(SPACE);
        if (pos == string::npos)
        {
            return false;
        }
        string rcode = in.substr(0, pos);
        string result = in.substr(pos + SPACELEN);
        _resultcode = atoi(rcode.c_str());
        _result = atoi(result.c_str());

        return true;
    }
    void debug()
    {
        cout << "---------------------------------------------" << endl;
        cout << " result: " << _result << " resultcode: " << _resultcode << endl;
        cout << "---------------------------------------------" << endl;
    }

public:
    int _resultcode;
    int _result;
};

bool buyRequest(string &msg, Request *req)
{
    // 将1+1输入的字符串存储到对象中
    char buff[1024];
    snprintf(buff, sizeof(buff), "%s", msg.c_str());
    char *left = strtok(buff, OPS);
    if (left == nullptr)
    {
        return false;
    }
    char *right = strtok(nullptr, OPS);
    if (right == nullptr)
    {
        return false;
    }
    char mid = msg[strlen(left)];

    req->_x = atoi(left);
    req->_y = atoi(right);
    req->_opr = mid;

    return true;
}

        写了这么多看下实战效果。

服务端

 客户端

        写了这么长代码是否有简单的方式就将结构化的数据转换成便于发送的数据呢我们接下来可以使用下json这是别人的方案。在使用json时它作为一个独立的第三方库我们要使用

sudo yum install -y jsoncpp-devel

来下载使用。

        注意makefie中编译时要带上-ljsoncpp否则会导致连接出错。

代码

request
// #define MYSOLUTION 1
    void serialize(string *out)
    {
#ifdef MYSOLUTION
        string x = to_string(_x);
        string y = to_string(_y);

        *out = x;
        *out += SPACE;
        *out += _opr;
        *out += SPACE;
        *out += y;
#else
        // 使用json
        // 1 value对象是万能对象 什么类型都能接收
        // 2 json是基于k-v
        // 3 json有两套方法
        // 4 json会将数据结合转换为字符串
        Json::Value root;
        root["x"] = _x;
        root["y"] = _y;
        root["opr"] = _opr;

        Json::FastWriter fw;
        *out = fw.write(root);
#endif
    }
    // 反序列化
    bool Deserialize(const string &in)
    {
#ifdef MYSOLUTION
        // 888 + 666
        size_t firpos = in.find(SPACE);
        if (firpos == string::npos)
            return false;
        size_t secpos = in.rfind(SPACE);
        if (secpos == string::npos)
            return false;

        string date1 = in.substr(0, firpos);
        string date2 = in.substr(secpos + SPACELEN);
        string opr = in.substr(firpos + SPACELEN, secpos - (firpos + SPACELEN));
        if (opr.size() != 1)
            return false;

        _x = atoi(date1.c_str());
        _y = atoi(date2.c_str());
        _opr = opr[0];
        return true;
#else
        // 使用json
        Json::Value root;
        Json::Reader rd;
        rd.parse(in, root);

        _x = root["x"].asInt();
        _y = root["y"].asInt();
        _opr = root["opr"].asInt();
        return true;
#endif
    }
Responce
void serialize(string *out)
    {
#ifdef MYSOLUTION
        // "_resultcode _result"
        string code = to_string(_resultcode);
        string res = to_string(_result);

        *out = code;
        *out += SPACE;
        *out += res;
#else
        // json
        Json::Value root;
        root["resultcode"] = _resultcode;
        root["result"] = _result;
        Json::FastWriter fw;
        *out = fw.write(root);
#endif
    }
    // 反序列化
    bool Deserialize(const string &in)
    {
#ifdef MYSOLUTION
        // "_resultcode _result"
        size_t pos = in.find(SPACE);
        if (pos == string::npos)
        {
            return false;
        }
        string rcode = in.substr(0, pos);
        string result = in.substr(pos + SPACELEN);
        _resultcode = atoi(rcode.c_str());
        _result = atoi(result.c_str());

        return true;
#else
        // json
        Json::Value root;
        Json::Reader rd;
        rd.parse(in, root);

        _resultcode = root["resultcode"].asInt();
        _result = root["result"].asInt();
        return true;
#endif
    }

 结果

        将Json::FastWriter fw改为Json::StyledWriter fw。这是另外一种方案。

        如何随时改变关于序列化与反序列化的方案改变宏是一种方法在makefie中增加内容也是一种方法。

代码

makefile:
.PHONY:all
all:tcpclient tcpserver
Method=-DMYSOLUTION

tcpclient:tcpclient.cc
	g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp -lpthread
tcpserver:tcpserver.cc
	g++ -o $@ $^ $(Method) -std=c++11 -ljsoncpp -lpthread

.PHONY:clean
clean:
	rm -f tcpclient tcpserver tcpserver.log

         现在默认使用我们的solution。

结果

         

         将Mothed屏蔽。

        写了这么多总共包括

        1.基本系统socket套接字的使用listen等

        2.基本的协议定制序列化等

        3.业务的实现计算器等

        写到这里关于Tcp网络程序的编写也告一段落了不知道是否有人会看到这里感谢观看我们下次再见。这次的Tcp网络程序其中还有很多BUG限于时间和学识的约束以后会有机会来完善它的。

        

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6