完成端口模型
是什么?
利用线程池处理异步I/O请求,利用完成端口模型可以管理成百上千Socket。
可以把完成端口看成系统维护的一个队列,操作系统把重叠I/O操作完成的事件通知放到该队列中,因此称其为“完成”端口,当Socket被创建后,可以将其与一个完成端口联系起来。
一个应用程序可以创建多个工作线程用于处理完成端口上的通知事件,通常应该为每个CPU创建一个线程。
一个完成端口实际就是一个通知队列,操作系统把已经完成的重叠I/O请求的通知放到队列中,当某项IO操作完成后,系统会向服务端完成端口发送一个i/o完成数据包,此操作在系统内部完成,应用程序在收到I/o完成数据包后,完成端口队列的一个线程被唤醒,为客户端提供服务,服务完成后,该线程会继续在完成端口上等待.
为什么?
(1) 首先,如果使用“同步”的方式来通信的话,这里说的同步的方式就是说所有的操作都在一个线程内顺序执行完成,这么做缺点是很明显的:因为同步的通信操作会阻塞住来自同一个线程的任何其他操作,只有这个操作完成了之后,后续的操作才可以完成;一个最明显的例子就是咱们在MFC的界面代码中,直接使用阻塞Socket调用的代码,整个界面都会因此而阻塞住没有响应!所以我们不得不为每一个通信的Socket都要建立一个线程,多麻烦?这不坑爹呢么?所以要写高性能的服务器程序,要求通信一定要是异步的。
(2) 各位读者肯定知道,可以使用使用“同步通信(阻塞通信)+多线程”的方式来改善(1)的情况,那么好,想一下,我们好不容易实现了让服务器端在每一个客户端连入之后,都要启动一个新的Thread和客户端进行通信,有多少个客户端,就需要启动多少个线程,对吧;但是由于这些线程都是处于运行状态,所以系统不得不在所有可运行的线程之间进行上下文的切换,我们自己是没啥感觉,但是CPU却痛苦不堪了,因为线程切换是相当浪费CPU时间的,如果客户端的连入线程过多,这就会弄得CPU都忙着去切换线程了,根本没有多少时间去执行线程体了,所以效率是非常低下的,承认坑爹了不? (3) 而微软提出完成端口模型的初衷,就是为了解决这种"one-thread-per-client"的缺点的,它充分利用内核对象的调度,只使用少量的几个线程来处理和客户端的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能,
怎么用?
服务端:
1.初始化windows socket 环境
2.创建完成端口对象completionPort
3.根据当前计算机CPU的数量创建工作线程,并将新建的完成端口对象CompletionPort作为线程的参数
4.创建监听SocketListen 并将其绑定到本地地址 端口
5.在while循环中处理来自客服端的请求连接,接收连接,并将得到的与客户端进行通信的socketAccept保存到PER_HANDLE_DATA结构体对象PerHandleData中.将SocketAccept与前面的端口completionPort关联
6.在socket accept上调用 WSARecv函数,异步接收socket上来自客户端的数据,WSARecv函数立即返回,此时socketAccept上不一定有客户端发送来的消息,在工作线程中会检测完成端口对象的状态,并接收来自客户端的数据,再将这些数据发送回客户端程序.
// CompletionPortServer.cpp : 定义控制台应用程序的入口点。 // #include "stdafx.h" #include <winsock2.h> #include <windows.h> #include <stdio.h> #define PORT 9990 // 监听的端口 #define DATA_BUFSIZE 8192 // 发送和接收消息的最大长度 #pragma comment(lib, "Ws2_32") // 定义I/O操作的结构体 typedef struct { OVERLAPPED Overlapped; // 重叠结构 WSABUF DataBuf; // 缓冲区对象 CHAR Buffer[DATA_BUFSIZE]; // 缓冲区数组 DWORD BytesSEND; // 发送字节数 DWORD BytesRECV; // 接收的字节数 } PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA; // 套接字句柄结构体 typedef struct { SOCKET Socket; } PER_HANDLE_DATA, * LPPER_HANDLE_DATA; // 服务器端工作线程 DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID); int _tmain(int argc, _TCHAR* argv[]) { SOCKADDR_IN InternetAddr; // 服务器地址 SOCKET Listen; // 监听套接字 SOCKET Accept; // 与客户端进行通信的套接字 HANDLE CompletionPort; // 完成端口句柄 SYSTEM_INFO SystemInfo; // 获取系统信息(这里主要用于获取CPU数量) LPPER_HANDLE_DATA PerHandleData; // 套接字句柄结构体 LPPER_IO_OPERATION_DATA PerIoData; // 定义I/O操作的结构体 DWORD RecvBytes; // 接收到的字节数 DWORD Flags; // WSARecv()函数中指定的标识位 DWORD ThreadID; // 工作线程编号 WSADATA wsaData; // Windows Socket初始化信息 DWORD Ret; // 函数返回值 // 初始化Windows Sockets环境 if ((Ret = WSAStartup(0x0202, &wsaData)) != 0) { printf("WSAStartup failed with error %d\n", Ret); return -1; } // 创建新的完成端口 if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0)) == NULL) { printf( "CreateIoCompletionPort failed with error: %d\n", GetLastError()); return -1; } // 获取系统信息 GetSystemInfo(&SystemInfo); // 根据CPU数量启动线程 for(int i = 0; i<SystemInfo.dwNumberOfProcessors * 2; i++) { HANDLE ThreadHandle; // 创建线程,运行ServerWorkerThread()函数 if ((ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread, CompletionPort, 0, &ThreadID)) == NULL) { printf("CreateThread() failed with error %d\n", GetLastError()); return -1; } CloseHandle(ThreadHandle); } // 创建监听套接字 if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) { printf("WSASocket() failed with error %d\n", WSAGetLastError()); return -1; } // 绑定到本地地址的9990端口 InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(PORT); if (bind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR) { printf("bind() failed with error %d\n", WSAGetLastError()); return -1; } // 开始监听 if (listen(Listen, 5) == SOCKET_ERROR) { printf("listen() failed with error %d\n", WSAGetLastError()); return -1; } // 监听端口打开,就开始在这里循环,一有socket连上,WSAAccept就创建一个socket, // 这个socket 和完成端口联上 while(TRUE) { // 等待客户连接 if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0)) == SOCKET_ERROR) { printf("WSAAccept() failed with error %d\n", WSAGetLastError()); return -1; } // 分配并设置套接字句柄结构体 if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA))) == NULL) { printf("GlobalAlloc() failed with error %d\n", GetLastError()); return -1; } PerHandleData->Socket = Accept; // 将与客户端进行通信的套接字Accept与完成端口CompletionPort相关联 if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData, 0) == NULL) { printf("CreateIoCompletionPort failed with error %d\n", GetLastError()); return -1; } // 为I/O操作结构体分配内存空间 if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,sizeof(PER_IO_OPERATION_DATA))) == NULL) { printf("GlobalAlloc() failed with error %d\n", GetLastError()); return -1; } // 初始化I/O操作结构体 ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED)); PerIoData->BytesSEND = 0; PerIoData->BytesRECV = 0; PerIoData->DataBuf.len = DATA_BUFSIZE; PerIoData->DataBuf.buf = PerIoData->Buffer; Flags = 0; // 接收数据,放到PerIoData中,而perIoData又通过工作线程中的ServerWorkerThread函数取出, 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 -1; } } } return 0; } // 工作线程,循环检测完成端口状态,获取PerIoData中的数据 DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID) { HANDLE CompletionPort = (HANDLE) CompletionPortID; // 完成端口句柄 DWORD BytesTransferred; // 数据传输的字节数 LPOVERLAPPED Overlapped; // 重叠结构体 LPPER_HANDLE_DATA PerHandleData; // 套接字句柄结构体 LPPER_IO_OPERATION_DATA PerIoData; // I/O操作结构体 DWORD SendBytes, RecvBytes; // 发送和接收的数量 DWORD Flags; // WSARecv()函数中的标识位 while(TRUE) { // 检查完成端口的状态 if (GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0) { printf("GetQueuedCompletionStatus failed with error %d\n", GetLastError()); return 0; } // 如果数据传送完了,则退出 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; } // 如果还没有记录接收的数据数量,则将收到的字节数保存在PerIoData->BytesRECV中 if (PerIoData->BytesRECV == 0) { PerIoData->BytesRECV = BytesTransferred; PerIoData->BytesSEND = 0; } else // 如果已经记录了接收的数据数量,则记录发送数据量 { PerIoData->BytesSEND += BytesTransferred; } // 将收到的数据原样发送回客户端 if (PerIoData->BytesRECV > PerIoData->BytesSEND) { ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED)); //清0为发送准备 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; 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; } } } } }
具体参照
《windows网络编程》第八章 8.7 基于完成端口模型的服务器编程
完成端口(CompletionPort)详解 - 手把手教你玩转网络编程系列之三
单播,组播,多播
是什么?
单播,1 对 1
组播,1 对 多
广播,1 对 局域网所有
为什么?
单播,基于tcp,建立可靠连接,需要c/s ip;
组播,基于udp,监听同一ip(224.0.0.0至239.255.255.255)的一组都会接 收到消息;
广播,基于udp,监听 地址:xx.xx.xx.255 都会接收到消息
用途
单播用于连接建立后的私密通讯,组播和广播用于通知,建立连接前的通讯,获取ip。
安全套接字协议SSL
是什么?
SSL 可以用来保障在internet上数据传输的安全,利用数据加密技术,可确保数据在网络上的传输过程不会被截取及其监听.
SSL用于在web服务器和浏览器之间建立加密连接的标准安全技术.
SSL协议提供的安全信道有以下三个特性:
私密性。因为在握手协议定义了会话密钥后,所有的消息都被加密。
确认性。因为尽管会话的客户端认证是可选的,但是服务器端始终是被认证的。
可靠性。因为传送的消息包括消息完整性检查(使用MAC)。
主要工作在应用层(http,ftp,telnet)和 传输层(tcp/udp)之间的
参考
<<windows网络编程代码>>以及广播代码
百度网盘代码 提取码:72y7