实现简单的Udp网络程序
Version-1 回声服务器Echo Server
Init-创建服务端
服务端创建套接字
我们把服务器封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字。
Socket函数
创建套接字的函数叫做socket,该函数原型如下:
int socket(int domain, int type, int protocol);
参数说明:
1.domain:创建套接字的域或者叫做协议家族,也就是创建套接字的类型。该参数就相当于struct sockaddr结构的前16个位。如果是本地通信就设置为AF_UNIX,如果是网络通信就设置为AF_INET(IPv4)或AF_INET6(IPv6)。
2.type:创建套接字时所需的服务类型。其中最常见的服务类型是SOCK_STREAM和SOCK_DGRAM,如果是基于UDP的网络通信,我们采用的就是SOCK_DGRAM,叫做用户数据报服务,如果是基于TCP的网络通信,我们采用的就是SOCK_STREAM,叫做流式套接字,提供的是流式服务。
3.protocol:创建套接字的协议类别。你可以指明为TCP或UDP,但该字段一般直接设置为0就可以了,设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。返回值说明:
套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码会被设置。
socket是什么类型的接口?
网络协议栈是分层的,按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口。
socket是被谁调用的?
socket这个函数是被程序调用的,但并不是被程序在编码上直接调用的,而是程序编码形成的可执行程序运行起来变成进程,当这个进程被CPU调度执行到socket函数时,然后才会执行创建套接字的代码,也就是说socket函数是被进程所调用的。
socket底层做了什么?
socket函数是被进程所调用的,而每一个进程在系统层面上都有一个进程地址空间PCB(task_struct)、文件描述符表(files_struct)以及对应打开的各种文件。而文件描述符表里面包含了一个数组fd_array,其中数组中的0、1、2下标依次对应的就是标准输入、标准输出以及标准错误。

Linux下一切皆文件
当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件双链表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的“网络文件”,最后3号文件描述符作为socket函数的返回值返回给了用户。

其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*和write*)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。

对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。
服务端创建套接字demo:

服务端绑定
现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是刷到网卡,此时该文件还没有与网络关联起来。

UDP作为无连接协议,通信双方没有预先建立的通道。与TCP不同,UDP通信需要显式指定目标地址,而服务端绑定端口是为了确立一个固定的通信端点,使客户端能准确找到服务。(所以客户端不需要绑定,服务端需要绑定)
bind函数
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
作用其实就是给套接字绑定个名字,其实也就是给套接字赋值ip+port(ip地址和端口号)

参数说明:
- sockfd:绑定的文件的文件描述符。也就是我们创建套接字时获取到的文件描述符。
- addr:网络相关的属性信息,包括协议家族、IP地址、端口号等。
- addrlen:传入的addr结构体的长度。
返回值说明:
- 绑定成功返回0,绑定失败返回-1,同时错误码会被设置。
填充IP和Port
一些细节:
bzero清空
因为sockaddr_in会有一些编译器插入的填充值


端口号转字节序
此外,因为端口号必须在两个主机之间流通,所以必须传输到网络中!!因此要转成字节序!!
字符串IP vs 整数IP
IP地址的表现形式有两种:
- 字符串IP:类似于
192.168.233.123这种字符串形式的IP地址,叫做基于字符串的点分十进制IP地址。 - 整数IP:IP地址在进行网络传输时所用的形式,用一个32位的整数来表示IP地址。
整数IP的意义

网络传输数据时是寸土寸金的,如果我们在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址至少就需要15个字节(1Byte=8bit),但实际并不需要耗费这么多字节。
IP地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个比特位就能表示,因此我们实际只需要32个比特位就能够表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,我们将IP地址的这种表示方法称之为整数IP,此时表示一个IP地址只需要4个字节。

因为采用整数IP的方案表示一个IP地址只需要4个字节,并且在网络通信也能表示同样的含义,因此在网络通信时就没有用字符串IP而用的是整数IP,因为这样能够减少网络通信时数据的传送。
字符串IP -> 整数IP 的方法
转换的方式有很多,
法一:
比如我们可以定义一个位段A,位段A当中有四个成员,每个成员的大小都是8个比特位,这四个成员就依次表示IP地址的四个区域,一共32个比特位。
然后我们再定义一个联合体IP,该联合体当中有两个成员,其中一个是32位的整数,其代表的就是整数IP,还有一个就是位段A类型的成员,其代表的就是字符串IP。

由于联合体的空间是成员共享的,因此我们设置IP和读取IP的方式如下:
* 当我们想以整数IP的形式设置IP时,直接将其赋值给联合体的第一个成员就行了。
* 当我们想以字符串IP的形式设置IP时,先将字符串分成对应的四部分,然后将每部分转换成对应的二进制序列依次设置到联合体中第二个成员当中的p1、p2、p3和p4就行了。
* 当我们想取出整数IP时,直接读取联合体的第一个成员就行了。
* 当我们想取出字符串IP时,依次获取联合体中第二个成员当中的p1、p2、p3和p4,然后将每一部分转换成字符串后拼接到一起就行了。

