C++ 封装 Socket 进行通信

基于 C++ 的封装

通信 socket 的封装

此处封装 socket 的功能主要是用于通信,因此包括用于通信的文件套接字 m_fd,用于通信的函数 sendMsg 和 recvMsg;另外,在通信时,会出现 TCP 粘包问题,因此通过辅助函数 readn 和 writen 来解决该问题;

对于客户端和服务器,其中服务器的套接字有两种,用于监听和用于通信;而客户端的套接字功能只有通信,因此客户端也使用当前套接字。

由于是用于通信的 socket,因此客户端直接使用该套接字即可;因此在该类中定义了客户端连接服务器的函数 connectToHost

#ifndef _TCPSOCKET_H_
#define _TCPSOCKET_H_
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
using namespace std;

class Socket
{
public:
    Socket();
    Socket(int socket);
    ~Socket();
    int connectHost(string ip, unsigned short port);
    string recvMsg();
    int sendMsg(string msg);

private:
    int readn(char* msg, int size);
    int writen(const char* msg, int size);
private:
    int m_fd;
};


#endif  // _TCPSOCKET_H_

实现:

#include "Socket.h"

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

Socket::Socket(int socket)
{
    m_fd = socket;
}

Socket::~Socket()
{
    if (m_fd > 0) {
        close(m_fd);
    }
}

// 知晓 ip 和 port 后,直接连接服务器,注意大端小端的转换!!!
int Socket::connectHost(string ip, unsigned short port)
{
    sockaddr_in addr;
    // string.data() 返回一个指向字符串内部字符串数组的指针
    addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip.data(), (char*)&addr.sin_addr.s_addr);
    addr.sin_port = htons(port);
    int ret = connect(m_fd, (sockaddr*)&addr, sizeof(addr));
    if (ret == -1) {
        cout << "connect fail..." << endl;
        return -1;
    }
    return ret;
}

// 接收时注意到信息最开始四个字节为信息大小,这可以解决粘包问题
string Socket::recvMsg()
{
    int len = 0;
    // 首先接收前四个字节的信息,即后面整条信息的长度
    read(m_fd, &len, 4);
    len = ntohl(len);
    cout << "信息大小为:" << len << endl;
    char* msg = new char[len + 1];
    int ret = readn(msg, len);
    if (ret != len) {
        cout << "recvMsg fail...\n" << endl;
        return string();
    }
    msg[len] = '\0';
    string res(msg);
    delete[] msg;
    return res;
}

// 在信息前加上大小,以解决粘包问题
int Socket::sendMsg(string msg)
{
    int len = msg.size();
    char* buf = new char[len + 4];
    int bigLen = htonl(len);
    memcpy(buf, &bigLen, 4);
    memcpy(buf + 4, msg.data(), len);
    int ret = writen(buf, len + 4);
    if (ret != len) {
        cout << "send fail...\n" << endl;
        return -1;
    }
    delete[] buf;
    return ret;
}

// 一次性接收大小为 size 的 msg 信息
int Socket::readn(char *msg, int size)
{
    char* p = msg;
    int count = size;
    while (count > 0) {
        int len = recv(m_fd, p, count, 0);
        if (len == -1) {
            cout << "recv fail...\n" << endl;
            return -1;
        } else if (len == 0) {
            continue;
        }
        count -= len;
        p += len;
    }
    return size;
}

// 一次性将大小为 size 的信息 msg 发送出去
int Socket::writen(const char *msg, int size)
{
    const char* p = msg;
    int count = size;
    while (count > 0) {
        int len = send(m_fd, p, count, 0);
        if (len == 0) {
            continue;
        } else if (len == -1) {
            cout << "send fail...\n" << endl;
            return -1;
        }
        p += len;
        count -= len;
    }
    return size;
}

服务端的封装

服务端主要有两个功能,监听客户端请求和和客户端通信。通信的套接字在 Socket 中已经定义了,此处仅仅定义具有监听功能的套接字。

因此成员变量只有一个,即用于监听的套接字。而成员函数主要有两个,即将服务器 ip 和 port 绑定到套接字上的函数,以及创建通信套接字的函数。因此有如下定义:

#ifndef _TCPSERVER_H_
#define _TCPSERVER_H_
#include "Socket.h"

class Server {
public:
    Server();
    ~Server();
    // 绑定时只需要指定端口即可,ip 可以自动获取
    int BindAndListen(unsigned short port);
    // 和客户端建立连接后需要记录客户端的 addr,并获取用于通信的 socket
    Socket* acceptConn(sockaddr_in* addr);
private:
    int m_fd;
};

#endif  // _TCPSERVER_H_

实现:

#include "Server.h"

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

