通过多线程+Socket,实现群聊服务器。

服务端:

  • 每当有一个连接时,服务端起一个线程去维护;
  • .将收到的信息转发给所有的客户端;
  • 当某个客户端断开连接时需要处理断开连接

客户端:

  • 接收与发送信息
  • 断开连接
  • 自定义用户名

最终效果:(这里演示的是三个客户端之间的聊天效果,按照从左至右、从上至下的顺序发送消息)

目录

服务端

客户端

通过命令行运行程序


服务端

首先准备一个TCP的模板:

TCP套接字编程详解

#include <iostream>
#include<WinSock2.h>//第二版本的网络库
#pragma comment(lib,"ws2_32.lib")int main() {//初始化套接字库WORD wVersion;WSADATA wsaData;int err;wVersion = MAKEWORD(1, 1);  //MAKEWORD(a,b) b|a<<8err = WSAStartup(wVersion, &wsaData);//检查1if (err != 0) {return err;}//检查2if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {//清理套接字库WSACleanup();return -1;}//创建tcp套接字SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);//绑定到本机//绑定即要指明绑定的哪个IP地址,同时指明绑定的端口号//准备绑定信息SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//协议族,与上面保持一致addrSrv.sin_family = AF_INET;//端口;0~65535,其中1024以下的端口为系统保留的addrSrv.sin_port = htons(6000);//绑定if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) {printf("bind ERROR ERRORnum=%d\n", GetLastError());}//监听//linsten用来监听该端口上的连接数,当执if (listen(sockSrv, 10) == SOCKET_ERROR) {printf("linten ERROR ROOROnum=%d\n", GetLastError());}std::cout << "Server start at 6000" << std::endl;//接收请求前的准备工作SOCKADDR_IN addrCli;int len = sizeof(SOCKADDR);char recvBuf[100];char sendBuf[100];while (true) {//接收链接请求,返回针对客户端的套接字SOCKET sockConn = accept(sockSrv,(SOCKADDR*)&addrCli, &len);closesocket(sockConn);}//关闭套接字closesocket(sockSrv);//清理套接字库WSACleanup();system("pause");
}

服务器要对每个连接的客户端进行维护,所以我们要创建一个全局数组去保存这些连接客户端的Socket,同时也要声明一个整型的全局变量,用来记录已经连接的客户端数量。

//客户端socket数组
#define MAXSIZE 256
SOCKET clntSocks[MAXSIZE];int clntCnt = 0;//记录已经连接的客户端数量

因为我们会有多个线程去访问这些全局变量,所以应添加互斥对象,保证每个线程能单独访问这些临界资源。

HANDLE hMutex;
hMutex = CreateMutex(NULL, FALSE, NULL);

每当有一个客户端连接,将客户端Socket保存在数组中,服务端再起一个线程去维护这个Socket。Socket数组是全局变量,所以在访问数组前先去申请临界资源的访问权。

while (true) {//接收链接请求,返回针对客户端的套接字SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len);//每来一个连接,全局数组应该加一个成员,最大连接数+1WaitForSingleObject(hMutex,INFINITE);//放到Socket数组中clntSocks[clntCnt++] = sockConn;ReleaseMutex(hMutex);//客户端每来一个连接,服务端起一个线程hThread = (HANDLE)_beginthreadex(NULL, 0, HandleCln, (void*)&sockConn, 0, NULL);printf("Connect client IP :%s\n", inet_ntoa(addrCli.sin_addr));WaitForSingleObject(hMutex, INFINITE);printf("Connect client num :%d\n", clntCnt);ReleaseMutex(hMutex);
}

HandleCln是一个自定义的线程函数,负责去维护连接的客户端;接收客户端发送的信息并转发给其他客户端;以及客户端关闭连接时处理断开连接。

unsigned WINAPI HandleCln(void* arg) {//1.接收传递过来的客户端socketSOCKET hClntSock = *(SOCKET*)arg;int iLen = 0, i;char szMsg[MAXSIZE] = { 0 };//2进行数据的收发,循环接收//接收到客户端的数据//recv会一直等待数据接收,接收成功后返回数据的字节数,否则返回一些对应错误while (true) {iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0);if (iLen != -1) {//将接收到的数据发送给所有客户端SendMsg(szMsg, iLen);}else {break;}}//3.处理断开连接WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++) {//遍历socket数组if (hClntSock == clntSocks[i]) {//将该socket剔除while (i < clntCnt) {clntSocks[i] = clntSocks[i + 1];i++;}break;}}//最大连接数-1clntCnt--;printf("此时连接数目:%d", clntCnt);ReleaseMutex(hMutex);//关闭该套接字closesocket(hClntSock);return 0;
}

