1、 TCP服务端
① 先WSAStartup->检测DLL版本 信号检测
② socket() ->创建套接字 买了个手机
③ bind ->绑定IP、端口号 办了个卡
④ listen ->监听 手机待机
⑤ accept ->接收客户端连接 把对方保存到通讯录
⑥ send/recv ->收发消息 和客户端通讯
⑦ 。。。。收尾WSACleanup closesocket
2、 客户端
不需要listen,不需要accept,直接connect
甚至不需要明显的bind
一. 消息选择模型
① 创建MFC程序
② 创建自定义消息#define MY_SOCKET WM_USER+100
③ 创建自定义消息响应函数
④ 初始化、创建、绑定、监听套接字,不需要accept,把accept交给系统
// 初始化服务器
BOOL CTCPServer::InitServer(char* strIP, short port)
{
WSADATA wsd; //实际加载的库信息
m_bInitSuccess = FALSE;
// 1.wsastartup: 初始化winsock环境
if (WSAStartup(MAKEWORD(2, 2), &wsd))
{
printf("WSAStartupfailed!\n");
return false;
}
//检查实际加载的库版本是否是2.2
if (LOBYTE(wsd.wVersion) != 2 || HIBYTE(wsd.wVersion) != 2)
{
WSACleanup();
return false;
}
//2.创建套接字(服务器监听套接字)
m_sListenHostSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == m_sListenHostSock)
{
printf("socketfailed! \n");
WSACleanup();
return false;
}
//3.设置服务器地址
m_addrServer.sin_family = AF_INET;
m_addrServer.sin_addr.s_addr = (inet_addr(strIP));
m_addrServer.sin_port = htons(port);
//4.绑定套接字(绑定监听顺序可以调换)
int retVal = bind(m_sListenHostSock,
(sockaddr *)&m_addrServer,
sizeof(SOCKADDR_IN));
if (SOCKET_ERROR == retVal)
{
printf("bindfailed! \n");
closesocket(m_sListenHostSock);
WSACleanup();
return false;
}
//5.监听套接字
retVal = listen(m_sListenHostSock, 1);
if (SOCKET_ERROR == retVal)
{
printf("listenfailed! \n");
closesocket(m_sListenHostSock);
WSACleanup();
return false;
}
m_bInitSuccess = TRUE;
return true;
}
⑤ 在按钮点击事件里,启动服务器
if (m_clsServer.InitServer("127.0.0.1",1234))
⑥ 托管参数1的参数4事件给系统,如果事件发生,系统给参数2的窗口发送参数3消息
WSAAsyncSelect(m_clsServer.m_sListenHostSock, m_hWnd, MY_SOCKET, FD_ACCEPT | FD_CLOSE);
⑦ 因为监视的是服务器套接字的FD_ACCEPT | FD_CLOSE事件
所以当有客户端连接进来的时候,我们的主窗口就会收到MY_SOCKET消息
⑧ 去处理MY_SOCKET消息
OnSocket函数(WPARAM,LPARAM)
其中:WPARAM就是触发该消息的socket,就是咱上面的m_clsServer.m_sListenHostSock
LPARAM:LOWORD(lparam)表示是FD_ACCEPT还是FD_CLOSE
HIWORD(lparam)表示网络错误->客户端流氓退出,不发送FD_CLOSE
就退出了,也就是右上角的X
⑨ 第1次触发的是FD_ACCEPT事件
case FD_ACCEPT:// 第1次一定触发accept事件
{
//调用accspt方法:将客户端socket保存起来
SOCKET sClient= m_clsServer.acceptClient();
/*
以前accept到客户端后要新建线程等客户端发消息
现在继续托管给系统,当客户端有消息过来时给我们发送MY_SOCKET消息
*/
WSAAsyncSelect(sClient, m_hWnd, MY_SOCKET, FD_READ | FD_CLOSE);
}
⑩ 以后收到的消息,就可能是多个客户端发来的消息,我们可以通过wparam来判断是哪个客户端发来的消息
二、事件选择模型
1、 初始化服务器
CTCPServer server;
// 启动服务器
if (server.InitServer("127.0.0.1",1234))
2、 创建内核对象(事件)->类似于消息选择模型的MY_SOCKET
WSAEVENT Event = WSACreateEvent();
3、 把内核对象和socket绑定->类似于消息选择模型的WSAAsyncSelect绑定自定义消息
// 将服务器socket和事件对象建立一个连接
参数1的参数3网络事件被触发了,就激活参数2内核对象
WSAEventSelect(server.m_sListenHostSock, Event, FD_ACCEPT | FD_CLOSE);
4、 创建等待事件对象被激活的线程
// 等待网络事件发生
HANDLE hThread;
hThread = CreateThread(NULL, 0, WSAEventSelectProc, &server, 0, NULL);
WSAEventSelectProc:函数内
//等待网络事件发生
int index = WSAWaitForMultipleEvents(size,
&g_vecEvent[0],// 等待的内核对象数组
FALSE,
WSA_INFINITE,
FALSE);
5、 如果等待的事件对象被激活
WSAWaitForMultipleEvents的返回值就是被激活的内核对象在数组中的索引值
//1. 通过获取事件的下标来确定是哪个socket/event
index = index - WSA_WAIT_EVENT_0;
6、确定触发的是什么网络事件
//2. 根据相应的套接字,获取其网络事件,第二个参数为事件对象,
// 如果传null,需要自己手动把信号关闭,WSAResetEvent(hEvent);
// 否则,此函数返回后会自动把信号置为无信号状态。
//被WSAWaitForMultipleEvents返回的事件对象还是有信号的
// 我们可以自己resetevent把它关闭,也可以把内核对象作为下面函数的第2个参数传进去,让它自动关闭信号
WSANETWORKEVENTS stcNetEvent;
WSAEnumNetworkEvents(g_vecSocket[index], g_vecEvent[index], &stcNetEvent);
6、通过WSANETWORKEVENTS结构体里面的值来判断触发的是什么事件
if (stcNetEvent.lNetworkEvents&FD_ACCEPT)
7、如果触发FD_ACCEPT事件,说明有客户端连接进来,继续把客户端socket托管给系统
WSAEVENT clientEvent = WSACreateEvent();
g_vecEvent.push_back(clientEvent);
g_vecSocket.push_back(clientSock);
// 如果参数1客户端的参数3网络事件触发了,我就激活参数2内核对象
// 这样等待参数2的函数就会返回-> WSAWaitForMultipleEvents
WSAEventSelect(clientSock, clientEvent, FD_READ | FD_CLOSE);
线程函数内的WSAWaitForMultipleEvents返回后
WSAEnumNetworkEvents枚举得到被触发的网络事件
总:
对于消息选择模型来说
我们要创建窗口、和自定义消息,把网络事件托管给系统
系统通过给窗口发送自定义消息来通知我们进行相应的网络事件处理
对于事件选择模型:
创建一个线程,等待内核对象,这个内核对象和socket绑定
消息选择模型 事件选择模型
网络事件FD_ACCEPT…和socket绑定 网络事件FD_ACCEPT…也和socket绑定
FD_ACCEPT通知窗口的自定义消息 FD_ACCEPT激活对应的内核对象(事件)
wparam是触发事件的socket 自己构造数组,和内核对象对应的数组
lparam低word知道触发了什么网络事件 WSAEnumNetworkEvents确定触发的什么事件
lparam高word知道发生了什么错误 WSAEnumNetworkEvents确定什么错误
只能用于窗口程序 一个线程一次最多等待64个
三、完成端口模型
1、初始化服务器
CTCPServer server;
// 1. 启动服务器
if (server.InitServer("127.0.0.1", 1234))
2.创建完成端口和完成端口线程
//2. 创建完成端口
g_hPort = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL, //完成端口
0, //完成键
0 //并发线程数量(0的话,默认允许与核心数量的相同的线程数(4)同时并发)
);
if (!g_hPort) {
printf("创建完成端口失败!");
return FALSE;
}
//3. 创建服务(工作)线程
// 获取CPU核心数量,并开启工作线程(一般是核心数量的两倍)
SYSTEM_INFO stcSystemInfo = { 0 }; //计算系统信息
GetSystemInfo(&stcSystemInfo); //获取计算机系统信息
for (DWORD i = 0; i < stcSystemInfo.dwNumberOfProcessors * 2; i++) {
HANDLE result = CreateThread(0, 0, ServerWorkerThread, //工作线程,处理完成端口收到的消息
(LPVOID)&server, 0, 0);
if (result == NULL) {
printf("创建工作线程失败!");
}
}
3.不同于消息模型和事件模型,完成端口模型的accept需要我们自己处理,
如果熟悉完成端口模型后,也可以交给完成端口模型
//4.accept接收连接
// (其实这些方法,都能封装到 TCPServer 里面去,变成一个类,但是呢,这就跟我们以前的代码杂糅在一起了)
// 分开写,利于我们着重理解 今天的模型,但是大家有时间可以好好的封装封装。
HANDLE hThread;
hThread = CreateThread(NULL, 0, AcceptThread, (LPVOID)&server, 0, NULL);
4.在accept线程里面
DWORD WINAPI AcceptThread(LPVOID lparam) {
CTCPServer *pThis = (CTCPServer*)lparam;
// 等待客户端连接
while (TRUE) {
// accept接收连接
// accept事件最好也应该用完成端口来响应,效率更高(使用acceptEx)
//取到新建立连接的 通信套接字
SOCKET clientSock = pThis->acceptClient();
// 创建完成键
// 就是传给完成端口的参数(自定义的结构,那么我们可以把通信套接字节点 当做完成键 )
// 将连接进来的客户端socket跟完成端口绑定,并为这个客户端绑定一个参数(完成键)
CreateIoCompletionPort(
(HANDLE)clientSock, // 客户端套接字与完成端口绑定
g_hPort,// 完成端口
(ULONG_PTR)clientSock,// 当客户端再发来消息,被完成端口响应到时,在工作线程中通知我们。
// 并且通知我们时,会携带这个参数的内容,到时就能用它了。
0);
// 投递消息(请求完成端口帮我接收消息/响应事件)
PMYOVERLAPPED pOverlapped = new MYOVERLAPPED;
memset(pOverlapped, 0, sizeof(MYOVERLAPPED)); //将OverLapped结构初始化为0
pOverlapped->stcWsaBuf.buf = pOverlapped->buf;
pOverlapped->stcWsaBuf.len = 1024;
DWORD RecvBytes = 0;
DWORD Flags = 0;
int dwError;
// 请求完成端口帮我接收recv事件触发时发过来的数据 (WSAsend)
dwError = WSARecv(
clientSock, //连接进来的客户端套接字
&(pOverlapped->stcWsaBuf), //WSABUF结构的地址
1, //希望接收的字节数,填1,故意失败,托管给系统
&RecvBytes, //返回客户端实际发送过来的字节数
&Flags, //标志位,常为0
(LPWSAOVERLAPPED)pOverlapped, //单I/O信息的结构体(重叠结构体)
NULL); //默认,由系统帮我们选择线程执行接收任务
}
return true;
}
5.accept线程接收到客户端请求后,和消息选择模型以及事件选择模型不一样的是
消息选择模型->绑定socket和自定义消息
事件选择模型->绑定socket和事件内核对象
完成端口模型->绑定socket和完成端口
// 创建完成键
// 就是传给完成端口的参数(自定义的结构,那么我们可以把 通信套接字节点 当做完成键 )
// 将连接进来的客户端socket跟完成端口绑定,并为这个客户端绑定一个参数(完成键)
CreateIoCompletionPort( (HANDLE)clientSock, // 客户端套接字与完成端口绑定
g_hPort,// 完成端口
(ULONG_PTR)clientSock,// 当客户端再发来消息,被完成端口响应到时,在工作线程中通知我们。
// 并且通知我们时,会携带这个参数的内容,到时就能用它了。
0);
完成键的作用:
怎么知道是哪个socket触发了网络事件
消息选择模型->wparam
事件选择模型->自定义数组
完成端口模型->绑定时设置的完成键
这样在完成端口任务线程里,
//Worker线程进入不占用CPU的睡眠状态,直到完成端口上出现了需要处理的网络操作或者超出了等待的时间限制为止。
BOOL bResult = GetQueuedCompletionStatus(
g_hPort, // 完成端口(唯一的一个)
&dwSize, // 实际传输数据的字节数
(PULONG_PTR)&clientSocket, // 完成键,使用CreateIoCompletionPort函数将客户端socket跟完成端口绑定时指定的
(LPOVERLAPPED*)&pOverLapped, // 单I/O操作,使用函数 WSARecv等 投递消息时指定的。
INFINITE);
6.当完成端口绑定了客户端socket的时候,它还不能替我们接收客户端发来的消息
和异步I/O一样,绑定完文件句柄,需要Read/WriteFile才能让完成端口替我们完成读写操作
我们需要向完成端口提交接收客户端通讯的Recv事件
// 投递消息(请求完成端口帮我接收消息/响应事件)
PMYOVERLAPPED pOverlapped = new MYOVERLAPPED;
memset(pOverlapped, 0, sizeof(MYOVERLAPPED)); //将OverLapped结构初始化为0
pOverlapped->stcWsaBuf.buf = pOverlapped->buf;
pOverlapped->stcWsaBuf.len = 1024;
DWORD RecvBytes = 0;
DWORD Flags = 0;
int dwError;
// 请求完成端口帮我接收recv事件触发时发过来的数据 (WSAsend)
dwError = WSARecv(
clientSock, //连接进来的客户端套接字
&(pOverlapped->stcWsaBuf), //WSABUF结构的地址
1, //希望接收的字节数,填1,故意失败,托管给系统
&RecvBytes, //返回客户端实际发送过来的字节数
&Flags, //标志位,常为0
(LPWSAOVERLAPPED)pOverlapped, //单I/O信息的结构体(重叠结构体)
NULL);
8、 当真正recv的时候,系统会把读取的内容放在&(pOverlapped->stcWsaBuf)指向的缓冲区里
我们去完成端口的等待线程内读取该内容,并继续提交recv请求
// 继续投递消息(请求完成端口帮我接收消息/响应事件)
// 因为每次投递完成端口只会帮我们监视一次,所以处理完要反复投递
memset(pOverLapped, 0, sizeof(MYOVERLAPPED)); //将OverLapped结构初始化为0
pOverLapped->stcWsaBuf.buf = pOverLapped->buf;
pOverLapped->stcWsaBuf.len = 1024;
DWORD RecvBytes = 0;
DWORD Flags = 0;
int dwError;
// 请求完成端口帮我接收recv事件触发时发过来的数据 (WSAsend)
dwError = WSARecv(
clientSocket, //连接进来的客户端套接字
&(pOverLapped->stcWsaBuf), //WSABUF结构的地址
1, //希望接收的字节数,填1,故意失败,托管给系统
&RecvBytes, //返回客户端实际发送过来的字节数
&Flags, //标志位,常为0
(LPWSAOVERLAPPED)pOverLapped, //单I/O信息的结构体(重叠结构体)
NULL); //默认,由系统帮我们选择线程执行接收任务
模型 | 套接字和谁绑定 | 套接字对应的FD_READ等事件触发时,怎么知道触发的是哪个socket | 怎么知道触发的是什么事件 | 错误信息 |
消息选择 | 窗口消息MY_SOCKET | wparam | LOWORD(LPARAM) | HIWORD(LPARAM) |
事件选择 | 事件内核对象 | 等待的事件内核对象数组的索引值定位自定义socket数组成员 | WSAEnumNetworkEvents获取,WSANETWORKEVENTS中的lNetworkEvents成员与对应的网络事件进行按位&运算得知 | WSANETWORKEVENTS的iErrorCode数组 |
完成端口 | 完成端口绑定 | 完成键 | 不需要知道,实在想知道,通过自定义OVERLAPPED结构体标记对应的事件 | GetQueuedCompletionStatus返回NULL |