【Linux网络】基于UDP的Socket编程,实现简单聊天室
前言:
上文我们讲到了,基于UDP的Dict Server的Socket编程。【Linux网络】Socket编程实战,基于UDP协议的Dict Server-CSDN博客
本文我们再来实现一个基于UDP的简单聊天室
实现思路
大体实现思路
客户端第一次向服务器发送消息,我们视为登录。
客户端给服务器发送消息,服务器要向消息转发给当前所有的在线用户,包括自己!
客户端不断的接收服务器的消息,实现聊天室的大致功能。
客户端实现思路
1.创建Socket,要bind端口与IP地址,但有操作系统自动绑定!不需要我们显式的绑定
2.使用sendto函数,向服务器发送信息
3.使用recvform函数,接收服务器的信息
4.客户端的发送信息与接收信息,必须使用多线程!不然发送信息与接收信息是串行的,将会导致必须发送信息,才能接收信息的逆天局面!!!
服务器实现思路
1.创建Socket,显式的绑定IP地址与端口号(IP地址绑定任意地址:INADDR_ANY。让任意客户端都可以访问服务器)
2.使用recvform函数,接收服务器的信息。
3.调用对应的方法,将接收到的信息转发给所有在线用户
转发模块实现思路
1.使用vector,作为在线用户表的容器
2.只要服务器调用了该方法,那么执行以下步骤:
判断当前用户是否在在线用户表中;若不在,则进行登录操作:将该用户压入用户表中
若当前用户在在线用户表中,则将该用户发送的信息,转发给全部在线用户(包括自己)
判断用户发送的信息是否为"QUIT",若是则表明用户要退出,将当前用户从在线用户表中删除
代码实现
客户端代码实现
//UdpClient.cc#include "Log.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include "Thread.hpp"
using namespace ThreadModule;
using namespace LogModule;// 客户端也要进行多线程的改造
// 不然客户端,将会出现不发现信息,就不能接收信息的逆天局面// 全局变量
struct sockaddr_in local;
int sockfd;
pthread_t tid = 0;
bool get_quit = false;// 标准输出、标准错误 + 命令行的输出重定向:实现一个窗口显示输入信息,一个显示接收信息// 发送信息
void Send()
{while (!get_quit){// 向服务器发送信息cout << "Please Cin # "; // 1,标准输出std::string buff;cin >> buff; // 0,标准输入// std::getline(std::cin, buff);// buff.size()-1 会丢失最后一个字符,应改为 buff.size()ssize_t s = sendto(sockfd, buff.c_str(), buff.size(), 0, (struct sockaddr *)&local, sizeof(local));if (s < 0){LOG(LogLevel::WARNING) << "向服务器发送信息失败";exit(1);}}
}// 接收信息
void Receive()
{while (!get_quit){// 接收服务器返回的信息char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t ss = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);if (ss < 0){LOG(LogLevel::WARNING) << "接收服务器信息失败";exit(1);}// printf("%s\n", buffer);buffer[ss] = 0;cerr << buffer << endl; // 2,标准错误// 判断是否退出if (strcmp(buffer, "QUIT") == 0){get_quit = true;LOG(LogLevel::INFO) << "get_quit = true";break;}}
}// 给出 ip地址 端口号
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Please use: " << argv[0] << " IP " << "PORT " << endl;exit(1);}uint32_t ip = inet_addr(argv[1]); // 注:字符串转合法ip地址uint16_t port = stoi(argv[2]); // 注:字符串转整数// 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){LOG(LogLevel::FATAL) << "创建套接字失败";exit(1);}LOG(LogLevel::INFO) << "创建套接字";// 绑定?不用显示绑定,OS会自动的绑定// 填写服务器信息memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_addr.s_addr = ip;local.sin_port = htons(port);// 创建线程Thread send(Send);Thread receive(Receive);tid = send.Tid();// 执行各自的任务:发送信息、接收信息send.Start();receive.Start();// 等待:需要进行等待,不然主线程结束,整个进程就都结束了,连同子线程也会被强制结束send.Join();receive.Join();
}
服务器代码实现
//UdpServer.hpp#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <functional>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;class udpserver
{using func_t = function<void(int &, string, InetAddr &)>;public:udpserver(uint16_t port, func_t func)// : _addr(inet_addr(addr.c_str())), // 注:直接将其转换为合法的ip地址: _port(port),_func(func){_running = false;}// 初始化:1.创建套接字 2.填充并绑定地址信息void Init(){// 1.创建套接字// 返回套接字描述符 地址族 数据类型 传输协议_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(LogLevel::FATAL) << "创建套接字失败!";exit(1);}LOG(LogLevel::INFO) << "创建套接字";// 2.绑定信息// 2.1填充信息struct sockaddr_in local;// 将指定内存块的所有字节清零bzero(&local, sizeof(local));local.sin_family = AF_INET; // IPv4地址族// local.sin_addr.s_addr = _addr; //IP地址(主机序列转化为网络序列)local.sin_addr.s_addr = INADDR_ANY; // 赋值为INADDR_ANY,表示任意地址local.sin_port = htons(_port); // 端口号// 2.2绑定信息int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(LogLevel::FATAL) << "绑定失败";exit(1);}LOG(LogLevel::INFO) << "绑定成功";}// 启动运行:一直运行不停止;1.接收客户端的信息 2.对客户端发送来的信息进行回显void Start(){// 一定是死循环_running = true;while (_running){// 接收客户端的信息char buff[1024];struct sockaddr_in peer;unsigned int len = sizeof(peer);// 套接字描述符,数据存放的缓冲区,接收方式:默认,保存发送方的ip与端口,输入输出参数:输入peer的大小,输出实际读取的数据大小ssize_t s = recvfrom(_sockfd, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);// 显示发送方的ip与protInetAddr iaddr(peer);cout << iaddr.ip() << " : " << iaddr.prot() << " : ";// 显示发送的信息buff[s] = 0;printf("%s\n", buff);// 回显消息if (s > 0){// 调用自定义方法_func(_sockfd, string(buff), iaddr);// 将数据发送给客户端// 套接字描述符,要发送的信息,发送方式:默认,接收方的ip与端口信息// ssize_t t = sendto(_sockfd, ss.c_str(), ss.size(), 0, (struct sockaddr *)&peer, len);// if (t < 0)// {// LOG(LogLevel::WARNING) << "信息发送给客户端失败";// }}memset(&buff, 0, sizeof(buff)); // 清理缓存}}private:int _sockfd;uint32_t _addr;uint16_t _port;bool _running;// 回调方法func_t _func;
};
//UdpServer.cc#include "UdpServer.hpp"
#include <cstdlib>
#include "InetAddr.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;// 实现聊天:
// 服务器端,要将从客户端收到的信息,转发给说有在线用户!// 给出 端口号
int main(int argc, char* argv[])
{if (argc != 2){std::cout << "Please use: " << argv[0] << " PORT" << endl;}else{// 1.数据路由模块Route route;udpserver us(port, [&route](int &sockfd, std::string message, InetAddr &addr){ return route.MessageRoute(sockfd, message, addr); });us.Init();us.Start();}
}
//InetAddr.hpp#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>// 实现网络地址与主机地址的转换class InetAddr
{
public:InetAddr(struct sockaddr_in &addr): _addr(addr){_prot = ntohs(_addr.sin_port); // 网络地址转主机地址_ip = inet_ntoa(_addr.sin_addr); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP}uint16_t prot(){return _prot;}string ip(){return _ip;}// 运算符重载bool operator==(InetAddr &addr){return _prot == addr._prot && _ip == addr._ip;}string Getname(){return _ip + ':' + to_string(_prot);}private:struct sockaddr_in _addr;uint16_t _prot;std::string _ip;
};
转发模块代码实现
//Route.hpp// 消息路由模块:将客户端的信息转发给所有在线用户#pragma once
#include <iostream>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"
using namespace LogModule;class Route
{
private:bool IsExit(InetAddr &addr){for (auto &e : _online_user){if (e == addr)return true;}return false;}void AddUesr(InetAddr &addr){_online_user.push_back(addr);LOG(LogLevel::INFO) << "用户:" << addr.Getname() << "登录";}void DeleteUser(InetAddr &addr){for (auto it = _online_user.begin(); it < _online_user.end(); it++){if (addr == *it){// erase参数只能是迭代器_online_user.erase(it);LOG(LogLevel::INFO) << "用户" << it->Getname() << "退出登录";}}}public:void MessageRoute(int sockfd, std::string &message, InetAddr &addr){// 判断当前用户是否在,在线用户中if (!IsExit(addr)){// 没在,则添加AddUesr(addr);}// 当前用户一定在// 将当前用户的消息转发给所有人,包括自己for (auto &user : _online_user){sendto(sockfd, message.c_str(), message.size(), 0, (const struct sockaddr *)&user, sizeof(user));}// 输入QUIT,表示用户退出if (message == "QUIT"){DeleteUser(addr);}}private:// 首次发消息,视为登录std::vector<InetAddr> _online_user;
};
服务器优化
上述,我们实现服务器的单线程的!那么存在多个用户同时登录、发消息退出登录的操作吗?当然存在!
所以,我们要对服务器进一步的优化!既:将服务器优化为多线程的,这里我们采用进程池进行优化~
#include "UdpServer.hpp"
#include <cstdlib>
#include "InetAddr.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
using namespace ThreadPoolModule;// 实现聊天:
// 服务器端,要将从客户端收到的信息,转发给说有在线用户!// 给出 端口号
int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Please use: " << argv[0] << " PORT" << endl;}else{// 相当难理解!!!!!!! 不中了// 1.数据路由模块Route route;// 2.进程池模块using func_t = function<void()>;ThreadPool<func_t> threadpool(5);// 3. 网络服务器对象,提供通信功能uint16_t port = stoi(argv[1]); // 注:字符串转整数// 启动进程池threadpool.Start();std::unique_ptr<udpserver> usvr = std::make_unique<udpserver>(port, [&route, &threadpool](int &sockfd, std::string message, InetAddr &peer){func_t t = std::bind(&Route::MessageRoute,&route,sockfd, message, peer);threadpool.Equeue(t); });// 启动服务端usvr->Init();usvr->Start();}
}
补充
地址转换函数
IP地址转换函数:inet_ntop(网络转主机)、inet_pton(主机转网络)
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);参数说明:
af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)
src:指向点分十进制字符串的指针(如 "192.168.1.1")
dst:指向存储转换后二进制地址的缓冲区指针
返回值:
1:转换成功
0:输入不是有效的 IP 地址字符串
-1:地址族不支持或其他错误
#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);参数说明:
af:地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)
src:指向网络字节序二进制地址的指针
dst:指向存储转换后字符串的缓冲区指针
size:缓冲区大小
返回值:
成功:返回指向转换后字符串的指针
失败:返回 NULL
端口号转换函数:ntohs、ntohl(网络转主机);htons、htonl(主机转网络)
优化地址转化模块:InetAddr.hpp
//InetAddr.hpp#pragma once
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>// 实现网络地址与主机地址的转换class InetAddr
{
public:// 网络转主机InetAddr(struct sockaddr_in &addr): _addr(addr){_prot = ntohs(_addr.sin_port); // 网络地址转主机地址char buff[1024];inet_ntop(AF_INET, &addr.sin_addr, buff, sizeof(buff)); // 将4字节网络风格的IP -> 点分十进制的字符串风格的IP_ip = std::string(buff);}// 主机转网络InetAddr(std::string ip, uint16_t prot): _ip(ip),_prot(prot){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;//&addr.sin_addr 是一个指向 struct in_addr 的指针,其内存地址等价于 &(addr.sin_addr.s_addr)(因为结构体的起始地址就是第一个成员的起始地址)inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_prot);}// 直接获取sockaddr_insockaddr_in *Getaddr(){return &_addr;}uint16_t prot(){return _prot;}std::string ip(){return _ip;}// 运算符重载bool operator==(InetAddr &addr){return _prot == addr._prot && _ip == addr._ip;}std::string Getname(){return _ip + ':' + std::to_string(_prot);}private:struct sockaddr_in _addr;uint16_t _prot;std::string _ip;
};