一、实验目标
1.掌握非阻塞模式编程方法;
2.掌握基于WSAAsyncSelect模型的高级编程技术。
二、实验内容
在已有服务器程序基础上,补全空缺关键代码,使之可以正常运行。该服务器代码实现功能:
1) 基于 WSAAsyncSelect模型;
2) 接收多客户端连接,显示客户端连接信息;
3) 显示客户端发送的信息;
4) 接收多客户端发送的信息后,将接收到的信息回发给对应的客户端;
三、代码
服务器端代码
#define WIN32_LEAN_AND_MEAN #include <stdio.h> #include <tchar.h> #include <iostream> #include <string> #include <winsock2.h> #include <windows.h> #include <stdio.h> #include <conio.h> #pragma comment(lib,"ws2_32.lib") #define BACKLOG 10 // 指定等待连接队列的最大长度 #define MAXDATASIZE 100 // 指定数据缓冲区的最大长度 #define WM_SOCKET (WM_USER + 1) // 定义套接口网络事件设置用户消息值 // 保存套接字信息 typedef struct _SOCKET_INFORMATION { CHAR Buffer[MAXDATASIZE]; // 发送和接收数据的套接字 WSABUF DataBuf; // 定义数据的内容和长度 SOCKET Socket; // 套接字 _SOCKET_INFORMATION *Next; // 向一个套接字信息 } SOCKET_INFORMATION, * LPSOCKET_INFORMATION; // 创建套接字信息 void CreateSocketInformation(SOCKET s); // 获取指定套接字的信息 LPSOCKET_INFORMATION GetSocketInformation(SOCKET s); char buf[MAXDATASIZE]; // 数据缓冲区 HWND MakeWorkerWindow(void); // 创建接收事件的窗口 SOCKET Accept; // 与客户端进行通信的套接字 LPSOCKET_INFORMATION SocketInfoList; // 所有套接字信息的列表 // 用于处理事件消息的窗口例程 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); // 主函数 int _tmain(int argc, _TCHAR* argv[]) { MSG msg; // 用于获取消息 DWORD Ret; // GetMessage()函数的返回结果 SOCKET sockfd; // 套接字 struct sockaddr_in my_addr; // 绑定到套接字的本地地址 HWND Window; // 用于接收事件消息的窗口句柄 // 创建窗口 if ((Window = MakeWorkerWindow()) == NULL) return 0; // 初始化Windows Sockets环境 WSADATA ws; WSAStartup(MAKEWORD(2,2),&ws); //监听 SOCKET sListen = ::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); // 创建套接字 sockfd = socket(AF_INET, SOCK_STREAM, 0); // 定义使用Window窗口接收FD_ACCEPT事件消息 WSAAsyncSelect(sockfd, Window, WM_SOCKET, FD_ACCEPT); // 设置地址和端口 my_addr.sin_family = AF_INET; my_addr.sin_port = htons(1234); my_addr.sin_addr.s_addr = INADDR_ANY; // 将套接字sockaddr绑定到my_addr if(bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))==SOCKET_ERROR) return -1; // 监听指定的套接字 listen(sockfd, BACKLOG); // 获取消息队列中获取一个消息 while(Ret = GetMessage(&msg, NULL, 0, 0)) { if (Ret == -1) { printf("GetMessage() failed with error %d\n", GetLastError()); return 0; } // 将虚拟键消息转换为字符串消息 TranslateMessage(&msg); // 将消息转发到窗口例程 DispatchMessage(&msg); } return 0; } // 窗口例程 LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { LPSOCKET_INFORMATION SocketInfo; // 套接字消息 SOCKADDR_IN clientAddr; // 客户端地址结构体 int addrLen = sizeof(clientAddr); // 客户端地址长度 // 处理用户自定义消息 if (uMsg == WM_SOCKET) { if (WSAGETSELECTERROR(lParam)) // 判断套接字是否错误 { printf("Socket failed with error %d\n", WSAGETSELECTERROR(lParam)); } else { switch(WSAGETSELECTEVENT(lParam)) // 判断消息类型 { case FD_ACCEPT: // 接收来自客户端的连接,Accept是与客户端进行通信的套接字 if ((Accept = accept(wParam, NULL, NULL)) == INVALID_SOCKET) { printf("accept() failed with error %d\n", WSAGetLastError()); break; } // 为Accept创建套接字信息 CreateSocketInformation(Accept); // 为Accept套接字订阅FD_READ、FD_WRITE和FD_CLOSE消息 WSAAsyncSelect(Accept, hwnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE); break; case FD_READ: // 处理FD_READ消息 SocketInfo = GetSocketInformation(wParam); // 获取套接字信息 int Rec = recv(SocketInfo->Socket, buf, MAXDATASIZE, 0); // 接收数据 buf[Rec] = '\0'; if( Rec > 0) // 打印接收到的字符串 { // 获取客户端地址信息 if (getpeername(SocketInfo->Socket, (SOCKADDR*)&clientAddr, &addrLen) == SOCKET_ERROR) { printf("getpeername() failed with error %d\n", WSAGetLastError()); } else { std::cout << "received: " << buf <<" Received from IP: "<< inet_ntoa(clientAddr.sin_addr) << " port: "<<ntohs(clientAddr.sin_port) << std::endl; } // 发送客户端地址信息回客户端 send(SocketInfo->Socket, buf, Rec, 0); } else if (Rec == 0) { // 客户端关闭连接 printf("Client disconnected.\n"); } else { // 发生错误 printf("recv() failed with error %d\n", WSAGetLastError()); } break; } } return 0; } // 如果不是用户自定义消息,则调用默认的窗口例程 return DefWindowProc(hwnd, uMsg, wParam, lParam); } LPSOCKET_INFORMATION GetSocketInformation(SOCKET s) { SOCKET_INFORMATION *SI = SocketInfoList; while(SI) { if (SI->Socket == s) return SI; SI = SI->Next; } return NULL; }
客户端代码
#define WIN32_LEAN_AND_MEAN #include <stdio.h> #include <tchar.h> #include <string> #include <winsock2.h> #include <iostream> #pragma comment(lib,"WS32_32.lib") #define BUF_SIZE 64 // 缓冲区大小 int _tmain(int argc, _TCHAR* argv[]) { WSADATA wsd; // 用于初始化Windows Socket SOCKET sHost; // 与服务器进行通信的套接字 SOCKADDR_IN servAddr; // 服务器地址 char buf[BUF_SIZE]; // 用于接受数据缓冲区 int retVal; // 调用各种Socket函数的返回值 // 初始化Windows Socket if(WSAStartup(MAKEWORD(2,2),&wsd) != 0) { printf("WSAStartup failed !\n"); return 1; } // 创建套接字 sHost = socket(AF_INET,SOCK_STREAM,IPPROTO_IP); if(INVALID_SOCKET == sHost) { printf("socket failed !\n"); WSACleanup(); return -1; } // 设置套接字为非阻塞模式 int iMode = 1; retVal = ioctlsocket(sHost, FIONBIO, (u_long FAR*) &iMode); if(retVal == SOCKET_ERROR) { printf("ioctlsocket failed !\n"); WSACleanup(); return -1; } // 设置服务器地址 servAddr.sin_family = AF_INET; servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); // 用户需要根据实际情况修改 servAddr.sin_port = htons(1234); // 在实际应用中,建议将服务器的IP地址和端口号保存在配置文件中 int sServerAddlen = sizeof(servAddr); // 计算地址的长度 // 循环等待 while(true) { // 连接服务器 Sleep( 200 ); retVal = connect(sHost,(LPSOCKADDR)&servAddr,sizeof(servAddr)); Sleep( 200 ); if(SOCKET_ERROR == retVal) { int err = WSAGetLastError(); if(err == WSAEWOULDBLOCK || err == WSAEINVAL) // 无法立即完成非阻塞套接字上的操作 { //Sleep(500); continue; } else if(err == WSAEISCONN) // 已建立连接 { break; } else { continue; } } } // 循环向服务器发送字符串,并显示反馈信息。 // 发送quit将使服务器程序退出,同时客户端程序自身也将退出 while(true) { // 向服务器发送数据 printf("Please input a string to send: "); // 接收输入的数据 std::string str; std::getline(std::cin, str); // 将用户输入的数据复制到buf中 ZeroMemory(buf,BUF_SIZE); strcpy(buf,str.c_str()); // 循环等待 while(true) { // 向服务器发送数据 retVal = send(sHost,buf,strlen(buf),0); if(SOCKET_ERROR == retVal) { int err = WSAGetLastError(); if(err == WSAEWOULDBLOCK) // 无法立即完成非阻塞套接字上的操作 { Sleep(500); continue; } else { printf("send failed !\n"); closesocket(sHost); WSACleanup(); return -1; } } break; } while(true) { ZeroMemory(buf,BUF_SIZE); // 清空接收数据的缓冲区 retVal = recv(sHost,buf,sizeof(buf)+1,0); // 接收服务器回传的数据 if(SOCKET_ERROR == retVal) { int err = WSAGetLastError(); // 获取错误编码 if(err == WSAEWOULDBLOCK) // 接收数据缓冲区暂无数据 { Sleep(100); printf("waiting back msg !\n"); continue; } else if(err == WSAETIMEDOUT || err == WSAENETDOWN) { printf("recv failed !\n"); closesocket(sHost); WSACleanup(); return -1; } break; } break; } //ZeroMemory(buf,BUF_SIZE); // 清空接收数据的缓冲区 //retVal = recv(sHost,buf,sizeof(buf)+1,0); // 接收服务器回传的数据 printf("Recv From Server: %s\n",buf); // 如果收到quit,则退出 if(strcmp(buf, "quit") == 0) { printf("quit!\n"); break; } } // 释放资源 closesocket(sHost); WSACleanup(); // 暂停,按任意键继续 system("pause"); return 0; }
最终效果
四、结论
本次实验是基于 WSAAsyncSelect模型的简略代码,其中还有许多问题需要解决,代码仅供参考