Bootstrap

【linux网络编程】| socket套接字 | 实现UDP协议聊天室

        前言:本节内容将带友友们实现一个UDP协议的聊天室。 主要原理是客户端发送数据给服务端。 服务端将数据再转发给所有链接服务端的客户端。 所以, 我们主要就是要实现客户端以及服务端的逻辑代码。 那么, 接下来开始我们的学习吧。

        ps:本节内容建议了解socket套接字的接口的友友们进行观看哦,本节内容中涉及到的接口都不会讲解, 直接就用了。

目录

 整体代码

Udpclient

UdpServer

main(配合UdpServer, UdpServer的入口) 

准备文件

实现步骤

实现服务端客户端的收发消息

Udpserver

Init函数

run函数 

 UdpServer析构

Udpclient

实现客户端之间的聊天功能

Udpserver

Udpclient

运行结果


 整体代码

        先上整体代码:

Udpclient


#include<iostream>
using namespace std;
#include<string>
#include<sys/types.h>
#include"Log.hpp"
#include<sys/socket.h>
#include<pthread.h>
#include<arpa/inet.h>
#include<string.h>
#include<netinet/in.h>
Log lg;


class ThreadData
{
public:
    sockaddr_in server;
    int sockfd;
};

void* recv_message(void* args)
{
    char buffer[1024];
    ThreadData* td = static_cast<ThreadData*>(args);    
    while (true)
    {
        //接收数据
        sockaddr_in temp;
        socklen_t len;
        string info;
        ssize_t s = recvfrom(td->sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&temp, &len);

        if (s < 0)
        {
            lg(Error, "recv error, error: %d, strerror: %s", errno, strerror(errno));
            continue;
        }
        buffer[s] = 0;
        info = buffer;
        cout << info << endl;
    }

}

void* send_message(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    string message;
    while (true)
    { 
        getline(cin, message);  //获取数据
        //发送数据
        sendto(td->sockfd, message.c_str(), message.size(), 0, (sockaddr*)&td->server, sizeof(td->server));

    }
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        cout << "Client server" << endl;
    }

    //先拿到套接字的参数
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    ThreadData td;

    //创建套接字与打开网卡
    memset(&td.server, 0, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建文件描述符, 网卡的文件描述符, 网络传输就是使用网络文件描述符找到对应的文件内的数据
    if (td.sockfd < 0)
    {
        lg(Error, "client create sockfd error, errno: %d, strerror: %s", errno, strerror(errno));
        exit(1);
    }

    //创建线程, 然后运行线程, 等待线程
    pthread_t recv, send;
    pthread_create(&recv, nullptr, recv_message, &td);
    pthread_create(&send, nullptr, send_message, &td);

    pthread_join(recv, nullptr);
    pthread_join(send, nullptr);

    close(td.sockfd);
    return 0;
}

UdpServer

#include<iostream>
using namespace std;
#include<sys/types.h>
#include<string>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<strings.h>
#include"Log.hpp"
#include<functional>
#include<netinet/in.h>
#include<unordered_map>


int defaultport = 8080;
string defaultip = "0.0.0.0";

using func_t = function<string(string, sockaddr_in&, unordered_map<string, sockaddr_in>&)>; 
Log lg;

enum
{
    SockError = 2,
    BindError = 3,
    RecvError = 4,
};

class UdpServer
{
public:
    UdpServer(uint16_t port = defaultport) 
        : port_(port), ip_(defaultip), isrunning_(false)
    {}

    void Init()
    {
        //先创建套接字变量并且完成初始化。 然后就创建网卡文件
        sockaddr_in local;
        
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "create sock error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(SockError);
        }
        lg(Info, "create sock success");
        
        //绑定
        if (bind(sockfd_, (sockaddr*)&local, sizeof(local)) < 0) 
        {
            lg(Fatal, "bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind success");
    }

