Muduo网络库实现 [三] - Socket模块

目录

设计思路

类的设计 

模块的实现

基础模块

特殊模块 

集成模块 

主函数

主函数实现

主函数测试

疑惑点


设计思路

Socket模块主要是对套接字的基础操作进行封装,简化我们对套接字的操作,不需要调用C的原生接口,而是以面向对象的方式来调用。 

那么我们需要封装哪些接口呢?

  • 首先,最基础的接口,创建套接字,绑定地址信息,建立连接,开始监听,获取新连接,读取数据,写入数据,关闭套接字这几个基本功能我们还是需要提供的。
  • 其次就是两个特殊的功能: 设置套接字非阻塞,因为后续我们读取和写入都是非阻塞进行的。 还有就是设置地址信息和端口号复用,这是为了便于服务器崩溃之后能够立即以固定端口重启。
  • 还需要提供两个集成的功能,创建一个服务器连接,以及建立一个客户端连接

类的设计 

public: /*-- - 基础功能-- -*/bool Create();//创建套接字bool Bind();//绑定地址信息bool Connect();//向服务端发起连接bool Listen();//服务端开始监听int Accept();//获取客户端连接ssize_t send();//发送数据ssize_t Recv();//接收数据void close();//关闭套接字/*-- - 特殊功能-- -*/ssize_t SendNonBlock(void *buf, size_t len) // 非阻塞发送数据void SetNonBlock();//设置套接字非阻塞void SetAddrReuse();//设置地址信息和端口号复用/*-- - 整合功能-- -*/bool CreateServer();//创建一个服务器连接bool CreateClient();//创建一个客户端连接
};

模块的实现

这个模块是把服务端的Socket和客户端的Socket整合到一起了

基础模块

class Socket
{
private:int _sockfd;public:Socket() // 接收监听套接字的构造函数: _sockfd(-1){}Socket(int sockfd) // 接收客户端连接后的通信套接字: _sockfd(sockfd){}~Socket() { Close(); }/*-- - 基础功能-- -*/bool Create() // 创建套接字{_sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if (_sockfd < 0){ERR_LOG("Create failed");return false;}std::cout << "sockfd:" << _sockfd << std::endl;return true;}bool Bind(const string &ip, uint16_t port) // 绑定地址信息{struct sockaddr_in addr;memset(&addr, 0, sizeof addr);addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);socklen_t len = sizeof addr;int n = bind(_sockfd, (struct sockaddr *)&addr, len);if (n < 0){ERR_LOG("Bind failed");return false;}std::cout << "Bind:" << n << std::endl;return true;}bool Listen(int backlog = MAX_LISTEN) // 服务端开始监听{int n = listen(_sockfd, backlog);if (n < 0){ERR_LOG("Listen failed");return false;}std::cout << "Listen:" << n << std::endl;return true;}bool Connect(const string &ip, uint16_t port) // 向服务端发起连接{struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(ip.c_str());addr.sin_port = htons(port);socklen_t len = sizeof addr;int n = connect(_sockfd, (struct sockaddr *)&addr, len);if (n < 0){ERR_LOG("Connect failed");return false;}std::cout << "Connect:" << n << std::endl;return true;}int Accept() // 获取客户端连接{int connfd = accept(_sockfd, nullptr, nullptr);std::cout << "Accept:" << connfd << std::endl;if (connfd < 0){ERR_LOG("Accept failed");return -1;}return connfd;}ssize_t Send(const void *buf, size_t len, int flag = 0) // 将数据从用户态缓冲区发送到内核缓冲区{int n = send(_sockfd, buf, len, flag);if (n < 0){if (errno == EAGAIN || errno == EINTR){return 0;ERR_LOG("send failed");return -1;}}return n;}ssize_t Recv(void *buf, size_t len, int flag = 0) // 接收数据{int n = recv(_sockfd, buf, len, flag);if (n < 0){if (errno == EAGAIN || errno == EINTR){return 0;ERR_LOG("recv failed");return -1;}}return n;}void Close() // 关闭套接字{close(_sockfd);}ssize_t SendNonBlock(void *buf, size_t len) // 非阻塞发送数据{return Recv(buf, len, MSG_DONTWAIT);}};

特殊模块 

为什么需要把套接字设置成非阻塞属性呢?

设置非阻塞其实就两个步骤,首先获取描述符当前属性,然后再在获取到的属性上加上我们的非阻塞属性,再将其设置进描述符中。 这里需要用到 fcntl() 接口 

int fcntl(int fd, int cmd, ... /* arg */);

    man手册中说明了,获取和设置O_CLOEXEC 也就是不可被拷贝,是使用 F_GETFD和F_SETFD,而其他属性的获取和设置则需要使用F_GETFL 和 F_SETFL。

