套接字通信类的封装

        在掌握了基于TCP的套接字通信流程之后,为了方便使用,提高编码效率,可以对通信操作进行封装,本着有浅入深的原则,先基于C语言进行面向过程的函数封装,然后再基于C++进行面向对象的类封装。

1. 基于C语言的封装

        基于TCP的套接字通信分为两部分:服务器端通信和客户端通信。我们只要掌握了通信流程,封装出对应的功能函数也就不在话下了,先来回顾一下通信流程:

服务器端

  • 创建用于监听的套接字
  • 将用于监听的套接字和本地的IP以及端口进行绑定
  • 启动监听
  • 等待并接受新的客户端连接,连接建立得到用于通信的套接字和客户端的IP、端口信息
  • 使用得到的通信的套接字和客户端通信(接收和发送数据)
  • 通信结束,关闭套接字(监听 + 通信)

客户端

  • 创建用于通信的套接字
  • 使用服务器端绑定的IP和端口连接服务器
  • 使用通信的套接字和服务器通信(发送和接收数据)
  • 通信结束,关闭套接字(通信)

1.1 函数声明

        通过通信流程可以看出服务器和客户端有些操作步骤是相同的,因此封装的功能函数是可以共用的,相关的通信函数声明如下:

 服务器 ///
int bindSocket(int lfd, unsigned short port);
int setListen(int lfd);
int acceptConn(int lfd, struct sockaddr_in *addr);客户端 ///
int connectToHost(int fd, const char* ip, unsigned short port);/ 共用 
int createSocket();
int sendMsg(int fd, const char* msg);
int recvMsg(int fd, char* msg, int size);
int closeSocket(int fd);
int readn(int fd, char* buf, int size);
int writen(int fd, const char* msg, int size);

1.2 函数定义

// 创建监套接字
int createSocket()
{int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd == -1){perror("socket");return -1;}printf("套接字创建成功, fd=%d\n", fd);return fd;
}// 绑定本地的IP和端口
int bindSocket(int lfd, unsigned short port)
{struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(port);saddr.sin_addr.s_addr = INADDR_ANY;  // 0 = 0.0.0.0int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));if(ret == -1){perror("bind");return -1;}printf("套接字绑定成功, ip: %s, port: %d\n",inet_ntoa(saddr.sin_addr), port);return ret;
}// 设置监听
int setListen(int lfd)
{int ret = listen(lfd, 128);if(ret == -1){perror("listen");return -1;}printf("设置监听成功...\n");return ret;
}// 阻塞并等待客户端的连接
int acceptConn(int lfd, struct sockaddr_in *addr)
{int cfd = -1;if(addr == NULL){cfd = accept(lfd, NULL, NULL);}else{int addrlen = sizeof(struct sockaddr_in);cfd = accept(lfd, (struct sockaddr*)addr, &addrlen);}if(cfd == -1){perror("accept");return -1;}       printf("成功和客户端建立连接...\n");return cfd; 
}// 接收数据
int recvMsg(int cfd, char** msg)
{if(msg == NULL || cfd <= 0){return -1;}// 接收数据// 1. 读数据头int len = 0;readn(cfd, (char*)&len, 4);len = ntohl(len);printf("数据块大小: %d\n", len);// 根据读出的长度分配内存char *buf = (char*)malloc(len+1);int ret = readn(cfd, buf, len);if(ret != len){return -1;}buf[len] = '\0';*msg = buf;return ret;
}// 发送数据
int sendMsg(int cfd, char* msg, int len)
{if(msg == NULL || len <= 0){return -1;}// 申请内存空间: 数据长度 + 包头4字节(存储数据长度)char* data = (char*)malloc(len+4);int bigLen = htonl(len);memcpy(data, &bigLen, 4);memcpy(data+4, msg, len);// 发送数据int ret = writen(cfd, data, len+4);return ret;
}// 连接服务器
int connectToHost(int fd, const char* ip, unsigned short port)
{// 2. 连接服务器IP portstruct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(port);inet_pton(AF_INET, ip, &saddr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));if(ret == -1){perror("connect");return -1;}printf("成功和服务器建立连接...\n");return ret;
}// 关闭套接字
int closeSocket(int fd)
{int ret = close(fd);if(ret == -1){perror("close");}return ret;
}// 接收指定的字节数
// 函数调用成功返回 size
int readn(int fd, char* buf, int size)
{int nread = 0;int left = size;char* p = buf;while(left > 0){if((nread = read(fd, p, left)) > 0){p += nread;left -= nread;}else if(nread == -1){return -1;}}return size;
}// 发送指定的字节数
// 函数调用成功返回 size
int writen(int fd, const char* msg, int size)
{int left = size;int nwrite = 0;const char* p = msg;while(left > 0){if((nwrite = write(fd, msg, left)) > 0){p += nwrite;left -= nwrite;}else if(nwrite == -1){return -1;}}return size;
}

