Encapsulation of C/C + + socket communication class

Posted by beselabios on Thu, 09 Dec 2021 09:45:53 +0100

After mastering the socket communication process based on TCP, in order to facilitate use and improve coding efficiency, the communication operation can be encapsulated. Based on the principle of shallow to deep, process oriented function encapsulation is carried out based on C language, and then object-oriented class encapsulation is carried out based on C + +.

1. Encapsulation based on C language

Socket communication based on TCP is divided into two parts: server-side communication and client-side communication. As long as we master the communication process and encapsulate the corresponding function, let's review the communication process first:

Server side

  • Create socket for listening
  • Bind the socket used for listening with the local IP and port
  • lsnrctl start
  • Wait and accept a new client connection, and the connection is established to obtain the socket for communication and the IP and port information of the client
  • Communicate with the client (receive and send data) using the socket of the resulting communication
  • After communication, close the socket (listening + communication)

client

  • Create socket for communication
  • Use the server-side bound IP and port to connect to the server
  • Communicate with the server (send and receive data) using sockets for communication
  • End of communication, close socket (Communication)

1.1 function declaration

It can be seen from the communication process that some operation steps of the server and the client are the same, so the encapsulated function can be shared. The relevant communication functions are declared as follows:

/////////////////////////////////////////////////// 
////////////////////Server///////////////////////
///////////////////////////////////////////////////
int bindSocket(int lfd, unsigned short port);
int setListen(int lfd);
int acceptConn(int lfd, struct sockaddr_in *addr);

/////////////////////////////////////////////////// 
////////////////////Client///////////////////////
///////////////////////////////////////////////////
int connectToHost(int fd, const char* ip, unsigned short port);

/////////////////////////////////////////////////// 
/////////////////////Share////////////////////////
///////////////////////////////////////////////////
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);

For the functions readn() and writen(), refer to Processing of TCP data sticky packets

1.2 function definition

// Create monitoring socket
int createSocket()
{
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1)
    {
        perror("socket");
        return -1;
    }
    printf("Socket created successfully, fd=%d\n", fd);
    return fd;
}

// Bind local IP and port
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("Socket binding succeeded, ip: %s, port: %d\n",
           inet_ntoa(saddr.sin_addr), port);
    return ret;
}

// Set listening
int setListen(int lfd)
{
    int ret = listen(lfd, 128);
    if(ret == -1)
    {
        perror("listen");
        return -1;
    }
    printf("Successfully set listening...\n");
    return ret;
}

// Blocking and waiting for client connections
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("Successfully established connection with client...\n");
    return cfd; 
}

// receive data 
int recvMsg(int cfd, char** msg)
{
    if(msg == NULL || cfd <= 0)
    {
        return -1;
    }
    // receive data 
    // 1. Read data header
    int len = 0;
    readn(cfd, (char*)&len, 4);
    len = ntohl(len);
    printf("Block size: %d\n", len);

    // Allocate memory according to the read length
    char *buf = (char*)malloc(len+1);
    int ret = readn(cfd, buf, len);
    if(ret != len)
    {
        return -1;
    }
    buf[len] = '\0';
    *msg = buf;

    return ret;
}

// send data
int sendMsg(int cfd, char* msg, int len)
{
   if(msg == NULL || len <= 0)
   {
       return -1;
   }
   // Requested memory space: data length + packet header 4 bytes (storage data length)
   char* data = (char*)malloc(len+4);
   int bigLen = htonl(len);
   memcpy(data, &bigLen, 4);
   memcpy(data+4, msg, len);
   // send data
   int ret = writen(cfd, data, len+4);
   return ret;
}

// Connect server
int connectToHost(int fd, const char* ip, unsigned short port)
{
    // 2. Connect to the server 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("Successfully established connection with the server...\n");
    return ret;
}

// Close socket
int closeSocket(int fd)
{
    int ret = close(fd);
    if(ret == -1)
    {
        perror("close");
    }
    return ret;
}

