当前位置: 首页 > news >正文

UDP套接字编程(代码)

什么是socket套接字编程?

通过Ip地址 + 端口号这种方式定位一台主机,这样的方式我们就叫做socket套接字。

Udp Socket

接口介绍

这些案列我们使用的接口基本都是一样的,所以在这里我先把接口介绍完,具体的细节后面在说明。

创建socket

#include <sys/socket.h>
int socket(int domain, int type, int protocol); //创建套接字
domain: AF_INET //- 代表网络通信
type:   SOCK_DGRAM  //udp通信
    (Supports datagrams (connectionless, unreliable messages of a fixed maximum length))                          
protocol: 这个我们默认给 0 就可以,前面的已经可以表示我们是网络udp通信了。

    返回值:如果成功返回文件描述符(整数),失败则是-1//所以我们网络通信在Linux当中也是文件读写操作

绑定套接字 bind

网络字节序:

  • 在我们计算机当中,大于一个字节的数据就要区分如何存储了,所以我们计算机当中有大端存储和小端存储。

大端存储:高权值位的存储在低地址处,低权值位的存储在高地址处。

小端存储:高权值位的存储在高地址处,低权值位的存储在低地址处。

而在网络当中也是一样的,网络字节序为大端字节序,一般在网络通信当中,无论是大端机器还是小端机器都要转换一下(代码的可移植性)。

而转换操作C语言也为我们提供了库函数,如下:

//这里我们主要是为了转换端口号,端口号为无符号16字节,所以我们只关注前两个
#include <arpa/inet.h> //所需头文件

uint16_t htons(uint16_t hostshort); //主机序列转为网络序列
uint16_t ntohs(uint16_t netshort);  //网络序列转为主机序列
//uint32_t htonl(uint32_t hostlong);
// uint32_t ntohl(uint32_t netlong);


in_addr_t inet_addr(const char *cp); //把字符串转为四字节的网络字节序
// typedef uint32_t in_addr_t; 32位无符号整形
///
//把网络字节序转为 192.168.132.10 这样的字符串
//char Ip[1024];
//::inet_ntop(AF_INET,&(sockaddr_in.sin_addr),Ip,sizeof(Ip) 用法
const char *inet_ntop(int af, const void *restrict src,
char dst[restrict.size], socklen_t size);
int af                      : 需要转换的数据类型 (AF_INET) 
const void *restrict src    : sockaddr_in 结构体中的sin_addr (无符号整数)
char dst[restrict.size]     : 存放结果的字符数组
socklen_t size              : 存放结果的数组大小
///
#define AF_INET   PF_INET
#define PF_INET   2 /* IP protocol family.  */                    
AF_INET本质是一个整数
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen); //绑定套接字
/*
这里的const struct sockaddr *addr,虽然参数是这个,但是在网络基础我们也说了,这个是数据结构是为了
统一接口而设定的,真正我们要用的是 struct sockaddr_in 这个结构体。所以最后我们需要强壮一下。
*/
在绑定套接字的时候,我们需要创建struct sockaddr_in 这个结构体,同时需要自己初始化一些属性。
#include <netinet/in.h> //所需头文件
struct sockaddr_in {
sa_family_t     sin_family;     /* AF_INET */
in_port_t       sin_port;       /* Port number */
struct in_addr  sin_addr;       /* IPv4 address */
};
如上,我们真正所需要设置的就是上面三个值,其实这个结构体中还有一个成员(如下),不过这个我们一般不管。
/* Pad to size of `struct sockaddr'.  */
// unsigned char sin_zero[sizeof (struct sockaddr)
// - __SOCKADDR_COMMON_SIZE
// - sizeof (in_port_t)
// - sizeof (struct in_addr)];
//
那么下面三个参数如何设置呢?
struct sockaddr_in {
sa_family_t     sin_family;     /* AF_INET */
in_port_t       sin_port;       /* Port number */
struct in_addr  sin_addr;       /* IPv4 address */
};
//struct in_addr  sin_addr 内容解析
struct in_addr
{
in_addr_t s_addr;//32位无符号整型 其实就是一个32为无符号整形
// typedef uint32_t in_addr_t;
};

struct sockaddr_in local; //名字以local为例子
local.sin_family = AF_INET ; //设置网络通信
local.sin_port = ::htons(port) //把端口转为网络字节序
    local.sin_addr.s_addr = INADDR_ANY; //32位无符号整型
#define INADDR_ANY    ((in_addr_t) 0x00000000) //这个表示我们这个服务端可以接受任意IP地址的数据
//一般我们服务器的来源IP都设置位 000...,而不是指定一个IP地址。
  • 注意: 这里要注意我们服务端需要进行绑定操作,一般服务器都会有一个固定的端口号。而对于客户端而言,在01023个端口号被一些公司厂家等占用,例如HTTP端口为80等等。而客户端我们用户选择的范围就是102465536。但是用户并不知道自己的电脑上究竟哪些端口被占用了,如果用户使用了一个正在被使用的端口号,那么另外一个进程就使用不了了,所以为了避免这个问题,客户端我们无需进行绑定操作,操作系统自动帮我们绑定一个端口号。