2. 基于C++的封装

        编写C++程序应当遵循面向对象三要素:封装、继承、多态。简单地说就是封装之后的类可以隐藏掉某些属性使操作更简单并且类的功能要单一,如果要代码重用可以进行类之间的继承,如果要让函数的使用更加灵活可以使用多态。因此,我们需要封装两个类:客户端类和服务器端的类。

2.1 版本1

根据面向对象的思想,整个通信过程不管是监听还是通信的套接字都是可以封装到类的内部并且将其隐藏掉,这样相关操作函数的参数也就随之减少了,使用者用起来也更简便。

2.1.1 客户端

class TcpClient
{
public:TcpClient();~TcpClient();// int connectToHost(int fd, const char* ip, unsigned short port);int connectToHost(string ip, unsigned short port);// int sendMsg(int fd, const char* msg);int sendMsg(string msg);// int recvMsg(int fd, char* msg, int size);string recvMsg();// int createSocket();// int closeSocket(int fd);private:// int readn(int fd, char* buf, int size);int readn(char* buf, int size);// int writen(int fd, const char* msg, int size);int writen(const char* msg, int size);private:int cfd;	// 通信的套接字
};

通过对客户端的操作进行封装,我们可以看到有如下的变化:

  1. 文件描述被隐藏了,封装到了类的内部已经无法进行外部访问
  2. 功能函数的参数变少了,因为类成员函数可以直接使用类内部的成员变量。
  3. 创建和销毁套接字的函数去掉了,这两个操作可以分别放到构造和析构函数内部进行处理。
  4. 在C++中可以适当的将char* 替换为 string 类,这样操作字符串就更简便一些。

2.1.2 服务器端

class TcpServer
{
public:TcpServer();~TcpServer();// int bindSocket(int lfd, unsigned short port) + int setListen(int lfd)int setListen(unsigned short port);// int acceptConn(int lfd, struct sockaddr_in *addr);int acceptConn(struct sockaddr_in *addr);// int sendMsg(int fd, const char* msg);int sendMsg(string msg);// int recvMsg(int fd, char* msg, int size);string recvMsg();// int createSocket();// int closeSocket(int fd);private:// int readn(int fd, char* buf, int size);int readn(char* buf, int size);// int writen(int fd, const char* msg, int size);int writen(const char* msg, int size);private:int lfd;	// 监听的套接字int cfd;	// 通信的套接字
};

        通过对服务器端的操作进行封装,我们可以看到这个类和客户端的类结构以及封装思路是差不多的,并且两个类的内部有些操作的重叠的:接收和发送通信数据的函数recvMsg()、sendMsg(),以及内部函数readn()、writen()。不仅如此服务器端的类设计成这样样子是有缺陷的:服务器端一般需要和多个客户端建立连接,因此通信的套接字就需要有N个,但是在上面封装的类里边只有一个。

        既然如此,我们如何解决服务器和客户端的代码冗余和服务器不能跟多客户端通信的问题呢?

        答:瘦身、减负。可以将服务器的通信功能去掉,只留下监听并建立新连接一个功能。将客户端类变成一个专门用于套接字通信的类即可。服务器端整个流程使用服务器类+通信类来处理;客户端整个流程通过通信的类来处理。

2.2 版本2

        根据对第一个版本的分析,可以对以上代码做如下修改:

2.2.1 通信类

        套接字通信类既可以在客户端使用,也可以在服务器端使用,职责是接收和发送数据包。

类声明

class TcpSocket
{
public:TcpSocket();TcpSocket(int socket);~TcpSocket();int connectToHost(string ip, unsigned short port);int sendMsg(string msg);string recvMsg();private:int readn(char* buf, int size);int writen(const char* msg, int size);private:int m_fd;	// 通信的套接字
};

类定义