// Receive the specified number of bytes
// size returned after successful function call
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;
}

// Sends the specified number of bytes
// size returned after successful function call
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;
}

2. C + + based packaging

Writing C + + programs should follow three object-oriented elements: encapsulation, inheritance and polymorphism. In short, the encapsulated class can hide some attributes to make the operation simpler, and the function of the class should be single. If you want code reuse, you can inherit between classes, and if you want to make the use of functions more flexible, you can use polymorphism. Therefore, we need to encapsulate two classes: the client-side class and the server-side class.

2.1 version 1

According to the object-oriented idea, the socket of the whole communication process, whether monitoring or communication, can be encapsulated into the class and hidden, so that the parameters of the relevant operation functions are reduced and easier for users to use.

2.1. 1 client

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; // Socket for communication
};

By encapsulating the operation of the client, we can see the following changes:

  • The file description is hidden, encapsulated inside the class, and cannot be accessed externally
  • Function functions have fewer parameters because class member functions can directly use member variables inside the class.
  • The functions of creating and destroying sockets are removed, and these two operations can be processed inside the constructor and destructor respectively.
  • In C + +, you can appropriately replace char * with string class, which makes it easier to operate strings.

2.1. 2 server side

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; // Listening socket
    int cfd; // Socket for communication
};

By encapsulating the server-side operations, we can see that the class structure and encapsulation idea of this class are similar to those of the client, and some internal operations of the two classes overlap: the functions recvMsg(), sendMsg(), which receive and send communication data, and the internal functions readn(), write(). Not only that, the class design of the server side is flawed: the server side generally needs to establish connections with multiple clients, so there need to be N sockets for communication, but there is only one class encapsulated above.

In that case, how can we solve the problem of code redundancy between the server and the client and the server can not communicate with multiple clients?

A: lose weight and reduce burden. The communication function of the server can be removed, leaving only the function of listening and establishing a new connection. Turn the client class into a class dedicated to socket communication. The whole process of the server side uses server class + communication class to process; The whole process of the client is handled through the communication class.

2.2 version 2

According to the analysis of the first version, the above code can be modified as follows:

2.2. 1. Communication

Socket communication class can be used both on the client side and on the server side. Its responsibility is to receive and send data packets.

Class declaration

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; // Socket for communication
};

Class definition

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)
{
    // Connect to server 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 << "Successfully established connection with the server..." << endl;
    return ret;
}

int TcpSocket::sendMsg(string msg)
{
    // Requested memory space: data length + packet header 4 bytes (storage data length)
    char* data = new char[msg.size() + 4];
    int bigLen = htonl(msg.size());
    memcpy(data, &bigLen, 4);
    memcpy(data + 4, msg.data(), msg.size());
    // send data
    int ret = writen(data, msg.size() + 4);
    delete[]data;
    return ret;
}

string TcpSocket::recvMsg()
{
    // receive data 
    // 1. Read data header
    int len = 0;
    readn((char*)&len, 4);
    len = ntohl(len);
    cout << "Block size: " << len << endl;

    // Allocate memory according to the read length
    char* buf = new char[len + 1];
    int ret = readn(buf, len);
    if (ret != len)
    {
        return string();
    }
    buf[len] = '\0';
    string retStr(buf);
    delete[]buf;

    return retStr;
}

