网络基础入门---使用udp协议改进程序

news/2024/5/18 14:41:22 标签: 网络, 网络协议, udp, linux

目录标题

  • 前言
  • 改进一:单词翻译程序
    • 准备工作
    • transform函数的实现
    • init_dictionary函数的实现
    • transform函数的实现
    • 其他地方的修改
    • 测试
  • 改进二:远程指令执行程序
    • popen
    • execCommand函数实现
    • 测试
  • 改进三:群聊程序
    • Usr类
    • onlineUser类
      • adduser
      • delUser
      • isOnline
      • broadcast
    • routeMessage
    • 其他地方的修改
    • 测试

前言

在前面的学习过程中我们知道了socket套接字以及有关的函数,并且使用这些函数实现了一个简单的消息发送的功能,具体内容大家可以点击这个链接进行查看:点击此处查看文章。服务端对应的代码如下:

//udpServer.hpp
#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
static const string defaultIp="0.0.0.0";
static const int gnum =1024;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
class udpServer
{
public:
    udpServer(const uint16_t& port, const string & ip=defaultIp)//构造函数
    //构造函数就负责获取ip和端口号
    :_ip(ip)
    ,_port(port)
    {}
    void initServer()//初始化函数
    //初始化函数里面就创建对应的端口号,然后对端口号进行bind
    {
        _sockfd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd==-1)
        {
            //运行到这里说明创建端口失败
            cout<<"socket error: "<< errno<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
        cout<<"socket success"<<" : "<<_sockfd<<endl;
        struct sockaddr_in local;
        bzero(&local,sizeof(sockaddr_in));
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        //local.sin_addr.s_addr=inet_addr(_ip.c_str());
        local.sin_addr.s_addr=INADDR_ANY;
        int res=bind(_sockfd,(struct sockaddr*)&local,sizeof(local));
        if(res==-1)
        {
            cout<<"bind error: "<<errno<< strerror(errno)<<endl;
            exit(BIND_ERR);
        }   
    }
    void start()//运行函数
    {
        char buffer[gnum]={0};
        for(;;)
        {
            struct sockaddr_in peer; 
            socklen_t len = sizeof(peer); //必填
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            if(s>0)
            {
                buffer[s]=0;
                string clientip=inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port);
                cout << clientip <<"[" << clientport << "]# " << buffer << endl; 
            }
        }
    }
    ~udpServer(){}
private:
    int _sockfd;
    string  _ip;
    uint16_t _port;   
};

客户端对应的代码如下:

//udpClient.hpp
#include <iostream>
#include <string>
#include <strings.h>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR};
class udpClient
{
  public:
  udpClient(const string serverip,const uint16_t port)
  :_serverip(serverip)
  ,_serverport(port)
  ,_sockfd(-1)
  {}
  void initClient()
  {
      //创建套接字
      _sockfd=socket(AF_INET,SOCK_DGRAM,0);
      if(_sockfd==-1)
      {
          cerr<<"socket error: "<<errno<<strerror(errno)<<endl;
          exit(SOCKET_ERR);
      }
      //无需绑定
       cout << "socket success: " << " : " << _sockfd << endl;
  }
  void run()
  {
    struct sockaddr_in server;//用来记录服务端口的信息
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(_serverip.c_str());
    server.sin_port = htons(_serverport);
    string tmp;
    while(1)
    {
      cout << "Please Enter# ";
      cin>>tmp;
      sendto(_sockfd,tmp.c_str(),tmp.size(),0,(struct sockaddr*)&server,sizeof(server));
    }
  }
  ~udpClient()
  {}
  private:  
  int _sockfd;
  string _serverip;
  uint16_t _serverport;
};

