文章目录

  • 一、程序演示
  • 二、项目介绍
  • 三、代码详解
    • 服务器
    • 客户端

一、程序演示


虽然最开始是打算写个局域网就好了的,但其实如果你有云服务器,可以向微信、QQ一样与相隔甚远的朋友聊天,只需要将客户端IP修改为云服务器的IP,并将服务器程序运行到云服务器上,端口可自行确定。

因为我原本就租了一个云服务器,所以项目里也有我已经改好了的Linux服务器代码,在Ubuntu上可正常运行。

注意: 本文只详解介绍各个功能模块代码, 如果你想要一步一步从头写出该软件, 可以看我的这篇文章:MFG开发多人聊天室

进阶项目:C++ 实现聊天室(单聊、群聊、文件传输)

该项目使用WTL界面库以及boost asio网络库进行开发,是本文的升级版本,服务器代码完全跨平台,客户端最终生成的可执行文件只有160kb

注意本项目可能存在的问题:

由于linux系统默认采用的utf-8编码,而windows系统采用的一般为GB2312GBK编码,为了客户端能够简单方便的处理两种平台服务器的信息,我便将我的linux系统编码调整到了GBK,所以想要直接使用我编译好的这个linux服务器,需要你调整你的linux系统编码为GBKGB2312,否则应该是启动不了的。又或者你可以重新将该linux服务器源码在你的linux上编译一次,应该就好了,这个问题比较复杂,我并没有打算去修补。

二、项目介绍

项目下载点这里

或者到本文章的最后 , 扫码进入微信公众号, 回复LANChat , 即可免费下载:

文件解压后:

文件介绍:

  • LANClient:客户端源代码
  • LANSever:Windows服务端源代码
  • Sever:Linux服务端源代码
  • LANChat.sln :项目文件,用vs打开即可
  • LANClient.exe:客户端程序
  • LANSever.exe:windows服务端程序
  • Sever.out:Linux服务端程序

因我使用的当前最新版本VS2022,如果你为低版本,编译可能会出现部分问题,如vs2019需进行以下设置:

三、代码详解

因考虑到初次学习网络编程的同学,所以源代码并没有进行任何封装,只是按着逻辑一步一步写的。

最简单的一个网络程序,点这里查看

服务器

