C/C++ 套接字通信類的封裝
在掌握了基于 TCP 的套接字通信流程之后,為了方便使用,提高編碼效率,可以對(duì)通信操作進(jìn)行封裝,本著有淺入深的原則,先基于 C 語(yǔ)言進(jìn)行面向過(guò)程的函數(shù)封裝,然后再基于 C++ 進(jìn)行面向?qū)ο蟮念惙庋b。
1. 基于 C 語(yǔ)言的封裝
基于 TCP 的套接字通信分為兩部分:服務(wù)器端通信和客戶端通信。我們只要掌握了通信流程,封裝出對(duì)應(yīng)的功能函數(shù)也就不在話下了,先來(lái)回顧一下通信流程:
服務(wù)器端
創(chuàng)建用于監(jiān)聽(tīng)的套接字 將用于監(jiān)聽(tīng)的套接字和本地的 IP 以及端口進(jìn)行綁定 啟動(dòng)監(jiān)聽(tīng) 等待并接受新的客戶端連接,連接建立得到用于通信的套接字和客戶端的 IP、端口信息 使用得到的通信的套接字和客戶端通信(接收和發(fā)送數(shù)據(jù)) 通信結(jié)束,關(guān)閉套接字(監(jiān)聽(tīng) + 通信)
客戶端
創(chuàng)建用于通信的套接字 使用服務(wù)器端綁定的 IP 和端口連接服務(wù)器 使用通信的套接字和服務(wù)器通信(發(fā)送和接收數(shù)據(jù)) 通信結(jié)束,關(guān)閉套接字(通信)
1.1 函數(shù)聲明
通過(guò)通信流程可以看出服務(wù)器和客戶端有些操作步驟是相同的,因此封裝的功能函數(shù)是可以共用的,相關(guān)的通信函數(shù)聲明如下:
///////////////////////////////////////////////////?
////////////////////?服務(wù)器?///////////////////////
///////////////////////////////////////////////////
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);
關(guān)于函數(shù) readn() 和 writen() 的作用請(qǐng)參考TCP數(shù)據(jù)粘包的處理
1.2 函數(shù)定義
//?創(chuàng)建監(jiān)套接字
int?createSocket()
{
????int?fd?=?socket(AF_INET,?SOCK_STREAM,?0);
????if(fd?==?-1)
????{
????????perror("socket");
????????return?-1;
????}
????printf("套接字創(chuàng)建成功,?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;
}
//?設(shè)置監(jiān)聽(tīng)
int?setListen(int?lfd)
{
????int?ret?=?listen(lfd,?128);
????if(ret?==?-1)
????{
????????perror("listen");
????????return?-1;
????}
????printf("設(shè)置監(jiān)聽(tīng)成功...\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;?
}
//?接收數(shù)據(jù)
int?recvMsg(int?cfd,?char**?msg)
{
????if(msg?==?NULL?||?cfd?<=?0)
????{
????????return?-1;
????}
????//?接收數(shù)據(jù)
????//?1.?讀數(shù)據(jù)頭
????int?len?=?0;
????readn(cfd,?(char*)&len,?4);
????len?=?ntohl(len);
????printf("數(shù)據(jù)塊大小:?%d\n",?len);
????//?根據(jù)讀出的長(zhǎng)度分配內(nèi)存
????char?*buf?=?(char*)malloc(len+1);
????int?ret?=?readn(cfd,?buf,?len);
????if(ret?!=?len)
????{
????????return?-1;
????}
????buf[len]?=?'\0';
????*msg?=?buf;
????return?ret;
}
//?發(fā)送數(shù)據(jù)
int?sendMsg(int?cfd,?char*?msg,?int?len)
{
???if(msg?==?NULL?||?len?<=?0)
???{
???????return?-1;
???}
???//?申請(qǐng)內(nèi)存空間:?數(shù)據(jù)長(zhǎng)度?+?包頭4字節(jié)(存儲(chǔ)數(shù)據(jù)長(zhǎng)度)
???char*?data?=?(char*)malloc(len+4);
???int?bigLen?=?htonl(len);
???memcpy(data,?&bigLen,?4);
???memcpy(data+4,?msg,?len);
???//?發(fā)送數(shù)據(jù)
???int?ret?=?writen(cfd,?data,?len+4);
???return?ret;
}
//?連接服務(wù)器
int?connectToHost(int?fd,?const?char*?ip,?unsigned?short?port)
{
????//?2.?連接服務(wù)器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("成功和服務(wù)器建立連接...\n");
????return?ret;
}
//?關(guān)閉套接字
int?closeSocket(int?fd)
{
????int?ret?=?close(fd);
????if(ret?==?-1)
????{
????????perror("close");
????}
????return?ret;
}
//?接收指定的字節(jié)數(shù)
//?函數(shù)調(diào)用成功返回?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;
}
//?發(fā)送指定的字節(jié)數(shù)
//?函數(shù)調(diào)用成功返回?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++ 程序應(yīng)當(dāng)遵循面向?qū)ο笕兀悍庋b、繼承、多態(tài)。簡(jiǎn)單地說(shuō)就是封裝之后的類可以隱藏掉某些屬性使操作更簡(jiǎn)單并且類的功能要單一,如果要代碼重用可以進(jìn)行類之間的繼承,如果要讓函數(shù)的使用更加靈活可以使用多態(tài)。因此,我們需要封裝兩個(gè)類:客戶端類和服務(wù)器端的類。
2.1 版本 1
根據(jù)面向?qū)ο蟮乃枷?,整個(gè)通信過(guò)程不管是監(jiān)聽(tīng)還是通信的套接字都是可以封裝到類的內(nèi)部并且將其隱藏掉,這樣相關(guān)操作函數(shù)的參數(shù)也就隨之減少了,使用者用起來(lái)也更簡(jiǎn)便。
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;?//?通信的套接字
};
通過(guò)對(duì)客戶端的操作進(jìn)行封裝,我們可以看到有如下的變化:
文件描述被隱藏了,封裝到了類的內(nèi)部已經(jīng)無(wú)法進(jìn)行外部訪問(wèn) 功能函數(shù)的參數(shù)變少了,因?yàn)轭惓蓡T函數(shù)可以直接使用類內(nèi)部的成員變量。 創(chuàng)建和銷毀套接字的函數(shù)去掉了,這兩個(gè)操作可以分別放到構(gòu)造和析構(gòu)函數(shù)內(nèi)部進(jìn)行處理。 在 C++ 中可以適當(dāng)?shù)膶?char* 替換為 string 類,這樣操作字符串就更簡(jiǎn)便一些。
2.1.2 服務(wù)器端
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;?//?監(jiān)聽(tīng)的套接字
????int?cfd;?//?通信的套接字
};
通過(guò)對(duì)服務(wù)器端的操作進(jìn)行封裝,我們可以看到這個(gè)類和客戶端的類結(jié)構(gòu)以及封裝思路是差不多的,并且兩個(gè)類的內(nèi)部有些操作的重疊的:接收和發(fā)送通信數(shù)據(jù)的函數(shù) recvMsg()、sendMsg(),以及內(nèi)部函數(shù) readn()、writen()。不僅如此服務(wù)器端的類設(shè)計(jì)成這樣樣子是有缺陷的:服務(wù)器端一般需要和多個(gè)客戶端建立連接,因此通信的套接字就需要有 N 個(gè),但是在上面封裝的類里邊只有一個(gè)。
既然如此,我們?nèi)绾谓鉀Q服務(wù)器和客戶端的代碼冗余和服務(wù)器不能跟多客戶端通信的問(wèn)題呢?
答:瘦身、減負(fù)??梢詫⒎?wù)器的通信功能去掉,只留下監(jiān)聽(tīng)并建立新連接一個(gè)功能。將客戶端類變成一個(gè)專門用于套接字通信的類即可。服務(wù)器端整個(gè)流程使用服務(wù)器類 + 通信類來(lái)處理;客戶端整個(gè)流程通過(guò)通信的類來(lái)處理。
2.2 版本 2
根據(jù)對(duì)第一個(gè)版本的分析,可以對(duì)以上代碼做如下修改:
2.2.1 通信類
套接字通信類既可以在客戶端使用,也可以在服務(wù)器端使用,職責(zé)是接收和發(fā)送數(shù)據(jù)包。
類聲明
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)
{
????//?連接服務(wù)器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?<"成功和服務(wù)器建立連接..."?<????return?ret;
}
int?TcpSocket::sendMsg(string?msg)
{
????//?申請(qǐng)內(nèi)存空間:?數(shù)據(jù)長(zhǎng)度?+?包頭4字節(jié)(存儲(chǔ)數(shù)據(jù)長(zhǎng)度)
????char*?data?=?new?char[msg.size()?+?4];
????int?bigLen?=?htonl(msg.size());
????memcpy(data,?&bigLen,?4);
????memcpy(data?+?4,?msg.data(),?msg.size());
????//?發(fā)送數(shù)據(jù)
????int?ret?=?writen(data,?msg.size()?+?4);
????delete[]data;
????return?ret;
}
string?TcpSocket::recvMsg()
{
????//?接收數(shù)據(jù)
????//?1.?讀數(shù)據(jù)頭
????int?len?=?0;
????readn((char*)&len,?4);
????len?=?ntohl(len);
????cout?<"數(shù)據(jù)塊大小:?"?<
????//?根據(jù)讀出的長(zhǎng)度分配內(nèi)存
????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;
}
在第二個(gè)版本的套接字通信類中一共有兩個(gè)構(gòu)造函數(shù):
TcpSocket::TcpSocket()
{
????m_fd?=?socket(AF_INET,?SOCK_STREAM,?0);
}
TcpSocket::TcpSocket(int?socket)
{
????m_fd?=?socket;
}
其中無(wú)參構(gòu)造一般在客戶端使用,通過(guò)這個(gè)套接字對(duì)象再和服務(wù)器進(jìn)行連接,之后就可以通信了 有參構(gòu)造主要在服務(wù)器端使用,當(dāng)服務(wù)器端得到了一個(gè)用于通信的套接字對(duì)象之后,就可以基于這個(gè)套接字直接通信,因此不需要再次進(jìn)行連接操作。
2.2.2 服務(wù)器類
服務(wù)器類主要用于套接字通信的服務(wù)器端,并且沒(méi)有通信能力,當(dāng)服務(wù)器和客戶端的新連接建立之后,需要通過(guò) TcpSocket 類的帶參構(gòu)造將通信的描述符包裝成一個(gè)通信對(duì)象,這樣就可以使用這個(gè)對(duì)象和客戶端通信了。
類聲明
class?TcpServer
{
public:
????TcpServer();
????~TcpServer();
????int?setListen(unsigned?short?port);
????TcpSocket*?acceptConn(struct?sockaddr_in*?addr?=?nullptr);
private:
????int?m_fd;?//?監(jiān)聽(tīng)的套接字
};
類定義
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:?"
????????<????????<",?port:?"?<
????ret?=?listen(m_fd,?128);
????if?(ret?==?-1)
????{
????????perror("listen");
????????return?-1;
????}
????cout?<"設(shè)置監(jiān)聽(tīng)成功..."?<
????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);
}
通過(guò)調(diào)整可以發(fā)現(xiàn),套接字服務(wù)器類功能更加單一了,這樣設(shè)計(jì)即解決了代碼冗余問(wèn)題,還能使這兩個(gè)類更容易維護(hù)。
3. 測(cè)試代碼
3.1 客戶端
int?main()
{
????//?1.?創(chuàng)建通信的套接字
????TcpSocket?tcp;
????//?2.?連接服務(wù)器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)
????{
????????//?發(fā)送數(shù)據(jù)
????????tcp.sendMsg(string(tmp,?length));
????????cout?<"send?Msg:?"?<????????cout?<????????memset(tmp,?0,?sizeof(tmp));
????????//?接收數(shù)據(jù)
????????usleep(300);
????}
????sleep(10);
????return?0;
}
3.2 服務(wù)器端
struct?SockInfo
{
????TcpServer*?s;
????TcpSocket*?tcp;
????struct?sockaddr_in?addr;
};
void*?working(void*?arg)
{
????struct?SockInfo*?pinfo?=?static_cast(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("接收數(shù)據(jù):?.....\n");
????????string?msg?=?pinfo->tcp->recvMsg();
????????if?(!msg.empty())
????????{
????????????cout?<????????}
????????else
????????{
????????????break;
????????}
????}
????delete?pinfo->tcp;
????delete?pinfo;
????return?nullptr;
}
int?main()
{
????//?1.?創(chuàng)建監(jiān)聽(tīng)的套接字
????TcpServer?s;
????//?2.?綁定本地的IP?port并設(shè)置監(jiān)聽(tīng)
????s.setListen(10000);
????//?3.?阻塞并等待客戶端的連接
????while?(1)
????{
????????SockInfo*?info?=?new?SockInfo;
????????TcpSocket*?tcp?=?s.acceptConn(&info->addr);
????????if?(tcp?==?nullptr)
????????{
????????????cout?<"重試...."?<????????????continue;
????????}
????????//?創(chuàng)建子線程
????????pthread_t?tid;
????????info->s?=?&s;
????????info->tcp?=?tcp;
????????pthread_create(&tid,?NULL,?working,?info);
????????pthread_detach(tid);
????}
????return?0;
}
文章來(lái)源:https://subingwen.com/linux/socket-class/
