我们现在想要实现一个聊天功能,一个客户端向服务端发送信息,服务端收到信息,并把这个用户对应的信息保存起来,再把消息从服务端发送回客户端;此时我们再来一个用户,一样的操作,这时候两个客户端应该可以看到两个人发送的信息。
// 添加一个成员变量
std::unordered_map<std::string, struct sockaddr_in> _users; // IP-PORT : sockaddr_in
// 修改一下Start成员函数
char buffer[SIZE];
char key[64]; // 将ip-port写到key中
for (;;)
{
// ...
ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
if (s > 0)
{
// ...
snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port);
logMessage(NORMAL, "key: %s", key);
auto it = _users.find(key); // 把信息写到map中
if (it == _users.end())
{
logMessage(NORMAL, "add new user: %s", key);
_users.insert({key, peer});
}
}
// 写回数据
for (auto& iter : _users)
{
std::string sendMessage = key;
sendMessage += "# ";
sendMessage += buffer;
logMessage(NORMAL, "push message to %s", iter.first.c_str());
sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&iter.second, sizeof(iter.second));
}
}
想法很美好,但是现实往往和想象中的不一样,一开始还行,后面打印的都是什么东西,原因就是IO被阻塞了,就是当我们getline拿用户输入的数据的时候,后面的sendto和recvfrom都不会执行,所以现在就可以使用多线程,一个线程发数据,另一个线程负责收数据。
这就有一个要注意的点,不管是读数据还是写数据都要用sock,如果使用多线程就要把sock设置成全局的,或者再把客户端封装成一个类,成员变量对于整个类也是全局的。那会不会有线程安全的问题呢,这也是没有的,因为在多线程之前就要创建出socket,而线程只是用这个socket,并不会修改它,所以可以并发访问。
我们再把之前已经封装好的线程拿过来,这样sock会直接传入ThreadData中,所以也就不需要把sock定义成全局的。
#include "thread.hpp"
// port、ip
uint16_t serverport = 0;
std::string serverip;
// .udp_client ip port
static void usage(std::string proc)
{
std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}
static void *udpSend(void *args)
{
// 拿到sock
int sock = *(int *)((ThreadData *)args)->args_;
std::string name = ((ThreadData *)args)->name_;
// 填充服务端信息
std::string message;
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
while (true)
{
// 输入数据,发送
std::cerr << "请输入:";
std::getline(std::cin, message);
if (message == "quit")
break;
sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
return nullptr;
}
static void *udpRecv(void *args)
{
int sock = *(int *)((ThreadData *)args)->args_;
std::string name = ((ThreadData *)args)->name_;
char buffer[1024];
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (s > 0)
{
buffer[s] = 0;
std::cout << buffer << std::endl;
}
}
return nullptr;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
// 1.创建套接字
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error" << std::endl;
exit(2);
}
serverport = atoi(argv[2]);
serverip = argv[1];
std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock)); // 发送线程
std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock)); // 接收线程
sender->start();
recver->start();
sender->join();
recver->join();
close(sock);
return 0;
}
至此多线程就写好了,虽然用的socket都是同一个,但是没有读写冲突的情况,因为UDP是全双工的,可以同时进行收和发,不会受到干扰。
我们在目录下使用mkfifo创建两个管道文件client1和client2,将客户端输出重定向到管道文件,再使用cat输出重定向,这就好像类似于一个输入框和一个显示框。