目录
一、前言
二、设计需求
1.服务器需求
2.客户端需求
三、服务端设计
1.项目准备
2.初始化网络库
3.SOCKET创建服务器套接字
4. bind 绑定套接字
5. listen监听套接字
6. accept接受客户端连接
7.建立套接字数组
8. 建立多线程与客户端通信
9. 处理线程函数,收消息
10. 发消息给客户端
11.处理断开的客户端
四、客户端设计
1.项目准备
2. 处理main函数参数
3.初始化网络库
4.SOCKET创建客户端套接字
5. 配置IP地址和端口号,连接服务器
6.创建两线程,发送和接收
7.处理发送消息线程函数
五、项目运行
1.编译生成可执行文件
2.运行可执行程序
3.进行通讯
六、总代码展示
1.服务端代码:
2.客户端代码:
七、最后
一、前言
今天我们不学习其他的知识点,主要是复习之前学习过的TCP网络通信和多线程以及线程同步互斥,然后结合这以上知识点设计实现一个小的项目,主要仿照qq群聊的服务器可客户端的实现,下面我将会说明一下设计需求,以下是整个设计示意图。
二、设计需求
1.服务器需求
需求一:对于每一个上线连接的客户端,服务端会起一个线程去维护。
需求二:将服务器受到的消息转发给全部的客户端。例如:服务器接收客户端A的消息后,将立即发送给客户端A,B,C...
需求三:当某个客户端断开(下线),需要处理断开的链接。
2.客户端需求
需求一:请求连接上线,
需求二:发消息给服务器。
需求三:客户端等待服务端的消息。
需求四:等待用户自己的关闭(下线)。
三、服务端设计
1.项目准备
在创建项目后,引入一些必需的头文件以及创建项目需要的宏,例如:允许客户端连接的最大数量,接收文件字节的大小,客户端连接的个数等等。
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
#define MAX_CLEN 256 // 最大连接数量
#define MAX_BUF_SIZE 1024 // 接收文件大小
SOCKET clnSockets[MAX_CLEN]; // 所有的连接客户端的socket
int clnCnt = 0; // 客户端连接的个数
// 互斥的句柄
HANDLE hMutex;
2.初始化网络库
WSAStartup初始化Winsock,这个函数用于初始化网络环境,都是固定写法,必须要有的,直接复制粘贴即可。
// 1. 初始化库
WSADATA wsaData;
int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (stu != 0) {
std::cout << "WSAStartup 错误:" << stu << std::endl;
return 0;
}
3.SOCKET创建服务器套接字
这和我们之前学的windwos网络一样都是固定写法,重点时查看函数原型以及它的参数,代码如下:
// 2. socket 创建套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
if (sockSrv == INVALID_SOCKET)
{
std::cout << "socket failed!" << GetLastError() << std::endl;
WSACleanup(); //释放Winsock库资源
return 1;
}
4. bind 绑定套接字
这个流程主要是绑定服务器的IP地址,端口号,以及协议版本。
// 3 bind 绑定套接字
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 地址 IP地址any
addrSrv.sin_family = AF_INET; // ipv4协议
addrSrv.sin_port = htons(6000); // 端口号
if ( SOCKET_ERROR == bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR)))
{
std::cout << "bind failed!" << GetLastError() << std::endl;
WSACleanup(); //释放Winsock库资源
return 1;
}
5. listen监听套接字
listen函数最重要的是理解它的第二个参数,为等待连接的最大队列长度 ,这个解释我有专门出过一篇文章windows网络进阶之listen参数含义。
// 4. 监听
if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的监听数目,执行到listen
{
printf("listen error = %d\n", GetLastError());
return -1;
}
6. accept接受客户端连接
对于每一个被接受的连接请求,accept
函数都会创建一个新的套接字,用于与该客户端的后续通信。也都是固定流程,后面互斥和多线程就比较难理解了。
// 5. accept接受客户端连接
SOCKADDR_IN addrCli;
int len = sizeof(SOCKADDR);
while (true)
{
// 接受客户端的连接
SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
}
7.建立套接字数组
将accept生成的套接字放入全局套接字数组中,同时加上互斥锁。
//创建一个互斥对象
hMutex = CreateMutex(NULL, false, NULL);
while (true)
{
// 接受客户端的连接
SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
// 全局变量要加锁
WaitForSingleObject(hMutex, INFINITE);
// 将连接放到数组里面
clnSockets[clnCnt++] = sockCon;
// 解锁
ReleaseMutex(hMutex);
}
closesocket(sockSrv);
CloseHandle(hMutex);
WSACleanup();
return 0;
8. 建立多线程与客户端通信
每通过accept
函数返回的新创建的套接字,就建立一个线程去维护。
//创建一个互斥对象
hMutex = CreateMutex(NULL, false, NULL);
while (true)
{
// 接受客户端的连接
SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
// 全局变量要加锁
WaitForSingleObject(hMutex, INFINITE);
// 将连接放到数组里面
clnSockets[clnCnt++] = sockCon;
// 解锁
ReleaseMutex(hMutex);
// 每接收一个客户端的连接,都安排一个线程去维护
hThread = (HANDLE)_beginthreadex(NULL, 0, &handleCln, (void*)&sockCon, 0, NULL);
printf("Connect client IP = %s\n, Num = %d \n", inet_ntoa(addrCli.sin_addr), clnCnt);
}
closesocket(sockSrv);
CloseHandle(hMutex);
WSACleanup();
return 0;
9. 处理线程函数,收消息
上个步骤我们对每一个接受连接的套接字都创建了线程,现在我们开始来写线程函数中的逻辑代码,主要有三个部分:收到客户端的消息,将收到的消息再发给所有客户端,处理断开的客户端。
下面我们开始完成第一个部分: 收到客户端的消息。
因为客户端发消息会不止一个,所以我们要建立while循环,通关判断接收到的消息来判断,如果为0就退出循环。
// 处理线程函数, 收发消息
unsigned WINAPI handleCln(void *arg)
{
SOCKET hClnSock = *((SOCKET *)arg);
int iLen = 0;
char recvBuff[MAX_BUF_SIZE] = { 0 };
while (1)
{
// iLen 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);
//
if (iLen >= 0)
{
// 将收到的消息转发给所有客户端
SendMsg(recvBuff,iLen);
}
else
{
break;
}
}
10. 发消息给客户端
完成第二个部分: 将收到的消息再发给所有客户端。
因为是仿照qq的小demo,所以服务器一旦收到消息,就要再发送给所有的客户端。这段逻辑写在SendMsg 函数中,同时还需要注意因为在多线程中,所以要避免多个线程同时访问共享资源时产生数据不一致的问题,需要加互斥锁和解锁。
// 将收到的消息转发给所有客户端
void SendMsg(char* msg, int len)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < clnCnt; i++)
{
send(clnSockets[i], msg, len, 0);
}
ReleaseMutex(hMutex);
}
11.处理断开的客户端
完成第三个部分: 处理断开的客户端。
这里也是通过 for 循环遍历 socket 数组,通过匹配每一项,如果相匹配,就然后断开连接。同时 socket 数组 中的数量减 1。
// 处理消息, 收发消息
unsigned WINAPI handleCln(void *arg)
{
SOCKET hClnSock = *((SOCKET *)arg);
int iLen = 0;
char recvBuff[MAX_BUF_SIZE] = { 0 };
while (1)
{
// iLen 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);
//
if (iLen >= 0)
{
// 将收到的消息转发给所有客户端
SendMsg(recvBuff,iLen);
}
else
{
break;
}
}
printf("此时连接的客户端数量 = %d\n", clnCnt);
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < clnCnt; i++)
{
// 找到哪个连接下线的,移除这个连接
if (hClnSock == clnSockets[i])
{
while (i++ < clnCnt)
{
clnSockets[i] = clnSockets[i + 1];
}
break;
}
}
// 断开连接减 1
clnCnt--;
printf("断开连接后连接的客户端数量 = %d\n", clnCnt);
ReleaseMutex(hMutex);
// 断开连接
closesocket(hClnSock);
return 0;
}
四、客户端设计
1.项目准备
客户端设计和服务器端其实差别不大,代码有些基本都相同,逻辑也大多一致,所以有些代码不在过多赘述。
项目准备代码:
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
#define NAME_SIZE 256
#define MAX_BUF_SIZE 1024
char szName[NAME_SIZE] = "[DEFAULT]"; // 默认的昵称
char szMsg[MAX_BUF_SIZE]; // 收发数据的大小
2. 处理main函数参数
项目为仿qq群聊,所以我用main函数中的命令行参数作为我们输入的每一个客户端的名字,项目启动在终端开始启动,否则就退出程序。
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("必须输入两个参数,包括昵称\n");
printf("例如: WXS\n");
system("pause");
return -1;
}
sprintf_s(szName, "[%s]", argv[1]);
printf("this is Client");
}
3.初始化网络库
和服务器端代码一样。
// 初始化库
WSADATA wsaData;
int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (stu != 0) {
std::cout << "WSAStartup 错误:" << stu << std::endl;
return 0;
}
4.SOCKET创建客户端套接字
以服务器类似。
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
if (sockCli == INVALID_SOCKET)
{
std::cout << "socket failed!" << GetLastError() << std::endl;
WSACleanup(); //释放Winsock库资源
return 1;
}
5. 配置IP地址和端口号,连接服务器
也是基本固定写法。
// 配置IP地址 和 端口号
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET; // ipv4协议
addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.7"); // 地址 IP地址any
addrSrv.sin_port = htons(6000); // 端口号
// 连接服务器
int res = connect(sockCli, (sockaddr*)&addrSrv, sizeof(sockaddr));
6.创建两线程,发送和接收
这里我们创建了两个线程,分别处理发送消息给客户端同时接收消息。同时这个函数WaitForSingleObject 会阻塞主进程代码,直到子进程结束。
// 定义两个线程
HANDLE hSendThread, hRecvThread;
// 发送消息
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);
7.处理发送消息线程函数
我们客户端发送消息是通过控制台程序进行发送的,所以要用到用户输入。同时发送的时候带上自己的名字前缀,也要处理快捷键客户端下线的逻辑,不能一致发送消息。
unsigned WINAPI SendMsg(void* arg)
{
SOCKET hClnSock = *((SOCKET*)arg);
char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵称和消息
while (1)
{
memset(szMsg, 0, MAX_BUF_SIZE);
// 阻塞这一句,等待控制台的消息
//fgets(szMsg, MAX_BUF_SIZE, stdin);
// 第二种写法
std::cin >> szMsg;
if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n"))
{
// 处理下线
closesocket(hClnSock);
exit(0);
}
// 拼接 名字和字符串一起发送
sprintf_s(szNameMsg, "%s %s", szName, szMsg);
send(hClnSock, szNameMsg, strlen(szNameMsg) + 1, 0);
}
}
7.处理接收消息线程函数
这里接收消息比较简单,和正常接收客户端消息的逻辑差不多,代码如下:
unsigned WINAPI RecvMsg(void* arg)
{
SOCKET hClnSock = *((SOCKET*)arg);
char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵称和消息
int len;
while (1)
{
len = recv(hClnSock, szNameMsg, sizeof(szNameMsg), 0);
if (len <= 0)
{
break;
return -2;
}
szNameMsg[len] = 0;
std::cout << szNameMsg << std::endl;
// fputs(szNameMsg, stdout);
}
}
五、项目运行
以上我们分别讲解了服务器和客户端代码的实现逻辑,现在我们来进行步骤验证我们的操作结果。
1.编译生成可执行文件
如图所示:
2.运行可执行程序
这里要注意服务器直接运行exe文件即可,而客户端要通过命令行输入运行。
服务器端:
客户端运行需要打开终端,输入exe文件的路径,以及名字。另外进行通讯还需要打开多个客户端。
3.进行通讯
结果展示为:
六、总代码展示
1.服务端代码:
如下所示:
// 1. 对于每一个上线的客户端,服务端会起一个线程去维护
// 2. 将受到的消息转发给全部的客户端
// 3. 当某个客户端断开(下线),需要处理断开的链接。怎么处理呢?
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
#define MAX_CLEN 256
#define MAX_BUF_SIZE 1024
SOCKET clnSockets[MAX_CLEN]; // 所有的连接客户端的socket
int clnCnt = 0; // 客户端连接的个数
HANDLE hMutex;
// 将收到的消息转发给所有客户端
void SendMsg(char* msg, int len)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for (i = 0; i < clnCnt; i++)
{
send(clnSockets[i], msg, len, 0);
}
ReleaseMutex(hMutex);
}
// 处理消息, 收发消息
unsigned WINAPI handleCln(void *arg)
{
SOCKET hClnSock = *((SOCKET *)arg);
int iLen = 0;
char recvBuff[MAX_BUF_SIZE] = { 0 };
while (1)
{
// iLen 成功时返回接收的字节数(收到EOF时为0),失败时返回SOCKETERROR。
iLen = recv(hClnSock, recvBuff, MAX_BUF_SIZE, 0);
//
if (iLen >= 0)
{
// 将收到的消息转发给所有客户端
SendMsg(recvBuff,iLen);
}
else
{
break;
}
}
printf("此时连接的客户端数量 = %d\n", clnCnt);
WaitForSingleObject(hMutex, INFINITE);
for (int i = 0; i < clnCnt; i++)
{
// 找到哪个连接下线的,移除这个连接
if (hClnSock == clnSockets[i])
{
while (i++ < clnCnt)
{
clnSockets[i] = clnSockets[i + 1];
}
break;
}
}
// 断开连接减 1
clnCnt--;
printf("断开连接后连接的客户端数量 = %d\n", clnCnt);
ReleaseMutex(hMutex);
// 断开连接
closesocket(hClnSock);
return 0;
}
int main(int argc, char* argv[])
{
printf("this is Server\n");
//0. 初始化网络
#if 1
// 0 初始化网络库
// 初始化库
WSADATA wsaData;
int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (stu != 0) {
std::cout << "WSAStartup 错误:" << stu << std::endl;
return 0;
}
#endif
HANDLE hThread;
// 1. 创建一个互斥对象
hMutex = CreateMutex(NULL, false, NULL);
// 2. socket 创建套接字
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
if (sockSrv == INVALID_SOCKET)
{
std::cout << "socket failed!" << GetLastError() << std::endl;
WSACleanup(); //释放Winsock库资源
return 1;
}
// 3 bind 绑定套接字
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); // 地址 IP地址any
addrSrv.sin_family = AF_INET; // ipv4协议
addrSrv.sin_port = htons(6000); // 端口号
if ( SOCKET_ERROR == bind(sockSrv, (sockaddr*)&addrSrv, sizeof(SOCKADDR)))
{
std::cout << "bind failed!" << GetLastError() << std::endl;
WSACleanup(); //释放Winsock库资源
return 1;
}
// 4. 监听
if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的监听数目,执行到listen
{
printf("listen error = %d\n", GetLastError());
return -1;
}
// 5
SOCKADDR_IN addrCli;
int len = sizeof(SOCKADDR);
while (true)
{
// 接受客户端的连接
SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
// 全局变量要加锁
WaitForSingleObject(hMutex, INFINITE);
// 将连接放到数组里面
clnSockets[clnCnt++] = sockCon;
// 解锁
ReleaseMutex(hMutex);
// 每接收一个客户端的连接,都安排一个线程去维护
hThread = (HANDLE)_beginthreadex(NULL, 0, &handleCln, (void*)&sockCon, 0, NULL);
printf("Connect client IP = %s\n, Num = %d \n", inet_ntoa(addrCli.sin_addr), clnCnt);
}
closesocket(sockSrv);
CloseHandle(hMutex);
WSACleanup();
return 0;
}
2.客户端代码:
如下所示:
// 客户端做的事情:
//1 请求连接上线,
//2 发消息
//3 客户端等待服务端的消息
//4 等待用户自己的关闭(下线)
#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
#define NAME_SIZE 256
#define MAX_BUF_SIZE 1024
char szName[NAME_SIZE] = "[DEFAULT]"; // 默认的昵称
char szMsg[MAX_BUF_SIZE]; // 收发数据的大小
unsigned WINAPI SendMsg(void* arg)
{
SOCKET hClnSock = *((SOCKET*)arg);
char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵称和消息
while (1)
{
memset(szMsg, 0, MAX_BUF_SIZE);
// 阻塞这一句,等待控制台的消息
//fgets(szMsg, MAX_BUF_SIZE, stdin);
std::cin >> szMsg;
if (!strcmp(szMsg, "Q\n") || !strcmp(szMsg, "q\n"))
{
// 处理下线
closesocket(hClnSock);
exit(0);
}
// 拼接 名字和字符串一起发送
sprintf_s(szNameMsg, "%s %s", szName, szMsg);
send(hClnSock, szNameMsg, strlen(szNameMsg) + 1, 0);
}
}
unsigned WINAPI RecvMsg(void* arg)
{
SOCKET hClnSock = *((SOCKET*)arg);
char szNameMsg[NAME_SIZE + MAX_BUF_SIZE] = { 0 }; // 昵称和消息
int len;
while (1)
{
len = recv(hClnSock, szNameMsg, sizeof(szNameMsg), 0);
if (len <= 0)
{
break;
return -2;
}
szNameMsg[len] = 0;
std::cout << szNameMsg << std::endl;
// fputs(szNameMsg, stdout);
}
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("必须输入两个参数,包括昵称\n");
printf("例如: WXS\n");
system("pause");
return -1;
}
sprintf_s(szName, "[%s]", argv[1]);
printf("this is Client");
//0. 初始化网络
#if 1
// 0 初始化网络库
// 初始化库
WSADATA wsaData;
int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (stu != 0) {
std::cout << "WSAStartup 错误:" << stu << std::endl;
return 0;
}
#endif
// 定义两个线程
HANDLE hSendThread, hRecvThread;
// 1. 建立 socket
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
if (sockCli == INVALID_SOCKET)
{
std::cout << "socket failed!" << GetLastError() << std::endl;
WSACleanup(); //释放Winsock库资源
return 1;
}
// 2, 配置IP地址 和 端口号
SOCKADDR_IN addrSrv;
addrSrv.sin_family = AF_INET; // ipv4协议
addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.1.7"); // 地址 IP地址any
addrSrv.sin_port = htons(6000); // 端口号
// 3. 连接服务器
int res = connect(sockCli, (sockaddr*)&addrSrv, sizeof(sockaddr));
// 4. 发送服务器消息,启动线程
hSendThread = (HANDLE)_beginthreadex(NULL, 0, &SendMsg, (void*)&sockCli, 0, NULL);
// 5. 等待
hRecvThread = (HANDLE)_beginthreadex(NULL, 0, &RecvMsg, (void*)&sockCli, 0, NULL);
WaitForSingleObject(hSendThread,INFINITE);
WaitForSingleObject(hRecvThread, INFINITE);
closesocket(sockCli);
WSACleanup();
return 0;
}
七、最后
制作不易,熬夜肝的,还请多多点赞,拯救下秃头的博主吧!!