Jirairya

WinSocket——part2

2017-08-09
code  C++

Winsock提供了一些I/O模型帮助应用程序以异步方式在一个或多个套接字上管理I/O。

多线程

进程

程序是计算机指令的集合,它以文件的形式存储在磁盘上。进程通常被定义为一个正在运行的程序的实例,是一个程序在其自身的地址空间中的一次执行活动。一个程序可以对应多个进程。

进程的组成部分:

  • 操作系统用来管理进程的内核对象。内核对象是系统用于存放关于进程的统计信息的地方,内核对象属于OS内分配的一个内存块,该内存块是一种数据结构,其成员负责维护该对象的各种信息。由于内核对象的数据结构只能被内核访问使用,因此应用程序在内存中无法找到该数据结构,并直接改变其内容,只能通过Windows提供的函数进行内核操作
  • 地址空间。包含所有可执行模块或DLL模块的代码和数据。也包含动态内存分配的空间,例如线程的栈和堆分配空间

真正完成代码执行的是线程,而进程只是线程的容器,或者或是线程的执行环境

线程

线程组成部分:

  • 线程的内核对象。操作系统由它来对线程实施管理。内核对象也是系统用来存放线程统计信息的地方。
  • 线程栈:用于维护线程在执行代码时需要的所有函数参数和局部变量。

线程总是在某个进程环境中创建。系统从进程的地址空间中分配内存,供线程的栈使用。线程只有一个内核对象和一个栈,保留的记录很少,因此所需内存很少。

线程创建函数:

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  DWORD dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID lpParameter,
  DWORD dwCreationFlags,
  LPDWORD lpThreadID
  );
  • lpThreadAttributes:指向SECURITY_ATTRIBUTES结构体指针,可以传递NULL让线程使用默认的安全性。但是,若希望所有的子进程能够继承该线程对象的句柄,就必须设定一个SECURITY_ATTRIBUTES结构体,将它的bInheritHandle成员初始化为TRUE
  • dwStackSize;设置线程初始栈的大小,即线程可以将多少地址空间用于它的栈,以字节为单位。若为0,或者小于默认提交的大小,将默认使用与调用该函数的线程相同的栈空间大小。
  • lpStartAddress:指向应用程序定义的LPTHREAD_START_ROUTINE类型的函数的指针,这个函数将由新线程执行,表明新线程的起始地址。main是主线程的入口器函数,新创建的线程也需要一个入口函数,这个函数的地址由此参数指定。新线程的入口函数名称任意,但是格式必须为DWORD WINAPI ThreadProc(LPVOID lpParameter),
  • lpParameter:该参数提供了一种将初始化值传递给线程函数的手段。这个参数的值既可以是一个数值,也可以是指向其他信息的指针
  • dwCreationFlags:设置用于控制线程创建的附加标记。若该值为CREATE_SUSPENDED,那么咸线程创建后处于暂停状态,直到程序调用ResumeThread函数为止;若该值是0,那么线程在创建之后立即运行
  • lpThreadId:这个参数是一个返回值,它指向一个变量,用来接收线程ID。当创建一个线程时,系统会为该线程分配一个ID。

在程序中,若想让某个线程暂停运行,可调用Sleep函数,该函数可使调用线程暂停自己的运行,直到时间间隔过去,函数声明原型为:

void Sleep(DWORD dwMilliseconds);

其参数为指定线程睡眠时间值,以毫秒为单位,参数若为1000,世纪是让线程睡眠1秒。

简单的多线程例子:

#include<windows.h>
#include<iostream>
using namespace std;

DWORD WINAPI Fun1Proc(LPVOID lpParameter);

int index = 0;

void main() {
  HANDLE hThread1;//构造句柄,构造
  hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  //第一个参数NULL,让新线程使用默认的安全性;
  //第二个参数设置为0,让新线程采用与调用线程一样的栈大小;
  //第三个参数指定线程1入口函数的地址
  //第四个参数是传递给线程1的参数,这里不需要使用这个参数
  //第五个参数为线程创建标记,设置为0,让线程一旦创建就运行
  //第六个参数为新线程ID,不需要使用该ID,所以将其设置为NULL
  CloseHandle(hThread1);//关闭线程句柄,析构,让该线程内核对象的引用计数减1
  while(index++<1000)
    cout << "main thread is running\n" << endl;
  Sleep(10);
  system("pause");

}

