分享
 
 
 

自学WinSocket I/O Methods

王朝vc·作者佚名  2006-01-08
窄屏简体版  字體: |||超大  

自学WinSocket I/O Methods

1.首先你需要知道什么是"Socket Modes",什么是"Socket I/O Models"?

Socket Modes : Determines how winsock functions behave when called with a socket.

Socket I/O Models : Describes how an application manages and processes I/O on a socket.

2.那么Winsock提供了哪几种"Socket Modes"给程序员用呢?

a.Blocking Mode(阻塞式的)

b.Noblocking Mode(非阻塞式的)

3.详细解释一下阻塞和非阻塞.

我们那最常用的send和recv两个函数来说吧...

比如你调用send函数发送一定的Byte,在系统内部send做的工作其实只是把数据传输(Copy)到TCP/IP协议栈的输出缓冲区,它执行成功并不代表数据已经成功的发送出去了,如果TCP/IP协议栈没有足够的可用缓冲区来保存你Copy过来的数据的话...这时候就体现出阻塞和非阻塞的不同之处了:对于阻塞模式的socket send函数将不返回直到系统缓冲区有足够的空间把你要发送的数据Copy过去以后才返回,而对于阻塞的socket来说send会立即返回WSAEWOULDDBLOCK告诉调用者说:"发送操作被阻塞了!!!你想办法处理吧..."

对于recv函数,同样道理,该函数的内部工作机制其实是在等待TCP/IP协议栈的接收缓冲区通知它说:嗨,你的数据来了.对于阻塞模式的socket来说如果TCP/IP协议栈的接收缓冲区没有通知一个结果给它它就一直不返回:耗费着系统资源....对于阻塞模式的socket该函数会马上返回,然后告诉你:WSAEWOULDDBLOCK.

3.对于阻塞模式(Blocking Mode)我采取开线程的方式不就搞定了吗,而且还不用我去判断基本的收/发什么时候完成,多省心?

没错,这位大虾说的开线程的方式具体的做法如下:我们将应用划分成一个读线程(Reading Thread)一个工作线程(Working Thread),两个线程靠同步对象(Synchronization Object,可以是Event,Mutex...)来共享一个数据缓冲区,读线程(Reading Thread)负责从网

络读取数据并将其放到共享缓冲区中,在适当的时机(比如说读线程将工作线程需要的数据接收下来之后)触发一个事件,通知/唤醒工作线程(Working Thread),下面是上述文字的简单代码实现:

CRITICAL_SECTION g_data;

HANDLE g_hEvent;

TCHAR g_szBuffer[MAX_BUFFER_SIZE];

int g_nBytes;

void ReadThread(void)

{

int nTotle = nRead = nLeft = nBytes = 0;

while(!done)

{

nTotal = 0;

nLeft = NUM_BYTES_REQUIRED; //要读多少的Bytes

while( nTotal != NUM_BYTE_REQUIRED)

{

// 因为涉及线程和全局变量所以说需要用到关键代码段(Critical Section)

EnterCriticalSection(&g_data);

nRead = recv(sock,&(g_szBuffer[MAX_BUFFER_SIZE] - nBytes),nLeft);

if( -1 == nread )

{

printf("socket %d error.\n",WSAGetLasError());

ExitThread();

}

nTotal += nRead;

nLeft -= nRead;

nBytes += nRead;

LeaveCriticalSelection(&g_data);

}

// 从网络接收完指定的(NUM_BYTES_REQUIRED)Bytes之后通知WorkThread启动

SetEvent(g_hEvent);

}

}

void WorkThread(void)

{

WaitForSingleObject(g_hEvent);

EnterCriticalSection(&g_data);

// 主要的处理工作...

DoSomeWorkOnData(g_szBuffer);

g_nBytes -= NUM_BYTE_REQUIRED;

LeaveCriticalSelection(&g_data);

}

这种方式的一个缺点是:应用程序很难同时通过多个建立好连接的socket通信.当然话说回来我们可以为每一个连接好的socket都开一个Reading Thread and WorkThread,但是呢这会带来很大的开销,而且扩展性很差,试想:如果用这样的模型建立一个服务器端来一个连接开至少两个线程...天..要是业务负担很大/连接的socket很多的话怎么办??所以说别多想了...

这样的方式是很不适合做服务器端的.(注意:我说的服务器端是指C/S中的S端逻辑).

从编写代码的角度解读一下IO中最复杂的IOCP模型

下面的一段代码是Microsoft Win32 SDK里面的例子,这是一个Echo服务器程序:

//

// 这就不说了,为了使用Winsock2必须包含的头文件...

//

#include <winsock2.h>

#include <windows.h>

#include <stdio.h>