HandleCln函数将客户端发送的信息调用SendMsg函数转发给其他客户端,当客户端断开连接,此时就要更新Socket数组和记录连接数的变量,因为属于全局变量,所以要在访问前先去申请对临界资源的访问。

SendMsg函数

//发送给所有的客户端
void SendMsg(char* szMsg, int iLen) {int i = 0;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++) {send(clntSocks[i], szMsg, iLen, 0);}ReleaseMutex(hMutex);
}

当有多个线程去调用这个函数时,每个线程都有自己的栈空间,因此函数中的局部变量不受影响,但函数中如果使用了全局变量则需要加锁。

完整代码

#include <iostream>
#include<WinSock2.h>//第二版本的网络库
#include<Windows.h>
#include<process.h>
#pragma comment(lib,"ws2_32.lib")//服务端的设计
//1.每来一个连接,服务端起一个线程去维护
//2.将收到的消息转发给所有的客户端
//3,断开某个连接,需要处理断开的连接//客户端socket数组
#define MAXSIZE 256
SOCKET clntSocks[MAXSIZE];HANDLE hMutex;int clntCnt = 0;//记录已经连接的客户端数量
//处理客户端连接的函数//发送给所有的客户端
//多个线程去调用这个函数时
//每个线程都有自己的栈空间,因此函数中的局部变量会保存在不同的栈上,因此互不影响
//而函数中有全局变量时则必须给变量加锁
void SendMsg(char* szMsg, int iLen) {int i = 0;WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++) {send(clntSocks[i], szMsg, iLen, 0);}ReleaseMutex(hMutex);
}unsigned WINAPI HandleCln(void* arg) {//1.接收传递过来的客户端socketSOCKET hClntSock = *(SOCKET*)arg;int iLen = 0, i;char szMsg[MAXSIZE] = { 0 };//2进行数据的收发,循环接收//接收到客户端的数据//recv会一直等待数据接收,接收成功后返回数据的字节数,否则返回一些对应错误while (true) {iLen = recv(hClntSock, szMsg, sizeof(szMsg), 0);if (iLen != -1) {//将接收到的数据发送给所有客户端SendMsg(szMsg, iLen);}else {break;}}//3.处理断开连接WaitForSingleObject(hMutex, INFINITE);for (i = 0; i < clntCnt; i++) {//遍历socket数组if (hClntSock == clntSocks[i]) {//将该socket剔除while (i < clntCnt) {clntSocks[i] = clntSocks[i + 1];i++;}break;}}//最大连接数-1clntCnt--;printf("此时连接数目:%d", clntCnt);ReleaseMutex(hMutex);//关闭该套接字closesocket(hClntSock);return 0;
}int main() {//初始化套接字库WORD wVersion;WSADATA wsaData;int err;//HANDLE hThread;wVersion = MAKEWORD(1, 1);  //MAKEWORD(a,b) b|a<<8err = WSAStartup(wVersion, &wsaData);//检查1if (err != 0) {return err;}//检查2if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {//清理套接字库WSACleanup();return -1;}//创建一个互斥对象 hMutex = CreateMutex(NULL, FALSE, NULL);//创建tcp套接字SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);//绑定到本机//绑定即要指明绑定的哪个IP地址,同时指明绑定的端口号//准备绑定信息SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);//协议族,与上面保持一致addrSrv.sin_family = AF_INET;//端口;0~65535,其中1024以下的端口为系统保留的addrSrv.sin_port = htons(6000);//绑定if (bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) {printf("bind ERROR ERRORnum=%d\n", GetLastError());}//监听//linsten用来监听该端口上的连接数,当执if (listen(sockSrv, 10) == SOCKET_ERROR) {printf("linten ERROR ROOROnum=%d\n", GetLastError());}std::cout << "Server start at 6000" << std::endl;//接收请求前的准备工作SOCKADDR_IN addrCli;int len = sizeof(SOCKADDR);char recvBuf[100];char sendBuf[100];while (true) {//接收链接请求,返回针对客户端的套接字SOCKET sockConn = accept(sockSrv,(SOCKADDR*)&addrCli, &len);//每来一个连接,全局数组应该加一个成员,最大连接数+1WaitForSingleObject(hMutex, INFINITE);clntSocks[clntCnt++] = sockConn;ReleaseMutex(hMutex);//客户端每来一个连接,服务端起一个线程hThread = (HANDLE)_beginthreadex(NULL, 0,HandleCln, (void*)&sockConn, 0, NULL);printf("Connect client IP :%s\n", inet_ntoa(addrCli.sin_addr));WaitForSingleObject(hMutex, INFINITE);printf("Connect client num :%d\n", clntCnt);ReleaseMutex(hMutex);}//关闭套接字closesocket(sockSrv);//清理套接字库WSACleanup();system("pause");
}