接受消息发送消息

//接受消息
#include <sys/socket.h> //头文件

ssize_t recvfrom(int sockfd, void buf[restrict.len], size_t len,
int flags,
struct sockaddr *_Nullable restrict src_addr,
    socklen_t *_Nullable restrict addrlen);

$ sockfd                   :创建socket的返回值(文件描述符)
$ void buf[restrict.len]   :接受数据的数组
$ size_t len               :接受数据的大小(数组大小)-1   
$ int flags                :0 , 阻塞等待
$ struct sockaddr *_Nullable restrict src_addr
- 自己创建一个sockaddr_in结构体,用来接受消息发送端的IP地址和端口信息。
$ socklen_t *_Nullable restrict addrlen);
自己创建的sockaddr_in结构体大小 

///

//发送消息
#include <sys/socket.h> //头文件

ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);

$ sockfd                   :创建socket的返回值(文件描述符)
$ void buf[restrict.len]   :发送数据的数组
$ size_t len               :发送数据的大小(数组大小)-1   
$ int flags                :0 
$ struct sockaddr *_Nullable restrict src_addr
包含目的主机的IP地址和端口信息。(struct sockaddr_in) 上方自己创建的结构体
$ socklen_t *_Nullable restrict addrlen (sizoef(struct sockaddr_in));

Echo Server

代码目的:实现客户端向服务端发送信息,服务端给我们返回消息。

# Udp_server.hpp
#pragma once 
#include <iostream>
#include <cstdint>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Commn.hpp"
#include "Log.hpp"
#include <string>
#include <arpa/inet.h>
using namespace LogModule;
const uint16_t gport = 8888;
class Udp_server
{
public:
Udp_server()
:_port(gport)
{}
void Init()
{
    _socketfd = ::socket(AF_INET,SOCK_DGRAM,0); //获取文件描述符
    if (_socketfd < 0) 
    {
        Die(SOCKET_ERROR);
    }
    LOG(LogLevel::DEBUG) << "socket create success, fd is " << _socketfd;
    //进行绑定
    sockaddr_in local;
    local.sin_family = AF_INET; //本质也是无符号16位整数
    local.sin_port = ::htons(_port); //主机序列转为网络序列
    local.sin_addr.s_addr = INADDR_ANY; //s2位无符号整型
    socklen_t len = sizeof(local);
    int n = ::bind(_socketfd,CONV(&local),len);
    if (n < 0)
    {
        Die(BIND_ERROR);
    }
    LOG(LogLevel::DEBUG) << "bind create success";
}
void Start()
{
    char buffer[1024];
    sockaddr_in peer; //对端消息
    socklen_t len = sizeof(peer); //一定要初始化!!!!
    while(true)
        {
            int n = ::recvfrom(_socketfd,buffer,sizeof(buffer)-1,0,CONV(&peer),&len);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string client = "echo server says# ";
                client += buffer;
                char Ip[64];
                ::inet_ntop(AF_INET,&(peer.sin_addr),Ip,64); //把网络ip地址转为点分十进制 192.132.168.10
                std::string ip = Ip;
                std::string port = std::to_string(ntohs(_port));
                LOG(LogLevel::INFO) << ip << ":" << port << "says# " << buffer;

                ::sendto(_socketfd,client.c_str(),client.size(),0,CONV(&peer),len);
            }
        }
}
~Udp_server()
{}
private:
int _socketfd;
uint16_t _port;
};
//Udp_serverMain.cc
#include "udp_server.hpp"

#include <memory>
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cout << "Please use for ./server_udp + port!!" << std::endl;
        Die(USE_ERROR);
    }
    std::shared_ptr<Udp_server> server = std::make_shared<Udp_server>(); //使用智能指针创建对象
    server->Init(); //初始化套接字
    server->Start();//开始通信
    return 0;
}
//Udp_clientMain.cc
#include "udp_client.hpp"
#include "Commn.hpp"
#include "Log.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace LogModule;
#include <limits>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Please use for ./client_udp + ip + port" << std::endl;
        Die(USE_ERROR);
    }
    int socketfd = ::socket(AF_INET6, SOCK_DGRAM, 0);

    // char = argv[1];
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    if (socketfd < 0)
    {
        Die(SOCKET_ERROR);
    }
    LOG(LogLevel::DEBUG) << "socket create success, fd is " << socketfd;

    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = ::inet_addr(ip.c_str()); 
    server.sin_port = ::htons(port);
    socklen_t len = sizeof(server);
    std::string message;

    while(true)
        {
            std::cout << "Please Enter# ";
            getline(std::cin,message);
            ::sendto(socketfd,message.c_str(),message.size(),0,CONV(&server),len);   

            sockaddr_in peer;
            socklen_t Len = sizeof(peer); //一定要初始化

            char inbuffer[1024];
            int n = ::recvfrom(socketfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&Len);
            if (n > 0)
            {
                inbuffer[n] = 0;
                LOG(LogLevel::INFO) << inbuffer;
            }
        }
    return 0;
}