#define PORT 5150 // 这个服务器选择5150端口

#define DATA_BUFSIZE 8192 // 把Buffer定义为8K是一个比较被Microsoft推荐的大小

//

// 这个结构体很重要,请一定看清楚,到后面用到的时候别糊涂了

// 特别注意:把Overlapped放最开头是有原因的,你应该明确的是

// 如果有:

// PER_IO_OPERATION_DATA PerIoData;

// &PerIoData->Overlapped 和 &PerIoData是一样的/等价的/

// 指向同一块内存区域的.

//

typedef struct

{

OVERLAPPED Overlapped;

WSABUF DataBuf;

CHAR Buffer[DATA_BUFSIZE];

DWORD BytesSEND; // 下面两个参数是用来记录每个connected socket上面的收发的Bytes的

DWORD BytesRECV;

} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

// 别把这个结构体和上面的结构体混淆了,名字很像的...其实它在这个程序中就相当于是那个和Client端连接的socket

typedef struct

{

SOCKET Socket;

} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);

void main(void)

{

SOCKADDR_IN InternetAddr;

//

// 一个listen和一个Accept socket是服务器程序的老套路了...

//

SOCKET Listen;

SOCKET Accept;

//

// IO完成端口映射到程序就是一个HANDLE

//

HANDLE CompletionPort;

SYSTEM_INFO SystemInfo;

//

// 看清楚,这是指针...不怕你笑话我们处还真有分不清和指针相关的一切概念的人,比如下面的代码:

// 我写了一个DLL,里面有一个这样的函数:

// BOOL GetPlayingPosition(DWORD * dwPos);

// 结果大虾调用的时候是这样的....

// DWORD * p;

// GetPlayingPostion(p);

// 它是调用爽了...我呢...:-(

// 当然了,也不好定义说它就是分不清指针,也许人家就不会写程序呢..对吧..

//

LPPER_HANDLE_DATA PerHandleData;

LPPER_IO_OPERATION_DATA PerIoData;

int i;

DWORD RecvBytes;

DWORD Flags;

DWORD ThreadID;

WSADATA wsaData;

DWORD Ret;

// 就像CoInitialize()一样的把Winsock 2.2服务启动起来...

if ((Ret = WSAStartup(0x0202, &wsaData)) != 0)

{

printf("WSAStartup failed with error %d\n", Ret);

return;

}

// CreateIoCompletionPort有两个不同的用途:1.创建完成端口;2.把已经创建好的完成端口和一个Socket关联/绑定起来.

// 下面这句话是它的第一个用途:创建完成端口

if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL)

{

printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError());

return;

}

//

// 下面这一些代码说得是:根据该服务器跑的机器上有几个CPU建立2*CPU数量的Thread.

// 这涉及到完成端口工作线程应该创建多少个的问题,在早期(MSDN 2000)Microsoft说

// 是WorkThreadNums = 2 * CPU + 2,现在的MSDN 2003说是WorkThreadNums = 2 * CPU;

//

GetSystemInfo(&SystemInfo);

for(i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)

{

HANDLE ThreadHandle;

// 创建一个线程并把上面创建的完成端口传给线程

if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort,

0, &ThreadID)) == NULL)

{

printf("CreateThread() failed with error %d\n", GetLastError());

return;

}

// 这个风格我喜欢,创建完Thread就把人家的Handle给release,省得要是线程异常退出搞得资源泄漏之类的麻烦事情.

CloseHandle(ThreadHandle);

}

// 开始创建监听Socket...

if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)

{

printf("WSASocket() failed with error %d\n", WSAGetLastError());

return;

}

InternetAddr.sin_family = AF_INET;

InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);

InternetAddr.sin_port = htons(PORT);

// 把监听socket和绑定TCP/IP协议栈做一个绑定...

if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR)

{

printf("bind() failed with error %d\n", WSAGetLastError());

return;

}

// 设置一次能接受5个连接

if (listen(Listen, 5) == SOCKET_ERROR)

{

printf("listen() failed with error %d\n", WSAGetLastError());

return;

}

//

// 下面是服务器程序标准的循环了...

//

while(TRUE)

{

if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR)

{

printf("WSAAccept() failed with error %d\n", WSAGetLastError());

return;

}

//

// 接收到一个连接就new一个新的socket并承揽下连接进来的socket的接下来的接待/处理工作

// 为什么不用new呢?我想是因为用Windows提供的内存管理函数,这样的话在业务量大的情况下

// 出现内存匮乏的情况下不至于直接就异常了,怎么着也就是该次的内存分配失败...

//

if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL)

{

printf("GlobalAlloc() failed with error %d\n", GetLastError());

return;

}

// Associate the accepted socket with the original completion port.