客户端

同理,先准备一个客户端的模板

TCP套接字编程详解

#include <iostream>
#include<WinSock2.h>//第二版本的网络库
#pragma comment(lib,"ws2_32.lib")#include <iostream>int main() {//初始化套接字库WORD wVersion;WSADATA wsaData;int err;wVersion = MAKEWORD(1, 1);  //MAKEWORD(a,b) b|a<<8err = WSAStartup(wVersion, &wsaData);//检查1if (err != 0) {return err;}//检查2if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {//清理套接字库WSACleanup();return -1;}//创建tcp套接字,与服务器保持一致SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);//准备连接信息//指明要连接的IP地址和端口号SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//协议族,与上面保持一致addrSrv.sin_family = AF_INET;//端口;0~65535,其中1024以下的端口为系统保留的addrSrv.sin_port = htons(6000);//连接服务器connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//发送的数据char sendBuf[] = "world";//接收的数据char recvBuf[100];//发送数据到服务器send(sockCli, sendBuf, strlen(sendBuf) + 1, 0);//接收数据到服务器recv(sockCli, recvBuf, sizeof(recvBuf), 0);std::cout << recvBuf << std::endl;//关闭套接字closesocket(sockCli);WSACleanup();system("pause");return 0;
}

我们将main函数写成带参形式,在运行时传入一个字符串、来作为客户端的用户名。为此,我们需要准本一个字符数组来存储这个用户名。

#define NAME_SIZE 20
char szName[NAME_SIZE] = "[DEFAULT]";int main(int argc,char * argv){//保存用户名sprintf(szName,argv);return 0;
}

这样我们就可应通过命令行的形式运行这个程序时传入一个字符串来作为用户的名称。

当客户端与服务器连接成功后,我们就要起两个线程去处理接收和发送数据的任务。等到两个线程都执行完毕,我们就可应清空套接字库,结束掉main函数。

//连接服务器
if (connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) {printf("connect error error code:=%d", GetLastError());return -1;
}//发送消息 起一个线程
hSendThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg ,(void*)&sockCli, 0, NULL);hRecvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg,(void*)&sockCli, 0, NULL);//等待内核对象的变化
WaitForSingleObject(hSendThread, INFINITE);
WaitForSingleObject(hRecvThread, INFINITE);WSACleanup();return 0;

SendMsg是我们自定义的线程函数,负责去向服务器发送数据,他会一直从控制台中读取数据然后保存在一个数组缓冲区中。

我们规定如果用户输入了“q/n”或者“Q/n”就表明与服务器断开连接,因此在读取完后要进行判断。

发送数据的格式为:[用户名] 消息

unsigned WINAPI SendMsg(void* arg) {SOCKET hClntSock = *(SOCKET*)arg;//名字和消息的组合char szNameMsg[NAME_SIZE + BUF_SIZE];//循环接收来自控制台的消息while (true) {//接收控制台应用fgets(szMsg,BUF_SIZE,stdin);//当收到q或Q,退出if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")) {closesocket(hClntSock);exit(0);}//准备发送的数据sprintf(szNameMsg, "%s %s", szName, szMsg);//字符串拼接//发送给服务端send(hClntSock, szNameMsg, strlen(szNameMsg), 0);}return 0;
}