Server::~Server()
{  
    if (m_fd > 0) {
        close(m_fd);
    }
}

int Server::BindAndListen(unsigned short port)
{
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = INADDR_ANY;
    int ret = bind(m_fd, (sockaddr*)&addr, sizeof(addr));
    if (ret == -1) {
        cout << "bind fail...\n" << endl;
        return -1;
    }
    cout << "套接字绑定成功, ip 为" << inet_ntoa(addr.sin_addr) << endl;
    ret = listen(m_fd, 128);
    if (ret == -1) {
        cout << "listen fail...\n" << endl;
        return -1;
    }
    cout << "设置监听成功" << endl;
    return ret;
}

Socket *Server::acceptConn(sockaddr_in *addr)
{
    if (addr == nullptr) {
        return nullptr;
    }
    socklen_t len = sizeof(addr);
    int cfd = accept(m_fd, (sockaddr*)addr, &len);
    if (cfd == -1) {
        cout << "accept fail...\n" << endl;
        return nullptr;
    }
    cout << "成功建立连接" << endl;
    return new Socket(cfd);
}

测试-客户端

客户端仅仅一个单一的功能,即通信;因此直接使用 Socket 即可;

  • 定义 Socket;
  • 读取文件到指定大小的缓存 tmp 中;
  • 使用 Socket 封装的函数发送;
#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include "Socket.h"
using namespace std;

int main() {
    Socket sk;
    // 注意此处的 ip 改为自己的 ip
    int ret = sk.connectHost("127.0.0.1", 9898);
    if (ret == -1) {
        cout << "connect fail...\n" << endl;
        return -1;
    }
    int fd1 = open("LQ.txt", O_RDONLY);
    char tmp[128];
    int len = 0;
    memset(tmp, 0, sizeof(tmp));
    while ((len = read(fd1, tmp, sizeof(tmp))) > 0) {
        sk.sendMsg(string(tmp, len));
        cout << "send msg" << endl;
        cout << tmp << "\n\n\n" << endl;
        memset(tmp, 0, sizeof(tmp));
        usleep(300);
    }
    sleep(10);
    return 0;
}

测试-服务端

服务端用于接收客户端请求(监听);以及和客户端通信;此处要求服务端能处理多个客户端的请求,因此此处使用多线程进行处理;

  • 服务端首先定义服务端的对象 Server,该对象用于监听客户端请求;

  • 在设置好 bind 和 listen 后即可通过 accept 与客户端建立请求后进行通信;

  • 由于需要处理多个客户端,因此此处使用一个死循环来进行 accept 以建立和客户端的通信,并通过创建线程的方式来在线程能创建一个用于通信的套接字。

  • 该线程专门用于通信。因此需要创建一个函数,该函数负担了当前线程的任务,即与客户端进行通信。该函数若要和客户端进行通信,所需要的信息是客户端的信息,该信息是通过 accept 得到的,需要通过参数传递给函数;处理客户端的 addr,还需要的信息是用于通信的 socket,这也是通过 accept 获取的。由于要传递多个参数,因此将这两个进行封装,将其封装为结构体进行参数传递。

#include <string>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <iostream>
#include <fcntl.h>
#include <pthread.h>
#include "Server.h"
using namespace std;

struct SockInfo {
    Socket* sock;
    sockaddr_in addr;
};

void* working(void* arg) {
    SockInfo* info = static_cast<SockInfo*> (arg);
    char ip[32];
    printf("客户端的IP: %s, 端口: %d\n",
        inet_ntop(AF_INET, &info->addr.sin_addr.s_addr, ip, sizeof(ip)),
        ntohs(info->addr.sin_port));
    while (1) {
        cout << "接收数据" << endl;
        string msg = info->sock->recvMsg();
        if (!msg.empty()) {
            cout << msg << "\n\n\n";
        } else {
            break;
        }
    }
    delete info->sock;
    delete info;
    return nullptr;
}

int main() {
    Server ss;
    int ret = ss.BindAndListen(9898);
    if (ret == -1) {
        cout << "bind and listen fail...\n" << endl;
        return -1;
    }
    while (1) {
        SockInfo* info = new SockInfo;
        Socket *sock = ss.acceptConn(&info->addr);
        info->sock = sock;
        pthread_t tid;
        pthread_create(&tid, nullptr, working, info);
        pthread_detach(tid);
    }
    return 0;
}

测试结果

g++ Socket.cpp Server.cpp server.cpp -o server -lpthread
g++ client.cpp Socket.cpp -o client

先启动服务端,后启动客户端:

./server
./client

即可顺利进行测试;若需要测试多线程功能,则仿造以上的客户端再写一个即可;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值