C++小项目(聊天室)——select模型+mysql+花生壳端口映射打造一个可以用外网连接的小qq

成品展示:
B站视频链接

这个小软件是我初学网络编程写的小玩具,记录一下,等学完完成端口模型再利用完成端口写别的好玩的软件,看的课程是这个老师,真的强烈推荐,课程28块钱,老师讲的巨棒,很细,很适合新手看看课程链接

该篇博客,只记录一下自己设计的想法,并没有介绍一些基础知识,比如select的用法,如果遇到不明白的函数,可以去msdn搜,各个函数介绍的都很棒msdn,在文中会提到在编写代码遇到的问题及一些细节

select模型缺点很明显,只能连接小部分用户,一般定义为64,可以无限大,但是最好不要超过1024,除非客户端的人很有耐心,需要耐心的等。(如果要在linux修改这个值的话,需要重新编译内核才行)

实现的功能有:

  • 登录
  • 重复登录被迫下线
  • 把消息发送给所有人

正文:

C/S模型
服务器连接的是内网IP地址,选择一个你喜欢的数字作为端口号,0 - 2^16 - 1可以作为端口号,但是一些端口号已经有别的用途了,比如mysql的是3306,FTP的是21。我的小QQ选的是9996这个端口号作为服务器的端口号,用cmd进入命令行,输入netstat -aon|findstr “9996”可查该端口是否被占用。客户端是要主动连接服务器的,所以客户端的IP地址是用公网IP地址和端口映射的端口。客户端的公网IP和端口号都是花生壳提供的。下面简单介绍一下花生壳。

端口映射

下载一个花生壳,然后端口映射一下,映射成功后,左下角有一个诊断,点击诊断就出现下面这张图了,注意这个IP地址,客户端是要用它提供的外网IP地址,而不是自己本机的外网IP的地址,我一开始就写错了。别忘了开启Telnet服务。Telnet服务开启教程
在这里插入图片描述
对mysql连接VS C++有不会的同学可以参考我之前写的博客:
mysql连接VS C++

mysql中文乱码
在mysql下输入show variables like ‘%character%’;查看mysql字符编码集,我也尝试改这些编码集,可是并没有什么用,还不如在查询语句前面加一条 *mysql_set_character_set(conn, “gbk”);*因为我的 character_set_results是gbk,所以也设置成gbk。在这里插入图片描述

处理登录验证
我的登录与密码验证的时候,单独开一根线程来进行mysql的查询,如果查询到正确结果,创建客户端的套接字并且给他返回正确的标志。我的代码这里有点小问题,如果一个用户一直不输入账号和密码,一直挂在这里,我服务器的这根验证线程就要一直等待一直等待,会消耗CPU的资源,可以写一个计算器,然后定个时,超过多长时间还没有输入账号与密码,直接强制关闭或者先退出线程,等用户再输入的时候再给他开线程,等后面学到更高级的网络模型在修改这里,现在还是个小玩具

广播给所有人
广播单独开一个线程,有消息进来了就开始广播所有有效的套接字。读取注意这里要加一个锁,因为在验证重复登录的时候,会对套接字进行改动,如果不加锁,一个改一个读会产生数据错误。

服务器的头文件

head.h

#pragma once
#ifndef HEAD_H_INCLUDED
#define HEAD_H_INCLUDED
#include <stdio.h>
#include<thread>
#include<Windows.h>
#include<vector>
#include<set>
#include<map>
#include<deque>
//#include<WinSock2.h>
//#include <WS2tcpip.h>
#include<unordered_set>
#include<mysql.h>
#include<list>
#include<iostream>
#include<tuple>
#include<mutex>
#pragma comment(lib,"libmysql.lib")
#pragma comment(lib, "ws2_32.lib")
#endif // !1

server_initial.h文件

