文章目录
- 1.Echo server
- 2. Dict server
- 3. ChatServer
1.Echo server
简单的回显服务器和客户端代码。
- 服务器代码:
#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]; // stringstruct 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;//将获取到的信息写回clientstd::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和portint _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);}elsesvr_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.创建sockfdsockfd = ::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 地址上面获取的。
结果如下:
2. Dict server
上述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");elsereturn 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]; // stringstruct 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);//将获取到的信息写回clientstd::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和portint _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);}elsesvr_uptr = std::make_unique<UdpServer>([&dictionary](const std::string& key){return dictionary.Translate(key);});svr_uptr->InitServer();svr_uptr->Start();return 0;
}
客户端函数不需要改变可以直接使用,结果如下:
3. ChatServer
对于聊天室的实现,我们需要对聊天对象进行管理,所以需要新建一个类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());错误错误!!!!不能取地址messagessize_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]; // stringstruct 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和portint _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);}elsesvr_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. 创建socketsockfd = ::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. clientdonewhile (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,然后运行客户端,这样服务器群发收到的消息就会写入到管道中