【网络编程】完成端口 IOCP
10.11 完成端口
10.11.1 基本概念
完成端口的全称是I/O 完成端口,英文为IOCP(I/O Completion Port) 。IOCP是一个异 步I/O 的 API, 可以高效地将I/O 事件通知给应用程序。与使用select() 或是其他异步方法不同 的是,一个套接字与一个完成端口关联了起来,然后就可以继续进行正常的Winsock 操作了。 然而,当一个事件发生的时候,此完成端口就将被操作系统加入一个队列中。然后应用程序可 以对核心层进行查询以得到此完成端口。
这里我要对上面的一些概念略做补充,在解释“完成”两字之前,想再次复习一下同步和 异步这两个概念,从逻辑上来讲做完一件事后再去做另一件事就是同步,而同时一起做两件或 两件以上的事就是异步了。你也可以拿单线程和多线程来做比喻,但是我们一定要将同步和堵 塞、异步和非堵塞区分开来。
所谓的堵塞函数诸如accept(…), 当调用此函数后,线程将挂起, 直到操作系统通知它,“有人连进来了”,那个挂起的线程将继续进行工作,也就是符合“生 产者-消费者”模型。堵塞和同步看上去有两分相似,但却是完全不同的概念。大家都知道I/O 设备是一个相对慢速的设备,不论打印机、调制解调器还是硬盘,与CPU 相比都是奇慢无比 的,坐下来等I/O 的完成是不明智的,有时候数据的流动率非常惊人,把数据从你的文件服务 器中以Ethernet 速度搬走,其速度可能高达每秒一百万字节。如果你尝试从文件服务器中读取 100KB, 在用户的眼光来看几乎是瞬间完成,但是要知道,你的线程执行这个命令,已经浪费 了10个一百万次CPU 周期。
所以说,我们一般使用另一个线程来进行I/O。重叠IO(overlapped I/O) 是 Win32 的一项技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你。 这也就是“完成”的含义。这项技术使你的程序在I/O 进行的过程中仍然能够继续处理事务。 事实上,操作系统内部正是以线程来完成overlapped I/O。你可以获得线程所有利益,而不需要付出什么痛苦的代价。
完成端口中所谓的“端口”并不是我们在TCP/IP 中所提到的端口,可以说完全没有关系。 笔者其实也困惑一个I/O 设 备(I/O Device) 和端口(IOCP 中 的Port) 到底有什么关系。IOCP 只不过是用来进行读写操作,和文件I/O 倒是有些类似。既然是一个读写设备,我们所能要求 它的只是在处理读与写上的高效。
接着我们再来探究一下“完成”的含义。首先,它之所以叫“完成”端口,因为系统在网 络I/O 操作“完成”之后才会通知我们。也就是说,我们在接到系统通知的时候,其实网络操 作已经完成了(在系统通知我们的时候,并非是有数据从网络上到来,而是来自于网络上的数 据已经接收完毕了;或者是客户端的连入请求已经被系统接入完毕了,等等),我们只需要处 理后面的事情就好了。
各位同志可能会很开心,什么?已经处理完毕了才通知我们,那岂不是很爽?其实也没什 么爽的,那是因为我们在之前给系统分派工作的时候都嘱咐好了,我们会通过代码告诉系统“你 给我做这个做那个,等待做完了再通知我”,只是这些工作是做在之前还是之后的区别而已。
其次,我们需要知道,所谓的完成端口其实和 HANDLE 一样,也是一个内核对象, Windows 大师Jeff Richter曾说,“完成端口可能是最为复杂的内核对象了”,但是我们也不 用去管它复杂,因为具体的内部是如何实现的和我们无关,只要我们能够学会用它相关的API 把这个完成端口的框架搭建起来就可以了。我们暂时只用把它大体理解为一个容纳网络通信操 作的队列就好了,它会把网络操作完成的通知都放在这个队列里面,咱们只用从这个队列里面 取就行了,取走一个就少一个。
10.11.2 完成端口能干什么
完成端口会主动帮我们完成网络I/O 数据复制。这一点其实也就是他与其他网络模型最直 接的区别了。 一般网络操作包括两个步骤,以recv 来说,如果是一般模型,那么其第一步是 通知等待的线程有数据可以读取,这时线程会调用recv 或 者recvfrom 等函数将数据从读缓冲 区复制到用户空间,然后做下一步的处理,而IOCP 能帮我们的是,它会在内核中帮我们监听 那些我们感兴趣的事件。
例如,我们希望接收客户端数据,那么我们向完成端口投递一个读事 件,完成端口在监测有读事件到来的时候会主动地去帮我们把数据从内存空间复制到用户空 间,然后通知我们过来取数据就可以了,这就是IOCP 提供的方便之处。
另外,IOCP 在内部管理线程,实现负载平衡。上面提到了Windows 的 alertable I/O的 负 载均衡是它的一个弊端,那么IOCP 是如何自己管理线程调度的呢?简单地说就是以栈的方式 进行管理。
10.11.3 完成端口的优势
完成端口会充分利用Windows 内核来进行I/O 的调度,是用于C/S 通信模式中性能最好 的网络通信模型,没有之一;甚至连和它性能接近的通信模型都没有。
微软提出完成端口模型的初衷就是为了解决同步方式那种一个线程处理一个客户端的模式 (one-thread-per-client)缺点的,它充分利用内核对象的调度,只使用少量的几个线程来处 理和客户端的所有通信,消除了无谓的线程上下文切换,最大限度地提高了网络通信的性能。
相比于其他异步模型,对于内存占用都是差不多的,真正的差别就在于CPU 的占用,其他的网络模型都需要更多的CPU 动力来支撑同样的连接数据。
完成端口被广泛地应用于各个高性能服务器程序上,例如著名的Apache 服务器,如果你 想要编写的服务器端需要同时处理的并发客户端连接数量有数百上千个,那不用纠结了,就是 它 了 。
总而言之,完成端口的优势就是效率高。在完成端口模型中,我们会实现开好几个线程, 一般是有多少个CPU 就开多少个线程(其实一般是CPU*2 个 ) 。 建 立CPU*2 个线程的好处 是,在一个工作线程被Sleep( 或 者WaitForSingleObject() 被停止的情况下,IOCP 能唤醒同在 一 个CPU 上的另一个线程代替这个Sleep的线程继续执行,这样完成端口就实现了CPU 的 满 负荷工作,效率也就高了。
这样做的好处是可以避免线程的上下文切换。然后让这几个线程等 待,当有用户请求来到的时候,就把这些请求添加到一个公共的消息队列中去。这个时候我们 刚刚开好的那几个线程就有用了,他们会排队逐个去消息队列中提取消息,并加以处理。(其 实这就是一个线程池处理消息的过程, 一个线程队列,一个消息队列,线程队列不断获取消息 队列中的消息。)这种方式很优雅地实现了异步通信和负载均衡的问题,并且线程在没事干的 时候会被系统挂起来,不会占用CPU 周期。
举个例子:假设有100万个用户同时与一个进程保持着TCP 连接,而每一个时刻只有几十或几百个 TCP 连接,所以我们只需要处理100万连接中的一小部分连接,在使用别的模型时只能通过 select的方式对所有的连接都遍历一遍,查询出其中有事件的连接。可想而知,这种查询方式 效率是多么的低下!这时我们的完成端口就闪亮登场了。完成端口是这么干的: 一旦一个连接 上有事件发生,它就会立即将事件组成一个完成包放入到完成端口中(其实就是放入到一个队 列里面),这时我们事先开启的等待线程就可以直接从该队列中取出该事件了,就避免了select 的查询,效率也就提高了很多,同一时间的用户量越大,效率越明显!
10.11.4 完成端口编程的基本流程
总体上讲,使用完成端口只用遵循如下几个编程步骤:
- (1)调用 CreateloCompletionPort() 函数创建一个完成端口,而且在一般情况下,我们需 要且只需要建立这一个完成端口。把它的句柄保存好,我们今后会经常用到它。
- (2)创建一个工作者线程A, 实际上会根据系统中有多少个处理器就建立多少个工作者 线程,这几个线程是专门用来和客户端进行通信的,目前暂时没有什么工作。这里为了说明原 理,我们就说创建一个工作者线程A。
- (3)A 线程循环调用GetQueuedCompletionStatus (函数来得到I/O 操作结果,这个函数是 一个阻塞函数。
- (4)主线程循环里调用accept 等待客户端连接上来。
- (5)主线程里accept 返回新连接建立以后,把这个新的套接字句柄用CreateloCompletionPortO关联到完成端口,然后发出一个异步的WSASend 或者WSARecv 调用以提交I/O 操作,因为 是异步函数,WSASend/WSARecv 会马上返回,实际的发送或者接收数据的操作由WINDOWS 系统去做。
- (6)主线程继续下一次循环,阻塞在accept这里等待客户端连接。
- (7)Windows 系统完成WSASend 或者WSArecv 的操作,把结果发到完成端口。
- (8)A 线程里的 GetQueuedCompletionStatus) 马上返回,并从完成端口取得刚完成的 WSASend/WSARecv的结果。
- ( 9 ) 在A 线程里对这些数据进行处理(如果处理过程很耗时,需要新开线程处理),然 后接着发出WSASend/WSARecv, 并继续下一次循环阻塞在GetQueuedCompletionStatus()。
10.11.5 相 关API
10.11.5.1 函 数CreateloCompletionPort
该函数创建一个输入/输出(I/O) 完成端口并将其与指定的文件句柄关联,或者创建尚未 与文件句柄关联的I/O 完成端口,允许以后进行关联。该函数声明如下:
HANDLE CreateIoCompletionPort(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR CompletionKey,DWORD NumberOfConcurrentThreads
);
- FileHandle: 完成端口用来关联的一个文件句柄,当使用 CreateFile 函数创建文件句 柄的时候你必须指定该句柄包含 FILE_FLAG_OVERLAPPED 标志位。如果 FileHandle 的值是INVALID_HANDLE_VALUE,CreateloCompletionPort 将会创建 一个不和任何文件关联的完成端口。在这种情况下,参数ExistingCompletionPort 必 须是NULL 并且参数CompletionKey 的内容将会被无视。
- ExistingCompletionPort: 现 有I/O 完成端口的句柄或NULL, 如果此参数为现有I/O 完成端口,那么该函数将其与FileHandle 参数指定的句柄相关联。如果成功,函数就 返回现有I/O 完成端口的句柄。如果此参数为NULL, 则该函数将创建一个新的I/O 完成端口。如果 FileHandle 参数有效,则将其与新的I/0 完成端口相关联。否则, 不会发生文件句柄关联。如果成功,那么该函数将把句柄返回给新的I/O 完成端口。
- CompletionKey: 该值就是类似线程里面传递的 一个参数,我们在 GetQueuedCompletionStatus 中第三个参数获得的就是这个值。
- NumberOfConcurrentThreads:如果此参数为NULL, 那么系统允许与系统中的处理器 一样多的并发运行的线程。如果 ExistingCompletionPort参数不是NULL, 则忽略此参数 。
如果函数执行成功,返回值一定是一个完成端口的地址;如果函数执行失败,就返回NULL。可以调用GetLastError 函数去获得详细的错误信息。
I/O系统可以指示发送I/O完成通知到完成端口,它们在那里排队,CreateloCompletionPort 函数提供了这个功能。完成端口的句柄是一个智能指针,没有人调用的话就会被释放。如果想 要释放完成端口的句柄,那么每个与它关联的文件句柄都必须被释放,然后调用CloseHandle 函数去释放完成端口的句柄。与完成端口关联的文件句柄不能够再被 ReadFileEx 或者 WriteFileEx 函数调用。最好是不要分享这种关联的文件或者继承或调用DuplicateHandle 函数。 这种复制句柄的操作将会产生完成消息通知。
执行一个文件的I/O 操作处理具有关联的I/O 完成端口,在I/O 操作完成时I/O 系统发送 完成通知包到完成端口。该 I/O 完成端口的完成包在 一 个先入先出队列中。使用 GetQueuedCompletionStatus函数来检索这些排队的I/O完成数据包。在同一进程中线程可以使 用 PostQueuedCompletionStatus函数放置在一个完成端口的队列中的I/O 完成通知包。通过这 样做,你可以使用完成端口去接收从进程的其他线程通信,除了接受来自I/O 系统的I/O 完成 通知包。
10.11.5.2 函 数GetQueuedCompletionStatus
该函数尝试从指定的I/O完成端口将I/O完成数据包出列,通俗点说,就是从完成端口中 获取已经完成的消息。如果没有完成数据包排队,那么函数等待与完成端口关联的挂起I/O 操 作完成。函数声明如下:
BOOL WINAPI GetQueuedCompletionStatus(In_ HANDLE CompletionPort,Out_LPDWORD lpNumberOfBytes, _Out_PULONG_PTR lpCompletionKey, _Out_LPOVERLAPPED *lpOverlapped,In DWORD dwMilliseconds);
- CompletionPort: 完成端口的句柄。
- lpNumberOfBytes: 该变量接收已完成的I/O 操作期间传输的字节数。
- lpCompletionKey: 该变量接收CreateloCompletionPort 中传递的第三个参数。
- lpOverlapped: 接收完成的I/O 操作启动时指定的OVERLAPPED 结构的地址。我们 可以通过 CONTAINING_RECORD 这个宏获取以该重叠结构为首地址的结构体信 息,也就是该重叠结构为什么必须放在结构体首地址的原因。
- dwMilliseconds: 超时时间(毫秒),如果为 INFINITE 就一直等待,直到有消息到
来。
如果函数成功就返回TRUE, 失败则返回 FALSE 。如果设置了超时时间,超时将返回FALSE。
10.11.5.3 宏 CONTAINING_RECORD
该宏返回给定结构类型的结构实例的基地址和包含结构中字段的地址。该宏定义如下:
PCHAR CONTAINING RECORD([in] PCHAR Address,[in] TYPE Type, [in] PCHAR Field);
- Address: 通 过GetQueuedCompletionStatus 获取的重叠结构。
- Type: 以重叠结构为首地址的结构体。
- Field:Type 结构体的重叠结构变量。
返回包含Field 域(成员)的结构体的基地址。
为了更好地理解原理,下面看一个简单的例子。服务器端使用完成端口接收来自客户端发 送过来的TCP 消息,进行显示,并发送确认消息(ack) 给客户端,客户端再把收到的消息显示出 来 。
【例10.8】一个简单的端口实例
服务端
// serv.cpp : 定义控制台应用程序的入口点。
//#include <WinSock2.h>#pragma comment(lib, "Ws2_32.lib") // Socket编程需用的动态链接库
#pragma comment(lib, "Kernel32.lib") // IOCP需要用到的动态链接库#define BUFFER_SIZE 1024
#define OP_READ 18
#define OP_WRITE 28
#define OP_ACCEPT 38
#define CHECK_CODE 0x010110BOOL bStopThread = false;typedef struct _PER_HANDLE_DATA
{SOCKET s;sockaddr_in addr; // 客户端地址char buf[BUFFER_SIZE];int nOperationType;
}PER_HANDLE_DATA, *PPER_HANDLE_DATA;#pragma pack(1)
typedef struct MsgAsk
{int iCode;int iBodySize;char szBuffer[32];
}MSG_ASK, *PMSG_ASK;typedef struct MsgBody
{int iBodySize;int iOpType;char szBuffer[64];
}MSG_BODY, *PMSG_BODY;typedef struct MsgAck
{int iCheckCode;char szBuffer[32];
}MSG_ACK, *PMSG_ACK;
#pragma pack()DWORD WINAPI ServerWorkThread(LPVOID lpParam)
{// 得到完成端口句柄HANDLE hCompletion = (HANDLE)lpParam;DWORD dwTrans;PPER_HANDLE_DATA pPerHandle;OVERLAPPED* pOverLapped;while (!bStopThread){// 在关联到此完成端口的所有套接字上等待I/O完成BOOL bOK = ::GetQueuedCompletionStatus(hCompletion,&dwTrans, (PULONG_PTR)&pPerHandle, &pOverLapped, WSA_INFINITE);if (!bOK){::closesocket(pPerHandle->s);::GlobalFree(pPerHandle);::GlobalFree(pOverLapped);continue;}switch(pPerHandle->nOperationType){case OP_READ:{MSG_ASK msgAsk = {0};memcpy(&msgAsk, pPerHandle->buf, sizeof(msgAsk));if (msgAsk.iCode != CHECK_CODE|| msgAsk.iBodySize != sizeof(msgAsk)){printf("error\n");}else{msgAsk.szBuffer[strlen(msgAsk.szBuffer) + 1] = '\n';printf(msgAsk.szBuffer);printf("Recv bytes = %d, msgAsk.size = %d\n", dwTrans, msgAsk.iBodySize);}MSG_BODY msgBody = {0};memcpy(&msgBody, pPerHandle->buf + msgAsk.iBodySize, sizeof(MSG_BODY));if (msgBody.iOpType == OP_READ && msgBody.iBodySize == sizeof(MSG_BODY)){printf("msgBody.szBuffer = %s\n", msgBody.szBuffer);}MSG_ACK msgAck = {0};msgAck.iCheckCode = CHECK_CODE;memcpy(msgAck.szBuffer, "This is the ack package",strlen("This is the ack package"));// 继续投递发送I/O请求pPerHandle->nOperationType = OP_WRITE;WSABUF buf;buf.buf = (char*)&msgAck;buf.len = sizeof(MSG_ACK);OVERLAPPED *pol = (OVERLAPPED *)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));DWORD dwFlags = 0, dwSend = 0;::WSASend(pPerHandle->s, &buf, 1, &dwSend, dwFlags, pol, NULL); }break;case OP_WRITE:{if (dwTrans == sizeof(MSG_ACK)){printf("Transfer successfully\n");}// 然后投递接收I/O请求}break;case OP_ACCEPT:break;}}return 0;
}DWORD InitWinsock()
{DWORD dwRet = 0;WSADATA wsaData; dwRet = WSAStartup(MAKEWORD(2,2), &wsaData); if (dwRet != NO_ERROR) { printf("error code = %d\n", GetLastError()); dwRet = GetLastError(); } return dwRet;
}void UnInitWinsock()
{WSACleanup();
}int main(int argc, _TCHAR* argv[])
{int nPort = 6000;InitWinsock();// 创建完成端口对象HANDLE hCompletion = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);if (hCompletion == NULL){DWORD dwRet = GetLastError();return dwRet;}// 确定处理器的核心数量SYSTEM_INFO mySysInfo;GetSystemInfo(&mySysInfo);#if 1// 基于处理器的核心数量创建线程for(DWORD i = 0; i < (mySysInfo.dwNumberOfProcessors * 2); ++i){// 创建服务器工作器线程,并将完成端口传递到该线程HANDLE ThreadHandle = CreateThread(NULL, 0, ServerWorkThread, hCompletion, 0, NULL);if(NULL == ThreadHandle){printf("Create Thread Handle failed. Error:%d",GetLastError());//system("pause");return -1;}CloseHandle(ThreadHandle);}
#else::CreateThread(NULL, 0, ServerWorkThread, (LPVOID)hCompletion, 0, 0);
#endif// 创建监听套接字SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);SOCKADDR_IN si;si.sin_family = AF_INET;si.sin_port = ::htons(nPort);si.sin_addr.s_addr = INADDR_ANY;::bind(sListen, (sockaddr*)&si, sizeof(si));::listen(sListen, 10);while (TRUE){SOCKADDR_IN saRemote;int nRemoteLen = sizeof(saRemote);printf("Accepting...\n");SOCKET sNew = ::accept(sListen, (sockaddr*)&saRemote, &nRemoteLen);//SOCKET sNew = ::accept(sListen, NULL, NULL);if (sNew == INVALID_SOCKET){continue;}printf("Accept one!\n");// 接受新连接后,创建一个per-handle数据,并关联到完成端口对象PPER_HANDLE_DATA pPerHandle = (PPER_HANDLE_DATA)::GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));pPerHandle->s = sNew;memcpy(&pPerHandle->addr, &saRemote, nRemoteLen);pPerHandle->nOperationType = OP_READ;::CreateIoCompletionPort((HANDLE) pPerHandle->s, hCompletion, (ULONG_PTR)pPerHandle, 0);// 投递一个接收请求OVERLAPPED *pol = (OVERLAPPED *)::GlobalAlloc(GPTR, sizeof(OVERLAPPED));WSABUF buf;buf.buf = pPerHandle->buf;buf.len = BUFFER_SIZE;DWORD dwRecv = 0;DWORD dwFlags = 0;::WSARecv(pPerHandle->s, &buf, 1, &dwRecv, &dwFlags, pol, NULL);}return 0;
}
客户端
// client.cpp : 定义控制台应用程序的入口点。
//#include <WinSock2.h>#pragma comment(lib, "Ws2_32.lib") // Socket编程需用的动态链接库
#pragma comment(lib, "Kernel32.lib") // IOCP需要用到的动态链接库#define CHECK_CODE 0x010110
#define OP_READ 18
#define OP_WRITE 28
#define OP_ACCEPT 38#pragma pack(1)typedef struct MsgAsk
{int iCode;int iBodySize;char szBuffer[32];
}MSG_ASK, *PMSG_ASK;typedef struct MsgBody
{int iBodySize;int iOpType;char szBuffer[64];
}MSG_BODY, *PMSG_BODY;typedef struct MsgAck
{int iCheckCode;char szBuffer[32];
}MSG_ACK, *PMSG_ACK;#pragma pack()DWORD SendAll(SOCKET &clientSock, char* buffer, int size)
{DWORD dwStatus = 0;char *pTemp = buffer;int total = 0, count = 0;while(total < size){count = send(clientSock, pTemp, size - total, 0);if(count < 0){dwStatus = WSAGetLastError();break;}total += count;pTemp += count;}return dwStatus ;
}DWORD RecvAll(SOCKET &sock, char* buffer, int size)
{ DWORD dwStatus = 0; char *pTemp = buffer; int total = 0, count = 0; while (total < size) { count = recv(sock, pTemp, size-total, 0); if (count < 0) { dwStatus = WSAGetLastError(); break; } total += count; pTemp += count; } return dwStatus;
} int _tmain(int argc, _TCHAR* argv[])
{WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2,2), &wsaData); if (iResult != NO_ERROR) { printf("error code = %d\n", GetLastError()); return -1; } sockaddr_in clientAddr; clientAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); clientAddr.sin_family = AF_INET; clientAddr.sin_port = htons(6000); SOCKET clientSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSock == INVALID_SOCKET) { printf("Create socket failed, error code = %d\n", WSAGetLastError()); return -1; } //connect while (connect(clientSock, (SOCKADDR *)&clientAddr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR) { printf("Connecting...\n"); Sleep(1000); } MSG_ASK msgAsk = {0};msgAsk.iBodySize = sizeof(MSG_ASK);msgAsk.iCode = CHECK_CODE;memcpy(msgAsk.szBuffer, "This is a header", strlen("This is a header"));// 发送头部SendAll(clientSock, (char*)&msgAsk, msgAsk.iBodySize);MSG_BODY msgBody = {0};msgBody.iBodySize = sizeof(MSG_BODY);msgBody.iOpType = OP_READ;memcpy(msgBody.szBuffer, "This is the body", strlen("This is the body"));// 发送bodySendAll(clientSock, (char*)&msgBody, msgBody.iBodySize);MSG_ACK msgAck = {0};RecvAll(clientSock, (char*)&msgAck, sizeof(msgAck));if (msgAck.iCheckCode == CHECK_CODE){printf("The process is successful,\nmsgAck.szBuffer = %s \n", msgAck.szBuffer);}else{printf("failed\n");}closesocket(clientSock);WSACleanup();return 0;
}
相关文章:

