高级Socket编程技术
实验目的
1.了解阻塞与非阻塞模式Socket编程的工作原理。
2.了解Socket编程的5种模型的工作原理和使用方法。
3.掌握Select模型和WSAAsyncSelect模型的网络应用程序编程。
二、实验内容
1.参考教材编写工作于非阻塞模式的多线程TCP服务器应用程序和工作于非阻塞模式客户端应用程序,并进行数据收发测试。
2.编写基于WSAAsyncSelect模型、工作于并发方式的基于对话框的TCP服务器程序和客户端程序,并进行数据收发测试。
基于WSAAsyncSelect模型的服务器程序
基于WSAAsyncSelect模型的客户端程序
要求:(1)客户端程序和服务器端程序之间都能够多次发送消息给对方,也能够多次接收对方发送的消息。(2)将上述1小题的全部代码附在附录上,将2小题源代码打包和实验报告一并提交。
三、实验原理
1.非阻塞模式
概念
Ioctlsocket
函数功能是控制套接口的模式,可用于任一状态的任一套接口,可以调用次函数来讲Socket设置为非阻塞模式。
CreateThread函数
用于创建于一个专门的通信线程,实现多客户端通信。
WSAEWOULDBLOCK
Winsock异常 WSAEWOULDBLOCK说明Output Buffer 已经满了,无法再写入数据
2. WSAAsyncSelect模型工作原理
WSAAsyncSelect模型是Windows Sockets的一个异步I/O模型。应用程序可以用它在一个套接字上接收以Windows消息为基础的网络事件。应用程序创建套接字后,调用WSAAsyncSelect()函数注册感兴趣的网络事件,当事件发生时,windows窗口接收到消息,然后程序就可以对收到的网络事件进行处理。WSAAsyncSelect模型是非阻塞的。应用程序在调用recv()函数接收数据之前,调用WSAAsyncSelect()函数注册网络事件。WSAAsyncSelect()函数立即返回,线程继续运行。当系统中数据准备好时,向应用程序发送消息。程序接收到消息后调用recv()函数接收数据。
WSAAsyncSelect函数编程方法步骤
1)初始化套接字相关信息:
(2)开始启动一个事件通知。WSAAsyncSelect(Sock,hWnd,自定义消息,网络事件)
(3)响应窗口的自定义消息处理函数,其中lparam的高位字包含了可能出现的错误
代码,低字节表示发生的网络事件。wParam表示发生网络事件的套接字。
WSAGETSELECTERROR(lParam);查看是否出现错误,获取低字节位
WSAGETSELECTEVENT(lParam);查看发生了什么事件,获取高字节位
事件种类请查看MSDN,可用WSAGetLastError()来获取错误信息。
四、实验步骤
1.非阻塞模式下的客服端和服务器端信息的收发测试,如图,客户端1在1588端口上向服务器端发送数据,客户端2在3124端口上向服务器端发送数据。处于非阻塞状态。
- 基于WSAAsyncSelect模型、工作于并发方式的基于对话框的TCP服务器程序和客户端程序,并进行数据收发测试。
五、实验小结
附:程序源代码
服务器(窗口)
#include "stdafx.h"
#include "Exp07_AsyncServer.h"
#include<WINSOCK2.h>
#pragma comment(lib,"ws2_32.lib")
#define MAX_LOADSTRING 100
#define WM_SOCKET WM_USER+0x10 //①自定义socket消息
// 全局变量:
HINSTANCE hInst; // 当前实例
TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
SOCKET sockSer,sockConn; //服务器端要创建两个套接字
SOCKADDR_IN addrSer, addrCli;
int len =sizeof(SOCKADDR);
char sendbuf[256],recvbuf[256];
char clibuf[999]="客户端: >",serbuf[999]="服务器: >";
// 此代码模块中包含的函数的前向声明:
BOOL InitInstance(HINSTANCE, int);
INT_PTR CALLBACK AsyncSrvProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此放置代码。
MSG msg;
HACCEL hAccelTable;
// 初始化全局字符串
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_EXP07_ASYNCSERVER, szWindowClass, MAX_LOADSTRING);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_EXP07_ASYNCSERVER));
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // 将实例句柄存储在全局变量中
hWnd =CreateDialog(hInstance, MAKEINTRESOURCE(IDD_SERVER), GetDesktopWindow(), AsyncSrvProc);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK AsyncSrvProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
char ip[16],port[5];
switch (message)
{
case WM_INITDIALOG:
SetDlgItemText(hDlg,IDC_IP,"127.0.0.1"); //设置ip文本框的内容
SetDlgItemText(hDlg,IDC_PORT,"5566"); //设置端口文本框的内容
WSADATA wsaData;
if(WSAStartup(MAKEWORD(2,2), &wsaData)) {
MessageBox(hDlg,"Winsock加载失败","警告",0);
WSACleanup();
return (INT_PTR)TRUE;
}
case WM_SOCKET: //三:自定义消息
if(WSAGETSELECTERROR(lParam))
{
MessageBoxA(NULL, "Socket错误", "tip", 0);
}
else{
switch (WSAGETSELECTEVENT(lParam)) { //选择要处理的事件
case FD_ACCEPT:{ //接收请求事件
sockConn=accept(sockSer,(SOCKADDR*) &addrCli,&len);
WSAAsyncSelect(sockConn, hDlg,WM_SOCKET,FD_READ | FD_CLOSE);
}
break;
case FD_READ: { //可读事件
recv(sockConn,recvbuf,256,0); //接收消息
strcat_s(clibuf,recvbuf);
//将接收到的消息添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
strcpy_s(clibuf, "客户端: >"); } //重新给字符串赋值
break;
case FD_CLOSE: { //关闭连接事件
MessageBoxA(NULL, "正常关闭连接", "tip", 0); }
break;
}//switch (WSAGETSELECTEVENT(lParam))
}//else
break;
case WM_COMMAND:
switch(LOWORD(wParam) ){
case IDC_QUIT: //单击了退出按钮
EndDialog(hDlg, LOWORD(wParam));
closesocket(sockSer); //关闭套接字
WSACleanup(); //卸载WinSock协议栈
return TRUE;
case IDC_CREATE: //单击了创建服务器按钮
GetDlgItemText(hDlg,IDC_IP,ip,16); //获取编辑框中的IP值
GetDlgItemText(hDlg,IDC_PORT,port,5);
EnableWindow(GetDlgItem(hDlg,IDC_CREATE),FALSE); //禁用按钮
sockSer=socket(AF_INET,SOCK_STREAM,0);
//②设置异步方式
WSAAsyncSelect(sockSer, hDlg,WM_SOCKET,FD_ACCEPT);
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(atoi(port));
addrSer.sin_addr.S_un.S_addr=inet_addr(ip);
bind(sockSer,(SOCKADDR*) &addrSer,len); //绑定套接字
listen(sockSer,5); //监听
break;
case IDC_SEND: //单击了发送按钮
GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256);
send(sockConn,sendbuf,strlen(sendbuf)+1,0); //发送消息
SetDlgItemText(hDlg,IDC_SENDBUF,NULL); //清空编辑框
strcat_s(serbuf,sendbuf);
//将已发送的消息添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);
strcpy_s(serbuf, "服务器: >");
break;
} //switch(LOWORD(wParam) )
}//switch (message)
return (INT_PTR)FALSE;
}
客户端(窗口)
#include "stdafx.h"
#include "Exp07_AsyncClient.h"
#include<WINSOCK2.h>
#pragma comment(lib,"ws2_32.lib")
#define WM_SOCKET WM_USER+0x10 //①自定义socket消息
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例
TCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
SOCKET sockCli; //声明客户端套接字
SOCKADDR_IN addrSer;
// 此代码模块中包含的函数的前向声明:
BOOL InitInstance(HINSTANCE, int);
INT_PTR CALLBACK AsyncClientProc(HWND, UINT, WPARAM, LPARAM);
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPTSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此放置代码。
MSG msg;
HACCEL hAccelTable;
// 初始化全局字符串
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_EXP07_ASYNCCLIENT, szWindowClass, MAX_LOADSTRING);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_EXP07_ASYNCCLIENT));
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // 将实例句柄存储在全局变量中
hWnd =CreateDialog(hInstance, MAKEINTRESOURCE(IDD_ASYNCCLIENT), GetDesktopWindow(), AsyncClientProc);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK AsyncClientProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
char ip[16], port[5];
char sendbuf[256],recvbuf[256],ren[10];
char serbuf[256]="服务器: >", clibuf[256]="客户端: >";
int len =sizeof(SOCKADDR);
int res;
switch (message){
case WM_INITDIALOG: //对话框初始化
SetDlgItemText(hDlg,IDC_IP,"127.0.0.1");
SetDlgItemText(hDlg,IDC_PORT,"5566");
WSADATA wsaData;
WSAStartup(MAKEWORD(2,2), &wsaData); //加载协议栈
sockCli=socket(AF_INET,SOCK_STREAM,0); //创建套接字
//设置异步方式
WSAAsyncSelect(sockCli, hDlg,WM_SOCKET, FD_READ |FD_CLOSE);
break;
case WM_SOCKET: //自定义消息
switch (WSAGETSELECTEVENT(lParam)){
case FD_READ: { //可读事件
recv(sockCli,recvbuf,256,0); //接收信息
strcat_s(serbuf,recvbuf);
//将接收到的数据添加到列表框中
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)serbuf);
strcpy_s(serbuf, "服务器: >"); } //重新给字符串赋值
break;
case FD_CLOSE: { //关闭连接事件
MessageBoxA(NULL, "正常关闭连接", "tip", 0); }
break; }
break;
case WM_COMMAND:{
switch(LOWORD(wParam)) {
case IDC_QUIT: //单击了退出按钮
EndDialog(hDlg, LOWORD(wParam));
closesocket(sockCli);
WSACleanup();
break;
case IDC_CONN: //单击了“连接服务器”按钮
GetDlgItemText(hDlg,IDC_IP,ip,16); //获取ip
GetDlgItemText(hDlg,IDC_PORT,port,5);
EnableWindow(GetDlgItem(hDlg,IDC_CONN),FALSE); //禁用连接按钮
addrSer.sin_family=AF_INET;
addrSer.sin_port=htons(atoi(port));
addrSer.sin_addr.S_un.S_addr=inet_addr(ip);
res =connect(sockCli,(SOCKADDR*)&addrSer,sizeof(SOCKADDR));
// itoa(res,ren,10);
// MessageBox(NULL,ren,"通知",0);
if(res ==10035 || res==-1)
MessageBox(NULL,"客户端连接服务器成功","通知",0);
else
MessageBox(NULL,"客户端连接服务器失败","警告",0);
break;
case IDC_SEND: //单击了“发送”按钮
GetDlgItemText(hDlg,IDC_SENDBUF,sendbuf,256); //获得发送框的内容
send(sockCli,sendbuf,strlen(sendbuf)+1,0); //发送消息
SetDlgItemText(hDlg,IDC_SENDBUF,""); //清空发送框
strcat_s(clibuf,sendbuf);
SendDlgItemMessage(hDlg,IDC_RECVBUF,LB_ADDSTRING,0,(LPARAM)clibuf);
strcpy_s(clibuf, "客户端: >"); //重新给字符串赋值
break;
}//switch(LOWORD(wParam))
}//case WM_COMMAND:
}
return (INT_PTR)FALSE;
}
服务器:
//
#include <afxwin.h>
#include "stdafx.h"
#include <WINSOCK2.H>
#include <iostream>
#pragma comment(lib,"WS2_32.lib")
#define BUF_SIZE 64 // 缓冲区大小
sockaddr_in addrClient; // 客户端地址
DWORD WINAPI AnswerThread(LPVOID lparam)
{
char buf[BUF_SIZE]; // 用于接受客户端数据的缓冲区
int retVal; // 调用各种Socket函数的返回值
SOCKET sClient=(SOCKET)(LPVOID)lparam;
// 循环接收客户端的数据,直接客户端发送quit命令后退出。
while(true)
{
ZeroMemory(buf,BUF_SIZE); // 清空接收数据的缓冲区
retVal = recv(sClient,buf,BUFSIZ,0); // 接收来自客户端的数据,因为是非阻塞模式,所以即使没有数据也会继续
if(SOCKET_ERROR == retVal)
{
int err = WSAGetLastError(); // 获取错误编码
if(err == WSAEWOULDBLOCK) // 接收数据缓冲区暂无数据
{
Sleep(100);
continue;
}
else if(err == WSAETIMEDOUT || err == WSAENETDOWN)
{
printf("recv failed !\n");
closesocket(sClient);
WSACleanup();
return -1;
}
}
// 获取当前系统时间
SYSTEMTIME st;
GetLocalTime(&st);
char sDateTime[30];
sprintf(sDateTime, "%4d-%2d-%2d %2d:%2d:%2d",st.wYear,st.wMonth,st.wDay,st.wHour,st.wMinute,st.wSecond);
// 打印输出的信息
printf("%s, Recv From Client [%s:%d] :%s\n", sDateTime, inet_ntoa(addrClient.sin_addr), addrClient.sin_port, buf);
// 如果客户端发送quit字符串,则服务器退出
if(strcmp(buf, "quit") == 0)
{
retVal = send(sClient,"quit",strlen("quit"),0);
break;
}
else // 否则向客户端发送回显字符串
{
char msg[BUF_SIZE];
sprintf(msg, "Message received - %s", buf);
while(true)
{
// 向服务器发送数据
retVal = send(sClient, msg, strlen(msg),0);
if(SOCKET_ERROR == retVal)
{
int err = WSAGetLastError();
if(err == WSAEWOULDBLOCK) // 无法立即完成非阻塞套接字上的操作
{
Sleep(500);
continue;
}
else
{
printf("send failed !\n");
closesocket(sClient);
WSACleanup();
return -1;
}
}
break;
}
}
}
// 关闭套接字
closesocket(sClient);
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsd; // WSADATA变量,用于初始化Windows Socket
SOCKET sServer; // 服务器套接字,用于监听客户端请求
SOCKET sClient; // 客户端套接字,用于实现与客户端的通信
int retVal; // 调用各种Socket函数的返回值
// 初始化套接字动态库
if(WSAStartup(MAKEWORD(2,2),&wsd) != 0)
{
printf("WSAStartup failed !\n");
return 1;
}
// 创建用于监听的套接字
sServer = socket(AF_INET,SOCK_STREAM, IPPROTO_IP);
if(INVALID_SOCKET == sServer)
{
printf("socket failed !\n");
WSACleanup();
return -1;
}
// 设置套接字为非阻塞模式
int iMode = 1;
retVal = ioctlsocket(sServer, FIONBIO, (u_long FAR*) &iMode);
if(retVal == SOCKET_ERROR)
{
printf("ioctlsocket failed !\n");
WSACleanup();
return -1;
}
// 设置服务器套接字地址
SOCKADDR_IN addrServ;
addrServ.sin_family = AF_INET;
addrServ.sin_port = htons(9990); // 监听端口为9990
addrServ.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
// 绑定套接字sServer到本地地址,端口9990
retVal = bind(sServer,(const struct sockaddr*)&addrServ,sizeof(SOCKADDR_IN));
if(SOCKET_ERROR == retVal)
{
printf("bind failed !\n");
closesocket(sServer);
WSACleanup();
return -1;
}
// 监听套接字
retVal = listen(sServer, SOMAXCONN);
if(SOCKET_ERROR == retVal)
{
printf("listen failed !\n");
closesocket(sServer);
WSACleanup();
return -1;
}
// 接受客户请求
printf("TCP Server start...\n");
int addrClientlen = sizeof(addrClient);
// 循环等待
while(true)
{
sClient = accept(sServer,(sockaddr FAR*)&addrClient,&addrClientlen);
if(INVALID_SOCKET == sClient)
{
int err = WSAGetLastError();
if(err == WSAEWOULDBLOCK) // 无法立即完成非阻塞套接字上的操作
{
Sleep(100);
continue;
}
//else
//{
// //printf("accept failed !\n");
// //closesocket(sServer);
// //WSACleanup();
// //return -1;
// Sleep(100);
// continue;
//}
}
// 创建专用通信线程
DWORD dwThreadId;
CreateThread(NULL, NULL, AnswerThread, (LPVOID)sClient, 0, &dwThreadId);
}
// 释放套接字
closesocket(sServer);
WSACleanup();
// 暂停,按任意键退出
system("pause");
return 0;
}
客户端:
#include "stdafx.h"
#include <winsock2.h>
#include <string>
#include <iostream>
#pragma comment(lib,"WS2_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(9990);
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;
printf("connect failed !\n");
closesocket(sHost);
WSACleanup();
return -1;
}
}
}
// 循环向服务器发送字符串,并显示反馈信息。
// 发送quit将使服务器程序退出,同时客户端程序自身也将退出
while(true)
{
// 向服务器发送数据
printf("Please input a string to send: ");
std::string str;
std::getline(std::cin, str); // 接收输入的数据
ZeroMemory(buf,BUF_SIZE);
strcpy(buf,str.c_str()); // 将用户输入的数据复制到buf中
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;
}
int count=0;
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);
count=count+1;
if (count<10){
printf("waiting back msg !\n");
continue;
}
else{
printf("recv back msg failed! \n");
break;
}
}
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\n",buf);
if(strcmp(buf, "quit") == 0) // 如果收到quit,则退出
{
printf("quit!\n");
break;
}
}
closesocket(sHost);
WSACleanup();
system("pause");
return 0;
}