大家仔细的阅读一下就可以知道上面代码实现的功能就是客户端使用udp协议像服务端发送一个数据,然后服务端就会将发过来的数据打印到屏幕上,但是这里存在两个问题:数据发送过来难道就是简单的打印到屏幕上而不作其他的处理了吗?难道就只能客户端向服务端发送数据而不能反过来吗?答案肯定不是的,我们要对数据进行处理让其执行一些任务,然后将任务处理的结构发送给客户端,所以就有了这里的不同的改进方式,那么在下面的文章中我们将对上面的代码进行三种形式的改进,第一个改进就是实现一个单词翻译程序,客户端发送一个英文给服务端,服务端对英文进行翻译得到中文,然后将中文发送给客户端并进行打印,第二种形式就类似于我们使用的云服务器,我们向客户端输入指令,客户端就会将指令发送给服务端,服务端执行对应的指令最后再将指令执行的结果发给客户端并进行打印,第三种就类似于qq的群聊系统,每个人都以客户端的身份登录系统然后发送消息,只要是登录的用户都可以接收到他发送的消息,所以这就相当于一个微信群或者qq群,只不过我们这里退出系统之后就无法收到别人的消息。那么这就是我们要实现的三种形式就下来我们就先实现第一种改动。

改进一:单词翻译程序

准备工作

首先udpServer.hpp文件里面装的是描述服务端的类,而服务端的可执行程序则是装在udpServer.cc文件里面,我们要实现的单词翻译功能肯定是集成在一个名为transform的函数,那么在udpServer.hpp中添加一个可以接收transform函数的function重命名,并在udpServer类中添加一个function成员变量,比如说下面的代码:

using namespace std;
static const string defaultIp="0.0.0.0";
static const int gnum =1024;
enum {USAGE_ERR = 1, SOCKET_ERR, BIND_ERR,OPEN_ERR};
typedef function<void(int,string,uint16_t,string)> func_t;
class udpServer
{
public:
    udpServer(func_t func,const uint16_t& port, const string & ip=defaultIp)//构造函数
    //构造函数就负责获取ip和端口号
    :_ip(ip)
    ,_port(port)
    ,_func(func)
    {//...}
    void initServer(){//...}
    void start(){//...}
    ~udpServer(){}
private:
    int _sockfd;
    string  _ip;
    uint16_t _port;
    func_t _func; 
};

这样在创建udpServer对象的时候就可以传递不同的函数让其实现不同功能的通信,这样我们就可以做到功能实现和网络通信之间的解耦,未来我们想要实现不同功能的话就只用传递不同的函数即可不需要改变网络通信的实现,那么udpServer.hpp文件里面就只装载和网络通信有关的内容,而其他与功能实现的相关函数就全部放到udpServer.cc文件里面。

transform函数的实现

因为我们要实现英文到中文的转换,所以我们这里得先创建一个文件,文件中记载着因为对应的中文意思,比如说下面的图片:
在这里插入图片描述
左边是英文右边是中文,两者之间用::来进行分割,一行只对应一个单词,那么这里我们就先创建一个全局的哈希表,然后init_dictionary函数的作用就是该文件中的内容获取出来,将每一行的内容进行分割把英文作为key中文作为value插入到哈希表中,那么这里就创建一个名为cut_string的函数专门用来分割字符串,该函数需要一个string类型的参数表示要分割的字符串,还需要两个string类型的参数用来存放你分割出来的key和value,最后需要一个string类型的参数表示你的文件中是以什么来作为中英文的分割值,因为字符串的分割可能成功也可能失败,而哈希表只插入分割成功的行所以该函数的返回值就是bool类型,那么这里的函数声明如下:

static bool cut_string(const string& target,string* key,string *value,const string& sep)
{
}

首先使用find函数在target中查找sep出现的位置,然后判断一下target中是否存在sep,如果不存在的话就直接返回一个false:

static bool cut_string(const string& target,string* key,string *value,const string& sep)
{
    auto pos=target.find(sep);
    if(pos==string::npos)
    {
        return false;
    }
}

如果存在我们就可以使用substr函数对其进行切割
在这里插入图片描述

pos记录的是分隔符第一次出现的下表,以pos作为长度刚好是一个左边又开的范围,所以key值就是subs(0,pos),然后对pos的值加上分隔符的长度就可以来到value的第一个下标,再直接进行切割就可以得到value,那么这里的代码就如下:

static bool cut_string(const string& target,string* key,string *value,const string& sep)
{
    auto pos=target.find(sep);
    if(pos==string::npos)
    {
        return false;
    }
    *key=target.substr(0,pos);
    *value=target.substr(pos+sep.size());
    return true;
}

init_dictionary函数的实现

有了cut_string函数之后就可以很好的实现init_dictionary函数,首先创建一个ifstream对象用来打开装有单词翻译的文件,然后按行读取每一行的内容:

const string dictTxt="./dict.txt";
unordered_map<string, string> dict;
void init_dictionary()
{
    ifstream in(dictTxt,std::ios::binary);
    cout<<"reload"<<endl;
    if(!in.is_open())
    {
        cerr << "open file " << dictTxt << " error" << endl;
        exit(OPEN_ERR);
    }
    string line;
    string key;
    string value;
    while(getline(in,line))
    {
    }
}

因为可能会出现行的形式不符合规定,所以这里就使用if函数判断一下如果cut_string函数返回true就将哈希表中插入对应的元素,否者就不进行任何的处理,循环结束之后就关闭ifstream对象:

const string dictTxt="./dict.txt";
unordered_map<string, string> dict;
void init_dictionary()
{
    ifstream in(dictTxt,std::ios::binary);
    //dictTxt是一个宏表示文件的地址
    cout<<"reload"<<endl;
    if(!in.is_open())
    {
        cerr << "open file " << dictTxt << " error" << endl;
        exit(OPEN_ERR);
    }
    string line;
    string key;
    string value;
    while(getline(in,line))
    {
        if(cut_string(line,&key,&value,"::"))
        {
            dict.insert(make_pair(key,value));
        }
    }
    in.close();
}

transform函数的实现

transform函数就是要传递给Udpserver的函数,该函数的参数形式都已经固定好了,因为该函数要向对应的服务端发送数据,所以他需要套接字和服务端的ip和port端口,因为还要对发给服务端的数据进行处理,所以还需要一个string类型的对象用来接收发过来的message,那么该函数的声明如下:

void transform(int sockfd,string ip,uint16_t port,string message)

首先创建一个名为result的string对象用来存储要发给服务端的消息,因为此时的message装的都是要翻译的因为,所以这里就查找一下message是否在哈希表中出现,如果没有出现就将result的值赋值为UNKNOW,如果找到了就把value赋值给result,那么这里的代码如下:

void transform(int sockfd,string ip,uint16_t port,string message)
{
    string result;
    auto iter=dict.find(message);
    if(iter==dict.end())
    {
        result="UNKNOW";
    }
    else
    {
        result=iter->second;
    }
}

然后就是上篇文章的熟悉套路,创建一个sockadd_In对象,然后填写内部的内容,在填写的时候别忘记使用inet_addr函数将ip地址进行转换,使用htons将port进行转换,最后使用sendto函数往套接字sockfd发送消息result,那么该函数的完整代码如下:

void transform(int sockfd,string ip,uint16_t port,string message)
{
    string result;
    auto iter=dict.find(message);
    if(iter==dict.end())
    {
        result="UNKNOW";
    }
    else
    {
        result=iter->second;
    }
    struct sockaddr_in client;
    client.sin_family=AF_INET;
    client.sin_addr.s_addr=inet_addr(ip.c_str());
    client.sin_port=htons(port);
    sendto(sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&client,sizeof(client));
}

其他地方的修改

第一处:
首先就是服务端中的start函数,在打印出客户端发来的消息之后就可以执行类中的_func对象来执行对应的翻译和发送的功能:

void start()//运行函数
{
    char buffer[gnum]={0};
    for(;;)
    {
        struct sockaddr_in peer; 
        socklen_t len = sizeof(peer); //必填
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
        if(s>0)
        {
            buffer[s]=0;
            string clientip=inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            cout << clientip <<"[" << clientport << "]# " << buffer<<endl; 
            _func(_sockfd,clientip,clientport,buffer);
       }
    }
}

第二处:
第二处就是客户端的run函数,之前只是单向的向服务端发送数据,那么现在还得向接收服务发过来的数据,那么这里也是老套路就不多说了:

void run()
  {
    struct sockaddr_in server;//用来记录服务端口的信息
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(_serverip.c_str());
    server.sin_port = htons(_serverport);
    string tmp;
    while(1)
    {
      cout << "Please Enter# ";
      cin>>tmp;
      sendto(_sockfd,tmp.c_str(),tmp.size(),0,(struct sockaddr*)&server,sizeof(server));
      char buffer[1024];
      struct sockaddr_in temp;
      socklen_t len=sizeof(temp);
      size_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
      if(n>0)
      {
        buffer[n]=0;
      }
      cout<<"翻译的结果为:"<<buffer<<endl;
    }
  }

测试

单词库的内容如下:
在这里插入图片描述
将客户端和服务端以本地环回的方式运行起来:
在这里插入图片描述
服务端输入hello就可以看到客户端显示了hello然后服务端就又会接收到你好这两个字样:
在这里插入图片描述
再输入year就会显示年:
在这里插入图片描述
如果输入一个不存在的值就会显示一个UNKNOW:
在这里插入图片描述
那么这就是单词翻译系统,希望大家能够理解。

改进二:远程指令执行程序

在前面的改进一我说了这么一句话:类中添加一个function对象可以做到功能实现和网络通信的分离,那么在改进二中就可以体现到这一特性,改进二是实现一个远程指令执行程序,那么这里就可以创建一个名为execCommand的函数,该函数来实现执行远程指令,然后在创建udpServer对象的时候就可以直接传递该函数而不修改类中的其他数据,那么这就是解耦,我们首先来认识一个名为popen的函数。

popen

在前面学习操作系统的时候我们模拟实现过一个简单命令行解释器,对应的文章点击这个链接:点击此处查看文章,当时在实现的时候就是通过创建子进程,让子进程执行execvp函数来模拟实现的命令行解释器,那么接下来要实现的popen函数就相当于fork+execvp+pipe函数,他会帮助我们创建一个子进程,然后在子进程中执行传递过来的指令,最后将指令执行的结果通过pipe函数输出到一个管道文件里面,如果我们想查看结果的话就直接访问文件即可,该函数的介绍如下:
在这里插入图片描述
第一个参数表示你要执行什么样的操作,第二个参数表示你要以什么样的方式来访问存储结果的管道文件,如果执行成功就会返回一个文件c类型的文件描述符,那么这就是该函数的功能,接下来我们就利用该函数来实现函数execCommand。

execCommand函数实现

服务端负责执行指令,但是在执行指令之前我们得先判断一下该指令是否会存在危险比如说删除文件,转移文件等等,所以在执行指令之前我们先查找一下是否存在危险指令,如果存在的话就在服务端打印这是一个危险的操作,然后不做任何处理:

oid execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
    if(cmd.find("rm")!=string::npos||cmd.find("mv")!=string::npos||cmd.find("rmdir")!=string::npos)
    {
        cerr<<clientip<<" : "<<clientport<<" 正在做一个危险的行为: "<<cmd<<endl;
        return ;       
    }
}

然后就可以创建一个string对象用来储存popen打开的管道文件中的内容,然后就可以使用popen函数以读的方式执行指令,因为popen函数可能会执行失败,所以这里就对返回值进行判断,如果返回值为空的话就往string对象中输入cmd+"exec failed"

void execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
    if(cmd.find("rm")!=string::npos||cmd.find("mv")!=string::npos||cmd.find("rmdir")!=string::npos)
    {
        cerr<<clientip<<" : "<<clientport<<" 正在做一个危险的行为: "<<cmd<<endl;
        return ;       
    }
    string res;
    FILE* fp=popen(cmd.c_str(),"r");
    if(fp==nullptr)
    {
        res+=cmd+"exec failed";
    }
}

如果管道文件创建成功就可以创建一个缓冲区,然后使用while循环和fgets函数不停的把管道文件中的数据读取到缓冲区中,再把缓冲区中的数据存储到string对象中,读取结束之后就可以关闭管道文件:

char buffer[1024];
while(fgets(buffer,sizeof(buffer),fp))
{
    res+=buffer;
}
pclose(fp);

这时res里面就装着指令的执行结果,那么这个时候我们就可以创建sockadd_in对象,填写里面的信息然后使用sendto函数发送res里面的内容,那么该函数完整的代码如下:

void execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
    if(cmd.find("rm")!=string::npos||cmd.find("mv")!=string::npos||cmd.find("rmdir")!=string::npos)
    {
        cerr<<clientip<<" : "<<clientport<<" 正在做一个危险的行为: "<<cmd<<endl;
        return ;       
    }
    string res;
    FILE* fp=popen(cmd.c_str(),"r");
    if(fp==nullptr)
    {
        res+=cmd+"exec failed";
    }
    char buffer[1024];
    while(fgets(buffer,sizeof(buffer),fp))
    {
        res+=buffer;
    }
    pclose(fp);
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family=AF_INET;
    client.sin_port=htons(clientport);
    client.sin_addr.s_addr=inet_addr(clientip.c_str());
    sendto(sockfd,res.c_str(),res.size(),0,(struct sockaddr*)&client,sizeof(client));
}