【网络编程】完成端口 IOCP
10.11 完成端口 10.11.1 基本概念 完成端口的全称是I/O 完成端口,英文为IOCP(I/O Completion Port) 。IOCP是一个异 步I/O 的 API, 可以高效地将I/O 事件通知给应用程序。与使用select() 或是其他异步方法不同 的是,一个套接字与一个完成端口关联了起来…...

《苍穹外卖》SpringBoot后端开发项目重点知识整理(DAY1 to DAY3)
目录 一、在本地部署并启动Nginx服务1. 解压Nginx压缩包2. 启动Nginx服务3. 验证Nginx是否启动成功: 二、导入接口文档1. 黑马程序员提供的YApi平台2. YApi Pro平台3. 推荐工具:Apifox 三、Swagger1. 常用注解1.1 Api与ApiModel1.2 ApiModelProperty与Ap…...

管理网络安全
防火墙在 Linux 系统安全中有哪些重要的作用? 防火墙作为网络安全的第一道防线,能够根据预设的规则,对进出系统的网络流量进行严格筛选。它可以阻止未经授权的外部访问,只允许符合规则的流量进入系统,从而保护系统免受…...

明日直播|Go IoT 开发平台,开启万物智联新征程
在物联网技术飞速发展的当下,物联网行业却深受协议碎片化、生态封闭、开发低效等难题的困扰。企业和开发者们渴望找到一个能突破这些困境,实现高效、便捷开发的有力工具。 3 月 11 日(星期二)19:00,GitCode 特别邀请独…...