#define  _WINSOCK_DEPRECATED_NO_WARNINGS
#include<iostream>
#include<WinSock2.h>
#include<map>
#include<thread>
#include<WS2tcpip.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;map<SOCKET*, string> m_clients; //存储socket和名称的映射关系unsigned __stdcall RecvMSG(void* param) {SOCKET* cli = (SOCKET*)param;//通知所有客户端for (auto i : m_clients) {if (i.first == cli) continue;string tm = "1:";tm += (m_clients[cli] + ":加入聊天室");send(*i.first, tm.data(), tm.size(), 0);}//向新客户端发送已有用户string tn = "4:";for (auto i : m_clients) {if (i.first == cli) continue;tn +=(i.second+":");}send(*cli, tn.data(), tn.size(), 0);for (auto i : m_clients) {if (i.first == cli) continue;int len = send(*cli, i.second.data(), i.second.size(), 0);if (len != i.second.size()) {cout << i.second << ":发送出错" << endl;}}char msg[0xFF];while (1) {int len = recv(*cli, msg, sizeof(msg), 0);//正常接收,转发消息if (len > 0) {for (auto i : m_clients) {if (i.first == cli) continue;string tm = "3:"+m_clients[cli] + ':';tm += msg;send(*i.first, tm.data(),tm.size(), 0);}continue;}//客户端断连,通知for (auto i : m_clients) {if (i.first == cli) continue;string exitMsg = "2:";exitMsg+= (m_clients[cli] + ":退出聊天室");send(*i.first, exitMsg.data(), exitMsg.size(), 0);}cout << m_clients[cli]<< ":退出聊天室" << endl;m_clients.erase(cli);closesocket(*cli);delete[] cli;break;}return 0;
}int main() {WSADATA wsadata;int sta = WSAStartup(MAKEWORD(2, 2), &wsadata);if (sta != 0) {cout << "创建协议栈失败!";return 0;}SOCKET sockSev = socket(AF_INET, SOCK_STREAM, 0);SOCKADDR_IN addrSev;addrSev.sin_family = AF_INET;addrSev.sin_port = htons(9999);addrSev.sin_addr.S_un.S_addr = INADDR_ANY;bind(sockSev, (sockaddr*)&addrSev, sizeof(addrSev));listen(sockSev, 5);cout << "服务器启动成功!" << endl;while (true) {SOCKADDR_IN addrCli;int len = sizeof(addrCli);SOCKET* sockCli = new SOCKET;*sockCli = accept(sockSev, (sockaddr*)&addrCli, &len);if (*sockCli == INVALID_SOCKET) {cout << inet_ntoa(addrCli.sin_addr) << ":连接失败!" << endl;continue;}char msg[20];len = recv(*sockCli, msg, 20, 0);if (len <= 0) {closesocket(*sockCli);delete sockCli;cout << inet_ntoa(addrCli.sin_addr) << ":接收数据失败!" << endl;}m_clients.insert(pair<SOCKET*, string>(sockCli, msg));cout << msg << ":进入聊天室!" << endl;//为新客户端开启线程接收信息_beginthreadex(0, 0, RecvMSG, sockCli, 0, 0);}closesocket(sockSev);
}

拿到源码,还是直接看main函数

对于windows服务器来说,网络编程需要固定的以下几步骤:

  1. 网络环境初始化:WSAStartup
  2. 创建服务器套接字:socket
  3. 绑定本机IP和端口:bind
  4. 监听客户端:listen
  5. 等待客户端连接:accept
  6. 发送消息:send
  7. 接收消息:recv

基本函数使用方法已经在另一篇文章中有过说明

这里只对核心代码和主要逻辑进行必要性说明:

  • 初始化网络环境,创建服务器socket,绑定端口与IP地址,进入while死循环
  • 在while循环中,等待客户端连接
  • 当有客户端连接成功时,等待客户端发送昵称,插入全局map类型变量中,并单独为此客户端开启一个线程,然后进行下一次循环
  • 线程中首先向当前所有在线用户通知新成员上线,并向新用户发送当前已在线的成员
  • 接着进入while循环等待客户端发送来的消息,并根据消息内容进行不同的处理

主要特点是通过接受到的每个字符串第一个数字决定要执行的命令,比如1:代表有新人加入,2代表有人退出,等等

这里用到了一个map数据结构,用途就是将昵称和SOCKET进行绑定,便于发送消息等

遍历map结构我使用到了for(auto i : map),这是一种较新的遍历方法,auto代表自动推断类型,因为该数据类型实在太长了

遍历到的i,主要有两个成员,first和second,如其名,first代表第一个变量,second代表第二个变量

客户端

客户端采用了MFC框架进行简单开发

界面:


主要成员变量:

注意,这里的控件变量是通过MFC提供的工具自动绑定的,右键要绑定变量的控件,添加变量

然后按需求选择与填写即可

后面就可以通过该变量名直接操控控件或控件内容

连接按钮代码:

if (isCon) { //判断当前是否连接,入如果已经连接,则断开连接closesocket(m_client);m_client = -1;isCon = false;AfxMessageBox(L"成功断开连接!");SetDlgItemText(IDC_BTN_CNT, _T("连接"));m_Member.DeleteAllItems();return;
}if (m_client == -1) {m_client = socket(AF_INET, SOCK_STREAM, 0);
}
UpdateData(); //更新控件中的数据到变量中
if (m_name.IsEmpty()) {AfxMessageBox(L"请输入昵称!");return;
}
SOCKADDR_IN addrSev;
addrSev.sin_family = AF_INET;
addrSev.sin_port = htons(GetDlgItemInt(IDC_ET_PORT));
DWORD ip;
m_ip.GetAddress(ip);
addrSev.sin_addr.S_un.S_addr = htonl(ip);int res = connect(m_client, (sockaddr*)&addrSev, sizeof(addrSev)); //连接
if (res == -1) {AfxMessageBox(L"连接服务器失败!");return;
}
m_Member.InsertItem(0, m_name); //加入当前在线成员列表
SetDlgItemText(IDC_BTN_CNT, _T("连接成功!"));
_beginthreadex(0, 0, RecvMsg, &m_client, 0, 0); //开启一个线程接收来自服务器的消息
Sleep(500);
SetDlgItemText(IDC_BTN_CNT, _T("断开连接!"));
isCon = true;
std::string na = WtoA(m_name);  //将宽字符转化为窄字符
send(m_client, na.data(), na.size(), 0); //发送昵称


发送消息按钮:

if (m_client == -1) { //还未连接服务器SetDlgItemText(IDC_BTN_SEND, _T("网络错误!"));Sleep(500);SetDlgItemText(IDC_BTN_SEND, _T("发送"));return;
}
CString msg;
GetDlgItemText(IDC_ET_MSG, msg);
if (msg.IsEmpty()) {SetDlgItemText(IDC_BTN_SEND, _T("消息为空!"));Sleep(500);SetDlgItemText(IDC_BTN_SEND, _T("发送"));return;
}
UpdateData(); //将控件数据更新到变量中
std::string str = WtoA(msg);
int len = send(m_client, str.data(), str.size(), 0); //发送消息
if (len == str.size()) {msg = _T("@你:") + msg + _T("\r\n");m_et_Msg.Append(msg);SetDlgItemText(IDC_ET_MSG, _T(""));UpdateData(false);
}m_showMSg.LineScroll(m_showMSg.GetLineCount() - 10); //滚动历史消息,保证显示最新消息


接收消息的线程:

unsigned __stdcall CLANClientDlg::RecvMsg(void* param)
{SOCKET* cli = (SOCKET*)param;while (1) {char* buf = new char[0xFF]{};int len = recv(*cli, buf, 0xFF, 0);if (len <= 0) {::PostMessageW(hwnd, UM_MODIUSER, 0, (LPARAM)buf); //接收消息错误,发出退出消息break;}::PostMessageW(hwnd, UM_MODIUSER, 1, (LPARAM)buf); //成功接收消息}return 0;
}


这里为自定义消息UM_MODIUSER,将该消息发送到主线程的处理函数中进行处理

自定义消息处理函数:

 if (!wParam) {  //接受消息发送错误char* msg = (char*)lParam;delete[] msg;UpdateData();m_et_Msg.Append(_T("你已经断线!\r\n"));UpdateData(false);m_Member.DeleteAllItems();return -1;}char* msg = (char*)lParam;if (msg[0] == '1' && msg[1] == ':') { //1:有新成员加入USES_CONVERSION;CString s = A2W(&msg[2]);UpdateData();m_et_Msg.Append(s + L"\r\n");UpdateData(false);int index = s.Find(L':');s.GetBuffer()[index] = L'\0';m_Member.InsertItem(0,s);}else if (msg[0] == '2' && msg[1] == ':') { //2:有成员退出USES_CONVERSION;CString s = A2W(&msg[2]);UpdateData();m_et_Msg.Append(s + L"\r\n");UpdateData(false);int index = s.Find(L':');s.GetBuffer()[index] = L'\0';for (int i = 0; i < m_Member.GetItemCount(); i++) {if (m_Member.GetItemText(i, 0)==s) {m_Member.DeleteItem(i);break;}}}else if (msg[0] == '3' && msg[1] == ':') { //3:正常接收消息USES_CONVERSION;CString s = A2W(&msg[2]);UpdateData();m_et_Msg.Append(s + L"\r\n");UpdateData(false);}else if (msg[0] == '4' && msg[1] == ':') { //4:更新已有的成员USES_CONVERSION;CString s = A2W(&msg[2]);int index = 0;for (int i = 0; i < s.GetLength(); i++) {if (s[i] == L':') {s.GetBuffer()[i] = L'\0';m_Member.InsertItem(0, &s.GetBuffer()[index]);index = i + 1;}}}m_showMSg.LineScroll(m_showMSg.GetLineCount() - 10); //滚动消息列表到最新delete[] msg; //删除分配的内存,避免内存泄露return 0;


设置按钮:

if (isSet)
{RECT rect;GetWindowRect(&rect);rect.right += 360;MoveWindow(&rect);
}
else
{RECT rect;GetWindowRect(&rect);rect.right -= 360;MoveWindow(&rect);
}
isSet = !isSet;


该按钮作用就是隐藏或展示右边的内容

C/C++实现聊天室(详解版)相关推荐

  1. pomelo分布式聊天服务器详解

    pomelo分布式聊天服务器详解 2014-01-05 11:43:49|  分类: node |  标签:pomelo  pomelo聊天  nodejs分布式聊天  pomelo分布式  |举报| ...

  2. 电脑连接电视方法详解_电脑如何连网?——校园宽带的连接方法(详解版)

    玉屏洲电脑联网 详解版  联网前必备!--注册好的运维云账号 如果不知道啥是运维云,可以在公众号里发消息 运维云 获取运维云账号注册方法! 注册好的运维云样板 1 第一步·宽带连接 用网线一端连接墙上 ...

  3. Python Tkinter——数字拼图游戏详解版

    Python Tkinter 实践系列--数字拼图游戏详解版 import random #Python中的random是一个标准库用于生成随机数.随机整数.还有随机从数据集取数据. import t ...

  4. AT指令(中文详解版)(二)

    AT指令(中文详解版)(二) 常 用 AT 命 令 手 册   1.常用操作 1.1 AT 命令解释:检测 Module 与串口是否连通,能否接收 AT 命令: 命令格式:AT<CR> 命 ...

  5. 案例1:金融数据分析----code知识点详解版

    案例1:金融数据分析----code详解版 1.引言 1.1案例分析目标 1.2涉及知识点 1.3案例分析流程 2.数据获取 `涉及知识点:` 2.1安装*tushare*库 2.2获取Token 2 ...

  6. 不定积分常用公式(详解版)

    不定积分常用公式(详解版)(持续更新中~) 太长不看可移步这篇文章 不定积分常用公式(简洁版) 正文 不定积分常用公式(详解版)(持续更新中~) 第一部分 第二部分 第三部分 第四部分 其他 第一部分 ...

  7. php 微信 群聊,vbot微信机器人微信聊天消息详解(18):群组变动

    <vbot微信机器人微信聊天消息详解(18):群组变动>要点: 本文介绍了vbot微信机器人微信聊天消息详解(18):群组变动,希望对您有用.如果有疑问,可以联系我们. 当微信群新增了成员 ...

  8. Java Swing布局管理器(详解版)

    在使用 Swing 向容器添加组件时,需要考虑组件的位置和大小.如果不使用布局管理器,则需要先在纸上画好各个组件的位置并计算组件间的距离,再向容器中添加.这样虽然能够灵活控制组件的位置,实现却非常麻烦 ...

  9. 互为质数的勾股数c语言,C语言求勾股数(详解版)

    搜索热词 问题描述 求100以内的所有勾股数. 所谓勾股数,是指能够构成直角三角形三条边的三个正整数(a,b,c). 问题分析 根据"勾股数"定义,所求三角形三边应满足条件 a2 ...

  10. Python Pandas绘图教程(详解版)

    Python Pandas绘图教程(详解版) Pandas 在数据分析.数据可视化方面有着较为广泛的应用,Pandas 对 Matplotlib 绘图软件包的基础上单独封装了一个plot()接口,通过 ...

最新文章

  1. 1098 Insertion or Heap Sort 需再做
  2. 复习07统计学习方法(支持向量机SVM)---图片版
  3. 推荐几个堪称神器的学习网站
  4. fopen参数mode详解
  5. 剑指offer和LeetCode题目笔记
  6. 机器学习第三篇:详解朴素贝叶斯算法
  7. HQL中左连接,右连接、内连接
  8. 如何解决wampmysqld服务无法启动,错误id=1067
  9. wireshark使用方法总结
  10. Python 各种画图
  11. 安卓开发 给控件左边右边下边添加阴影_Android 控件布局实现卡片效果,阴影效果...
  12. alanwang[GDOU] 写两个函数,分别求两个整数的最大公约数和最小公倍数,用主函数调用这两个函数,并输出结果。两个整数由键盘输入
  13. 计算机考研考电路学校,集成电路工程考研学校排名
  14. UVM基础-Sequence、Sequencer(二)
  15. python中怎么撤回_python如何查看微信消息撤回
  16. 含有使字的诗句_带有使字的诗-带有使字的诗句
  17. 关于ubuntu浏览器模糊不清的解决方法
  18. Layui提示说明弹框
  19. mac看图软件哪个好用_办公记事软件哪个好?工作记事本便签app哪个好用
  20. c语言正方形和三角形面积,【c语言】计算长方形,三角形和圆形的面积,根据用户的选择求不同形状的面积。...

热门文章

  1. 【CSS】使HTML页面表格中文字水平且垂直居中的方法(易错)
  2. 计算机技术应用基础2010,计算机应用基础(Windows7+Office2010双色版中等职业教育课程改革国家规划新教材)...
  3. css绘制八方向云台 环形按钮盘
  4. 实战QT数据采集与显示
  5. VMware Workstation 快照与克隆的使用
  6. 树莓派linux系统配置AODV协议,linux上模拟AODV路由协议 下面一些信息求各路大神解释!!...
  7. Linux个性化桌面,颜值即正义,超好用的 Linux 桌面个性化工具推荐
  8. 软件测试工作内容太简单怎么办?
  9. 昂达平板不能开机刷机_昂达平板电脑打不开机怎么办
  10. GIt下Yii2.0运用gii命令模式下生成model和crud