引言
为了刚好的理解网络编程,本篇文章实现了网络聊天室的客户端代码实现。而服务器端的实现,大家可以阅读我之前的文章
网络聊天室服务器的代码实现-CSDN博客
这个代码的架构在上一篇文章里面我们已经讲述了,所以这里就不再陈述了
代码
这里提供了客户端的架构,客户端要接受信息,和发送信息,所以我们对于一个客户端需要两个线程,因为我们在发送信息的同时也可以接受到信息,所以我们一个线程来发送信息,一个线程来接受信息。正是因为有两个线程,所以我们的running标志我们选择的是原子变量,防止一个线程修改了,另一个线程因为CPU调度的问题仍然在执行。
#ifndef CHATCLIENT_H
#define CHATCLIENT_H
#include <string>
#include <vector>
#include <thread>
#include <queue>
#include <condition_variable>
#include <mutex>
#include <functional>
#include <cstring>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
#include <atomic>
#define N 128 // 消息缓冲区的大小
#define LOGIN 1 // 表示登录消息的类型
#define CHAT 2 // 表示聊天消息的类型
#define QUIT 3 // 表示退出消息的类型
namespace LchatClient {
class chatClient {
public:
// 定义一些公共类型
struct MSG {
int type;
char name[20];
char text[N];
std::string serialize() const {
std::string data;
data.append(reinterpret_cast<const char *>(&type), sizeof(type));
data.append(name, sizeof(name));
data.append(text, sizeof(text));
return data;
}
void deserialize(const std::string& data) {
int offset = 0;
memcpy(&type, data.data() + offset, sizeof(type));
offset += sizeof(type);
memcpy(name, data.data() + offset, sizeof(name));
offset += sizeof(name);
memcpy(text, data.data() + offset, sizeof(text));
}
};
private:
int cfd;
std::string name; // 客户端的用户名
std::atomic<bool> running; // 客户端是否在运行中
void errlog(const char* msg); // 错误日志函数
void sendMsg(int type, const std::string& text = ""); // 发送消息函数
void recvMsg(); // 接收消息函数
public:
chatClient(const char* ip, int port, const std::string& name);
~chatClient();
void run(); // 运行客户端
};
}
#endif
对于构造函数里面,我们要传入的是客户端的ip地址,端口号,和客户端的名字,然后创建一个套接字用来连接服务器,最后给服务器发送一个登录的信息
而对于析构函数来说,也就是这个客户端下线了,那么就要发送一个QUIT的消息给服务器,告诉这个客户端是正常的退出。
chatClient::chatClient(const char* ip, int port, const std::string& name) : name(name)
{
running.store(true);
// 创建客户端套接字
cfd = socket(AF_INET, SOCK_STREAM, 0);
if (cfd < 0) {
errlog("socket error");
return;
}
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.s_addr = inet_addr(ip);
if(connect(cfd, (sockaddr *)&sin, sizeof(sin)) < 0) {
errlog("connect error");
return;
}
// 连接成功,向服务器发送登录消息
sendMsg(LOGIN, name);
}
chatClient::~chatClient() {
sendMsg(QUIT);
close(cfd);
}
这个是客户端的运行函数,首先为了在发消息的时候可以接受到别人的消息,所以我们会多创建一个线程,这个线程执行的就是recvMsg函数。然后在主线程里面,我们while()循环的目的就是不断地判断这个客户端是否退出,如果没有退出,那么我们就发送信息,如果退出,就执行我们地另一个函数。最后我们在while()退出之后,因为有子线程在接受消息,所以调用join()阻塞函数,去回收子线程。
还要注意地一个就是每次发送消息,都要单独传递这个消息地类型,这个我们之后说
void chatClient::run()
{
// 创建一个分支线程,用于接受服务器端的数据
std::thread recvThread(&chatClient::recvMsg, this);
// 主线程向服务器发送消息
std::string text;
while (running.load()) {
std::getline(std::cin, text);
if (text == "quit") {
sendMsg(QUIT, "quit");
running.store(false);
break;
}
sendMsg(CHAT, text);
}
recvThread.join();
}
我们之所以要单独传递消息的类型是因为消息的类型是int,而在网络传递之中,因为大端存储和小端存储的问题,int需要被转化为网络字节序,统一处理。所以大家可以看到在接受这个信息之后,服务器端提取出了msg.type,然后把这个转化成了ntohs(msg.type),然后用strncpy函数给每一个结构体的数据赋值,这个text是我们传递的参数,而name是我们每一个主机自己私有的名称
最后用data接受序列化的msg,然后发送给主机
void chatClient::sendMsg(int type, const std::string& text)
{
MSG msg;
msg.type = htonl(type);
strncpy(msg.name, name.data(), sizeof(msg.name));
strncpy(msg.text, text.data(), sizeof(msg.text));
std::string data = msg.serialize();
if (send(cfd, data.data(), data.size(), 0) < 0) {
errlog("send error");
return;
}
std::cout << "消息发送成功" << std::endl;
}
这个是接受函数,接受函数首先就是用一个容器接受recv进来的数据,然后用msg接受这个信息(反序列化),最后打印到终端上。
void chatClient::recvMsg()
{
char buf[sizeof(MSG)];
while(running.load()) {
int recv_len = recv(cfd, buf, sizeof(MSG), 0);
if (recv_len <= 0) {
errlog("recv error");
running.store(false);
return;
}
MSG msg;
msg.deserialize(std::string(buf, recv_len));
std::cout << msg.name << ": " << msg.text << std::endl;
}
}
这个是创建一个客户端的主函数,传递的参数就是ip地址,端口号,用户名
int main(int argc, char* argv[]) {
if (argc < 4) {
std::cout << "请输入ip地址、端口号和用户名" << std::endl;
return -1;
}
chatClient client(argv[1], atoi(argv[2]), argv[3]);
client.run();
return 0;
}
总结
网络聊天室的所有代码实现就到这里结束了!!!希望可以帮助大家更好的理解网络编程~~~



