c++实战篇(三) ——对socket通讯服务端与客户端的封装

前言

在前面几篇文章中我们介绍过一些有关于网络编程的相关知识,我们介绍了在tcp传输数据的时候发送缓冲区与接收缓冲区,我们介绍了在tcp发送与接收的过程中可能出现的分包与粘包的问题:
c++理论篇(一) ——浅谈tcp缓存与tcp的分包与粘包
我们介绍了在网络编程如何利用IO多路复用来实现服务端对大量客户端进行通讯:
c++高级篇(二) ——Linux下IO多路复用之select模型
c++高级篇(三) ——Linux下IO多路复用之poll模型
但是说了那么多我们好像还是不知道客户端和服务端之间的连接究竟是一个怎样的过程,而这就是我们今天的主题,通过对Tcp通讯中客户端与服务端的连接来探究一些网络编程的细节。

客户端类的编写

客户端通讯的过程

客户端连接的过程其实很好理解,主要就是以下几步:

  • 创建客户端socket
  • 基于客户端socket和服务端ip以及服务端开放的通讯端口与服务端建立连接
  • 读取/发送数据
  • 关闭socket,断开连接
    而我们的对客户端类的编写,也是基于上面的几步过程来展开的。

客户端的私有成员

在上面我们提到了客户端连接服务端所需的一些信息,例如客户端socket,服务端ip以及服务端开放的通讯端口(云服务器开放通讯端口需要设置安全组),所以我们可以这样定义客户端类:

private:int m_socket; // 客户端的socketunsigned int server_port;// 服务端的端口string server_ip; //服务端的ip

客户端的公共函数

我们上面说过客户端连接服务端以及相关工作的大致流程,所以在定义客户端类的函数时,大概是以下类型的函数:

public:ctcpclient(){m_socket=-1;}bool Connect(const unsigned int port,const string& ip);  //客户端连接服务端bool Read(string& buff,const int itimeout=0);  //接收文本数据bool Read(void* buff,const int bufflen,const int itimeout=0); //接收二进制数据bool Write(const string& buff); //发送文本数据bool Write(const void* buff,const int bufflen); //发送二进制数据void Close(); //关闭连接~ctcpclient(){Close();}

这里的构造函数与析构函数无需多言,接下来我们主要对相关的工作函数进行探究。

Connect(客户端连接)函数