    void BroadCast(string message, unordered_map<string, sockaddr_in>& clients)
    {
        cout << "1 "<< endl;
        for (auto& e : clients)
        {
            sendto(sockfd_, message.c_str(), message.size(), 0, (sockaddr*)&e.second, sizeof(e.second));
        }
        cout << "2 " << endl;
    }

    
    void run(func_t func)
    {
        isrunning_ = true;
        char inbuffer[1024];


        while (isrunning_)
        {
            memset(inbuffer, 0, sizeof(inbuffer));
            sockaddr_in client;
            socklen_t client_len;
            memset(&client, 0, sizeof(client));
            //接收数据的同时监听到客户端的来源
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&client, &client_len);
            if (s < 0) 
            {
                lg(Waring, "recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[s] = 0;

            //处理数据
            //创建套接字, 用来监听是哪一个客户端

            string message = inbuffer;
            message = func(message, client, clients);

            //处理完成后, 返回发送给客户端
            BroadCast(message, clients);
            // sendto(sockfd_, message.c_str(), message.size(), 0, (sockaddr*)&client, sizeof(client));


        }
    }

    ~UdpServer()
    {
        if (sockfd_ > 0) close(sockfd_);    
    }
private:
    int sockfd_;
    uint16_t port_;
    string ip_;
    bool isrunning_;
    unordered_map<string, sockaddr_in> clients;

};

main(配合UdpServer, UdpServer的入口) 


#include"UdpServer.hpp"
#include<memory>

string Handler(string message, sockaddr_in& client, unordered_map<string, sockaddr_in>& clients)
{
    string tmp = inet_ntoa(client.sin_addr) + to_string(client.sin_port);
    if (!clients.count(tmp))
    {
        clients[tmp] = client; 
        cout << "ip " << inet_ntoa(client.sin_addr) << " : port " << client.sin_port << " has add in talk room" << endl;
    }
    message = "[" + string(inet_ntoa(client.sin_addr)) + ":" + to_string(client.sin_port) + "]#: " + message; 
    return message;
}

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "has return" << endl;
        return 1;
    }
    //
    uint16_t serverport = stoi(argv[1]);  

    unique_ptr<UdpServer> svr(new UdpServer(serverport));
    //
    svr->Init();
    svr->run(Handler);


    return 0;
}

准备文件

        我们要准备三个文件

  •         Udpclient.cc——用来运行起来客户端
  •         UdpServer.hpp——用来实现服务端的各种接口
  •         main.cc——用来运行起来服务端

        除了这三个主要的文件。 其实博主还准备了两个可以忽略的文件(为了方便)。 一个是博主自己写的日志程序, 用来打印日志。 一个是makefile, 方便编译。 

        如果没有日志程序的话,打印错误信息时直接cout, printf打印即可。 makefile建议带上, 方便编译养成好习惯。

实现步骤

注意, 一步到位是很难的。 所以我们先实现简单的功能, 再实现困难的功能。

        这里简单的功能就是,先让客户端能够将数据发给服务端了, 然后服务端接收到消息后再将数据返回给客户端。

        这里困难的功能就是当多个客户端如何看到互相的信息。然后如何能够一遍发信息,一边收信息。 

实现服务端客户端的收发消息

Udpserver

        实现逻辑:Udpserver.hpp中封装一个类。这个类里面封装一些接口, 然后我们在main函数中创建类对象, 在执行接口操作。

        所以, 先封装一个类, 将要实现的接口以及要用到的变量写上, 实现一个框架:

#include<iostream>
using namespace std;
#include<string>

int defaultport = 8080;       //默认的端口号,我们要创建一个默认的端口号
string defaultip = "0.0.0.0"; //在服务器中使用套接字的时候, bind函数不能绑定公网IP, 因为
//服务器的公网IP可能是虚拟的, 注意,IP地址是和网卡挂钩的, 一个网卡只能有一个IP地址。 
//绑定ip地址就是说在绑定网卡,也就是说绑定某个IP地址后就只能监听这一个网卡的消息了。 但是
//有些机器是有很多张网卡的, 所以就有一个默认IP:0.0.0.0, 绑定这个IP就能监听在本机器下面
//所有的网卡的信息。 


Log lg;

class UdpServer
{
public:
    UdpServer(uint16_t port = defaultport) 
        : port_(port), ip_(defaultip), isrunning_(false)
    {}

    void Init()
    {

    }


    void run(func_t func)
    {

    }

    ~UdpServer()
    {

    }
private:
    int sockfd_;     //服务端的网卡文件的编号
    uint16_t port_;  //服务器起来后的端口号
    string ip_;      //服务器起来的时候所在的ip地址
    bool isrunning_; //服务器是否正在运行


};

        下面是main.cpp里面的内容, 直接启动服务端。 

#include"UdpServer.hpp"
#include<memory>



int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "has return" << endl;
        return 1;
    }
    //
    uint16_t serverport = stoi(argv[1]);  

    unique_ptr<UdpServer> svr(new UdpServer(serverport));
    //
    svr->Init();
    svr->run();


    return 0;
}

Init函数

        Udpserver里面的Init函数, 这个函数用来绑定服务端的套接字的。 什么是绑定? 博主目前的理解就是将我们运行的服务端这个程序能够和网卡建立起关系。         

        这个关系中, 关系的两端是我们运行的服务端程序和socket网卡文件(网卡文件就代表了网卡)。关系的纽带是ip地址和端口号。 利用ip地址和端口号来将我们的服务端程序绑定给网卡, 这个时候因为网卡的工作性质, 其他的进程都不能再绑定网卡了, 直到我们的服务端退出。 

    void Init()
    {
        //先创建套接字变量并且完成初始化。 然后就创建网卡文件
        sockaddr_in local;
        
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "create sock error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(SockError);
        }
        lg(Info, "create sock success");
        
        //绑定
        if (bind(sockfd_, (sockaddr*)&local, sizeof(local)) < 0) 
        {
            lg(Fatal, "bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind success");
    }

