前言
上一篇文章我们简单了解了一下什么是套接字编程,这篇文章我们利用UDP套接字来实现一个简单的网络聊天室。
编写UDP套接字服务器
成员变量
// 1. socket的id,相当于文件id
int _sock;
// 2. port
uint16_t _port;
// 3 一个线程负责收放消息;另一个线程负责发消息
Thread *c; // 收消息
Thread *p; // 发消息
// 4 环形队列来实现生产消费
RingQueue<std::string> rq;
// 5 信息要群发给所有人,要记录所有人的ip和端口号
std::unordered_map<std::string, sockaddr_in> _users;
// 6 一把锁保证在读取要发送给哪些人时是安全的
// 在读取_users时,另一个线程收到了消息,会修改这个map
pthread_mutex_t _mmtx;
成员函数
- 构造函数:
今天我们知道,对于服务器而言,需要指定端口号。我们在初始化服务器的时候,对锁和线程都进行初始化。
UdpServer(uint16_t port = default_port) : _port(port)
{
pthread_mutex_init(&_mmtx,nullptr);
p = new Thread(1,std::bind(&UdpServer::Recv,this));
c = new Thread(1,std::bind(&UdpServer::BroadCast,this));
}
- 析构函数
析构函数完成资源释放,不要忘记等待进程。
~UdpServer()
{
pthread_mutex_destroy(&_mmtx);
p->join();
c->join();
delete p;
delete c;
}
- Start
void start()
{
// 1 创建socket接口,打开网络文件
_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (_sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKED_ERR);
}
std::cout << "create socket success" << std::endl;
// 2 给服务器指明IP地址和Port
struct sockaddr_in local;
memset(&local, sizeof(local), 0);
local.sin_family = AF_INET;
local.sin_port = htons(_port); // host to network short
local.sin_addr.s_addr = INADDR_ANY; // bind本主机任意ip
if (bind(_sock, (sockaddr *)&local, sizeof(local)) < 0)
{
std::cerr << "bind socket error" << std::endl;
exit(BIND_ERR);
}
std::cout << "bind socket success" << std::endl;
c->run();
p->run();
}
- addUser
因为UDP不面向连接,要构建一个聊天室,就必须要记录下曾经所有给服务器发送过消息的主机,这样才能转发信息。因为是向map里插入数据,本身是线程不安全的,需要加锁。
void addUser(std::string &name, sockaddr_in &peer)
{
LockGuard lg(&_mmtx);
_users[name] = peer;
}
- recv
接口细节我们已经在上一节着重说过,这里不做赘述。
void Recv()
{
char buffer[1024];
// 网络服务都是循环!
while (true)
{
// recv里有两个输出型参数
sockaddr_in peer;
socklen_t len = sizeof(peer);
int n = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&peer, &len);
if (n > 0)
{
buffer[n] = 0;
}
else
{
continue;
}
// 发消息人的ip和port
std::string client_ip = inet_ntoa(peer.sin_addr); // net->ascii
uint16_t client_port = ntohs(peer.sin_port);
// 构造了发消息人的姓名
std::string name = client_ip + "-" + to_string(client_port);
addUser(name, peer);
rq.push(name + buffer);
}
}
- broadcast
这里我们用了一个临时数组来存储要广播发送的主机,再向这个数组内的所有主机发送队列里的消息。那这样做有什么好处呢?
先把用户拷贝走,再让这个线程去独立发送数据,这样可以让使用map的其他线程更快得到锁,因为拷贝的过程要比发送数据快得多。
void BroadCast()
{
while (true)
{
std::string send_string;
rq.pop(&send_string);
std::vector<sockaddr_in> v; // 临时数组,存放要发给那些人
{
LockGuard lg(&_mmtx);
for (auto user : _users)
{
v.push_back(user.second);
}
}
for (auto user : v)
{
sendto(_sock, send_string.c_str(), send_string.size(), 0, (sockaddr *)&user, sizeof(user));
}
}
}
编写UDP客户端
客户端的编写和服务器端编写大同小异。有一个地方需要我们注意一下,套接字编程需要绑定ip和端口号,我们在服务端也确实这样做了,那么客户端需不需要bind端口号和ip呢
答案是肯定需要,只是这个工作不是由我们来干了,而是操作系统替我们做了,在第一次发送的时候自动绑定ip和端口号。
原因也是很好想的,对于客户端来说,端口号都是随机的,自然应该让操作系统帮忙。
void *recv(void *args)
{
int sock = *(static_cast<int *>(args));
char buffer[1024];
sockaddr_in temp;
socklen_t len = sizeof(temp);
while (true)
{
int n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&temp, &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cout << "请输入程序名 + 对方ip + 对方端口号" << std::endl;
exit(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = atoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "create socket error" << std::endl;
exit(SOCKED_ERR);
}
// 系统在我们第一次调用系统调用发送的时候,会自动给我绑定ip和端口号
// 填充server的信息
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());
//
pthread_t r_id;
pthread_create(&r_id, nullptr, recv, (void*)&sock);
while(true)
{
std::string message;
std::cout << "请输入..." << std::endl;
getline(std::cin,message);
sendto(sock,message.c_str(),message.size(),0,(sockaddr*)&server,sizeof(server));
}
pthread_join(r_id,nullptr);
return 0;
}
效果测试
这样一个简单的Udp服务器就完成了。