DWORD WINAPI Fun1Proc(LPVOID lpParameter) {
  while (index++<1000)
    cout << "Thread1 is running!\n" << endl;
  return 0;
}

句柄(handle)是一种特殊的智能指针。当一个应用程序要引用其他系统(如数据库、操作系统)所管理的内存块或对象时,就要使用句柄。句柄与普通指针的区别在于,指针包含的是引用对象的内存地址,而句柄则是由系统所管理的引用标识,该标识可以被系统重新定位到一个内存地址上。 https://www.zhihu.com/question/27656256 http://blog.csdn.net/wenzhou1219/article/details/17659485

简单的多线程例子

主线程运行一段时间后,当它的时间片到期后,OS会选择线程1开始运行。为线程1分配一个时间片,当线程1运行一段时间后,时间片到期,OS又开始选择主线程开始运行。于是就看到主线程和线程1交替运行


互斥对象实现线程同步

互斥对象(mutex)属于内核对象,能够确保线程拥有对单个资源的互斥访问权。互斥对象包含一个使用数量,一个线程ID和一个计数器。其中ID用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数

为了创建互斥对象,需要调用CreateMutex函数,该函数可以创建或打开一个命名的或匿名的互斥对象,然后程序就可以利用该互斥对象完成线程间的同步。函数原型声明如下:

HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  BOOL bInitialOwner,
  LPCTSTR lpName
  );
  • lpMutexAttributes:指向SECURUTY_ATTRIBUTES结构的指针,可以给该参数传递NULL值,让互斥对象使用默认的安全性
  • bInitialOwner:BOOL类型,指定互斥对象初始的拥有者。若为真,则创建这个互斥对象的线程获得该对象的所有权;否则,该线程将不获得所创建的互斥对象的所有权
  • lpName:指定互斥对象的名称。若此参数为NULL,则创建一个匿名的互斥对象。

当线程对共享资源访问结束后,应释放该对象的所有权,即让该对象处于已通知状态。此时调用ReleaseMutex函数,该函数将释放指定对象的所有权。函数声明原型如下:

BOOL ReleaseMutex(HANDLE hMutex);

线程必须主动请求共享对象的使用权才有可能获得该所有权,这可以通过调用WaitForSingleObject函数实现,函数原型声明如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMillisseconds);
  • hHandle:所请求对象的句柄。一旦互斥对象处于有信号状态,该函数立即返回;若始终处于无信号状态,即未通知的状态,则该函数就会一直等待,这样就会暂停线程的执行。
  • dwMilliseconds:指定等待的时间间隔,以毫秒为单位。若指定的时间间隔已过,即使所请求的对象仍处于无信号状态,WaitForSingleObject函数也会返回。若将此参数设置为0,那么WaitForSingleObject函数将测试该对象的状态立即返回;若将此参数设置为INFINITE,则函数会永远等待,直到等待的对象处于有信号状态才会返回。

调用WaitForSingleobject函数后,该函数会一直等待,只有在以下两种情况才会返回:

  • 指定的对象变成有信号状态
  • 指定的等待时间间隔已过

WaitForSingleObject函数可能的返回值:

返回值 说明
WAIT_OBJECT_0 所请求对象有信号
WAIT_TIMEOUT 所指定的时间间隔已过,并且所请求的对象是无信号状态
WAIT_ABANDONED 所请求的对象是一个互斥对象,并且先前拥有该对象的线程在终止前没有释放该对象。这时,该对象的所有权将授予当前调用线程,并且将该互斥对象被设置为无信号状态

例子:

#include<windows.h>
#include<iostream>
using namespace std;


