客户端注销后再次登录,失败了。这种假死的场景在Ubuntu比较小概率,但是在centos大概率会发生。
我们gdb去调试看看是什么原因:
gdb attach 4207
我们的客户端有2个线程:一个专门读(主线程,接收客户端用户的输入,然后发送),一个专门写(子线程,recv接收,打印信息)
但是,我们现在看到,这2个线程都在recv
主线程阻塞在这里:
很明显,我们的子线程也在recv上,
我们看到,子线程也阻塞在recv
也就是说,现在的现象是:客户端刚才登录成功,然后注销退出,然后重新登录,客户端没反应了。
我们是通过gdb attach到正在运行的进程上,大致的看所有的线程和线程调用堆栈的详细信息,就可以知道现在的任务是阻塞在哪里了。
现在的情况是:接收线程(读线程)现在在recv处阻塞了,主线程(写线程)也是在它对应的recv处阻塞了。
我们刚开始进行登录的时候,打包了登录的json包数据发送到服务器,
然后就在recv处阻塞,等待服务器对登录的消息进行响应,客户端得到响应后,对发送过来的json数据进行反序列化。
拿到相应的字段进行业务的处理,然后启动子线程,登录成功后,把读线程启动
进入mainMenu,进行登录时的信息的打印
然后现在注销了,并没有把子线程关闭掉,注销以后,我们再进行登录的时候,主线程去send,send后主线程马上recv,子线程此时也在recv,因为子线程的代码是无限的for循环,不断的recv。
实际上,我们是想要主线程发的登录消息,由主线程去接收它的响应,接收了以后就不会阻塞了,就继续向下执行,但是,第二次send的时候,由于子线程抢到了recv,因为主线程和子线程都是处理的是同一个clientfd,子线程把这个数据recv了,但是子线程的代码没有相应的消息类型的处理啊,子线程只有单聊和群聊的消息类型的操作,然后子线程转了一圈,又阻塞在recv了,而主线程在recv登录的响应,却被子线程接收了,所以导致主线程的recv并没有接收到任何响应,一直阻塞住。
彻底的把线程的接收分离开。
主线程:发送线程
子线程:接收线程
我们把客户端创建socket,connect服务器成功以后,我们就把子线程启动,而不是等登录之后才启动子线程。
TCP连接都创建成功了,就把子线程启动就好了,子线程专门做for无限循环做recv接收服务器的响应消息,专门做读操作。
然后主线程进来首界面(1登录,2注册,3退出)
主线程做的事情是:登录:提升用户输入id和密码。打包数据进行json的序列化,然后send,通过网络发送到服务器,服务器响应,统一是由子线程进行接收,子线程接收响应消息的时候可以根据消息的类型识别这是登录的响应消息,主线程在send以后,应该等一等:等待登录是成功还是登录失败,这是在子线程进行判断的,所以,我们要添加通知机制,主线程(发送线程)send之后要进行等待,然后子线程来recv接收这个登录的响应消息,然后根据登录的响应消息:成功或者失败,去写一些标志,然后通过信号量,告诉主线程,(线程间的通信),然后主线程继续往下走,通过一个变量判断登录成功还是失败,登录失败的话,继续转到首界面,登录成功的话,转到主界面。
C++11提供了互斥锁和条件变量,但是没有提供信号量,所以我们使用Linux原生的pthread库的信号量
除了增加信号量(负责通知主线程,子线程已经把响应处理完了),我们还要增加一个标志(登录成功还是失败),这个标志变量定义成原子类型(C++11 CAS,自带volitale属性),因为在2个线程中要使用。
//控制主菜单页面程序
bool isMainMenuRunning = false;
//用于读写线程之间的通信
sem_t rwsem;
//记录登录状态
atomic_bool g_isLoginSuccess{false};
//聊天客户端程序实现,main线程用作发送线程,子线程用作接收线程
int main(int argc, char **argv)
{
if (argc < 3)//判断命令的个数,客户端启动要输入命令行的ip地址和端口port
{
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 << "choice:";
int choice = 0;
cin >> choice;//输入选择,整数,我们在输入的时候有加回车
cin.get();//读掉缓冲区残留的回车
switch (choice)
{
case 1://login登录业务
{
int id = 0;
char pwd[50] = {0};
cout << "userid:";
cin >> id;
cin.get();//读掉缓冲区残留的回车
cout << "userpassword:";
cin.getline(pwd, 50);
//客户端和服务器要保持语言一致!!!
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);//cin>>S有回车才结束,遇见空格就结束一个参数的输入
cout << "userpassword:";
cin.getline(pwd, 50);
json js;
js["msgid"] = REG_MSG;
js["name"] = name;
js["password"] = pwd;
string request = js.dump();//json对象序列化成字符串
int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0);
if (len == -1)//注册失败
{
cerr << "send reg msg error:" << request << endl;
}
sem_wait(&rwsem); //等待信号量,子线程处理完注册消息会通知
}
break;
case 3://quit退出业务,因为还没登录,就退出
close(clientfd);//关闭描述符
sem_destroy(&rwsem);
exit(0);
default://除了1,2,3
cerr << "invalid input!" << endl;
break;
}
}
return 0;
}
//处理登录的响应逻辑
void doLoginResponse(json &responsejs)
{
if (0 != responsejs["errno"].get())//登录失败
{
cerr << responsejs["errmsg"] << endl;
g_isLoginSuccess = false;
}
else//登录成功
{
//记录当前用户的id和name
g_currentUser.setId(responsejs["id"].get());
g_currentUser.setName(responsejs["name"]);
//记录当前用户的好友列表信息
if (responsejs.contains("friends"))
{
//初始化
g_currentUserFriendList.clear();
//重新装入,新的登录
vector vec = responsejs["friends"];
for (string &str : vec)
{
json js = json::parse(str);
User user;
user.setId(js["id"].get());
user.setName(js["name"]);
user.setState(js["state"]);
g_currentUserFriendList.push_back(user);
}
}
//记录当前用户的群组列表信息
if (responsejs.contains("groups"))
{
//初始化
g_currentUserGroupList.clear();
vector vec1 = responsejs["groups"];
for (string &groupstr : vec1)
{
json grpjs = json::parse(groupstr);
Group group;
group.setId(grpjs["id"].get());
group.setName(grpjs["groupname"]);
group.setDesc(grpjs["groupdesc"]);
vector vec2 = grpjs["users"];
for (string &userstr : vec2)
{
GroupUser user;
json js = json::parse(userstr);
user.setId(js["id"].get());
user.setName(js["name"]);
user.setState(js["state"]);
user.setRole(js["role"]);
group.getUsers().push_back(user);
}
g_currentUserGroupList.push_back(group);
}
}
//显示登录用户的基本信息
showCurrentUserData();
//显示当前用户的离线消息 个人聊天信息或者群组消息
if (responsejs.contains("offlinemsg"))
{
vector vec = responsejs["offlinemsg"];
for (string &str : vec)
{
json js = json::parse(str);
// time + [id] + name + " said: " + xxx
if (ONE_CHAT_MSG == js["msgid"].get())
{
cout << js["time"].get() << " [" << js["id"] << "]" << js["name"].get()
<< " said: " << js["msg"].get() << endl;
}
else
{
cout << "群消息[" << js["groupid"] << "]:" << js["time"].get() << " [" << js["id"] << "]" << js["name"].get()
<< " said: " << js["msg"].get() << endl;
}
}
}
g_isLoginSuccess = true;
}
}
//处理注册的响应逻辑
void doRegResponse(json &responsejs)
{
if (0 != responsejs["errno"].get())//注册失败
{
cerr << "name is already exist, register error!" << endl;
}
else//注册成功
{
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);
}
//接收ChatServer转发的数据,反序列化生成json数据对象
json js = json::parse(buffer);
int msgtype = js["msgid"].get();
if (ONE_CHAT_MSG == msgtype)//如果是聊天消息的话
{
cout << js["time"].get() << " [" << js["id"] << "]" << js["name"].get()
<< " said: " << js["msg"].get() << endl;
continue;
}
if (GROUP_CHAT_MSG == msgtype)//如果是群组消息
{
cout << "群消息[" << js["groupid"] << "]:" << js["time"].get() << " [" << js["id"] << "]" << js["name"].get()
<< " said: " << js["msg"].get() << endl;
continue;
}
if (LOGIN_MSG_ACK == msgtype)//登录响应消息
{
doLoginResponse(js);//处理登录响应的业务逻辑
sem_post(&rwsem);//通知主线程,登录结果处理完成
continue;
}
if (REG_MSG_ACK == msgtype)//注册响应消息
{
doRegResponse(js);
sem_post(&rwsem);//通知主线程,注册结果处理完成
continue;
}
}
}