RecvMsg是我们自定义的线程函数,负责接收服务器发送的数据,并显示在控制台上。当客户端和服务器保存连接,当服务器并未发送数据时,线程会一直阻塞在recv那里,直到有数据发送过来。当与客户端断开连接时,recv会返回-1。

//接收数据
unsigned WINAPI RecvMsg(void* arg) {SOCKET hClntSock = *(SOCKET*)arg;char szNameMsg[NAME_SIZE + BUF_SIZE];int iLen = 0;while (true) {iLen = recv(hClntSock, szNameMsg, NAME_SIZE + BUF_SIZE - 1, 0);//服务器断开if (iLen == -1) {return -1;}//从第0待iLen-1的位置都是收到的数据szNameMsg[iLen] = 0;//输出到控制台,printf函数也可以fputs(szNameMsg, stdout);}return 0;
}

完整代码

//1.接收与发送数据,(起一个线程负责
// 2.断开连接
#include <iostream>
#include<WinSock2.h>//第二版本的网络库
#include<Windows.h>
#include<process.h>
#pragma comment(lib,"ws2_32.lib")//最大缓冲区大小
#define BUF_SIZE 256
#define NAME_SIZE 20char szName[NAME_SIZE] = "[DEFAULT]";
char szMsg[BUF_SIZE];//发送数据
unsigned WINAPI SendMsg(void* arg) {SOCKET hClntSock = *(SOCKET*)arg;//名字和消息的组合char szNameMsg[NAME_SIZE + BUF_SIZE];//循环接收来自控制台的消息while (true) {//接收控制台应用fgets(szMsg,BUF_SIZE,stdin);//当收到q或Q,退出if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n")) {closesocket(hClntSock);exit(0);}//准备发送的数据sprintf(szNameMsg, "%s %s", szName, szMsg);//字符串拼接//发送给服务端send(hClntSock, szNameMsg, strlen(szNameMsg), 0);}return 0;
}//接收数据
unsigned WINAPI RecvMsg(void* arg) {SOCKET hClntSock = *(SOCKET*)arg;char szNameMsg[NAME_SIZE + BUF_SIZE];int iLen = 0;while (true) {iLen = recv(hClntSock, szNameMsg, NAME_SIZE + BUF_SIZE - 1, 0);//服务器断开if (iLen == -1) {return -1;}//从第0待iLen-1的位置都是收到的数据szNameMsg[iLen] = 0;//输出到控制台,printf函数也可以fputs(szNameMsg, stdout);}return 0;
}//待参数的main函数,用命令行启动,在当前目录按下shift+鼠标右键 cmd
int main(int argc,char *argv[]) {//初始化套接字库WORD wVersion;WSADATA wsaData;int err;HANDLE hSendThread, hRecvThread;//负责收发信息wVersion = MAKEWORD(1, 1);  //MAKEWORD(a,b) b|a<<8err = WSAStartup(wVersion, &wsaData);//检查1if (err != 0) {return err;}//检查2if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) {//清理套接字库WSACleanup();return -1;}sprintf(szName, "[%s]", argv[1]);//创建tcp套接字,与服务器保持一致SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);//准备连接的信息//指明连接的IP地址和端口号SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//协议族,与上面保持一致addrSrv.sin_family = AF_INET;//端口;0~65535,其中1024以下的端 口为系统保留的addrSrv.sin_port = htons(6000);//连接服务器
if (connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)) == SOCKET_ERROR) {printf("connect error error code:=%d", GetLastError());return -1;
}//发送消息 起一个线程
hSendThread = (HANDLE)_beginthreadex(NULL, 0, SendMsg ,(void*)&sockCli, 0, NULL);hRecvThread = (HANDLE)_beginthreadex(NULL, 0, RecvMsg,(void*)&sockCli, 0, NULL);//等待内核对象的变化
WaitForSingleObject(hSendThread, INFINITE);
WaitForSingleObject(hRecvThread, INFINITE);WSACleanup();return 0;
}

通过命令行运行程序

我们去到.exe的文件目录下

在该目录下,按住shift+鼠标右键, 选择“在此处打开PowerShell窗口”

在该窗口下,我们输入“./程序名称” ,然后空格,输入向main函数传入的参数就可以运行这个程序了。