系统架构设计师—系统架构设计篇—软件架构风格
文章目录 概述经典体系结构风格数据流风格批处理管道过滤器对比 调用/返回风格主程序/子程序面向对象架构风格层次架构风格 独立构件风格进程通信事件驱动的系统 虚拟机风格解释器基于规则的系统 仓库风格(数据共享风格)数据库系统黑板系统超文本系统 闭…...

【MySQL_04】数据库基本操作(用户管理--配置文件--远程连接--数据库信息查看、创建、删除)
文章目录 一、MySQL 用户管理1.1 用户管理1.11 mysql.user表详解1.12 添加用户1.13 修改用户权限1.14 删除用户1.15 密码问题 二、MySQL 配置文件2.1 配置文件位置2.2 配置文件结构2.3 常用配置参数 三、MySQL远程连接四、数据库的查看、创建、删除4.1 查看数据库4.2 创建、删除…...

【Zinx】Day5-Part4:Zinx 的连接属性设置
目录 Day5-Part4:Zinx 的连接属性设置给连接添加连接配置的接口连接属性方法的实现 测试 Zinx-v1.0总结 Day5-Part4:Zinx 的连接属性设置 在 Zinx 当中,我们使用 Server 来开启服务并监听指定的端口,当接收到来自客户端的连接请求…...

