1 效果展示
1.1 服务器和客户端界面展示
1.2 一对一之间通信的功能展示
步骤:
- 启动服务器进行监听
- 客户端再连接服务器端
- 服务器与客户端互发消息
1.3 一对多之间通信的功能展示
步骤与前面一样
2 知识预备
2.1 WSAAsyncSelect模型介绍
????WSAAsyncSelect模型是Windows Sockets的一个异步I/O模型。利用该模型应用程序可以在一个套接字上,接收以Windows消息为基础的网络事件。Windows Sockets应用程序在创建套接字后,调用WSAAsyncSelect()函数注册感兴趣的网络事件。当该事件发生时Windows窗口收到消息,然后应用程序就可以对接收到的网络事件进行处理。
2.2 WSAAsyncSelect模型的过程图
????WSAAsyncSelect模型是非阻塞的。如图所示, Windows Sockets应用程序在调用recv)函数接收数据之前,调用WSAAsyncSelect()函数注册网络事件。WSAAsyncSelect()函数立即返回,线程继续运行。当系统中数据准备好时,向应用程序发送消息。应用程序接收到这个消息后,调用recv()函数接收数据。
2.3 与SELECT模型比较
SELECT模型介绍
????WSAAsyncSelect模型与Select模型的相同点是,他们都可以对Windows套接字应用程序所使用的多个套接字进行有效的管理。 但WSAAsyncSelect模型与Select模型相比存在以下不同。
- WSAAsyncSelect模型是异步的。在应用程序中调用WSAAsyncSelect()函数,通知系统感兴趣的网络事件,该函数立即返回,应用程序继续运行。
- 发生网络事件时,应用程序得到通知的方式不同。select()函数返回时,说明某个或者某些套接字满足可读可写的条件,应用程序需要使用FD_ISSET宏,判断套接字是否存在于可读可写集合中。而对于WSAAsyncSelect模型来说,当网络事件发生时,系统向应用程序发送消息。
- WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口.而Select模型广泛应用在Unix系统和Windows系统,使用该模型不需要创建窗口。
- 应用程序中调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式。而应用程序中调用select()函数后,并不能改变该套接字的工作方式。
2.4 套接字WSAAsyncSelect模型实现
WSAAsyncSelect()函数功能是请求当网络事件发生时为套接字发送消息。该函数声明如下:
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
unsigned int wMsg,
long lEvent
);
- s:需要事件通知的套接字。
- hWnd:当网络事件发生时接收消息的窗口句柄。
- wMsg:当网络事件发生时窗口收到的消息。
- lEvent:应用程序感兴趣的网络事件集合。
当应用程序中调用该函数后,自动将套接字设置为非阻塞模式。通常,应用程序声明的消息要比 Windows的WM_USER值大,以避免该消息与Windows预定义消息发生混淆。
2.5 网络事件种类
种类 | 含义 |
---|
FD_READ | 欲接收可读的通知 | FD_WRITE | 欲接收可写读的通知 | FD_ACCEPT | 欲接收等待接受连接的通知 | FD_CONNECT | 欲接收一次连接或者多点jion操作完成的通知 | FD_OOB | 欲接收有带外(OOB)数据到达的通知 | FD_CLOSE | 欲接收套接字关闭的通知 | FD_QoS | 欲接收套接字服务质量发生变化的通知 | FD_GROUP_QoS | 欲接收套接字组服务质量发生变化的通知 | FD_ROUTING_INTERFACE_CHANGE | 欲在指定方向上,与路由接口发生变化的通知 | FD_ADDRESS_LIST_CHANGE | 欲接收针对套接字的协议家族,本地地址列表发生变化的通知 |
调用WSAAsyncSelect()函数如下所示:
WSAAsyncSelect (s,hwnd,WM_SOCKET,FD_CONNECT | FD_READ|ED_CLOSE);
应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。 窗口例程通常定义如下:
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
- hWnd:窗口句柄。
- uMsg:消息。对Windows Sockets应用程序来说感兴趣的是在WSAAsyncSelect()函数中,由应用程序定义的消息。
- wParam:消息参数。在Windows Sockets应用程序中,该参数指明发生网络事件的套接字。
- lParam:消息参数。在Windows Sockets应用程序中,该参数低字节指明已经发生的网络事件。高字节包含可能出现的错误代码。
2.6 WSAAsyncSelect模型的优势和不足
优势:
- 该模型的使用方便了在基于消息的Windows环境下开发套接字的应用程序。开发人员可以像处理其他消息一样对网络事件消息进行处理。
- 该模型确保接收所有数据提供了很好的机制。通过注册FD_CLOSE网络事件,从容关闭服务器与客户端的连接保证了数据全部接收。
不足:
- 该模型局限在,他基于Windows的消息机制,必须在应用程序中创建窗口。当然,在开发中可以根据具体情况是否显示该窗口。MFC的CSocketWnd类就是用来创建一个不显示的窗口,并在该类中声明接收网络事件消息处理函数。
- 由于调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞状态。当应用程序为接收到网络事件调用相应函数时,未必能够成功返回。这无疑增加了开发人员使用该模型的难度。对于这一点可以从MFC CSocket类的Accept()、Receive()和Send()函数的实现得到验证。
3 代码展示
3.1 服务端
3.1.1 定义变量
在ServerDlg.h里
#define WM_SOCKET WM_USER+1000
...
public:
WSADATA wsd;
SOCKET listenSocket;
SOCKET acceptSocket;
sockaddr_in addr;
sockaddr_in addr1;
int n = 0;
SOCKET s[10];
int flag = 0;
vector<SOCKET> socket_arr[100];
int count = 0;
CListCtrl m_Listctrl;
CEdit m_SendText;
CButton m_Send;
CIPAddressCtrl m_Ip;
CEdit m_Port;
CButton m_Start;
afx_msg void OnBnClickedButton2();
afx_msg void OnBnClickedButton1();
CButton m_Close;
afx_msg void OnBnClickedButton3();
CEdit m_num;
void InitSocket();
...
在ServerDlg.cpp里
3.1.2 初始化界面
void CServerDlg::InitSocket()
{
m_Send.EnableWindow(false);
m_SendText.EnableWindow(false);
m_Close.EnableWindow(false);
char * strIP = "127.0.0.1";
DWORD dwAddress = ntohl(inet_addr(strIP));
m_Ip.SetAddress(dwAddress);
m_Port.SetWindowText("8080");
}
3.1.3 启动服务器
void CServerDlg::OnBnClickedButton2()
{
CString str, str1;
BYTE a, b, c, d;
m_Ip.GetAddress(a, b, c, d);
str.Format(_T("%d.%d.%d.%d"), a, b, c, d);
m_Port.GetWindowText(str1);
addr.sin_family = AF_INET;
addr.sin_port = htons(_ttoi(str1));
addr.sin_addr.S_un.S_addr = inet_addr(CT2CA(str));
listenSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
::bind(listenSocket, (sockaddr*)&addr, sizeof(addr));
::listen(listenSocket, 5);
CString str2, str3;
str2 = ::inet_ntoa(addr.sin_addr);
str3.Format("%d", ntohs(addr.sin_port));
m_Listctrl.InsertItem(count++, _T("服务器["+str2+":"+str3+"]已启动监听..."));
m_Start.EnableWindow(false);
m_Close.EnableWindow(true);
::WSAAsyncSelect(listenSocket, this->m_hWnd, WM_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE);
}
3.1.4 消息响应
LRESULT CServerDlg::OnSocket(WPARAM wPARAm, LPARAM lParam) {
CString str;
switch (lParam) {
case FD_ACCEPT:
{
int len = sizeof(addr1);
acceptSocket = ::accept(listenSocket, (sockaddr*)&addr1, &len);
socket_arr->push_back(acceptSocket);
flag++;
n = n + 1;
CString str1, str2,str3;
str1 = ::inet_ntoa(addr1.sin_addr);
str2.Format("%d", ntohs(addr1.sin_port));
str.Format("目前有%d个客户端\n", n);
str3.Format("%d", flag);
m_Listctrl.InsertItem(count++, _T("用户"+str3+"[" + str1 + ":" + str2 + "]已经连接成功"));
m_Listctrl.InsertItem(count++, _T(str));
m_Send.EnableWindow(true);
m_SendText.EnableWindow(true);
}
break;
case FD_READ:
{
char buf[100] = { '\0' };
int ret = 0;
int i = 0;
for (vector<SOCKET>::iterator it = socket_arr->begin(); it != socket_arr->end(); it++) {
ret=::recv(*it, buf, 100, 0);
SOCKADDR_IN addClient;
int nLen = sizeof(addClient);
getpeername(socket_arr->at(i), (sockaddr*)&addClient, &nLen);
CString str, str1, str2;
str.Format("%d", i + 1);
str1 = ::inet_ntoa(addClient.sin_addr);
str2.Format("%d", ntohs(addClient.sin_port));
if (ret > 0) {
m_Listctrl.InsertItem(count++, _T("用户" + str +"["+str1+":"+str2+ "]说:" + buf));
}
i++;
}
}
break;
case FD_CLOSE:
{
int ret = 0;
char buf[100] = { '\0' };
int i = 0;
for (vector<SOCKET>::iterator it = socket_arr->begin(); it != socket_arr->end(); it++) {
ret = ::recv(*it, buf, 100, 0);
if (ret==0){
n = n - 1;
CString str;
str.Format("目前有%d个客户端\n", n);
SOCKADDR_IN addClient;
int nLen = sizeof(addClient);
getpeername(*it, (sockaddr*)&addClient, &nLen);
CString str1, str2,str3;
str1 = ::inet_ntoa(addClient.sin_addr);
str2.Format("%d", ntohs(addClient.sin_port));
str3.Format("%d", i+1);
m_Listctrl.InsertItem(count++, _T("用户"+str3+"[" + str1 + ":" + str2 + "]主动关闭"));
m_Listctrl.InsertItem(count++, _T(str));
if (!(n > 0)) {
m_Send.EnableWindow(false);
m_SendText.EnableWindow(false);
}
it = socket_arr->erase(it);
if (it == socket_arr->end()) {
break;
}
}
}
}
break;
}
return 0;
}
3.1.5 发送消息
void CServerDlg::OnBnClickedButton1()
{
CString str = "";
m_SendText.GetWindowText(str);
if (str == "") {
MessageBox(_T("发送消息不能为空"), _T("提示"));
}
else {
CString str1;
m_num.GetWindowText(str1);
if (str1 == "") {
MessageBox(_T("发送的人不能为空"), _T("提示"));
}
else {
int num = _ttoi(str1);
if (::send(socket_arr->at(num-1), CT2CA(str), str.GetLength(), 0) != SOCKET_ERROR) {
m_Listctrl.InsertItem(count++, _T("服务器对用户" + str1 + "说:" + str));
m_SendText.SetWindowText("");
m_num.SetWindowText("");
}
else {
MessageBox(_T("发送消息失败"), _T("提示"));
}
}
}
}
3.1.6 断开
void CServerDlg::OnBnClickedButton3()
{
closesocket(listenSocket);
closesocket(acceptSocket);
WSACleanup();
m_Send.EnableWindow(false);
m_SendText.EnableWindow(false);
m_Start.EnableWindow(true);
m_Close.EnableWindow(false);
m_Listctrl.InsertItem(count++, _T("服务器已关闭"));
}
3.2 客户端
仅展示连接和发送(依葫芦画瓢)
3.2.1 连接服务器
void CClientDlg::OnBnClickedButton1()
{
CString str, str1;
int port;
BYTE a, b, c, d;
m_IpAddress.GetAddress(a, b, c, d);
str.Format(_T("%d.%d.%d.%d"), a, b, c, d);
m_Port.GetWindowText(str1);
if (str=="" || str1=="") {
MessageBox(_T("服务器地址或端口不能为空"), _T("提示"), MB_ICONEXCLAMATION);
}
else {
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) {
MessageBox(_T("WSAStartup error:" + WSAGetLastError()), _T("提示"), MB_ICONEXCLAMATION);
}
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) {
MessageBox(_T("create socket error : " + WSAGetLastError()), _T("提示"), MB_ICONEXCLAMATION);
}
port=_ttoi(str1);
addr.sin_port = ntohs(port);
addr.sin_addr.S_un.S_addr = inet_addr(CT2CA(str));
addr.sin_family = AF_INET;
if (::connect(s, (sockaddr*)&addr, sizeof(addr))!= SOCKET_ERROR) {
::WSAAsyncSelect(s, this->m_hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
m_ListCtrl.InsertItem(0, _T("连接服务器成功"));
m_Close.EnableWindow(true);
m_Connect.EnableWindow(false);
m_Port.EnableWindow(false);
m_IpAddress.EnableWindow(false);
m_Send.EnableWindow(true);
m_SendText.EnableWindow(true);
}
else {
m_ListCtrl.InsertItem(0, _T("连接服务器失败,请重试"));
}
}
}
3.2.2 消息响应
LRESULT CClientDlg::OnSocket(WPARAM wPARAm, LPARAM lParam) {
char buf[100] = { '\0' };
switch (lParam) {
case FD_READ:
{
::recv(s, buf, 100, 0);
CString str1, str2;
str1 = ::inet_ntoa(addr.sin_addr);
str2.Format("%d", ntohs(addr.sin_port));
m_ListCtrl.InsertItem(0, _T("服务器[" + str1 + ":" + str2 + "]:" + buf));
}
break;
case FD_CLOSE:
{
CString str1, str2;
str1 = ::inet_ntoa(addr.sin_addr);
str2.Format("%d", ntohs(addr.sin_port));
m_ListCtrl.InsertItem(0, _T("服务器[" + str1 + ":" + str2 + "]已关闭"));
m_Connect.EnableWindow(true);
m_Send.EnableWindow(false);
m_SendText.EnableWindow(false);
}
break;
}
return 0;
}
3.2.3 发送消息
void CClientDlg::OnBnClickedButton2()
{
CString str1;
m_SendText.GetWindowText(str1);
if (str1 == "") {
MessageBox(_T("发送消息不能为空"), _T("提示"));
}
else {
::send(s, CT2CA(str1), str1.GetLength(), 0);
m_ListCtrl.InsertItem(0, _T("我说:" + str1));
m_SendText.SetWindowText("");
}
}
4 总结与思考
????在设计中遇到的一个最主要问题就是如何在服务端显示不同客户端的信息以及接收它们不同的信息。查阅了很多资料,解决方法如下: 1、如何在服务端显示不同的客户端信息? 此代码来源于ServerDlg.cpp中的消息响应函数中
int len = sizeof(addr1);
acceptSocket = ::accept(listenSocket, (sockaddr*)&addr1, &len);
socket_arr->push_back(acceptSocket);
flag++;
n = n + 1;
...
CString str1, str2,str3;
str1 = ::inet_ntoa(addr1.sin_addr);
str2.Format("%d", ntohs(addr1.sin_port));
1、每当接收到一个客户端连接,就把该套接字储存起来,这里我用的是vector容器,当然也可以用数组。 2、inet_ntoa函数主要是返回点分十进制的字符串,ntohs函数主要是是将网络字节转为主机字节
2、如何在服务端接收不同客户端发来的信息?
case FD_READ:
{
char buf[100] = { '\0' };
int ret = 0;
int i = 0;
for (vector<SOCKET>::iterator it = socket_arr->begin(); it != socket_arr->end(); it++) {
ret=::recv(*it, buf, 100, 0);
SOCKADDR_IN addClient;
int nLen = sizeof(addClient);
getpeername(socket_arr->at(i), (sockaddr*)&addClient, &nLen);
CString str, str1, str2;
str.Format("%d", i + 1);
str1 = ::inet_ntoa(addClient.sin_addr);
str2.Format("%d", ntohs(addClient.sin_port));
if (ret > 0) {
m_Listctrl.InsertItem(count++, _T("用户" + str +"["+str1+":"+str2+ "]说:" + buf));
}
i++;
}
}
break;
这里有一个非常重要的函数是getpeername,它用于获取与某个套接字关联的外地协议地址。 再加上迭代器遍历acceptsocket,就可以循环接收不同客户端发来的消息
那还有一个疑问?反过来服务端给不同客户端发送消息怎么办?
解决办法,给每一个客户端标上flag,在界面添加一个文本框,每次服务端需要选择用户来发送消息,这样就可以根据标号来确定是哪一个acceptsocket了。
CString str1;
m_num.GetWindowText(str1);
if (str1 == "") {
MessageBox(_T("发送的人不能为空"), _T("提示"));
}
else {
int num = _ttoi(str1);
if (::send(socket_arr->at(num-1), CT2CA(str), str.GetLength(), 0) != SOCKET_ERROR) {
m_Listctrl.InsertItem(count++, _T("服务器对用户" + str1 + "说:" + str));
m_SendText.SetWindowText("");
m_num.SetWindowText("");
}
else {
MessageBox(_T("发送消息失败"), _T("提示"));
}
}
5 源码下载
源码下载
|