TcpSocket::TcpSocket()
{m_fd = socket(AF_INET, SOCK_STREAM, 0);
}TcpSocket::TcpSocket(int socket)
{m_fd = socket;
}TcpSocket::~TcpSocket()
{if (m_fd > 0){close(m_fd);}
}int TcpSocket::connectToHost(string ip, unsigned short port)
{// 连接服务器IP portstruct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(port);inet_pton(AF_INET, ip.data(), &saddr.sin_addr.s_addr);int ret = connect(m_fd, (struct sockaddr*)&saddr, sizeof(saddr));if (ret == -1){perror("connect");return -1;}cout << "成功和服务器建立连接..." << endl;return ret;
}int TcpSocket::sendMsg(string msg)
{// 申请内存空间: 数据长度 + 包头4字节(存储数据长度)char* data = new char[msg.size() + 4];int bigLen = htonl(msg.size());memcpy(data, &bigLen, 4);memcpy(data + 4, msg.data(), msg.size());// 发送数据int ret = writen(data, msg.size() + 4);delete[]data;return ret;
}string TcpSocket::recvMsg()
{// 接收数据// 1. 读数据头int len = 0;readn((char*)&len, 4);len = ntohl(len);cout << "数据块大小: " << len << endl;// 根据读出的长度分配内存char* buf = new char[len + 1];int ret = readn(buf, len);if (ret != len){return string();}buf[len] = '\0';string retStr(buf);delete[]buf;return retStr;
}int TcpSocket::readn(char* buf, int size)
{int nread = 0;int left = size;char* p = buf;while (left > 0){if ((nread = read(m_fd, p, left)) > 0){p += nread;left -= nread;}else if (nread == -1){return -1;}}return size;
}int TcpSocket::writen(const char* msg, int size)
{int left = size;int nwrite = 0;const char* p = msg;while (left > 0){if ((nwrite = write(m_fd, msg, left)) > 0){p += nwrite;left -= nwrite;}else if (nwrite == -1){return -1;}}return size;
}

在第二个版本的套接字通信类中一共有两个构造函数:

TcpSocket::TcpSocket()
{m_fd = socket(AF_INET, SOCK_STREAM, 0);
}TcpSocket::TcpSocket(int socket)
{m_fd = socket;
}
  • 其中无参构造一般在客户端使用,通过这个套接字对象再和服务器进行连接,之后就可以通信了
  • 有参构造主要在服务器端使用,当服务器端得到了一个用于通信的套接字对象之后,就可以基于这个套接字直接通信,因此不需要再次进行连接操作。

2.2.2 服务器类

        服务器类主要用于套接字通信的服务器端,并且没有通信能力,当服务器和客户端的新连接建立之后,需要通过TcpSocket类的带参构造将通信的描述符包装成一个通信对象,这样就可以使用这个对象和客户端通信了。

类声明

class TcpServer
{
public:TcpServer();~TcpServer();int setListen(unsigned short port);TcpSocket* acceptConn(struct sockaddr_in* addr = nullptr);private:int m_fd;	// 监听的套接字
};

类定义

TcpServer::TcpServer()
{m_fd = socket(AF_INET, SOCK_STREAM, 0);
}TcpServer::~TcpServer()
{close(m_fd);
}int TcpServer::setListen(unsigned short port)
{struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(port);saddr.sin_addr.s_addr = INADDR_ANY;  // 0 = 0.0.0.0int ret = bind(m_fd, (struct sockaddr*)&saddr, sizeof(saddr));if (ret == -1){perror("bind");return -1;}cout << "套接字绑定成功, ip: "<< inet_ntoa(saddr.sin_addr)<< ", port: " << port << endl;ret = listen(m_fd, 128);if (ret == -1){perror("listen");return -1;}cout << "设置监听成功..." << endl;return ret;
}TcpSocket* TcpServer::acceptConn(sockaddr_in* addr)
{if (addr == NULL){return nullptr;}socklen_t addrlen = sizeof(struct sockaddr_in);int cfd = accept(m_fd, (struct sockaddr*)addr, &addrlen);if (cfd == -1){perror("accept");return nullptr;}printf("成功和客户端建立连接...\n");return new TcpSocket(cfd);
}

        通过调整可以发现,套接字服务器类功能更加单一了,这样设计即解决了代码冗余问题,还能使这两个类更容易维护。

3. 测试代码

3.1 客户端

int main()
{// 1. 创建通信的套接字TcpSocket tcp;// 2. 连接服务器IP portint ret = tcp.connectToHost("192.168.237.131", 10000);if (ret == -1){return -1;}// 3. 通信int fd1 = open("english.txt", O_RDONLY);int length = 0;char tmp[100];memset(tmp, 0, sizeof(tmp));while ((length = read(fd1, tmp, sizeof(tmp))) > 0){// 发送数据tcp.sendMsg(string(tmp, length));cout << "send Msg: " << endl;cout << tmp << endl << endl << endl;memset(tmp, 0, sizeof(tmp));// 接收数据usleep(300);}sleep(10);return 0;
}

3.2 服务器端

