




版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、Windows SOCKET编程第一章 序言我写这个专题的目的,一方面是为了通过对网络编程再一次系统的总结,提高自己的网络编程水平,特别是Windows下的网络编程水平。同时,我也希望,能为众多初学网络编程的人提供一点帮助,因为我开始学习网络编程的时候,能找到的资料就很少。当然,花钱可以买到翻译版本的书:)首先向大家推荐一本很好的参考书,Network Programming for Microsoft Windows 2nd,初学网络编程的时候我还不知道有这样一本好书,只是上各大论坛把能找到的网络编程方面的文章和代码下载下来,然后自己研究。后来看到别人推荐这一本书,下载了一个,看了感觉非常好
2、,里面的内容写得很规范,条理也很清楚,英文好的朋友可以直接阅读,不然就只好去弄一本翻译好的来研究了。我试着从Windows编程的基础开始,一直到探索建立高性能的网络应用程序。我说过,我并不是以高手的身份写这本书,而是以和大家一起学习的心态学习网络编程,写书只是让自己的思路更清晰,以后还可以翻阅。所以,我不保证书中所有的内容都是绝对正确和标准的,有不妥的地方,还希望高手批评指正。这本书是完全免费的,读者可以任意使用书中的代码。但是如果需要转载,请注明原作者和出处。如果有商业运作的需求,请直接和我联系。第二章 Windows网络编程基础这本书主要探索Windows网络编程,开发平台是Windows
3、 2000 和Visual C+.NET,从一个合格的C+程序员到网络编程高手,还是需要花不少功夫,至少我认为写一个聊天程序很简单,而要写一个能同时响应成千上万用户的高性能网络程序,的确不容易。这篇文章所介绍的方法也并不是能直接应用于每一个具体的应用程序,只能作为学习的参考资料。开发高性能网络游戏恐怕是促使很多程序员研究网络编程的原因(包括我),现在的大型网络游戏对同时在线人数的要求比较高,真正的项目往往采取多个服务器(组)负荷分担的方式工作,我将首先把注意力放到单个服务器的情况。大家都知道,我们用得最多的协议是UDP和TCP,UDP是不可靠传输服务,TCP是可靠传输服务。UDP就像点对点的数
4、据传输一样,发送者把数据打包,包上有收信者的地址和其他必要信息,至于收信者能不能收到,UDP协议并不保证。而TCP协议就像(实际他们是一个层次的网络协议)是建立在UDP的基础上,加入了校验和重传等复杂的机制来保证数据可靠的传达到收信者。关于网络协议的具体内容,读者可以参考专门介绍网络协议的书籍,或者查看RFC中的有关内容。本书直接探讨编程实现网络程序的问题。21 Window Socket介绍Windows Socket是从UNIX Socket继承发展而来,最新的版本是2.2。进行Windows网络编程,你需要在你的程序中包含WINSOCK2.H或MSWSOCK.H,同时你需要添加引入库WS
5、2_32. LIB或WSOCK32.LIB。准备好后,你就可以着手建立你的第一个网络程序了。Socket编程有阻塞和非阻塞两种,在操作系统I/O实现时又有几种模型,包括Select,WSAAsyncSelect,WSAEventSelect ,IO重叠模型,完成端口等。要学习基本的网络编程概念,可以选择从阻塞模式开始,而要开发真正实用的程序,就要进行非阻塞模式的编程(很难想象一个大型服务器采用阻塞模式进行网络通信)。在选择I/O模型时,我建议初学者可以从WSAAsyncSelect模型开始,因为它比较简单,而且有一定的实用性。但是,几乎所有人都认识到,要开发同时响应成千上万用户的网络程序,完成
6、端口模型是最好的选择。既然完成端口模型是最好的选择,那为什么我们不直接写出一个使用完成端口的程序,然后大家稍加修改就OK了。我认为这确实是一个好的想法,但是真正做项目的时候,不同的情况对程序有不同的要求,如果不深入学习网络编程的各方面知识,是不可能写出符合要求的程序,在学习网络编程以前,我建议读者先学习一下网络协议。22 第一个网络程序由于服务器/客户端模式的网络应用比较多,而且服务器端的设计是重点和难点。所以我想首先探讨服务器的设计方法,在完成服务器的设计后再探讨其他模式的网络程序。设计一个基本的网络服务器有以下几个步骤:1、初始化Windows Socket2、创建一个监听的Socket3
7、、设置服务器地址信息,并将监听端口绑定到这个地址上4、开始监听5、接受客户端连接6、和客户端通信7、结束服务并清理Windows Socket和相关数据,或者返回第4步我们可以看出设计一个最简单的服务器并不需要太多的代码,它完全可以做一个小型的聊天程序,或进行数据的传输。但是这只是我们的开始,我们的最终目的是建立一个有大规模响应能力的网络服务器。如果读者对操作系统部分的线程使用还有疑问,我建议你现在就开始复习,因为我们经常使用线程来提高程序性能,其实线程就是让CPU不停的工作,而不是总在等待I/O,或者是一个CPI,累死了还是一个CPU。千万不要以为线程越多的服务器,它的性能就越好,线程的切换
8、也是需要消耗时间的,对于I/O等待少的程序,线程越多性能反而越低。下面是简单的服务器和客户端源代码。(阻塞模式下的,供初学者理解)TCPServer#include <winsock2.h>void main(void)WSADATA wsaData;SOCKET ListeningSocket;SOCKET NewConnection;SOCKADDR_IN ServerAddr;SOCKADDR_IN ClientAddr;int Port = 5150;/ 初始化Windows Socket 2.2WSAStartup(MAKEWORD(2,2), &wsaData)
9、;/ 创建一个新的Socket来响应客户端的连接请求ListeningSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);/ 填写服务器地址信息/ 端口为5150/ IP地址为INADDR_ANY,注意使用htonl将IP地址转换为网络格式ServerAddr.sin_family = AF_INET;ServerAddr.sin_port = htons(Port); ServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);/ 绑定监听端口bind(ListeningSocket, (SOCKADDR *
10、)&ServerAddr, sizeof(ServerAddr);/ 开始监听,指定最大同时连接数为5listen(ListeningSocket, 5); / 接受新的连接NewConnection = accept(ListeningSocket, (SOCKADDR *) &ClientAddr,&ClientAddrLen);/ 新的连接建立后,就可以互相通信了,在这个简单的例子中,我们直接关闭连接,/ 并关闭监听Socket,然后退出应用程序/ closesocket(NewConnection);closesocket(ListeningSocket);/
11、释放Windows Socket DLL的相关资源WSACleanup();TCPClient# include <winsock2.h>void main(void)WSADATA wsaData;SOCKET s;SOCKADDR_IN ServerAddr;int Port = 5150;/初始化Windows Socket 2.2WSAStartup(MAKEWORD(2,2), &wsaData);/ 创建一个新的Socket来连接服务器s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);/ 填写客户端地址信息/ 端口为51
12、50/ 服务器IP地址为9,注意使用inet_addr将IP地址转换为网络格式ServerAddr.sin_family = AF_INET;ServerAddr.sin_port = htons(Port); ServerAddr.sin_addr.s_addr = inet_addr(9);/ 向服务器发出连接请求connect(s, (SOCKADDR *) &ServerAddr, sizeof(ServerAddr); / 新的连接建立后,就可以互相通信了,在这个简单的例子中,我们直接关闭连接,/ 并关闭监听Socket,然后退出应
13、用程序closesocket(s);/ 释放Windows Socket DLL的相关资源WSACleanup();23 WSAAsyncSelect模式前面说过,Windows网络编程模式有好几种,他们各有特点,实现起来复杂程度各不相同,适用范围也不一样。下图是Network Programming for Microsoft Windows 2nd 一书中对不同模式的一个性能测试结果。服务器采用Pentium 4 1.7 GHz Xeon的CPU,768M内存;客户端有3台PC,配置分别是Pentium 2 233MHz ,128 MB 内存,Pentium 2 350 MHz ,128
14、MB内存,Itanium 733 MHz ,1 GB内存。具体的结果分析大家可以看看原书中作者的叙述,我关心的是哪种模式是我需要的。首先是服务器,勿庸置疑,肯定是完成端口模式。那么客户端呢,当然也可以采用完成端口,但是不同模式是在不同的操作系统下支持的,看下图:完成端口在Windows 98下是不支持的,虽然我们可以假定所有的用户都已经装上了Windows 2000和Windows XP,。但是,如果是商业程序,这种想法在现阶段不应该有,我们不能让用户为了使用我们的客户端而去升级他的操作系统。Overlapped I/O可以在Windows 98下实现,性能也不错,但是实现和理解起来快赶上完成
15、端口了。而且,最关键的一点,客户端程序不是用来进行大规模网络响应的,客户端的主要工作应该是进行诸如图形运算等非网络方面的任务。原书作者,包括我强烈推荐大家使用WSAAsyncSelect模式实现客户端,因为它实现起来比较直接和容易,而且他完全可以满足客户端编程的需求。下面是一段源代码,虽然我们是用它来写客户端,我还是把它的服务端代码放上来,一方面是有兴趣的朋友可以用他做测试和了解如何用它实现服务器;另一方面是客户端的代码可以很容易的从它修改而成,不同的地方只要参考一下2.1节里的代码就知道了。#define WM_SOCKET WM_USER + 1#include <winsock2.
16、h>#include <windows.h>int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine,int nCmdShow)WSADATA wsd;SOCKET Listen;SOCKADDR_IN InternetAddr;HWND Window;/ 创建主窗口Window = CreateWindow();/ 初始化Windows Socket 2.2WSAStartup(MAKEWORD(2,2), &wsd);/ 创建监听SocketListen = s
17、ocket (AF_INET, SOCK_STREAM, IPPROTO_TCP);/ 设置服务器地址InternetAddr.sin_family = AF_INET;InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);InternetAddr.sin_port = htons(5150);/ 绑定Socketbind(Listen, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr);/ 设置Windows消息,这样当有Socket事件发生时,窗口就能收到对应的消息通知/ 服务器一般设置 FD
18、_ACCEPT FD_READ | FD_CLOSE/ 客户端一般设置 FD_CONNECT FD_READ | FD_CLOSEWSAAsyncSelect(Listen, Window, WM_SOCKET, FD_ACCEPT FD_READ | FD_CLOSE);/ 开始监听listen(Listen, 5);/ Translate and dispatch window messages/ until the application terminateswhile (1) / .BOOL CALLBACK ServerWinProc(HWND hDlg,UINT wMsg,WPAR
19、AM wParam, LPARAM lParam)SOCKET Accept;switch(wMsg)case WM_PAINT:/ Process window paint messagesbreak;case WM_SOCKET:/ Determine whether an error occurred on the/ socket by using the WSAGETSELECTERROR() macroif (WSAGETSELECTERROR(lParam)/ Display the error and close the socketclosesocket( (SOCKET) w
20、Param);break;/ Determine what event occurred on the/ socketswitch(WSAGETSELECTEVENT(lParam)case FD_ACCEPT:/ Accept an incoming connectionAccept = accept(wParam, NULL, NULL);/ Prepare accepted socket for read,/ write, and close notificationWSAAsyncSelect(Accept, hDlg, WM_SOCKET,FD_READ FD_WRITE FD_CL
21、OSE);break;case FD_READ:/ Receive data from the socket in/ wParambreak;case FD_WRITE:/ The socket in wParam is ready/ for sending databreak;case FD_CLOSE:/ The connection is now closedclosesocket( (SOCKET)wParam);break;break;return TRUE;24 小节目前为止,我非常简要的介绍了Windows网络编程的一些东西,附上了一些源代码。可以说,读者特别是初学者,看了后不一
22、定就能马上写出程序来,而那些代码也不是可以直接应用于实际的项目。别急,万里长征才开始第一步呢,很多书里都是按照基础到应用的顺序来写的,但是我喜欢更直接一点,更实用一些的方式。而且,我写的这个专题,毕竟不是商业化的,时间上不能投入过多,只是作为给初学者的一个小小帮助。更多的还是希望读者自己刻苦研究,有问题的时候可以到我的论坛上给我留言,以后有机会我也会公布一些实际的代码。希望结交更多热爱编程和中国游戏事业的朋友。下一章里我将主要讲解完成端口编程,这也是我写这篇文章的初衷,希望对大家能有所帮助。第三章 完成端口模式下的高性能网络服务器31开始完成端口听起来好像很神秘和复杂,其实并没有想象的那么难。
23、这方面的文章在论坛上能找到的我差不多都看过,写得好点的就是CSDN.NET上看到的一组系列文章,不过我认为它只是简单的翻译了一下Network Programming for Microsoft Windows 2nd 中的相关内容,附上的代码好像不是原书中的,可能是另一本外文书里的。我看了以后,觉得还不如看原版的更容易理解。所以在我的开始部分,我主要带领初学者理解一下完成端口的有关内容,是我开发的经验,其他的请参考原书的相关内容。采用完成端口的好处是,操作系统的内部重叠机制可以保证大量的网络请求都被服务器处理,而不是像WSAAsyncSelect 和WSAEventSelect的那样对并发的
24、网络请求有限制,这一点从上一章的测试表格中可以清楚的看出。完成端口就像一种消息通知的机制,我们创建一个线程来不断读取完成端口状态,接收到相应的完成通知后,就进行相应的处理。其实感觉就像WSAAsyncSelect一样,不过还是有一些的不同。比如我们想接收消息,WSAAsyncSelect会在消息到来的时候直接通知Windows消息循环,然后就可以调用WSARecv来接收消息了;而完成端口则首先调用一个WSARecv表示程序需要接收消息(这时可能还没有任何消息到来),但是只有当消息来的时候WSARecv才算完成,用户就可以处理消息了,然后再调用一个WSARecv表示等待下一个消息,如此不停循环,
25、我想这就是完成端口的最大特点吧。Per-handle Data 和 Per-I/O Operation Data 是两个比较重要的概念,Per-handle Data用来把客户端数据和对应的完成通知关联起来,这样每次我们处理完成通知的时候,就能知道它是哪个客户端的消息,并且可以根据客户端的信息作出相应的反应,我想也可以理解为Per-Client handle Data吧。Per-I/O Operation Data则不同,它记录了每次I/O通知的信息,比如接收消息时我们就可以从中读出消息的内容,也就是和I/O操作有关的信息都记录在里面了。当你亲手实现完成端口的时候就可以理解他们的不同和用途了。
26、CreateIoCompletionPort函数中有个参数NumberOfConcurrentThreads,完成端口编程里有个概念Worker Threads。这里比较容易引起混乱,NumberOfConcurrentThreads需要设置多少,又需要创建多少个Worker Threads才算合适?NumberOfConcurrentThreads的数目和CPU数量一样最好,因为少了就没法利用多CPU的优势,而多了则会因为线程切换造成性能下降。Worker Threads的数量是不是也要一样多呢,当然不是,它的数量取决于应用程序的需要。举例来说,我们在Worker Threads里进行消息处
27、理,如果这个过程中有可能会造成线程阻塞,那如果我们只有一个Worker Thread,我们就不能很快响应其他客户端的请求了,而只有当这个阻塞操作完成了后才能继续处理下一个完成消息。但是如果我们还有其他的Worker Thread,我们就能继续处理其他客户端的请求,所以到底需要多少的Worker Thread,需要根据应用程序来定,而不是可以事先估算出来的。如果工作者线程里没有阻塞操作,对于某些情况来说,一个工作者线程就可以满足需要了。其他问题,Network Programming for Microsoft Windows 2nd中,作者还提出了如何安全的退出应用程序等等实现中的细节问题,这
28、里我就不一一讲述了,请读者参考原书的相关内容,如果仍有疑问,可以联系我。32实现下面是一般的实现步骤1. 获得计算机信息,得到CPU的数量。创建一个完成端口,第四个参数置0,指定NumberOfConcurrentThreads为CPU个数。2. Determine how many processors exist on the system.3. Create worker threads to service completed I/O requests on the completion port using processor information in step 2. In th
29、e case of this simple example, we create one worker thread per processor because we do not expect our threads to ever get in a suspended condition in which there would not be enough threads to execute for each processor. When the CreateThread function is called, you must supply a worker routine that
30、 the thread executes upon creation. We will discuss the worker threads responsibilities later in this section.4. Prepare a listening socket to listen for connections on port 5150.5. Accept inbound connections using the accept function.6. Create a data structure to represent per-handle data and save
31、the accepted socket handle in the structure.7. Associate the new socket handle returned from accept with the completion port by calling CreateIoCompletionPort. Pass the per-handle data structure to CreateIoCompletionPort via the completion key parameter.8. Start processing I/O on the accepted connec
32、tion. Essentially, you want to post one or more asynchronous WSARecv or WSASend requests on the new socket using the overlapped I/O mechanism. When these I/O requests complete, a worker thread services the I/O requests and continues processing future I/O requests, as we will see later in the worker
33、routine specified in step 3.9. Repeat steps 5-8 until server terminates.那么学习完成端口编程从哪里开始比较好,对于初学者而言,直接进入编程并不是一个好主意,我建议初学者首先学习用异步Socket模式,即WSAEventSelect模式构建一个简单的聊天服务器。当把Windows网络编程的概念有一个清晰的认识之后,再深入研究完成端口编程。接着就是深入研究具体的编程实现了,从Network Programming for Microsoft Windows 2nd中摘录的这段经典代码可以说是非常合适的,这里我只简单解释一下其中
34、比较关键的地方,还有不明白的可以参看原书,或者联系我。主程序段:1. HANDLE CompletionPort;2. WSADATA wsd;3. SYSTEM_INFO SystemInfo;4. SOCKADDR_IN InternetAddr;5. SOCKET Listen;6. int i;7. 8. typedef struct _PER_HANDLE_DATA 9. 10. SOCKET Socket;11. SOCKADDR_STORAGE ClientAddr;12. / 在这里还可以加入其他和客户端关联的数据13. PER_HANDLE_DATA, * LPPER_HAN
35、DLE_DATA;14. 15. / 初始化Windows Socket 2.216. StartWinsock(MAKEWORD(2,2), &wsd);17. 18. / Step 1:19. / 创建完成端口20. 21. CompletionPort = CreateIoCompletionPort(22. INVALID_HANDLE_VALUE, NULL, 0, 0);23. 24. / Step 2:25. / 检测系统信息26. 27. GetSystemInfo(&SystemInfo);28. 29. / Step 3: 创建工作者线程,数量和CPU的数量
36、一样多30. / Create worker threads based on the number of31. / processors available on the system. For this32. / simple case, we create one worker thread for each33. / processor.34. 35. for(i = 0; i < SystemInfo.dwNumberOfProcessors; i+)36. 37. HANDLE ThreadHandle;38. 39. / Create a server worker thr
37、ead, and pass the40. / completion port to the thread. NOTE: the41. / ServerWorkerThread procedure is not defined42. / in this listing.43. 44. ThreadHandle = CreateThread(NULL, 0,45. ServerWorkerThread, CompletionPort,46. 0, NULL;47. 48. / Close the thread handle49. CloseHandle(ThreadHandle);50. 51.
38、52. / Step 4:53. / 创建监听Socket54. 55. Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,56. WSA_FLAG_OVERLAPPED);57. 58. InternetAddr.sin_family = AF_INET;59. InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);60. InternetAddr.sin_port = htons(5150);61. bind(Listen, (PSOCKADDR) &InternetAddr,62.
39、sizeof(InternetAddr);63. 64. / 开始监听65. 66. listen(Listen, 5);67. 68. while(TRUE)69. 70. PER_HANDLE_DATA *PerHandleData=NULL;71. SOCKADDR_IN saRemote;72. SOCKET Accept;73. int RemoteLen;74. / Step 5: 等待客户端连接,然后将客户端Socket加入完成端口75. / Accept connections and assign to the completion76. / port77. 78. Remo
40、teLen = sizeof(saRemote);79. Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote, 80. &RemoteLen);81. 82. / Step 6: 初始化客户端数据83. / Create per-handle data information structure to 84. / associate with the socket85. PerHandleData = (LPPER_HANDLE_DATA) 86. GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA);
41、87. 88. printf(Socket number %d connected, Accept);89. PerHandleData->Socket = Accept;90. memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);91. 92. / Step 7:93. / Associate the accepted socket with the94. / completion port95. 96. CreateIoCompletionPort(HANDLE) Accept,97. Complet
42、ionPort, (DWORD) PerHandleData, 0);98. 99. / Step 8: 发出对客户端的I/O请求,等待完成消息100. / Start processing I/O on the accepted socket.101. / Post one or more WSASend() or WSARecv() calls102. / on the socket using overlapped I/O.103. WSARecv(.);104. 105. 106. 工作者线程DWORD WINAPI ServerWorkerThread(LPVOID Completi
43、onPortID)HANDLE CompletionPort = (HANDLE) CompletionPortID;DWORD BytesTransferred;LPOVERLAPPED Overlapped;LPPER_HANDLE_DATA PerHandleData;LPPER_IO_DATA PerIoData;DWORD SendBytes, RecvBytes;DWORD Flags;while(TRUE)/ 等待完成端口消息,未收到消息德时候则阻塞线程ret = GetQueuedCompletionStatus(CompletionPort,&BytesTransfe
44、rred,(LPDWORD)&PerHandleData,(LPOVERLAPPED *) &PerIoData, INFINITE);/ First check to see if an error has occurred/ on the socket; if so, close the / socket and clean up the per-handle data/ and per-I/O operation data associated with/ the socketif (BytesTransferred = 0 &&(PerIoData-&g
45、t;OperationType = RECV_POSTED PerIoData->OperationType = SEND_POSTED)/ A zero BytesTransferred indicates that the/ socket has been closed by the peer, so/ you should close the socket. Note: / Per-handle data was used to reference the/ socket associated with the I/O operation.closesocket(PerHandle
46、Data->Socket);GlobalFree(PerHandleData);GlobalFree(PerIoData);continue;/ Service the completed I/O request. You can/ determine which I/O request has just/ completed by looking at the OperationType/ field contained in the per-I/O operation data.if (PerIoData->OperationType = RECV_POSTED)/ Do something with the received data/ in PerIoData->Buffer/ Post another WSASend or WSARecv operation./ As an example, we will p
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 共同股权投资合同范本
- 关于续签监控合同范本
- 凉皮店用工合同范例
- 事业单位劳务合同范本3篇
- 公司考核合同范本
- 下班无偿保洁合同范本
- 入股销售合同范本
- 北京贷款合同范本
- 农业设备运输合同范例
- 公司签承揽合同范本
- 《魔方知识普及》课件
- 东芝授权委托书标准版
- 2023施工项目部标准化工作手册
- 中小学幼儿园中班下册点点回家公开课教案教学设计课件案例测试练习卷题
- SG-400140型火电厂锅炉中硫烟煤烟气喷雾干燥法脱硫+袋式除尘系统设计
- 中型轿车的盘式制动器的设计
- 低血糖急救护理课件
- 学做小小按摩师(课件)全国通用三年级上册综合实践活动
- 阴道镜检查临床医学知识及操作方法讲解培训PPT
- “教学评一体化”指导的语文教学设计以统编版语文四年级上册《蟋蟀的住宅》为例
- AI09人工智能-多智能体
评论
0/150
提交评论