聊天客户端实现
这一部分不是本项目的重点,且客户端并没有高并发等这些要求,所以只是做了一个差不多的程序方便测试。注意服务器处理对json数据解析的字段名要和客户端封装json数据的字段一一对应。
其中客户端程序中,main函数所在的主线程只用于发送,子线程用于接收数据,当客户端连接上服务器后就启动子线程进行轮询处理接收到的数据进行相应的处理。
主线程初始连接到服务端,然后显示首页面菜单,若选择登录且成功之后则进入用户首页面菜单。
主线程中发送出请求的json
数据之后,就使用一个信号量进行 sem_wait
在原地进行阻塞等待,子线程中进行doRegResponse
或者doLoginResponse
的逻辑处理,处理完成之后使用 sem_post
进行唤醒前面的主线程阻塞。重新显示首页面菜单,进行功能选择。
// 聊天客户端程序实现,主线程做发送线程,子线程用作接收线程
int main(int argc, char **argv)
{
if (argc < 3)
{
cerr << "command invalid ! example: ./ChatClient 127.0.0.1 6000";
exit(-1);
}
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);
}
//
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);
readTask.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); // 等待信号量,由子线程处理完登录的响应,notify唤醒这里
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); // 用getline可以使得输入的字符串中间可以有空格
cout << "userpassword:";
cin.getline(pwd, 50);
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 login 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;
}
// 子线程————接收线程
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);
}
// cout << buffer << endl;
// 接收ChatServer转发的数据,反序列化生成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 << "群消息[" << 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); // 通知主线程,登录结果处理完成
}
if (REG_MSG_ACK == msgtype) // 接收到注册消息的响应
{
doRegResponse(js); // 处理注册响应的业务逻辑
sem_post(&rwsem); // 通知主线程,登录结果处理完成
continue;
}
}
}
杂项
- 用getline可以使得输入的字符串中间可以有空格