struct SockInfo
{TcpServer* s;TcpSocket* tcp;struct sockaddr_in addr;
};void* working(void* arg)
{struct SockInfo* pinfo = static_cast<struct SockInfo*>(arg);// 连接建立成功, 打印客户端的IP和端口信息char ip[32];printf("客户端的IP: %s, 端口: %d\n",inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)),ntohs(pinfo->addr.sin_port));// 5. 通信while (1){printf("接收数据: .....\n");string msg = pinfo->tcp->recvMsg();if (!msg.empty()){cout << msg << endl << endl << endl;}else{break;}}delete pinfo->tcp;delete pinfo;return nullptr;
}int main()
{// 1. 创建监听的套接字TcpServer s;// 2. 绑定本地的IP port并设置监听s.setListen(10000);// 3. 阻塞并等待客户端的连接while (1){SockInfo* info = new SockInfo;TcpSocket* tcp = s.acceptConn(&info->addr);if (tcp == nullptr){cout << "重试...." << endl;continue;}// 创建子线程pthread_t tid;info->s = &s;info->tcp = tcp;pthread_create(&tid, NULL, working, info);pthread_detach(tid);}return 0;
}

原文链接: https://subingwen.cn/linux/socket-class/

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

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

相关文章

Linux基础篇——学习Linux基本工具安装教程视频链接

本篇文章就是记录一下学习Linux需要用到的基本工具的视频教程链接&#xff0c;方便以后查看 VMware15.5安装 安装视频教程&#xff1a;VMware15.5安装教程 centos7.6安装&#xff08;这个视频教程真的很nice&#xff09; 视频教程&#xff1a;centos7.6 虚拟机克隆、快照、…

ansible 模块扩展

uri模块 在Ansible中&#xff0c;uri模块是一个用于发送HTTP、HTTPS、FTP等请求的模块&#xff0c;可以用于获取网页内容、下载文件、上传文件等。本质上&#xff0c;它是一个HTTP客户端模块。 使用uri模块&#xff0c;需要指定一些参数来定义HTTP请求。下面是一些uri模块的常…

学习平台推荐_菜鸟教程官网

网址&#xff1a; 菜鸟教程 - 学的不仅是技术&#xff0c;更是梦想&#xff01;菜鸟教程(www.runoob.com)提供了编程的基础技术教程, 介绍了HTML、CSS、Javascript、Python&#xff0c;Java&#xff0c;Ruby&#xff0c;C&#xff0c;PHP , MySQL等各种编程语言的基础知识。 同…

Nginx-2

一、高级配置 1.1网页状态页 基于nginx 模块 ngx_http_stub_status_module 实现&#xff0c;在编译安装nginx的时候需要添加编译参数 --with-http_stub_status_module&#xff0c;否则配置完成之后监测会是提示语法错误注意: 状态页显示的是整个服务器的状态,而非虚拟主机的状…

opencv实现surface_matching记录

1 说明 使用的cv版本为4.7.0 , surface_matching功能是附加在contrib中的,并未直接包含在opencv 4.7.0中,因此编译的时候需要考虑contrib。 VS版本为2022, CMake版本为3.30-rc4. 2 编译opencv(含contrib) 参见: Win10 下编译 OpenCV 4.7.0详细全过程,包含xfeatures2…

Open3D (C++) 点云边界提取

边界提取 一、算法原理1、详细流程2、主要函数3、参考文献二、代码实现三、结果展示四、注意事项本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、详细流程 该算法完全复刻自PCL。 2、主要函数 /// \…

算法入门(上)

什么是算法&#xff1f; 算法&#xff08;Algorithm&#xff09;是解决特定问题求解步骤的描述&#xff0c;在计算机中表现为指令的有限序列&#xff0c;并且每条指令表示一个或多个操作。 给定一个问题&#xff0c;能够解决这个问题的算法是有很多种的。算式中的问题是千奇百怪…

C++为什么将 0.1f 更改为 0 性能会降低 10 倍

一、浮点数与整数的表示差异 在计算机内部&#xff0c;浮点数和整数的表示方式截然不同。浮点数遵循IEEE 754标准&#xff0c;通过符号位、指数位和尾数位来存储和表示数值&#xff0c;而整数则是直接的二进制表示。这种表示上的差异导致了它们在内存占用、处理速度以及精度上…

Debian/Ubuntu Linux安装OBS

先决条件 建议使用 xserver-xorg 1.18.4 或更新版本&#xff0c;以避免 OBS 中某些功能&#xff08;例如全屏投影仪&#xff09;出现潜在的性能问题。在 Linux 上使用 OBS Studio 需要 OpenGL 3.3&#xff08;或更高版本&#xff09;支持。在终端中输入以下内容来检查系统支持…

