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

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 8080
    
    int 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 8080
    
    int 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失败

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

    ​ 

    相关文章:

  • 字符,日期函数
  • SQL WITH RECURSIVE 递归
  • 【区块链安全 | 第十八篇】类型之引用类型(二)
  • 开源深度学习框架PyTorch
  • 为什么要指针压缩,为什么能指针压缩?原理是什么?
  • 01小游戏
  • 3月31号
  • lib-zo,C语言另一个协程库,激活文件IO操作协程化
  • http知识点
  • 2025年浙江省中等职业学校职业能力大赛(学生技术技能类)“移动应用与开发”赛项技术文件
  • FFTW库在vs2022下编译lib库及在QT6.8中调用
  • LeetCode hot 100—二叉搜索树中第K小的元素
  • 【VUE2】综合练习——智慧商城
  • visio导出pdf公式变形
  • Embedding原理
  • zk基础—1.一致性原理和算法一
  • 《算法:递归+记忆化搜索》
  • 【计算机视觉】OpenCV实战项目- 抖音动态小表情
  • ESP32移植Openharmony外设篇(11) mfrc522射频读卡器
  • 数据处理与机器学习入门
  • 电商网店运营实训报告/优化系统的软件
  • 定制版网站建设详细报价单/网页设计代码大全
  • 哪家公司建设网站/优化设计答案四年级上册语文
  • 嘉兴免费做网站/第三方网络营销平台有哪些
  • 深圳建设工程网/北京seo服务行者
  • 佛山网站建设哪个好点/百度搜索引擎优化