int TcpSocket::readn(char* buf, int size)
{
    int nread = 0;
    int left = size;
    char* p = buf;

    while (left > 0)
    {
        if ((nread = read(m_fd, p, left)) > 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;
}

In the second version of the socket communication class, there are two constructors:

TcpSocket::TcpSocket()
{
    m_fd = socket(AF_INET, SOCK_STREAM, 0);
}

TcpSocket::TcpSocket(int socket)
{
    m_fd = socket;
}
  • The parameterless structure is generally used by the client. Connect with the server through this socket object, and then you can communicate
  • The parameter structure is mainly used on the server side. After the server side obtains a socket object for communication, it can communicate directly based on this socket, so there is no need to connect again.

2.2. 2 server class

The server class is mainly used for the server side of socket communication and has no communication capability. After the new connection between the server and the client is established, the communication descriptor needs to be packaged into a communication object through the parameter structure of TcpSocket class, so that this object can be used to communicate with the client.

Class declaration

class TcpServer
{
public:
    TcpServer();
    ~TcpServer();
    int setListen(unsigned short port);
    TcpSocket* acceptConn(struct sockaddr_in* addr = nullptr);

private:
    int m_fd; // Listening socket
};

Class definition

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 << "Socket binding succeeded, ip: "
        << inet_ntoa(saddr.sin_addr)
        << ", port: " << port << endl;

    ret = listen(m_fd, 128);
    if (ret == -1)
    {
        perror("listen");
        return -1;
    }
    cout << "Successfully set listening..." << endl;

    return ret;
}

TcpSocket* TcpServer::acceptConn(sockaddr_in* addr)
{
    if (addr == NULL)
    {
        return nullptr;
    }

    socklen_t addrlen = sizeof(struct sockaddr_in);
    int cfd = accept(m_fd, (struct sockaddr*)addr, &addrlen);
    if (cfd == -1)
    {
        perror("accept");
        return nullptr;
    }
    printf("Successfully established connection with client...\n");
    return new TcpSocket(cfd);
}

Through adjustment, it can be found that the function of socket server class is more single. This design not only solves the problem of code redundancy, but also makes the two classes easier to maintain.

3. Test code

3.1 client

int main()
{
    // 1. Create a socket for communication
    TcpSocket tcp;

    // 2. Connect to the server IP port
    int ret = tcp.connectToHost("192.168.237.131", 10000);
    if (ret == -1)
    {
        return -1;
    }

    // 3. Communication
    int fd1 = open("english.txt", O_RDONLY);
    int length = 0;
    char tmp[100];
    memset(tmp, 0, sizeof(tmp));
    while ((length = read(fd1, tmp, sizeof(tmp))) > 0)
    {
        // send data
        tcp.sendMsg(string(tmp, length));

        cout << "send Msg: " << endl;
        cout << tmp << endl << endl << endl;
        memset(tmp, 0, sizeof(tmp));

        // receive data 
        usleep(300);
    }

    sleep(10);

    return 0;
}

3.2 server side

struct SockInfo
{
    TcpServer* s;
    TcpSocket* tcp;
    struct sockaddr_in addr;
};

void* working(void* arg)
{
    struct SockInfo* pinfo = static_cast<struct SockInfo*>(arg);
    // If the connection is established successfully, print the IP and port information of the client
    char ip[32];
    printf("Client IP: %s, port: %d\n",
        inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)),
        ntohs(pinfo->addr.sin_port));

    // 5. Communications
    while (1)
    {
        printf("receive data : .....\n");
        string msg = pinfo->tcp->recvMsg();
        if (!msg.empty())
        {
            cout << msg << endl << endl << endl;
        }
        else
        {
            break;
        }
    }
    delete pinfo->tcp;
    delete pinfo;
    return nullptr;
}

int main()
{
    // 1. Create a listening socket
    TcpServer s;
    // 2. Bind the local IP port and set listening
    s.setListen(10000);
    // 3. Block and wait for the client to connect
    while (1)
    {
        SockInfo* info = new SockInfo;
        TcpSocket* tcp = s.acceptConn(&info->addr);
        if (tcp == nullptr)
        {
            cout << "retry ...." << endl;
            continue;
        }
        // Create child thread
        pthread_t tid;
        info->s = &s;
        info->tcp = tcp;

        pthread_create(&tid, NULL, working, info);
        pthread_detach(tid);
    }

    return 0;
}

Article source: https://subingwen.com/linux/socket-class/