Halcon测量助手

模糊测量:基于模糊逻辑 模糊逻辑&#xff1a;模仿人脑的不确定性概念判断、推理思维方式&#xff0c;对于模型未知或不能确定的描述系统&#xff0c;以及强非线性、大滞后的控制对象&#xff0c;应用模糊集合和模糊规则进行推理&#xff0c;表达过渡性界限或定性知识经验&…

MySQL基础进阶:编写复杂查询

编写复杂查询 1. 子查询2. IN运算符3. 子查询VS连接4. ALL关键字5. ANY关键字6. 相关子查询7. EXISTS运算符8. SELECT子句中得子查询9. FROM子句中得子查询 1. 子查询 子查询&#xff1a; 任何一个充当另一个SQL语句的一部分的 SELECT 查询语句都是子查询&#xff0c;子查询是…

GMSB文章八:微生物中介分析

欢迎大家关注全网生信学习者系列&#xff1a; WX公zhong号&#xff1a;生信学习者Xiao hong书&#xff1a;生信学习者知hu&#xff1a;生信学习者CDSN&#xff1a;生信学习者2 介绍 中介分析&#xff08;Mediation Analysis&#xff09;是一种统计方法&#xff0c;用于研究一…

C# Benchmark

创建控制台项目&#xff08;或修改现有项目的Main方法代码&#xff09;&#xff0c;Nget导入Benchmark0.13.12&#xff0c;创建测试类&#xff1a; public class StringBenchMark{int[] numbers;public StringBenchMark() {numbers Enumerable.Range(1, 20000).ToArray();}[Be…

大语言模型(LLMs)全面学习指南,初学者入门,一看就懂!

大语言模型&#xff08;LLMs&#xff09;作为人工智能&#xff08;AI&#xff09;领域的一项突破性发展&#xff0c;已经改变了自然语言处理&#xff08;NLP&#xff09;和机器学习&#xff08;ML&#xff09;应用的面貌。这些模型&#xff0c;包括OpenAI的GPT-4o和Google的gem…

杨幂跨界学术圈:内容营销专家刘鑫炜带你了解核心期刊的学术奥秘

近日&#xff0c;知名艺人杨幂在权威期刊《中国广播电视学刊》上发表了一篇名为《浅谈影视剧中演员创作习惯——以电视剧<哈尔滨一九四四>为例》的学术论文&#xff0c;此举在学术界和娱乐圈均引起了广泛关注。该期刊不仅享有极高的声誉&#xff0c;还同时被北大中文核心…

数据库-数据完整性-用户自定义完整性实验

NULL/NOT NULL 约束&#xff1a; 在每个字段后面可以加上 NULL 修饰符来指定该字段是否可以为空&#xff1b;或者加上 NOT NULL 修饰符来指定该字段必须填上数据。 DEFAULT约束说明 DEFAULT 约束用于向列中插入默认值。如果列中没有规定其他的值&#xff0c;那么会将默认值添加…

发;flask的基本使用2

上一篇我们介绍了基本使用方法 flask使用 【 1 】基本使用 from flask import Flask# 1 实例化得到对象 app Flask(__name__)# 2 注册路由--》写视图函数 app.route(/) def index():# 3 返回给前端字符串return hello worldif __name__ __main__:# 运行app&#xff0c;默认…

Conformal Prediction

1 A Gentle Introduction to Conformal Prediction and Distribution-Free Uncertainty Quantification 2 Language Models with Conformal Factuality Guarantees

【启明智显分享】乐鑫ESP32-S3R8方案2.8寸串口屏:高性能低功耗,WIFI/蓝牙无线通信

近年来HMI已经成为大量应用聚焦的主题&#xff0c;在消费类产品通过创新的HMI设计带来增强的连接性和更加身临其境的用户体验之际&#xff0c;工业产品却仍旧在采用物理接口。这些物理接口通常依赖小型显示器或是简单的LED&#xff0c;通过简单的机电开关或按钮来实现HMI交互。…

【人工智能】—葡萄牙酒店预订信息多维度分析|预测是否取消预定算法模型大乱斗

引言 在当今数字化时代&#xff0c;数据驱动的决策在各个行业中变得越来越重要。酒店业&#xff0c;作为旅游和休闲服务的核心部分&#xff0c;正面临前所未有的机遇和挑战。随着在线预订平台的兴起&#xff0c;客户行为数据的积累为酒店提供了洞察消费者需求和优化运营策略的…