细节问题:

  1. socklen_t len这个参数一定要初始化!!!

  1. 无论是 recvfrom接受网络的消息,还是向网络中消息sendto,如果我们想要使用接受的数据需要把网络中的数据,转化为本机存储序列。向网络中发送数据也需要转化为网络字节序列。

通信效果图:

服务端我们启动的方式为: ./server_udp + 端口号

客户端启动方式:./client_udp + 访问的IP地址 + 对应的端口号

简单版英汉字典

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天

如上图是一份简单字典,我们将实现用户输入单词,服务端返回单词意思的功能。

实现代码

//Dictionary.hpp
#pragma once 
#include <unordered_map>
#include <string>
#include <fstream>
#include <iostream>
class Dictionary
{
public:
static std::unordered_map<std::string,std::string> _dict;
static Dictionary*_instance;
static Dictionary* Getinstace()
{   
    if (_instance==nullptr)
    {
        _instance = new Dictionary();
        return _instance;
    }
    return _instance;
}
~Dictionary()
{
    if(_instance)
    {
        delete _instance;
        _instance = nullptr;
    }
}
std::string Translate(std::string& word)
{
    if (_dict.find(word) == _dict.end()) return "NONE";
    return _dict[word];
}
void Debug()
{
    for (auto& x : _dict)
        {
            std::cout << x.first << " : " << x.second << std::endl;
        }
}
private:
Dictionary() 
{
    // std::cout << "开始加载字典" << std::endl;
    LoadDictionary();
}
Dictionary(const Dictionary& dic) = delete;
Dictionary& operator =(const Dictionary& dic) = delete;

void LoadDictionary()
{
    std::ifstream input("Dict.txt");
    if (!input.is_open()) 
    {
        std::cout << "无法打开文件" << std::endl;
        return;
    }
    std::string line;
    while(std::getline(input,line))
        {
            //apple: 苹果 - 数据格式
            size_t pos = line.find(':');
            std::string fruit = line.substr(0,pos);
            std::string content = line.substr(pos+2);
            _dict[fruit] = content;
        }
}
};
Dictionary* Dictionary::_instance = nullptr;
std::unordered_map<std::string,std::string> Dictionary::_dict;
//Udp_server.hpp
#pragma once 
#include <iostream>
#include <cstdint>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Commn.hpp"
#include "Log.hpp"
#include <string>
#include <arpa/inet.h>
#include <functional>
using namespace LogModule;
const uint16_t gport = 8888;
using func_t = std::function<std::string(std::string&)>;

class Udp_server
{
public:
Udp_server(func_t func,uint16_t port=gport) //把翻译函数传进来
:_port(port),
_translate(func)
{

}
void Init()
{
    _socketfd = ::socket(AF_INET,SOCK_DGRAM,0); //获取文件描述符
    if (_socketfd < 0) 
    {
        Die(SOCKET_ERROR);
    }
    LOG(LogLevel::DEBUG) << "socket create success, fd is " << _socketfd;
    //进行绑定
    sockaddr_in local;
    local.sin_family = AF_INET; //本质也是无符号16位整数
    local.sin_port = ::htons(_port); //主机序列转为网络序列
    local.sin_addr.s_addr = INADDR_ANY; //s2位无符号整型
    socklen_t len = sizeof(local);
    int n = ::bind(_socketfd,CONV(&local),len);
    if (n < 0)
    {
        Die(BIND_ERROR);
    }
    LOG(LogLevel::DEBUG) << "bind create success";
}
void Start()
{
    char buffer[1024];
    sockaddr_in peer; //对端消息
    socklen_t len = sizeof(peer); //一定要初始化!!!!
    while(true)
        {
            int n = ::recvfrom(_socketfd,buffer,sizeof(buffer)-1,0,CONV(&peer),&len);
            if (n > 0)
            {
                buffer[n] = 0;
                std::string word = buffer;
                std::string client = _translate(word); //存储翻译的结果

                //转换
                char Ip[64];
                ::inet_ntop(AF_INET,&(peer.sin_addr),Ip,64); //把网络ip地址转为点分十进制 192.132.168.10
                std::string ip = Ip; //等到客户端IP地址
                std::string port = std::to_string(ntohs(_port)); //等到客户端端口号
                LOG(LogLevel::INFO) << ip << ":" << port << "says# " << buffer;
                //发送给客户端
                ::sendto(_socketfd,client.c_str(),client.size(),0,CONV(&peer),len);
            }
        }
}
~Udp_server()
{}
private:
int _socketfd;
uint16_t _port;
func_t _translate;
};
//Udp_serverMain.cc
#include "udp_server.hpp"
#include "Dicitonary.hpp"