DWORD WINAPI Fun1Proc(LPVOID lpParamter);
DWORD WINAPI Fun2Proc(LPVOID lpParamter);
int index = 0;
int tickets = 100;
HANDLE  hMutex;//定义了一个HANDLE类型的全局变量:hMutex,用于保存即将创建的互斥对象句柄
void main() {
  HANDLE hThread1;
  HANDLE hThread2;

  hMutex = CreateMutex(NULL, FALSE, NULL);//创建一个匿名的互斥对象
  //FALSE表明当前没有线程拥有这个互斥对象,
  //于是OS将会将该互斥对象设置为有信号状态
  hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);

  CloseHandle(hThread1);
  CloseHandle(hThread2);

  Sleep(4000);
  system("pause");
}
DWORD WINAPI Fun1Proc(LPVOID lpParamter) {
  while (true)
  {
    /*在需要保护的代码前添加WaitForSingleObject函数的调用,
    让其请求互斥对象的所有权,这样线程1和线程2就会一直等待,
    除非所请求的对象处于有信号状态。在线程1和线程2访问它们
    共享的全局变量tickets之前,添加WaitForSingleObject语句实现线程同步*/
    WaitForSingleObject(hMutex, INFINITE);
    if (tickets > 0)
      cout << "Thread1 sell ticket: " << tickets-- << endl;
    else
      break;
    ReleaseMutex(hMutex);//对互斥对象来说,谁拥有谁释放
  }
  return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParamter) {
  while (TRUE) {
    WaitForSingleObject(hMutex, INFINITE);
    if (tickets > 0)
      cout << "Thread2 sell ticket: " << tickets-- << endl;
    else
      break;
    /*当对所要保护的代码操作完成之后,应该调用ReleaseMutex函数
    释放当前线程对互斥对象的所有权,从而获得对共享资源的访问*/
    ReleaseMutex(hMutex);
  }
  return 0;
}

互斥对象实现线程同步

=================================

保证应用程序只有一个实例运行:

#include<windows.h>
#include<iostream>
using namespace std;


DWORD WINAPI Fun1Proc(LPVOID lpParamter);
DWORD WINAPI Fun2Proc(LPVOID lpParamter);
int index = 0;
int tickets = 100;
HANDLE  hMutex;//定义了一个HANDLE类型的全局变量:hMutex,用于保存即将创建的互斥对象句柄
void main() {
  HANDLE hThread1;
  HANDLE hThread2;

  hMutex = CreateMutex(NULL, TRUE,"tickets");//创建一个匿名的互斥对象
  //FALSE表明当前没有线程拥有这个互斥对象,
  //于是OS将会将该互斥对象设置为有信号状态
  if (hMutex) {
    if (ERROR_ALREADY_EXISTS == GetLastError()) {
      cout << "only one instance can run!" << endl;
      system("pause");
      return;
    }
  
  }
  hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);

  CloseHandle(hThread1);
  CloseHandle(hThread2);
  WaitForSingleObject(hMutex, INFINITE);
  ReleaseMutex(hMutex);
  ReleaseMutex(hMutex);

  Sleep(4000);
  system("pause");
}
DWORD WINAPI Fun1Proc(LPVOID lpParamter) {
  while (true)
  {
    /*在需要保护的代码前添加WaitForSingleObject函数的调用,
    让其请求互斥对象的所有权,这样线程1和线程2就会一直等待,
    除非所请求的对象处于有信号状态。在线程1和线程2访问它们
    共享的全局变量tickets之前,添加WaitForSingleObject语句实现线程同步*/
    WaitForSingleObject(hMutex, INFINITE);
    if (tickets > 0)
      cout << "Thread1 sell ticket: " << tickets-- << endl;
    else
      break;
    ReleaseMutex(hMutex);//对互斥对象来说,谁拥有谁释放
  }
  return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParamter) {
  while (TRUE) {
    WaitForSingleObject(hMutex, INFINITE);
    if (tickets > 0)
      cout << "Thread2 sell ticket: " << tickets-- << endl;
    else
      break;
    /*当对所要保护的代码操作完成之后,应该调用ReleaseMutex函数
    释放当前线程对互斥对象的所有权,从而获得对共享资源的访问*/
    ReleaseMutex(hMutex);
  }
  return 0;
}

保证应用程序只有一个实例运行

事件对象

事件对象也属于内核对象,它包含以下三个成员:

  • 使用计数
  • 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值
  • 用于指明该事件处于已通知状态还是未通知状态的布尔值

