版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
1、Winsocket入门教程一:多线程阻塞式服务器和阻塞式客户端程序(TCP) 收藏 最近因为工作需要学习了Winsocket客户端服务器模型程序的设计。在学习的过程中,我发现学习Winsocket的资料不多并且十分的零散。我一直没有找到一本学习Winsocket方面的经典国外著作。而且这些资料中并没有提供源代码文件,所以我只有将这些源代码在自己敲一遍。在敲代码的过程中,我发现了这些源代码中的一些错误的地方和一些已经过时的Windows程序的输写方法(Win16?)。现将学习经验和通过阅读各种资料总结出来的模型以及代码分享出来。希望对学习Win
2、socket的初学者有一定的帮助。 我们首先来了解一下什么是Winsocket。Winsocket是unix/linux下的berkeley socket在Windows下的实现。unix/linux下的berkeley socket是网络通讯方面的基石,应用程序通过调用berkeley socket的API进行相互通讯,berkeley socket则利用具体的网络通讯协议和操作系统的调用来为我们完成具体的通讯工作。Winsocket保留了berkeley socket的所有内容,并且为了其能在Win32消息机制和多线程的环境下更好的工作。W
3、insocket在berkeley socket原有的基础上对其进行了扩充。如我们可以利用WSAAsyncSelect对Socket消息进行订阅,以及使用WSAGetLastError对多线程环境下的Winsocket错误进行捕获。 接着再让我们来了解一下服务器客户端应用程序模型。该模型是构建分布式系统的模型之一。服务器程序一直处于监听的状态,等待客户端程序的连接。客户端程序像服务器程序发送连接请求,服务器程序接受该连接请求,同时与客户端程序建立连接。此时客户端程序就可以向服务器发送具体的请求,获取相关的数据。服务器客户端模型有三种连接方式,
4、一种是面向连接的(TCP),面向连接的服务是一种可靠的服务,它通过数据流进行数据的传输,面向连接的服务实现了无差错无重复的顺寻数据发送。一种是面向无连接的(UDP),面向无连接的服务是一种不可靠的服务,它通过数据报进行数据传输,由于数据报进行传输时的顺序是无序的,所以它是不可靠的服务。最后一种是多播的方式,及服务器程序主动向多个客户端程序发送信息。面向连接的服务器客户端应用程序模型的程序流程图如下所示:在此模型的阻塞模式中,服务端程序在执行accept操作、客户端程序connect操作、以及服务端客户端在进行read和write操作时,如果这些操作既没有成功也没有失败,应用程序会在执行这些操作
5、的地方一直阻塞着。所以我们应该在服务端应用程序的主线程中不停的调用accept操作,以使服务端程序能不停地接受客户端程序发送过来的连接请求。而在接受了一个客户端的连接请求后,我们应改为每一个接受的连接请求开辟一个专门的线程来接受客户端程序发送的请求以及为具体的请求返回特定的信息。 根据以上的程序流程图以及说明,我们可以写出以下的服务端程序源代码:view plaincopy to clipboardprint?1. / 2. / file ServerMultTh
6、readServerMultThread.cpp 3. / 4. / brief 阻塞式多线程服务器程序。每当客户端程序请求与服务端连接时,服务端程序开放一个线程接受客户端程序的请求 5. / 并且向客户端回馈请求的信息。客户端请求的信息输出到控制台中. 6. / 7. #include <iostream>
7、0; 8. #include <cassert> 9. #include <WinSock2.h> 10. #include <process.h> 11. #pragma comment(lib, "ws2_32.lib" ) 12. #define ASSERT assert 13. #define THREAD
8、60;HANDLE 14. #define EVENT HANDLE 15. #define CloseThread CloseHandle 16. #define CloseEvent CloseHandle 17. using std:cin; 18. using std:cout; 19. using std:endl;
9、 20. / 21. / struct tagServerRecv 22. / 23. / brief 线程函数参数结构体,其中包含已建立连接的socket. 24. / 25. / author Shining100 26. / date 2010-05-18 27. / 28. typed
10、ef struct tagServerRecv 29. 30. SOCKET skAccept; / 已建立连接的socket 31. CRITICAL_SECTION *pcs; / 同步控制台输出的临界区
11、;32. EVENT e; / 保证结构体各个字段在结构体字段改变之前将其拷贝到线程中的信号量 33. THREAD t;
12、60; / 当前线程的内核对象 34. DWORD dwThreadID; / 当前线程的ID 35. SERVER_RECV, *PSERVER_RECV; 36. / 37. / fn static int ServerRecv(LPVOID l
13、Param) 38. / 39. / brief 服务器与建立连接的客户端进行通讯. 40. / 41. / author Shining100 42. / date 2010-05-18 43. / 44. / param lParam 线程函数参数,
14、160;详细信息见上面说明. 45. / 46. / return 总是返回0. 47. / 48. static int ServerRecv(LPVOID lParam); 49. static const int c_iPort = 10001; 50. int main() 51.
15、60; 52. int iRet = SOCKET_ERROR; 53. / 初始化Winsocket,所有Winsocket程序必须先使用WSAStartup进行初始化 54. WSADATA data; 55. ZeroMemory(&data,
16、60;sizeof(WSADATA); 56. iRet = WSAStartup(MAKEWORD(2, 0), &data); 57. ASSERT(SOCKET_ERROR != iRet); 58. / 建立服务端程序的监听套接字 59.
17、; SOCKET skListen = INVALID_SOCKET; 60. skListen = socket(AF_INET, SOCK_STREAM, 0); 61. ASSERT(INVALID_SOCKET != skListen); 62. / 初始化监听套接
18、字地址信息 63. sockaddr_in adrServ; / 表示网络地址 64. ZeroMemory(&adrServ, s
19、izeof(sockaddr_in); 65. adrServ.sin_family = AF_INET; / 初始化地址格式,只能为AF_INET 66. adrServ.sin_port
20、; = htons(c_iPort); / 初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序 67. adrServ.sin_addr.s_addr = INADDR_ANY; / 初始化IP,由于是服务器程序,所以可以将INADDR_ANY赋给该字段,表示任意的IP
21、160; 68. / 绑定监听套接字到本地 69. iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in); 70. ASSERT(SOCKET_ERROR != iRet); 71.
22、160;/ 使用监听套接字进行监听 72. iRet = listen(skListen, SOMAXCONN); / SOMAXCONN表示可以连接到该程序的最大连接数 73. ASSERT(SOCKET_ERROR != iRet); 74. / 输出控制台缓冲区,由于可能有多个客户端程序
23、可能同时向缓冲区发送请求信息 75. / 为了保证输出时能够一次性完整的输出完一个客户端的请求信息,所以在输出客 76. / 户程序的信息到控制台时,必须使用临界区阻塞其它线程 77. CRITICAL_SECTION cs; 78. InitializeCriticalSecti
24、on(&cs); 79. / 保证结构体各个字段在结构体字段改变之前将其拷贝到线程中的信号量 80. / 因为当该结构体拷贝到线程中之前, 有可能有新的连接到来并改变了结构体的值 81. / 所以我们必须先保证值拷贝过后再接受连接 82. EVENT e
25、 = NULL; 83. e = CreateEvent(NULL, FALSE, FALSE, NULL); 84. ASSERT(NULL != e); 85. for(;) 86. 87.
26、 / 客户端向服务器端发送连接请求,服务器端接受客户端的连接 88. SOCKET skAccept = INVALID_SOCKET; 89. sockaddr_in adrClit; 90
27、. ZeroMemory(&adrClit, sizeof(sockaddr_in); 91. int iLen = sizeof(sockaddr_in); 92. skAccept = acc
28、ept(skListen, (sockaddr*)&adrClit, &iLen); / 如果没有客户端程序请求连接,服务端程序会一直阻塞在这里等待连接 93. ASSERT(INVALID_SOCKET != skAccept); 94. SERVER_RECV sr;
29、 95. / 成功创建连接后创建一个独立的线程应答客户请求,以防止应用程序因为阻塞无法应答新的客户请求 96. / 我们应该先将线程挂起,以便我们能够在线程执行之前初始化线程所需要的结构体变量中的各个字段 97. THRE
30、AD hThread = NULL; 98. DWORD dwThreadID = 0; 99. hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ServerRecv,
31、160; 100. &sr, CREATE_SUSPENDED, &dwThreadID); 101. ASSERT(NULL != hThread); 102.
32、0; / 初始化结构体字段 103. sr.skAccept = skAccept; 104. sr.pcs = &cs; 105. &
33、#160; sr.e = e; 106. sr.t = hThread; 107.
34、; sr.dwThreadID = dwThreadID; 108. / 启动线程 109. DWORD dwRet = ResumeThread(hThread); 110.
35、160; ASSERT(-1 != dwRet); 111. 112. / 保证结构体被拷贝到线程中后再应答新的连接 113. dwRet = WaitF
36、orSingleObject (e, INFINITE); 114. ASSERT(WAIT_FAILED != dwRet); 115. 116. / 清理线程同步资源 117. DeleteCriticalS
37、ection(&cs); 118. BOOL bRet = FALSE; 119. bRet = CloseEvent(e); 120. ASSERT(bRet); 121. / 关闭该套接字的连接 122.
38、; iRet = shutdown(skListen, SD_SEND); 123. ASSERT(SOCKET_ERROR != iRet); 124. / 清理该套接字的资源 125. iRet = closesocket(skListen);
39、0;126. ASSERT(SOCKET_ERROR != iRet); 127. / 清理Winsocket资源 128. iRet = WSACleanup(); 129. ASSERT(SOCKET_ERROR != iRet); 13
40、0. cin.get(); 131. return 0; 132. 133. int ServerRecv(LPVOID lParam) 134. 135. / 拷贝结构体各个字段到线程中 136. PSERVER_RECV
41、 psr = (PSERVER_RECV)lParam; 137. SERVER_RECV sr = 138. psr->skAccept, 139. psr->pcs, 140
42、. psr->e, 141. psr->t, 142. psr->dwThreadID 143. 144.
43、160;/ 设置信号量, 使主线程能够接受新的连接 145. BOOL bRet = FALSE; 146. bRet = SetEvent(sr.e); 147. ASSERT(bRet); 148. const int
44、;c_iBufLen = 512; 149. char szBufc_iBufLen + 1 = '0' 150. const char c_szPrefix = "Server recv:" 151. const i
45、nt c_iPrefLen = strlen(c_szPrefix); 152. char szRelyc_iBufLen + 16 + 1 = '0' 153. strcpy(szRely, c_szPrefix); 154. int iRet&
46、#160;= SOCKET_ERROR; 155. for(;) 156. 157. iRet = recv(sr.skAccept, szBuf, c_iBufLen, 0); / 接收客户端发送的信息, 如果客户端不发送信息,则线程会阻塞到
47、此处 158. if(0 = iRet) / 客户端优雅的关闭了此连接 159. 160. cout << "
48、;Connection " << sr.dwThreadID << " shutdown." << endl; 161. break; 162.
49、60;163. else if(SOCKET_ERROR = iRet) / 客户端粗鲁的关闭了此连接或者接受信息出错 164. 165. cout &l
50、t;< "Connection " << sr.dwThreadID << " recv error." << endl; 166. break; 167.
51、0; 168. szBufiRet = '0' 169. EnterCriticalSection(sr.pcs); 170. cout <<
52、160;"Connection " << sr.dwThreadID << " says:" << szBuf << endl; / 输出接收到的信息 171. LeaveCriticalSection(sr.pcs); 172.
53、160; / 向客户端发送信息 173. strcpy(szRely + c_iPrefLen, szBuf); 174. iRet = send(sr.skAccept, szRely, strlen(szR
54、ely), 0); / 客户端如果没有足够的缓冲区接受信息,则线程会阻塞到此处 175. if(SOCKET_ERROR = iRet) 176. 177.
55、60; cout << "Connection " << sr.dwThreadID << " send error." << endl; 178. break; 179. &
56、#160; 180. 181. / 关闭该套接口 182. iRet = shutdown(sr.skAccept, SD_SEND); 183. while(recv(sr.skAccept, sz
57、Buf, c_iBufLen, 0) > 0); 184. ASSERT(SOCKET_ERROR != iRet); 185. / 清理该套接口的资源 186. iRet = closesocket(sr.skAccept); 187.
58、 ASSERT(SOCKET_ERROR != iRet); 188. / 关闭该线程对象 189. bRet = CloseThread(sr.t); 190. ASSERT(bRet); 191. cout <<&
59、#160;"Connection " << sr.dwThreadID << " exit." << endl; 192. return 0; 193. 在用Winsocket编写程序时,我们首先必须要进行如下的操作,以为该进程初始化W
60、insocket和Ws2_32.dll,而使后面的函数调用有效。 view plaincopy to clipboardprint?1. WSADATA data; 2. ZeroMemory(&data, sizeof(WSADATA); 3. iRet = WSAStartup(MAKEWORD(2, 0), &data); 4. ASSERT(SOCKET_ERROR
61、;!= iRet); WSAStartup第一个参数为要使用的Winsocket的版本,MAKEWORD(2, 0)表示我们使用Winsocket2.0。第二个参数在WSAStartup初始化后,可以获得一些Winsocket相关信息,如该版本Winsocket所支持的最大socket数量以及UDP包的最大大小。 在初始化了Winsocket后,我们就可以创建一个socket监听客户端的连接请求了。
62、 view plaincopy to clipboardprint?1. SOCKET skListen = INVALID_SOCKET; 2. skListen = socket(AF_INET, SOCK_STREAM, 0); 3. ASSERT(INVALID_SOCKET != skListen); socket函数分配相应的资源并将该
63、socket绑定到一个特定的传输服务提供者。socket的第一个参数为网络地址族,该参数只能为AF_INET,第二个参数可以为SOCK_STREAM或者SOCK_DGRAM。SOCK_STREAM为一个流式套接口,它提供双向可靠、面向连接的TCP服务。SOCK_DGRAM为一个数据报套接口,它提供不可靠、面向无连接的UDP服务。第三个参数一般选择为0,表示由Winsocket选择具体的协议使用。 在建立了一个监听socket后,我们就可以将该套接口与本地地址进行绑定,已将其设置成为网络中一个独一无二的地址。view plaincopy to c
64、lipboardprint?1. iRet = bind(skListen, (sockaddr*)&adrServ, sizeof(sockaddr_in); 2. ASSERT(SOCKET_ERROR != iRet); 在绑定了本地地址后,我们就可以将该socket设置为监听状态,以使该socket可以检测到来自客户端程序的连接请求。view plaincopy to clipboardprint
65、?1. iRet = listen(skListen, SOMAXCONN); / SOMAXCONN表示可以连接到该程序的最大连接数 2. ASSERT(SOCKET_ERROR != iRet); 接下来我们我们就可以利用套接口接受来自客户端程序的连接了。我们以该套接口为参数调用accept函数,accept函数调用成功后,将建立一个可以接受和发送数据的套接口skAccept。view plaincopy to clipboardprint?1. / 客
66、户端向服务器端发送连接请求,服务器端接受客户端的连接 2. SOCKET skAccept = INVALID_SOCKET; 3. sockaddr_in adrClit; 4. ZeroMemory(&adrClit, sizeof(sockaddr_in); 5. int iLen = sizeof(sockaddr_in); 6. skAccept = acce
67、pt(skListen, (sockaddr*)&adrClit, &iLen); / 如果没有客户端程序请求连接,服务端程序会一直阻塞在这里等待连接 7. ASSERT(INVALID_SOCKET != skAccept); 在成功的建立了新套接口后,我们就可以利用该套接口在我们的线程函数中接收和发送数据了。 view plaincopy to clipboardprint?1. iRet
68、160;= recv(sr.skAccept, szBuf, c_iBufLen, 0); / 接收客户端发送的信息, 如果客户端不发送信息,则线程会阻塞到此处 2. if(0 = iRet) / 客户端优雅的关闭了此连接 3.
69、60;4. cout << "Connection " << sr.dwThreadID << " shutdown." << endl; 5. &
70、#160; break; 6. 7. else if(SOCKET_ERROR = iRet) / 客户端粗鲁的关闭了此连接或者接受信息出错 8.
71、160; 9. cout << "Connection " << sr.dwThreadID << " recv error." << endl; 10.
72、0; break; 11. 12. szBufiRet = '0' view plaincopy to clipboardprint?1. strcpy(szRely + c_iPrefLen,
73、160;szBuf); 2. iRet = send(sr.skAccept, szRely, strlen(szRely), 0); / 客户端如果没有足够的缓冲区接受信息,则线程会阻塞到此处 3. if(SOCKET_ERROR = iRet)
74、0;4. 5. cout << "Connection " << sr.dwThreadID << " send error." <<
75、endl; 6. break; 7. 在客户端关闭该套接口或者出现接收发送数据错误的时候,我们都应该关闭该套接口。请注意在调用关闭套接口的closesocket函数之前,我们应该先调用shutdown函数,以使对方可以收到该套接口已经关闭的信息。在调用shutdown函数之后
76、,我们应该使用recv函数读取在队列之中仍未读完的数据,最后我们就可以使用closesocket函数关闭该套接口了,这就是所谓的优雅关闭。view plaincopy to clipboardprint?1. / 关闭该套接口 2. iRet = shutdown(sr.skAccept, SD_SEND); 3. while(recv(sr.skAccept, szBuf, c_iBufLe
77、n, 0) > 0); 4. ASSERT(SOCKET_ERROR != iRet); 5. / 清理该套接口的资源 6. iRet = closesocket(sr.skAccept); 7. ASSERT(SOCKET_
78、ERROR != iRet); 监听socket的关闭也与上面套接口关闭的方法一致。在关闭了监听套接口后,我们的服务器程序应该调用WSACleanup函数,已完成对Winsocket和ws2_32.dll的清理。上述就是该类型服务器程序应用程序执行的全过程了。客户端程序的代码跟服务器程序的代码相似,程序代码如下所示:view plaincopy to clipboardprint?1. / 2. / file
79、;ClientBlockClientBlock.cpp 3. / 4. / brief 连接服务器并向服务器发送信息,然后接受服务器发送的信息. 5. / 6. #include <iostream> 7. #include <cassert> 8. #include <WinSock2.h> 9. #pragma
80、60;comment(lib, "ws2_32.lib" ) 10. #define ASSERT assert 11. using std:cin; 12. using std:cout; 13. using std:endl; 14. static const char c_szIP = "127.0.0.1" &
81、#160; 15. static const int c_iPort = 10001; 16. int main() 17. 18. int iRet = SOCKET_ERROR; 19. / 初始化Winsocket,所有Winsocket程序必须先使用WS
82、AStartup进行初始化 20. WSADATA data; 21. ZeroMemory(&data, sizeof(WSADATA); 22. iRet = WSAStartup(MAKEWORD(2, 0), &data); 23.
83、60;ASSERT(SOCKET_ERROR != iRet); 24. / 建立连接套接字 25. SOCKET skClient = INVALID_SOCKET; 26. skClient = socket(AF_INET, SOCK_STREAM, 0);
84、60;27. ASSERT(INVALID_SOCKET != skClient); 28. / 初始化连接套接字地址信息 29. sockaddr_in adrServ;
85、160; / 表示网络地址 30. ZeroMemory(&adrServ, sizeof(sockaddr_in); 31. adrServ.sin_family &
86、#160;= AF_INET; / 初始化地址格式,只能为AF_INET 32. adrServ.sin_port = htons(c_iPort); /&
87、#160;初始化端口,由于网络字节顺序和主机字节顺序相反,所以必须使用htons将主机字节顺序转换成网络字节顺序 33. adrServ.sin_addr.s_addr = inet_addr(c_szIP); / 初始化IP, 由于网络字节顺序和主机字节顺序相反,所以必须使用inet_addr将主机字节顺序转换成网络字节顺序 34. / 使用连接套接字进行
88、连接 35. iRet = connect(skClient, (sockaddr*)&adrServ, sizeof(sockaddr_in); 36. ASSERT(SOCKET_ERROR != iRet); 37. const int c_iBufLen = 512;
89、 38. char szBufc_iBufLen + 16 + 1 = '0' 39. for(;) 40. 41. cout << "
90、;what you will say:" 42. cin >> szBuf; 43. if(0 = strcmp("exit", szBuf) 44. &
91、#160; 45. break; 46. 47. / 向服务器端发送信息 48. &
92、#160; iRet = send(skClient, szBuf, strlen(szBuf), 0); / 服务器端如果没有足够的缓冲区接受信息,则线程会阻塞到此处 49. if(SOCKET_ERROR = iRet) 50. &
93、#160; 51. cout << "send error." << endl; 52. break; 53.
94、; 54. / 接收服务器端发送的信息 55. iRet = recv(skClient, szBuf, c_iBufLen, 0); / 如果服务器端没有发送数据,则会阻塞到此处 56.
95、60; if(0 = iRet) 57. 58. cout << "connection shutdown." << endl; 59. break; 60. 61.
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 出版技术应用趋势-洞察与解读
- 初中八年级物理下学期期末试题高频错点深度剖析与精准复习教案
- 统编版小学三年级语文上册第一单元《大青树下的小学》创新教案
- 小学一年级道德与法治下册《家人的爱》单元教案(统编版)
- 基于“教、学、评”一体化理念的“5E+C”教学模式在高一化学教学中的实践研究
- 大学本科公共关系学三年级跨学科课程《区域复兴的沟通艺术:公关心理学视角下的鲁尔工业区改造》教学设计
- 初中历史与社会·地理七年级下册《数字研学:探源宝岛台湾》跨学科融合式探究教案
- 四年级数学下册《加法运算律》单元起始课教学设计
- 初中音乐八年级下册《圣火辉映:奥林匹克颂》深度学习与跨学科融合教案
- 小学英语三年级下册Unit3 At the Zoo深度学习活动设计与教案
- 2026中国中煤能源集团有限公司春季校园招聘备考题库及答案详解一套
- 【《柴油列管式换热器工艺计算案例》6700字(论文)】
- IT系统运维流程与管理方案
- 小学五育并举工作制度
- 实施方案中项目建设方案
- 盘锦北方沥青股份有限公司招聘笔试题库2026
- 律所反洗钱内部控制制度
- 安全隐患整改通知(回复)单(样表)
- JCT412.1-2018 纤维水泥平板 第1部分:无石棉纤维水泥平板
- 出具社会保险缴费证明申请表
- 《道德经》(老子)课件
评论
0/150
提交评论