#pragma once
#ifndef SOCKET_INITIAL_H_INCLUDED
#include"head.h"
#define SOCKET_INITIAL_H_INCLUDED
using std::vector;
using std::unordered_set;
using std::cout;
using std::endl;
using std::list;
using std::set;
using std::map;
using std::thread;
using std::deque;
using std::string;
using std::tuple;
class server_initial
{
private://服务器所需要的
	fd_set allsocket = { 0 };
	SOCKADDR_IN cAddr = { 0 };
	set<SOCKET> clientSet;//因为要频繁的插入和删除,并且还想效率高
	deque<std::pair<SOCKET, const char*>> msgdeque;
	SOCKET serverSocket;

//功能性数据结构
private:
	//这两个的string存放的都是loginNumber
	map<string,SOCKET>repeat_login;//目的是实现下线功能
	map<SOCKET, string>repeat_login_brthor;//他俩相辅相成

private://数据库所需要的成员变量
	MYSQL conn;
	MYSQL_RES* res_set;
	MYSQL_ROW row;
	bool mysql_flag = false;
	deque<tuple<SOCKET, string, string>>mq_deque;//存放套接字、账号和密码

private:
	std::mutex repeat_socket;//防止重复登录的时候,和广播时候socket造成了损失


public:
	server_initial();
	int socket_initial();//套接字初始化
	void broadcast();//广播
	bool mysql_initial(const string& usename, const string& password);//数据库的初始化
	void login(SOCKET client);//每个客户端另要开一个线程
	string select_usename(string& loginNumber);
	virtual ~server_initial();
};

#endif 

服务器源文件

#include "server_initial.h"


//单独开根线程,用来广播信息
void server_initial::broadcast() {
	while (1) {
		while (!msgdeque.empty()) {
			auto msg_it = msgdeque.front();
			msgdeque.pop_front();
			SOCKET resource = msg_it.first;//这个消息源自那个客户端
			const char* msg = msg_it.second;
			std::lock_guard<std::mutex> protectSocket(repeat_socket);//队列的消息不需要阻塞
			//尽管resource有可能被去掉,但是clientSet是不可能给resource发消息,所以这里的锁没出错
			auto it_socket = clientSet.begin();//装的是socket
			string usename = select_usename(repeat_login_brthor[resource]);//找到这个客户端的loginNumber,再调用mysql的查询语句
			string newmsg = usename +":" + msg;
			
			while (it_socket != clientSet.end()) {
				if (*it_socket != resource) {
					int num = send(*it_socket, newmsg.c_str(), static_cast<int>(newmsg.size()), 0);
					if (num < 0) {
						cout << "广播数据发送失败,err:" << WSAGetLastError() << endl;
					}
				}
				++it_socket;
			}
		}
		Sleep(500);//如果太少了的话会造成mysql查询会饥饿
	}
}


string server_initial::select_usename(string& loginNumber) {
	string query = "SELECT usename FROM smallqq where loginNumber = '" +
		loginNumber + "'";//查询语句的连接
	int status = mysql_query(&conn, query.c_str());
	res_set = mysql_store_result(&conn);
	row = mysql_fetch_row(res_set);
	string usename;
	if (row[0] != nullptr) {
		usename = row[0];
	}
	return usename;
}


bool server_initial::mysql_initial(const string& loginNumber,const string& password) {
	
	string query = "SELECT * FROM smallqq where loginNumber = '" + 
	loginNumber + "' and loginpassword = '" + password + "'";//查询语句的连接
	int status = mysql_query(&conn, query.c_str());
	res_set = mysql_store_result(&conn);
	uint64_t count = mysql_num_rows(res_set);//unsigned long long
	if (count > 0) {//有一条记录就返回登录成功
		return true;
	}
	return false;
}


void server_initial::login(SOCKET clientsocket) {//接收该客户端的登录账号和密码
	char buff[1501] = { 0 };
	string loginNumber, password;
	while (loginNumber.empty()|| password.empty()) {
		int r = recv(clientsocket, buff, 1499, 0);
		if (r > 0) {
			buff[r] = 0;
			if (loginNumber.empty()) {
				loginNumber = buff;
			}
			else {
				password = buff;
			}
		}
	}
	//这里面是登录成功的情况
	if (mysql_initial(loginNumber, password)) {

		//处理重复登录的情况
		if (repeat_login.find(loginNumber) != repeat_login.end()) {
			//防止给被删除的socket发数据
			std::lock_guard<std::mutex> protectSocket(repeat_socket);
			
			send(repeat_login[loginNumber], "quit",static_cast<int>(strlen("quit")), 0);
			
			
			//清除一下他的信息
			FD_CLR(repeat_login[loginNumber],&allsocket);//把当前的套接字也去掉
			clientSet.erase(repeat_login[loginNumber]);
			closesocket(repeat_login[loginNumber]);
			repeat_login[loginNumber] = clientsocket;//把他的套接字变更一下
			repeat_login_brthor.erase(clientsocket);

		}


		repeat_login_brthor.emplace(clientsocket, loginNumber);
		send(clientsocket, "T", 1, 0);
		FD_SET(clientsocket, &allsocket);
		repeat_login.emplace(loginNumber, clientsocket);
		clientSet.emplace(clientsocket);//有一个连接就加入到套接字中来
		printf("有客户端连接到服务器了:%s!\n", inet_ntoa(cAddr.sin_addr));
		cout << "在线人数为" << allsocket.fd_count - 1 << endl;//有一个是服务器的套接字
		
	}
	//处理登录失败的情况
	else {
		//cout << "无效登录" << endl;
		send(clientsocket, "F", static_cast<int>(strlen("F")), 0);
	}
}


