现在的位置: 首页 > 综合 > 正文

封装 WinSock C/S 通信

2018年02月24日 ⁄ 综合 ⁄ 共 4466字 ⁄ 字号 评论关闭

  最近在做一个小东西的时候又用上了TCP通信,之前用VB做过,用java做过,用MFC的CSocket也做过,不敢说精通此道,但体会还是有的,这次打算用C++与WinSock来实现一个DLL,这样不论是MFC程序还是非MFC程序都能用上。一直在用别人造的轮子,我也来试着造个轮子出来。

  想封装一下WinSock的一大原因就是WinSock的调用太繁琐了,对于TCP通信来说最重要的不就是IP地址和端口号吗,结果写个简单的示例程序都有那么一大坨在各种初始化。因此我的想法是:

  • 服务器,可以直接传入端口号后listen。因此,封装好的Server类提供了如下成员函数:
    bool listen(unsigned short port);
  • 客户端,可以直接传入ip地址和端口号后connect。因此,封装好的Client类提供了如下成员函数:
    bool init(const char *ip, unsigned short port);

  当然,如果只是提供这些操作的话,未免也太简单了点,java、MFC 中的类也都封装到这个程度了,而且这个实现起来一点难度都没有。然后我以一个使用者的角度来思考了一下:一般我们在建立TCP连接后,会启动一个工作线程,在工作线程中以阻塞模式的recv来接收对方发过来的数据。创建线程其实也挺繁琐的,那为什么不在init函数的最后自动启动一个接收线程呢?与此类似,accept操作也是阻塞模式的,何不自动启动一个工作线程来accept连接呢?由此,我又做了如下改动:

  • Client中init函数的最后启动了一个线程,并把Client的this指针做为线程的参数传了进去,线程中recv函数拿到数据后调用Client对象的
    virtual void onRecv(int len, const char *buf);
    成员函数。
  • Server中listen函数的最后启动了一个线程,并把Server的this指针做为线程的参数传了进去,线程中accept函数接收一个连接之后调用Server对象的virtual void onAccept(SOCKET socket, sockaddr_in *remote)=0;成员函数。

  这样做了之后,已经大大提升了可用性了,不过MFC的异步通信也封装到了这个程度,不行,我还得再想个法子来超越它。
我又想到以前做客户端/服务器程序的时候,因为send和recv是发送和接收字节数组的,一般我们还要自己来规范一下字节数组的格式,比如某个字节代表的是包的类型,某几个字节代表的是包的长度,也就是在TCP上又定义一层协议
。比较麻烦的是recv接收到的时候我们可能一次接收到了几个包,又有可能接收到了一个不完整的包,又或者传输过程中某些字节出错了(说TCP是可靠通信,我是从来不信的),所以在接收到数据的时候我们需要分析收到的数据,提取出完整、正确的包后再进行下一步操作,所以我实现了一个简单的美其名曰“参数交换协议”的东东:

  • 发送端通过Client的bool sendInts(int type, int n, ...); 或者bool sendIntArray(int type, int n, const int* array); 发送包含n个整型参数的一个包给对方,包中还搭载了包类型type、参数个数n、n取非后的校验。
  • 接收端通过Client的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

抱歉!评论已关闭.