1、Select模型
4、完成端口模型
局域网中最常用的有三个网络协议:MICROSOFT的NETBEUI、NOVELL的IPX/SPX和TCP/IP协议
通信协议。主要是对信息传输的速率、传输代码、代码结构、传输控制步骤、出错控制等作出规定并制定出标准。
C/C++的后台程序都需要进行网络通讯,实现方法无非有两种:使用系统底层Socket或者使用已有的封装好的网络库(重量级的ACE,轻量级的有Libevent,Libev,libcurl,还有 Boost的ASIO)
服务器:socket->bind->listen->accept->>recv/send->>close
客户端:socket->connect->>recv/send->>close
1. ACE
ACE是一个大型的中间件产品,代码20万行左右,过于宏大,一堆的设计模式,架构了一层又一层,使用的时候,要根据情况,看你从那一层来进行使用。支持跨平台。(详细资料可查看: http://www.cs.wustl.edu/~schmidt/ACE.html)
2. Boost的ASIO
Boost的ASIO是一个异步IO库,封装了对Socket的常用操作,简化了基于Socket程序的开发。它开源、免费、支持跨平台。(详细资料可查看:http://think-async.com/)
3. libevent
libevent是一个C语言写的网络库, 官方主要支持的是类linux操作系统, 最新的版本添加了对windows的IOCP的支持。由于IOCP是异步IO,与linux下的POLL模型,EPOLL模型,还有freebsd的KQUEUE等这些同步模型在用法上完全不一致,所以使用方法也不一样,就好比ACE中的Reactor和Proactor模式一样,使用起来需要转变思路。如果对性能没有特别的要求,那么使用libevent中的select模型来实现跨平台的操作, select模型可以横跨windows, linux, unix,solaris等系统。(详细资料可查看: http://libevent.org/)
4. libev
libev是一个C语言写的,它是一个C语言写的,只支持Linux系统的库,以前的时候只封装了EPOLL模型.使用方法类似libevent,但是非常简洁,代码量是最少的一个库,也就几千行代码。显然这样的代码跨平台肯定是无法支持的了,如果你只需要在Linux下面运行,那用这个库也是可以的。(详细资料可查看:http://software.schmorp.de/pkg/libev.html)
5. Linux Socket Programming In C++
详细资料可查看:http://tldp.org/LDP/LG/issue74/tougher.html)
6. C++ Sockets Library
它是一个跨平台的Sockets库,实现包括TCP、UDP、ICMP、SCTP协议。已实现的应用协议包括有SMTP、HTTP(S)、Ajp。具有SOCKS客户端实现以及匿名DNS,支持HTTP的GET/POST/PUT以及WebServer的框架。(详细参考资料可查看: http://www.alhem.net/Sockets/index.html)
7. Simple Socket
这个类库让编写基于Socket的客户/服务器程序更加容易。(详细资料可查看:http://home.kpn.nl/lcbokkers/simsock.htm)
8. POCO
POCO C++ Libraries提供一套C++的类库用以开发基于网络的可移植的应用程序,功能涉及线程、线程同步、文件系统访问、流操作、共享库和类加载、套接字以及网络协议包括:HTTP、FTP、SMTP等;其本身还包含一个HTTP服务器,提供XML的解析和SQL数据库的访问接口。POCO库的模块化、高效的设计及实现使得POCO特别适合嵌入式开发。在嵌入式开发领域,由于C++既适合底层(设备I/O、中断处理等)和高层面向对象开发,越来越流行。(详细资料可查看:http://pocoproject.org/)
9. Libcurl
免费的轻量级的客户端网络库,支持DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS,LDAP, LDAPS,POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet, TFTP.支持SSL, HTTPPOST,HTTPPUT, FTP上传, HTTP form上传,代理,cookies,用户名与密码认证。(详细资料可查看:http://curl.haxx.se/libcurl/)
10. libiop
一个c语言开发的跨平台网络IO库。功能特性:c/c++api,底层支持epoll,select,poll等io模型;异步事件模型;任务池模型,跨平台线程接口;跨平台(Linux/windows);日志服务;稳定,支持7*24小时无间断运行,自动处理异常状态;高并发与快速响应;API简洁,学习成本低。(详细资料可查看: http://sourceforge.net/projects/libiop/)
11.Muduo
muduo 是一个基于 Reactor 模式的现代 C++ 网络库,它采用非阻塞 IO 模型,基于事件驱动和回调,原生支持多核多线程,适合编写 Linux 服务端多线程网络应用程序。视频连接:http://v.youku.com/v_show/id_XNDIyNDc5MDMy.html,性能对比:http://www.oschina.net/p/muduo
使用 C++11 以及 Makefile 进行构建,详情请看https://github.com/AlexStocks/muduo
网络编程的4种IO模型
同时对多个套接字进行检测,看有没有数据到来啊,有没有人连进来啊。效率肯定 比检测单个套接字高。
FD_ZERO:初始化
FD_SET:将socket加进去
select():轮询,当socket的事件发生时,fd_set里面有相关的socket,如果没有socket有事件发生,select返回0
FD_ISSET检测,socket是否还在fd_set里,是的话,表示这个socket有事件发生
WSAAsyncSelect将socket与对应的窗口过程绑定,并指定这个socket对哪些事件感兴趣
就是说基本的socket函数只有写代码调用才会得到对应结果,当窗口编程时所有逻辑都在窗口里,所有事件都是以消息的形式通知我们的,我们肯定希望统一编程,继续以消息的形式获取到网络事件。所以WSAAsyncSelect就是能给窗口发送特定网络事件对应的消息的函数,我们只要在对应的消息到来时进行对应处理就可以了。
想要用WSAAsyncSelect这种机制,必须先后调用WSAStartup()WSAClearup()函数。因为任何用起来很轻松的工具都是有底层数据结构进行支撑的,比如堆排序,要事先准备数组,插入规则也有变化。所以WSAStartup就是为WSAAsyncSelect做准备的函数。WSAClearup是收尾函数 。
Windows Socket机制
基本的Socket系统调用: 自己使用listen,accept,recv,send等进行编程。不过需要自己判断有没有断开连接啊,有没有人连进来啊,有没有收到数据啊,该收多少啊,收完没有啊。比较麻烦。
Windows Socket的启动与终止:启动函数WSAStartup()建立与Windows Sockets DLL的连接,终止函数WSAClearup()终止使用该DLL,这两个函数必须成对使用。
Windows是一个非抢占式的操作系统,而不采取UNIX的阻塞机制。当一个通信事件产生时,操作系统要根据设置选择是否对该事件加以处理,WSAAsyncSelect()函数就是用来选择系统所要处理的相应事件。当Socket收到设定的网络事件中的一个时,会给程序窗口一个消息,这个消息里会指定产生网络事件的Socket,发生的事件类型和错误码。
服务端关键代码:
m_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建监听套接字
bind(m_socket, (sockaddr*)&sin, sizeof(sin));//进行绑定 sin.sin_addr.s_addr = INADDR_ANY;
WSAAsyncSelect(m_socket, m_hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE|FD_READ);//事件传给窗口
listen(m_socket, 5); //进入监听模式
BEGIN_MESSAGE_MAP(CMainDialog, CDialog)
ON_BN_CLICKED(IDC_START, OnStart)
ON_BN_CLICKED(IDC_CLEAR, OnClear)
ON_MESSAGE(WM_SOCKET, OnSocket) //给窗口添加收到网络消息交给OnSocket函数来处理
END_MESSAGE_MAP()
wParam是发生消息的套接字,lParam是网络类型
long CMainDialog::OnSocket(WPARAM wParam, LPARAM lParam)
{
SOCKET s = wParam; //得到句柄
if (WSAGETSELECTERROR(lParam)) //查看是否出错
return 0;
switch (WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT: //监听到有套接字中有连接进入
SOCKET client = ::accept(s, NULL, NULL);
break;
case FD_CLOSE:
closesocket(s);
break;
case FD_READ: //接收到对方发来的数据包
::recv(s, szContent, 1024, 0);
break;
}
return 0;
}
客户端关键代码:
m_socket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
WSAAsyncSelect(m_socket, m_hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CONNECT|FD_CLOSE);
connect(m_socket, (sockaddr*)&remote, sizeof(sockaddr));
BEGIN_MESSAGE_MAP(CMainDialog, CDialog)
ON_BN_CLICKED(IDC_CONNECT, OnConnect)
ON_BN_CLICKED(IDC_SEND, OnSend)
ON_MESSAGE(WM_SOCKET, OnSocket)
END_MESSAGE_MAP()
long CMainDialog::OnSocket(WPARAM wParam, LPARAM lParam)
{
SOCKET s = wParam;
if (WSAGETSELECTERROR(lParam))
{
::closesocket(m_socket);
return 0;
}
switch (WSAGETSELECTEVENT(lParam))
{
case FD_READ:
recv(s, szText, 1024, 0); break;
case FD_CONNECT:
m_bar.SetText("已经连接到服务器", 0, 0); break;
case FD_CLOSE:
break;
}
return 0;
}
WSAEventSelect模型是以事件的形式通知应用程序。有网络事件发生,则可以从事件对象中检测到该事件。
和WSAAsyncSelect本质上一样的,只是不依赖于窗口。
1)创建事件对象,注册网络事件 WSACreateEvent ()/WSAEventSelect ()
2)等待网络事件发生 WSAWaitForMultipleEvents ()
3)获取网络事件,获取事件类型 WSAWaitForMultipleEvents()/WSAEnumNetworkEvents()
4)手动设置信号量和释放资源 WSAResetEvent()
服务端主要代码:
WSAStartup(sockVersion, &wsaData);
// 创建监听套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(sListen, (sockaddr*)&sin, sizeof(sin));
listen(sListen, 5);
// 创建事件对象,并关联到新的套接字 和WSAAsyncSelect的区别是不是关联套接字到窗口了,没有窗口,于是关联事件
WSAEVENT event = WSACreateEvent();
WSAEventSelect(sListen, event, FD_ACCEPT|FD_CLOSE);
// 事件句柄和套接字句柄表// 并添加到表中,后面的 用[i]添加
WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS] = { event }
SOCKET sockArray[WSA_MAXIMUM_WAIT_EVENTS]={ sListen }
Int nEventTotal = 1;//总共的事件个数
// 处理网络事件
while(TRUE)
{
// 在所有事件对象上等待
int nIndex = WSAWaitForMultipleEvents(nEventTotal, eventArray, FALSE, WSA_INFINITE, FALSE);
// 对每个事件调用WSAWaitForMultipleEvents 函数,以便确定它的状态
nIndex = nIndex - WSA_WAIT_EVENT_0;
for(int i=nIndex; i<nEventTotal; i++)
{
nIndex = ::WSAWaitForMultipleEvents(1, &eventArray[i], TRUE, 1000, FALSE);
if(nIndex == WSA_WAIT_FAILED || nIndex == WSA_WAIT_TIMEOUT) continue;
else
{
// 获取到来的通知消息,WSAEnumNetworkEvents 函数会自动重置受信事件
WSANETWORKEVENTS event;
WSAEnumNetworkEvents(sockArray[i], eventArray[i], &event);
if(event.lNetworkEvents & FD_ACCEPT) // 处理FD_ACCEPT 通知消息
{
if(event.iErrorCode[FD_ACCEPT_BIT] == 0)
{
if(nEventTotal > WSA_MAXIMUM_WAIT_EVENTS)continue;
SOCKET sNew = ::accept(sockArray[i], NULL, NULL);
WSAEVENT event = ::WSACreateEvent();
WSAEventSelect(sNew, event, FD_READ|FD_CLOSE|FD_WRITE);
eventArray[nEventTotal] = event; // 添加到表中
sockArray[nEventTotal++] = sNew;
}
}
else if(event.lNetworkEvents & FD_READ) // 处理FD_READ 通知消息
{
if(event.iErrorCode[FD_READ_BIT] == 0)
{
char szText[256];
int nRecv = ::recv(sockArray[i], szText, 256, 0);
}
}
else if(event.lNetworkEvents & FD_CLOSE) // 处理FD_CLOSE 通知消息
{
if(event.iErrorCode[FD_CLOSE_BIT] == 0)
{
closesocket(sockArray[i]);
for(int j=i; j<nEventTotal-1; j++)
{
sockArray[j] = sockArray[j+1];
sockArray[j] = sockArray[j+1];
}
nEventTotal--;
}
}
else if(event.lNetworkEvents & FD_WRITE) // 处理FD_WRITE 通知消息
{
}
}
}
}
return 0;
}
WSACleanup();
利用内核对象的调度,使用少量的几个线程来处理和客户端的所有通信,从而消除线程上下文切换问题。当有事件产生时CPU能保证有资源可用,然后将这些事件加入到一个公共消息队列中去,当前哪一个线程空闲就去处理公共消息队列里的事件,如果没有事件了,线程就空闲下来。
1)初始化套接字组件 WSASocket()
2)绑定和监听 bind()
3)创建完成端口 CreateIoCompletionPort()
4)创建服务线程 GetSystemInfo()
5)连接客户端 accept()
6)套接字与完成端口关联 CreateIoCompletionPort()
7)将套接字与完成端口关联起来以后,应用程序调用发送数据/接收数据函数完成重叠IO操作
WSASend()/WSASendTo()
WSARecv()/WSARecvFrom()
8)等待重叠I/O操作结果 GetQueuedCompletionStatus()
9)投递完成通知 PostQueuedCompletionStatus()
一个完成端口实际上就是一个通知队列,操作系统吧已经完成的重叠IO请求通知发到队列中,完成端口会充分利用Windows最复杂的内核对象来进行IO的调度,属于异步IO,适用于CS通信模式中性能最好的网络通信模型。
完成端口IOCP模型的原理:
完成端口创建几个线程,等到用户请求的时候,就把这些请求都加入到一个公共消息队列中去,然后这几个线程就排队从消息队列中取出消息并加以处理,这种方式就很优雅的实现了异步通信和负载均衡的问题,因为它提供了一种机制来使用几个线程“公平的”处理来自多个客户端的输入输出,并且线程如果没事干的时候会被系统挂起,不会占用CPU周期,这个关键的作为交换的消息队列,它就是完成端口。
使用完成端口的基本流程
1、调用CreateIoCompletionPort函数创建一个完成端口,第四个参数设为0,让完成端口上每次处理一次只允许执行的线程
2、根据系统中CPU核心的数量建立对应的Worker线程,
3、一个用于监听的Socket,在指定的端口上监听连接请求
4、将接受的套接字绑定到完成端口
在新开辟的线程中:
1、在Worker线程使用GetQueuedCompletionStatus函数,它让Worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要大量处理的网络操作或者超出了等待的时间限制为止
2、使用重叠IO,在套接字上投递一个或者多个WSARecv或者WSASend请求
3、重复1~2步骤。
服务端代码
#include <iostream>
#include<WinSock2.h>
#include<cstdlib>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
DWORD WINAPI WorkerThread(LPVOID CompletionPortId);
typedef struct _MY_WSAOVERLAPPED
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
DWORD NumberOfBytesRecvd;
DWORD Flags;
SOCKET socket;
_MY_WSAOVERLAPPED()
{
Buffer.buf = new char[64]{ '\0' };
Buffer.len = 64;
Flags = 0;
overlap.hEvent = NULL;
}
~_MY_WSAOVERLAPPED()
{
delete[]Buffer.buf;
Buffer.buf = NULL;
Buffer.len = 0;
}
}MY_WSAOVERLAPPED, * PMY_WSAOVERLAPPED;
int main()
{
WSADATA wd;
if (WSAStartup(MAKEWORD(2, 2), &wd) != 0)
{
cout << "wsastartup error " << WSAGetLastError() << endl;
exit(EXIT_FAILURE);
}
//1 调用CreateIoCompletionPort函数创建一个完成端口,第四个参数设为0,让完成端口上每次处理一次只允许执行的线程
HANDLE completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (completionPort == NULL)
{
cout << "CreateIoCompletionPort " << WSAGetLastError() << endl;
exit(EXIT_FAILURE);
}
//1 根据系统中CPU核心的数量建立对应的Worker线程,
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
for (int i = 0; i < (int)sysInfo.dwNumberOfProcessors; i++)
{
HANDLE h = CreateThread(NULL, 0, WorkerThread, completionPort, 0, NULL);
CloseHandle(h);
}
cout << "创建了" << sysInfo.dwNumberOfProcessors << "个工作线程" << endl;
SOCKET s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (s == INVALID_SOCKET)
{
cout << "socket error " << WSAGetLastError() << endl;
exit(EXIT_FAILURE);
}
sockaddr_in addr;
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addr.sin_family = AF_INET;
addr.sin_port = htons(8000);
int bindRet = bind(s, (sockaddr*)&addr, sizeof sockaddr);
if (bindRet == SOCKET_ERROR)
{
cout << "bind error " << WSAGetLastError() << endl;
exit(EXIT_FAILURE);
}
int listenRet = listen(s, 5);
if (listenRet == SOCKET_ERROR)
{
cout << "listen error " << WSAGetLastError() << endl;
exit(EXIT_FAILURE);
}
while (true)
{
// 3 一个用于监听的Socket,在指定的端口上监听连接请求
SOCKET sClient = WSAAccept(s, NULL, NULL, NULL, NULL);
if (sClient == INVALID_SOCKET)
{
cout << "accept error " << WSAGetLastError() << endl;
continue;
}
char temp[64];
sprintf_s(temp, "欢迎%d进入客户端", sClient);
int sendRet = send(sClient, temp, strlen(temp), 0);
if (sendRet == SOCKET_ERROR)
{
closesocket(sClient);
}
cout << sClient << "进入客户端" << endl;
//4 将接受的套接字绑定到完成端口
CreateIoCompletionPort((HANDLE)sClient, completionPort, (ULONG_PTR)sClient, 0);
PMY_WSAOVERLAPPED pOver = new MY_WSAOVERLAPPED;
//5 使用重叠IO,在套接字上投递一个或者多个WSARecv或者WSASend请求
int ret = WSARecv(sClient, &pOver->Buffer, 1, &pOver->NumberOfBytesRecvd, &pOver->Flags, &pOver->overlap, NULL);
if (ret == SOCKET_ERROR)
{
int err = WSAGetLastError();
if (err != WSA_IO_PENDING)
{
cout << "wsarecv error " << WSAGetLastError() << endl;
closesocket(sClient);
delete pOver;
}
}
}
CloseHandle(completionPort);
closesocket(s);
if (WSACleanup() == SOCKET_ERROR)
{
cout << "wsacleanu 出错" << endl;
}
}
DWORD WINAPI WorkerThread(LPVOID CompletionPortId)
{
HANDLE completionPort = (HANDLE)CompletionPortId;
DWORD dwByteTransferred;
SOCKET sClient;
PMY_WSAOVERLAPPED pOver = NULL;
while (true)
{
//1 在Worker线程使用GetQueuedCompletionStatus函数,它让Worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要大量处理的网络操作或者超出了等待的时间限制为止
bool b = GetQueuedCompletionStatus(completionPort, &dwByteTransferred, (PULONG_PTR)&sClient, (LPOVERLAPPED*)&pOver, INFINITE);
if (sClient == NULL)
continue;
if (b && dwByteTransferred > 0)
{
cout << sClient << " 说:" << pOver->Buffer.buf << endl;
ZeroMemory(pOver->Buffer.buf, 64);
// 2 使用重叠IO,在套接字上投递一个或者多个WSARecv或者WSASend请求
int ret = WSARecv(sClient, &pOver->Buffer, 1, &pOver->NumberOfBytesRecvd, &pOver->Flags, &pOver->overlap, NULL);
if (ret == SOCKET_ERROR)
{
int err = WSAGetLastError();
if (err != WSA_IO_PENDING)
{
closesocket(sClient);
delete pOver;
cout << "wsarecv error" << err << endl;
}
}
}
else
{
cout << sClient << "离开了" << endl;
closesocket(sClient);
delete pOver;
}
}
}
三、Boost的ASIO
Asio 是一个跨平台的 C++ 库,常用于网络编程、底层的 I/O 编程等 (low-level I/O),其结构框架如下:
3.1 下载
Asio 库分为 Boost 版和 non-Boost 版,后者的下载地址为: http://think-async.com/ ,下载完成后,直接解压到合适位置即可。
3.2 配置
INCLUDEPATH += $$PWD/../../serialport/asio-1.10.8/include DEFINES += ASIO_STANDALONE |
使用 VS 2015,则 ASIO_STANDALONE 配置如下所示:
2.3简单的同步服务器的例子:
typedef
boost::shared_ptr<ip::tcp::socket> socket_ptr;
io_service service; //
//
需要一个io_service实例同底层操作系统的IO服务进行交互
ip::tcp::endpoint ep(
ip::tcp::v4(),
2001));
//
指定你想要监听的地址和端口
ip::tcp::acceptor acc(service, ep); //
监听对象
while (
true) {
socket_ptr sock(new
ip::tcp::socket(service)); //
创建监听套接字
acc.accept(*sock); //
对套接字进行监听
boost::thread(
boost::bind(
client_session, sock
)); //
开启线程
}
void
client_session
(socket_ptr sock) {
while (
true) {
char data[
512];
size_t len =
sock->read_some
(buffer(data)); //
读取数据
if ( len >
0)
write
(*sock, buffer(
"ok",
2)); //
发送数据
}
}
基础的同步客户端的例子:
using
boost::asio;
io_service
service; //
需要一个io_service实例同底层操作系统的IO服务进行交互
ip::tcp::endpoint ep(
ip::address::from_string(
"127.0.0.1"),
2001);//
指定你想要连接的地址和端口
ip::tcp::socket sock(service);
sock.
connect
(ep);
2.4基本的异步服务端例子:
using boost
::asio;
typedef boost
::shared_ptr<ip
::tcp::socket> socket_ptr;
io_service service;
ip
::tcp::endpoint ep( ip
::tcp::v4(),
2001));
//
ip
::tcp::acceptor acc(service, ep);
socket_ptr sock(
new ip
::tcp::socket(service));
start_accept(sock);
service.run();
void start_accept(socket_ptr sock) {
acc.async_accept(*sock, boost::bind( handle_accept, sock, _1) );
}
void handle_accept(socket_ptr sock, const boost::system::error_code &err) {
if ( err) return;
// 从这里开始, 你可以从socket读取或者写入
socket_ptr sock(new ip::tcp::socket(service));
start_accept(sock);
}这里很巧妙的是,在使用这个socket之后,你创建了一个新的socket,然后再次调用start_accept,用来创建另外一个“等待客户端连接“的异步操作,从而使service.run()循环一直保持忙碌状态。
套接字缓冲区…
异步的客户端例子:
using boost
::asio;
io_service service;
ip
::tcp::endpointep( ip
::address::from_string(
"127.0.0.1"),
2001);
ip
::tcp::socketsock(service);
sock
.async_connect
(ep, connect_handler);
service
.run();
voidconnect_handler(const boost
::system::error_code&
ec) {
// 如果ec返回成功我们就可以知道连接成功了
}