事件对象有两种不同的类型:人工重置的事件对象自动重置的事件对象。当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。

创建事件对象

在程序中可以通过CreateEvent函数创建或打开一个命名的或匿名的事件对象,该函数的原型声明如下:

HANDLE CreateEvent(
  LPSECURITY_ATTRIBUTES lpEventAttributes,
  BOOL bManualReset,
  BOOL bInitialState,
  LPCTSTR lpName
  );
  • lpEventAttributes:指向SECURITY_ATTRIBUTES结构体的指针。若为空,则使用默认的安全性
  • bManualRset:BOOL类型,指定创建的是人工重置事件对象,还是自动重置事件对象。若此参数为TRUE,表示该函数将创建一个人工重置对象;反之,创建自动重置对象。若为人工重置对象,当线程等待到该对象的所有权之后,需要调用ResetEvent函数手动地将该事件对象设置为无信号状态;若自动重置事件对象,当线程等到该对象的所有权之后,系统会自动将该对象设置为无信号状态
  • bInitialState:BOOL类型,指定事件对象的初始状态。若此参数为真,那么该事件对象初始是有信号状态;否则是无信号状态。
  • lpName:指定事件对象的名称,若此参数值为NULL,那么将创建一个匿名的事件对象

设置事件对象状态

SetEvent函数将把指定的事件对象设置为有信号状态,该函数的原型声明如下所示:

BOOL SetEvent(HANDLE hEvent);

SetEvent函数有一个HANDLE类型的参数,该参数指定将要设置其状态的事件对象的句柄。

重置事件对象状态

ResetEvent函数将把指定的事件对象设置为无信号状态,该函数的原型声明如下:

BOOL ResetEvent(HANDLE hEvent);

ResetEvent函数有一个HANDLE类型的参数,该参数指定将要重置其状态的事件对象的句柄。调用成功为非0,失败为0.

当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程,同时OS会将该事件对象设置无信号状态,这样当对所保护的代码执行完成后,需要调用SetEvent函数将该事件对象设置为有信号状态。而人工重置的事件对象,在一个线程得到该事件对象之后,OS并不会将该事件对象设置为无信号状态,除非显示地调用ResetEvent函数将其设置为无信号状态,否则该对象会一直是有信号状态

关键代码段

关键代码段,也称为临界区,工作在用户方式下,是指一个小代码段,在代码能够执行前,其必须独占对某些资源的访问权。

在进入关键代码之前,首先需要初始化关键代码段,这可以调用InitializeCriticalSection函数实现,该函数原型如下:

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

该函数的参数是一个指向CRITICAL_SECTION结构体的指针。该参数是out类型,即作为返回值使用。在使用时,需要构造一个CRITICAL_SECTION结构体类型的对象,然后将该对象的地址传递给InitializeCriticalSection函数,系统自动维护该对象。

想要进入关键代码段,首先需要调用EnterCriticalSection函数,以获得指定的临界区对象的所有权。该函数等待指定的临界区对象的所有权,若该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。 当调用程序获得了临界区对象的所有权后,该线程就进入关键代码段,对所保护的资源进行访问。 在线程使用完所保护的资源之后,需要调用LeaveCriticalSection函数,释放指定的临界区对象的所有权,之后其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进入关键代码段,访问保护的资源。 对临界区对象来说,不再需要时,需要调用DeleteCriticalSection函数释放该对象,该函数将释放一个没有被任何线程所拥有的临界区对象的所有资源。

#include<windows.h>
#include<iostream>

using namespace std;
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParammeter);

int tickets = 100;
CRITICAL_SECTION g_cs;//定义临界区对象

