正文
在掌握了基于 TCP 的套接字通信流程之后,為了方便使用,提高編碼效率,可以對通信操作進行封裝,本著有淺入深的原則,先基于 C 語言進行面向過程的函數封裝,然后再基于 C++ 進行面向對象的類封裝。
1. 基于 C 語言的封裝
基于 TCP 的套接字通信分為兩部分:服務器端通信和客戶端通信。我們只要掌握了通信流程,封裝出對應的功能函數也就不在話下了,先來回顧一下通信流程:
服務器端
創建用于監聽的套接字
將用于監聽的套接字和本地的 IP 以及端口進行綁定
啟動監聽
等待并接受新的客戶端連接,連接建立得到用于通信的套接字和客戶端的 IP、端口信息
使用得到的通信的套接字和客戶端通信(接收和發送數據)
通信結束,關閉套接字(監聽 + 通信)
客戶端
創建用于通信的套接字
使用服務器端綁定的 IP 和端口連接服務器
使用通信的套接字和服務器通信(發送和接收數據)
通信結束,關閉套接字(通信)
1.1 函數聲明
通過通信流程可以看出服務器和客戶端有些操作步驟是相同的,因此封裝的功能函數是可以共用的,相關的通信函數聲明如下:
/////////////////////////////////////////////////// ////////////////////服務器/////////////////////// /////////////////////////////////////////////////// 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);
關于函數 readn() 和 writen() 的作用請參考TCP數據粘包的處理
1.2 函數定義
//創建監套接字 intcreateSocket() { intfd=socket(AF_INET,SOCK_STREAM,0); if(fd==-1) { perror("socket"); return-1; } printf("套接字創建成功,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; } //設置監聽 intsetListen(intlfd) { intret=listen(lfd,128); if(ret==-1) { perror("listen"); return-1; } printf("設置監聽成功... "); 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; } //接收數據 intrecvMsg(intcfd,char**msg) { if(msg==NULL||cfd<=?0) ????{ ????????return?-1; ????} ????//?接收數據 ????//?1.?讀數據頭 ????int?len?=?0; ????readn(cfd,?(char*)&len,?4); ????len?=?ntohl(len); ????printf("數據塊大小:?%d ",?len); ????//?根據讀出的長度分配內存 ????char?*buf?=?(char*)malloc(len+1); ????int?ret?=?readn(cfd,?buf,?len); ????if(ret?!=?len) ????{ ????????return?-1; ????} ????buf[len]?=?'?'; ????*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?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("成功和服務器建立連接... "); ????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; } elseif(nread==-1) { return-1; } } returnsize; } //發送指定的字節數 //函數調用成功返回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++ 程序應當遵循面向對象三要素:封裝、繼承、多態。簡單地說就是封裝之后的類可以隱藏掉某些屬性使操作更簡單并且類的功能要單一,如果要代碼重用可以進行類之間的繼承,如果要讓函數的使用更加靈活可以使用多態。因此,我們需要封裝兩個類:客戶端類和服務器端的類。
2.1 版本 1
根據面向對象的思想,整個通信過程不管是監聽還是通信的套接字都是可以封裝到類的內部并且將其隱藏掉,這樣相關操作函數的參數也就隨之減少了,使用者用起來也更簡便。
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;//通信的套接字 };
通過對客戶端的操作進行封裝,我們可以看到有如下的變化:
文件描述被隱藏了,封裝到了類的內部已經無法進行外部訪問
功能函數的參數變少了,因為類成員函數可以直接使用類內部的成員變量。
創建和銷毀套接字的函數去掉了,這兩個操作可以分別放到構造和析構函數內部進行處理。
在 C++ 中可以適當的將 char* 替換為 string 類,這樣操作字符串就更簡便一些。
2.1.2 服務器端
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;//監聽的套接字 intcfd;//通信的套接字 };
通過對服務器端的操作進行封裝,我們可以看到這個類和客戶端的類結構以及封裝思路是差不多的,并且兩個類的內部有些操作的重疊的:接收和發送通信數據的函數 recvMsg()、sendMsg(),以及內部函數 readn()、writen()。不僅如此服務器端的類設計成這樣樣子是有缺陷的:服務器端一般需要和多個客戶端建立連接,因此通信的套接字就需要有 N 個,但是在上面封裝的類里邊只有一個。
既然如此,我們如何解決服務器和客戶端的代碼冗余和服務器不能跟多客戶端通信的問題呢?
答:瘦身、減負。可以將服務器的通信功能去掉,只留下監聽并建立新連接一個功能。將客戶端類變成一個專門用于套接字通信的類即可。服務器端整個流程使用服務器類 + 通信類來處理;客戶端整個流程通過通信的類來處理。
2.2 版本 2
根據對第一個版本的分析,可以對以上代碼做如下修改:
2.2.1 通信類
套接字通信類既可以在客戶端使用,也可以在服務器端使用,職責是接收和發送數據包。
類聲明
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) { //連接服務器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<"成功和服務器建立連接..."?<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; }
在第二個版本的套接字通信類中一共有兩個構造函數:
TcpSocket::TcpSocket() { m_fd=socket(AF_INET,SOCK_STREAM,0); } TcpSocket::TcpSocket(intsocket) { m_fd=socket; }
其中無參構造一般在客戶端使用,通過這個套接字對象再和服務器進行連接,之后就可以通信了
有參構造主要在服務器端使用,當服務器端得到了一個用于通信的套接字對象之后,就可以基于這個套接字直接通信,因此不需要再次進行連接操作。
2.2.2 服務器類
服務器類主要用于套接字通信的服務器端,并且沒有通信能力,當服務器和客戶端的新連接建立之后,需要通過 TcpSocket 類的帶參構造將通信的描述符包裝成一個通信對象,這樣就可以使用這個對象和客戶端通信了。
類聲明
classTcpServer { public: TcpServer(); ~TcpServer(); intsetListen(unsignedshortport); TcpSocket*acceptConn(structsockaddr_in*addr=nullptr); private: intm_fd;//監聽的套接字 };
類定義
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:?" ????????<
通過調整可以發現,套接字服務器類功能更加單一了,這樣設計即解決了代碼冗余問題,還能使這兩個類更容易維護。
3. 測試代碼
3.1 客戶端
intmain() { //1.創建通信的套接字 TcpSockettcp; //2.連接服務器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) { //發送數據 tcp.sendMsg(string(tmp,length)); cout<"send?Msg:?"?<
3.2 服務器端
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("接收數據:..... "); stringmsg=pinfo->tcp->recvMsg(); if(!msg.empty()) { cout<tcp; deletepinfo; returnnullptr; } intmain() { //1.創建監聽的套接字 TcpServers; //2.綁定本地的IPport并設置監聽 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; }
審核編輯:湯梓紅
-
封裝
+關注
關注
125文章
7592瀏覽量
142138 -
C語言
+關注
關注
180文章
7575瀏覽量
134024 -
C++
+關注
關注
21文章
2085瀏覽量
73301 -
面向對象
+關注
關注
0文章
64瀏覽量
9961
發布評論請先 登錄
相關推薦
評論