一个简单的Windows Socket可复用框架
转载自:http://www.cnblogs.com/fanzhidongyzby/archive/2012/07/28/2613118.html
说起网络编程,无非是建立连接,发送数据,接收数据,关闭连接。曾经学习网络编程的时候用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) getProto,getIP,getPort分别提取服务器信息。
(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发送到服务器,注意TCP下send的套接字参数是本地创建的套接字,和服务器的信息无关。而对于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); }
根据不同的通信协议使用recv和recvfrom接收服务器返回的数据,和发送数据参数类似。
(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功能相反,ntohs和htons功能相反。
(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_socket,serverAddr和m_type,这里引入clientAddrs保存客户端的信息列表,用addClient和delClient维护这个列表。
(2) CRITICAL_SECTION *cs记录服务器的临界区对象,用于保持线程处理函数内的同步。
(3) 构造函数和析构函数与Client功能类似,getProto,getIP,getPort允许获取服务器和客户端的地址信息。
(4) init初始化服务器参数,start启动服务器。
(5) connect,procRequest,disConnect用于实现用户自定义的服务器行为。
(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)相比UDP,TCP数据处理稍显复杂:
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!为了保证线程的并发控制,使用EnterCriticalSection和LeaveCriticalSection保证,中间的请求处理函数和UDP使用的相同。另外,线程的退出表示客户端的连接断开,这里更新客户端列表并调用disConnect允许服务器做最后的处理。和connect类似,这一对函数调用只针对TCP通信,对于UDP通信不存在调用关系。
(4)connect,procRequest,disConnect函数形式如下:
/*******************用户自定义**************************/ //用户自定义服务器处理功能函数:连接请求,请求处理,连接关闭 /*** 以下三个函数的功能由使用者自行定义,头文件包含自行设计 ***/ #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测试结果如下:
通过测试程序的简洁性和结果可以看出框架的设计还是比较合理的,当然,这里肯定还有很多的不足,希望读者能提出更好的设计建议。