void main() {

  HANDLE hThread1;
  HANDLE hThread2;
  hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
  hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
  CloseHandle(hThread1);
  CloseHandle(hThread2);

  InitializeCriticalSection(&g_cs);//创建临界区
  Sleep(4000);
  system("pause");
  DeleteCriticalSection(&g_cs);
  //释放未被任何线程使用的临界区对象的所有资源
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter) {
  while (TRUE) {
    //判断能否得到指定的临界区对象的所有权
    //若无法得到该所有权,那么EnterCriticalSection会一直等待,从而导致线程暂停运行
    //若能得到该所有权,那么该线程就进入到关键代码段中,访问受保护的资源
    EnterCriticalSection(&g_cs);
    Sleep(2);
    if (tickets > 0) {

      Sleep(1);
      cout << "Threa1 sell tickets: " << tickets-- << endl;
      LeaveCriticalSection(&g_cs);
      //访问完受保护的资源之后调用LeaveCriticalsection函数,
      //释放指定的临界区对象的所有权
    }
    else {
      LeaveCriticalSection(&g_cs);
      break;
    }
  }
  return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParameter) {
  while (TRUE) {
    EnterCriticalSection(&g_cs);
    Sleep(1);
    if (tickets > 0) {
      Sleep(1);
      cout << "Thread2 sell ticket: " << tickets-- << endl;
      LeaveCriticalSection(&g_cs);
    }
    else {
      LeaveCriticalSection(&g_cs);
      break;
    }

  }
  cout << "thread2 is runnning1" << endl;
  return 0;
}

临界区

异步、阻塞

参考:

  • 1:https://www.zybuluo.com/phper/note/595507
  • 2:https://www.zhihu.com/question/19732473

异步与同步

异步和同步的重点在于消息通知的方式上,即调用结果通知的方式。

  • 同步:当一个同步调用发出去之后,调用者要一直等待调用结果的通知后,才能进行后续的执行。
  • 异步:当一个异步调用发出去之后,调用者不能立即得到调用结果的返回。要想获得结果,一般有两种方式:
  • 主动轮询异步调用的结果
  • 被调用方通过callback来通知调用方调用结果

生活中的例子:

  • 同步买奶茶:小明点单交钱,然后等着拿奶茶;异步买奶茶:小明点单交钱,店员给小明一个小票,等小明奶茶做好了,再来取。

  • 异步买奶茶: 小明要想知道奶茶是否做好了,有两种方式:

小明主动去问店员,一会就去问一下:“奶茶做好了吗?”…直到奶茶做好。这叫轮训。 等奶茶做好了,店员喊一声:“小明,奶茶好了!”,然后小明去取奶茶。这叫回调

阻塞和非阻塞

阻塞和非阻塞重点在于进程/线程等待消息时候的行为,也就是在等待消息的时候,当前进程/线程是否挂起。

  • 阻塞:在发出调用之后,消息返回之前,当前进程/线程会被挂起,直到有消息返回,当前进程/线程才会被激活
  • 非阻塞:调用发出去之后,不会阻塞当前进程/线程,而会立即返回。

生活中的例子:

  • 阻塞买奶茶:小明点单交钱,干等着拿奶茶,什么事都不做;
  • 非阻塞买奶茶:小明点单交钱,等着拿奶茶,等的过程中,时不时刷刷微博、朋友圈。
  1. 同步阻塞:小明在柜台干等着拿奶茶;
  2. 同步非阻塞:小明在柜台边刷微博边等着拿奶茶;
  3. 异步阻塞:小明拿着小票啥都不干,一直等着店员通知他拿奶茶;
  4. 异步非阻塞:小明拿着小票,刷着微博,等着店员通知他拿奶茶。

Windows套接字I/O模型

Winsock提供了一些I/O模型帮助应用程序以异步方式在一个或多个套接字上管理I/O。

I/O模型大概分为6种:

  • 阻塞(blocking)模型
  • 选择(select)模型
  • WSAAsyncSelect模型
  • WSAEventSelect模型
  • 重叠(overlapped)模型
  • 完成端口(completion port)模型

阻塞模式

套接字创建时,默认为阻塞模式。如recv函数的调用会使程序进入等待状态,直到接收到数据才返回。 阻塞套接字的好处是使用简单,但当需要处理多个套接字连接时,就必须创建多个线程,即典型的一个连接使用一个线程的问题。

非阻塞模式

应用程序可以调用ioctlsocket函数显示地让套接字工作在非阻塞模式下。

u_long ul = 1;
SOCKET s = socket(AF_INET, SOCK_STREAM,0)
ioctlsocket(s, FIONBIO,(u_long*)&ul);
//此属性的作用是“允许或者禁止套接字的非阻塞模式”

