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

简介: 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; // 客户端的socket
    unsigned 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;//服务端的监听socket
        int m_connsock; //已连接的客户端socket
        int 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(); //获取客户端的ip
        void Closelisten(); //关闭监听socket
        void 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则是需要我们去一点点的实现,为了避免重复的书写代码,我们可以将它们封装成类来供我们去使用,以上就是这篇文章的全部内容了,大家下篇见!

相关文章
|
4月前
|
C++
C++ 语言异常处理实战:在编程潮流中坚守稳定,开启代码可靠之旅
【8月更文挑战第22天】C++的异常处理机制是确保程序稳定的关键特性。它允许程序在遇到错误时优雅地响应而非直接崩溃。通过`throw`抛出异常,并用`catch`捕获处理,可使程序控制流跳转至错误处理代码。例如,在进行除法运算或文件读取时,若发生除数为零或文件无法打开等错误,则可通过抛出异常并在调用处捕获来妥善处理这些情况。恰当使用异常处理能显著提升程序的健壮性和维护性。
76 2
|
5月前
|
消息中间件 Java C语言
消息队列 MQ使用问题之在使用C++客户端和GBase的ESQL进行编译时出现core dump,该怎么办
消息队列(MQ)是一种用于异步通信和解耦的应用程序间消息传递的服务,广泛应用于分布式系统中。针对不同的MQ产品,如阿里云的RocketMQ、RabbitMQ等,它们在实现上述场景时可能会有不同的特性和优势,比如RocketMQ强调高吞吐量、低延迟和高可用性,适合大规模分布式系统;而RabbitMQ则以其灵活的路由规则和丰富的协议支持受到青睐。下面是一些常见的消息队列MQ产品的使用场景合集,这些场景涵盖了多种行业和业务需求。
|
1月前
|
自然语言处理 编译器 Linux
告别头文件,编译效率提升 42%!C++ Modules 实战解析 | 干货推荐
本文中,阿里云智能集团开发工程师李泽政以 Alinux 为操作环境,讲解模块相比传统头文件有哪些优势,并通过若干个例子,学习如何组织一个 C++ 模块工程并使用模块封装第三方库或是改造现有的项目。
|
2月前
|
安全 程序员 编译器
【实战经验】17个C++编程常见错误及其解决方案
想必不少程序员都有类似的经历:辛苦敲完项目代码,内心满是对作品品质的自信,然而当静态扫描工具登场时,却揭示出诸多隐藏的警告问题。为了让自己的编程之路更加顺畅,也为了持续精进技艺,我想借此机会汇总分享那些常被我们无意间忽视却又导致警告的编程小细节,以此作为对未来的自我警示和提升。
155 5
|
2月前
|
网络协议 机器人 C++
KUKA机器人Socket通讯配置方法:技术干货分享
【10月更文挑战第7天】在现代自动化生产线上,KUKA机器人凭借其高效、灵活和精确的特点,成为众多企业的首选。为了实现KUKA机器人与其他设备或系统之间的数据交互,Socket通讯配置显得尤为重要。本文将详细介绍KUKA机器人Socket通讯的配置方法,帮助大家在工作中更好地掌握这一技术。
210 2
|
3月前
|
网络协议 Python
告别网络编程迷雾!Python Socket编程基础与实战,让你秒变网络达人!
在网络编程的世界里,Socket编程是连接数据与服务的关键桥梁。对于初学者,这往往是最棘手的部分。本文将用Python带你轻松入门Socket编程,从创建TCP服务器与客户端的基础搭建,到处理并发连接的实战技巧,逐步揭开网络编程的神秘面纱。通过具体的代码示例,我们将掌握Socket的基本概念与操作,让你成为网络编程的高手。无论是简单的数据传输还是复杂的并发处理,Python都能助你一臂之力。希望这篇文章成为你网络编程旅程的良好开端。
60 3
|
3月前
|
Java Android开发 C++
🚀Android NDK开发实战!Java与C++混合编程,打造极致性能体验!📊
在Android应用开发中,追求卓越性能是不变的主题。本文介绍如何利用Android NDK(Native Development Kit)结合Java与C++进行混合编程,提升应用性能。从环境搭建到JNI接口设计,再到实战示例,全面展示NDK的优势与应用技巧,助你打造高性能应用。通过具体案例,如计算斐波那契数列,详细讲解Java与C++的协作流程,帮助开发者掌握NDK开发精髓,实现高效计算与硬件交互。
146 1
|
3月前
|
数据安全/隐私保护 C语言 C++
C++(七)封装
本文档详细介绍了C++封装的概念及其应用。封装通过权限控制对外提供接口并隐藏内部数据,增强代码的安全性和可维护性。文档首先解释了`class`中的权限修饰符(`public`、`private`、`protected`)的作用,并通过示例展示了如何使用封装实现栈结构。接着介绍了构造器和析构器的使用方法,包括初始化列表的引入以及它们在内存管理和对象生命周期中的重要性。最后,通过分文件编程的方式展示了如何将类定义和实现分离,提高代码的模块化和复用性。
|
4月前
|
存储 算法 C++
C++ STL应用宝典:高效处理数据的艺术与实战技巧大揭秘!
【8月更文挑战第22天】C++ STL(标准模板库)是一组高效的数据结构与算法集合,极大提升编程效率与代码可读性。它包括容器、迭代器、算法等组件。例如,统计文本中单词频率可用`std::map`和`std::ifstream`实现;对数据排序及找极值则可通过`std::vector`结合`std::sort`、`std::min/max_element`完成;而快速查找字符串则适合使用`std::set`配合其内置的`find`方法。这些示例展示了STL的强大功能,有助于编写简洁高效的代码。
52 2
|
3月前
|
图形学 C++ C#
Unity插件开发全攻略:从零起步教你用C++扩展游戏功能,解锁Unity新玩法的详细步骤与实战技巧大公开
【8月更文挑战第31天】Unity 是一款功能强大的游戏开发引擎,支持多平台发布并拥有丰富的插件生态系统。本文介绍 Unity 插件开发基础,帮助读者从零开始编写自定义插件以扩展其功能。插件通常用 C++ 编写,通过 Mono C# 运行时调用,需在不同平台上编译。文中详细讲解了开发环境搭建、简单插件编写及在 Unity 中调用的方法,包括创建 C# 封装脚本和处理跨平台问题,助力开发者提升游戏开发效率。
246 0