socket编程五种模型—主讲原理,代码较少

客户端:创建套接字,连接服务器,然后不停的发送和接收数据。

  比较容易想到的一种服务器模型就是采用一个主线程,负责监听客户端的连接请求,当接收到某个客户端的连接请求后,创建一个专门用于和该客户端通信的套接字和一个辅助线程。以后该客户端和服务器的交互都在这个辅助线程内完成。这种方法比较直观,程序非常简单而且可移植性好,但是不能利用平台相关的特性。例如,如果连接数增多的时候(成千上万的连接),那么线程数成倍增长,操作系统忙于频繁的线程间切换,而且大部分线程在其生命周期内都是处于非活动状态的,这大大浪费了系统的资源。所以,如果你已经知道你的代码只会运行在Windows平台上,建议采用Winsock I/O模型。

一.Select模型: 轮询fd_set集合

  利用select函数,实现对I/O 的管理。最初设计该模型时,主要面向的是某些使用UNIX操作系统的计算机,它们采用的是Berkeley套接字方案。Select模型已集成到 Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。

int select(
    int nfds,
    fd_set* readfds,
    fd_set* writefds,
    fd_set* exceptfds,
    const struct timeval* timeout
);
nfds:本参数忽略,仅起到兼容作用。 readfds:(可选)指针,指向一组等待可读性检查的套接口。 writefds:(可选)指针,指向一组等待可写性检查的套接口。 exceptfds:(可选)指针,指向一组等待错误检查的套接口。 timeout:select()最多等待时间,对阻塞操作则为NULL。
FD_CLR(s,*set):  //从集合set中删除描述字s。
FD_ISSET(s,*set)://若s为集合中一员,非零;否则为零。
FD_SET(s,*set):  //向集合添加描述字s。
FD_ZERO(*set):   //将set初始化为空集NULL。

  timeout参数控制select()完成的时间。若timeout参数为空指针,则select()将一直阻塞到有一个描述字满足条件。否则的话,timeout指向一个timeval结构,其中指定了select()调用在返回前等待多长时间。如果timeval为{0,0},则 select()立即返回,这可用于探询所选套接口的状态。

  服务器来轮询查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)

  除了需要有条件接受客户端的连接外,还需要在连接数为0的情形下做特殊处理,因为如果读集中没有任何套接字,select函数会立刻返回。

  当调用非阻塞模式时,可以说socket在select上设置超时时间阻塞应用。select 会在超时时间测试fd_set集合是否可用,如果超时/没有数据可读了会清除当前集合成员。

二.异步选择

  应用程序可以在一个套接字上接收以WINDOWS消息为基础的网络事件通知。该模型的实现方法是通过调用WSAAsynSelect函数 自动将套接字设置为非阻塞模式,并向WINDOWS注册一个或多个网络时间,并提供一个通知时使用的窗口句柄。当注册的事件发生时,对应的窗口将收到一个基于消息的通知。

三.事件选择

  Winsock 提供了另一个有用的异步I/O模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。

  基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网络事件。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。

四.重叠I/O模型

  readfile或者writefile的调用马上就会返回,这时候你可以去做你要做的事,系统会自动替你完成readfile或者writefile,在你调用了readfile或者writefile后,你继续做你的事,系统同时也帮你完成readfile或writefile的操作,这就是所谓的重叠。

1.用事件通知方式实现的重叠I/O模型

  异步I/O函数WSARecv。在调用WSARecv时,指定一个WSAOVERLAPPED结构,这个调用不是阻塞的,也就是说,它会立刻返回。一旦有数据到达的时候,被指定的WSAOVERLAPPED结构中的 hEvent被Signaled。使得与该套接字相关联的WSAEVENT对象也被Signaled,所以WSAWaitForMultipleEvents的调用操作成功返回。

typedef struct _WSAOVERLAPPED {
  DWORD  Internal; 
  DWORD  InternalHigh; 
  DWORD  Offset; 
  DWORD  OffsetHigh; 
  WSAEVENT  hEvent;
} WSAOVERLAPPED, FAR * LPWSAOVERLAPPED;

2.用完成例程方式实现的重叠I/O模型

  WSARecv时传递CompletionROUTINE指针,回调函数,当IO请求完成时调用该回调函数完成我们需要处理的工作,在这个模型中,主线程只用不停的接受连接即可;辅助线程判断有没有新的客户端连接被建立,如果有,就为那个客户端套接字激活一个异步的WSARecv操作,然后调用SleepEx使线程处于一种可警告的等待状态,以使得I/O完成后 CompletionROUTINE可以被内核调用。如果辅助线程不调用SleepEx,则内核在完成一次I/O操作后,无法调用完成例程(因为完成例程的运行应该和当初激活WSARecv异步操作的代码在同一个线程之内)。