一旦套接字被置于非阻塞模式下,处理发送和接收数据或者管理连接的Winsock调用将会立即返回。大多数情况下,调用失败的出错代码是WSAEWOULDBLOCK,这意味着操作在调用期间没有完成。例如,系统输入缓冲区没有待处理的数据,那么对recv的调用将返回WSAEWOULDBLOCK。通常,要对相同函数调用多次,直到返回成功为止。

非阻塞调用经常以WSAEWOULDBLOCK出错代码失败,所以将套接字设置为非阻塞之后,关键的问题在于如何确定套接字什么时候可读/可写,也就是确定网络事件何时发生

选择(select)模型

select模型是一个广泛在Winsock中使用的I/O模型,称它为select模型,其主要用select函数来管理I/O。其目的是允许那些想要避免在套接字调用非阻塞的应用程序有能力管理多个套接字。

select函数

select()函数可以确定一个套接字或多个套接字的状态。若套接字没有网络事件发生,便进入等待状态,以便执行同步I/O.

int select(
    int nfds, //忽略,仅是为了与Berkeley套接字兼容
    fd_set* readfds, //指向一个套接字集合,用来检查其可读性
    fd_set* writefds, //指向一个套接字集合,用来检测其可读性
    fd_set* exceptfds, //指向一个套接字集合,用来检测错误
    const struct timeval* timeout //指定此函数等待的最长时间,如果为NULL,则最长时间为无限大
    );

函数调用成功,返回发生网络事件的所有套接字数量总和。若超出了时间限制,返回0,失败则返回SOCKET_ERROR。

套接字集合:

fd_set结构可以把多个套接字连在一起,形成套接字集合。select函数可以测试这个集合中哪些套接字有事件发生。

typedef struct fd_set{
  u_int fd_count;
  SOCKET fd_array[FD_SETSIZE];
}fd_set;

以下是WINSOCK定义的4个操作fd_set套接字集合的宏:

  • FD_ZERO(*set):初始化set为空集合。集合在使用前应该总是清空。
  • FD_CLR(s, *set):从set移除套接字s
  • FD_SET(s, *set):添加套接字到集合

网络事件:

select函数返回之后,若有下列事件发生,其对应的套接字就会被标识:

  • readfds集合
    • 数据可读
    • 连接已经关闭、重启或者中断
    • 若listen已经被调用,并且有一个连接未决,accept函数将成功
  • writefds集合
    • 数据能够发送
    • 若一个非阻塞连接调用正在被处理,连接已经成功
  • exceptfds集合
    • 若一个非阻塞连接调用正在被处理,连接失败
    • OOB数据可读

当select返回时,它通过移除没有未决I/O操作的套接字句柄修改每个fd_set集合。例如,想要测试套接字s是否可读时,必须将它添加到readfds集合,然后等待select函数返回。当select调用完成后再确定s是否仍然还在readfds集合中,若还在,就说明s可读了。三个参数中的任意两个都可以是NULL(至少要有一个不是NULL),任何不是NULL的集合必须至少包含一个套接字句柄。

设置超时:

最后的参数timeout是timeval结构的指针,它指定了select函数等待的最长时间。若设置为NULL,select将会无限阻塞,直到有网络事件发生。

typedef struct timeval{
  long tv_sec;//指示等待多少秒
  long tv_usec;//指示等待多少毫秒
}timeval;

例子: 程序运行之后,在4567端口监听,接受客户端连接请求,打印出接收到的数据。(即便是在单个线程内,也可管理多个套接字)

#include "../common/initsock.h"
#include <stdio.h>

