成品展示:
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
#include
#include
#include
#include
#include
#include
//#include
//#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
//#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;
}
*/