一个简单的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 dis Connect(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->dis Connect(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::dis Connect(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测试结果如下:

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值