测试

创建两个渠道分别运行客户端和服务端,客户端中输入指令便可以看到服务端显示了输入的指令,并将指令的执行结果发送给了客户端:
在这里插入图片描述
并且我们使用touch指令创建文件时也可以看到下面这样的场景,客户端如下:
在这里插入图片描述
服务端如下:
在这里插入图片描述
可以看到这里的执行出现了问题,我们是输入一个指令,但是在发送的时候确实将一个指令拆分成为两个来进行发送,那么这里的问题就出现在了客户端发送的时候,我们来看下面这张图片:
在这里插入图片描述
run函数在获取指令的时候是cin获取,他是以/n和空格来作为分隔符,而我们指令在执行的时候是通过空格来间隔开的,所以这里获取的时候就会出现问题,那么这里的解决方法就是按行读取使用getline函数:

在这里插入图片描述
再运行一下可以看到服务端的运行结果如下:
在这里插入图片描述
客户端的运行结果如下:
在这里插入图片描述
虽然服务端显示了一个看不懂的符号但是我们再输入ls指令时就可以看到多出来了一个mytest.cc文件:
在这里插入图片描述
那么这就说明没有啥问题了。虽然我们这里可以执行指令,但是实现的还是一个简单的版本一些复杂的指令用这种方法还是不能执行的比如说top指令,那么这就是我们实现的第二个改进。

