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

news2024/11/29 13:33:54

目录标题

  • 前言
  • 改进一:单词翻译程序
    • 准备工作
    • 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.coloradmin.cn/o/1290938.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

互联网Java工程师面试题·Spring Cloud篇

目录 1、什么是 Spring Cloud&#xff1f; 2、使用 Spring Cloud 有什么优势&#xff1f; 3、服务注册和发现是什么意思&#xff1f;Spring Cloud 如何实现&#xff1f; 4、负载平衡的意义什么&#xff1f; 5、什么是 Hystrix&#xff1f;它如何实现容错&#xff1f; 6、什么是…

C语言进阶之路之内存镜像与字符操作函数篇

目录 一、学习目标&#xff1a; 二、内存镜像 什么是进程 C进程内存布局 栈内存 静态数据 数据段&#xff08;存储静态数据&#xff09;与代码段 堆内存 三、字符操作函数 函数strstr 函数strlen strlen与sizeof的区别 函数strtok 函数strcat与strncat 函数strc…

PyTorch 基础篇(2):线性回归(Linear Regression)

# 包import torchimport torch.nn as nnimport numpy as npimport matplotlib.pyplot as plt # 超参数设置input_size 1output_size 1num_epochs 60learning_rate 0.001 # Toy dataset # 玩具资料&#xff1a;小数据集x_train np.array([[3.3], [4.4], [5.5], [6.71], [6.…

强敌环伺:金融业信息安全威胁分析——钓鱼和恶意软件

门口的敌人&#xff1a;分析对金融服务的攻击 Akamai会定期针对不同行业发布互联网状态报告&#xff08;SOTI&#xff09;&#xff0c;介绍相关领域最新的安全趋势和见解。最新的第8卷第3期报告主要以金融服务业为主&#xff0c;分析了该行业所面临的威胁和Akamai的见解。我们发…

浪潮信息KeyarchOS EDR 安全防护测评

背景 近几年服务器安全防护越来越受到企业的重视&#xff0c;企业在选购时不再仅仅看重成本&#xff0c;还更看重安全性&#xff0c;因为一旦数据泄露&#xff0c;被暴力破解&#xff0c;将对公司业务造成毁灭性打击。鉴于人们对服务器安全性的看重&#xff0c;本篇文章就来测…

智能外呼是什么?智能外呼怎么样?

智能外呼是什么&#xff1f; 当今业务通讯领域&#xff0c;智能外呼已经成为了一种普遍应用的技术。智能外呼是利用人工智能技术和自动化系统来进行电话呼叫和信息传递的一种方式。它可以帮助企业有效地进行市场营销和客户服务&#xff0c;极大地提高工作效率。 智能外呼系统…

HarmonyOS4.0从零开始的开发教程01运行Hello World

HarmonyOS&#xff08;一&#xff09;运行Hello World 下载与安装DevEco Studio 在HarmonyOS应用开发学习之前&#xff0c;需要进行一些准备工作&#xff0c;首先需要完成开发工具DevEco Studio的下载与安装以及环境配置。 进入DevEco Studio下载官网&#xff0c;单击“立即…

点滴生活记录1

2023/10/10 今天骑小电驴上班&#xff0c;带着小鸭子一起。路上的时候&#xff0c;我给小鸭子说&#xff0c;你要帮我看着点路&#xff0c;有危险的时候提醒我&#xff0c;也就刚说完没几分钟&#xff0c;一个没注意&#xff0c;直接撞到一个拦路铁墩子上&#xff0c;车子连人歪…

流量异常-挂马造成百度收录异常关键词之解决方案(虚拟主机)

一.异常现象&#xff1a;流量突然暴涨&#xff0c;达到平时流量几倍乃至几十倍&#xff0c;大多数情况下因流量超标网站被停止。 二.排查原因&#xff1a; 1.首先分析web日志&#xff1a;访问量明显的成倍、几十倍的增加&#xff1b;访问页面不同&#xff1b;访问IP分散并不固…