CInitSock theSock;    // 初始化Winsock库
int main()
{
  USHORT nPort = 4567;  // 此服务器监听的端口号

  // 创建监听套节字
  SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
  sockaddr_in sin;
  sin.sin_family = AF_INET;
  sin.sin_port = htons(nPort);
  sin.sin_addr.S_un.S_addr = INADDR_ANY;
  // 绑定套节字到本地机器
  if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
  {
    printf(" Failed bind() \n");
    return -1;
  }
  // 进入监听模式
  ::listen(sListen, 5);

    // select模型处理过程
  // 1)初始化一个套节字集合fdSocket,添加监听套节字句柄到这个集合
  fd_set fdSocket;    // 所有可用套节字集合
  FD_ZERO(&fdSocket);
  FD_SET(sListen, &fdSocket);
  while(TRUE)
  {
    // 2)将fdSocket集合的一个拷贝fdRead传递给select函数,
    // 当有事件发生时,select函数移除fdRead集合中没有未决I/O操作的套节字句柄,然后返回。
    fd_set fdRead = fdSocket;
    int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
    if(nRet > 0)
    {
      // 3)通过将原来fdSocket集合与select处理过的fdRead集合比较,
      // 确定都有哪些套节字有未决I/O,并进一步处理这些I/O。
      for(int i=0; i<(int)fdSocket.fd_count; i++)
      {
        if(FD_ISSET(fdSocket.fd_array[i], &fdRead))
        {
          if(fdSocket.fd_array[i] == sListen)   // (1)监听套节字接收到新连接
          {
            if(fdSocket.fd_count < FD_SETSIZE)
            {
              sockaddr_in addrRemote;
              int nAddrLen = sizeof(addrRemote);
              SOCKET sNew = ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
              FD_SET(sNew, &fdSocket);
              printf("接收到连接(%s)\n", ::inet_ntoa(addrRemote.sin_addr));
            }
            else
            {
              printf(" Too much connections! \n");
              continue;
            }
          }
          else
          {
            char szText[256];
            int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
            if(nRecv > 0)           // (2)可读
            {
              szText[nRecv] = '\0';
              printf("接收到数据:%s \n", szText);
            }
            else                // (3)连接关闭、重启或者中断
            {
              ::closesocket(fdSocket.fd_array[i]);
              FD_CLR(fdSocket.fd_array[i], &fdSocket);
            }
          }
        }
      }
    }
    else
    {
      printf(" Failed select() \n");
      break;
    }
  }
  return 0;
}

select服务器

使用select的好处是程序能够在单个线程内同时处理多个套接字连接,避免了阻塞模式下的线程膨胀问题。但是,添加到fd_set结构的套接字数量是有限的,默认情况下,最大值是FD_SETSIZE,它在winsock2.h文件中定义为64.为了增加套接字数量,应用程序可以将FD_SETSIZE定义为更大的值,通常不能超过1024.FD_SETSIZE值太大的话,服务器性能受到影响。

WSAAsyncSelect模型

WSAAsyncSelect模型允许应用程序以Windows消息的形式接收网络事件通知。其是为了适应Windows的消息驱动环境而设置的。

WSAAsyncSelect函数自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句柄,当有网络事件发生时,便向这个窗口发送消息。函数用法如下:

int WSAAsyncSelect(
    SOCKET s,//需要设置的套接字句柄
    HWND hWnd,//指定一个窗口句柄
    //套接字的通知消息将被发送到与其对应的窗口过程
    u_int wMsg,//网络事件到来时接收到的消息ID
    //可以在WM_USER以上的数值中任意选择一个用作ID
    long lEvent//指定哪些通知码需要发送
  )

最后一个参数IEvent指定了要发送的通知码,可以是如下取值的组合:

  • FD_READ:套接字接收到对方发送过来的数据包,表明这时可以去读套接字了
  • FD_WRITE:数据缓冲区满后再次变为空时,WinSock接口通过该通知码通知应用程序。表示可以发送数据了(短时间发送过多,便会造成数据缓冲区变满)
  • FD_ACCEPT:监听中的套接字检测到有连接进入
  • FD_CONNECT:若用套接字去连接对方的主机,当连接动作完成以后会接收到这个通知码
  • FD_CLOSE:检测到套接字对应的连接被关闭

在监听套接字时可如此调用WSAAsynvSelect函数:

::WSAAsyncSelect(sListen,hWnd,WM_SOCKET,FD_ACCEPT|FD_CLOSE);
//WM_SOCKET为自定义消息

