最近在做一个小东西的时候又用上了TCP通信,之前用VB做过,用java做过,用MFC的CSocket也做过,不敢说精通此道,但体会还是有的,这次打算用C++与WinSock来实现一个DLL,这样不论是MFC程序还是非MFC程序都能用上。一直在用别人造的轮子,我也来试着造个轮子出来。
想封装一下WinSock的一大原因就是WinSock的调用太繁琐了,对于TCP通信来说最重要的不就是IP地址和端口号吗,结果写个简单的示例程序都有那么一大坨在各种初始化。因此我的想法是:
bool listen(unsigned short port);
bool init(const char *ip, unsigned short port);
当然,如果只是提供这些操作的话,未免也太简单了点,java、MFC 中的类也都封装到这个程度了,而且这个实现起来一点难度都没有。然后我以一个使用者的角度来思考了一下:一般我们在建立TCP连接后,会启动一个工作线程,在工作线程中以阻塞模式的recv来接收对方发过来的数据。创建线程其实也挺繁琐的,那为什么不在init函数的最后自动启动一个接收线程呢?与此类似,accept操作也是阻塞模式的,何不自动启动一个工作线程来accept连接呢?由此,我又做了如下改动:
virtual void onRecv(int len, const char *buf);
成员函数。virtual void onAccept(SOCKET socket, sockaddr_in *remote)=0;
成员函数。
这样做了之后,已经大大提升了可用性了,不过MFC的异步通信也封装到了这个程度,不行,我还得再想个法子来超越它。 我又想到以前做客户端/服务器程序的时候,因为send和recv是发送和接收字节数组的,一般我们还要自己来规范一下字节数组的格式,比如某个字节代表的是包的类型,某几个字节代表的是包的长度,也就是在TCP上又定义一层协议。比较麻烦的是recv接收到的时候我们可能一次接收到了几个包,又有可能接收到了一个不完整的包,又或者传输过程中某些字节出错了(说TCP是可靠通信,我是从来不信的),所以在接收到数据的时候我们需要分析收到的数据,提取出完整、正确的包后再进行下一步操作,所以我实现了一个简单的美其名曰“参数交换协议”的东东:
bool sendInts(int type, int n, ...);
或者bool sendIntArray(int type, int n, const int* array);
发送包含n个整型参数的一个包给对方,包中还搭载了包类型type、参数个数n、n取非后的校验。virtual void onRecv(int type, const int *buf)=0;
接收到一个完整、"正确"的包(由于只校验了参数个数n,不敢保证完全正确)。
这样做了之后客户端的Client和服务器端的Client不用考虑怎么发送接收数据了,只要考虑接收到数据后做什么就可以了。
下面是个示例程序,在同一程序中实现了客户端/服务器,它们都对接收到的数据加1后发送给对方:
示例程序的源码如下:
#include "../ServerClient/ServerClient.h" #include <iostream> using namespace std; Client *demoClient; Server *demoServer; Client *demoServerClient; class DemoClient : public Client { protected: void onRecv(int type, const int *buf) { switch(type) { case 0: cout<<"client received : "<<buf[0]<<endl; if (buf[0] > 10) { closesocket(mSocket); return; } Sleep(1000); sendInts(0, 1, buf[0] + 1); break; } } }; class DemoServer : public Server { protected: void onAccept(SOCKET socket, sockaddr_in *remote) { demoServerClient->init(socket); } }; class DemoServerClient : public Client { protected: void onRecv(int type, const int *buf) { switch(type) { case 0: cout<<"serverclient received : "<<buf[0]<<endl; Sleep(1000); sendInts(0, 1, buf[0] + 1); break; } } }; int main() { const int port = 3000; demoServer = new DemoServer(); demoClient = new DemoClient(); demoServerClient = new DemoServerClient(); demoServer->listen(port); demoClient->init("127.0.0.1", port); demoClient->sendInts(0, 1, 0); demoClient->waitForClose(); delete demoServer; delete demoClient; delete demoServerClient; return 0; }
Server 和 Client 的声明在ServerClient.h中:
#ifndef _SERVERCLIENT_ #define _SERVERCLIENT_ #pragma comment(lib, "WS2_32.lib") #include <winsock2.h> #include <windows.h> #ifdef _CSREALISE_ #define EXPORT_CLASS __declspec(dllexport) #else #define EXPORT_CLASS __declspec(dllimport) #endif // 越界会抛异常的字节数组, 客户端使用它来缓存、分析收到的数据 class EXPORT_CLASS BoundArray { public: BoundArray(); ~BoundArray(); void append(int len, const char* buf); void removeHead(int size); // 删除头size个字节, 后边的数据前移 int getSize() {return mSize;} void* at(int pos) {return mBuffer + pos;} // 以下函数中会调用 inBound char pollchar(int& pos); short pollshort(int& pos); int pollint(int& pos); private: void inBound(int pos); // mBuffer[pos]会不会溢出? 溢出会抛出int类型的异常 char *mBuffer; int mSize; int mBufferSize; }; // 客户端的声明 class EXPORT_CLASS Client { public: Client(); // 这里会初始化mSocket ~Client(); // 这里会close bool init(const char *ip, unsigned short port); // 这里调用下面的init方法 bool init(unsigned long addr, unsigned short port); // addr是网络字节序, 这里会connect, 并调用init(mSocket) bool init(SOCKET socket); // 这里会调用 startRecvThread void close(); // 关闭 SOCKET 后 waitForClose void waitForClose(); // 这里会等待接收线程结束 bool sendInts(int type, int n, ...); // 最后是n个int, 调用sendIntArray来实现发送 bool sendIntArray(int type, int n, const int* array); protected: virtual void onRecv(int type, const int *buf)=0;// 用户来实现 virtual void onRecv(int len, const char *buf); // 这里会调用 onRecv(int, const int*) virtual void afterClose(){}; private: bool startRecvThread(); // 这里会启动一个接收线程 friend DWORD WINAPI RecvThread(LPVOID client); // 这里会调用 onRecv(int, const char*), mSocket关闭后线程退出前调用afterClose HANDLE mThread; BoundArray mBuffer; protected: SOCKET mSocket; }; // 服务器的声明 class EXPORT_CLASS Server { public: Server(); // 创建SOCKET ~Server(); // close bool listen(unsigned short port); // startAcceptThread void close(); // 关闭 SOCKET 后 waitForClose void waitForClose(); // 这里会等待Accept线程结束 protected: virtual void onAccept(SOCKET socket, sockaddr_in *remote)=0;// 用户来实现 virtual void afterClose(){}; private: bool startAcceptThread(); friend DWORD WINAPI AcceptThread(LPVOID server); // 这里会调用 onAccept HANDLE mThread; SOCKET mListen; }; #endif
最后,源码已托管到github:
https://github.com/1184893257/ServerClient