改进三:群聊程序

要实现群聊程序,那么我们首先就得创建一个类用描述每个用户,因为群聊是一个复杂的过程,每个人都有上线下线发消息的操作,所以我们这里就可以再创建一个类用来描述群聊这一过程并管理这个群聊的每一个用户,因为每个客户端既要接收消息又要发送消息如果只在一个页面进行的话就会显得比较混乱所以我们这里就可以采用多线程加重定向的方式来进行解决,那么接下来我们的第一步就是先实现user类。

Usr类

首先user类用来描述每个用户,而用户最关键的数据就是端口号和ip地址,所以在该类中就创建一个string对象和一个uint16_t无符号整数:

class Usr
{
public:
private:
    string _ip;
    uint16_t _port;
};

然后就是构造函数,因为这里就两个成员变量所以构造函数就有两个参数用来初始化这两个变量,因为这里的变量是私有的所以我们可以创建两个函数用来提供者两个变量的访问,那么该类的完整代码如下:

class Usr
{
public:
    Usr(const string &ip,const uint16_t&port)
    :_ip(ip)
    ,_port(port)
    {}
    string ip(){return _ip;}
    uint16_t port(){return _port;}
private:
    string _ip;
    uint16_t _port;
};

onlineUser类

该类就负责管理群聊用户,使用该类管理用户时需要判断下面这些情况:用户上线,用户下线,用户发送消息,以及判断一个用户是否上线了,所以该类就对应着有下面这些函数:adduser(用户上线),delUser(用户下线),isOnline(判断一个用户是否上线),broadcast(用户发送的消息需要转发给每个在线的用户),因为要对用户进行查找所以我们得给每个用户做一个标记,这个标记就对应着一个Usr,所以在onlineUser类中我们就可以创建一个哈希表,那么这里的代码如下:

class onlineUser
{
public:
    onlineUser(){}
    ~onlineUser(){}
    void adduser(){}
    void delUser(){}
     bool isOnline(){}
    void broadcast(){}   
private:
    unordered_map<string,Usr> users;
};

那么接下来就一一实现这些函数。

adduser

该函数就是添加用户,我们把ip地址和端口号合在一起作为Usr对象的标记,那么在该函数里面就是先创建一个标记再使用哈希的insert函数往里面插入以上线的成员,那么这里的代码如下:

void adduser(const string& ip,const uint16_t& port)
{
    string id=ip+"-"+to_string(port);
    users.insert(make_pair(id,Usr(ip,port)));
}

delUser

该函数也是同样的道理先创建标记,然后再调用erase函数将哈希表中的元素删除

void delUser(const string& ip,const uint16_t&port)
{
    string id=ip+"-"+to_string(port);
    users.erase(id);
}

isOnline

该函数也是同样的道理,先创建一个标记然后使用find函数看该标记在哈希表中是否存在如果存在的话就返回true,如果不存在就返回false,那么这里的代码如下:

 bool isOnline(const string& ip,const uint16_t&port)
{
    string id=ip+"-"+to_string(port);
    return users.find(id)==users.end()? false : true;
}

broadcast

该函数的作用就是将成员发送的消息转发给每一个在线的成员,所以该函数需要一个参数接收端口号,还需要两个参数表示是哪个ip地址哪个端口号发送的这个消息,最后还有一个参数表示发送的是什么消息,那么该函数的声明如下:

void broadcast(int sockfd,const string& ip,const uint16_t& port,const string& message)
{}

该函数的实现就很简单,首先创建一个范围for遍历哈希表中的每个元素,获取其中的value,这样我们就可以得到每个成员对应的ip地址和端口号,所以在for循环里面我们就可以先创建一个sockaddr_in对象然后根据value中的值进行填充:

void broadcast(int sockfd,const string& ip,const uint16_t& port,const string& message)
{
    for(auto &user:users)
    {
        struct sockaddr_in client;
        bzero(&client,sizeof(client));
        client.sin_family=AF_INET;
        client.sin_port=htons(user.second.port());
        client.sin_addr.s_addr=inet_addr(user.second.ip().c_str());
    }
}

我们群聊发送消息的时候会显示自己的名字是谁,那么我们这里就可以把ip地址和端口号来作为每个用户的名字,所以我们这里就对消息进行整合在消息的前面加上发送者的ip地址和端口号,再使用sendto进行消息发送,那么该函数的完整代码如下:

void broadcast(int sockfd,const string& ip,const uint16_t& port,const string& message)
{
    for(auto &user:users)
    {
        struct sockaddr_in client;
        bzero(&client,sizeof(client));
        client.sin_family=AF_INET;
        client.sin_port=htons(user.second.port());
        client.sin_addr.s_addr=inet_addr(user.second.ip().c_str());
        string s=ip+" - "+to_string(port)+"# "+message;
        sendto(sockfd,s.c_str(),s.size(),0,(struct sockaddr*)&client,sizeof(client));
    }
}

routeMessage

该函数就是负责对onlineUser类进行操作,首先该函数的参数和前面的保持一致:

onlineUser onlineuser;
void routeMessage(int sockfd,string clientip,uint16_t clientport,string message)

然后在函数的开始我们就判断一下当前的消息是否是online和offline,如果是的话就调用对应的addusr函数和delusr函数来添加用户和删除用户

onlineUser onlineuser;
void routeMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
    if(message=="online"){onlineuser.adduser(clientip,clientport);}
    if(message=="offline"){onlineuser.delUser(clientip,clientport);}
}

如果不为online或者offline的话就说明当前的message是需要发送给其他的用户的,所以我们就判断一下当前的用户是否已经上线了,如果上线的话就使用broadcast进行群发,如果没有上线的话就创建sockaddr_in对象添加发送这条消息的客户端信息,告诉客户端你当前还没有上线, 请先上线,运行: online,那么这里的代码如下:

