正文
在掌握了基于 TCP 的套接字通信流程之后,為了方便使用,提高編碼效率,可以對通信操作進行封裝,本著有淺入深的原則,先基于 C 語言進行面向過程的函數(shù)封裝,然后再基于 C++ 進行面向?qū)ο蟮念惙庋b。
1. 基于 C 語言的封裝
基于 TCP 的套接字通信分為兩部分:服務(wù)器端通信和客戶端通信。我們只要掌握了通信流程,封裝出對應的功能函數(shù)也就不在話下了,先來回顧一下通信流程:
服務(wù)器端
創(chuàng)建用于監(jiān)聽的套接字
將用于監(jiān)聽的套接字和本地的 IP 以及端口進行綁定
啟動監(jiān)聽
等待并接受新的客戶端連接,連接建立得到用于通信的套接字和客戶端的 IP、端口信息
使用得到的通信的套接字和客戶端通信(接收和發(fā)送數(shù)據(jù))
通信結(jié)束,關(guān)閉套接字(監(jiān)聽 + 通信)
客戶端
創(chuàng)建用于通信的套接字
使用服務(wù)器端綁定的 IP 和端口連接服務(wù)器
使用通信的套接字和服務(wù)器通信(發(fā)送和接收數(shù)據(jù))
通信結(jié)束,關(guān)閉套接字(通信)
1.1 函數(shù)聲明
通過通信流程可以看出服務(wù)器和客戶端有些操作步驟是相同的,因此封裝的功能函數(shù)是可以共用的,相關(guān)的通信函數(shù)聲明如下:
/////////////////////////////////////////////////// ////////////////////服務(wù)器/////////////////////// /////////////////////////////////////////////////// intbindSocket(intlfd,unsignedshortport); intsetListen(intlfd); intacceptConn(intlfd,structsockaddr_in*addr); /////////////////////////////////////////////////// ////////////////////客戶端/////////////////////// /////////////////////////////////////////////////// intconnectToHost(intfd,constchar*ip,unsignedshortport); /////////////////////////////////////////////////// /////////////////////共用//////////////////////// /////////////////////////////////////////////////// intcreateSocket(); intsendMsg(intfd,constchar*msg); intrecvMsg(intfd,char*msg,intsize); intcloseSocket(intfd); intreadn(intfd,char*buf,intsize); intwriten(intfd,constchar*msg,intsize);
關(guān)于函數(shù) readn() 和 writen() 的作用請參考TCP數(shù)據(jù)粘包的處理
1.2 函數(shù)定義
//創(chuàng)建監(jiān)套接字
intcreateSocket()
{
intfd=socket(AF_INET,SOCK_STREAM,0);
if(fd==-1)
{
perror("socket");
return-1;
}
printf("套接字創(chuàng)建成功,fd=%d
",fd);
returnfd;
}
//綁定本地的IP和端口
intbindSocket(intlfd,unsignedshortport)
{
structsockaddr_insaddr;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(port);
saddr.sin_addr.s_addr=INADDR_ANY;//0=0.0.0.0
intret=bind(lfd,(structsockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("bind");
return-1;
}
printf("套接字綁定成功,ip:%s,port:%d
",
inet_ntoa(saddr.sin_addr),port);
returnret;
}
//設(shè)置監(jiān)聽
intsetListen(intlfd)
{
intret=listen(lfd,128);
if(ret==-1)
{
perror("listen");
return-1;
}
printf("設(shè)置監(jiān)聽成功...
");
returnret;
}
//阻塞并等待客戶端的連接
intacceptConn(intlfd,structsockaddr_in*addr)
{
intcfd=-1;
if(addr==NULL)
{
cfd=accept(lfd,NULL,NULL);
}
else
{
intaddrlen=sizeof(structsockaddr_in);
cfd=accept(lfd,(structsockaddr*)addr,&addrlen);
}
if(cfd==-1)
{
perror("accept");
return-1;
}
printf("成功和客戶端建立連接...
");
returncfd;
}
//接收數(shù)據(jù)
intrecvMsg(intcfd,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
",?len);
????//?根據(jù)讀出的長度分配內(nèi)存
????char?*buf?=?(char*)malloc(len+1);
????int?ret?=?readn(cfd,?buf,?len);
????if(ret?!=?len)
????{
????????return?-1;
????}
????buf[len]?=?'?';
????*msg?=?buf;
????return?ret;
}
//?發(fā)送數(shù)據(jù)
int?sendMsg(int?cfd,?char*?msg,?int?len)
{
???if(msg?==?NULL?||?len?<=?0)
???{
???????return?-1;
???}
???//?申請內(nèi)存空間:?數(shù)據(jù)長度?+?包頭4字節(jié)(存儲數(shù)據(jù)長度)
???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ù)器建立連接...
");
????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;
}
elseif(nread==-1)
{
return-1;
}
}
returnsize;
}
//發(fā)送指定的字節(jié)數(shù)
//函數(shù)調(diào)用成功返回size
intwriten(intfd,constchar*msg,intsize)
{
intleft=size;
intnwrite=0;
constchar*p=msg;
while(left>0)
{
if((nwrite=write(fd,msg,left))>0)
{
p+=nwrite;
left-=nwrite;
}
elseif(nwrite==-1)
{
return-1;
}
}
returnsize;
}
2. 基于 C++ 的封裝
編寫 C++ 程序應當遵循面向?qū)ο笕兀悍庋b、繼承、多態(tài)。簡單地說就是封裝之后的類可以隱藏掉某些屬性使操作更簡單并且類的功能要單一,如果要代碼重用可以進行類之間的繼承,如果要讓函數(shù)的使用更加靈活可以使用多態(tài)。因此,我們需要封裝兩個類:客戶端類和服務(wù)器端的類。
2.1 版本 1
根據(jù)面向?qū)ο蟮乃枷?,整個通信過程不管是監(jiān)聽還是通信的套接字都是可以封裝到類的內(nèi)部并且將其隱藏掉,這樣相關(guān)操作函數(shù)的參數(shù)也就隨之減少了,使用者用起來也更簡便。
2.1.1 客戶端
classTcpClient
{
public:
TcpClient();
~TcpClient();
//intconnectToHost(intfd,constchar*ip,unsignedshortport);
intconnectToHost(stringip,unsignedshortport);
//intsendMsg(intfd,constchar*msg);
intsendMsg(stringmsg);
//intrecvMsg(intfd,char*msg,intsize);
stringrecvMsg();
//intcreateSocket();
//intcloseSocket(intfd);
private:
//intreadn(intfd,char*buf,intsize);
intreadn(char*buf,intsize);
//intwriten(intfd,constchar*msg,intsize);
intwriten(constchar*msg,intsize);
private:
intcfd;//通信的套接字
};
通過對客戶端的操作進行封裝,我們可以看到有如下的變化:
文件描述被隱藏了,封裝到了類的內(nèi)部已經(jīng)無法進行外部訪問
功能函數(shù)的參數(shù)變少了,因為類成員函數(shù)可以直接使用類內(nèi)部的成員變量。
創(chuàng)建和銷毀套接字的函數(shù)去掉了,這兩個操作可以分別放到構(gòu)造和析構(gòu)函數(shù)內(nèi)部進行處理。
在 C++ 中可以適當?shù)膶?char* 替換為 string 類,這樣操作字符串就更簡便一些。
2.1.2 服務(wù)器端
classTcpServer
{
public:
TcpServer();
~TcpServer();
//intbindSocket(intlfd,unsignedshortport)+intsetListen(intlfd)
intsetListen(unsignedshortport);
//intacceptConn(intlfd,structsockaddr_in*addr);
intacceptConn(structsockaddr_in*addr);
//intsendMsg(intfd,constchar*msg);
intsendMsg(stringmsg);
//intrecvMsg(intfd,char*msg,intsize);
stringrecvMsg();
//intcreateSocket();
//intcloseSocket(intfd);
private:
//intreadn(intfd,char*buf,intsize);
intreadn(char*buf,intsize);
//intwriten(intfd,constchar*msg,intsize);
intwriten(constchar*msg,intsize);
private:
intlfd;//監(jiān)聽的套接字
intcfd;//通信的套接字
};
通過對服務(wù)器端的操作進行封裝,我們可以看到這個類和客戶端的類結(jié)構(gòu)以及封裝思路是差不多的,并且兩個類的內(nèi)部有些操作的重疊的:接收和發(fā)送通信數(shù)據(jù)的函數(shù) recvMsg()、sendMsg(),以及內(nèi)部函數(shù) readn()、writen()。不僅如此服務(wù)器端的類設(shè)計成這樣樣子是有缺陷的:服務(wù)器端一般需要和多個客戶端建立連接,因此通信的套接字就需要有 N 個,但是在上面封裝的類里邊只有一個。
既然如此,我們?nèi)绾谓鉀Q服務(wù)器和客戶端的代碼冗余和服務(wù)器不能跟多客戶端通信的問題呢?
答:瘦身、減負。可以將服務(wù)器的通信功能去掉,只留下監(jiān)聽并建立新連接一個功能。將客戶端類變成一個專門用于套接字通信的類即可。服務(wù)器端整個流程使用服務(wù)器類 + 通信類來處理;客戶端整個流程通過通信的類來處理。
2.2 版本 2
根據(jù)對第一個版本的分析,可以對以上代碼做如下修改:
2.2.1 通信類
套接字通信類既可以在客戶端使用,也可以在服務(wù)器端使用,職責是接收和發(fā)送數(shù)據(jù)包。
類聲明
classTcpSocket
{
public:
TcpSocket();
TcpSocket(intsocket);
~TcpSocket();
intconnectToHost(stringip,unsignedshortport);
intsendMsg(stringmsg);
stringrecvMsg();
private:
intreadn(char*buf,intsize);
intwriten(constchar*msg,intsize);
private:
intm_fd;//通信的套接字
};
類定義
TcpSocket::TcpSocket()
{
m_fd=socket(AF_INET,SOCK_STREAM,0);
}
TcpSocket::TcpSocket(intsocket)
{
m_fd=socket;
}
TcpSocket::~TcpSocket()
{
if(m_fd>0)
{
close(m_fd);
}
}
intTcpSocket::connectToHost(stringip,unsignedshortport)
{
//連接服務(wù)器IPport
structsockaddr_insaddr;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(port);
inet_pton(AF_INET,ip.data(),&saddr.sin_addr.s_addr);
intret=connect(m_fd,(structsockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("connect");
return-1;
}
cout<"成功和服務(wù)器建立連接..."?<0)
{
if((nread=read(m_fd,p,left))>0)
{
p+=nread;
left-=nread;
}
elseif(nread==-1)
{
return-1;
}
}
returnsize;
}
intTcpSocket::writen(constchar*msg,intsize)
{
intleft=size;
intnwrite=0;
constchar*p=msg;
while(left>0)
{
if((nwrite=write(m_fd,msg,left))>0)
{
p+=nwrite;
left-=nwrite;
}
elseif(nwrite==-1)
{
return-1;
}
}
returnsize;
}
在第二個版本的套接字通信類中一共有兩個構(gòu)造函數(shù):
TcpSocket::TcpSocket()
{
m_fd=socket(AF_INET,SOCK_STREAM,0);
}
TcpSocket::TcpSocket(intsocket)
{
m_fd=socket;
}
其中無參構(gòu)造一般在客戶端使用,通過這個套接字對象再和服務(wù)器進行連接,之后就可以通信了
有參構(gòu)造主要在服務(wù)器端使用,當服務(wù)器端得到了一個用于通信的套接字對象之后,就可以基于這個套接字直接通信,因此不需要再次進行連接操作。
2.2.2 服務(wù)器類
服務(wù)器類主要用于套接字通信的服務(wù)器端,并且沒有通信能力,當服務(wù)器和客戶端的新連接建立之后,需要通過 TcpSocket 類的帶參構(gòu)造將通信的描述符包裝成一個通信對象,這樣就可以使用這個對象和客戶端通信了。
類聲明
classTcpServer
{
public:
TcpServer();
~TcpServer();
intsetListen(unsignedshortport);
TcpSocket*acceptConn(structsockaddr_in*addr=nullptr);
private:
intm_fd;//監(jiān)聽的套接字
};
類定義
TcpServer::TcpServer()
{
m_fd=socket(AF_INET,SOCK_STREAM,0);
}
TcpServer::~TcpServer()
{
close(m_fd);
}
intTcpServer::setListen(unsignedshortport)
{
structsockaddr_insaddr;
saddr.sin_family=AF_INET;
saddr.sin_port=htons(port);
saddr.sin_addr.s_addr=INADDR_ANY;//0=0.0.0.0
intret=bind(m_fd,(structsockaddr*)&saddr,sizeof(saddr));
if(ret==-1)
{
perror("bind");
return-1;
}
cout<"套接字綁定成功,?ip:?"
????????<
通過調(diào)整可以發(fā)現(xiàn),套接字服務(wù)器類功能更加單一了,這樣設(shè)計即解決了代碼冗余問題,還能使這兩個類更容易維護。
3. 測試代碼
3.1 客戶端
intmain()
{
//1.創(chuàng)建通信的套接字
TcpSockettcp;
//2.連接服務(wù)器IPport
intret=tcp.connectToHost("192.168.237.131",10000);
if(ret==-1)
{
return-1;
}
//3.通信
intfd1=open("english.txt",O_RDONLY);
intlength=0;
chartmp[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:?"?<
3.2 服務(wù)器端
structSockInfo
{
TcpServer*s;
TcpSocket*tcp;
structsockaddr_inaddr;
};
void*working(void*arg)
{
structSockInfo*pinfo=static_cast(arg);
//連接建立成功,打印客戶端的IP和端口信息
charip[32];
printf("客戶端的IP:%s,端口:%d
",
inet_ntop(AF_INET,&pinfo->addr.sin_addr.s_addr,ip,sizeof(ip)),
ntohs(pinfo->addr.sin_port));
//5.通信
while(1)
{
printf("接收數(shù)據(jù):.....
");
stringmsg=pinfo->tcp->recvMsg();
if(!msg.empty())
{
cout<tcp;
deletepinfo;
returnnullptr;
}
intmain()
{
//1.創(chuàng)建監(jiān)聽的套接字
TcpServers;
//2.綁定本地的IPport并設(shè)置監(jiān)聽
s.setListen(10000);
//3.阻塞并等待客戶端的連接
while(1)
{
SockInfo*info=newSockInfo;
TcpSocket*tcp=s.acceptConn(&info->addr);
if(tcp==nullptr)
{
cout<"重試...."?<s=&s;
info->tcp=tcp;
pthread_create(&tid,NULL,working,info);
pthread_detach(tid);
}
return0;
}
審核編輯:湯梓紅
-
封裝
+關(guān)注
關(guān)注
128文章
9040瀏覽量
147563 -
C語言
+關(guān)注
關(guān)注
183文章
7636瀏覽量
144275 -
C++
+關(guān)注
關(guān)注
22文章
2120瀏覽量
76609 -
面向?qū)ο?/span>
+關(guān)注
關(guān)注
0文章
64瀏覽量
10210
發(fā)布評論請先 登錄
C語言實現(xiàn)面向對象的方式 C++中的class的運行原理
基于C/C++面向對象的方式封裝socket通信類流程簡析
C++筆記004:C++類通俗點說—— C結(jié)構(gòu)體復習
STM32 C++代碼封裝初探相關(guān)資料推薦
面向對象程序設(shè)計—C++語言描述_PDF版
C++課程資料詳細資料合集包括了:面向對象程序設(shè)計與C++,算法,函數(shù)等
C++語言和面向對象程序設(shè)計教程
STM32 C++編程系列二:STM32 C++代碼封裝初探

基于C/C++面向?qū)ο蟮姆绞椒庋bsocket通信類
評論