#include <memory>
int main(int argc, char* argv[])
{
    // std::string t;
    // Dictionary::Getinstace()->Debug();
    // while(true)
    // {
    //     std::cin >> t;
    //     std::cout << "查询: " << t << " ";
    //     std::cout << " 结果为: " << Dictionary::Getinstace()->Translate(t) << std::endl;
    // }
    if (argc != 2)
    {
        std::cout << "Please use for ./server_udp + port!!" << std::endl;
        Die(USE_ERROR);
    }
    auto dict = Dictionary::Getinstace(); //获取字典单例
    //把翻译函数注册进去
    std::shared_ptr<Udp_server> server = std::make_shared<Udp_server>(
        [&dict](std::string&word){ return dict->Translate(word);
    });
    server->Init();
    server->Start();
    return 0;
}
//Udp_clientMain.cc
#include "udp_client.hpp"
#include "Commn.hpp"
#include "Log.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace LogModule;
#include <limits>

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Please use for ./client_udp + ip + port" << std::endl;
        Die(USE_ERROR);
    }
    int socketfd = ::socket(AF_INET6, SOCK_DGRAM, 0);

    // char = argv[1];
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    if (socketfd < 0)
    {
        Die(SOCKET_ERROR);
    }
    LOG(LogLevel::DEBUG) << "socket create success, fd is " << socketfd;
    
    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = ::inet_addr(ip.c_str()); 
    server.sin_port = ::htons(port);
    socklen_t len = sizeof(server);
    std::string message;
    
    while(true)
    {
        std::cout << "Please Enter# ";
        getline(std::cin,message);
        // std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
        ::sendto(socketfd,message.c_str(),message.size(),0,CONV(&server),len);   

        sockaddr_in peer;
        socklen_t Len = sizeof(peer); //一定要初始化

        char inbuffer[1024];
        int n = ::recvfrom(socketfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&Len);
        if (n > 0)
        {
            inbuffer[n] = 0;
            LOG(LogLevel::INFO) << inbuffer;
        }
    }
    return 0;
}

细节问题

  1. 在传入翻译函数的时候,需要使用lambda表达式,我们需要捕获单例对象的地址,然后才可以在lambda表达式中使用。

通信效果图

对于字典当中没有的数据直接返回NONE。

简单聊天室

实现思路

通过前面两个案例我们可以发现,在我们**服务端绑定Ip和端口是一样的,**同时服务端接受消息的时候都需要解析网络中的IP和端口号。那么我们可以把这两部进行封装。

所以我们可以创建 InetAddr.hpp

#pragma once 
#include <cstdint>
#include <string>
#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Commn.hpp"
#include <cstring>
class InetAddr
{
public:
InetAddr(sockaddr_in& addr) //传入sockaddr_in默认把网络字节序解析得到IP字符串与主机序列的端口号
:_net_addr(addr)
{
    Net2HostIp();
    Net2HostPort();
}
void Net2HostIp()
{
    char Ip[1024];
    memset(Ip,0,sizeof(Ip));
    //::inet_ntop(AF_INET,&(_net_addr.sin_addr),Ip,sizeof(Ip)); //把网络ip地址转为点分十进制 192.132.168.10
    if(::inet_ntop(AF_INET,&(_net_addr.sin_addr),Ip,sizeof(Ip)) == nullptr){
        perror("inet_ntop failed");
        _ip = "InvalidIP";
    }else{
        _ip = Ip;
    }
}

void Net2HostPort()
{
    _port = ::ntohs(_net_addr.sin_port);
}

InetAddr(uint16_t port) //如果只是传入端口号,则是初始化服务端bindIP地址 + 端口号。
:_port(port)
{
    _net_addr.sin_family = AF_INET;
    _net_addr.sin_port = ::htons(_port);
    _net_addr.sin_addr.s_addr = INADDR_ANY; //IP地址默认接受任意来源的IP
}

sockaddr* NetAddr()
{
    return (sockaddr*)(&_net_addr);
}

socklen_t Len() { return sizeof(_net_addr);}
std::string IP() {return _ip;}
uint16_t Port() { return _port;}

std::string netaddr() { //返回Ip地址和端口号的字符串形式。
    std::string Ip = _ip;
    std::string pp = std::to_string(_port);
    std::string ans = Ip +":" + pp;
    return ans;
}
private:
uint16_t _port;
std::string _ip = "";
sockaddr_in _net_addr;
};

那么如果管理每一个客户端呢?

//User.hpp
#pragma once 
#include <iostream>
#include <string>
#include "InetAddr.hpp"
#include "Log.hpp"
#include <vector>
#include <memory>
#include <list>
#include "Mutex.hpp"
#include <algorithm>