法二:
实际在进行字符串IP和整数IP的转换时,我们不需要自己编写转换逻辑,系统已经为我们提供了相应的转换函数,我们直接调用即可。
将字符串IP转换成整数IP的函数叫做inet_addr,该函数的函数原型如下:
inet_addr函数
in_addr_t inet_addr(const char *cp);
该函数使用起来非常简单,我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。而且这个整数IP还是网络序列的。
最后在bind即可
demo:

服务端现阶段demo

运行结果

Start-运行服务器
UDP服务器的初始化就只需要创建套接字和绑定就行了,当服务器初始化完毕后我们就可以启动服务器了。
服务器实际上就是在周而复始的为我们提供某种服务,服务器之所以称为服务器,是因为服务器运行起来后就永远不会退出,因此服务器实际执行的是一个死循环代码。由于UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接读取客户端发来的数据。
读取数据
因为udp不是面向字节流的,所以只能用recvfrom接口来读取数据
recvfrom有两个作用:
- 接收数据本身
- 同时获取发送方的地址信息
recvfrom函数
该函数的函数原型如下:
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数说明:
sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
buf:读取数据的存放位置。
len:期望读取数据的字节数。
flags:读取的方式。一般设置为0,表示阻塞读取。
src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等,也是一个输入输出型参数,我们从这里得到发送方的信息。
addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。返回值说明:
读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
注意:
1.由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
2.在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
3.由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型的进行强转。
发回数据
sendto函数

第五个和第六个是输入型数,通过客户端套接字信息将处理后的数据发送过去
运行服务器现阶段demo

客户端创建套接字
同样的,我们把客户端也封装成一个类,当我们定义出一个客户端对象后也是需要对其进行初始化,而客户端在初始化时也需要创建套接字,之后客户端发送数据或接收数据也就是对这个套接字进行操作。
客户端创建套接字时选择的协议家族也是AF_INET,需要的服务类型也是SOCK_DGRAM,当客户端被析构时也可以选择关闭对应的套接字。
与服务端不同的是,客户端在初始化时只需要创建套接字就行了,而不需要进行绑定操作。
客户端是否要绑定
客户端需要显示地调用bind函数绑定吗?
不需要。
首先,由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号,只不过服务端需要进行端口号的绑定,而客户端不需要。
因为服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,IP地址一般对应的就是域名,而端口号一般没有显示指明过,因此服务端的端口号一定要是一个众所周知的端口号,并且选定后不能轻易改变,否则客户端是无法知道服务端的端口号的,这就是服务端要进行绑定的原因,只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。
而客户端在通信时虽然也需要端口号,但客户端一般是不进行绑定的,客户端访问服务端的时候,端口号只要是唯一的就行了,不需要和特定客户端进程强相关。
如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了,因此客户端端口可以动态的进行设置,并且客户端的端口号不需要我们来设置,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的随机的端口号。
也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要我们的端口号没有被耗尽,客户端就永远可以启动。
客户端demo
引入命令行参数,第二个第三个参数对应 服务端ip和端口号,所以只需要服务端显示绑定一个socket这样其他客户端来访问一个固定的socket就能享受服务了。
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
void Usage(std::string proc)
{
std::cerr<<"Usage:"<<proc<<" serverip serverport"<<std::endl;
}
// ./udp_client server_ip server_port
int main(int argc,char *argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
std::string server_ip=argv[1];
uint16_t server_port=std::stoi(argv[2]);
//AF_INET:使用IPv4协议族进行网络通信 SOCK_DGRAM:无连接通信 第三个参数设置为0系统根据前两个参数自动选择默认协议
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
std::cout<<"create socket error"<<std::endl;
return 0;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(server_port);
server.sin_addr.s_addr=inet_addr(server_ip.c_str());
while(true)
{
std::cout<<"Please Enter@ ";
std::string line;
std::getline(std::cin,line);
//写
sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
char buffer[1024];
int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);
if(m>0)
{
buffer[m]=0;
std::cout<<buffer<<std::endl;
}
}
return 0;
}
本地测试
现在服务端和客户端的代码都已经编写完毕,我们可以先进行本地测试,此时服务器没有绑定外网,绑定的是本地环回。现在我们运行服务器时指明端口号为8081,再运行客户端,此时客户端要访问的服务器的IP地址就是本地环回127.0.0.1,服务端的端口号就是8081。

测试时我们可以用netstat命令查看网络信息,看到服务端的端口和客户端的端口。这里客户端能被netstat命令查看到,说明客户端也已经动态绑定成功了,这就是我们所谓的网络通信。
我们在客户端输入信息,服务端就能收到了。

INADDR_ANY
现在本地测试搞定了,接下来进行网络测试,那直接让服务器绑定公网IP,这样这个服务器就能被外网访问了
但是我们会发现绑定失败,因为云服务器禁止用户绑定公网IP