        // 设置套接字非阻塞属性void SetNonBlock(){// int fcntl(int fd, int cmd, ...)int flag = fcntl(_sockfd, F_GETFL, 0);fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);}

     地址信息和端口号复用需要用到的 setsockopt()  接口

    int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
    

    第一个参数就是要设置的文件描述符或者说套接字,第二个参数就是要设置的层级,第三个参数表示要进行什么操作,第四个参数表示要设置的值,1 表示激活,0表示取消,第四个参数表示第三个参数的大小。

        void SetAddrReuse() // 设置地址信息和端口号复用{int val = 1;int ret = setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &val, sizeof val);// 设置 SO_REUSEADDR 可以绑定处于 TIME_WAIT 状态的端口// 设置 SO_REUSEPORT 可以让一个端口被多个 socket 绑定,可以用于实现负载均衡}

    集成模块 

    然后就是设计两个集成端口,首先创建服务器套接字,他需要创建一个套接字绑定端口和IP设置非阻塞,还要设置地址复用,以及开始监听。 

        bool CreateServer(uint16_t port, const string &ip = "0.0.0.0", bool nonblock = false) // 创建一个服务器连接{// 1.创建套接字 2.绑定地址 3.开始监听 4.打开地址重用 5.设置非阻塞if (Create() == false)return false;if (Bind(ip, port) == false)return false;if (Listen() == false)return false;SetAddrReuse();if (nonblock)SetNonBlock();return true;}

    而建立一个客户端套接字,他也需要创建套接字,但是它不需要我们显式绑定端口和IP,然后就是调用Conect进行连接服务器。注意客户端套接字不要connect之前设置非阻塞,因为如果设置了非阻塞,那么我们就无法判断connect是否连接成功。

        bool CreateClient(uint16_t port, const string &ip) // 创建一个客户端连接{// 1.创建套接字 2.连接客户端if (Create() == false)return false;if (Connect(ip, port) == false)return false;return true;}

    主函数

    主函数实现

    server.cc

    #include <iostream>
    #include "Socket.hpp"#define PORT 8080int main()
    {// 创建服务端套接字Socket serverSocket;// 创建并初始化服务端if (!serverSocket.CreateServer(PORT, "0.0.0.0", true)){std::cerr << "Server initialization failed!" << std::endl;return -1;}std::cout << "Server is listening on port " << PORT << "..." << std::endl;// 接受客户端连接int clientSocket = -1;while (1){sleep(1);clientSocket = serverSocket.Accept();if (clientSocket < 0){std::cerr << "Failed to accept client connection!" << std::endl;}else{break;}}std::cout << "Client connected!" << std::endl;Socket conSocket(clientSocket);int RecvCount = 5;// 接收数据while (RecvCount){char buffer[1024] = {0};ssize_t bytesReceived = conSocket.Recv(buffer, sizeof(buffer));if (bytesReceived > 0){std::cout << "Received from client: " << buffer << std::endl;RecvCount--;}else{std::cerr << "Failed to receive data from client!" << std::endl;}sleep(1);}// 发送数据到客户端int SendCount = 5;while (SendCount){const char *response = "Hello from server!";ssize_t bytesSent = conSocket.Send(response, strlen(response));if (bytesSent > 0){std::cout << "Sent to client: " << response << std::endl;SendCount--;}else{std::cerr << "Failed to send data to client!" << std::endl;}sleep(1);}// 关闭连接serverSocket.Close();std::cout << "Server closed!" << std::endl;return 0;
    }

    client.cc 

    #include <iostream>
    #include "Socket.hpp"#define PORT 8080int main()
    {// 创建客户端套接字Socket clientSocket;// 创建并初始化客户端if (!clientSocket.CreateClient(PORT, "127.0.0.1")){std::cerr << "Client initialization failed!" << std::endl;return -1;}std::cout << "Client connected to server!" << std::endl;// 发送数据到服务端int SendCount = 5;while (SendCount){const char *message = "Hello from client!";ssize_t bytesSent = clientSocket.Send(message, strlen(message));if (bytesSent > 0){std::cout << "Sent to server: " << message << std::endl;SendCount--;}else{std::cerr << "Failed to send data to server!" << std::endl;}sleep(1);}// 接收数据从服务端int RecvCount = 5;while (RecvCount){char buffer[1024] = {0};ssize_t bytesReceived = clientSocket.Recv(buffer, sizeof(buffer));if (bytesReceived > 0){std::cout << "Received from server: " << buffer << std::endl;RecvCount--;}else{std::cerr << "Failed to receive data from server!" << std::endl;}sleep(1);}// 关闭连接clientSocket.Close();std::cout << "Client closed!" << std::endl;return 0;
    }

    主函数测试

    客户端往服务端发送数据

    Client.cc

