Bootstrap

c++网络编程实战——开发基于协议的文件传输模块(一)如何实现一个简单的tcp长连接

前言

在之前的几篇内容中我们已经介绍过基于ftp协议的文件传输模块,而这个系列我们所想实现的就是如何实现基于tcp进行的文件传输模块,话不多说,开坑开坑!

什么是tcp长连接

我们知道tcp在建立连接的时候会通过三次握手与四次挥手来建立tcp连接,而服务端与客户端之间的工作流程一般是这样的:
在这里插入图片描述
它的工作流程如下:

1.客户端向服务端发送连接请求
2.服务端接收客户端连接请求
3.二者之间相互发送报文实现数据的传输
4.断开连接

这种一完成数据交换就断开连接的通讯方式我们称为tcp的短连接。那么现在问题来了: 客户端与服务端连接是需要时间的,同时是否可以立即连接上是不确定的(如果现在服务端可连接的客户端已达到上限),如果我们希望让客户端与服务端始终保持连接状态,应该怎么办呢?这就是我们今天所要探讨的——tcp长连接

什么是tcp长连接?相对于tcp短连接在业务流程结束后就会断开服务端与客户端之间的连接,tcp长连接在不进行通讯的时候也会保持连接, 以便后续可以继续使用该连接进行通信。

tcp长连接的实现机制

其实tcp长连接的实现机制很简单,在之前的文章中我们就已经讲过我们通过进程心跳来告诉进程守护模块进程是否在正常运行,在tcp长连接中我们亦可以通过发送心跳报文来让保证客户端与服务端之间的连接。

思考:
为什么我们要发送心跳报文呢?理论上只要我们不主动断开的话服务端和客户端不就是一直连接,但是由于在等待过程中可能会出现特殊情况导致连接断开,所以我们需要发送心跳报文来进行对tcp连接的监控

tcp长连接的实现

在实现长连接之前我写了一个用来实现tcp短连接的tcp服务端与客户端,我们可以来看一下:

//基于多进程实现的服务端
#include "../public/_public.h"
using namespace idc;

ctcpserver tcpserver;  // 创建服务端对象。
clogfile logfile;            // 服务程序的运行日志。

void FathEXIT(int sig);  // 父进程退出函数。
void ChldEXIT(int sig);  // 子进程退出函数。

string strsendbuffer;   // 发送报文的buffer。
string strrecvbuffer;    // 接收报文的buffer。
int total=1000; //设置的初始余额

bool bizmain();    // 业务处理主函数。

int main(int argc,char *argv[])
{
    if (argc!=3)
    {
      printf("Using:./demo04 port logfile\n");
      printf("Example:./demo04 5005 /log/idc/demo04.log\n\n"); 
      return -1;
    }

    // 关闭全部的信号和输入输出。
    // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
    // 但请不要用 "kill -9 +进程号" 强行终止
    //closeioandsignal(false); 
    signal(SIGINT,FathEXIT); signal(SIGTERM,FathEXIT);

    if (logfile.open(argv[2])==false) { printf("logfile.open(%s) failed.\n",argv[2]); return -1; }

    // 服务端初始化。
    if (tcpserver.Initserver(atoi(argv[1]))==false)
    {
      logfile.write("tcpserver.initserver(%s) failed.\n",argv[1]); return -1;
    }

    while (true)
    {
        // 获取客户端的连接请求。
        if (tcpserver.Accept()==false)
        {
            logfile.write("tcpserver.accept() failed.\n"); FathEXIT(-1);
        }

        logfile.write("客户端(%s)已连接。\n",tcpserver.getclientip());

        if (fork()>0) { tcpserver.Closeconn(); continue; }  // 父进程继续回到accept()。
   
        // 子进程重新设置退出信号。
        signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT);

        tcpserver.Closelisten();     // 子进程关闭监听的socket。

        while (true)
        {
            // 子进程与客户端进行通讯,处理业务。
            if (tcpserver.Read(strrecvbuffer)==false)
            {
                printf("%d",tcpserver.m_connsock);
                perror("tcpserver.read()");
                logfile.write("tcpserver.read() failed.\n"); ChldEXIT(0);
            }
            logfile.write("接收:%s\n",strrecvbuffer.c_str());

            bizmain();    // 业务处理主函数。

            if (tcpserver.Write(strsendbuffer)==false)
            {
                logfile.write("tcpserver.send() failed.\n"); ChldEXIT(0);
            }
            logfile.write("发送:%s\n",strsendbuffer.c_str());
        }

        ChldEXIT(0);
    }
}

