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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据-62 Kafka 高级特性 主题 kafka-topics相关操作参数 KafkaAdminClient 偏移量管理

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

类加载机制

概述 所谓机制就是某种流程规范或运作模式。简单来说&#xff0c;将类文件加载到JVM中的过程&#xff0c;需要对这个过程进行限定和约束&#xff0c;这就是Java类加载的机制。 具体说来&#xff0c;对Java类加载机制的描述可以从三个方面&#xff1a; 按需加载 需要某一个类…

Web开发-html篇-上

HTML发展史 HTML的历史可以追溯到20世纪90年代初。当时&#xff0c;互联网尚处于起步阶段&#xff0c;Web浏览器也刚刚问世。HTML的创建者是蒂姆伯纳斯-李&#xff08;Tim Berners-Lee&#xff09;&#xff0c;他在1991年首次提出了HTML的概念。HTML的初衷是为了方便不同计算机…

python常用库

目录 tqdm库介绍用法 argparse库介绍用法 tqdm库 介绍 封装一个可视化&#xff0c;可拓展的进度条&#xff0c;以了解项目运行的时长&#xff0c;了解项目进展情况。 传入第 用法 安装 pip install tqdm1直接使用 for i in tqdm(range(1000)):time.sleep(0.01)等价 for i…

DNS处理模块 dnspython

DNS处理模块 dnspython 标题介绍安装dnspython 模块常用方法介绍实践&#xff1a;DNS域名轮询业务监控 标题介绍 Dnspython 是 Python 的 DNS 工具包。它可用于查询、区域传输、动态更新、名称服务器测试和许多其他事情。 dnspython 模块提供了大量的 DNS 处理方法&#xff0c…

django集成pytest进行自动化单元测试实战

文章目录 一、引入pytest相关的包二、配置pytest1、将django的配置区分测试环境、开发环境和生产环境2、配置pytest 三、编写测试用例1、业务测试2、接口测试 四、进行测试 在Django项目中集成Pytest进行单元测试可以提高测试的灵活性和效率&#xff0c;相比于Django自带的测试…

PyQt5入门

Python中经常使用的GUI控件集有PyQt、Tkinter、wxPython、Kivy、PyGUI和Libavg。其中PyQt是Qt(c语言实现的)为Python专门提供的扩展 PyQt是一套Python的GUI开发框架,即图形用户界面开发框架.。而在Python中则使用PyQt这一工具包&#xff08;PyQt5、PyQt5-tools、PyQt5-stubs&am…

卡码网--数组篇(二分法)

系列文章目录 文章目录 系列文章目录前言数组二分查找 前言 详情看&#xff1a;https://programmercarl.com/ 总结知识点用于复习 数组 概念: 数组是存放在连续内存空间上的相同类型数据的集合。 数组可以方便的通过下标索引的方式获取到下标对应的数据。 特点&#xff1a;…

安卓基本布局(下)

TableLayout 常用属性描述collapseColumns设置需要被隐藏的列的列号。shrinkColumns设置允许被伸缩的列的列号。stretchColumns设置允许被拉伸的列的列号。 <TableLayout xmlns:android"http://schemas.android.com/apk/res/android"android:id"id/TableL…

状体管理-装饰器

State 自己的状态 注意:不是状态变量的所有更改都会引起刷新。只有可以被框架观察到的修改才会引起UI刷新。 1、boolean、string、number类型时&#xff0c;可以观察到数值的变化。 2、class或者Object时&#xff0c;可以观察 自身的赋值 的变化&#xff0c;第一层属性赋值的变…

CC++:贪吃蛇小游戏教程

❀创作不易&#xff0c;关注作者不迷路❀&#x1f600;&#x1f600; 目录 &#x1f600;贪吃蛇简介 &#x1f603;贪吃蛇的实现 &#x1f40d;生成地图 &#x1f40d;生成蛇模块 ❀定义蛇的结构体 ❀初始化蛇的相关信息 ❀初始化食物的相关信息 &#x1f40d;光标定位和…

[Spring] SpringBoot统一功能处理与图书管理系统

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

USB 2.0 规范摘录

文章目录 1、USB 体系简介2、USB 数据流模型四种传输类型 3、USB 物理规范和电气规范4、USB 协议层规范事务传输&#xff08;Transaction&#xff09;的流程 5、USB 框架6、USB 主机&#xff1a;硬件和软件7、USB HUB 规范数据的转发唤醒信号的转发USB HUB 的帧同步HUB Repeate…

前端常见场景、JS计算精度丢失问题(Decimal.js 介绍)

目录 一. Decimal.js 介绍 二. 常用方法 1. 创建 Decimal 实例 2.加法 add 或 plus 3.减法 sub 或 minus 4.乘法 times 或 mul 5.除法 div 或 dividedBy 6.取模 7.幂运算 8.平方根 9.保留小数位 toFixed方法(四舍五入) 三.项目应用 前端精度丢失问题通常由以下原因…

【Kubernetes】kubeadmu快速部署k8s集群

目录 一.组件部署 二.环境初始化 三.所有节点部署docker&#xff0c;以及指定版本的kubeadm 四.所有节点安装kubeadm&#xff0c;kubelet和kubectl 五.高可用配置 六.部署K8S集群 1.master01 节点操作 2.master02、master03节点 3.master01 节点 4.master02、master…

C语言 ——— 学习、使用 strcmp函数 并模拟实现

目录 strcmp函数的功能 学习strcmp函数​编辑 使用strcmp函数 模拟实现strcmp函数 strcmp函数的功能 strcmp函数的功能是字符串比较&#xff0c;两个字符串的对应位置的字符进行比较&#xff0c;直到字符不同或达到终止的 \0 字符为止 举例说明&#xff1a; 字符串1&am…

leetcode-二叉树oj题1(共三道)--c语言

目录 a. 二叉树的概念以及实现参照博客&#xff1a; 一、三道题的oj链接 二、每题讲解 1.单值二叉树 a. 题目&#xff1a; b. 题目所给代码 c. 思路 d. 代码&#xff1a; 2. 相同的树 a. 题目 b. 题目所给代码 c. 思路 d. 代码 3. 二叉树的前序遍历 a. 题目 b.…

前端-05-VSCode自定义代码片段console.log(js/ts配置)、代码段快捷提示放在首位

目录 配置VSCode自定义代码片段console.log()log代码段快捷提示放在首位 配置VSCode自定义代码片段console.log() 点击VSCode左下角设置图标&#xff0c;点击用户代码片段 点击用户代码片段后&#xff0c;VSCode上方出现弹窗如下图&#xff08;没有显示这两个文件的话搜索一下…

Redis结合Lua脚本的简单使用

我们就拿购物车举例子 现在有5个东西免费送&#xff0c;我们只能选择1个 例如 可乐 美年达 香蕉 苹果 薯片 我们选择后就放进redis里面 然后我们不能选重复&#xff0c;只能选不同 Lua脚本 我们redis使用lua脚本的时候&#xff0c;会传两个参数进去 一个是List<Strin…

MySQL:数据库权限与角色

权限 MySQL 的权限管理系统是保障数据库安全性的关键组件之一。它允许数据库管理员精确控制哪些用户可以对哪些数据库对象执行哪些操作。 自主存取控制 DAC&#xff08;DiscretionaryAccess Control)&#xff1a;用户对于不同的数据库对象有不同的存取权限&#xff0c;不同的…