????????在掌握了基于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);
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.0int 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 portstruct 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;
}
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 portstruct 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;
}
- 其中無參構造一般在客戶端使用,通過這個套接字對象再和服務器進行連接,之后就可以通信了
- 有參構造主要在服務器端使用,當服務器端得到了一個用于通信的套接字對象之后,就可以基于這個套接字直接通信,因此不需要再次進行連接操作。
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.0int 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);
}
????????通過調整可以發現,套接字服務器類功能更加單一了,這樣設計即解決了代碼冗余問題,還能使這兩個類更容易維護。
3. 測試代碼
3.1 客戶端
int main()
{// 1. 創建通信的套接字TcpSocket tcp;// 2. 連接服務器IP portint 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;
}
3.2 服務器端
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;
}
原文鏈接: https://subingwen.cn/linux/socket-class/