基于C/C++面向对象的方式封装socket通信类

正文
在掌握了基于 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);  
关于函数 readn() 和 writen() 的作用请参考tcp数据粘包的处理
1.2 函数定义
// 创建监套接字int createsocket(){    int fd = socket(af_inet, sock_stream, 0);    if(fd == -1)    {        perror(socket);        return -1;    }    printf(套接字创建成功, fd=%d, 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.0    int ret = bind(lfd, (struct sockaddr*)&saddr, sizeof(saddr));    if(ret == -1)    {        perror(bind);        return -1;    }    printf(套接字绑定成功, ip: %s, port: %d,           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(设置监听成功...);    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(成功和客户端建立连接...);    return cfd; }// 接收数据int recvmsg(int cfd, char** msg){    if(msg == null || cfd  0)        {            p += nread;            left -= nread;        }        else if(nread == -1)        {            return -1;        }    }    return size;}// 发送指定的字节数// 函数调用成功返回 sizeint 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; // 通信的套接字};  
通过对客户端的操作进行封装,我们可以看到有如下的变化:
文件描述被隐藏了,封装到了类的内部已经无法进行外部访问
功能函数的参数变少了,因为类成员函数可以直接使用类内部的成员变量。
创建和销毁套接字的函数去掉了,这两个操作可以分别放到构造和析构函数内部进行处理。
在 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 port    struct 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  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.0    int 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 << 设置监听成功... < 0)    {        // 发送数据        tcp.sendmsg(string(tmp, length));        cout << send msg:  << endl;        cout << tmp << endl << endl addr.sin_port));    // 5. 通信    while (1)    {        printf(接收数据: .....);        string msg = pinfo->tcp->recvmsg();        if (!msg.empty())        {            cout << msg << endl << endl addr);        if (tcp == nullptr)        {            cout << 重试.... tcp = tcp;        pthread_create(&tid, null, working, info);        pthread_detach(tid);    }    return 0;}  


智能化电力运维系统
TriQuint推出用于下一代3G/4G智能手机的集成式多频带多模式功率放大器
CMHK宣布完成端到端5G网络测试
是否可以把电源平面上面的信号线使用微带线模型计算特性阻抗?
你对于PCB行业有没有看法
基于C/C++面向对象的方式封装socket通信类
NUC240/NUC140/NUC130在电梯控制器和监测的应用设计
最佳智能手机变焦功能机型公布:小米10至尊纪念版
我的天!三星S8和iPhone8上市将严重缺货,恐要加价才能买到!
支付宝用技术创新促进了交通行业的信息化改革
12月14日发布,OPPO第二颗自研芯片强势来袭,又有新突破!
麒麟9000和麒麟9000soc有什么区别
三极管和MOS管怎么对接下拉电阻,电阻自动降压电阻该如何设计?
一加5t怎么样 全面屏之中的一股清流
东风汽车在动力电池领域再落一子
IC封测中的芯片封装技术
Phandroid分享有关即将发布的Droid Turbo 2的一些细节
三星ISOCELLslim 3T2:像素更高、尺寸更小的图像传感器新品
电火花加工的加工特性_电火花加工的工艺参数
python3 cookbook中常遇问题的解答记录