// 父进程退出函数。
void FathEXIT(int sig)  
{
    // 以下代码是为了防止信号处理函数在执行的过程中被信号中断。
    signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);

    logfile.write("父进程退出,sig=%d。\n",sig);

    tcpserver.Closelisten();    // 关闭监听的socket。

    kill(0,15);     // 通知全部的子进程退出。

    exit(0);
}

// 子进程退出函数。
void ChldEXIT(int sig)  
{
    // 以下代码是为了防止信号处理函数在执行的过程中被信号中断。
    signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);

    logfile.write("子进程退出,sig=%d。\n",sig);

    tcpserver.Closeconn();    // 关闭客户端的socket。

    exit(0);
}

void biz001();   // 登录。
void biz002();   // 查询余额。
void biz003();   // 转帐。

bool bizmain()    // 业务处理主函数。
{
    int bizid;  // 业务代码。
    getxmlbuffer(strrecvbuffer,"bizid",bizid);

    switch(bizid)
    {
        case 1:    // 登录。
            biz001();
            break;
        case 2:    // 查询余额。
            biz002();
            break;
        case 3:    // 转帐。
            biz003();
            break;
        default:   // 非法报文。
            strsendbuffer="<retcode>9</retcode><message>业务不存在。</message>";
            break;
    }

    return true;
}

void biz001()   // 登录。
{
    string username,password;
    getxmlbuffer(strrecvbuffer,"username",username);
    getxmlbuffer(strrecvbuffer,"password",password);
    logfile.write("用户名:%s,密码:%s。\n",username.c_str(),password.c_str());

    if ( (username=="test") && (password=="123456") )
        strsendbuffer="<retcode>0</retcode><message>成功。</message>";
    else
        strsendbuffer="<retcode>-1</retcode><message>用户名或密码不正确。</message>";
}

void biz002()   // 查询余额。
{
    strsendbuffer=sformat("<retcode>0</retcode><message>成功</message><query>%d</query>",total);
}

void biz003()   // 转帐。
{
    int query;
    getxmlbuffer(strrecvbuffer,"query",query);
    logfile.write("转帐金额:%d。\n",query);
    total+=query;
    strsendbuffer="<retcode>0</retcode><message>成功。</message>";
}
//客户端
    #include "../public/_public.h"

using namespace idc;

ctcpclient tcpclient;

string strsendmessage;  //发送数据的报文
string strrecvmessage;  //接收数据的报文

void biz001(); //登录
void biz002(); //查询余额
void biz003(); //转账

int main(int argc, char *argv[])
{
    if(argc!=3)
    {
        printf("using: ./server ip port\n");
        return -1;
    }
    if(tcpclient.Connect(atoi(argv[2]),argv[1]) == false)
    {
        printf("Connect failed.\n");
    }
    biz001(); //登录
    biz002(); //查询余额
    biz003(); //转账
    biz002(); //查询余额
    return 0;
}