多线程+socket 实现群聊服务器相关推荐

  1. 实现多人聊天——简单群聊服务器的实现

    作为一个现代人,我们对当前众多的聊天通信平台并不陌生,facebook,qq,微信等都是大部分人每天都会接触的.那你有想过构建一个自己打造的聊天室,按照自己喜欢的模式,然后和朋友一起使用吗?下面就讲下 ...

  2. Python 基于 Socket 实现群聊

    互联网时代,怎么能不懂点网络编程?套接字(Sockets)是双向通信信道的端点,本文详细介绍了使用Socket实现类似微信群聊的功能,快进来一起学习下吧! 1.前言 套接字(Sockets)是双向通信 ...

  3. 群聊服务器的实现=服务端

    第一步 初始化套接字,具体就不写了 ,重复的代码 std::cout << "this is server" << std::endl; ... 第二步  ...

  4. Python基于Socket实现群聊

    作者:huny https://www.cnblogs.com/huny/p/14051152.html 1. 前言 套接字(Sockets)是双向通信信道的端点.套接字可以在一个进程内,在同一机器上 ...

  5. 多线程实现 qq 群聊的服务端和客户端

    效果比较简陋,但是功能可以实现 效果: 服务器 #include <iostream> #include <winsock2.h>//必须写在windows.h前面 #incl ...

  6. 基于CSocket类网络群聊服务器开发

    第一次接触MFC的Socket网络编程. 参考了:MFC网络编程--简单的服务器/客户端-CSDN论坛 这里用的编译器是:Visual Studio 2019 服务器开发: 新建一个MFC项目 项目名 ...

  7. socket通信 _ 一个简单的群聊系统

    群聊系统要用到通信socket协议,在java中要用到两个类java.net.ServerSocket和 Java.net.Socket.ServerSocket用于创建服务器,而Socket用于创建 ...

  8. 通信(服务器客户端的群聊与网络画板)

    进入了通信阶段,感觉和之前学的内容差异很大,对这方面也没有之前那么感兴趣--有点缺乏热情进 度上不来--同时还在改进之前的线程游戏,很久没更新了.做了可以连接多个客户端,群发消息的服务 器,和可以同步 ...

  9. 【Android】基于Socket的即时聊天(群聊)

    近来感觉秋招无望,学习Socket的时候,便做了个基于Socket的群聊工具: 先看看最终效果吧 项目GitHub通道(详细代码请自行copy) 如何利用Socket通信 socket又称为" ...

最新文章

  1. Docker 私有仓库 Harbor registry 安全认证搭建 [Https]
  2. [翻译]Global Descriptor Table-GDT
  3. 认真了解一下javascript
  4. CH Round #30 摆花[矩阵乘法]
  5. [再寄小读者之数学篇](2014-06-03 华罗庚等式)
  6. 绕过html标签,巧妙绕过WAF的XSS技巧
  7. 随心所欲的Web页面打印技术
  8. php多文件上传存储到表,PHP 实现一种多文件上传的方法
  9. 关于Jquery中animate可以操作css样式属性总结
  10. 异星工厂机器人科技树_异星工场(Factorio)玩法分析与讨论
  11. Android水波纹特效的简单实现
  12. Atcoder ABC162 D - RGB Triplets
  13. JetPack中的Room
  14. 非计算机专业学生怎么走上计算机技术之路?
  15. 时间序列预测方法_让我们使用经典方法预测您的时间序列
  16. 移动互联应用学习心得
  17. 【MySQL技术内幕】49-事务的实现之group commit
  18. freetype库的移植
  19. 完整优雅的卸载腾讯云云服务器安全监控组件
  20. php彩蛋是什么,php彩蛋

热门文章

  1. Python变量与字符串
  2. 中国羊奶皂市场趋势报告、技术动态创新及市场预测
  3. Android9.0对非 SDK 接口的限制
  4. 生成函数多项式操作合集
  5. Android Studio 实现桌面小组件(APPWidget)
  6. python实验六到十二作业(待完善)
  7. Oracle11g for Windows
  8. sort函数用法详解
  9. Unity 游戏实例开发集合 之 CompoundBigWatermelon (简单合成一个大西瓜) 休闲小游戏快速实现
  10. 面试官问你什么是单点登录,把这篇甩给他!