    const char *message = "Hello from client!";

    ssize_t bytesSent = clientSocket.Send(message, strlen(message));

    这两段代码的过程如下:

    Server.CC 

    char buffer[1024] = {0};

    ssize_t bytesReceived = conSocket.Recv(buffer, sizeof(buffer));

    这两段代码的过程如下:

     服务端与客户端双向发送/接收,以及关闭

    疑惑点

    关于套接字的基础功能,为啥已经建立连接了,后面又获取连接了?

    因为你建立的连接是客户端->服务端的,而获取新连接,是指的服务端获取到了客户端的连接。 

    socket函数的用法 

    int socket(int domain, int type, int protocol);
    

    这里为啥要用到memset?

    bind函数的用法 

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    

     accept的用法

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    


     

    send的用法

    send read write接口的解释

    ssize_t send(const void* buf, size_t len, int flag = 0)  这里的buf指的是内核发送缓冲区还是用户态的输出缓冲区?

    send的sockfd参数是干嘛的

    是不是意思就是说,A与B进行通信的话 会有个sockfd1 C与D进行通信的话,会有个sockfd2?

    不是的

    但是每对通信的客户端和服务端都需要独立的套接字进行数据传输 

     send发送数据

      send发送数据指的是从用户态缓冲区发送到内核态缓冲区,并不是从客户端发送到服务端了

    这里send函数为啥不传_sockfd 

     为什么会有两个构造函数

    ​ 

    send()是把数据从用户输出缓冲区发送到内核的输出缓冲区中 这个内核的输出缓冲区是指的自己的内核缓冲区 还是对端的内核缓冲区?

    为什么Accept的返回值为-1

    因为此时客户端还没连接,但已经调用了获取客户端的接口。所以会返回-1

    CTRL + Z后,再次启动bind失败

    客户端发送了数据 但是服务端收不到数据 

    ​ 

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

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

    相关文章

    优选算法的巧思之径:模拟专题

    专栏&#xff1a;算法的魔法世界 个人主页&#xff1a;手握风云 目录 一、模拟 二、例题讲解 2.1. 替换所有的问号 2.2. 提莫攻击 2.3. Z字形变换 2.4. 外观数列 2.5. 数青蛙 一、模拟 模拟算法说简单点就是照葫芦画瓢&#xff0c;现在草稿纸上模拟一遍算法过程&#xf…

    贪心算法(13)(java)合并区间

    题目&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 示例 1&#xff1a; 输入&#xff…

    A股复权计算_权息数据整理

    目录 前置&#xff1a; 步骤&#xff1a; 1 以通达信为参照 2 从优矿获取所需数据 2.1 股票配股信息 2.2 股票分红信息 2.3 股票拆股信息 3 合并数据&#xff0c;制成权息数据表 权息数据截止20250329.7z 视频 前置&#xff1a; 1 本系列将以 “A股复权计算_” 开头…

    学习笔记—数据结构—二叉树(链式)

    目录 二叉树&#xff08;链式&#xff09; 概念 结构 初始化 遍历 前序遍历 中序遍历 后序遍历 层序遍历 结点个数 叶子结点个数 第k层结点个数 深度/高度 查找值为x的结点 销毁 判断是否为完整二叉树 总结 头文件Tree.h Tree.c 测试文件test.c 补充文件Qu…

    Open GL ES ->GLSurfaceView在正交投影下的图片旋转、缩放、位移

    XML文件 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width"match_parent"android:layout_height"match_parent"android:o…

    Day78 | 灵神 | 反转链表 两两交换链表中的节点

    Day78 | 灵神 | 反转链表 两两交换链表中的节点 24.两两交换链表中的节点 24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a; 这道题就是下面这道题的k2的情况 25. K 个一组翻转链表 - 力扣&#xff08;LeetCode&#xff09; 基本思路和…

    滤波---卡尔曼滤波

    卡尔曼滤波概览 一、定义 卡尔曼滤波是一种基于线性系统和高斯噪声假设的递归最优状态估计算法。其核心目标是通过融合系统模型预测值与传感器测量值&#xff0c;在噪声环境中实时估计系统的动态状态&#xff08;如位置、速度、加速度等&#xff09;。 数学基础&#xff1a; …

    23种设计模式-结构型模式-桥接器

    文章目录 简介问题解决方案示例总结 简介 桥接器是一种结构型设计模式&#xff0c;可将一个大类或一系列紧密相关的类拆分为抽象和实现两个独立的层次结构&#xff0c;从而能在开发时分别使用。 问题 假如你有一个几何形状Shape类&#xff0c;它有两个子类&#xff1a;圆形C…

    手工排查后门木马的常用姿势

