一、客户端首页
客户端首页: 客户端不需要高并发直接采用基于TCP的网络编程即可,服务器需要什么消息我们就发送什么消息,服务器返回什么消息我们就解析什么消息。 用户执行客户端,有注册、登录、退出业务。注册完新用户后进行登录,输入用户id、密码后客户端发送登录请求到服务器,服务器验证这些信息是否正确,正确用户便进入聊天菜单页面。
在进行用户登录业务处理前,我们需要提前处理好以下几点:
1、提前利用全局变量记录当前登录成功客户端记录用户相关信息,获取一次后续便无需再从服务器获取。
//登录成功客户端记录用户相关信息,后续无需再从服务器获取了
User g_currentUser; //记录当前系统登录的用户信息
vector<User> g_currentUserFriendList; //记录当前登录用户的好友列表信息
vector<Group> g_currentUserGroupList; //记录当前登录用户的群组列表信息
2、封装好接收线程:主线程用作发送线程,子线程用作接收线程,保证发送数据与接收数据可以并行处理,不会阻塞。
// 子线程,接收线程:接收用户的手动输入
void readTaskHandler(int clientfd)
{
for (;;)
{
char buffer[1024] = {0};
int len = recv(clientfd, buffer, 1024, 0);
if (-1 == len || 0 == len)
{
close(clientfd);
exit(-1);
}
// 接收数据,将网络发送过来的数据反序列化为json数据对象,如果是聊天信息则打印
json js = json::parse(buffer);
int msgtype = js["msgid"].get<int>();
if (ONE_CHAT_MSG == msgtype) // 点对点聊天消息
{
cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
<< " said: " << js["msg"].get<string>() << endl;
continue;
}
if (GROUP_CHAT_MSG == msgtype) // 群消息
{
cout << "groupmsg[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
<< " said: " << js["msg"].get<string>() << endl;
continue;
}
if (LOGIN_MSG_ACK == msgtype) // 处理登录响应消息
{
doLoginResponse(js);
sem_post(&rwsem); // 子线程给主线程通知信号量
continue;
}
if (REG_MSG_ACK == msgtype) // 处理注册响应消息
{
doRegResponse(js);
sem_post(&rwsem); // 子线程给主线程通知信号量
continue;
}
}
}
客户端首页业务流程:客户端创建socket成功后便启动子线程,专门接收服务端的响应消息(读操作);主线程进入首界面时,打包数据进行序列化发送给服务端,发送完成后需要等待在sem信号量上,依据子线程从服务器接收的响应通过信号量告知主线程业务处理结果。
1、主函数进入,解析客户端启动输入的服务区ip地址+端口号,然后进行标准的一套tcp网络编程。
2、进行客户端首页展示:用户选择登录、注册、退出业务。
3、登录业务:主线程先获取用户手动输入的用户id和密码,组装json数据,将json数据对象序列化为字符串后通过网络发送给服务器。若发送失败打印错误信息;若发送成功,主线程等待信号量,子线程反序列化从服务器接收的数据,依据是否登录成功给予用户提示,登录失败打印相关错误提示信息,登录成功打印提示信息并显示当前用户的好友列表、群组列表、离线消息。最终由子线程处理完登录的响应消息后通知主线程,登录成功进入聊天主菜单界面。
4、注册业务:主线程先获取用户手动输入的用户名和密码,组装json数据,将json数据对象序列化为字符串后通过网络发送给服务器。若发送失败打印错误信息;若发送成功,主线程等待信号量,子线程反序列化从服务器接收的数据,注册失败打印相关错误提示信息,注册成功打印成功提示并返回给用户注册的id号,最终由子线程处理完注册的响应消息后通知主线程。
5、退出业务:释放socket资源,关闭连接。
客户端首页核心代码:
// 登录成功客户端记录用户相关信息,后续无需再从服务器获取了
User g_currentUser; // 记录当前系统登录的用户信息
vector<User> g_currentUserFriendList; // 记录当前登录用户的好友列表信息
vector<Group> g_currentUserGroupList; // 记录当前登录用户的群组列表信息
bool isMainMenuRunning = false; // 控制主菜单页面程序:主菜单页面是否正在进行
sem_t rwsem; // 用于读写线程之间的通信
atomic_bool g_isLoginSuccess{false}; // 记录登录状态是否成功
// 显示当前登录成功用户的基本信息:打印用户id、名称,显示其好友列表与群组列表
void showCurrentUserData()
{
cout << "======================login user======================" << endl;
cout << "current login user id:" << g_currentUser.getId() << " name:" << g_currentUser.getName() << endl;
cout << "----------------------friend list---------------------" << endl;
if (!g_currentUserFriendList.empty())
{
for (User &user : g_currentUserFriendList)
{
cout << user.getId() << " " << user.getName() << " " << user.getState() << endl;
}
}
cout << "----------------------group list----------------------" << endl;
if (!g_currentUserGroupList.empty())
{
for (Group &group : g_currentUserGroupList)
{
cout << group.getId() << " " << group.getName() << " " << group.getDesc() << endl;
for (GroupUser &user : group.getUsers())
{
cout << user.getId() << " " << user.getName() << " " << user.getState() << " " << user.getRole() << endl;
}
}
}
cout << "======================login user======================" << endl;
}
// 处理登录响应逻辑
void doLoginResponse(json &responsejs)
{
if (0 != responsejs["errno"].get<int>()) // 登录失败
{
cerr << responsejs["errmsg"] << endl;
g_isLoginSuccess = false;
}
else // 登录成功
{
// 记录当前用户的id和name
g_currentUser.setId(responsejs["id"].get<int>());
g_currentUser.setName(responsejs["name"]);
// 记录当前用户的好友列表信息
if (responsejs.contains("friends"))
{
// 初始化
g_currentUserFriendList.clear();
vector<string> vec = responsejs["friends"];
for (string &str : vec)
{
json js = json::parse(str);
User user;
user.setId(js["id"].get<int>());
user.setName(js["name"]);
user.setState(js["state"]);
g_currentUserFriendList.push_back(user);
}
}
// 记录当前用户的群组列表信息
if (responsejs.contains("groups"))
{
// 初始化
g_currentUserGroupList.clear();
vector<string> vec1 = responsejs["groups"];
for (string &groupstr : vec1)
{
json grpjs = json::parse(groupstr);
Group group;
group.setId(grpjs["id"].get<int>());
group.setName(grpjs["groupname"]);
group.setDesc(grpjs["groupdesc"]);
vector<string> vec2 = grpjs["users"];
for (string &userstr : vec2)
{
GroupUser user;
json js = json::parse(userstr);
user.setId(js["id"].get<int>());
user.setName(js["name"]);
user.setState(js["state"]);
user.setRole(js["role"]);
group.getUsers().push_back(user);
}
g_currentUserGroupList.push_back(group);
}
}
// 显示当前用户的离线消息 个人聊天信息或者群组消息
if (responsejs.contains("offlinemsg"))
{
vector<string> vec = responsejs["offlinemsg"];
for (string &str : vec)
{
json js = json::parse(str);
// time + [id] + name + " said: " + xxx
if (ONE_CHAT_MSG == js["msgid"].get<int>())
{
cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
<< " said: " << js["msg"].get<string>() << endl;
}
else
{
cout << "groupmsg[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
<< " said: " << js["msg"].get<string>() << endl;
}
}
}
g_isLoginSuccess = true;
}
}
// 处理注册响应逻辑
void doRegResponse(json &responsejs)
{
if (0 != responsejs["errno"].get<int>()) // errno不为0,注册失败
{
cerr << "name is already exist, register error!" << endl;
}
else // errno为0,注册成功,返回userid
{
cout << "name register success, userid is " << responsejs["id"] << ", do not forget it!" << endl;
}
}
// 子线程,接收线程:接收用户的手动输入
void readTaskHandler(int clientfd)
{
for (;;)
{
char buffer[1024] = {0};
int len = recv(clientfd, buffer, 1024, 0);
if (-1 == len || 0 == len)
{
close(clientfd);
exit(-1);
}
// 接收数据,将网络发送过来的数据反序列化为json数据对象,如果是聊天信息则打印
json js = json::parse(buffer);
int msgtype = js["msgid"].get<int>();
if (ONE_CHAT_MSG == msgtype) // 点对点聊天消息
{
cout << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
<< " said: " << js["msg"].get<string>() << endl;
continue;
}
if (GROUP_CHAT_MSG == msgtype) // 群消息
{
cout << "groupmsg[" << js["groupid"] << "]:" << js["time"].get<string>() << " [" << js["id"] << "]" << js["name"].get<string>()
<< " said: " << js["msg"].get<string>() << endl;
continue;
}
if (LOGIN_MSG_ACK == msgtype) // 处理登录响应消息
{
doLoginResponse(js);
sem_post(&rwsem); // 子线程给主线程通知信号量
continue;
}
if (REG_MSG_ACK == msgtype) // 处理注册响应消息
{
doRegResponse(js);
sem_post(&rwsem); // 子线程给主线程通知信号量
continue;
}
}
}
// 获取系统时间(聊天信息需要显示发送时间)
string getCurrentTime()
{
auto tt = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
struct tm *ptm = localtime(&tt);
char date[60] = {0};
sprintf(date, "%d-%02d-%02d %02d:%02d:%02d",
(int)ptm->tm_year + 1900, (int)ptm->tm_mon + 1, (int)ptm->tm_mday,
(int)ptm->tm_hour, (int)ptm->tm_min, (int)ptm->tm_sec);
return std::string(date);
}
// 聊天客户端程序:main线程用作发送线程,子线程用作接收线程
int main(int argc, char **argv)
{
if (argc < 3)
{
cerr << "command invalid ! example:./ChatClient 127.0.0.1 6000" << endl;
exit(-1);
}
// 解析通过命令行参数传递的ip和port
char *ip = argv[1];
uint16_t port = atoi(argv[2]);
// 创建client端的socket
int clientfd = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == clientfd)
{
cerr << "socket create error" << endl;
exit(-1);
}
// 填写client需要连接的server信息ip + port
sockaddr_in server;
memset(&server, 0, sizeof(sockaddr_in));
server.sin_family = AF_INET;
server.sin_port = htons(port);
server.sin_addr.s_addr = inet_addr(ip);
// client和server进行连接
if (-1 == connect(clientfd, (sockaddr *)&server, sizeof(sockaddr_in)))
{
cerr << "connect server error" << endl;
close(clientfd);
exit(-1);
}
// 初始化读写线程通信用的信号量
sem_init(&rwsem, 0, 0);
// 连接服务器成功,启动接收子线程
std::thread readTask(readTaskHandler, clientfd); // 底层为pthread_create()
readTask.detach(); // 底层为pthread_detach
// main线程用于接收用户输入,负责发送数据
for (;;)
{
// 显示首页面菜单:登录、注册、退出
cout << "======================" << endl;
cout << "1. login " << endl;
cout << "2. register" << endl;
cout << "3. quit" << endl;
cout << "======================" << endl;
cout << "please choice:";
int choice = 0;
cin >> choice;
cin.get(); // 读掉缓冲区残留的回车
switch (choice)
{
case 1: // login业务
{
// 获取用户的用户id和密码
int id = 0;
char pwd[50] = {0};
cout << "userid:";
cin >> id;
cin.get(); // 读掉缓冲区残留的回车
cout << "userpassword:";
cin.getline(pwd, 50);
// 组装json数据,将json数据对象序列化为字符串后通过网络发送给服务器
json js;
js["msgid"] = LOGIN_MSG;
js["id"] = id;
js["password"] = pwd;
string request = js.dump();
g_isLoginSuccess = false;
int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0);
if (len == -1)
{
cerr << "send login msg error:" << request << endl;
}
sem_wait(&rwsem); // 等待信号量,由子线程处理完登录的响应消息后通知主线程
if (g_isLoginSuccess) // 登录成功
{
// 进入聊天主菜单页面
isMainMenuRunning = true;
mainMenu(clientfd);
}
}
break;
case 2: // register业务
{
// 获取用户输入的用户名、密码
char name[50] = {0};
char pwd[50] = {0};
cout << "username:";
cin.getline(name, 50);
cout << "userpassword:";
cin.getline(pwd, 50);
// 组装json数据,将json数据对象序列化为字符串后通过网络发送给服务器
json js;
js["msgid"] = REG_MSG;
js["name"] = name;
js["password"] = pwd;
string request = js.dump();
int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0);
if (len == -1) // 响应失败
{
cerr << "send res msg error:" << request << endl;
}
sem_wait(&rwsem); // 等待信号量,由子线程处理完登录的响应消息后通知主线程
}
break;
case 3: // quit业务
{
close(clientfd);
sem_destroy(&rwsem);
exit(0);
}
default:
cerr << "invalid input!" << endl;
break;
}
}
return 0;
}
二、客户端业务封装
客户端业务整体封装(开闭原则设计): 用户登录成功后进入主聊天页面,给用户显示所有业务命令,用户选择完成业务进行相应处理。
设计流程如下:
1、用户刚开始使用客户端时并不知道客户端支持的常用命令,我们提供help()给予用户帮助提示如何使用。
2、用户输入相应业务命令,我们查询commandHandlerMap表,执行命令映射的方法即可,好处为后续可扩展性好。
3、依据用户输入的命令进行解析,若找到则调用相应命令的事件处理回调,处理相应业务;若未找到则给予用户错误提示。
业务封装代码如下:
//系统支持的客户端命令列表
unordered_map<string, string> commandMap = {
{"help", "显示所有支持的命令,格式help"},
{"chat", "一对一聊天,格式chat:friendid:message"},
{"addfriend", "添加好友,格式addfriend:friendid"},
{"creategroup", "创建群组,格式creategroup:groupname:groupdesc"},
{"addgroup", "加入群组,格式addgroup:groupid"},
{"groupchat", "群聊,格式groupchat:groupid:message"},
{"loginout","注销,格式loginout"}
};
//帮助信息:打印系统所支持的命令
void help(int fd = 0, string str = "")
{
cout << "show command list >>> " << endl;
for (auto &p : commandMap)
{
cout << p.first << " : " << p.second << endl;
}
cout << endl;
}
//一对一聊天:int接收sockfd,string接收用户发送的数据
void chat(int, string);
//添加好友:int接收sockfd,string接收用户发送的数据
void addfriend(int, string);
//创建群组:int接收sockfd,string接收用户发送的数据
void creategroup(int, string);
//加入群组:int接收sockfd,string接收用户发送的数据
void addgroup(int, string);
//群聊:int接收sockfd,string接收用户发送的数据
void groupchat(int, string);
//注销:int接收sockfd,string接收用户发送的数据
void loginout(int, string);
//注册系统支持的客户端命令处理
unordered_map<string, function<void(int,string)>> commandHandlerMap = {
{"help", help},
{"chat", chat},
{"addfriend", addfriend},
{"creategroup", creategroup},
{"addgroup", addgroup},
{"groupchat", groupchat},
{"loginout", loginout}
};
// 登录成功后主聊天页面程序:设计符合开闭原则
void mainMenu(int clientfd)
{
help();
char buffer[1024] = {0};
for (;;)
{
cin.getline(buffer, 1024); //获取用户输入:命令分为两种,有冒号的业务与无冒号的业务
string commandbuf(buffer);
string command; //存储命令
int idx = commandbuf.find(":");
if (-1 == idx) //无冒号
{
command = commandbuf;
}
else //有冒号
{
command = commandbuf.substr(0, idx);
}
auto it = commandHandlerMap.find(command);
if (it == commandHandlerMap.end()) //输入错误,未找到用户输入对应的业务
{
cerr << "invalid input command!" << endl;
continue;
}
//调用响应命令的事件处理回调,mainMenu对修改封闭,添加新功能不需要修改该函数
it->second(clientfd, commandbuf.substr(idx + 1,commandbuf.size() - idx)); //调用命令处理方法
}
}
三、添加好友业务
添加好友业务: 向服务器发送添加好友消息标识、当前用户id、要加好友的id组装的json数据。
//添加好友:int接收sockfd,string接收用户发送的数据
void addfriend(int clientfd, string str)
{
int friendid = atoi(str.c_str());
json js;
js["msgid"] = ADD_FRIEND_MSG;
js["id"] = g_currentUser.getId();
js["friendid"] = friendid;
string buffer = js.dump();
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str() + 1), 0);
if (-1 == len)
{
cerr << "send addfriend msg error -> " << buffer << endl;
}
}
简单测试一下:给张三添加一个李四好友,可以看到成功添加到好友列表中。
四、聊天业务
聊天业务: 解析用户输入的数据,向服务器发送聊天消息标识、当前用户id、名称、要聊天用户id、消息内容、时间组装的json数据。
//一对一聊天:int接收sockfd,string接收用户发送的数据
void chat(int clientfd, string str)
{
//解析用户输入的命令
int idx = str.find(":"); //friendid:message
if (-1 == idx)
{
cerr << "chat command invalid!" << endl;
return;
}
int friendid = atoi(str.substr(0, idx).c_str());
string message = str.substr(idx + 1, str.size() - idx);
json js;
js["msgid"] = ONE_CHAT_MSG;
js["id"] = g_currentUser.getId();
js["name"] = g_currentUser.getName();
js["toid"] = friendid;
js["msg"] = message;
js["time"] = getCurrentTime();
string buffer = js.dump();
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str()) + 1, 0);
if (-1 == len)
{
cerr << "send chat msg error -> " << buffer << endl;
}
}
简单测试一下:可以看到离线消息与在线消息均可以接收到。
五、群组业务
群组聊天业务: 主要业务包含创建群组、加入群组、群组聊天,都是通过客户端组装好json数据发送给服务器端。
5.1 创建群组
// 创建群组:int接收sockfd,string接收用户发送的数据
void creategroup(int clientfd, string str)
{
int idx = str.find(":");
if (-1 == idx)
{
cerr << "creategroup command invalid!" << endl;
return;
}
string groupname = str.substr(0, idx);
string groupdesc = str.substr(idx + 1, str.size() - idx);
json js;
js["msgid"] = CREATE_GROUP_MSG;
js["id"] = g_currentUser.getId();
js["groupname"] = groupname;
js["groupdesc"] = groupdesc;
string buffer = js.dump();
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str()) + 1, 0);
if (-1 == len)
{
cerr << "send creategroup msg error -> " << buffer << endl;
}
}
我们测试一下:用户1张三创建了一个英语学习群,角色为creator。
5.2 加入群组
// 加入群组:int接收sockfd,string接收用户发送的数据
void addgroup(int clientfd, string str)
{
int groupid = atoi(str.c_str());
json js;
js["msgid"] = ADD_GROUP_MSG;
js["id"] = g_currentUser.getId();
js["groupid"] = groupid;
string buffer = js.dump();
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str()) + 1, 0);
if (-1 == len)
{
cerr << "send addgroup msg error -> " << buffer << endl;
}
}
我们测试一下:用户2加入群组,角色为normal。
5.3 群组聊天
// 群聊:int接收sockfd,string接收用户发送的数据
void groupchat(int clientfd, string str)
{
int idx = str.find(":");
if (-1 == idx)
{
cerr << "groupchat command invalid!" << endl;
return;
}
int groupid = atoi(str.substr(0, idx).c_str());
string message = str.substr(idx + 1, str.size() - idx);
json js;
js["msgid"] = GROUP_CHAT_MSG;
js["id"] = g_currentUser.getId();
js["name"] = g_currentUser.getName();
js["groupid"] = groupid;
js["msg"] = message;
js["time"] = getCurrentTime();
string buffer = js.dump();
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str()) + 1, 0);
if (-1 == len)
{
cerr << "send groupchat msg error -> " << buffer << endl;
}
}
我们测试一下:用户1先发送群聊消息,发送完成后用户2登录收到离线消息,然后用户们在线群聊。可以看到群在线消息与离线消息可以正常进行收发。
六、用户注销业务
用户注销业务:
// 注销:int接收sockfd,string接收用户发送的数据
void loginout(int clientfd, string str)
{
json js;
js["msgid"] = LOGIN_OUT_MSG;
js["id"] = g_currentUser.getId();
string buffer = js.dump();
int len = send(clientfd, buffer.c_str(), strlen(buffer.c_str()) + 1, 0);
if (-1 == len)
{
cerr << "send loginout msg error -> " << buffer << endl;
}
else
{
isMainMenuRunning = false;
}
}
测试一下:我们先登录再退出,退出后重新进行登录,可以看到可以正常登入登出。