【英伟达AI论文】多模态大型语言模型的高效长视频理解
摘要:近年来,基于视频的多模态大型语言模型(Video-LLMs)通过将视频处理为图像帧序列,显著提升了视频理解能力。然而,许多现有方法在视觉主干网络中独立处理各帧,缺乏显式的时序建模,…...

小程序事件系统 —— 32 事件系统 - 事件分类以及阻止事件冒泡
在微信小程序中,事件分为 冒泡事件 和 非冒泡事件 : 冒泡事件:当一个组件的事件被触发后,该事件会向父节点传递;(如果父节点中也绑定了一个事件,父节点事件也会被触发,也就是说子组…...

全球首款 5G-A 人形机器人发布
全球首款 5G-A 人形机器人于世界移动通信大会(MWC2025)上由中国移动、华为、乐聚联合发布。以下是关于这款机器人的详细介绍: 名称与背景 名称9:这款人形机器人名为 “夸父”,是中国移动、华为与乐聚机器人在 GTI 平台…...

Tomcat 新手入门指南
Tomcat 新手入门指南 Apache Tomcat 是一个开源的 Java Servlet 容器和 Web 服务器,广泛用于部署和运行 Java Web 应用程序。以下是 Tomcat 的入门指南,帮助你快速上手。 1. 安装 Tomcat 步骤 1: 下载 Tomcat 访问 Apache Tomcat 官网。选择适合的版…...

Flink-DataStreamAPI-生成水印
下面我们将学习Flink提供的用于处理事件时间戳和水印的API,也会介绍有关事件时间、流转时长和摄取时间,下面就让我们跟着官网来学习吧 一、水印策略介绍 为了处理事件时间,Flink需要知道事件时间戳,这意味着流中的每个元素都需要…...

【单片机】ARM 处理器简介
ARM 公司简介 ARM(Advanced RISC Machine) 是英国 ARM 公司(原 Acorn RISC Machine) 开发的一种精简指令集(RISC) 处理器架构。ARM 处理器因其低功耗、高性能、广泛适用性,成为嵌入式系统、移动…...

Flutter——最详细原生交互(MethodChannel、EventChannel、BasicMessageChannel)使用教程
MethodChannel(方法通道) 用途:实现 双向通信,用于调用原生平台提供的 API 并获取返回结果。 场景:适合一次性操作,如调用相机、获取设备信息等。 使用步骤: Flutter 端:通过 Meth…...

Kafka常用指令(详细)
Kafka常用指令(详细) 启停命令 前台启动 前台启动命令 ./bin/kafka-server-start.sh config/server.properties 后台启动方式1 后台启动命令加上参数-daemon,窗口关闭之后kafka后台程序继续运行 ./bin/kafka-server-start.sh -daemon co…...

供应链工作效率如何提升
提升供应链工作效率可以从以下几个关键方面入手: 1. 优化供应链管理 数据驱动决策:利用AI和大数据分析,提高预测准确性,优化库存管理。供应链可视化:采用ERP(企业资源计划)和SCM(供…...

1-001:MySQL的存储引擎有哪些?它们之间有什么区别?
MySQL 存储引擎 ├── InnoDB(默认引擎) │ ├── 事务支持:支持 ACID 和事务(事务日志、回滚、崩溃恢复) │ ├── 锁机制:支持行级锁,提高并发性能 │ ├── 外键支持:支持外键…...

Java/Kotlin逆向基础与Smali语法精解
1. 法律警示与道德边界 1.1 司法判例深度剖析 案例一:2021年某游戏外挂团伙刑事案 犯罪手法:逆向《王者荣耀》通信协议,修改战斗数据包 技术细节:Hook libil2cpp.so的SendPacket函数 量刑依据:非法经营罪ÿ…...

用AI学编程4——swift学习1
一个swift文件, 整理出swift从入门到精通的所有知识点, 给出注释, 给出这样的swift文件 以下是一个整合了 Swift 从入门到精通的主要知识点的 Swift 文件,包含了基础语法、面向对象编程、异常处理、集合框架、多线程、Lambda 表达…...

数据集构建与训练前准备
训练数据集目录结构与格式 作者笨蛋学法,先将其公式化,后面逐步自己进行修改,读者觉得看不懂可以理解成,由结果去推过程,下面的这个yaml文件就是结果,我们去推需要的文件夹(名字可以不固定,但是…...

在大型语言模型的提示词设计中,system、user和assistant三个角色的区别与联系
在大型语言模型的提示词设计中,system、user和assistant三个角色承担不同的功能,其区别与联系如下: 1. 角色定义与功能 system(系统指令) 作用:设定模型的整体行为、角色定位和任务框架。例如,“你是一位专业的科技作家”或“仅回答与医疗相关的问题”。特点:在多轮对话…...

Zabbix监控进程报警(Zabbix Monitoring Process Alarm)
zabbix监控进程占cpu、内存、磁盘RAID情况 1、cpu达到90%时报警 名称: cpu user percent gt 90% 表达式:{Template OS Linux:system.cpu.util[,idle].avg(1m)}<10 2、内存达到80%时报警 配置—主机(选择监控主机)—监控项—创建监控项 1、创建监控项 名称&…...

p5.js:sound(音乐)可视化,动画显示音频高低变化
本文通过4个案例介绍了使用 p5.js 进行音乐可视化的实践,包括将音频振幅转化为图形、生成波形图。 承上一篇:vite:初学 p5.js demo 画圆圈 cd p5-demo copy .\node_modules\p5\lib\p5.min.js . copy .\node_modules\p5\lib\addons\p5.soun…...

HAL库常用函数
一、通用函数 系统初始化: HAL_Init(): 初始化HAL库和系统时钟(调用前需配置系统时钟源)。 HAL_Delay(uint32_t Delay): 毫秒级阻塞延时(基于SysTick定时器)。 HAL_GetTick(): 获取系统运行时间(毫秒计数…...

【Zinx】Day5-Part3:Zinx 的连接管理
目录 Day5-Part3:Zinx 的连接管理创建连接管理模块将连接管理模块集成到 Zinx 当中将 ConnManager 集成到 Server 当中在 Connection 的工厂函数中将连接添加到 ConnManagerServer 中连接数量的判断连接的删除 补充:连接的带缓冲发包方式补充:…...

C语言:6.20字符型数据练习题
编写程序,输人一行数字字符(用回车结束),每个数字字符 的前后都有空格。 把这一行中的数字转换成一个整数。 例如,若输入(<CR>代表 Enter键):2 4 8 3<CR>则输出 整数:2483。 #include <stdio.h>int main() {char ch;int number 0;printf("请输入一行…...

SpringBoot Test详解
目录 spring-boot-starter-test 1、概述2、常用注解 2.1、配置类型的注解2.2、Mock类型的注解2.3、自动配置类型的注解2.4、启动测试类型的注解2.5、相似注解的区别和联系 3、SpringBootTest和Junit的使用 3.1、单元测试3.2、集成测试 4、MockMvc 4.1、简单示例4.2、自动配置4…...

CDefView::_GetPIDL函数分析之ListView_GetItem函数的参数item的item.mask 为LVIF_PARAM
CDefView::_GetPIDL函数分析之ListView_GetItem函数的参数item的item.mask 为LVIF_PARAM 第一部分: 1: kd> t SHELL32!CDefView::_GetPIDL: 001b:77308013 55 push ebp 1: kd> dv this 0x00000015 i 0n21 …...

Android Retrofit 框架注解定义与解析模块深度剖析(一)
一、引言 在现代 Android 和 Java 开发中,网络请求是不可或缺的一部分。Retrofit 作为 Square 公司开源的一款强大的类型安全的 HTTP 客户端,凭借其简洁易用的 API 和高效的性能,在开发者社区中广受欢迎。Retrofit 的核心特性之一便是通过注…...

项目上传到Gitee过程
在gitee上新建一个仓库 点击“克隆/下载”获取仓库地址 电脑上要装好git 在电脑本地文件夹右键“Git Bash Here” 依次执行如下命令 git init git remote add origin https://gitee.com/qlexcel/stm32-simple.git git pull origin master git add . git commit -m ‘init’…...