正文
在掌握了基于 TCP 的套接字通信流程之后,為了方便使用,提高編碼效率,可以對(duì)通信操作進(jìn)行封裝,本著有淺入深的原則,先基于 C 語(yǔ)言進(jìn)行面向過(guò)程的函數(shù)封裝,然后再基于 C++ 進(jìn)行面向?qū)ο蟮念?lèi)封裝。
1. 基于 C 語(yǔ)言的封裝
基于 TCP 的套接字通信分為兩部分:服務(wù)器端通信和客戶(hù)端通信。我們只要掌握了通信流程,封裝出對(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)
等待并接受新的客戶(hù)端連接,連接建立得到用于通信的套接字和客戶(hù)端的 IP、端口信息
使用得到的通信的套接字和客戶(hù)端通信(接收和發(fā)送數(shù)據(jù))
通信結(jié)束,關(guān)閉套接字(監(jiān)聽(tīng) + 通信)
客戶(hù)端
創(chuàng)建用于通信的套接字
使用服務(wù)器端綁定的 IP 和端口連接服務(wù)器
使用通信的套接字和服務(wù)器通信(發(fā)送和接收數(shù)據(jù))
通信結(jié)束,關(guān)閉套接字(通信)
1.1 函數(shù)聲明
通過(guò)通信流程可以看出服務(wù)器和客戶(hù)端有些操作步驟是相同的,因此封裝的功能函數(shù)是可以共用的,相關(guān)的通信函數(shù)聲明如下:
/////////////////////////////////////////////////// ////////////////////服務(wù)器/////////////////////// /////////////////////////////////////////////////// intbindSocket(intlfd,unsignedshortport); intsetListen(intlfd); intacceptConn(intlfd,structsockaddr_in*addr); /////////////////////////////////////////////////// ////////////////////客戶(hù)端/////////////////////// /////////////////////////////////////////////////// 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() 的作用請(qǐng)參考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)聽(tīng) intsetListen(intlfd) { intret=listen(lfd,128); if(ret==-1) { perror("listen"); return-1; } printf("設(shè)置監(jiān)聽(tīng)成功... "); returnret; } //阻塞并等待客戶(hù)端的連接 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("成功和客戶(hù)端建立連接... "); 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ù)讀出的長(zhǎng)度分配內(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; ???} ???//?申請(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ù)器建立連接... "); ????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++ 的封裝
編寫(xiě) C++ 程序應(yīng)當(dāng)遵循面向?qū)ο笕兀悍庋b、繼承、多態(tài)。簡(jiǎn)單地說(shuō)就是封裝之后的類(lèi)可以隱藏掉某些屬性使操作更簡(jiǎn)單并且類(lèi)的功能要單一,如果要代碼重用可以進(jìn)行類(lèi)之間的繼承,如果要讓函數(shù)的使用更加靈活可以使用多態(tài)。因此,我們需要封裝兩個(gè)類(lèi):客戶(hù)端類(lèi)和服務(wù)器端的類(lèi)。
2.1 版本 1
根據(jù)面向?qū)ο蟮乃枷耄麄€(gè)通信過(guò)程不管是監(jiān)聽(tīng)還是通信的套接字都是可以封裝到類(lèi)的內(nèi)部并且將其隱藏掉,這樣相關(guān)操作函數(shù)的參數(shù)也就隨之減少了,使用者用起來(lái)也更簡(jiǎn)便。
2.1.1 客戶(hù)端
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;//通信的套接字 };
通過(guò)對(duì)客戶(hù)端的操作進(jìn)行封裝,我們可以看到有如下的變化:
文件描述被隱藏了,封裝到了類(lèi)的內(nèi)部已經(jīng)無(wú)法進(jìn)行外部訪問(wèn)
功能函數(shù)的參數(shù)變少了,因?yàn)轭?lèi)成員函數(shù)可以直接使用類(lèi)內(nèi)部的成員變量。
創(chuàng)建和銷(xiāo)毀套接字的函數(shù)去掉了,這兩個(gè)操作可以分別放到構(gòu)造和析構(gòu)函數(shù)內(nèi)部進(jìn)行處理。
在 C++ 中可以適當(dāng)?shù)膶?char* 替換為 string 類(lèi),這樣操作字符串就更簡(jiǎn)便一些。
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)聽(tīng)的套接字 intcfd;//通信的套接字 };
通過(guò)對(duì)服務(wù)器端的操作進(jìn)行封裝,我們可以看到這個(gè)類(lèi)和客戶(hù)端的類(lèi)結(jié)構(gòu)以及封裝思路是差不多的,并且兩個(gè)類(lèi)的內(nèi)部有些操作的重疊的:接收和發(fā)送通信數(shù)據(jù)的函數(shù) recvMsg()、sendMsg(),以及內(nèi)部函數(shù) readn()、writen()。不僅如此服務(wù)器端的類(lèi)設(shè)計(jì)成這樣樣子是有缺陷的:服務(wù)器端一般需要和多個(gè)客戶(hù)端建立連接,因此通信的套接字就需要有 N 個(gè),但是在上面封裝的類(lèi)里邊只有一個(gè)。
既然如此,我們?nèi)绾谓鉀Q服務(wù)器和客戶(hù)端的代碼冗余和服務(wù)器不能跟多客戶(hù)端通信的問(wèn)題呢?
答:瘦身、減負(fù)??梢詫⒎?wù)器的通信功能去掉,只留下監(jiān)聽(tīng)并建立新連接一個(gè)功能。將客戶(hù)端類(lèi)變成一個(gè)專(zhuān)門(mén)用于套接字通信的類(lèi)即可。服務(wù)器端整個(gè)流程使用服務(wù)器類(lèi) + 通信類(lèi)來(lái)處理;客戶(hù)端整個(gè)流程通過(guò)通信的類(lèi)來(lái)處理。
2.2 版本 2
根據(jù)對(duì)第一個(gè)版本的分析,可以對(duì)以上代碼做如下修改:
2.2.1 通信類(lèi)
套接字通信類(lèi)既可以在客戶(hù)端使用,也可以在服務(wù)器端使用,職責(zé)是接收和發(fā)送數(shù)據(jù)包。
類(lèi)聲明
classTcpSocket { public: TcpSocket(); TcpSocket(intsocket); ~TcpSocket(); intconnectToHost(stringip,unsignedshortport); intsendMsg(stringmsg); stringrecvMsg(); private: intreadn(char*buf,intsize); intwriten(constchar*msg,intsize); private: intm_fd;//通信的套接字 };
類(lèi)定義
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è)版本的套接字通信類(lèi)中一共有兩個(gè)構(gòu)造函數(shù):
TcpSocket::TcpSocket() { m_fd=socket(AF_INET,SOCK_STREAM,0); } TcpSocket::TcpSocket(intsocket) { m_fd=socket; }
其中無(wú)參構(gòu)造一般在客戶(hù)端使用,通過(guò)這個(gè)套接字對(duì)象再和服務(wù)器進(jìn)行連接,之后就可以通信了
有參構(gòu)造主要在服務(wù)器端使用,當(dāng)服務(wù)器端得到了一個(gè)用于通信的套接字對(duì)象之后,就可以基于這個(gè)套接字直接通信,因此不需要再次進(jìn)行連接操作。
2.2.2 服務(wù)器類(lèi)
服務(wù)器類(lèi)主要用于套接字通信的服務(wù)器端,并且沒(méi)有通信能力,當(dāng)服務(wù)器和客戶(hù)端的新連接建立之后,需要通過(guò) TcpSocket 類(lèi)的帶參構(gòu)造將通信的描述符包裝成一個(gè)通信對(duì)象,這樣就可以使用這個(gè)對(duì)象和客戶(hù)端通信了。
類(lèi)聲明
classTcpServer { public: TcpServer(); ~TcpServer(); intsetListen(unsignedshortport); TcpSocket*acceptConn(structsockaddr_in*addr=nullptr); private: intm_fd;//監(jiān)聽(tīng)的套接字 };
類(lèi)定義
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:?" ????????<
通過(guò)調(diào)整可以發(fā)現(xiàn),套接字服務(wù)器類(lèi)功能更加單一了,這樣設(shè)計(jì)即解決了代碼冗余問(wèn)題,還能使這兩個(gè)類(lèi)更容易維護(hù)。
3. 測(cè)試代碼
3.1 客戶(hù)端
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); //連接建立成功,打印客戶(hù)端的IP和端口信息 charip[32]; printf("客戶(hù)端的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)聽(tīng)的套接字 TcpServers; //2.綁定本地的IPport并設(shè)置監(jiān)聽(tīng) s.setListen(10000); //3.阻塞并等待客戶(hù)端的連接 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)注
126文章
7852瀏覽量
142856 -
C語(yǔ)言
+關(guān)注
關(guān)注
180文章
7604瀏覽量
136556 -
C++
+關(guān)注
關(guān)注
22文章
2107瀏覽量
73587 -
面向?qū)ο?/span>
+關(guān)注
關(guān)注
0文章
64瀏覽量
9981
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論