C++语言实现网络聊天程序的设计与实现(基于TCP/IP协议的SOCKET编程)超详细(代码+解析)
2022/1/8 17:03:54
本文主要是介绍C++语言实现网络聊天程序的设计与实现(基于TCP/IP协议的SOCKET编程)超详细(代码+解析),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
一、实验目的
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.
谢谢友友们的观看,如果对你有帮助,就留个赞吧。
这篇关于C++语言实现网络聊天程序的设计与实现(基于TCP/IP协议的SOCKET编程)超详细(代码+解析)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-27文件掩码什么意思?-icode9专业技术文章分享
- 2024-12-27如何使用循环来处理多个订单的退款请求,代码怎么写?-icode9专业技术文章分享
- 2024-12-27VSCode 在编辑时切换到另一个文件后再切回来如何保持在原来的位置?-icode9专业技术文章分享
- 2024-12-27Sealos Devbox 基础教程:使用 Cursor 从零开发一个 One API 替代品 审核中
- 2024-12-27TypeScript面试真题解析与实战指南
- 2024-12-27TypeScript大厂面试真题详解与解析
- 2024-12-26怎么使用nsenter命令进入容器?-icode9专业技术文章分享
- 2024-12-26导入文件提示存在乱码,请确定使用的是UTF-8编码怎么解决?-icode9专业技术文章分享
- 2024-12-26csv文件怎么设置编码?-icode9专业技术文章分享
- 2024-12-25TypeScript基础知识详解