int server_initial::socket_initial() {
	if (!mysql_flag) {
		cout << "数据库启动失败,服务器配备失败" << endl;
		return 0 ;
	}
	int len = sizeof(SOCKADDR_IN);
	//1 请求协议版本
	WSADATA wsaData;
	WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (LOBYTE(wsaData.wVersion) != 2 ||
		HIBYTE(wsaData.wVersion) != 2) {
		printf("请求协议版本失败!\n");
		return -1;
	}
	printf("请求协议成功!\n");
	//2 创建socket
	serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (SOCKET_ERROR == serverSocket) {
		printf("创建socket失败!\n");
		WSACleanup();
		return -2;
	}
	printf("创建socket成功!\n");

	//3 创建协议地址族
	SOCKADDR_IN addr;
	addr.sin_family = AF_INET;//协议版本
	addr.sin_addr.S_un.S_addr = inet_addr("192.168.3.4");//用自己的ip
	addr.sin_port = htons(9996);//0 - 65535     10000左右
	//os内核 和其他程序  会占用掉一些端口   80  23  


	//4 绑定
	if (bind(serverSocket, (LPSOCKADDR)&addr, sizeof(addr)) == SOCKET_ERROR) {
		printf("bind失败!\n");
		closesocket(serverSocket);
		WSACleanup();
		return -2;
	}
	printf("bind成功!\n");

	//5 监听
	int r = listen(serverSocket, 10);
	if (r == -1) {
		printf("listen失败!\n");
		closesocket(serverSocket);
		WSACleanup();
		return -2;
	}
	printf("listen成功!\n");

	FD_ZERO(&allsocket);
	FD_SET(serverSocket, &allsocket);
	timeval st;
	st.tv_sec = 0;
	st.tv_usec = 0;

	//这个线程专门用来广播消息
	thread thread_broadcast(&server_initial::broadcast, this);
	thread_broadcast.detach();

	const string usename_flag = "smallqquse";
	const string password_flag = "smallqqpass";
	//6 等待客户端连接    
	//客户端协议地址族
	while (1) {
		fd_set tmpSocket = allsocket;
		int num = select(0, &tmpSocket, NULL, &tmpSocket, &st);//解决recv和accept傻等的问题
		//cout << num << endl;
		if (num > 0) {
			for (u_int i = 0; i < tmpSocket.fd_count; ++i) {
				if (tmpSocket.fd_array[i] == serverSocket) {//这个的时候接收客户端的连接
					SOCKET clientsocket = accept(serverSocket, (sockaddr*)&cAddr, &len);
					
					if (INVALID_SOCKET == clientsocket) {
						cout << "绑定失败" << endl;
						continue;
					}
					thread t(&server_initial::login,this,clientsocket);
					t.detach();

					//clientSet.emplace(clientsocket);//有一个连接就加入到套接字中来
					//FD_SET(clientsocket, &allsocket);
					//printf("有客户端连接到服务器了:%s!\n", inet_ntoa(cAddr.sin_addr));
				}
				else {//等于别的套接字的时候
					char buff[1500] = { 0 };//最大的MTU
					cout << buff << endl;
					int r = recv(tmpSocket.fd_array[i], buff, 1499, 0);
					if (r == 0) {//客户端下线了
						auto close_client_socket = tmpSocket.fd_array[i];
						clientSet.erase(close_client_socket);
						
						FD_CLR(tmpSocket.fd_array[i], &allsocket);
						closesocket(close_client_socket);
						string close_loginNumber = repeat_login_brthor[close_client_socket];
						repeat_login.erase(close_loginNumber);
						repeat_login_brthor.erase(close_client_socket);
						cout << "关闭连接" << endl;
						continue;
					}

					else if (r > 0) {//接受到了消息可以进行广播
						buff[r] = 0;
						msgdeque.emplace_back(std::make_pair(tmpSocket.fd_array[i], buff));
						cout << repeat_login_brthor[tmpSocket.fd_array[i]] << ":" << buff << endl;//在服务器端发送者名字和内容
					}
					else {//有故障了
						int err = WSAGetLastError();
						if (err == 10054) {//这个是强制关闭,也要清理一下
							auto close_client_socket = tmpSocket.fd_array[i];
							clientSet.erase(close_client_socket);
							FD_CLR(tmpSocket.fd_array[i], &allsocket);//因为已经把这个下标删了,如果在释放那么就会释放错了
							closesocket(close_client_socket);
							cout << "错误退出" << ' ' << err << endl;
						}
						//cout << "错误代号:" << err << endl;
						continue;
					}
				}

			}
		}

		else if(num < 0){
			cout << "select函数出错了" <<WSAGetLastError()<< endl;
		}

	}
	
}

