一、实验目的
1、熟悉VisualC++的基本操作;掌握最基本的Client/Server(客户机/服务器)模
式的网络编程技术,并在此基础上实现更为复杂的网络编程。
2、基本了解基于对话框的windows应用程序的编写过程。
3、对于Windows Socket编程建立初步概念。
二、编程工具
Visual Studio 2022
三、实验方法
程序分为服务端和客户端两个
用户需要创建两个C++项目,分别命名为
①聊天程序服务端
②聊天程序客户端
如图所示
同时为了避免编译软件出现c4996错误,导致编译不通过,应该鼠标右击项目列表的.CPP文件,将SDL检查,调成否。
并且点击目录最上方的“解决方案“XXXX””设置属性,将两个项目设置为同时启动
四、实验代码(附加超详细注释):
服务端:
#include"pch.h"//预编译头
#include<iostream>
#include<Winsock2.h>//socket头文件
#include<cstring>
using namespace std;
//载入系统提供的socket动态链接库
#pragma comment(lib,"ws2_32.lib") //socket库
//==============================全局变量区===================================
const int BUFFER_SIZE = 1024;//缓冲区大小
int RECV_TIMEOUT = 10;//接收消息超时
int SEND_TIMEOUT = 10;//发送消息超时
const int WAIT_TIME = 10;//每个客户端等待事件的时间,单位毫秒
const int MAX_LINK_NUM = 10;//服务端最大链接数
SOCKET cliSock[MAX_LINK_NUM];//客户端套接字 0号为服务端
SOCKADDR_IN cliAddr[MAX_LINK_NUM];//客户端地址
WSAEVENT cliEvent[MAX_LINK_NUM];//客户端事件 0号为服务端,它用于让程序的一部分等待来自另一部分的信号。例如,当数据从套接字变为可用时,winsock 库会将事件设置为信号状态
int total = 0;//当前已经链接的客服端服务数
//==============================函数声明===================================
DWORD WINAPI servEventThread(LPVOID IpParameter);//服务器端处理线程
int main()
{
//1、初始化socket库
WSADATA wsaData;//获取版本信息,说明要使用的版本
WSAStartup(MAKEWORD(2, 2), &wsaData);//MAKEWORD(主版本号, 副版本号)
//2、创建socket
SOCKET servSock = socket(AF_INET, SOCK_STREAM, 0);//面向网路的流式套接字
//3、将服务器地址打包在一个结构体里面
SOCKADDR_IN servAddr; //sockaddr_in 是internet环境下套接字的地址形式
servAddr.sin_family = AF_INET;//和服务器的socket一样,sin_family表示协议簇,一般用AF_INET表示TCP/IP协议。
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//服务端地址设置为本地回环地址
servAddr.sin_port = htons(12345);//host to net short 端口号设置为12345
//4、绑定服务端的socket和打包好的地址
bind(servSock, (SOCKADDR*)&servAddr, sizeof(servAddr));
//4.5给服务端sokect绑定一个事件对象,用来接收客户端链接的事件
WSAEVENT servEvent = WSACreateEvent();//创建一个人工重设为传信的事件对象
WSAEventSelect(servSock, servEvent, FD_ALL_EVENTS);//绑定事件对象,并且监听所有事件
cliSock[0] = servSock;
cliEvent[0] = servEvent;
//5、开启监听
listen(servSock, 10);//监听队列长度为10
//6、创建接受链接的线程
//不需要句柄所以直接关闭
CloseHandle(CreateThread(NULL, 0, servEventThread, (LPVOID)&servSock, 0, 0));
cout << "聊天室服务器已开启" << endl;
connect test
//int addrLen = sizeof(SOCKADDR);//用于接收客户端的地址包结构体长度
//SOCKET cliSOCK = accept(servSock, (SOCKADDR*)&servAddr,&addrLen);
//if (cliSOCK != INVALID_SOCKET)
//{
// cout << "链接成功" << endl;
//}
//while (1)
//{
// char buf[100] = { 0 };//测试缓冲区
// int nrecv = recv(cliSOCK, buf, sizeof(buf), 0);
// if (nrecv > 0)//如果接收到客户端的信息就输出到屏幕
// {
// cout << buf << endl;
// }
//}
//需要让主线程一直运行下去
//发送消息给全部客户端
while (1)
{
char contentBuf[BUFFER_SIZE] = { 0 };
char sendBuf[BUFFER_SIZE] = { 0 };
cin.getline(contentBuf, sizeof(contentBuf));
sprintf(sendBuf, "[智能小易]%s", contentBuf);
for (int j =1 ; j <= total; j++)
{
send(cliSock[j], sendBuf, sizeof(sendBuf), 0);
}
}
//1-关闭socket库的收尾工作
WSACleanup();
return 0;
}
DWORD WINAPI servEventThread(LPVOID IpParameter) //服务器端线程
{
//该线程负责处理服务端和各个客户端发生的事件
//将传入的参数初始化
SOCKET servSock = *(SOCKET*)IpParameter;//LPVOID为空指针类型,需要先转成SOCKET类型再引用,即可使用传入的SOCKET
while (1) //不停执行
{
for (int i = 0; i < total+1; i++)//i代表现在正在监听事件的终端
{
//若有一个客户端链接,total==1,循环两次,包含客户端和服务端
//对每一个终端(客户端和服务端),查看是否发生事件,等待WAIT_TIME毫秒
int index = WSAWaitForMultipleEvents(1, &cliEvent[i], false, WAIT_TIME, 0);
index -= WSA_WAIT_EVENT_0;//此时index为发生事件的终端下标
if (index==WSA_WAIT_TIMEOUT||index==WSA_WAIT_FAILED)
{
continue;//如果出错或者超时,即跳过此终端
}
else if (index==0)
{
WSANETWORKEVENTS networkEvents;
WSAEnumNetworkEvents(cliSock[i], cliEvent[i], &networkEvents);//查看是什么事件
//事件选择
if (networkEvents.lNetworkEvents & FD_ACCEPT)//若产生accept事件(此处与位掩码相与)
{
if (networkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
{
cout <<"连接时产生错误,错误代码" << networkEvents.iErrorCode[FD_ACCEPT_BIT] << endl;
continue;
}
//接受链接
if (total + 1 < MAX_LINK_NUM)//若增加一个客户端仍然小于最大连接数,则接受该链接
{
//total为已连接客户端数量
int nextIndex = total + 1;//分配给新客户端的下标
int addrLen = sizeof(SOCKADDR);
SOCKET newSock = accept(servSock, (SOCKADDR*)&cliAddr[nextIndex], &addrLen);
if (newSock != INVALID_SOCKET)
{
//设置发送和接收时限
/*setsockopt(newSock, SOL_SOCKET, SO_SNDTIMEO, (const char*) & SEND_TIMEOUT, sizeof(SEND_TIMEOUT));
setsockopt(newSock, SOL_SOCKET, SO_SNDTIMEO, (const char*) &RECV_TIMEOUT, sizeof(RECV_TIMEOUT));*/
//给新客户端分配socket
cliSock[nextIndex] = newSock;
//新客户端的地址已经存在cliAddr[nextIndex]中了
//为新客户端绑定事件对象,同时设置监听,close,read,write
WSAEVENT newEvent = WSACreateEvent();
WSAEventSelect(cliSock[nextIndex], newEvent, FD_CLOSE | FD_READ | FD_WRITE);
cliEvent[nextIndex] = newEvent;
total++;//客户端连接数增加
cout <<"#" << nextIndex<< "游客(IP:" << inet_ntoa(cliAddr[nextIndex].sin_addr) << ")进入了聊天室,当前连接数:" << total << endl;
//给所有客户端发送欢迎消息
char buf[BUFFER_SIZE] = "[智能小易]欢迎游客(IP:";
strcat(buf, inet_ntoa(cliAddr[nextIndex].sin_addr));
strcat(buf, ")进入聊天室");
for (int j = i; j <=total; j++)
{
send(cliSock[j], buf, sizeof(buf),0);
}
}
}
}
else if (networkEvents.lNetworkEvents & FD_CLOSE)//客户端被关闭,即断开连接
{
//i表示已关闭的客户端下标
total--;
cout << "#" << i << "游客(IP:" << inet_ntoa(cliAddr[i].sin_addr) << ")退出了聊天室,当前连接数:"<<total << endl;
//释放这个客户端的资源
closesocket(cliSock[i]);
WSACloseEvent(cliEvent[i]);
//数组调整,用顺序表删除元素
for (int j = i; j < total; j++)
{
cliSock[j] = cliSock[j + 1];
cliEvent[j] = cliEvent[j + 1];
cliAddr[j] = cliAddr[j + 1];
}
//给所有客户端发送退出聊天室的消息
char buf[BUFFER_SIZE] = "[智能小易](IP:";
strcat(buf, inet_ntoa(cliAddr[i].sin_addr));
strcat(buf, ")退出聊天室");
for (int j = 1; j <=total; j++)
{
send(cliSock[j], buf, sizeof(buf), 0);
}
}
else if (networkEvents.lNetworkEvents & FD_READ)//接收到消息
{
char buffer[BUFFER_SIZE] = { 0 };//字符缓冲区,用于接收和发送消息
char buffer2[BUFFER_SIZE] = { 0 };
for (int j = 1; j <= total; j++)
{
int nrecv = recv(cliSock[j], buffer, sizeof(buffer), 0);//nrecv是接收到的字节数
if (nrecv > 0)//如果接收到的字符数大于0
{
sprintf(buffer2,"[#%d]%s",j,buffer);
//在服务端显示
cout << buffer2 << endl;
//在其他客户端显示(广播给其他客户端)
for (int k = 1; k <= total; k++)
{
send(cliSock[k], buffer2, sizeof(buffer),0);
}
}
}
}
}
}
}
return 0;
}
客户端:
// 聊天程序客户端
#include"pch.h"//预编译头
#include<iostream>
#include<Winsock2.h>//socket头文件
#include<cstring>
using namespace std;
//载入系统提供的socket动态链接库
#pragma comment(lib,"ws2_32.lib") //socket库
const int BUFFER_SIZE = 1024;//缓冲区大小
DWORD WINAPI recvMsgThread(LPVOID IpParameter);
int main() {
//1、初始化socket库
WSADATA wsaData;//获取版本信息,说明要使用的版本
WSAStartup(MAKEWORD(2, 2), &wsaData);//MAKEWORD(主版本号, 副版本号)
//2、创建socket
SOCKET cliSock = socket(AF_INET, SOCK_STREAM, 0);//面向网路的流式套接字,第三个参数代表自动选择协议
//3、打包地址
//客户端
SOCKADDR_IN cliAddr = { 0 };
cliAddr.sin_family = AF_INET;
cliAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//IP地址
cliAddr.sin_port = htons(12345);//端口号
//服务端
SOCKADDR_IN servAddr = { 0 };
servAddr.sin_family = AF_INET;//和服务器的socket一样,sin_family表示协议簇,一般用AF_INET表示TCP/IP协议。
servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//服务端地址设置为本地回环地址
servAddr.sin_port = htons(12345);//host to net short 端口号设置为12345
if (connect(cliSock, (SOCKADDR*)&servAddr, sizeof(SOCKADDR)) == SOCKET_ERROR)
{
cout << "链接出现错误,错误代码" << WSAGetLastError() << endl;
}
//创建接受消息线程
CloseHandle(CreateThread(NULL, 0, recvMsgThread, (LPVOID)&cliSock, 0, 0));
//主线程用于输入要发送的消息
while (1)
{
char buf[BUFFER_SIZE] = { 0 };
cin.getline(buf,sizeof(buf));
if (strcmp(buf, "quit") == 0)//若输入“quit”,则退出聊天室
{
break;
}
send(cliSock, buf, sizeof(buf), 0);
}
closesocket(cliSock);
WSACleanup();
return 0;
}
DWORD WINAPI recvMsgThread(LPVOID IpParameter)//接收消息的线程
{
SOCKET cliSock = *(SOCKET*)IpParameter;//获取客户端的SOCKET参数
while (1)
{
char buffer[BUFFER_SIZE] = { 0 };//字符缓冲区,用于接收和发送消息
int nrecv = recv(cliSock, buffer, sizeof(buffer), 0);//nrecv是接收到的字节数
if (nrecv > 0)//如果接收到的字符数大于0
{
cout << buffer << endl;
}
else if (nrecv<0)//如果接收到的字符数小于0就说明断开连接
{
cout << "与服务器断开连接" << endl;
break;
}
}
return 0;
}
五、实验效果:
这里需要注意,如果想要多开客户端,需要到你的代码文件目录列表中去找到exe文件启动,开客户端就可以了
That's all.
谢谢友友们的观看,如果对你有帮助,就留个赞吧。
版权归原作者 爱编程的小云 所有, 如有侵权,请联系我们删除。