printf("Socket number %d connected\n", Accept);

PerHandleData->Socket = Accept;

//

// 把accept socket和IO完成端口关联/绑定起来,并把accept socket传给IOCP机制这样的话

// 当调用GetQueuedCompletionStatus的时候就可以得到该值.

//

if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,

0) == NULL)

{

printf("CreateIoCompletionPort failed with error %d\n", GetLastError());

return;

}

// 下面这个结构体也是会被传到工作线程中去的,怎么传呢?注意看下面红色部分

if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,sizeof(PER_IO_OPERATION_DATA))) == NULL)

{

printf("GlobalAlloc() failed with error %d\n", GetLastError());

return;

}

ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->BytesSEND = 0;

PerIoData->BytesRECV = 0;

PerIoData->DataBuf.len = DATA_BUFSIZE;

PerIoData->DataBuf.buf = PerIoData->Buffer;

Flags = 0;

//

// 在accept socket上面发起IO请求:WSARecv()以此激发IOCP工作机制..

// 注意WSARecv的第五个参数,也就是说我这时候告诉WSARecv的不仅仅是

// PerIoData->Overlapped的地址同时也是PerIoData的地址...

//

if (WSARecv(Accept, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,

&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)

{

if (WSAGetLastError() != ERROR_IO_PENDING)

{

printf("WSARecv() failed with error %d\n", WSAGetLastError());

return;

}

}

}

}

//

// 工作线程

//

DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)

{

HANDLE CompletionPort = (HANDLE) CompletionPortID;

DWORD BytesTransferred;

LPOVERLAPPED Overlapped;

LPPER_HANDLE_DATA PerHandleData;

LPPER_IO_OPERATION_DATA PerIoData;

DWORD SendBytes, RecvBytes;

DWORD Flags;

while(TRUE)

{

//

// 这个函数解释清楚了整个机制也就明白了...

// 首先我们看CompletionPort应该没有问题,是从创建线程的主线程传过来的.

// BytesTransferred指明了这次IO传输了多少Bytes...

// PerHandleData是CreateIoCompletionPort的时候穿过来的,MSDN里面叫CompletionKey.

// 请注意PerIoData这个参数,本来按照MSDN的解释如下:

// "Pointer to a variable that receives the address of the OVERLAPPED structure that was

// specified when the completed I/O operation was started."

// 其实在这这个lpOverlapped传过来的就是上面WSARecv传入的overlapped的地址...

// 这下明白了吧:主线程借着overlapped的名声传了一个结构体指针过来..:-)

//

if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,

(LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)

{

printf("GetQueuedCompletionStatus failed with error %d\n", GetLastError());

return 0;

}

//

// First check to see if an error has occured on the socket and if so

// then close the socket and cleanup the SOCKET_INFORMATION structure

// associated with the socket.

//

if (BytesTransferred == 0)

{

printf("Closing socket %d\n", PerHandleData->Socket);

if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)

{

printf("closesocket() failed with error %d\n", WSAGetLastError());

return 0;

}

GlobalFree(PerHandleData);

GlobalFree(PerIoData);

continue;

}

//

// Check to see if the BytesRECV field equals zero. If this is so, then

// this means a WSARecv call just completed so update the BytesRECV field

// with the BytesTransferred value from the completed WSARecv() call.

//

if (PerIoData->BytesRECV == 0)

{

PerIoData->BytesRECV = BytesTransferred;

PerIoData->BytesSEND = 0;

}

else

{

PerIoData->BytesSEND += BytesTransferred;

}

if (PerIoData->BytesRECV > PerIoData->BytesSEND)

{

//

// Post another WSASend() request.

// Since WSASend() is not gauranteed to send all of the bytes requested,

// continue posting WSASend() calls until all received bytes are sent.

//

ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.buf = PerIoData->Buffer + PerIoData->BytesSEND;

PerIoData->DataBuf.len = PerIoData->BytesRECV - PerIoData->BytesSEND;

if (WSASend(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &SendBytes, 0,

&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)

{

if (WSAGetLastError() != ERROR_IO_PENDING)

{

printf("WSASend() failed with error %d\n", WSAGetLastError());

return 0;

}

}

}

else

{

PerIoData->BytesRECV = 0;

// Now that there are no more bytes to send post another WSARecv() request.

Flags = 0;

ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

PerIoData->DataBuf.len = DATA_BUFSIZE;

PerIoData->DataBuf.buf = PerIoData->Buffer;

if (WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,

&(PerIoData->Overlapped), NULL) == SOCKET_ERROR)

{

if (WSAGetLastError() != ERROR_IO_PENDING)

{

printf("WSARecv() failed with error %d\n", WSAGetLastError());

return 0;

}

}

}

}

}

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有