using namespace MpthreayMutex;
using namespace LogModule;
class UserStrategy //用户接口类
{
public:
UserStrategy() = default;
virtual ~UserStrategy() {}
virtual void SendMessage(int socketfd, std::string message) = 0; //抽象类
virtual std::string Info() = 0; 
virtual bool operator==(InetAddr& addr) = 0;
};

class User : public UserStrategy
{  
public:
User(InetAddr& addr) //使用我们自己的InetAddr初始化
:_net_addr(addr)
{}

void SendMessage(int socketfd, std::string message)
{
    ::sendto(socketfd,message.c_str(),message.size(),0,_net_addr.NetAddr(),_net_addr.Len());
}
std::string Info()
{
    return _net_addr.netaddr();
}
virtual bool operator==(InetAddr& addr) {return _net_addr.IP()==addr.IP() && _net_addr.Port() == addr.Port();}
~User(){}
private:
InetAddr _net_addr;
};

class UserManage
{
public:
UserManage()
{}

void AddUser(InetAddr& in)
{
    LockGround lock(_mtx); //要加锁,我们链表被多线程使用是一份临界资源。
    for (auto user_ptr:_user_set)
        {
            if (*user_ptr== in) 
            {
                std::cout << in.netaddr() << "已经存在" << std::endl;
                return;
            }
        }
    _user_set.emplace_back(std::make_shared<User>(in)); //指针指向的是子类
    LOG(LogLevel::DEBUG) << _user_set.back()->Info() << "添加成功";
    PrintUser(); //打印所有的在线用户
}

void Route(int fd,std::string message)
{
    LockGround lock(_mtx);
    for (auto _ptr:_user_set) 
        {
            _ptr->SendMessage(fd,message);
        }
}

void Delete(InetAddr& out)
{
    LockGround lock(_mtx);
    //remove_if 把目标值放到迭代器的末尾,然后返回这个迭代器
    auto pos = std::remove_if(_user_set.begin(),_user_set.end(),[&](std::shared_ptr<UserStrategy>& id){
        return *id==out;
    });
    _user_set.erase(pos,_user_set.end()); //一个迭代器区间范围
    PrintUser();
}
void PrintUser()
{
    for (auto _ptr : _user_set)
        {
            LOG(LogLevel::INFO) <<  "在线用户" << _ptr->Info();
        }
}
~UserManage(){}
private:
//使用接口类构造,User子类创建元素,父类指针,指针指向的是子类,形成多态。
std::list<std::shared_ptr<UserStrategy>> _user_set; //使用链表管理所有的用户
Mutex _mtx;
};
//Udp_server.hpp
#pragma once 
#include <iostream>
#include <cstdint>
#include <sys/socket.h>
#include <netinet/in.h>
#include "Commn.hpp"
#include "Log.hpp"
#include <string>
#include <arpa/inet.h>
#include <functional>
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include <cstring>

using namespace LogModule;
const uint16_t gport = 8888;
using func_t = std::function<std::string(std::string&)>;
using adduser_t = std::function<void(InetAddr&)>;
using deluser_t = std::function<void(InetAddr&)>;
using route_t = std::function<void(int fd,std::string& message)>;
using task_t = std::function<void()>;
using namespace MyThreadPool;

class Udp_server
{
public:
Udp_server(uint16_t port=gport)
:_port(port)
{

}
void Init()
{
    _socketfd = ::socket(AF_INET,SOCK_DGRAM,0); //获取文件描述符
    if (_socketfd < 0) 
    {
        Die(SOCKET_ERROR);
    }
    LOG(LogLevel::DEBUG) << "socket create success, fd is " << _socketfd;
    //进行绑定
    InetAddr local(_port); //只需要传入一个端口号即可。

    // local.sin_family = AF_INET; //本质也是无符号16位整数
    // local.sin_port = ::htons(_port); //主机序列转为网络序列
    // local.sin_addr.s_addr = INADDR_ANY; //s2位无符号整型
    // socklen_t len = sizeof(local);

    int n = ::bind(_socketfd,local.NetAddr(),local.Len());
    if (n < 0)
    {
        Die(BIND_ERROR);
    }
    LOG(LogLevel::DEBUG) << "bind create success";
}

void Register(adduser_t adduser,route_t route,deluser_t deluser)
{
    _adduser = adduser;
    _route = route;
    _deluser = deluser;
}

void Start()
{
    char buffer[1024];
    sockaddr_in peer; //对端消息
    socklen_t len = sizeof(peer); //一定要初始化!!!!
    while(true)
        {
            int n = ::recvfrom(_socketfd,buffer,sizeof(buffer)-1,0,CONV(&peer),&len);
            if (n > 0)
            {
                buffer[n] = 0;
                InetAddr Addr(peer);
                std::string message;

                if (strcmp(buffer,"QUIT") == 0)
                {
                    _deluser(Addr);
                    message = "我走了,你们聊!";
                }
                else 
                {
                    _adduser(Addr);
                    message = Addr.netaddr() + " #" + buffer;
                }

                LOG(LogLevel::INFO) << message;
                task_t t = std::bind(Udp_server::_route,_socketfd,message); //也可以直接绑定
                // task_t t = [=]() mutable{ //这里要传值传递,传引用传递的话可能会出现乱码!!!!!!!!!!!!!!!!!!!!!!!!
                //     _route(_socketfd, message);
                // };
                threadpool<task_t>::Getinstance()->Equeue(t);
                
                // std::string client = "echo server says# ";
                // client += buffer;
                // //转换
                // // char Ip[64];
                // // ::inet_ntop(AF_INET,&(peer.sin_addr),Ip,64); //把网络ip地址转为点分十进制 192.132.168.10
                // // std::string ip = Ip;
                // // std::string port = std::to_string(ntohs(_port));
                // LOG(LogLevel::INFO) << Addr.IP() << ":" << Addr.Port() << "says# " << buffer;
                // ::sendto(_socketfd,client.c_str(),client.size(),0,CONV(&peer),len);
            }
        }
    }
    ~Udp_server()
    {}
private:
    int _socketfd;
    uint16_t _port;
    adduser_t _adduser;
    route_t _route;
    deluser_t _deluser;
};
//Udp_serverMain.cc
#include "udp_server.hpp"
#include <memory>
#include "User.hpp"

