打印
[ARM入门]

C/C++ 套接字通信类的封装

[复制链接]
489|4
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
呐咯密密|  楼主 | 2023-9-27 10:43 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
在掌握了基于 TCP 的[color=var(--weui-LINK)][url=]套接字[/url]通信流程之后,为了方便使用,提高编码效率,可以对通信操作进行封装,本着有浅入深的原则,先基于 C 语言进行面向过程的函数封装,然后再基于 C++ 进行面向对象的类封装。

使用特权

评论回复

相关帖子

沙发
呐咯密密|  楼主 | 2023-9-27 10:43 | 只看该作者
1. 基于 C 语言的封装

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

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

使用特权

评论回复
板凳
呐咯密密|  楼主 | 2023-9-27 10:44 | 只看该作者
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数据粘包的处理



使用特权

评论回复
地板
呐咯密密|  楼主 | 2023-9-27 10:44 | 只看该作者
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.0
    int 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 port
    struct 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;
}



使用特权

评论回复
5
呐咯密密|  楼主 | 2023-9-27 10:45 | 只看该作者
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; // 通信的套接字
};

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

  • 文件描述被隐藏了,封装到了类的内部已经无法进行外部访问
  • 功能函数的参数变少了,因为类成员函数可以直接使用类内部的成员变量。
  • 创建和销毁套接字的函数去掉了,这两个操作可以分别放到构造和[color=var(--weui-LINK)][url=]析构函数[/url]内部进行处理。
  • 在 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; // 通信的套接字
    };





使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:苏州澜宭自动化科技嵌入式工程师
简介:本人从事磁编码器研发工作,负责开发2500线增量式磁编码器以及17位、23位绝对值式磁编码器,拥有多年嵌入式开发经验,精通STM32、GD32、N32等多种品牌单片机,熟练使用单片机各种外设。

504

主题

3896

帖子

47

粉丝