上述代码将套接字sListen设为窗口通知消息类型。WM_SOCKET为自定义网络通知消息,FD_CLOSE|FD_ACCEPT指定了sListen套接字只接收两个消息。当有客户连接或套接字关闭时,Winsock接口将向指定的窗口发送WM_SOCKET消息。

成功调用WSAAsyncSelect之后,应用程序便开始以Windows消息的形式在窗口函数接收网络事件通知,窗口函数的定义:

LRESULT CALLBACK WindowProc(HWND hWnd,UINT uMsg,WPARAM wParam, LPARAM lParam);

wParam参数指定了发生网络事件的套接字句柄,lParam参数的低字位指定了发生的网络事件,高字位包含了任何可能出现的错误代码,可以使宏WSAGETSELECTERROR和WSAGETSELECTEVENT将这些信息取出。

#include"../../common/initsock.h"

#include<stdio.h>

#define WM_SOCKET WM_USER + 101

CInitSock theSock;

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int main() {
  char szClassName[] = "MainWClass";
  WNDCLASSEX wndclass;

  wndclass.cbSize = sizeof(wndclass);
  wndclass.style = CS_HREDRAW | CS_VREDRAW;
  wndclass.lpfnWndProc = WindowProc;
  wndclass.cbClsExtra = 0;
  wndclass.cbWndExtra = 0;
  wndclass.hInstance = NULL;
  wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
  wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);
  wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
  wndclass.lpszMenuName = NULL;
  wndclass.lpszClassName = szClassName;
  wndclass.hIconSm = NULL;
  ::RegisterClassEx(&wndclass);

  HWND hWnd = ::CreateWindowEx(
    0,
    szClassName,
    "",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    CW_USEDEFAULT,
    NULL,
    NULL,
    NULL,
    NULL);
  if (hWnd == NULL) {
    ::MessageBox(NULL, "Create Window Falied!", "error", MB_OK);
    return -1;
  }
  USHORT nPort = 4567;

  SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  sockaddr_in sin;
  sin.sin_family = AF_INET;
  sin.sin_port = htons(nPort);
  sin.sin_addr.S_un.S_addr = INADDR_ANY;

  if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) {
    printf("Failed bind() \n");
    return -1;
  }

  ::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
  ::listen(sListen, 5);

  MSG msg;

  while (::GetMessage(&msg, NULL, 0, 0)) {
    ::TranslateMessage(&msg);
    ::DispatchMessage(&msg);
  }
  return msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
  switch (uMsg) {
  case WM_SOCKET: {
    SOCKET s = wParam;
    if (WSAGETSELECTERROR(lParam)) {
      ::closesocket(s);
      return 0;
    }
    switch (WSAGETSELECTEVENT(lParam)) {
    case FD_ACCEPT: {
      SOCKET client = ::accept(s, NULL, NULL);
      ::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
    }
            break;
    case FD_WRITE: {

    }
             break;
    case FD_READ: {
      char szText[1024] = { 0 };
      if (::recv(s, szText, 1024, 0) == -1)
        ::closesocket(s);
      else
        printf("Accept the data: %s", szText);
    }
            break;
    case FD_CLOSE: {
      ::closesocket(s);
    }
             break;
    }
  }
          return 0;
  case WM_DESTROY:
    ::PostQuitMessage(0);
    return 0;
  }

  return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}

网络事件消息到达消息处理函数之后,应用程序首先会检查lParam参数的高位,判断是否在套接字上发生了网络错误。宏WSAGETSELECTERROR返回高字节包含的错误信息。若应用程序发现套接字上没有产生错误,便可以调可用宏WSAGETSELECTEVENT读取lParam参数的低字位确定发生的网络事件

WSAAsyncSelect模型最突出的特点是与Windows消息驱动机制融合,会使得开发带GUI的网络程序变得简单。但连接增加,单个Windows函数处理请求过多,性能会受影响

WSAEventSelect

Winsock提供了另一种有用的异步事件通知IO模型——WSAEventSelect模型,允许应用程序在一个或多个套接字上接收基于事件的网络通知。它与WSAAsyncSelect模型类似是因为其也接收FD_XXX类型的网络事件,不过并不是依靠Windows消息驱动机制,而是经由事件对象句柄通知。


下一篇 TCP/IP

Comments

Content