run函数 

        我们这里思考一个问题, 我们要实现的其实是服务端与客户端之间收发消息。 所以, 我们就要客户端先发, 然后服务端收消息。 

        然后! 服务端收到消息将消息 处理一下 再将消息发回客户端。

        所以,这个过程中服务端有三个主要的动作, 一个是收,一个是处理, 一个是发。         

        然后我们的处理怎么处理, 我们可以将处理动作暴露出去,  交给main.cpp来决定。 ——利用回调函数, main.cpp中将要执行的动作作为函数传给run函数。 

如下为接口:

    //这里的func_t是一个回调函数的类型。 什么类型, 使用包装器包装的!注意
    //包含头文件functional
    using func_t = function<string(string)>; 


    void run(func_t func)
    {
        isrunning_ = true;
        char inbuffer[1024];


        while (isrunning_)
        {
            memset(inbuffer, 0, sizeof(inbuffer));
            sockaddr_in client;
            socklen_t client_len;
            memset(&client, 0, sizeof(client));
            //接收数据的同时监听到客户端的来源
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&client, &client_len);
            if (s < 0) 
            {
                lg(Waring, "recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[s] = 0;

            //处理数据
            //创建套接字, 用来监听是哪一个客户端

            string message = inbuffer;
            message = func(message);//这里的处理使用一个外部的接口

            //处理完成后, 返回发送给客户端

            sendto(sockfd_, message.c_str(), message.size(), 0, (sockaddr*)&client, sizeof(client));


        }
    }

 UdpServer析构

析构函数不解释

    ~UdpServer()
    {
        if (sockfd_ > 0) close(sockfd_);    
    }

Udpclient

        客户端就是给对应的服务端发送数据。 数据被服务端处理后接收即可:

#include<iostream>
#include<cstdlib>
#include<unistd.h>
using namespace std;
#include<sys/types.h>
#include<strings.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<string>



//./udpclient serverip serverport 
int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        cout << "has return" << endl;
        return 1;
    }
    //
    string serverip = argv[1];
    uint16_t serverprot = stoi(argv[2]);
    
    //创建套接字
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverprot);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t serlen = sizeof(server);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "socker error" << endl; 
        return 1;
    }


    string message;
    char buffer[1024];
    while(true)
    {   

        cout << "please Enter@:" << endl;
        getline(cin, message);  
        
        //发送数据
        sendto(sockfd, message.c_str(), message.size(), 0,  (struct sockaddr*)&server, serlen);
        cout << "yes" << endl;

        //接收数据
        sockaddr_in temp;
        socklen_t socklen;
        ssize_t sz = recvfrom(sockfd, (void*)buffer, sizeof(buffer) - 1, 0, (sockaddr*)&temp, &socklen); //sockfd其实就是网卡的pid
        
        if(sz > 0)
        {
            buffer[sz] = 0;
            cout << buffer << endl;
        }
    }
    

    close(sockfd);
    return 0;
}

实现客户端之间的聊天功能

Udpserver

        实现客户端之间的聊天可是说是在上面的代码中改两个地方。 一个是创建一个哈希表存储有多少客户端连接了服务端。 然后以后发消息就直接便利整个哈希表, 然后将数据发给每一个客户端。 如下:

#include<iostream>
using namespace std;
#include<sys/types.h>
#include<string>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<string.h>
#include<strings.h>
#include"Log.hpp"
#include<functional>
#include<netinet/in.h>
#include<unordered_map>


int defaultport = 8080;
string defaultip = "0.0.0.0";

//包装类要进行修改一下
using func_t = function<string(string, sockaddr_in&, unordered_map<string, sockaddr_in>&)>; 

Log lg;

enum
{
    SockError = 2,
    BindError = 3,
    RecvError = 4,
};

class UdpServer
{
public:
    UdpServer(uint16_t port = defaultport) 
        : port_(port), ip_(defaultip), isrunning_(false)
    {}
    
    //Init不变
    void Init()
    {
        //先创建套接字变量并且完成初始化。 然后就创建网卡文件
        sockaddr_in local;
        
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            lg(Fatal, "create sock error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(SockError);
        }
        lg(Info, "create sock success");
        
        //绑定
        if (bind(sockfd_, (sockaddr*)&local, sizeof(local)) < 0) 
        {
            lg(Fatal, "bind error, errno: %d, strerror: %s", errno, strerror(errno));
            exit(BindError);
        }
        lg(Info, "bind success");
    }
    