由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。
因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。
修改代码:




绑定INADDR_ANY的好处
当一个服务器的带宽足够大时,一台机器接收数据的能力就约束了这台机器的IO效率,因此一台服务器底层可能装有多张网卡,此时这台服务器就可能会有多个IP地址,但一台服务器上端口号为8081的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8081的服务。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。而如果服务端绑定的是INADDR_ANY,那么只要是发送给端口号为8081的服务的数据,系统都会可以将数据自底向上交给该服务端。

因此服务端绑定INADDR_ANY这种方案也是强烈推荐的方案,所有的服务器具体在操作的时候用的也就是这种方案。
修改完代码后,此时我们启动服务端,通过netstat查看,就会发现服务端IP变成0.0.0.0(任意IP)
这样我们启动的时候使用任意IP地址就都可以得到想要的结果

Version-2 翻译服务器DictServer
在运行服务端后,接受完客户端消息,我们可以设计自己想要处理的业务。这里我们实现一个翻译功能


在服务端实现业务,选择你要处理的业务实现函数,将函数作为参数传进对象,从而实现网络和业务解耦


dict.txt
apple: 苹果
banana: ⾹蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: ⽼师
student: 学⽣
car: 汽⻋
bus: 公交⻋
love: 爱
hate: 恨
hello: 你好
goodbye: 再⻅
summer: 夏天
winter: 冬天
DictClient.cc
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
void Usage(std::string proc)
{
std::cerr<<"Usage:"<<proc<<" serverip serverport"<<std::endl;
}
// ./udp_client server_ip server_port
int main(int argc,char *argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(0);
}
std::string server_ip=argv[1];
uint16_t server_port=std::stoi(argv[2]);
//AF_INET:使用IPv4协议族进行网络通信 SOCK_DGRAM:无连接通信 第三个参数设置为0系统根据前两个参数自动选择默认协议
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(sockfd<0)
{
std::cout<<"create socket error"<<std::endl;
return 0;
}
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_family=AF_INET;
server.sin_port=htons(server_port);
server.sin_addr.s_addr=inet_addr(server_ip.c_str());
while(true)
{
std::cout<<"Please Enter@ ";
std::string line;
std::getline(std::cin,line);
//写
sendto(sockfd,line.c_str(),line.size(),0,(struct sockaddr*)&server,sizeof(server));
//读
struct sockaddr_in temp;
socklen_t len=sizeof(temp);
char buffer[1024];
int m=recvfrom(sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&len);
if(m>0)
{
buffer[m]=0;
std::cout<<buffer<<std::endl;
}
}
return 0;
}
Dictionary.hpp
#pragma once
#include<iostream>
#include<string>
#include<unordered_map>
#include<fstream>
#include"logger.hpp"
static const std::string sep=": ";//冒号+空格
class Dictionary
{
private:
void LoadConf()
{
std::ifstream in(_path);
if(!in.is_open())
{
LOG(LoggerLevel::ERROR)<<"open file error: "<<_path;
return;
}
std::string line;
while(std::getline(in,line))
{
LOG(LoggerLevel::DEBUG)<<"load dict message: "<<line;
//cat: 猫,拆出来放map里
//单词拆分
auto pos=line.find(sep);
if(pos==std::string::npos)
{
LOG(LoggerLevel::WARNING)<<"format error: "<<line;
continue;
}
std::string word=line.substr(0,pos);//substr(pos,len)
std::string value=line.substr(pos+sep.size());//len不写默认读到末尾
if(word.empty()||value.empty())
{
LOG(LoggerLevel::WARNING)<<"format error,word or value is empty "<<line;
continue;
}
//存进表中
_dict.insert(std::make_pair(word,value));
}
in.close();
}
public:
Dictionary(const std::string&path):_path(path)
{
LOG(LoggerLevel::INFO)<<"construct Dictionary obj";
LoadConf();
}
std::string Translate(const std::string&word,const std::string&whoip,uint16_t whoport)
{
//暂时不用,防止报警
(void)whoip;
(void)whoport;
auto iter=_dict.find(word);//find函数返回键值对的迭代器,没找到就返回end()
//end()表示容器末尾的空位置
if(iter==_dict.end())
{
return "unknown";
}
return iter->first +"->"+ iter->second;
}
~Dictionary()
{}
private:
std::string _path;
std::unordered_map<std::string,std::string> _dict;
};
DictServer.cc
#include"DictServer.hpp"
#include"Dictionary.hpp"
#include<iostream>
#include<memory>
void Usage(std::string proc)
{
std::cerr<<"Usage:"<<proc<<" Local_port"<<std::endl;
}
// std::string Translate(const std::string&word,const std::string&whoip,uint16_t whoport)
// {
// return "哈哈";
// }
// ./udp_server serverport
int main(int argc,char*argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(0);
}
//打印日志策略
EnableConsoleStrategy();
uint16_t port=std::stoi(argv[1]);
Dictionary dict("./dict.txt");
//智能指针不用手动delete防止内存泄漏
std::unique_ptr<DictServer> usvr=std::make_unique<DictServer>(port,[&dict]
(const std::string&word,const std::string&whoip,uint16_t whoport)->std::string{
return dict.Translate(word,whoip,whoport);
});
usvr->Init();
usvr->Start();
return 0;
}
DictServer.hpp
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<cstdlib>
#include<functional>
#include"logger.hpp"
using callback_t =std::function<std::string(const std::string&word,const std::string&whoip,uint16_t whoport)>;
static const int gdefaultsockfd=-1;
class DictServer
{
public:
DictServer(uint16_t port,callback_t cb)
:
_port(port),
_sockfd(gdefaultsockfd),
_isrunning(false),
_cb(cb)
{}
void Init()
{
//1.创建socket fd
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
//打印日志后退出
LOG(LoggerLevel::FATAL)<<"create socket error";
exit(1);
}
LOG(LoggerLevel::INFO)<<"create socket success:"<<_sockfd; //从3开始(死去的记忆开始攻击)
//2.bind
//2.1:填充IP和Port
struct sockaddr_in local;
//清空sockaddr_in 可能有编译器插入的填充字节,避免填充位干扰
bzero(&local,sizeof(local));
local.sin_family=AF_INET;//表示进行网络通信还是域间通信,sockaddr_in是网络套接字
local.sin_port=htons(_port);//网络通信->端口号必须转成网络字节序
local.sin_addr.s_addr=htonl(INADDR_ANY);//任意IP绑定,不加htonl也行,推荐加上
//local.sin_addr.s_addr=inet_addr(_ip.c_str());//字符串IP转整数IP,且整数IP是网络序列的
//2.2 和socketfd进行bind
int n=bind(_sockfd,(sockaddr*)&local,sizeof(local));
if(n<0)
{
LOG(LoggerLevel::FATAL)<<"bind socket error";
exit(2);
}
LOG(LoggerLevel::INFO)<<"bind socket success:"<<_sockfd;
}
void Start()
{
_isrunning=true;
while(_isrunning)
{
char buffer[1024];
buffer[0]=0;//清空缓存区
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//1.读取数据,获得发送端(也就是客户端)的socket
ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
if(n>0)
{
buffer[n]=0;//防止读取上一次读取还存在缓冲区的数据
//client ip 和 端口号
uint16_t client_port=ntohs(peer.sin_port);
std::string client_ip=inet_ntoa(peer.sin_addr);//inet_nota将网络字节序转点分十进制字符串形式
//这里我们也可以写接收到客户端消息后,处理的业务,这里我们写的是一个翻译
std::string word=buffer;
//回调
std::string result=_cb(word,client_ip,client_port);
//2.将收取的数据当作字符串加工一下,然后返回给客户端
// buffer[n]=0;
// LOG(LoggerLevel::DEBUG)<<"["<<client_ip<<":"<<client_port<<"]# "<<buffer;
// std::string echo_string="server echo# ";
// echo_string+=buffer;
sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
}
}
_isrunning=false;
}
void Stop()
{
_isrunning=false;
}
~DictServer(){}
private:
int _sockfd;
uint16_t _port;
//std::string _ip;//暂时
callback_t _cb;
bool _isrunning;
};
logger.hpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"
enum class LoggerLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LoggerLevelToString(LoggerLevel level)
{
switch (level)
{
case LoggerLevel::DEBUG:
return "Debug";
case LoggerLevel::INFO:
return "Info";
case LoggerLevel::WARNING:
return "Warning";
case LoggerLevel::ERROR:
return "Error";
case LoggerLevel::FATAL:
return "Fatal";
default:
return "Unknown";
}
}
std::string GetCurrentTime()
{
// 获取时间戳
time_t timep = time(nullptr);
// 把时间戳转化为时间格式
struct tm currtm;
localtime_r(&timep, &currtm);
// 转化为字符串
char buffer[64];
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return buffer;
}
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
~ConsoleLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
Mutex _lock;
};
const std::string default_dir_path_name = "log";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string dir_path_name = default_dir_path_name,
const std::string filename = default_filename)
: _dir_path_name(dir_path_name), _filename(filename)
{
if (std::filesystem::exists(_dir_path_name))
{
return;
}
try
{
std::filesystem::create_directories(_dir_path_name);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\r\n";
}
}
~FileLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lock(&_lock);
std::string target = _dir_path_name;
target += '/';
target += _filename;
std::ofstream out(target.c_str(), std::ios::app);
if (!out.is_open())
{
return;
}
out << logmessage << "\n";
out.close();
}
}
private:
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
class Logger
{
public:
Logger()
{
}
void EnableConsoleStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger)
: _curr_time(GetCurrentTime()), _level(level), _pid(getpid())
, _filename(filename), _line(line), _logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LoggerLevelToString(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "]"
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 时间戳
LoggerLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _filename; // 文件名
int _line; // 行号
std::string _loginfo; // 一条合并完成的,完整的日志信息
Logger &_logger; // 提供刷新策略的具体做法
};
LogMessage operator()(LoggerLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
Mutex.hpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Get()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex *_mutex) : _mutexp(_mutex)
{
_mutex->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
Makefile
.PHONY:all
all:dict_client dict_server
dict_client:DictClient.cc
g++ -o $@ $^ -std=c++17
dict_server:DictServer.cc
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f dict_client dict_server
Version-3 聊天室+多线程ChatSystemDemo
大致流程速览

ThreadPool.hpp
#pragma once
#include <iostream>
#include<cstdio>
#include <queue>
#include <vector>
#include <unistd.h>
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
//单例线程池-懒汉模式
const static int defaultthreadnum = 3; // for debug
template <class T>
class ThreadPool
{
private:
bool QueueIsEmpty()
{
return _q.empty();
}
void Routine(const std::string &name)
{
// for test
while (1)
{
// 把任务从线程获取到线程私有(数据或资源仅对特定线程可见,临界区->线程私有栈)
T t;
{
LockGuard Lockguard(&_lock);
while (QueueIsEmpty()&&_is_running)
{
_wait_thread_num++;
_cond.Wait(_lock);
_wait_thread_num--;
}
if(!_is_running&&QueueIsEmpty())
{
LOG(LoggerLevel::INFO)<<"线程池退出&&任务队列为空,"<<name<<"退出";
break;
}
// 队列中此时有任务了,拿到任务t,但是
//1.线程池退出---销毁历史任务
//2.线程池,没有退出---正常处理任务
t = _q.front();
_q.pop();
}
t();
//for debug
//LOG(LoggerLevel::DEBUG) << name << "handler task:" << t.ResultToString();
}
}
//构造函数私有化,这样就不允许创建对象了
ThreadPool(int threadnum = defaultthreadnum)
: _threadnum(threadnum), _is_running(false), _wait_thread_num(0)
{
for (int i = 0; i < _threadnum; ++i)
{
// 方法一
// bind预先绑定函数与参数
// auto f=std::bind(hello,this);
// Thread t(f);
// 方法二(推荐,可读性高):
// lambda使得线程调用类内函数的方法
std::string name = "thread-" + std::to_string(i + 1);
_threads.emplace_back([this](const std::string &name)
{ this->Routine(name); }, name);
// Thread t([this](){
// this->hello();
// },name);
// _threads.push_back(std::move(t));
}
LOG(LoggerLevel::INFO) << "thread_pool obj create success";
}
ThreadPool<T> &operator=(const ThreadPool<T> &)=delete;
ThreadPool(const ThreadPool<T> &)=delete;
public:
// 启动线程
void Start()
{
if (_is_running)
return;
_is_running = true;
for (auto &t : _threads)
{
t.Start();
}
LOG(LoggerLevel::INFO) << "thread_pool obj running success";
}
// 取消线程
// 核心思想:我们应该让线程走正常的唤醒逻辑退出
// 1.如果被唤醒&&任务队列没有任务 = 让线程退出
// 2.如果被唤醒&&任务队列有任务 = 线程不能立即退出,而应该让线程把任务处理完,再退出
// 3.线程本身没有被休眠,我们应该让他把他能处理的任务全部处理完成,在退出
// 3 || 2 -> 1
//如果线程有任务,线程是不会休眠的
void Stop()
{
if (!_is_running)
return;
_is_running = false;
if (_wait_thread_num)_cond.NotifyAll();
// 不推荐这种做法
// if (!_is_running)
// return;
// _is_running = false;
// for (auto &t : _threads)
// {
// t.Stop();
// }
// LOG(LoggerLevel::INFO)<<"thread_pool obj stop success";
}
void Wait()
{
for (auto &t : _threads)
{
t.Join();
}
LOG(LoggerLevel::INFO) << "thread_pool obj wait success";
}
void Enqueue(const T &t)
{
if(!_is_running)
return;
{
LockGuard lockguard(&_lock);
_q.push(t);
if (_wait_thread_num > 0)
{
_cond.NotifyOne();
}
}
}
static std::string ToHex(ThreadPool<T> *addr)
{
char buffer[64];
snprintf(buffer,sizeof(buffer),"%p",addr);
return buffer;
}
//获取单例
static ThreadPool<T> *GetInstance()
{
//成员方法可以访问类内静态属性吗?可以!
if(!_instance)
{
_instance=new ThreadPool<T>();
LOG(LoggerLevel::DEBUG)<<"线程池单例首次被使用,创建并初始化"<<ToHex(_instance);
_instance->Start();
}
else
{
LOG(LoggerLevel::DEBUG)<<"线程池单例已经存在,直接获取"<<ToHex(_instance);
}
return _instance;
}
~ThreadPool()
{
}
private:
// 任务队列
std::queue<T> _q; // 临界资源
// 多个线程
std::vector<Thread> _threads;
int _threadnum;
int _wait_thread_num;
// 保护机制
Mutex _lock;
Cond _cond;
// 其他属性
bool _is_running;
//单例中静态指针
static ThreadPool<T> *_instance;
};
//类内声明静态指针,在类外需要初始化
template<class T>
ThreadPool<T> *ThreadPool<T>::_instance=nullptr;
Thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <functional>
#include <sys/syscall.h>
#include "logger.hpp"
#define get_lwp_id() syscall(SYS_gettid);
using func_t = std::function<void(const std::string&name)>;
const std::string threadnamedefault = "Nobody";
class Thread
{
public:
Thread(func_t func, const std::string &name = threadnamedefault) : _name(name), _func(func), _isrunning(false)
{
LOG(LoggerLevel::INFO) << _name << "create thread obj success";
}
// static->变成全局,因为类内函数会多一个this指针,会显示参数不兼容
static void *start_routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
// self->
self->_isrunning = true;
self->_lwpid = get_lwp_id();
self->_func(self->_name);
pthread_exit((void *)0);
}
void Start()
{
// this把当前对象传进来
int n = pthread_create(&_tid, nullptr, start_routine, this);
if (n == 0)
{
LOG(LoggerLevel::INFO) << _name << "running success";
}
}
void Stop()
{
int n = pthread_cancel(_tid);
(void)n;
}
// 检测线程结束并且回收的功能
void Join()
{
if (!_isrunning)
return;
int n = pthread_join(_tid, nullptr);
if (n == 0)
{
LOG(LoggerLevel::INFO) << _name << "join success";
}
}
~Thread()
{
// LOG(LoggerLevel::INFO) << _name << "destory thread obj success";
}
private:
bool _isrunning; // 线程创建标识符
pthread_t _tid;
pid_t _lwpid;
std::string _name;
func_t _func;
};
#endif
logger.hpp
#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"
enum class LoggerLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LoggerLevelToString(LoggerLevel level)
{
switch (level)
{
case LoggerLevel::DEBUG:
return "Debug";
case LoggerLevel::INFO:
return "Info";
case LoggerLevel::WARNING:
return "Warning";
case LoggerLevel::ERROR:
return "Error";
case LoggerLevel::FATAL:
return "Fatal";
default:
return "Unknown";
}
}
std::string GetCurrentTime()
{
// 获取时间戳
time_t timep = time(nullptr);
// 把时间戳转化为时间格式
struct tm currtm;
localtime_r(&timep, &currtm);
// 转化为字符串
char buffer[64];
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d-%02d-%02d",
currtm.tm_year + 1900, currtm.tm_mon + 1, currtm.tm_mday,
currtm.tm_hour, currtm.tm_min, currtm.tm_sec);
return buffer;
}
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
~ConsoleLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lockguard(&_lock);
std::cout << logmessage << std::endl;
}
}
private:
Mutex _lock;
};
const std::string default_dir_path_name = "log";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string dir_path_name = default_dir_path_name,
const std::string filename = default_filename)
: _dir_path_name(dir_path_name), _filename(filename)
{
if (std::filesystem::exists(_dir_path_name))
{
return;
}
try
{
std::filesystem::create_directories(_dir_path_name);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\r\n";
}
}
~FileLogStrategy()
{
}
virtual void SyncLog(const std::string &logmessage) override
{
{
LockGuard lock(&_lock);
std::string target = _dir_path_name;
target += '/';
target += _filename;
std::ofstream out(target.c_str(), std::ios::app);
if (!out.is_open())
{
return;
}
out << logmessage << "\n";
out.close();
}
}
private:
std::string _dir_path_name;
std::string _filename;
Mutex _lock;
};
class Logger
{
public:
Logger()
{
}
void EnableConsoleStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void EnableFileStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger)
: _curr_time(GetCurrentTime()), _level(level), _pid(getpid())
, _filename(filename), _line(line), _logger(logger)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LoggerLevelToString(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "]"
<< " - ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _curr_time; // 时间戳
LoggerLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _filename; // 文件名
int _line; // 行号
std::string _loginfo; // 一条合并完成的,完整的日志信息
Logger &_logger; // 提供刷新策略的具体做法
};
LogMessage operator()(LoggerLevel level, std::string filename, int line)
{
return LogMessage(level, filename, line, *this);
}
~Logger()
{
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()
Mutex.hpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Get()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex *_mutex) : _mutexp(_mutex)
{
_mutex->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
Cond.hpp
#pragma once
#include"Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex& lock)
{
pthread_cond_wait(&_cond, lock.Get());
}
void NotifyOne()
{
pthread_cond_signal(&_cond);
}
void NotifyAll()
{
pthread_cond_broadcast(&_cond);
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
Route.hpp
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include"InetAddr.hpp"
class Route
{
private:
//当不需要修改的时候,只是访问数据推荐加const
bool IsExists(const InetAddr&addr)
{
for(auto&user:_online_user)
{
//==运算符重载
if(user==addr)return true;
}
return false;
}
void AddUser(const InetAddr &addr)
{
if(!IsExists(addr))
_online_user.push_back(addr);
}
void DeleteUser(const std::string &message,const InetAddr &addr)
{
//权宜之计,待更改
if(message=="QUIT")
{
auto iter=_online_user.begin();
for(;iter!=_online_user.end();++iter)
{
if(*iter==addr)
{
_online_user.erase(iter);
break;
}
}
}
}
void SendMessageToAll(int sockfd,const std::string &message,InetAddr &addr)
{
for(auto &user:_online_user)
{
LOG(LoggerLevel::DEBUG)<<"route ["<<message <<"] to: "<<user.ToString();
std::string info=addr.ToString();
info +='#';
info += message;//XXX-PORT#
sendto(sockfd,message.c_str(),message.size(),0,user.Addr(),user.Length());
}
}
public:
Route()
{}
void RouteMessageToAll(int sockfd,const std::string &message,InetAddr &addr)
{
AddUser(addr);
//此时一定存在在线用户列表
SendMessageToAll(sockfd,message,addr);
//如果用户想退出,就erase用户,否则只遍历一次不做任何改动
DeleteUser(message,addr);
}
// void RouteMessageToOne(){}
~Route()
{}
private:
std::vector<InetAddr> _online_user; //在线用户
};
InetAddr.hpp
#pragma once
// 该类 用于描述客户端套接字信息
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define Conv(addr) ((struct sockaddr*)&addr)
class InetAddr
{
private:
void Net2Host()
{
_port = ntohs(_addr.sin_port);
_ip = inet_ntoa(_addr.sin_addr);
}
void Host2Net()
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
}
public:
// 网络风格地址构造
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
Net2Host();
}
// 主机地址风格构造
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
: _port(port), _ip(ip)
{
Host2Net();
}
std::string Ip()
{
return _ip;
}
uint16_t Port()
{
return _port;
}
struct sockaddr* Addr()
{
return Conv(_addr);
}
socklen_t Length()
{
return sizeof(_addr);
}
std::string ToString()
{
return _ip+"-"+std::to_string(_port);
}
bool operator==(const InetAddr&addr)
{
return _ip==addr._ip&&_port==addr._port;
//return _ip==addr._ip;
}
~InetAddr()
{
}
private:
struct sockaddr_in _addr; // 网络风格地址
// 主机风格地址
std::string _ip;
uint16_t _port;
};
ChatClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <thread>
void Usage(std::string proc)
{
std::cerr << "Usage:" << proc << " serverip serverport" << std::endl;
}
int sockfd = -1;
std::string server_ip;
uint16_t server_port;
void InitClient(const std::string &server_ip, uint16_t server_port)
{
// AF_INET:使用IPv4协议族进行网络通信 SOCK_DGRAM:无连接通信 第三个参数设置为0系统根据前两个参数自动选择默认协议
// 创建客户端套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cout << "create socket error" << std::endl;
}
}
void recver()
{
while (true)
{
// 读取服务端处理后的数据
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
// recvfrom函数作用:1.接收数据本身 2.获取发送端的socket(可以用于验证是否来自目标服务端)
int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cerr << buffer << std::endl;//filefd: 1->2
}
}
}
void sender()
{
// 根据命令行参数获取服务端套接字
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
while (true)
{
std::cout << "Please Enter@ ";
std::string line;
std::getline(std::cin, line);
// 将需求写入服务端
sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server));
}
}
// ./udp_client server_ip server_port
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
InitClient(server_ip, server_port);
std::thread trecv(recver);
std::thread tsend(sender);
trecv.join();
tsend.join();
return 0;
}
ChatServer.hpp
#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<string.h>
#include<cstdlib>
#include<functional>
#include"InetAddr.hpp"
#include"logger.hpp"
//message不要用引用了,用拷贝拷出来,因为sockfd和clientaddr都是临时变量
using callback_t =std::function<void(int sockfd,const std::string message,InetAddr addr)>;
static const int gdefaultsockfd=-1;
class ChatServer
{
public:
ChatServer(uint16_t port,callback_t cb)
:
_port(port),
_sockfd(gdefaultsockfd),
_isrunning(false),
_cb(cb)
{}
void Init()
{
//1.创建socket fd
_sockfd=socket(AF_INET,SOCK_DGRAM,0);
if(_sockfd<0)
{
//打印日志后退出
LOG(LoggerLevel::FATAL)<<"create socket error";
exit(1);
}
LOG(LoggerLevel::INFO)<<"create socket success:"<<_sockfd; //从3开始(死去的记忆开始攻击)
//2.bind
// //2.1:填充IP和Port
InetAddr local(_port);
// struct sockaddr_in local;
// //清空sockaddr_in 可能有编译器插入的填充字节,避免填充位干扰
// bzero(&local,sizeof(local));
// local.sin_family=AF_INET;//表示进行网络通信还是域间通信,sockaddr_in是网络套接字
// local.sin_port=htons(_port);//网络通信->端口号必须转成网络字节序
// local.sin_addr.s_addr=htonl(INADDR_ANY);//任意IP绑定,不加htonl也行,推荐加上
// //local.sin_addr.s_addr=inet_addr(_ip.c_str());//字符串IP转整数IP,且整数IP是网络序列的
//2.2 和socketfd进行bind
int n=bind(_sockfd,local.Addr(),local.Length());
if(n<0)
{
LOG(LoggerLevel::FATAL)<<"bind socket error";
exit(2);
}
LOG(LoggerLevel::INFO)<<"bind socket success:"<<_sockfd;
}
void Start()
{
_isrunning=true;
while(_isrunning)
{
char buffer[1024];
buffer[0]=0;//清空缓存区
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
//1.读取数据,获得发送端(也就是客户端)的socket
//recvfrom一是读取数据,而是获取发送端的socket
ssize_t n=recvfrom(_sockfd,buffer,sizeof(buffer),0,(struct sockaddr*)&peer,&len);
//下面完成服务端的任务,然后写回给客户端
if(n>0)
{
//buffer存聊天消息
buffer[n]=0;//防止读取上一次读取还存在缓冲区的数据
//得到对应client(正在聊天的人是谁)
InetAddr clientaddr(peer);
LOG(LoggerLevel::DEBUG)<<"get a client info: "
<<clientaddr.Ip()<<"-"<<clientaddr.Port()<<":"
<<buffer;
//client ip 和 端口号
// uint16_t client_port=ntohs(peer.sin_port);
// std::string client_ip=inet_ntoa(peer.sin_addr);//inet_nota将网络字节序转点分十进制字符串形式
//这里我们也可以写接收到客户端消息后,处理的业务,这里我们写的是一个翻译
std::string message=buffer;
//回调
_cb(_sockfd,message,clientaddr);
//后边的都不需要了,服务端只需要读,然后处理任务
//2.将收取的数据当作字符串加工一下,然后返回给客户端
// buffer[n]=0;
// LOG(LoggerLevel::DEBUG)<<"["<<client_ip<<":"<<client_port<<"]# "<<buffer;
// std::string echo_string="server echo# ";
// echo_string+=buffer;
//sendto(_sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&peer,len);
}
}
_isrunning=false;
}
void Stop()
{
_isrunning=false;
}
~ChatServer(){}
private:
int _sockfd;
uint16_t _port;
//std::string _ip;//暂时
callback_t _cb;
bool _isrunning;
};
ServerMain.cc
#include"ThreadPool.hpp"
#include"Route.hpp"
#include"ChatServer.hpp"
#include<iostream>
#include<memory>
void Usage(std::string proc)
{
std::cerr<<"Usage:"<<proc<<" Local_port"<<std::endl;
}
//for debug
// void chat(int sockfd,const std::string message,InetAddr addr)
// {
// LOG(LoggerLevel::DEBUG)<<"sockfd"<<":"<<sockfd;
// LOG(LoggerLevel::DEBUG)<<"message"<<":"<<message;
// LOG(LoggerLevel::DEBUG)<<"client info"<<":"<<addr.ToString();
// sendto(sockfd,message.c_str(),message.size(),0,addr.Addr(),addr.Length());
// }
//任务一般都设置为无参
using task_t=std::function<void()>;
// ./udp_server serverport
int main(int argc,char*argv[])
{
if(argc!=2)
{
Usage(argv[0]);
exit(0);
}
//打印日志策略
EnableConsoleStrategy();
uint16_t port=std::stoi(argv[1]);
//1. 消息转发功能
std::unique_ptr<Route> r=std::make_unique<Route>();
//2. 线程池对象
auto tp= ThreadPool<task_t>::GetInstance();
//3. 服务器对象
std::unique_ptr<ChatServer> usvr=std::make_unique<ChatServer>(port,[&r,&tp](int sockfd,std::string message,InetAddr addr){
task_t task=std::bind(&Route::RouteMessageToAll,r.get(),sockfd,message,addr);
tp->Enqueue(task);
});
//智能指针不用手动delete防止内存泄漏
// std::unique_ptr<ChatServer> usvr=std::make_unique<ChatServer>(port,[]
// (const std::string&word,const std::string&whoip,uint16_t whoport)->std::string{
// });
usvr->Init();
usvr->Start();
return 0;
}
Makefile
.PHONY:all
all:chat_client chat_server
chat_client:ChatClient.cc
g++ -o $@ $^ -std=c++17 -lpthread
chat_server:ServerMain.cc
g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
rm -f chat_client chat_server
此篇完。

896

被折叠的 条评论
为什么被折叠?