服务端代码:

#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#include <iostream>

#pragma comment(lib, "ws2_32.lib")

using namespace std;

#define  PORT 6000
//#define  IP_ADDRESS "10.11.163.113"  //表示服务器端的地址
#define  IP_ADDRESS "127.0.0.1"  //直接使用本机地址

#define MSGSIZE 1024

void CALLBACK completionRoutine(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);
DWORD WINAPI workThread(LPVOID lpParam);

SOCKET newClientConn;
bool bNewClientArrive = false;

//与重叠IO结构相关的一些信息,把它们封装在一个结构体中方便管理
class PerSocketData
{
public:
    WSAOVERLAPPED overlap;//每一个socket连接需要关联一个WSAOVERLAPPED对象
    WSABUF buffer;//与WSAOVERLAPPED对象绑定的缓冲区
    char          szMessage[MSGSIZE];//初始化buffer的缓冲区
    DWORD          NumberOfBytesRecvd;//指定接收到的字符的数目
    DWORD          flags;
    SOCKET socket;
};

//使用这个工作线程来通过重叠IO的方式与客户端通信
DWORD WINAPI workThread(LPVOID lpParam)
{
    PerSocketData * lpIOdata = NULL;

    while (true)
    {
        //判断有没有新的客户端连接建立,如果有,就需要为这个客户端建立
        //一个异步的WSARecv操作
        if (bNewClientArrive)
        {
            //在新的连接中建立一个异步的操作
            lpIOdata = (PerSocketData*)HeapAlloc(
                GetProcessHeap(),
                HEAP_ZERO_MEMORY,
                sizeof(PerSocketData));
            lpIOdata->buffer.len = MSGSIZE;
            lpIOdata->buffer.buf = lpIOdata->szMessage;
            lpIOdata->socket = newClientConn;

            WSARecv(lpIOdata->socket,
                &lpIOdata->buffer,
                1,
                &lpIOdata->NumberOfBytesRecvd,
                &lpIOdata->flags,
                &lpIOdata->overlap,
                completionRoutine);//这里向系统登记一个回调函数,接收数据完成时会被调用

            bNewClientArrive = false;//处理完这个连接后将当前连接重置为false,等待下一个连接的到来

        }

        // 使此线程为警觉线程等待,如果有需要,可以立马唤醒,如果最后一个参数为false,则不会调用回调函数
        SleepEx(1000, true);//这一行代码是必须的,使上面的completionRoutine能在当前的上下文中调用
    }

    return 0;
}

//可以在msdn中查看WSARecv中最后一个函数也就是完成例程completionRoutine的原型
void CALLBACK completionRoutine(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags)
{
    //注意这里将WSAOVERLAPPED类型的指针转换成了PerSocketData类型的指针
    PerSocketData * lpIOdata = (PerSocketData*)(lpOverlapped);

    //下面表示客户端断开了连接
    if (dwError != 0 || cbTransferred == 0)
    {
        cout << "客户端断开连接" << endl;
        closesocket(lpIOdata->socket);
        HeapFree(GetProcessHeap(), 0, lpIOdata);
    }
    else
    {
        cout << lpIOdata->szMessage << endl;

        send(lpIOdata->socket, lpIOdata->szMessage, cbTransferred + 1, 0);//多发送一个字符,将字符串结束符也发送过去

        memset(&lpIOdata->overlap, 0, sizeof(WSAOVERLAPPED));
        lpIOdata->buffer.len = MSGSIZE;
        lpIOdata->buffer.buf = lpIOdata->szMessage;
        // 再次投递,等待接收
        WSARecv(lpIOdata->socket,
            &lpIOdata->buffer,
            1,
            &lpIOdata->NumberOfBytesRecvd,
            &lpIOdata->flags,
            &lpIOdata->overlap,
            completionRoutine);
    }
}