    声明&#xff01;本文章所有的工具分享仅仅只是供大家学习交流为主&#xff0c;切勿用于非法用途&#xff0c;如有任何触犯法律的行为&#xff0c;均与本人及团队无关&#xff01;&#xff01;&#xff01; 1. 检查异常文件 &#xff08;1&#xff09;查找最近修改的文件 # 查…

    工业机器人核心算法体系解析:从感知到决策的技术演进

    工业机器人作为智能制造的核心装备,其技术竞争力的本质是算法体系的优化与创新。从静态轨迹执行到动态环境适应,从单一任务控制到复杂场景决策,工业机器人的算法体系涵盖环境感知、运动控制、路径规划、行为决策四大核心模块。本文将深入解析各模块的关键算法及其技术演进,…

    当 EcuBus-Pro + UTA0401 遇上 NSUC1500

    文章目录 1.前言2.EcuBus-Pro简介2.1 官方地址2.2 概览 3.纳芯微NSUC1500简介3.1 NSUC1500概述3.2 产品特性 4.测试环境5.基础功能5.1 数据发送5.2 数据监控 6.自动化功能6.1 脚本创建6.2 脚本编辑6.3 脚本编辑与测试 7.音乐律动7.1 导入例程7.2 效果展示 ECB工程 1.前言 最近…

    说说Redis的内存淘汰策略?

    大家好&#xff0c;我是锋哥。今天分享关于【说说Redis的内存淘汰策略?】面试题。希望对大家有帮助&#xff1b; 说说Redis的内存淘汰策略? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Redis的内存淘汰策略用于管理当内存达到最大限制时&#xff0c;如何处理过…

    Python实现音频数字水印方法

    数字水印技术可以将隐藏信息嵌入到音频文件中而不明显影响音频质量。下面我将介绍几种在Python中实现音频数字水印的方法。 方法一&#xff1a;LSB (最低有效位) 水印 import numpy as np from scipy.io import wavfile def embed_watermark_lsb(audio_path, watermark, ou…

    Altium Designer 24 PCB 走线倒圆弧方法

    Altium Designer 24 PCB 走线倒圆弧方法 问题描述解决方法设置倒圆弧参数选择需要优化的走线进行走线优化 优化效果展示 在 PCB 设计中&#xff0c;走线转角过于尖锐不仅影响美观&#xff0c;还可能引起信号完整性问题。本文介绍如何在 Altium Designer 24 中通过倒圆弧优化走线…

    Cookie与Token详解及测试需重点关注点

    在现代Web应用中&#xff0c;Cookie 和 Token 是两种常见的身份验证与会话管理机制。它们分别在不同的场景下扮演着重要的角色&#xff0c;在性能、灵活性和安全性方面具有各自的特点。作为测试人员&#xff0c;理解它们的工作原理以及如何对其进行有效的测试&#xff0c;是保证…

    Unity 2022.3.x部分Android设备播放视频黑屏问题

    Android平台视频兼容性问题很多…类似的黑屏问题真的很头大&#xff0c;总结一些常见问题&#xff1a; 1. 视频文件不支持压缩 如果使用AssetBundle加载视频&#xff0c;这个AssetBundle压缩格式要选None。有人可能会说最新版Unity已经支持bundle压缩下播放视频&#xff0c;稳…

    Redis - 概述

    目录 ​编辑 一、什么是redis 二、redis能做什么&#xff08;有什么特点&#xff09;&#xff1f; 三、redis有什么优势 四、Redis与其他key-value存储有什么不同 五、Redis命令 六、Redis数据结构 1、基础数据结构 2、高级数据结构 一、什么是redis 1、redis&#x…

    数据库部署在服务器表不存在解决方案

    MySQL 数据库表不存在错误解决方案 MySqlException (0x80004005): Table store.SysLogOperate doesnt exist 服务器用的mysql5.6 用这个表syslogoperate只是全是小写 看起来你在使用 Pomelo.EntityFrameworkCore.MySql 作为 MySQL 数据库的提供程序&#xff0c;并且在初始化…

    图灵完备——游戏中进行实践

    图灵完备 简述结构一、基本逻辑电路1、低电平2、高电平3、非门4、与门5、三路与门6、或门7、三路或门8、与非门9、或非门10、异或门11、同或门 二、算数运算&&存储器1、二进制速算2、成对的麻烦 简述 这周就要学习计算机组成原理了&#xff0c;为了学起来不那么吃力&am…

    踏过强化学习的每一步推导

    给定 l [ a n , . . . , a 0 ] l[a_n, ..., a_0] l[an​,...,a0​]&#xff0c;现在 for idx in range(len(l)-2, -1, -1):l[idx] l[idx1] * ld注&#xff1a;这里的ld就是 λ \lambda λ&#xff0c;定义 λ 0 1 \lambda^01 λ01 证明变换后&#xff1a; l [ ∑ i 0 n …