版本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 类,这样操作字符串就更简便一些。
服务器端
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
根据对第一个版本的分析,可以对以上代码做如下修改:
通信类
套接字通信类既可以在客户端使用,也可以在服务器端使用,职责是接收和发送数据包
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 << endl; // 根据读出的长度分配内存 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; }
在第二个版本的套接字通信类中一共有两个构造函数:
TcpSocket::TcpSocket() { m_fd = socket(AF_INET, SOCK_STREAM, 0); } TcpSocket::TcpSocket(int socket) { m_fd = socket; }
其中无参构造一般在客户端使用,通过这个套接字对象再和服务器进行连接,之后就可以通信了
有参构造主要在服务器端使用,当服务器端得到了一个用于通信的套接字对象之后,就可以基于这个套接字直接通信,因此不需要再次进行连接操作
服务器类
服务器类主要用于套接字通信的服务器端,并且没有通信能力,当服务器和客户端的新连接建立之后,需要通过 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 << "设置监听成功..." << 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("成功和客户端建立连接...\n"); return new TcpSocket(cfd); }
通过调整可以发现,套接字服务器类功能更加单一了,这样设计即解决了代码冗余问题,还能使这两个类更容易维护
测试代码
客户端
int main() { // 1. 创建通信的套接字 TcpSocket tcp; // 2. 连接服务器IP port int ret = tcp.connectToHost("192.168.237.131", 10000); if (ret == -1) { return -1; } // 3. 通信 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) { // 发送数据 tcp.sendMsg(string(tmp, length)); cout << "send Msg: " << endl; cout << tmp << endl << endl << endl; memset(tmp, 0, sizeof(tmp)); // 接收数据 usleep(300); } sleep(10); return 0; }
服务器端
struct SockInfo { TcpServer* s; TcpSocket* tcp; struct sockaddr_in addr; }; void* working(void* arg) { struct SockInfo* pinfo = static_cast<struct SockInfo*>(arg); // 连接建立成功, 打印客户端的IP和端口信息 char ip[32]; printf("客户端的IP: %s, 端口: %d\n", inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, ip, sizeof(ip)), ntohs(pinfo->addr.sin_port)); // 5. 通信 while (1) { printf("接收数据: .....\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. 创建监听的套接字 TcpServer s; // 2. 绑定本地的IP port并设置监听 s.setListen(10000); // 3. 阻塞并等待客户端的连接 while (1) { SockInfo* info = new SockInfo; TcpSocket* tcp = s.acceptConn(&info->addr); if (tcp == nullptr) { cout << "重试...." << endl; continue; } // 创建子线程 pthread_t tid; info->s = &s; info->tcp = tcp; pthread_create(&tid, NULL, working, info); pthread_detach(tid); } return 0; }
推荐一个零声学院项目课,个人觉得老师讲得不错,分享给大家:
零声白金学习卡(含基础架构/高性能存储/golang云原生/音视频/Linux内核)
https://xxetb.xet.tech/s/VsFMs