int main(int argc, char* argv[])
{

    if (argc != 2)
    {
        std::cout << "Please use for ./server_udp + port!!" << std::endl;
        Die(USE_ERROR);
    }

    std::shared_ptr<UserManage> um = std::make_shared<UserManage>();

    std::shared_ptr<Udp_server> server = std::make_shared<Udp_server>();

    server->Init();
    //为了降低程序的耦合度,把用户管理的方式注册进去,而不是当作服务端对象的参数。
    //这样以后有什么新的方法,直接注册进去就可以。
    server->Register([&um](InetAddr& in){um->AddUser(in);}, 
        [&um](int fd,std::string message){um->Route(fd,message);},
        [&um](InetAddr& out){um->Delete(out);});
    server->Start();
    return 0;
}
//Udp_clientMain.cc
#include "udp_client.hpp"
#include "Commn.hpp"
#include "Log.hpp"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace LogModule;
#include <limits>
#include <signal.h>
int socketfd = -1;
sockaddr_in server;

void ClientQuit(int signo)
{
    (void)signo;
    std::string quit = "QUIT";
    ::sendto(socketfd,quit.c_str(),quit.size(),0,CONV(&server),sizeof(server)); //退出的时候发送QUIT   
    exit(0);
}

void* Receive(void* args)
{
    while(true)
        {
            //出现的问题 !!!
            sockaddr_in peer; //这个必须在循环里创建,udp每一个都有独立的数据报,如果是全局的,那么len大小会错误,会导致输出不能正常显示。
            socklen_t len = sizeof(peer);

            char inbuffer[1024];
            int n = ::recvfrom(socketfd,inbuffer,sizeof(inbuffer)-1,0,CONV(&peer),&len);

            if (n > 0)
            {
                inbuffer[n] = 0;
                std::cerr << inbuffer << std::endl;
            }

        }
    return nullptr;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cout << "Please use for ./client_udp + ip + port" << std::endl;
        Die(USE_ERROR);
    }

    signal(2,ClientQuit); //处理用户退出
    socketfd = ::socket(AF_INET6, SOCK_DGRAM, 0);

    // char = argv[1];
    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    if (socketfd < 0)
    {
        Die(SOCKET_ERROR);
    }
    LOG(LogLevel::DEBUG) << "socket create success, fd is " << socketfd;


    //手动写
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = ::inet_addr(ip.c_str()); 
    server.sin_port = ::htons(port);
    socklen_t len = sizeof(server);

    std::string message;

    pthread_t tid;
    int ret = pthread_create(&tid,nullptr,Receive,nullptr); //创建线程来发送消息

    //如何让用户一进来就可以看到服务器当中发送的消息呢?
    //向服务器发送消息,服务器轮询发送给所有的用户
    std::string online = " ...来了哈!";
    ::sendto(socketfd,online.c_str(),online.size(),0,CONV(&server),len);

    while(true)
        {
            std::cout << "Please Enter# ";
            getline(std::cin,message);
            // std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
            ::sendto(socketfd,message.c_str(),message.size(),0,CONV(&server),len);   
        }
    return 0;
}

通信效果图

当我们按下 ctrl + c,的时候自动发送QUIT消息,然后退出进程。

为了方便观看,我们标准错误重定向到管道文件当中,再新开一个终端从管道文件当中读取,这样就可以把服务端返回的消息与输出结果分割开来。注意:管道文件必须读端和写端都打开我们才可以看到结果,不然会一直阻塞住。

问题集

  1. 这个必须在循环里创建