void biz001()
{
    strsendmessage="<bizid>1</bizid><username>test</username><password>123456</password>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    printf("Send message:\n%s\n",strsendmessage.c_str());
    if(tcpclient.Read(strrecvmessage)== false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
    if(strrecvmessage.find("<error>0</error>") != -1)
    {
        printf("Login failed.\n");
        return;
    }
    printf("Login success.\n");
}

void biz002()
{
    strsendmessage="<bizid>2</bizid>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    if(tcpclient.Read(strrecvmessage)== false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
    if(strrecvmessage.find("<retcode>-1</retcode>") == 1)
    {
        printf("Query failed.\n");
        return;
    }
    int query;
    getxmlbuffer(strrecvmessage,"query",query);
    printf("Query success,query=%d\n",query);
}

void biz003()
{
    strsendmessage="<bizid>3</bizid><query>100</query>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    if(tcpclient.Read(strrecvmessage)== false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
    if(strrecvmessage.find("<retcode>-1</retcode>") == 1)
    {
        printf("Query failed.\n");
        return;
    }
    printf("Query success.\n");
}

有关于tcpclienttcpserver的封装可以参考我之前的文章:
c++实战篇(三) ——对socket通讯服务端与客户端的封装

现在如果我们想实现tcp的长连接就需要实现发送心跳报文,心跳报文的实现流程如下:

  • 我们设置心跳的超时时间
  • 添加发送报文的函数以及对对应报文的接收与处理函数

具体的代码实现如下:

//客户端
    #include "../public/_public.h"

using namespace idc;

ctcpclient tcpclient;

string strsendmessage;  //发送数据的报文
string strrecvmessage;  //接收数据的报文
int timeout;

void biz000(); //发送心跳报文
void biz001(); //登录
void biz002(); //查询余额
void biz003(); //转账

int main(int argc, char *argv[])
{
    if(argc!=4)
    {
        printf("using: ./server ip port timeout\n");
        return -1;
    }
    timeout = atoi(argv[3]);
    if(tcpclient.Connect(atoi(argv[2]),argv[1]) == false)
    {
        printf("Connect failed.\n");
    }
    biz001(); //登录
    biz002(); //查询余额
    sleep(10);
    biz000(); //发送心跳报文
    biz003(); //转账
    biz002(); //查询余额
    return 0;
}

void biz000()
{
    strsendmessage="<bizid>0</bizid>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    printf("Send message:\n%s\n",strsendmessage.c_str());
    if(tcpclient.Read(&strrecvmessage,timeout) == false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
}

void biz001()
{
    strsendmessage="<bizid>1</bizid><username>test</username><password>123456</password>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    printf("Send message:\n%s\n",strsendmessage.c_str());
    if(tcpclient.Read(strrecvmessage,timeout) == false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
    if(strrecvmessage.find("<error>0</error>") != -1)
    {
        printf("Login failed.\n");
        return;
    }
    printf("Login success.\n");
}

void biz002()
{
    strsendmessage="<bizid>2</bizid>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    if(tcpclient.Read(strrecvmessage,timeout) == false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
    if(strrecvmessage.find("<retcode>-1</retcode>") == 1)
    {
        printf("Query failed.\n");
        return;
    }
    int query;
    getxmlbuffer(strrecvmessage,"query",query);
    printf("Query success,query=%d\n",query);
}

void biz003()
{
    strsendmessage="<bizid>3</bizid><query>100</query>";
    if(tcpclient.Write(strsendmessage) == false)
    {
        printf("Write failed.\n");
        return;
    }
    if(tcpclient.Read(strrecvmessage,timeout) == false)
    {
        printf("Read failed.\n");
        return;
    }
    printf("Recv message:\n%s\n",strrecvmessage.c_str());
    if(strrecvmessage.find("<retcode>-1</retcode>") == 1)
    {
        printf("Query failed.\n");
        return;
    }
    printf("Query success.\n");
}
//服务端
#include "../public/_public.h"
using namespace idc;

ctcpserver tcpserver;  // 创建服务端对象。
clogfile logfile;            // 服务程序的运行日志。

void FathEXIT(int sig);  // 父进程退出函数。
void ChldEXIT(int sig);  // 子进程退出函数。

string strsendbuffer;   // 发送报文的buffer。
string strrecvbuffer;    // 接收报文的buffer。
int total=1000; //设置的初始余额
int timeout;

bool bizmain();    // 业务处理主函数。

int main(int argc,char *argv[])
{
    if (argc!=4)
    {
      printf("Using:./demo04 port logfile timeout\n");
      printf("Example:./demo04 5005 /log/idc/demo04.log 30\n\n"); 
      return -1;
    }

    timeout=atoi(argv[3]);

    // 关闭全部的信号和输入输出。
    // 设置信号,在shell状态下可用 "kill + 进程号" 正常终止些进程
    // 但请不要用 "kill -9 +进程号" 强行终止
    //closeioandsignal(false); 
    signal(SIGINT,FathEXIT); signal(SIGTERM,FathEXIT);

    if (logfile.open(argv[2])==false) { printf("logfile.open(%s) failed.\n",argv[2]); return -1; }

    // 服务端初始化。
    if (tcpserver.Initserver(atoi(argv[1]))==false)
    {
      logfile.write("tcpserver.initserver(%s) failed.\n",argv[1]); return -1;
    }

    while (true)
    {
        // 获取客户端的连接请求。
        if (tcpserver.Accept()==false)
        {
            logfile.write("tcpserver.accept() failed.\n"); FathEXIT(-1);
        }

        logfile.write("客户端(%s)已连接。\n",tcpserver.getclientip());

        if (fork()>0) { tcpserver.Closeconn(); continue; }  // 父进程继续回到accept()。
   
        // 子进程重新设置退出信号。
        signal(SIGINT,ChldEXIT); signal(SIGTERM,ChldEXIT);

        tcpserver.Closelisten();     // 子进程关闭监听的socket。

        while (true)
        {
            // 子进程与客户端进行通讯,处理业务。
            if (tcpserver.Read(strrecvbuffer,timeout)==false)
            {
                printf("%d",tcpserver.m_connsock);
                perror("tcpserver.read()");
                logfile.write("tcpserver.read() failed.\n"); ChldEXIT(0);
            }
            logfile.write("接收:%s\n",strrecvbuffer.c_str());

            bizmain();    // 业务处理主函数。

            if (tcpserver.Write(strsendbuffer)==false)
            {
                logfile.write("tcpserver.send() failed.\n"); ChldEXIT(0);
            }
            logfile.write("发送:%s\n",strsendbuffer.c_str());
        }

        ChldEXIT(0);
    }
}

// 父进程退出函数。
void FathEXIT(int sig)  
{
    // 以下代码是为了防止信号处理函数在执行的过程中被信号中断。
    signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);

    logfile.write("父进程退出,sig=%d。\n",sig);

    tcpserver.Closelisten();    // 关闭监听的socket。

    kill(0,15);     // 通知全部的子进程退出。

    exit(0);
}

// 子进程退出函数。
void ChldEXIT(int sig)  
{
    // 以下代码是为了防止信号处理函数在执行的过程中被信号中断。
    signal(SIGINT,SIG_IGN); signal(SIGTERM,SIG_IGN);

    logfile.write("子进程退出,sig=%d。\n",sig);

    tcpserver.Closeconn();    // 关闭客户端的socket。

    exit(0);
}

void biz001();   // 登录。
void biz002();   // 查询余额。
void biz003();   // 转帐。

bool bizmain()    // 业务处理主函数。
{
    int bizid;  // 业务代码。
    getxmlbuffer(strrecvbuffer,"bizid",bizid);

    switch(bizid)
    {
        case 0:
            strsendbuffer="<retcode>0</retcode>";
            break;
        case 1:    // 登录。
            biz001();
            break;
        case 2:    // 查询余额。
            biz002();
            break;
        case 3:    // 转帐。
            biz003();
            break;
        default:   // 非法报文。
            strsendbuffer="<retcode>9</retcode><message>业务不存在。</message>";
            break;
    }

    return true;
}

void biz001()   // 登录。
{
    string username,password;
    getxmlbuffer(strrecvbuffer,"username",username);
    getxmlbuffer(strrecvbuffer,"password",password);
    logfile.write("用户名:%s,密码:%s。\n",username.c_str(),password.c_str());

    if ( (username=="test") && (password=="123456") )
        strsendbuffer="<retcode>0</retcode><message>成功。</message>";
    else
        strsendbuffer="<retcode>-1</retcode><message>用户名或密码不正确。</message>";
}

void biz002()   // 查询余额。
{
    strsendbuffer=sformat("<retcode>0</retcode><message>成功</message><query>%d</query>",total);
}

void biz003()   // 转帐。
{
    int query;
    getxmlbuffer(strrecvbuffer,"query",query);
    logfile.write("转帐金额:%d。\n",query);
    total+=query;
    strsendbuffer="<retcode>0</retcode><message>成功。</message>";
}

这样我们就实现一个简单的可以依托于心跳报文来实现tcp长连接的客户端与服务端了,我们这里只要一直能够超时时间前发送心跳报文给服务端,服务端就课题一致运行下去。

tcp长连接的优点与应用场景

1.减少连接建立和断开的开销
长连接减少了频繁建立和断开连接带来的额外开销,如三次握手和四次挥手过程的时间消耗和资源消耗。
2.提高通信效率
由于连接已经建立好,因此后续的数据传输可以更快地开始,从而提高了数据传输的效率。
3.支持连续的数据流
长连接非常适合需要连续发送数据的应用场景,如视频流媒体传输、实时聊天应用等。

;