onlineUser onlineuser;
void routeMessage(int sockfd,string clientip,uint16_t clientport,string message)
{
    if(message=="online"){onlineuser.adduser(clientip,clientport);}
    if(message=="offline"){onlineuser.delUser(clientip,clientport);}
    if(onlineuser.isOnline(clientip,clientport))
    {
        onlineuser.broadcast(sockfd,clientip,clientport,message);
    }
    else
    {
        struct sockaddr_in client;
        bzero(&client,sizeof(client));
        client.sin_family=AF_INET;
        client.sin_addr.s_addr=inet_addr(clientip.c_str());
        client.sin_port=htons(clientport);
        string result="你还没有上线, 请先上线,运行: online";
        sendto(sockfd,result.c_str(),result.size(),0,(struct sockaddr*)&client,sizeof(client));
    }
}

其他地方的修改

这里就需要修改客户端的run函数,因为客户端及存在发送消息和接收消息的两种操作,如果只使用一个页面的话就会显得十分的混乱,所以在客户端这里我们就选着使用多线程技术来让主线程只负责发送数据,让子线程负责读取数据,虽然主线程负责发送数据但是他本身也是得在屏幕上打印一些数据的,所以我们这里就采用重定向的方式来进行打印,子线程往标准输出里面输出数据,而主线程则是往标准错误中输出数据,那么首先在类中添加一个pthread_t的变量,然后在run函数里面先试用pthread_create函数创建一个线程,然后穿件sockaddr_in对象并填写服务端的信息:

static void *readmessage(void *args)
 {}
void run()
{
  pthread_create(&_reader,nullptr,readmessage,(void*)&_sockfd);
  struct sockaddr_in server;//用来记录服务端口的信息
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr(_serverip.c_str());
  server.sin_port = htons(_serverport);
}

创建一个string对象用来发送消息,和一个buffer缓冲区用来接收输入的消息,然后就可以创建一个死循环:

void run()
 {
   pthread_create(&_reader,nullptr,readmessage,(void*)&_sockfd);
   struct sockaddr_in server;//用来记录服务端口的信息
   server.sin_family = AF_INET;
   server.sin_addr.s_addr = inet_addr(_serverip.c_str());
   server.sin_port = htons(_serverport);
   string message;
   char cmdline[1024];
   while(1)
   {
   }
}

在循环里面就先向标准错误中打印一些消息表示在这里输入要发送的消息,然后在使用fgets函数将用户输入的消息放到buffer缓冲区中,因为fgets函数会将\n也读取进去,所以我们将缓冲区中的有效数据的最后一个赋值为0,然后就可以将缓冲区的内容赋值给message对象,最后使用sendto函数将message发送给服务端:

void run()
{
  pthread_create(&_reader,nullptr,readmessage,(void*)&_sockfd);
  struct sockaddr_in server;//用来记录服务端口的信息
  server.sin_family = AF_INET;
  server.sin_addr.s_addr = inet_addr(_serverip.c_str());
  server.sin_port = htons(_serverport);
  string message;
  char cmdline[1024];
  while(1)
  {
    //cerr << "# ";//这里输出到错误是因为后面的输出从对象
    fprintf(stderr,"Enter# ");
    fflush(stderr);
    fgets(cmdline,sizeof(cmdline),stdin);
    cmdline[strlen(cmdline)-1]=0;//把斜杠n去掉
    message=cmdline;
    sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,sizeof(server));
  }
}

接下来就要实现子线程执行的函数,因为主线程一直处于死循环状态无法对该线程进行回收,所以在函数里面就先使用pthread_detach函数表示自行回收,然后对参数进行强转得到套接字,再就是创建一个循环,在循环内部创建一个缓冲区将所有发送给客户端的消息都打印出来,那么这里的代码如下:

static void *readmessage(void *args)
 {
     pthread_detach(pthread_self());
     int _sockfd=*(static_cast<int *>(args));
     while(true)
     {
         char buffer[1024];
         struct sockaddr_in temp;
         socklen_t len=sizeof(temp);
         size_t n=recvfrom(_sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
         if(n>0)
         {
           buffer[n]=0;
         }
         cout<<buffer<<endl;
     }
 }

如果大家仔细观察的话也不难发现这里做的就是将之前的run函数一分为二,那么这就是改进三的代码,接下来我们将进行测试:

测试

这里在打开客户端的时候得这样做,首先创建几个管道文件:
在这里插入图片描述
然后运行客户端将客户端的消息重定向输出到管道文件fifo里面:
在这里插入图片描述
然后再创建一个渠道使用cat输出管道文件里面的内容:
在这里插入图片描述
然后我们再运行客户端的程序:
在这里插入图片描述
然后我们在客户端中随便输入一个数据:
在这里插入图片描述
可以看到因为没有上线所以客户端收到的消息就是请先上线,并且服务端显示了客户端发来的消息:
在这里插入图片描述
然后我们再输入online,就可以看到下面的现象:
在这里插入图片描述
这是我们再发送消息就不会提醒我们上线了:
在这里插入图片描述
并且我们再创建一个渠道执行这个函数的时候可以发现当前的ip没有变但是端口号发生了变化:
在这里插入图片描述
那么这就是我们实现的一个群聊系统。


http://www.niftyadmin.cn/n/5244304.html

相关文章

ElasticSearch 能说说ElasticSearch 写索引的逻辑吗?

ElasticSearch是一个基于Lucene的开源、分布式、RESTful搜索引擎。它提供了一种快速、灵活的方式来存储、搜索和分析大量数据。在写索引的逻辑方面&#xff0c;ElasticSearch主要遵循以下步骤&#xff1a; 创建索引&#xff1a;首先&#xff0c;您需要创建一个索引来存储您的数…

下划线css

思路&#xff1a; Q1:为什么下划线不用边框border 而使用背景色呢&#xff1f; 要实现动画效果&#xff0c;随着行盒的方向走 新知识点 线性渐变&#xff1a;linear-gradient 方法&#xff1a;linear-gradient(direction, color-stop1, color-stop2, ...) 详情见&#xff1a…

ROS参数服务器——参数操作(C++)

目录 一、参数服务器的新增、修改参数 1、API 2、代码 二、参数服务器获取参数 1、API 2、代码 三、参数服务器删除参数 1、API 2、代码 一、参数服务器的新增、修改参数 1、API 在 roscpp 中提供了两套 API 实现参数操作ros::NodeHandlesetParam("键",值…

python基于轻量级GhostNet模型开发构建23种常见中草药图像识别系统

轻量级识别模型在我们前面的博文中已经有过很多实践了&#xff0c;感兴趣的话可以自行移步阅读&#xff1a; 《移动端轻量级模型开发谁更胜一筹&#xff0c;efficientnet、mobilenetv2、mobilenetv3、ghostnet、mnasnet、shufflenetv2驾驶危险行为识别模型对比开发测试》 《基…

【文件上传系列】No.1 大文件分片、进度图展示(原生前端 + Node 后端 Koa)

分片&#xff08;500MB&#xff09;进度效果展示 效果展示&#xff0c;一个分片是 500MB 的 分片&#xff08;10MB&#xff09;进度效果展示 大文件分片上传效果展示 前端 思路 前端的思路&#xff1a;将大文件切分成多个小文件&#xff0c;然后并发给后端。 页面构建 先在页…

JS 语句语法3 vue1

21.案例-城市天气检索.js // 1. 先自动定位城市 let currentCity ""; function autoLocation() {axios({url: "https://restapi.amap.com/v3/ip",method: "GET",params: {key: "e45ba07980d4a0817d2edeba0de23add",},}).then((data)…

倒计时模块复习

经典回顾倒计时 倒计时的基本布局介绍。 一个内容区域和一个输入区域&#xff0c;内容区域进行划分 直接使用flex布局会更快一点。 js代码 我们利用一下模块化思想&#xff0c;直接把获得时间这个功能写成一个函数。方便后续的调用 function getTime() {const date new Date…

Redis中HyperLogLog的使用

目录 前言 HyperLogLog 前言 在学习HyperLogLog之前&#xff0c;我们需要先学习两个概念 UV&#xff1a;全称Unique Visitor&#xff0c;也叫独立访客量&#xff0c;是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站&#xff0c;只记录1次。PV&am…