sockaddr_in peer; // 客户端Recv的结构体

socklen_t len = sizeof(peer);

//这个必须在循环里创建,udp每一个都有独立的数据报,如果是全局的,那么len大小会错误,会导致输出不能正常显示。

  1. 一定要初始化!!! ->socklen_t len = sizeof(peer);
  2. 传值传递

// task_t t = = mutable{ //这里要传值传递,传引用传递的话可能会出现乱码!! 位置 -> 服务端创建任务

// _route(_socketfd, message);

// };

其他所需hpp文件

//Log.hpp
#pragma once 
#include <iostream>
#include <memory>
#include <unistd.h>
#include <string>
#include "Mutex.hpp"
#include <filesystem>
#include <fstream>
#include <time.h>
#include <sstream>

namespace LogModule 
{
    using namespace MpthreayMutex;
    const std::string defaultlogpath = "./log/";    //log日志所保存的路径
    const std::string defaultlogname = "log.txt";  //log文件名

    //日志等级
    enum class LogLevel
    {
    DEBUG = 1,  //调式
    INFO,       //正常
    WARNING,    //警告
    ERROR,      //错误
    FATAL       //致命的
};

    //获取时间信息
    std::string GerCurrTime()
    {
        time_t timp = time(nullptr);
        struct tm t;
        localtime_r(&timp,&t);
        char buffer[1024];
        snprintf(buffer,sizeof(buffer),"%4d-%02d-%02d %02d:%02d:%02d", 
            t.tm_year+1900,
            t.tm_mon+1,
            t.tm_wday+1,
            t.tm_hour,
            t.tm_min,
            t.tm_sec);
        return buffer;
    }
    std::string loglevelToString(LogLevel loglevel)
    {
        switch(loglevel)
            {
                case LogLevel::DEBUG:
                    return "DEBUG";
                case LogLevel::INFO:
                    return "INFO";
                case LogLevel::WARNING:
                    return "WARNING";
                case LogLevel::ERROR:
                    return "ERROR";
                case LogLevel::FATAL:
                    return "FATAL";
                default:
                    return "NONE";                                                                            
            }
    }

    //1.策略模式
    class Stragety
    {
    public:
    virtual ~Stragety() = default; //多态的话先把析构处理了
    virtual void Synclog(const std::string& message) = 0;   //基类提供抽象类,有子类提供实现。1. 不能实列化对象 2. 但是可以创建指针
};

    //默认策略模式 - 向显示器输出日志信息
    class ConsoleStragety : public Stragety
    {
    public:
    //override: 如果没有不是虚函数重写就报错
    void Synclog(const std::string& message) override 
    {
        LockGround lock(_mtx); // 多线程的话日志输出也是临界资源
        std::cout << message << std::endl; 
    }
    ~ConsoleStragety()
    {

    }
    private:
    Mutex _mtx; 
};

    class FileLogStragety : public Stragety
    {
    public:
    FileLogStragety(const std::string& logpath = defaultlogpath,const std::string logname = defaultlogname)
    :_logpath(logpath),
    _logname(logname)
    {
        LockGround lock(_mtx);
        //检查对应的目录以及文件是否存在
        if (std::filesystem::exists(_logpath)) //如果存在的话直接返回就可以
        {
            return;
        }
        else 
        {
            //创建文件夹
            try 
                {
                    std::filesystem::create_directories(_logpath);
                }
            catch(const std::filesystem::filesystem_error & e)
                {
                    std::cout << e.what() << std::endl;
                }
        }
    }
    void Synclog(const std::string& message) override
        {
            LockGround lock(_mtx);
            std::string filename = _logpath + _logname;
            std::ofstream  out(filename.c_str(),std::ios::app); //如果不存在创建,存在追加
            if (!out.is_open()) return;
            out << message << std::endl;
            out.close();
        }
        ~FileLogStragety()
        {}
    private:
        std::string _logpath;
        std::string _logname;
        Mutex _mtx; 
    };  


    //具体的日志类
    class Logger
    {
    public:
        Logger()
        {
            _strategy = std::make_shared<ConsoleStragety>();
        }
        void EnableConsoleStragety()
        {
            _strategy = std::make_shared<ConsoleStragety>();
        }
        void EnableFilesystemStragety()
        {
             _strategy = std::make_shared<FileLogStragety>();
        }

        //为什么要实现这一个内部类呢?
        class LogMessage
        {
        public:
            LogMessage(LogLevel level,int line,const std::string& filename,Logger& logger)
                :_level(level),
                _line(line),
                _filename(filename),
                _logger(logger)
            {
                //初始化_loginfo 
                //1. 时间:
                std::stringstream ss;
                ss  << "[" << GerCurrTime() << "] "
                    << "[" << loglevelToString(_level) << "] "
                    << "[" << getpid() << "] "
                    << "[" << filename << "] "
                    << "[" << _line << "] " 
                    << " - ";
                _loginfo += ss.str();
            }
            //因为不确定传入什么类型 ,调用方式: LOG(DUBUG) << "hello world" << 3 << 2.2 << 1;
            template<class 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 _loginfo; //总的信息
            LogLevel _level;
            int _line;
            std::string _filename;
            Logger& _logger;
        };