void main()
{

    WSADATA wsaData;
    int err;

    //1.加载套接字库
    err = WSAStartup(MAKEWORD(1, 1), &wsaData);
    if (err != 0)
    {
        cout << "Init Windows Socket Failed::" << GetLastError() << endl;
        return;
    }

    //2.创建socket
    //套接字描述符,SOCKET实际上是unsigned int
    SOCKET serverSocket;
    serverSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSocket == INVALID_SOCKET)
    {
        cout << "Create Socket Failed::" << GetLastError() << endl;
        return;
    }


    //服务器端的地址和端口号
    struct sockaddr_in serverAddr, clientAdd;
    serverAddr.sin_addr.s_addr = inet_addr(IP_ADDRESS);
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);

    //3.绑定Socket,将Socket与某个协议的某个地址绑定
    err = bind(serverSocket, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
    if (err != 0)
    {
        cout << "Bind Socket Failed::" << GetLastError() << endl;
        return;
    }


    //4.监听,将套接字由默认的主动套接字转换成被动套接字
    err = listen(serverSocket, 10);
    if (err != 0)
    {
        cout << "listen Socket Failed::" << GetLastError() << endl;
        return;
    }

    cout << "服务器端已启动......" << endl;

    int addrLen = sizeof(clientAdd);
    HANDLE hThread = CreateThread(NULL, 0, workThread, NULL, 0, NULL);
    if (hThread == NULL)
    {
        cout << "Create Thread Failed!" << endl;
    }
    CloseHandle(hThread);

    while (true)
    {
        //5.接收请求,当收到请求后,会将客户端的信息存入clientAdd这个结构体中,并返回描述这个TCP连接的Socket
        newClientConn = accept(serverSocket, (struct sockaddr*)&clientAdd, &addrLen);
        if (newClientConn == INVALID_SOCKET)
        {
            cout << "Accpet Failed::" << GetLastError() << endl;
            return;
        }
        cout << "客户端连接:" << inet_ntoa(clientAdd.sin_addr) << ":" << clientAdd.sin_port << endl;

        //将之前的第6步替换成了上面启动workThread这个线程函数和下面这一行代码
        //在线程函数workThread中会持续对这个变量进行判断
        bNewClientArrive = true;

    }

    closesocket(serverSocket);
    //7.清理Windows Socket库
    WSACleanup();
}

小TIP:
Windows提供了四种异步IO技术,机制几乎是相同的,区别在于通知结果的方式不同:
1、使一个设备内核对象变为有信号
Windows将设备句柄看作可同步的对象,即它可以处于有信号或处于无信号状态,当创建设备句柄、以异步的方式发送IO请求时,该句柄处于无信号状态,当异步IO完成之后,该句柄受信,通过WaitForSingleobject或WatiForMultipleObjects函数可以判断设备操作合适完成。该技术只能用于一个设备只发送一个IO请求,否则,若一个设备对应多个操作,当句柄受信时无法判断是该设备的那个操作完成。
2、使一个事件内核对象变为有信号
针对每个I/O操作绑定一个内核事件对象,并将等待事件等待函数等待该事件的受信,当I/O操作完成后系统使得与该操作绑定的事件受信,从而判断那个操作完成。该技术解决了使一个设备内核对象变为有信号技术中一个设备只能对应一个操作的不足。
3、警告I/O
在该技术中,当发出设备IO请求时,同时要求我们传递一个被称为完成例程的回调函数,当IO请求完成时调用该回调函数完成我们需要处理的工作。该技术允许单个设备同时进行多个I/O请求。
4、完成端口
完成端口技术多用于处理大规模的请求,通过内在的进程池技术可以达到很高的性能。

五.完成端口模型

  只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。

  完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能。

首先要创建一个 I / O完成端口对象

HANDLE CreateIoCompletionPort (
  HANDLE FileHandle,              // handle to file
  HANDLE ExistingCompletionPort,  // handle to I/O completion port
  ULONG_PTR CompletionKey,        // completion key
  DWORD NumberOfConcurrentThreads // number of threads to execute concurrently
);

我们深入探讨其中的各个参数之前,首先要注意该函数实际用于两个明显有别的目的: ■ 用于创建一个完成端口对象.
■ 将一个句柄同完成端口关联到一起。

  最开始创建一个完成端口时,唯一感兴趣的参数便是 NumberOfConcurrentThreads(并发 线程的数量);前面三个参数都会被忽略。NumberOfConcurrentThreads参数的特殊之处在于,它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码创建一个I / O完成端口:

CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)

  该语句的作用是返回一个句柄,在为完成端口分配了一个套接字句柄后,用来对那个端 口进行标定(引用)。

工作者线程调用 GetQueuedCompletionStatus 来轮询完成端口队列。

  如果你想在Windows平台上构建服务器应用,那么I/O模型是你必须考虑的。Windows操作系统提供了 选择(Select)、异步选择(WSAAsyncSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped I/O)和完成端口(Completion Port)共五种I/O模型。每一种模型均适用于一种特定的应用场景。程序员应该对自己的应用需求非常明确,而且综合考虑到程序的扩展性和可移植性等因素,作出自己的选择。

posted @ 2018-02-28 17:46  老耗子  阅读(334)  评论(0编辑  收藏  举报