zoukankan      html  css  js  c++  java
  • 一个简单的Windows Socket可复用框架

    一个简单的Windows Socket可复用框架

     

    说起网络编程,无非是建立连接,发送数据,接收数据,关闭连接。曾经学习网络编程的时候用Java写了一些小的聊天程序,Java对网络接口函数的封装还是很简单实用的,但是在Windows下网络编程使用的Socket就显得稍微有点繁琐。这里介绍一个自己封装的一个简单的基于Windows Socket的一个框架代码,主要目的是为了方便使用Windows Socket进行编程时的代码复用,闲话少说,上代码。

    熟悉Windows Socket的都知道进行Windows网络编程必须引入头文件和库:

    #pragma once
    /********************公用数据预定义***************************/

    //WinSock必须的头文件和库
    #include <WinSock2.h>
    #pragma comment(lib,"ws2_32.lib")

    在网络编程中需要对很多API进行返回值检测,这里使用assert断言来处理错误,另外还有一些公用的宏定义,如下:

    //辅助头文件
    #include <assert.h>

    //网络数据类型
    #define TCP_DATA 1
    #define UDP_DATA 2

    //TCP连接限制
    #define MAX_TCP_CONNECT 10

    //缓冲区上限
    #define MAX_BUFFER_LEN 1024

    接下来从简单的开始,封装一个Client类,用于创建一个客户端,类定义如下:

    /*******************客户端*************************/
    //客户端类
    class Client
    {
        int m_type;//通信协议类型
        SOCKET m_socket;//本地套接字
        sockaddr_in serverAddr;//服务器地址结构
    public:
        Client();
        void init(int inet_type,char*addr,unsigned short port);//初始化通信协议,地址,端口
        char*getProto();//获取通信协议类型
        char*getIP();//获取IP地址
        unsigned short getPort();//获取端口
        void sendData(const char * buff,const int len);//发送数据
        void getData(char * buff,const int len);//接收数据
        virtual ~Client(void);
    };

    (1)   字段m_type标识通信协议是TCP还是UDP

    (2)       m_socket保存了本地的套接字,用于发送和接收数据。

    (3)       serverAddr记录了连接的服务器的地址和端口信息。

    (4)    构造函数使用WSAStartup(WINSOCK_VERSION,&wsa)加载WinSock DLL

    (5)       init函数初始化客户端进行通信的服务器协议类型,IP和端口。

    (6)       getProtogetIPgetPort分别提取服务器信息。

    (7)       sendData向服务器发送指定缓冲区的数据。

    (8)       getData从服务器接收数据保存到指定缓冲区。

    (9)   析构函数使用closesocket(m_socket)关闭套接字,WSACleanup卸载WinSock DLL

    Client类的实现如下:

    1)对于init,实现代码为:

    void Client::init(int inet_type,char*addr,unsigned short port)
    {
        int rslt;
        m_type=inet_type;
        if(m_type==TCP_DATA)//TCP数据
            m_socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//创建TCP套接字
        else if(m_type==UDP_DATA)//UDP数据
            m_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//创建UDP套接字
        assert(m_socket!=INVALID_SOCKET);
        serverAddr.sin_family=AF_INET;
        serverAddr.sin_addr.S_un.S_addr=inet_addr(addr);
        serverAddr.sin_port=htons(port);
        memset(serverAddr.sin_zero,0,8);
        if(m_type==TCP_DATA)//TCP数据
        {
            rslt=connect(m_socket,(sockaddr*)&serverAddr,sizeof(sockaddr));//客户端连接请求
            assert(rslt==0);
        }
    }

    首先,Client根据不同的协议类型创建不同的套接字m_socket,然后填充serverAddr结构,其中inet_addr是将字符串IP地址转化为网络字节序的IP地址,htons将整形转化为网络字节顺序,对于短整型,相当于高低字节交换。如果通信是TCP协议,那么还需要客户端主动发起connect连接,UDP不需要做。

    2)初始化连接后就可以发送数据了,sendData实现如下:

    这里根据不同的通信类型将数据使用send或者sendto发送到服务器,注意TCPsend的套接字参数是本地创建的套接字,和服务器的信息无关。而对于UDP,需要额外指定服务器的地址信息serverAddr,因为UDP是面向无连接的。

    3)若客户端需要接收数据,使用getData:

    void Client::getData(char * buff,const int len)
    {
        int rslt;
        int addrLen=sizeof(sockaddr_in);
        memset(buff,0,len);
        if(m_type==TCP_DATA)//TCP数据
        {
            rslt=recv(m_socket,buff,len,0);
        }
        else if(m_type==UDP_DATA)//UDP数据
        {
            rslt=recvfrom(m_socket,buff,len,0,(sockaddr*)&serverAddr,&addrLen);
        }
        assert(rslt>0);
    }

     

    根据不同的通信协议使用recvrecvfrom接收服务器返回的数据,和发送数据参数类似。

    4)有时需要获取客户端连接的服务器信息,这里封装的三个函数实现如下:

    char* Client::getProto()
    {
        if(m_type==TCP_DATA)
            return "TCP";
        else if(m_type==UDP_DATA)
            return "UDP";
        else
            return "";
    }

    char* Client::getIP()
    {
        return inet_ntoa(serverAddr.sin_addr);
    }

    unsigned short Client::getPort()
    {
        return ntohs(serverAddr.sin_port);
    }

    需要额外说明的是,inet_ntoa将网络字节序的IP地址转换为字符串IP,和前边inet_addr功能相反,ntohshtons功能相反。

    5)构造函数和析构函数的具体代码如下:

    Client::Client()
    {
        WSADATA wsa;
        int rslt=WSAStartup(WINSOCK_VERSION,&wsa);//加载WinSock DLL
        assert(rslt==0);
    }
    Client::~Client(void)
    {
        if(m_socket!=INVALID_SOCKET)
            closesocket(m_socket);
        WSACleanup();//卸载WinSock DLL
    }

    6)如果需要对客户端的功能进行增强,可以进行复用Client类。

    服务器类Server比客户端复杂一些,首先服务器需要处理多个客户端连接请求,因此需要为每个客户端开辟新的线程(UDP不需要),Server的定义如下:

    /*********************服务器********************/
    //服务器类

    #include <list>
    using namespace std;

    class Server
    {
        CRITICAL_SECTION *cs;//临界区对象
        int m_type;//记录数据包类型
        SOCKET m_socket;//本地socket
        sockaddr_in serverAddr;//服务器地址
        list<sockaddr_in*> clientAddrs;//客户端地址结构列表
        sockaddr_in* addClient(sockaddr_in client);//添加客户端地址结构
        void delClient(sockaddr_in *client);//删除客户端地址结构
        friend DWORD WINAPI threadProc(LPVOID lpParam);//线程处理函数作为友元函数
    public:
        Server();
        void init(int inet_type,char*addr,unsigned short port);
        void start();//启动服务器
        char* getProto();//获取协议类型
        char* getIP(sockaddr_in*serverAddr=NULL);//获取IP
        unsigned short getPort(sockaddr_in*serverAddr=NULL);//获取端口
        virtual void connect(sockaddr_in*client);//连接时候处理
        virtual int procRequest(sockaddr_in*client,const char* req,int reqLen,char*resp);//处理客户端请求
        virtual void disConnect(sockaddr_in*client);//断开时候处理
        virtual ~Server(void);
    };

    (1)       Client类似,Server也需要字段m_socketserverAddrm_type,这里引入clientAddrs保存客户端的信息列表,用addClientdelClient维护这个列表。

    (2)              CRITICAL_SECTION *cs记录服务器的临界区对象,用于保持线程处理函数内的同步。

    (3)       构造函数和析构函数与Client功能类似,getProtogetIPgetPort允许获取服务器和客户端的地址信息。

    (4)              init初始化服务器参数,start启动服务器。

    (5)              connectprocRequestdisConnect用于实现用户自定义的服务器行为。

    (6)       友元函数threadProc是线程处理函数。

    具体实现如下:

    (1)       init具体代码为:

    void Server::init(int inet_type,char*addr,unsigned short port)
    {
        int rslt;
        m_type=inet_type;
        if(m_type==TCP_DATA)//TCP数据
            m_socket=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);//创建TCP套接字
        else if(m_type==UDP_DATA)//UDP数据
            m_socket=socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);//创建UDP套接字
        assert(m_socket!=INVALID_SOCKET);
        serverAddr.sin_family=AF_INET;
        serverAddr.sin_addr.S_un.S_addr=inet_addr(addr);
        serverAddr.sin_port=htons(port);
        memset(serverAddr.sin_zero,0,8);
        rslt=bind(m_socket,(sockaddr*)&serverAddr,sizeof(serverAddr));//绑定地址和端口
        assert(rslt==0);
        if(m_type==TCP_DATA)//TCP需要侦听
        {
            rslt=listen(m_socket,MAX_TCP_CONNECT);//监听客户端连接
            assert(rslt==0);
        }
    }

    首先根据通信协议类型创建本地套接字m_socket,填充地址serverAddr,使用bind函数绑定服务器参数,对于TCP通信,需要listen进行服务器监听。

    (2)       初始化服务器后使用start启动服务器:

    void Server::start()
    {
        int rslt;
        sockaddr_in client;//客户端地址结构
        int addrLen=sizeof(client);
        SOCKET clientSock;//客户端socket
        char buff[MAX_BUFFER_LEN];//UDP数据缓存
        while(true)
        {
            if(m_type==TCP_DATA)//TCP数据
            {
                clientSock=accept(m_socket,(sockaddr*)&client,&addrLen);//接收请求
                if(clientSock==INVALID_SOCKET)
                    break;
                assert(clientSock!=INVALID_SOCKET);
                sockaddr_in*pc=addClient(client);//添加一个客户端
                connect(pc);//连接处理函数
                SockParam sp(clientSock,pc,this);//参数结构
                HANDLE thread=CreateThread(NULL,0,threadProc,(LPVOID)&sp,0,NULL);//创建连接线程
                assert(thread!=NULL);
                CloseHandle(thread);//关闭线程
            }
            else if(m_type==UDP_DATA)//UDP数据
            {
                memset(buff,0,MAX_BUFFER_LEN);
                rslt=recvfrom(m_socket,buff,MAX_BUFFER_LEN,0,(sockaddr*)&client,&addrLen);
                assert(rslt>0);
                char resp[MAX_BUFFER_LEN]={0};//接收处理后的数据
                rslt=procRequest(&client,buff,rslt,resp);//处理请求
                rslt=sendto(m_socket,resp,rslt,0,(sockaddr*)&client,addrLen);//发送udp数据
            }
        }
    }

    TCP服务器不断的监听新的连接请求,使用accept接收请求,获得客户端的地址结构和socket,然后更新客户端列表,调用connect进行连接时候的处理,使用CreateThread创建一个TCP客户端线程,线程参数传递了客户端socket和地址,以及服务器对象的指针,交给procThread处理数据的接收和发送。参数结构如下:

    //服务器线程处理函数参数结构
    struct SockParam
    {
        SOCKET rsock;//远程的socket
        sockaddr_in *raddr;//远程地址结构
        Server*pServer;//服务器对象指针
        SockParam(SOCKET rs,sockaddr_in*ra,Server*ps)
        {
            rsock=rs;
            raddr=ra;
            pServer=ps;
        }
    };

    但是对于UDP服务器,只需要不断使用recvfrom检测接收新的数据,直接处理即可,请求处理函数proRequest功能可以由用户自定义。处理后的数据使用sendto发送给客户端。

    3)相比UDPTCP数据处理稍显复杂:

    DWORD WINAPI threadProc(LPVOID lpParam)//TCP线程处理函数
    {
        SockParam sp=*(SockParam*)lpParam;
        Server*s=sp.pServer;
        SOCKET sock=s->m_socket;
        SOCKET clientSock=sp.rsock;
        sockaddr_in *clientAddr=sp.raddr;
        
        CRITICAL_SECTION*cs=s->cs;
        int rslt;
        char req[MAX_BUFFER_LEN+1]={0};//数据缓冲区,多留一个字节,方便输出
        do
        {
            rslt=recv(clientSock,req,MAX_BUFFER_LEN,0);//接收数据
            if(rslt<=0)
                break;
            char resp[MAX_BUFFER_LEN]={0};//接收处理后的数据
            EnterCriticalSection(cs);
            rslt=s->procRequest(clientAddr,req,rslt,resp);//处理后返回数据的长度
            LeaveCriticalSection(cs);
            assert(rslt<=MAX_BUFFER_LEN);//不会超过MAX_BUFFER_LEN
            rslt=send(clientSock,resp,rslt,0);//发送tcp数据
        }
        while(rslt!=0||rslt!=SOCKET_ERROR);
        s->delClient(clientAddr);
        s->disConnect(clientAddr);//断开连接后处理
        return 0;
    }

    线程处理函数使用传递的服务器对象指针pServer获取服务器socket,地址和临界区对象。和客户端不同的是,服务接收发送数据使用的socket不是本地socket而是客户端的socket!为了保证线程的并发控制,使用EnterCriticalSectionLeaveCriticalSection保证,中间的请求处理函数和UDP使用的相同。另外,线程的退出表示客户端的连接断开,这里更新客户端列表并调用disConnect允许服务器做最后的处理。和connect类似,这一对函数调用只针对TCP通信,对于UDP通信不存在调用关系。

    4connectprocRequestdisConnect函数形式如下:

    /*******************用户自定义**************************/
    //用户自定义服务器处理功能函数:连接请求,请求处理,连接关闭

    /***
        以下三个函数的功能由使用者自行定义,头文件包含自行设计
    **
    */
    #include <iostream>
    void Server::connect(sockaddr_in*client)
    {
        cout<<"客户端"<<getIP(client)<<"["<<getPort(client)<<"]"<<"连接。"<<endl;
    }

    int Server::procRequest(sockaddr_in*client,const char* req,int reqLen,char*resp)
    {
        cout<<getIP(client)<<"["<<getPort(client)<<"]:"<<req<<endl;
        if(m_type==TCP_DATA)
            strcpy(resp,"TCP回复");
        else if(m_type==UDP_DATA)
            strcpy(resp,"UDP回复");
        return 10;
    }

    void Server::disConnect(sockaddr_in*client)
    {
        cout<<"客户端"<<getIP(client)<<"["<<getPort(client)<<"]"<<"断开。"<<endl;
    }

    这里为了测试,进行了一下简单的输出,实际功能可以自行修改。

    5)剩余的函数实现如下:

    Server::Server()
    {
        cs=new CRITICAL_SECTION();
        InitializeCriticalSection(cs);//初始化临界区
        WSADATA wsa;
        int rslt=WSAStartup(WINSOCK_VERSION,&wsa);//加载WinSock DLL
        assert(rslt==0);
    }
    char* Server::getProto()
    {
        if(m_type==TCP_DATA)
            return "TCP";
        else if(m_type==UDP_DATA)
            return "UDP";
        else
            return "";
    }

    char* Server::getIP(sockaddr_in*addr)
    {
        if(addr==NULL)
            addr=&serverAddr;
        return inet_ntoa(addr->sin_addr);
    }

    unsigned short Server::getPort(sockaddr_in*addr)
    {
        if(addr==NULL)
            addr=&serverAddr;
        return htons(addr->sin_port);
    }

    sockaddr_in* Server::addClient(sockaddr_in client)
    {
        sockaddr_in*pc=new sockaddr_in(client);
        clientAddrs.push_back(pc);
        return pc;
    }

    void Server::delClient(sockaddr_in *client)
    {
        assert(client!=NULL);
        delete client;
        clientAddrs.remove(client);
    }

    Server::~Server(void)
    {
        for(list<sockaddr_in*>::iterator i=clientAddrs.begin();i!=clientAddrs.end();++i)//清空客户端地址结构
        {
            delete *i;
        }
        clientAddrs.clear();
        if(m_socket!=INVALID_SOCKET)
            closesocket(m_socket);//关闭服务器socket
        WSACleanup();//卸载WinSock DLL
        DeleteCriticalSection(cs);
        delete cs;
    }

    以上是整个框架的代码,整体看来我们可以总结如下:

    (1)       使用协议类型,IP,端口初始化客户端后,可以自由的收发数据。

    (2)       使用协议类型,IP,端口初始化服务器后,可以自由的处理请求数据和管理连接,并且功能可以由使用者自行定义。

    (3)       复用这块代码时候可以直接使用或者继承Client类和Server进行功能扩展,不需要直接修改类的整体设计。

    将上述所有的代码整合到一个Inet.h的文件里,在需要使用类似功能的程序中只需要引入这个头文件即可。

    下面通过构造一个测试用例来体会这种框架的简洁性:

    首先测试服务器代码:

    void testServer()
    {
        int type;
        cout<<"选择通信类型(TCP=0/UDP=1):";
        cin>>type;
        Server s;
        if(type==1)
            s.init(UDP_DATA,"127.0.0.1",90);
        else
            s.init(TCP_DATA,"127.0.0.1",80);
        cout<<s.getProto()<<"服务器"<<s.getIP()<<"["<<s.getPort()<<"]"<<"启动成功。"<<endl;
        s.start();
    }

    然后是测试客户端代码:

    void testClient()
    {
        int type;
        cout<<"选择通信类型(TCP=0/UDP=1):";
        cin>>type;
        Client c;
        if(type==1)
            c.init(UDP_DATA,"127.0.0.1",90);
        else
            c.init(TCP_DATA,"127.0.0.1",80);
        cout<<"客户端发起对"<<c.getIP()<<"["<<c.getPort()<<"]的"<<c.getProto()<<"连接。"<<endl;
        char buff[MAX_BUFFER_LEN];
        while(true)
        {
            cout<<"发送"<<c.getProto()<<"数据到"<<c.getIP()<<"["<<c.getPort()<<"]:";
            cin>>buff;
            if(strcmp(buff,"q")==0)
                break;
            c.sendData(buff,MAX_BUFFER_LEN);
            c.getData(buff,MAX_BUFFER_LEN);
            cout<<"接收"<<c.getProto()<<"数据从"<<c.getIP()<<"["<<c.getPort()<<"]:"<<buff<<endl;
        }
    }

    最后我们把这个测试程序整合在一块:

    #include "Inet.h"
    #include <iostream>
    using namespace std;

    int main()
    {
        int flag;
        cout<<"构建服务器/客户端(0-服务器|1-客户端):";
        cin>>flag;
        if(flag==0)
            testServer();
        else
            testClient();
        return 0;
    }

    对于TCP测试结果如下:

    对于UDP测试结果如下:

    通过测试程序的简洁性和结果可以看出框架的设计还是比较合理的,当然,这里肯定还有很多的不足,希望读者能提出更好的设计建议。

  • 相关阅读:
    zeplin使用教程
    如何卸载命令行全局安装的包
    webstrom快捷键
    更新npm至最新版本
    mac环境下安装react项目环境
    横向滚动条布局
    JAVA语法基础——动手动脑
    JAVA语言课堂测试
    暑假第八周进度报告
    暑假第七周进度报告
  • 原文地址:https://www.cnblogs.com/fanzhidongyzby/p/2613118.html
Copyright © 2011-2022 走看看