目录
[4. bind 绑定套接字](#4. bind 绑定套接字)
[5. listen监听套接字](#5. listen监听套接字)
[6. accept接受客户端连接](#6. accept接受客户端连接)
[8. 建立多线程与客户端通信](#8. 建立多线程与客户端通信)
[9. 处理线程函数,收消息](#9. 处理线程函数,收消息)
[10. 发消息给客户端](#10. 发消息给客户端)
[2. 处理main函数参数](#2. 处理main函数参数)
[5. 配置IP地址和端口号,连接服务器](#5. 配置IP地址和端口号,连接服务器)
一、前言
今天我们不学习其他的知识点,主要是复习之前学习过的TCP网络通信和多线程以及线程同步互斥,然后结合这以上知识点设计实现一个小的项目,主要仿照qq群聊的服务器可客户端的实现,下面我将会说明一下设计需求,以下是整个设计示意图。
二、设计需求
1.服务器需求
需求一**:** 对于每一个上线连接的客户端,服务端会起一个线程去维护。
需求二:将服务器受到的消息转发给全部的客户端。例如:服务器接收客户端A的消息后,将立即发送给客户端A,B,C...
需求三:当某个客户端断开(下线),需要处理断开的链接。
2.客户端需求
需求一:请求连接上线,
需求二:发消息给服务器。
需求三:客户端等待服务端的消息。
需求四:等待用户自己的关闭(下线)。
三、服务端设计
1.项目准备
在创建项目后,引入一些必需的头文件以及创建项目需要的宏,例如:允许客户端连接的最大数量,接收文件字节的大小,客户端连接的个数等等。
cpp
#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,这个函数用于初始化网络环境,都是固定写法,必须要有的,直接复制粘贴即可。
cpp
// 1. 初始化库
WSADATA wsaData;
int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (stu != 0) {
std::cout << "WSAStartup 错误:" << stu << std::endl;
return 0;
}
3.SOCKET创建服务器套接字
这和我们之前学的windwos网络一样都是固定写法,重点时查看函数原型以及它的参数,代码如下:
cpp
// 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地址,端口号,以及协议版本。
cpp
// 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参数含义。
cpp
// 4. 监听
if (listen(sockSrv, 5) == SOCKET_ERROR) // 5 是指最大的监听数目,执行到listen
{
printf("listen error = %d\n", GetLastError());
return -1;
}
6. accept接受客户端连接
对于每一个被接受的连接请求,accept
函数都会创建一个新的套接字,用于与该客户端的后续通信。也都是固定流程,后面互斥和多线程就比较难理解了。
cpp
// 5. accept接受客户端连接
SOCKADDR_IN addrCli;
int len = sizeof(SOCKADDR);
while (true)
{
// 接受客户端的连接
SOCKET sockCon = accept(sockSrv, (sockaddr*)&addrCli, &len);
}
7.建立套接字数组
将accept生成的套接字放入全局套接字数组中,同时加上互斥锁。
cpp
//创建一个互斥对象
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
函数返回的新创建的套接字,就建立一个线程去维护。
cpp
//创建一个互斥对象
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就退出循环。
cpp
// 处理线程函数, 收发消息
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 函数中,同时还需要注意因为在多线程中,所以要避免多个线程同时访问共享资源时产生数据不一致的问题,需要加互斥锁和解锁。
cpp
// 将收到的消息转发给所有客户端
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。
cpp
// 处理消息, 收发消息
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.项目准备
客户端设计和服务器端其实差别不大,代码有些基本都相同,逻辑也大多一致,所以有些代码不在过多赘述。
项目准备代码:
cpp
#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函数中的命令行参数作为我们输入的每一个客户端的名字,项目启动在终端开始启动,否则就退出程序。
cpp
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.初始化网络库
和服务器端代码一样。
cpp
// 初始化库
WSADATA wsaData;
int stu = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (stu != 0) {
std::cout << "WSAStartup 错误:" << stu << std::endl;
return 0;
}
4.SOCKET创建客户端套接字
以服务器类似。
cpp
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地址和端口号,连接服务器
也是基本固定写法。
cpp
// 配置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 会阻塞主进程代码,直到子进程结束。
cpp
// 定义两个线程
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.处理发送消息线程函数
我们客户端发送消息是通过控制台程序进行发送的,所以要用到用户输入。同时发送的时候带上自己的名字前缀,也要处理快捷键客户端下线的逻辑,不能一致发送消息。
cpp
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.处理接收消息线程函数
这里接收消息比较简单,和正常接收客户端消息的逻辑差不多,代码如下:
cpp
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.服务端代码:
如下所示:
cpp
// 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.客户端代码:
如下所示:
cpp
// 客户端做的事情:
//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;
}
七、最后
制作不易,熬夜肝的,还请多多点赞,拯救下秃头的博主吧!!