    //遍历哈希表,将数据分发给所有的客户端
    void BroadCast(string message, unordered_map<string, sockaddr_in>& clients)
    {
        cout << "1 "<< endl;
        for (auto& e : clients)
        {
            sendto(sockfd_, message.c_str(), message.size(), 0, (sockaddr*)&e.second, sizeof(e.second));
        }
        cout << "2 " << endl;
    }

    
    void run(func_t func)
    {
        isrunning_ = true;
        char inbuffer[1024];


        while (isrunning_)
        {
            memset(inbuffer, 0, sizeof(inbuffer));
            sockaddr_in client;
            socklen_t client_len;
            memset(&client, 0, sizeof(client));
            //接收数据的同时监听到客户端的来源
            ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (sockaddr*)&client, &client_len);
            if (s < 0) 
            {
                lg(Waring, "recvfrom error, errno: %d, strerror: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[s] = 0;

            //处理数据
            //创建套接字, 用来监听是哪一个客户端

            string message = inbuffer;
            message = func(message, client, clients);//这里的处理使用一个外部的接口

            //处理完成后, 返回发送给客户端
            BroadCast(message, clients);

        }
    }

    ~UdpServer()
    {
        if (sockfd_ > 0) close(sockfd_);    
    }
private:
    int sockfd_;
    uint16_t port_;
    string ip_;
    bool isrunning_;
    unordered_map<string, sockaddr_in> clients; //添加哈希表

};

#include"UdpServer.hpp"
#include<memory>

//main.cc主要修改就是Handler函数, 我们要通过client里面的ip地址和端口号作为key,client作为value,然后放入哈希表。 同时将message处理一下,方便我们观看结果。 
string Handler(string message, sockaddr_in& client, unordered_map<string, sockaddr_in>& clients)
{
    string tmp = inet_ntoa(client.sin_addr) + to_string(client.sin_port);
    if (!clients.count(tmp))
    {
        clients[tmp] = client; 
        cout << "ip " << inet_ntoa(client.sin_addr) << " : port " << client.sin_port << " has add in talk room" << endl;
    }
    message = "[" + string(inet_ntoa(client.sin_addr)) + ":" + to_string(client.sin_port) + "]#: " + message; 
    return message;
}

// 

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        cout << "has return" << endl;
        return 1;
    }
    //
    uint16_t serverport = stoi(argv[1]);  

    unique_ptr<UdpServer> svr(new UdpServer(serverport));
    //
    svr->Init();
    svr->run(Handler);


    return 0;
}

Udpclient

#include<iostream>
using namespace std;
#include<string>
#include<sys/types.h>
#include"Log.hpp"
#include<sys/socket.h>
#include<pthread.h>
#include<arpa/inet.h>
#include<string.h>
#include<netinet/in.h>
Log lg;


class ThreadData
{
public:
    sockaddr_in server;
    int sockfd;
};

//数据接收函数
void* recv_message(void* args)
{
    char buffer[1024];
    ThreadData* td = static_cast<ThreadData*>(args);    
    while (true)
    {
        //接收数据
        sockaddr_in temp;
        socklen_t len;
        string info;
        ssize_t s = recvfrom(td->sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&temp, &len);

        if (s < 0)
        {
            lg(Error, "recv error, error: %d, strerror: %s", errno, strerror(errno));
            continue;
        }
        buffer[s] = 0;
        info = buffer;
        cout << info << endl;
    }

}

//数据发送函数
void* send_message(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    string message;
    while (true)
    {  
        getline(cin, message);  //获取数据
        //发送数据
        sendto(td->sockfd, message.c_str(), message.size(), 0, (sockaddr*)&td->server, sizeof(td->server));

    }
}

int main(int argc, char* argv[])
{
    if (argc != 3)
    {
        cout << "Client server" << endl;
    }

    //先拿到套接字的参数
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    ThreadData td;

    //创建套接字与打开网卡
    memset(&td.server, 0, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0); //创建文件描述符, 网卡的文件描述符, 网络传输就是使用网络文件描述符找到对应的文件内的数据
    if (td.sockfd < 0)
    {
        lg(Error, "client create sockfd error, errno: %d, strerror: %s", errno, strerror(errno));
        exit(1);
    }

    

    //创建线程, 然后运行线程, 等待线程
    pthread_t recv, send;
    pthread_create(&recv, nullptr, recv_message, &td);
    pthread_create(&send, nullptr, send_message, &td);

    pthread_join(recv, nullptr);
    pthread_join(send, nullptr);

    close(td.sockfd);
    return 0;
}

运行结果

最后就是运行结果, 运行结果就是下图了, 我们已经能够成功的进行两个客户端之间的远程交流

 ——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!! 

;