一个简单的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 断言来处理错误,另外还有一些公用的宏定义,如下:
接下来从简单的开始,封装一个 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 使用的相同。另外,线程的退出表示客户端的连接断开,这里更新客户端列表并调用 dis Connect 允许服务器做最后的处理。和 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 测试结果如下:
通过测试程序的简洁性和结果可以看出框架的设计还是比较合理的,当然,这里肯定还有很多的不足,希望读者能提出更好的设计建议。
标签: Windows Socket
作者: Leo_wl
出处: http://www.cnblogs.com/Leo_wl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
版权信息查看更多关于一个简单的Windows Socket可复用框架的详细内容...