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

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>";elsestrsendbuffer="<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>";elsestrsendbuffer="<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.支持连续的数据流
长连接非常适合需要连续发送数据的应用场景,如视频流媒体传输、实时聊天应用等。

相关文章:

  • vulnhub靶机:Tomato
  • 【Spring】详细了解静态代理和动态代理的使用
  • Android读取拨号记录功能
  • 【九】Hadoop3.3.4HA高可用配置
  • Vue3 + js-echarts 实现前端大屏可视化
  • java计算机毕设课设—网上招聘系统(附源码、文章、相关截图、部署视频)
  • 扩展------零拷贝技术(Mmap,SendFile)
  • 统计语言模型——Ngram
  • SpringMVC 工作流程简述
  • 2024年华数杯数学建模竞赛——赛题浅析
  • FFmpeg实现文件夹多视频合并
  • 使用Python创建多功能文件管理器
  • AcWing食物链
  • Lua 脚本编程基础
  • 搭建nexus上传jar包,并结合jenkins运行项目
  • 《Java8实战》-第四章读书笔记(引入流Stream)
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • HTTP 简介
  • interface和setter,getter
  • java概述
  • js作用域和this的理解
  • 从setTimeout-setInterval看JS线程
  • 工程优化暨babel升级小记
  • 你真的知道 == 和 equals 的区别吗?
  • 微信小程序--------语音识别(前端自己也能玩)
  • 不要一棍子打翻所有黑盒模型,其实可以让它们发挥作用 ...
  • ​你们这样子,耽误我的工作进度怎么办?
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • (¥1011)-(一千零一拾一元整)输出
  • (HAL库版)freeRTOS移植STMF103
  • (续)使用Django搭建一个完整的项目(Centos7+Nginx)
  • (一)Java算法:二分查找
  • (一)utf8mb4_general_ci 和 utf8mb4_unicode_ci 适用排序和比较规则场景
  • (转)winform之ListView
  • (转)程序员技术练级攻略
  • .net framework4与其client profile版本的区别
  • .NET 表达式计算:Expression Evaluator
  • .NET 解决重复提交问题
  • .NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)
  • .net图片验证码生成、点击刷新及验证输入是否正确
  • /etc/X11/xorg.conf 文件被误改后进不了图形化界面
  • /proc/vmstat 详解
  • [2016.7 test.5] T1
  • [AIGC] Java List接口详解
  • [Asp.net MVC]Asp.net MVC5系列——Razor语法
  • [Assignment] C++1
  • [C#7] 1.Tuples(元组)
  • [C#学习笔记]LINQ
  • [Contiki系列论文之2]WSN的自适应通信架构
  • [Gamma]阶段测试报告
  • [Java][Android][Process] 暴力的服务能够解决一切,暴力的方式运行命令行语句
  • [javaSE] GUI(事件监听机制)
  • [LeetCode][LCR190]加密运算——全加器的实现
  • [linux]资料收纳