在讲解Connect函数之前我们先来看一下它的具体执行的逻辑:

  bool ctcpclient::Connect(const unsigned int port, const string &ip){if (m_socket != -1){Close();}// 忽略SIGPIPE信号,防止程序异常退出。// 如果send到一个disconnected socket上,内核就会发出SIGPIPE信号。这个信号// 的缺省处理方法是终止进程,大多数时候这都不是我们期望的。我们重新定义这// 个信号的处理方法,大多数情况是直接屏蔽它。signal(SIGPIPE, SIG_IGN);server_port = port;server_ip = ip;m_socket = socket(AF_INET, SOCK_STREAM, 0);if (m_socket < 0){return false;}struct sockaddr_in server_addr;struct hostent *h;memset(&server_addr, 0, sizeof(server_addr));if ((h = gethostbyname(ip.c_str())) == NULL){Close();return false;}server_addr.sin_family = AF_INET;//指定通讯协议server_addr.sin_port = htons(server_port); //指定通讯端口memset(h, 0, sizeof(h));memcpy(h->h_addr, &server_ip[0], server_ip.length());  //指定通讯IP地址if (connect(m_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0){return false;}return true;}

上述主要经过了一下几个步骤:

  • 检查socket,查看当前客户端是否处于未连接状态
  • 设置相关信号的处理方式,防止异常情况的出现
  • 初始化客户端socket
  • 定义server_addr struct hostent *h结构体配置相关信息
  • 与客户端建立连接

Read(接收)函数

Read函数在这里所起到的作用主要是接收数据的作用,接下来我们将从接收数据的不同作为开始来探究其中的细节。

接收文本数据

在对相关函数的执行逻辑与细节进行讲解之前,我们先来看一下相关的函数签名与函数实现:

 bool Read(string& buff,const int itimeout=0);  //接收文本数据bool tcpread(const int sockfd,string &buffer,const int itimeout=0); // 读取文本数据bool readn(const int sockfd, char *buffer, const size_t n);
  bool ctcpclient::Read(string &buff, const int itimeout){if (m_socket < 0){return false;}return (tcpread(m_socket, buff, itimeout));}bool tcpread(const int sock, string &buff, const int itimeout){if (sock < 0){return false;}if (itimeout > 0){struct pollfd fds;fds.fd = sock;fds.events = POLLIN;int ret = poll(&fds, 1, itimeout * 1000);if (ret < 0){return false;}if (ret == 0){return false;}}if (itimeout < -1){struct pollfd fds;fds.fd = sock;fds.events = POLLIN;int ret = poll(&fds, 1, 0);if (ret < 0){return false;}if (ret == 0){return false;}}int bufflen = 0;if (readn(sock, (char *)&bufflen, 4) == false) // 读取报文长度{return false;}buff.resize(bufflen);if (readn(sock, &buff[0], bufflen) == false) // 读取报文内容{return false;}return true;}bool readn(const int sockfd, char *buffer, const size_t n){int nleft = n; // 剩余需要读取的字节数。int idx = 0;   // 已成功读取的字节数。int nread;     // 每次调用recv()函数读到的字节数。while (nleft > 0){if ((nread = recv(sockfd, buffer + idx, nleft, 0)) <= 0)return false;idx = idx + nread;nleft = nleft - nread;}return true;}

我们可以看到上面有关于数据接收的函数一共有三个,这里主要是客户端与服务端接收/发送数据的方式基本一致,所以我们选择对相关函数进行封装避免多次书写重复函数使代码编的臃肿,下面来给大家解释主要函数的作用:

  • tcpread
    我们知道端到端的通讯其实不是每次都是立即进行的,所以接收数据的一方有时候要等待发送数据的一方将数据发送过来,而这里我们基于poll实现了一个超时机制,让我们可以手动设置接收数据方是否等待以及等待的最大时长
  • readn
    这个主要是实现对数据的读写,相对于直接调用recv函数,每次从socket读取指定数量的字节,即使recv函数不能一次读取所有字节。通过在循环中跟踪剩余需要读取的字节数,可以确保读取完整的数据,进而避免因为recv函数每次读取的字节数不固定而导致的数据读取不完整或错误。

二进制数据

二进制数数接收与文本数据的接收又有所不同,我们来看一下它的函数签名与具体逻辑:

  • 函数签名
bool Read(void* buff,const int bufflen,const int itimeout=0); //接收二进制数据
bool tcpread(const int sockfd, void *buffer, const int ibuflen, const int itimeout = 0);//接收二进制数据
bool readn(const int sockfd, char *buffer, const size_t n);
  • 函数逻辑
    bool ctcpclient::Read(void *buff, const int bufflen, const int itimeout){if (m_socket < 0){return false;}return (tcpread(m_socket, buff, bufflen, itimeout));}bool tcpread(int sock, void *buff, const int bufflen, const int itimeout){if (sock < 0){return false;}if (itimeout > 0){struct pollfd fds;fds.fd = sock;fds.events = POLLIN;int ret = poll(&fds, 1, itimeout * 1000);if (ret <= 0){return false;}}if (itimeout < -1){struct pollfd fds;fds.fd = sock;fds.events = POLLIN;int ret = poll(&fds, 1, 0);if (ret <= 0){return false;}}if (readn(sock, (char *)buff, bufflen) == false) // 读取报文内容{return false;}return true;}bool readn(const int sockfd, char *buffer, const size_t n){int nleft = n; // 剩余需要读取的字节数。int idx = 0;   // 已成功读取的字节数。int nread;     // 每次调用recv()函数读到的字节数。while (nleft > 0){if ((nread = recv(sockfd, buffer + idx, nleft, 0)) <= 0)return false;idx = idx + nread;nleft = nleft - nread;}return true;}

我们可以发现二进制数据的接收与文本数据相比有所不同,相对于文本数据,二进制数据减少了一个接收数据长度的过程,这是因为我们在接收/二进制数据时,二进制数据通常会包含自身的大小信息。在通信双方约定好数据格式之后,发送方会在发送数据时先将数据的大小信息编码到数据中,接收方在接收数据时可以直接根据数据的大小信息来确定整个报文的大小,从而正确地解析和处理数据。

Write函数

write函数的细节与read函数类似,这里不做赘述,直接看函数签名与逻辑了:

  • 函数签名
 bool Write(const string& buff); //发送文本数据bool Write(const void* buff,const int bufflen); //发送二进制数据bool tcpwrite(const int sockfd, const void *buffer, const int ibuflen);//发送二进制数据bool tcpwrite(const int sockfd, const string &buffer);  //发送文本数据bool readn(const int sockfd, char *buffer, const size_t n);
  • 函数逻辑
 bool ctcpclient::Write(const string &buff){if (m_socket < 0){return false;}return (tcpwrite(m_socket, buff));}bool ctcpclient::Write(const void *buff, const int bufflen){if (m_socket < 0){return false;}return (tcpwrite(m_socket, (char *)buff, bufflen));}bool tcpwrite(const int sock, const string &buff){if (sock < 0){return false;}int bufflen = buff.length();if (writen(sock, (char *)&bufflen, 4) == false) // 发送报文长度{return false;}if (writen(sock, &buff[0], bufflen) == false) // 发送报文内容{return false;}return true;}bool tcpwrite(int sock, const void *buff, const int bufflen){if (sock < 0){return false;}if (writen(sock, (char *)buff, bufflen) == false) // 发送报文内容{return false;}return true;}bool writen(const int sockfd, const char *buffer, const size_t n){int nleft = n; // 剩余需要写入的字节数。int idx = 0;   // 已成功写入的字节数。int nwritten;  // 每次调用send()函数写入的字节数。while (nleft > 0){if ((nwritten = send(sockfd, buffer + idx, nleft, 0)) <= 0)return false;nleft = nleft - nwritten;idx = idx + nwritten;}return true;}

Close函数

Close函数主要用来关闭已经打开的socket

void ctcpclient::Close()
{if (m_socket > 0){close(m_socket);m_socket = -1;}
}

服务端类的编写

服务端类的工作流程

  • 初始化监听socket,指定端口与ip,将socket设置为监听状态
  • 从等待连接的队列中选取一个客户端进行连接
  • 发送/接收数据
  • 关闭socket,断开连接

服务端类的成员

class ctcpserver{private:int m_listensock;//服务端的监听socketint m_connsock; //已连接的客户端socketint sockaddr_len;//客户端地址的长度struct sockaddr_in server_addr;//服务端地址struct sockaddr_in client_addr;//客户端地址public:ctcpserver(){m_listensock=-1;m_connsock=-1;}bool Initserver(const unsigned int port,const int backlog=5);//初始化服务端bool Accept(); //从已连接队列中获取一个客户端连接bool Read(string& buff,const int itimeout=0); //接收文本数据bool Read(void* buff,const int bufflen,const int itimeout=0); //接收二进制数据bool Write(const string& buff); //发送文本数据bool Write(const void* buff,const int bufflen); //发送二进制数据char* getclientip(); //获取客户端的ipvoid Closelisten(); //关闭监听socketvoid Closeconn(); //关闭已连接的客户端socket~ctcpserver(){Closeconn();Closelisten();}};

Initserver函数

在讲解前我们来看一下函数的具体逻辑:

bool ctcpserver::Initserver(const unsigned int port, const int backlog) // backlog:等待连接队列的最大长度{if (m_listensock != -1){Closelisten();}signal(SIGPIPE, SIG_IGN);// 打开SO_REUSEADDR选项,当服务端连接处于TIME_WAIT状态时可以再次启动服务器,// 否则bind()可能会不成功,报:Address already in use。int opt = 1;setsockopt(m_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));m_listensock = socket(AF_INET, SOCK_STREAM, 0);if (m_listensock < 0){return false;}memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;m_listensock = socket(AF_INET, SOCK_STREAM, 0);if (m_listensock < 0){return false;}struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(port);if (bind(m_listensock, (struct sockaddr *)&server_addr, sizeof(server_addr)) != 0){Closelisten();return false;}if (listen(m_listensock, backlog) != 0){Closelisten();return false;}return true;}

我们梳理一下它这个的工作流程;

  • 检查服务端是否已经被初始化
  • 设置相关信号的处理方式,防止异常情况的出现
  • 初始化服务端的监听socket
  • 设置相关参数,并指定其为用于通信的ip与端口(bind)
  • 将socket设置为监听状态

Accept函数

一个时间段可能会有多个客户端连接服务端,这时候就形成了一个等待队列,服务单会在这个等待队列里面利用accept函数选取一个客户端进行连接:

  bool ctcpserver::Accept(){if (m_listensock < 0){return false;}sockaddr_len = sizeof(struct sockaddr_in);if ((m_connsock = accept(m_listensock, (sockaddr *)&client_addr, (socklen_t *)&sockaddr_len)) < 0){return false;}return true;}

Read与Write函数

服务端接收与发送数据与客户端基本功一致,这里就不做赘述,基本的这一点什么都已经提出来了,我们直接看代码:

bool ctcpserver::Read(string &buff, const int itimeout){if (m_listensock < 0){return false;}return (tcpread(m_connsock, buff, itimeout));}bool ctcpserver::Read(void *buff, const int bufflen, const int itimeout){if (m_listensock < 0){return false;}return (tcpread(m_connsock, buff, bufflen, itimeout));}bool ctcpserver::Write(const string &buff){if (m_listensock < 0){return false;}return (tcpwrite(m_connsock, buff));}bool ctcpserver::Write(const void *buff, const int bufflen){if (m_listensock < 0){return false;}return (tcpwrite(m_connsock, (char *)buff, bufflen));}

getclientip()函数

该函数主要的作用是获取连接的客户端的ip:

  char *ctcpserver::getclientip(){return inet_ntoa(client_addr.sin_addr);}

Close函数

 void ctcpserver::Closelisten(){if (m_listensock > 0){close(m_listensock);m_listensock = -1;}}void ctcpserver::Closeconn(){if (m_connsock > 0){close(m_connsock);m_connsock = -1;}}

结语

Cpp不同于其他语言,像Go,Python等语言对上述的细节其实已经封装好了,但是cpp则是需要我们去一点点的实现,为了避免重复的书写代码,我们可以将它们封装成类来供我们去使用,以上就是这篇文章的全部内容了,大家下篇见!

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

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

相关文章

MongoDB与Elasticsearch特性及知识点对比

仅作为技术选型和面试参考。对比记忆更佳。 目录 1.数据模型 2.索引机制 3. 查询性能 4.分布式架构 5.数据一致性 6.文档大小限制 7.存储引擎 8.数据压缩 10.实时搜索 11.安全性 12.版本控制 13 事务支持 14.地理空间搜索 15.多租户支持 16 运维复杂度 17,监…

直播素材安卓情侣飞行棋v2.22 仿dofm 支持自定义模式—可用直播素材

一个情侣间增进友谊的小游戏非常好玩&#xff0c;适合男孩女孩之间增进感情&#xff01;快和你暗恋的女孩一块玩吧&#xff0c;极速升温 永久免费&#xff01;解锁激活码内容全部畅玩&#xff01;全网最强超级给力&#xff01;真人说书音频 网盘自动获取 链接&#xff1a;http…

重要综述!全文翻译!宫鹏教授、陈镜明教授、梁顺林教授等《Nature Climate Change》!

2013年&#xff0c;由宫鹏教授、陈镜明教授和梁顺林教授等联合发表了一篇《Nature Climate Change》综述文章&#xff0c;其主题是卫星遥感在全球变化中的作用研究。&#xff08;已被引510次&#xff0c;来源谷歌学术&#xff09;。 卫星遥感方式对于气象问题、大气、陆地和海洋…

【机器学习】集成方法---Boosting之AdaBoost

一、Boosting的介绍 1.1 集成学习的概念 1.1.1集成学习的定义 集成学习是一种通过组合多个学习器来完成学习任务的机器学习方法。它通过将多个单一模型&#xff08;也称为“基学习器”或“弱学习器”&#xff09;的输出结果进行集成&#xff0c;以获得比单一模型更好的泛化性…

【中断】【ARM64】学习总结

optee中的异常向量表解读–中断处理解读 https://mp.weixin.qq.com/s/gBsy4YDYTHGRsy2zcVr6Vg

操作系统:磁盘交换空间

什么是磁盘交换空间? 磁盘交换空间(swap space)是在磁盘上预留出来的一块区域&#xff0c;用作补充系统物理内存&#xff08;RAM&#xff09;的一种方式。当系统的物理内存不足以存储当前所有活动进程所需的数据时&#xff0c;操作系统会将一部分暂时不用或使用较少的内存数据…

windows ubuntu sed,awk,grep篇:13.其他 awk 命令

目录 85. 使用 printf 格式化输出 86. awk 内置数值函数 87. 随机数生成器 88. 常用字符串函数 89. GAWK/NAWK 的字符串函数 90. GAWK 字符串函数 91.处理参数(ARGC,ARGV,ARGIND) 92. OFMT 93. GAWK 内置的环境变量 94. pgawk – awk 运行分析器 95. 位操作 96.用户…

Linux搭建sqlilabs靶场

提前准备&#xff1a; 文章中所使用到的Linux系统&#xff1a;Ubantu20.4sqlilabs靶场下载地址&#xff1a;GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. 一. 安装phpstudy phpstudy安装命令&#xff1a;wget -O install.sh h…

python 的继承、封装和多态

1. 继承&#xff08;Inheritance&#xff09; 继承是面向对象编程中的一个重要概念&#xff0c;它允许一个类&#xff08;子类&#xff09;继承另一个类&#xff08;父类&#xff09;的属性和方法。子类可以重用父类的代码&#xff0c;同时也可以扩展或修改父类的行为。 常用…

托普利兹矩阵(T矩阵)及其应用(Matlab demo测试)

托普利兹矩阵&#xff08;T矩阵&#xff09;及其应用&#xff08;Matlab demo测试&#xff09; 1. 概念2. Matlab简单测试2.1 生成测试2.2 基本性质及原理2.3 性质验证 3. 其他应用总结3.1 其他性质3.2 文献阅读看到的 参考资料 1. 概念 托普利兹矩阵&#xff0c;简称为T型矩阵…

H3C MSTP 实验

H3C MSTP 实验 实验拓扑 ​​ 实验需求 所有交换机上创建 Vlan10&#xff0c;Vlan20&#xff0c;Vlan30 和 Vlan40所有交换机之间的端口配置为 Trunk&#xff0c;并放行相关 VLAN按照图示分区域配置 MSTP&#xff0c;并配置主备根网桥 实验步骤 VLAN基础配置&#xff08;…

力扣面试150 简化路径 栈 模拟

Problem: 71. 简化路径 思路 &#x1f469;‍&#x1f3eb; 三叶题解 复杂度 时间复杂度: O ( n ) O(n) O(n) 空间复杂度: O ( n ) O(n) O(n) Code class Solution {public String simplifyPath(String path){ArrayDeque<String> d new ArrayDeque<>();…

2022 亚马逊云科技中国峰会,对话开发者论坛

目录 前言 最近整理资料发现还有一些前 2 年的内容没发出来&#xff0c;故补发记录&#xff0c;每年都有新的感悟。 开发者论坛 1. 你认为什么是开发者社区&#xff0c;如何定义一个成功的开发者社区&#xff1f; 我认为可以把开发者社区看成一个 “产品” 来对待&#xff…

【RAG 论文】GenRead:“generate-read“ 可能比 “retrieve-read“ 更有效

论文&#xff1a;Generate rather than Retrieve: Large Language Models are Strong Context Generators ⭐⭐⭐⭐ ICLR 2023 Code: github.com/wyu97/GenRead 一、论文速读 该工作发现&#xff1a;由 LLM 生成的文档中&#xff0c;往往比 retrieved documents 更可能包含正确…

C++校招八股

c类的访问权限与继承方式 公有成员在任何地方都可以被访问&#xff0c;包括类的外部和派生类。受保护成员在类的内部和派生类中可以被访问&#xff0c;但在类的外部不可访问。 私有成员只能在类的内部访问&#xff0c;包括类的成员函数和友元函数&#xff0c;不允许在类的外部…

一步一步写线程之十一线程池应用内存池

一、内存池 内存池&#xff0c;非常好理解&#xff0c;就是存储内存的一个池子&#xff08;Pool&#xff09;&#xff0c;一般来说&#xff0c;都是使用各种容器或者自己实现的类似容器的内存管理类。内存池其实就是为了解决两个主要问题&#xff0c;一个是内存反复分配回收的…

关于“泼辣”DB 你应该知道的几件事

PolarDB PolarDB for PostgreSQL&#xff08;以下简称 PolarDB&#xff09;是一款阿里云自主研发的企业级数据库产品&#xff0c;采用计算存储分离架构&#xff0c;100% 兼容 PostgreSQL。 PolarDB 的存储与计算能力均可横向扩展&#xff0c;具有高可靠、高可用、弹性扩展等企…

文件(夹)批量重命名数字、字母、日期、中文数字大写小写

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 目标是重命名下面5个文件&#xff08;也可以是文件夹等&#xff0c;任意&#xff09;&#xff0c;从大写中文数字“贰”开始 打开工具&#xff0c;找到“文…

使用机器学习确定文本的编程语言

导入必要的库 norman Python 语句&#xff1a;import <span style"color:#000000"><span style"background-color:#fbedbb"><span style"color:#0000ff">import</span> pandas <span style"color:#0000ff&quo…

Java面试题:解释Java内存模型(JMM)是什么,它为何重要?

Java内存模型&#xff08;Java Memory Model, JMM&#xff09; 定义&#xff1a; Java内存模型是一个抽象的概念&#xff0c;它定义了Java程序中各种变量&#xff08;线程共享变量&#xff09;的访问规则&#xff0c;以及在并发环境下&#xff0c;这些变量的读写操作如何与内存…