2023 IoTDB 用户大会成功举办,深入洞察工业互联网数据价值

2023 年 12 月 3 日&#xff0c;中国通信学会作为指导单位&#xff0c;Apache IoTDB Community、清华大学软件学院、中国通信学会开源技术委员会联合主办&#xff0c;“科创中国”开源产业科技服务团和天谋科技&#xff08;北京&#xff09;有限公司承办的 2023 IoTDB 用户大会…

深度学习(六):paddleOCR理解及识别手写体,手写公式,表格

1.介绍 1.1 什么是OCR? 光学字符识别&#xff08;Optical Character Recognition, OCR&#xff09;&#xff0c;ORC是指对包含文本资料的图像文件进行分析识别处理&#xff0c;获取文字及版面信息的技术&#xff0c;检测图像中的文本资料&#xff0c;并且识别出文本的内容。…

JVM 执行引擎篇

机器码、指令、汇编语言 机器码 各种用二进制编码方式表示的指令&#xff0c;叫做机器指令码。开始&#xff0c;人们就用它采编写程序&#xff0c;这就是机器语言。机器语言虽然能够被计算机理解和接受&#xff0c;但和人们的语言差别太大&#xff0c;不易被人们理解和记忆&a…

Centos服务器上根据端口号查询jar包,根据jar包查端口号

在开发springboot服务器时&#xff0c;经常会遇到其他人部署的java服务&#xff0c;需要自己维护&#xff0c;留下的信息又非常少。经常面临找不到jar包位置&#xff0c;或者不知道占用端口&#xff0c;不知道启动命令的问题。这里记录一下常用的centos服务器上的命令&#xff…

物联网安全芯片ACL16 采用 32 位内核,片内集成多种安全密码模块 且低成本、低功耗

ACL16 芯片是研制的一款32 位的安全芯片&#xff0c;专门面向低成本、低功耗的应用领域&#xff0c; 特别针对各类 USB KEY 和安全 SE 等市场提供完善而有竞争力的解决方案。芯片采用 32 位内核&#xff0c;片内集成多种安全密码模块&#xff0c;包括SM1、 SM2、SM3、 SM4 算法…

103.进程概述

目录 1.并行和并发 区别&#xff1a; 2.PCB 3.进程状态 4. 进程命令 从严格意义上来讲&#xff0c;程序和进程是两个不同的概念&#xff0c;他们的状态&#xff0c;占用的系统资源都是不同的。 程序&#xff1a;程序是一种静态实体&#xff0c;是存储在计算机存储介质上的…

docker基本管理和相关概念

1、docker是什么&#xff1f; docker是开源的应用容器引擎。基于go语言开发的&#xff0c;运行在Linux系统当中开源轻量级的“虚拟机”。 docker可以在一台主机上轻松的为任何应用创建一个轻量级的&#xff0c;可移植的&#xff0c;自给自足的容器。docker的宿主机是Linux系统…

5 TF-A

一、TF-A的使用 TF-A是什么&#xff1f; TF-A 全称是 Arm Trusted Firmware&#xff0c;TF-A是为了保证安全&#xff0c;arm退出的可信固件&#xff0c;简称TF-A。它的作用就是隔离硬件&#xff0c;为硬件提供一个安全环境并提供安全服务。 1. 系统源码获取 STM32MP1Dev - STM…

【算法】约瑟夫环

约瑟夫问题是个有名的问题&#xff1a;N个人围成一圈&#xff0c;从第一个开始报数&#xff0c;第M个将被杀掉&#xff0c;最后剩下一个&#xff0c;其余人都将被杀掉。例如N6&#xff0c;M5&#xff0c;被杀掉的顺序是&#xff1a;5&#xff0c;4&#xff0c;6&#xff0c;2&a…

软件测试外包干了2个月,技术进步2年。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;18年通过校招进入北京某软件公司&#xff0c;干了接近2年的功能测试&#xff0c;今年国庆&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了2年的功能测试&…