        LogMessage operator()(LogLevel level,int line,const std::string& filename)
        {
            //这里需要注意理论上应该创建两个LogMessage的,但是编译器直接给优化掉了,避免了不必要的拷贝
            return LogMessage(level,line,filename,*this);
        }

        ~Logger()
        {

        }
    private:
        std::shared_ptr<Stragety> _strategy;
    };

    Logger logger;
    //定义调用的宏。,返回的是匿名对象,生命周期在当前行,然后调用LogMessage的析构
    #define LOG(type) logger(type,__LINE__,__FILE__) 
    //定义转换策略模式的宏
    #define ENABLE_CONSOLE_LOG_STRATEGY() logger.EnableConsoleStragety()
    #define ENABLE_FILESYSTEM_LOG_STRATEGY() logger.EnableFilesystemStragety()
}
//Mutex.hpp
#pragma once
#include <pthread.h>
namespace MpthreayMutex
{
    class Mutex
    {
    public:
    Mutex()
    {
        int n = pthread_mutex_init(&_mtx,nullptr);
        (void)n;
    }
    Mutex(const Mutex& _mtx) = delete;
    Mutex& operator=(const Mutex& _mtx) = delete;
    ~Mutex()
    {
        int n = pthread_mutex_destroy(&_mtx);
        (void)n;
    }
    pthread_mutex_t* LockPtr()
    {
        return &_mtx;
    }
    void Lock()
    {
        pthread_mutex_lock(&_mtx);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_mtx);
    }
    private:
    pthread_mutex_t _mtx;
};

    class LockGround
    {
    public:
    LockGround(Mutex& mtx) :_mtx(mtx) 
    {
        _mtx.Lock();            
    }
    ~LockGround()
    {
        _mtx.Unlock();
    }
    private:
    Mutex& _mtx;
};
}
//Cond.hpp
#pragma once
#include "Mutex.hpp"
#include <pthread.h>

namespace MyCond
{
    using namespace MpthreayMutex;
    class Cond
    {
    public:
    Cond()
    {
        int n = pthread_cond_init(&_cond, nullptr);
        (void)n;
    }
    void wait(Mutex& mtx)
    {
        int n = pthread_cond_wait(&_cond, mtx.LockPtr());
        (void)n;
    }
    void Notify() // 通知
    {
        int n = pthread_cond_signal(&_cond);
        (void)n;
    }
    void NotifyAll()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n;
    }
    pthread_cond_t *Adrr()
    {
        return &_cond;
    }
    ~Cond()
    {
        int n = pthread_cond_destroy(&_cond);
        (void)n;
    }

    private:
    pthread_cond_t _cond;
};
}

相关文章:

  • 聊聊langchain4j的MCP
  • TreeKEM 原理解析
  • ACF介绍及选用规则
  • CF1011(Div.2)A~D
  • 数组,指针 易混题解析(二)
  • Python---数据分析(Pandas七:二维数组DataFrame中元素的索引与访问,其他常用方法)
  • Java——ArrayList集合
  • Linux高级IO
  • c++高精度加法
  • JavaScript案例0323
  • 【LC插件开发】基于Java实现FSRS(自由间隔重复调度算法)
  • 虚拟机第二章-类加载子系统
  • c++之迭代器
  • Java 安装开发环境(Mac Apple M1 Pro)
  • Linux Namespace(网络命名空间)系列三 --- 使用 Open vSwitch 和 VLAN 标签实现网络隔离
  • 【Centos7搭建Zabbix4.x监控HCL模拟网络设备:zabbix-server搭建及监控基础05
  • 专题|Python贝叶斯网络BN动态推理因果建模:MLE/Bayes、有向无环图DAG可视化分析呼吸疾病、汽车效能数据2实例合集
  • 实战指南:使用 OpenRewrite 将 Spring Boot 项目从 JDK 8 升级到 JDK
  • 嵌入式项目:利用心知天气获取天气数据实验方案
  • 从指令集鸿沟到硬件抽象:AI 如何重塑手机与电脑编程语言差异——PanLang 原型全栈设计方案与实验性探索1
  • 中国田径巡回赛西安站完赛:男子跳远石雨豪夺冠,女子跳高刘肼毅折桂
  • 天问二号探测器顺利转入发射区,计划5月底择机发射
  • 人民网:激发博物馆创新活力,让“过去”拥有“未来”
  • 人民日报和音:相信中国就是相信明天
  • 首届中国人文学科年度发展大会启幕,共话AI时代人文使命
  • 当“小铁人”遇上青浦,看00后如何玩转长三角铁三