//构造函数
server_initial::server_initial() {
	
	mysql_init(&conn);//数据库初始化
	if (!mysql_real_connect(&conn, "localhost", "root", "123456", "stu", 3306, NULL, 0)) {
		fprintf(stderr, "Failed to connect to database: Error: %s\n",mysql_error(&conn));
		mysql_flag = false;
	}
	else {
		fprintf(stderr, "数据库创建成功!\n");
		mysql_set_character_set(conn, "gbk");//设置编码集
		mysql_flag = true;
	}
	socket_initial();
}




//析构函数
server_initial::~server_initial() {
	for (u_int i = 0; i < allsocket.fd_count; ++i) {
		closesocket(allsocket.fd_array[i]);//将每个select函数模型里的套接字也删掉
	}
	FD_ZERO(&allsocket);
	//8 关闭socket
	closesocket(serverSocket);
	//9 清除协议信息
	WSACleanup();
	mysql_free_result(res_set);  //释放一个结果集合使用的内存。
	mysql_close(&conn);//关闭数据库
}

启动函数

#include"head.h"
#include"server_initial.h"

int main() {
	server_initial s;
	return 0;
}

客户端的代码就很少了,如下

#include <stdio.h>
#include<string>
#include<iostream>
#include<thread>
#include <windows.h>
#include<atomic>
//#pragma comment(lib, "ws2_32.lib")
using std::string;
using std::cout;
using std::endl;
class smallqq_client {
	SOCKET smallqq_clientSocket = 0;
	//HWND hWnd;
	std::atomic_bool flag = false;
void smallqq_clientrecv() {
	int r = 0;
	fd_set readSocket;
	FD_ZERO(&readSocket);
	FD_SET(smallqq_clientSocket, &readSocket);
	timeval st;
	st.tv_sec = 2;
	st.tv_usec = 0;
	while (1) {
		char recvBuff[1501];
		if (flag) {
			break;
		}
		auto tmpSocket = readSocket;
		//FD_SET(smallqq_clientSocket, &tmpSocket);
		int num = select(0, &tmpSocket, NULL, &tmpSocket, &st);//解决recv傻等的问题
		if (num > 0) {
			for (u_int i = 0; i < tmpSocket.fd_count; ++i) {
				r = recv(tmpSocket.fd_array[i], recvBuff, 1500, 0);
				if (r > 0) {
					recvBuff[r] = 0;
					if (strcmp(recvBuff, "quit") == 0) {//等于0说明这两个字符串相等
						flag = true;
							cout << "该账号重复登录,被迫下线" << endl;
							break;
						}
					cout << endl;
					// << "接受到一条消息:";
					cout << recvBuff << endl;
				}
				else if (r < 0) {
					cout << "err:" << WSAGetLastError();
				}
			}
				//cout << readSocket.fd_count << endl;
		}
		else if (num < 0) {
				cout << "select函数错误" << WSAGetLastError() << endl;
		}
	}
	for (u_int i = 0; i < readSocket.fd_count; ++i) {

		closesocket(readSocket.fd_array[i]);
		readSocket.fd_array[i] = INVALID_SOCKET;
	}
	FD_ZERO(&readSocket);//清空信息
	cout << "该线程退出" << endl;
}

bool login() {
	cout << "请输入登录账号和密码" << endl;
	string usename, password;
	std::getline(std::cin, usename);
	std::getline(std::cin, password);
	int num = send(smallqq_clientSocket, usename.c_str(), static_cast<int>(usename.size()), 0);//参数是int类型,做一个强制转换
	num = send(smallqq_clientSocket, password.c_str(), static_cast<int>(password.size()), 0);
	if (num < 0)return 0;
	//cout << num << endl;
	string flag;
	while (flag.empty()) {
		char judge_buff[1024] = { 0 };
		int r = recv(smallqq_clientSocket, judge_buff, 1023, NULL);
		if (r > 0) {
			judge_buff[r] = 0;
			flag = judge_buff;
		}
		else {
			cout << WSAGetLastError() << endl;
			return false;
		}
	}
	if (flag == "T") {
		cout << "登录成功" << endl;
		return true;
	}
	else {
		cout << "登录失败" << endl;
	}
	return false;
}
public:
	smallqq_client() = default;
	int initial() {
		//初始化界面
		//hWnd = initgraph(300, 400, SHOWCONSOLE);

		//1 请求协议版本
		WSADATA wsaData;
		WSAStartup(MAKEWORD(2, 2), &wsaData);
		if (LOBYTE(wsaData.wVersion) != 2 ||
			HIBYTE(wsaData.wVersion) != 2) {
			printf("请求协议版本失败!\n");
			return -1;
		}
		printf("请求协议成功!\n");

		//2 创建socket
		smallqq_clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
		if (SOCKET_ERROR == smallqq_clientSocket) {
			printf("创建socket失败!\n");
			WSACleanup();
			return -2;
		}
		printf("创建socket成功!\n");

		//3 获取服务器协议地址族
		SOCKADDR_IN addr = { 0 };
		addr.sin_family = AF_INET;//协议版本
		addr.sin_addr.S_un.S_addr = inet_addr("103.46.128.49");//绑定的是服务器的ip
		addr.sin_port = htons(51758);//0 - 65535     10000左右
		//os内核 和其他程序  会占用掉一些端口   80  23  

		//4 连接服务器
		int c = connect(smallqq_clientSocket, (sockaddr*)&addr, sizeof addr);
		if (c == -1) {
			printf("连接服务器失败!\n");
			return -1;
		}
		printf("连接服务器成功!\n");

		if (!login()) {
			return 0;
		}
		std::thread t(&smallqq_client::smallqq_clientrecv, this);
		while (1) {
			if (flag) {//收到服务器断开的消息
				break;
			}
			string buff;
			cout << "想说点什么:";
			std::getline(std::cin, buff);
			if (buff == "quit") {
				flag = true;//准备让接收的线程也退出来。
				
				break;
			}
			send(smallqq_clientSocket, buff.c_str(), static_cast<int>(buff.size()), 0);
		}
		if (t.joinable()) {
			t.join();//等待线程退出
		}
		
		
		cout << "已断开连接" << endl;
		return 0;
	}
	~smallqq_client() {
		if (smallqq_clientSocket != INVALID_SOCKET)
		{
			//cout << "套接字被析构了" << endl;
			closesocket(smallqq_clientSocket);
			smallqq_clientSocket = INVALID_SOCKET;
		}
		WSACleanup();//清除协议
	}
};
int main() {
	smallqq_client c;
	c.initial();
	system("pause");
	return 0;
}


//自动获取IP地址
/*
bool GetLocalIP(char* ip)
{
	//1.初始化wsa
	WSADATA wsaData;
	int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if (ret != 0)
	{
		return false;
	}
	//2.获取主机名
	char hostname[256];
	ret = gethostname(hostname, sizeof(hostname));
	if (ret == SOCKET_ERROR)
	{
		return false;
	}
	//3.获取主机ip
	HOSTENT* host = gethostbyname(hostname);
	if (host == NULL)
	{
		return false;
	}
	//4.转化为char*并拷贝返回
	strcpy(ip, inet_ntoa(*(in_addr*)*host->h_addr_list));
	return true;
}
*/
  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值