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

目录标题

  • 前言
  • 改进一:单词翻译程序
    • 准备工作
    • 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没有变但是端口号发生了变化:
在这里插入图片描述
那么这就是我们实现的一个群聊系统。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/224698.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

mixamo根动画导入UE5问题:滑铲

最近想做一个跑酷游戏&#xff0c;从mixamo下载滑铲动作后&#xff0c;出了很多动画的问题。花了两周时间&#xff0c;终于是把所有的问题基本上都解决了。 常见问题&#xff1a; 1.【动画序列】人物不移动。 2.【动画序列】人物移动朝向错误。 3.【蒙太奇】人物移动后会被拉回…

TensorRT Provider 与TensorRT Native的对比

TensorRT Provider 的优势为&#xff1a; TensorRT EP 可以实现与本机 TensorRT 的性能等价。使用 TensorRT EP 的好处之一是&#xff0c;如果模型中存在不受支持的 TensorRT 操作&#xff0c;就可以运行不能在本机 TensorRT 中运行的模型。这些操作将自动退回到其他 EP&#…

8051单片机的CPU组成与四个并行I/O端口

AT89S51的CPU与并行I/O端口 本文主要涉及8051的CPU组成以及并行的4个I/O端口。CPU&#xff0c;主要由运算器&#xff08;ALU&#xff09;和控制器&#xff08;CU&#xff09;构成&#xff1b;4个双向的8位并行I/O端口&#xff0c;分别记为P0、P1、P2和P3 文章目录 AT89S51的CPU…

Swagger——接口文档自动生成和测试

目录 1 介绍2 使用步骤 1 介绍 Swagger 是一个规范和完整的框架&#xff0c;用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是&#xff1a; 使得前后端分离开发更加方便&#xff0c;有利于团队协作 接口的文档在线自动生成&…

厦门排水管网监测系统,实时感知城市健康

在厦门城市化建设的步伐中&#xff0c;有一个不可或缺的环节&#xff0c;那就是排水管网监测系统。它就像城市生命线上的守护者&#xff0c;默默地守护着城市的正常运行&#xff0c;防止内涝等问题的出现。 排水管网监测系统是城市基础设施建设中的重要一环&#xff0c;其重要性…

金鸣表格文字识别大师扫描仪使用技巧

所需硬件&#xff1a;PC&#xff08;电脑&#xff09;、扫描仪 所需软件&#xff1a;金鸣表格文字识别大师5.0以上版本&#xff08;以下简称“本软件”&#xff09; 实现功能&#xff1a;直接用扫描仪扫描图片并将其转换为可编辑的excel或word. 实现原理&#xff1a;本软件利…

Qt OpenCV 学习(二):两个简单图片识别案例

1. 寻找匹配物体 1.1 mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <opencv2/opencv.hpp>#include <QImage> #include <QString> #include <QPixmap>QT_BEGIN_NAMESPACE namespace Ui { class Main…

配置OSS后如何将服务器已有文件上传至OSS,推荐使用ossutil使用

1.下载安装ossutil sudo -v ; curl https://gosspublic.alicdn.com/ossutil/install.sh | sudo bash2.交互式配置生成配置文件 ossutil config 根据提示分别设置配置文件路径、设置工具的语言、Endpoint、AccessKey ID、AccessKey Secret和STSToken参数&#xff0c;STSToken留…

【QT】容器类的迭代

迭代器(iterator)为访问容器类里的数据项提供了统一的方法&#xff0c;Qt有两种迭代器类&#xff1a;Java类型的迭代器和STL类型的迭代器。 Java类型的迭代器更易于使用&#xff0c;且提供一些高级功能&#xff0c;而STL类型的迭代器效率更高。 Qt还提供一个关键字foreach&…

Self-supervised Graph Learning for Recommendation 详解

目录 摘要 引言 预备知识 方法 3.1 图结构数据增强 3.2 对比学习 3.3 多任务学习 3.4 理论分析 摘要 基于用户-物品图的推荐表示学习已经从使用单一 ID 或交互历史发展到利用高阶邻居。这导致了图卷积网络(GCNs)在推荐方面的成功&#xff0c;如 PinSage 和 LightGCN。尽管具…

[Firefly-Linux] RK3568 pca9555芯片驱动详解

文章目录 一、PAC9555 介绍二、ITX-3568JQ PAC9555 使用2.1 原理图2.2 设备树三、RK3568 I2C 介绍四、PAC9555 驱动4.1 介绍4.2 数据结构4.3 驱动分析一、PAC9555 介绍 PAC9555 是一种高性能、低功耗 I/O 扩展芯片,能够提供 16 个 GPIO 通道,每个通道可以单独配置为输入或输…

C语言再学习 -- 单精度(float)和双精度(double)浮点数 与 十六进制(HEX) 之间转换(转载))

之前讲过浮点数部分&#xff0c;参看&#xff1a;C语言再学习 – 浮点数 现在程序中要将浮点数&#xff0c;通过TCP发送。那得先将其转换为十六进制才行呀。 那么问题就来了。 参看&#xff1a;C语言&#xff1a;单精度(float)和双精度(double)浮点数 与 十六进制(HEX) 之间…

完数的C语言实现xdoj30

时间限制: 2 S 内存限制: 10000 Kb 问题描述: 请写一个程序&#xff0c;给出指定整数范围[a&#xff0c;b]内的所有完数&#xff0c;0 < a < b < 10000。 一个数如果恰好等于除它本身外的所有因子之和&#xff0c;这个数就称为"完数"。 例如6是…

git 关于分支、merge、commit提交

最近开始用git终端提交代码&#xff0c;梳理了一些知识点 一 关于分支 关于分支&#xff0c;git的分支分为本地分支远程分支两种分支&#xff0c;在上传代码时&#xff0c;我们要确保当前本地分支连接了一个远程分支。 我们可以通过下面代码查看当前的本地分支&#xff1a; g…

mybatis的分页插件

在mybatis核心配置文件中&#xff1a; 这时已经用了SSM整合&#xff0c;好多像是mapper或者数据源等都移出去了 <?xml version"1.0" encoding"UTF-8" ?> <!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""…

nodejs微信小程序+python+PHP新闻发布系统的设计与实现-计算机毕业设计推荐

目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性&#xff1a;…

这些Java并发容器,你都了解吗?

文章目录 前言并发容器1.ConcurrentHashMap 并发版 HashMap示例 2.CopyOnWriteArrayList 并发版 ArrayList示例 3.CopyOnWriteArraySet 并发 Set示例 4.ConcurrentLinkedQueue 并发队列 (基于链表)示例 5.ConcurrentLinkedDeque 并发队列 (基于双向链表)示例 6.ConcurrentSkipL…

【LLM_05】使用fastgpt搭建本地离线大语言模型(Chatglm3)问答+知识库平台

1、FastGPT 的知识库逻辑1.1 基础概念1.2 FastGPT知识库的导入1.3 FastGPT新建应用&#xff08;1&#xff09;创建一个知识库助手&#xff08;2&#xff09;创建一个python开发小助手 2、词向量比较测试2.1 开启词向量模型2.2 词向量模型性能比较 3、配置好之后的运行3.1 运行大…

Redis ziplist源码解析

area |<---- ziplist header ---->|<----------- entries ------------->|<-end->|size 4 bytes 4 bytes 2 bytes ? ? ? ? 1 byte--------------------------------------------------------------- comp…

多人聊天UDP

服务端 package 多人聊天;import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintStream; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList;…