简单的回显服务器和客户端代码。
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Common.hpp"
using namespace LogModule;
using namespace InetAddrModule;
const static int gsockfd = -1;
const static uint16_t gdefaultport = 8080;
class UdpServer
{
public:
UdpServer(uint16_t port = gdefaultport)
: _sockfd(gsockfd),
_addr(port),
_isrunning(false)
{
}
void InitServer()
{
// 1. 创建socket
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;
// 2. bind : 设置进入内核中
int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
Die(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
bool Start()
{
_isrunning = true;
while (true)
{
char inbuffer[1024]; // string
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须设定
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer),&len);
if(n > 0)
{
inbuffer[n] = 0;
InetAddr client(peer);//获取cilent相关信息
std::string cilentmessage = client.Ip()+":"+std::to_string(client.Port())+"# "+ inbuffer;
LOG(LogLevel::DEBUG)<<cilentmessage;
//将获取到的信息写回client
std::string echo_string = "echo# ";
echo_string += inbuffer;
::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,client.NetAddr(),client.NetAddrLen());
}
}
_isrunning = false;
}
~UdpServer()
{
if (_sockfd > gsockfd)
::close(_sockfd);
}
private:
InetAddr _addr; // 服务器地址包括ip和port
int _sockfd;
bool _isrunning; // 服务器运行状态
};
#endif
要使用网络服务器需要使用socket创建套接字,然后将IP和端口号bind进入内核,最后就可以调用recv/sendto接口进行网络发送和接收信息了
服务器使用代码:
#include "UdpServer.hpp"
// ./server_udp localport
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY();
std::unique_ptr<UdpServer> svr_uptr;
if (argc == 2)
{
uint16_t port = std::stoi(argv[1]);
svr_uptr = std::make_unique<UdpServer>(port);
}
else
svr_uptr = std::make_unique<UdpServer>();
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
服务器需要输入端口号,不输入也行,服务器默认端口号为8080
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;
using namespace InetAddrModule;
int sockfd = -1;
//./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if(argc!=3)
{
LOG(LogLevel::ERROR)<<"Usage:" << argv[0] << " serverip serverport" ;
Die(ARGV_ERR);
}
//1.创建sockfd
sockfd = ::socket(AF_INET,SOCK_DGRAM,0);
if(sockfd < 0)
{
LOG(LogLevel::WARNING)<<"client sockfd fail...";
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO)<<"client sockfd success...";
//2.填充服务器信息
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
InetAddr ServerAddr(serverip,serverport);
//3.发送请求给服务器
while(true)
{
//3.1获取信息
std::cout << "Please Enter# ";
std::string message;
std::getline(std::cin, message);
//3.2发送信息给服务器
ssize_t n = ::sendto(sockfd,message.c_str(),sizeof(message),0,ServerAddr.NetAddr(),ServerAddr.NetAddrLen());
if(n < 0)
{
LOG(LogLevel::ERROR)<<"client sendto fail...";
continue;
}
//3.3从服务器接收信息
char buffer[1024];
struct sockaddr_in tmp;
socklen_t len = sizeof(tmp);
ssize_t m = ::recvfrom(sockfd,buffer,sizeof(buffer)-1,0,CONV(&tmp),&len);
if(m > 0)
{
buffer[m] = 0;
std::cout<<buffer<<std::endl;
}
else
{
LOG(LogLevel::ERROR)<<"client recvfrom fail...";
}
}
return 0;
}
同样,客户端要进行网络通信也需要创建套接字,但是不需要bind信息进入内核,因为在接收到网络信息时会自动进行bind
客户端需要输入服务器IP地址和端口号port
因为在进行网络通信时不可避免的需要频繁使用到相关信息,所以我们可以考虑将它们封装成为一个类,设置一些常用的方法
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
namespace InetAddrModule
{
class InetAddr
{
private:
void PortNet2Host() // port网络转主机
{
_port = ::ntohs(_net_addr.sin_port);
}
void IpNet2Host() // IP网络转主机
{
char ipbuffer[64];
const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
_ip = ip;
}
public:
InetAddr()
{
}
InetAddr(const struct sockaddr_in &addr) : _net_addr(addr) // 获取传来的sockaddr的ip和port
{
PortNet2Host();
IpNet2Host();
}
InetAddr(uint16_t port) : _port(port), _ip("")
{
_net_addr.sin_family = AF_INET;
_net_addr.sin_port = htons(_port);//主机转网络
_net_addr.sin_addr.s_addr = INADDR_ANY;//表示可以介绍任何ip地址
}
InetAddr(const std::string& ip,uint16_t port) : _port(port), _ip(ip)
{
_net_addr.sin_family = AF_INET;
_net_addr.sin_port = htons(_port);//主机转网络
_net_addr.sin_addr.s_addr = ::inet_addr(ip.c_str());
}
struct sockaddr *NetAddr() { return CONV(&_net_addr); }
socklen_t NetAddrLen() { return sizeof(_net_addr); }
std::string Ip() { return _ip; }
uint16_t Port() { return _port; }
~InetAddr()
{
}
private:
struct sockaddr_in _net_addr;
std::string _ip;
uint16_t _port;
};
}
因为各种机器之间不兼容等例如大端/小端模式,所以在实际进行网络通信时我们需要将发送的ip地址和端口号从主机模式转换为网络模式。
在网络编程中,当一个进程需要绑定一个网络端口以进行通信时,可以使用INADDR_ANY 作为 IP 地址参数。这样做意味着该端口可以接受来自任何 IP 地址的连接请求,无论是本地主机还是远程主机。例如,如果服务器有多个网卡(每个网卡上有不同的 IP 地址),使用 INADDR_ANY 可以省去确定数据是从服务器上具体哪个网卡/IP 地址上面获取的。
结果如下:
上述echo server仅仅是将收到的消息回显给客户端,其实我们还可以在服务器中加一点业务处理,比如翻译功能。
所以我们可以创建一个Dictionary
类,将翻译词典封装起来:
Dict.txt:
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
Dict.hpp:
#pragma once
#include <iostream>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
namespace DictionaryModule
{
const std::string sep = ": "; // 分割符
using namespace LogModule;
class Dict
{
private:
//将词典内容从Dict.txt中加载进来
void DownloadDict()
{
std::ifstream in(_dictpath);
if (!in.is_open())
{
LOG(LogLevel::WARNING) << "DownloadDict fail...";
return;
}
std::string line;
while (getline(in, line))
{
if (line.empty())
continue;
// 加入词典
size_t pos = line.find(sep);
_dict.insert({line.substr(0, pos), line.substr(pos + sep.size())});
}
}
public:
Dict(const std::string &dictpath = "./Dict.txt") : _dictpath(dictpath)
{
DownloadDict(); // 加载词典
}
std::string Translate(const std::string &key)
{
auto iter = _dict.find(key);
if (iter == _dict.end())
return std::string("Unknown");
else
return iter->second;
}
~Dict()
{
}
private:
std::string _dictpath;
std::unordered_map<std::string, std::string> _dict;
};
}
有了翻译的功能后,我们就可以将其嵌入服务器内部使用,所以我们在UdpServer
类成员中添加一个回调方法,并在Start
函数中使用:
#ifndef __UDP_SERVER_HPP__
#define __UDP_SERVER_HPP__
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"
#include "Common.hpp"
using namespace LogModule;
using namespace InetAddrModule;
using func_t = std::function<std::string(const std::string&)>;
const static int gsockfd = -1;
const static uint16_t gdefaultport = 8080;
class UdpServer
{
public:
UdpServer(func_t func,uint16_t port = gdefaultport)
: _sockfd(gsockfd),
_addr(port),
_isrunning(false),
_func(func)
{
}
void InitServer()
{
// 1. 创建socket
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
Die(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "socket success, sockfd is : " << _sockfd;
// 2. bind : 设置进入内核中
int n = ::bind(_sockfd, _addr.NetAddr(), _addr.NetAddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
Die(BIND_ERR);
}
LOG(LogLevel::INFO) << "bind success";
}
bool Start()
{
_isrunning = true;
while (true)
{
char inbuffer[1024]; // string
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须设定
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer)-1, 0, CONV(&peer),&len);
if(n > 0)
{
inbuffer[n] = 0;
InetAddr client(peer);//获取cilent相关信息
std::string cilentmessage = client.Ip()+":"+std::to_string(client.Port())+"# "+ inbuffer;
LOG(LogLevel::DEBUG)<<cilentmessage;
//调用回调方法处理翻译业务
std::string value = _func(inbuffer);
//将获取到的信息写回client
std::string echo_string = "Translate# ";
echo_string += value;
::sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,client.NetAddr(),client.NetAddrLen());
}
}
_isrunning = false;
}
~UdpServer()
{
if (_sockfd > gsockfd)
::close(_sockfd);
}
private:
InetAddr _addr; // 服务器地址包括ip和port
int _sockfd;
bool _isrunning; // 服务器运行状态
func_t _func; //回调业务方法
};
#endif
最后在定义服务器时使用lambda
表达式将Dict
类中的Translate
方法绑定给UdpServer
:
#include "UdpServer.hpp"
#include "Dict.hpp"
using namespace DictionaryModule;
// ./server_udp localport
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY();
Dict dictionary;
std::unique_ptr<UdpServer> svr_uptr;
if (argc == 2)
{
uint16_t port = std::stoi(argv[1]);
svr_uptr = std::make_unique<UdpServer>([&dictionary](const std::string& key){return dictionary.Translate(key);},port);
}
else
svr_uptr = std::make_unique<UdpServer>([&dictionary](const std::string& key){return dictionary.Translate(key);});
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
客户端函数不需要改变可以直接使用,结果如下:
对于聊天室的实现,我们需要对聊天对象进行管理,所以需要新建一个类usermanager
以及描述聊天对象的类user
:
#pragma once
#include <iostream>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"
namespace UserModule
{
using namespace InetAddrModule;
using namespace LogModule;
using namespace MutexModule;
class UserInterface
{
public:
virtual ~UserInterface() = default;
virtual void SendTo(int sockfd, const std::string &message) = 0; // 纯虚函数
virtual bool operator==(const InetAddr &u) const = 0;
virtual std::string Id() = 0;
};
//描述对象
class User : public UserInterface
{
public:
User(const InetAddr &id) : _id(id)
{
}
void SendTo(int sockfd, const std::string &message) override
{
// ssize_t n = ::sendto(sockfd, &message, message.size(), 0, _id.NetAddr(), _id.NetAddrLen());错误错误!!!!不能取地址message
ssize_t n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());
LOG(LogLevel::DEBUG) << "send message to " << _id.Addr() << " info: " << message;
if (n < 0)
{
LOG(LogLevel::WARNING) << "Snedto fail...";
return;
}
}
bool operator==(const InetAddr &u) const override
{
return _id == u;
}
std::string Id()
{
return _id.Addr();
}
~User()
{
}
private:
InetAddr _id;
};
//管理对象
class UserManage
{
public:
UserManage()
{
}
void AddUser(InetAddr &id)
{
LockGuard lock(_mutex);//因为要访问公共资源所以要加锁保护
// 1.先遍历整个链表查找是否已经添加过了
for (auto &user : _online_user)
{
if (*user == id) // User已经重载==
{
LOG(LogLevel::INFO) << id.Addr() << "用户已经存在...";
return;
}
}
// 2.如果是新用户就添加
_online_user.push_back(std::make_shared<User>(id));
LOG(LogLevel::INFO) << "添加用户: " << id.Addr() << "成功...";
}
void DelUser(InetAddr &id)
{
LockGuard lock(_mutex);
// 1.先遍历整个链表查找是否有该用户
auto pos = std::remove_if(_online_user.begin(), _online_user.end(), [&id](std::shared_ptr<UserInterface> &user)
{ return *user == id; });
// 2.如果有就删除
_online_user.erase(pos, _online_user.end());
}
// 路由转发
void Router(int sockfd, const std::string &message)
{
LockGuard lock(_mutex);
for (auto &user : _online_user)
{
user->SendTo(sockfd, message);
}
}
void PrintUser()
{
LockGuard lock(_mutex);
for (auto user : _online_user)
{
LOG(LogLevel::DEBUG) << "在线用户-> " << user->Id();
}
}
~UserManage()
{
}
private:
std::list<std::shared_ptr<UserInterface>> _online_user;
Mutex _mutex;
};
};
对于描述对象参数我们可以使用之前实现的InetAddr类,对于对象的管理方法主要有添加对象、删除对象以及路由转发(群发)这三个部分;因为后续有多个线程而它们内部实现需要访问公共资源,所以需要加锁保护。
在服务器代码中其他都与前面类似,我们只需要将服务器的Start
方法修改一下即可:
bool Start()
{
_isrunning = true;
while (true)
{
char inbuffer[1024]; // string
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须设定
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &len);
if (n > 0)
{
inbuffer[n] = 0;
InetAddr client(peer); // 1.获取cilent相关信息
std::string message = client.Addr() + "# " + inbuffer;
LOG(LogLevel::DEBUG) << message;
// 2.判断是否为quit信息
if (std::strcmp(inbuffer, "quit") == 0)
{
_deluser(client);
message = client.Addr() + "# " + "我走了,你们聊!";
}
else
{
// 3.添加新用户
_adduser(client);
}
// 3. 构建转发任务,推送给线程池,让线程池进行转发
task_t task = std::bind(UdpServer::_router, _sockfd, message);
ThreadPool<task_t>::GetInstance()->Enqueue(task);
}
}
_isrunning = false;
}
服务器不再是简单的接收信息,还需要对接收的消息进行处理;因为转发任务消耗的时间可能较长,我们可以利用之前实现的线程池来处理多个转发任务,主线程则继续收消息然后往线程池里添加转发任务。
除了Start
方法,服务器类也需要添加几个回调方法(在Start
方法中使用):
using add_t = std::function<void(InetAddr &id)>;
using del_t = std::function<void(InetAddr &id)>;
using router_t = std::function<void(int sockfd, const std::string &message)>;
using task_t = std::function<void()>;
class UdpServer
{
public:
UdpServer(add_t adduser, del_t deluser, router_t router, uint16_t port = gdefaultport)
: _sockfd(gsockfd),
_addr(port),
_isrunning(false),
_adduser(adduser),
_deluser(deluser),
_router(router)
{
}
private:
InetAddr _addr; // 服务器地址包括ip和port
int _sockfd;
bool _isrunning; // 服务器运行状态
add_t _adduser;
del_t _deluser;
router_t _router;
};
#endif
在main
函数中使用服务器对象时就需要绑定上述回调方法:
#include "UdpServer.hpp"
#include "User.hpp"
using namespace UserModule;
// ./server_udp localport
int main(int argc, char *argv[])
{
ENABLE_CONSOLE_LOG_STRATEGY();
std::shared_ptr<UserManage> um = std::make_shared<UserManage>();
std::unique_ptr<UdpServer> svr_uptr;
if (argc == 2)
{
uint16_t port = std::stoi(argv[1]);
svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id)
{ return um->AddUser(id); },
[&um](InetAddr &id)
{ return um->DelUser(id); },
[&um](int sockfd, const std::string &message)
{ return um->Router(sockfd, message); },port);
}
else
svr_uptr = std::make_unique<UdpServer>([&um](InetAddr &id)
{ return um->AddUser(id); },
[&um](InetAddr &id)
{ return um->DelUser(id); },
[&um](int sockfd, const std::string &message)
{ return um->Router(sockfd, message); });
svr_uptr->InitServer();
svr_uptr->Start();
return 0;
}
对于客户端代码,我们也需要创建两个线程,主线程用来向服务器发送消息,另一个线程则用来接收群发的消息:
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <signal.h>
int sockfd = -1;
struct sockaddr_in server;
void ClientQuit(int signo)
{
(void)signo;
const std::string quit = "QUIT";
int n = ::sendto(sockfd, quit.c_str(), quit.size(), 0, CONV(&server), sizeof(server));
exit(0);
}
void *Recver(void *args)
{
while (true)
{
(void)args;
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
if (n > 0)
{
buffer[n] = 0;
std::cout << buffer << std::endl; // 代码没问题,重定向也没问题,管道读写同时打开,才会继续向后运行
// fprintf(stderr, "%s\n", buffer);
// fflush(stderr);
}
}
}
// CS
// ./client_udp serverip serverport
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
Die(USAGE_ERR);
}
signal(2, ClientQuit);
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建socket
sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "socket error" << std::endl;
Die(SOCKET_ERR);
}
std::cout<<"sockfd: "<<sockfd<<std::endl;
// 1.1 填充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 tid;
pthread_create(&tid, nullptr, Recver, nullptr);
// 1.2 启动的时候,给服务器推送消息即可
const std::string online = " ... 来了哈!";
int n = ::sendto(sockfd, online.c_str(), online.size(), 0, CONV(&server), sizeof(server));
// 2. clientdone
while (true)
{
std::cout << "Please Enter# ";
std::string message;
std::getline(std::cin, message);
// client 不需要bind吗?socket <-> socket
// client必须也要有自己的ip和端口!但是客户端,不需要自己显示的调用bind!!
// 而是,客户端首次sendto消息的时候,由OS自动进行bind
// 1. 如何理解client自动随机bind端口号? 一个端口号,只能被一个进程bind
// 2. 如何理解server要显示的bind?服务器的端口号,必须稳定!!必须是众所周知且不能改变轻易改变的!
int n = ::sendto(sockfd, message.c_str(), message.size(), 0, CONV(&server), sizeof(server));
(void)n;
}
return 0;
}
在运行客户端代码之前,我们可以创建一个管道将其重定向到cerro,然后运行客户端,这样服务器群发收到的消息就会写入到管道中