通过多线程+Socket,实现群聊服务器。
服务端:
- 每当有一个连接时,服务端起一个线程去维护;
- .将收到的信息转发给所有的客户端;
- 当某个客户端断开连接时需要处理断开连接
客户端:
- 接收与发送信息
- 断开连接
- 自定义用户名
最终效果:(这里演示的是三个客户端之间的聊天效果,按照从左至右、从上至下的顺序发送消息)
目录
服务端
首先准备一个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<<8
err = WSAStartup(wVersion, &wsaData);
//检查1
if (err != 0) {
return err;
}
//检查2
if (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);
//每来一个连接,全局数组应该加一个成员,最大连接数+1
WaitForSingleObject(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.接收传递过来的客户端socket
SOCKET 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;
}
}
//最大连接数-1
clntCnt--;
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.接收传递过来的客户端socket
SOCKET 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;
}
}
//最大连接数-1
clntCnt--;
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<<8
err = WSAStartup(wVersion, &wsaData);
//检查1
if (err != 0) {
return err;
}
//检查2
if (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);
//每来一个连接,全局数组应该加一个成员,最大连接数+1
WaitForSingleObject(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");
}
客户端
同理,先准备一个客户端的模板
#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<<8
err = WSAStartup(wVersion, &wsaData);
//检查1
if (err != 0) {
return err;
}
//检查2
if (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 20
char 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<<8
err = WSAStartup(wVersion, &wsaData);
//检查1
if (err != 0) {
return err;
}
//检查2
if (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函数传入的参数就可以运行这个程序了。