【Linux】Socket编程UDP Echo 服务器→翻译服务器→多人聊天室

文章目录


一、V1 版本 - echo server

UdpServer

服务端先从网络中读消息然后再写。

初始化套接字(Init)

系统调用socket

创建套接字的系统调用:

参数一:

协议族/域,用来告诉套接字是做本地通信还是网络通信还是其他。

参数二:

套接字类型:

参数三:

默认为0即可。

返回值:

我们能看到返回值竟然是一个文件描述符,所以这正是linux下一切皆文件的体现,网卡本质也是文件,未来网络通信,本质也是文件通信!

现在我们调用socket只是把网络在文件系统中打开了,还需要给文件绑定ip地址和端口号,这就要用到下面的bind。

bind

套接字编程中,bind 是将【IP 地址 + 端口号】绑定到 套接字(socket)文件描述符 上,而不是直接绑定到进程。

在介绍bind之前小编先介绍几个接下来几个常用的结构体:

struct sockaddr 通用地址结构 函数参数通用类型

struct sockaddr_in IPv4 地址结构 实际存放 IP + 端口

struct sockaddr_in6 IPv6 地址结构 IPv6 使用

第一个参数传递socket的返回值。

第二个参数传递一个填充好ip地址和端口号的结构体:

第三个参数传递结构体的大小。

返回值等于0表示绑定成功,不等于0表示失败。

在介绍bind代码代码实现之前小编先介绍两个需要包的头文件:<netinet/in.h>头文件能提供各种数据类型(sockaddr_in就在该头文件中),<arpa/inet.h>能提供各种大小端转化的方法,加上之前介绍的<sys/types.h>和<sys/socket.h>,这四个头文件是我们网络编程时几乎必须包含的四个头文件。

bind代码分两步实现:

1、填充ip和port

在填充ip和port之前,还需要在 struct sockaddr_in 结构体中,将sin_family 字段赋值为 AF_INET,用来告诉操作系统:这个套接字使用 IPv4 地址格式来进行网络通信,后续的 IP 地址和端口号都要按照 IPv4 协议来解析和处理。

首先要创建一个sockaddr_in类型结构体,然后把它的内存空间清零,我们可以用memset,小编介绍一种新方法,用bzero,需要包<strings.h>头文件,它可以将一段指定的内存空间清零。

ip和端口号需要通过命令行参数传递,所以udpserver.cc中的main函数要接受命令行参数,然后创建UdpServer对象时再把接收到的ip和端口号以参数的形式传递。

填入端口号_port时要把小端转大段端htons(_port),主机字节序转网络字节序),因为端口号也要随着报文一起发给对方主机,这样对方主机在回消息时才知道发送方在哪里。(htons中的s表示short,将16 位无符号短整型 从主机字节序转换为网络字节序)

填入ip地址要把可读性好的字符串风格的点分十进制ip转化为4个字节的整数ip,方便网络传输,这里需要用到inet_addr,它不仅可以将字符串风格的点分十进制ip转为整数ip,并且它的返回值已经是网络字节序,所以不需要再通过htons转换。

还需要注意填ip地址时要这样:local.sin_addr.s_addr,因为sin_addr是结构体,C++规定结构体不能整体赋值,所以只能对sin_addr内部的整型变量s_addr赋值。

2、sockaddr_in和socketfd进行bind

调用bind系统调用即可。

开始通信(Start)

目前服务器已经初始化完成,接下来我们就要基于文件描述符来进行通信了。

1、读取数据

因为udp不是面向字节流的,所以不能直接用read,udp有一套自己的读取、发送数据接口:

recvfrom:

参数:

1、接收方文件描述符。

2、输出型参数,用户自定义一段缓冲区,将读到的数据带出。

3、表示参数2用户自定义缓冲区的大小。

4、表示读取策略,默认置为0,表示阻塞读取。

5、输出型参数,是对端主机的ip和端口号信息,是一个sockaddr_in类型的结构体变量,方便我们回消息。(套接字编程:发送数据 = 消息内容 + 发送主体)

6、输入输出型参数,表示参数5的结构体大小,输入表示传入的结构体大小,输出表示实际结构体大小。

返回值:

大于0表示实际读取多少字节,小于0表示读取失败。

2、发送数据

sendto:

参数:

1、发送方文件描述符。这里可以引出一个结论:
udp既可以读时写,也可以写时读,支持全双工通信。

其他参数类似recvfrom,只不过第5个参数是输入型参数,要传递目标主机的sockaddr_in结构体,还要注意第六个参数是整形类型,不是recvfrom的指针类型。

服务端源码-V1

cpp 复制代码
//UdpServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include "Logger.hpp"

static const int gdefaultsockfd = -1;

class UdpServer
{
public:
    UdpServer(const std::string &ip, const int port) 
    :_ip(ip)
    ,_port(port)
    ,_sockfd(gdefaultsockfd)
    {

    }
    void Init()
    {
        // 1、创建套接字fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            //创建套接字失败
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
        // 2、bind
        // 2.1 填充ip和port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        //用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // 2.2sockaddr_in和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; //清空缓冲区
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
             (struct sockaddr *)&peer, &len);
            if(n > 0)
            {
                buffer[n] = 0; //给接收到的字符串的末尾字符的下一个位置写/0
                LOG(LogLevel::DEBUG) << "client say# " << buffer;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
                 (struct sockaddr *)&peer, len);
            }
        }
        _isrunning = false; //以防服务器自己跑出来
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~UdpServer()
    {}

private:
    int _sockfd;
    uint16_t _port;
    std::string _ip;

    bool _isrunning;
}; 
cpp 复制代码
//UdpServer.cc
#include "UdpServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localip localpoat" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    EnableConsoleLogStrategy();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(ip, port);
    usvr->Init();
    usvr->Start();
    return 0;
}

UdpClient

客户端对于服务端来说是客户端先开口说话,所以客户端先写数据到网络里然后再读。

写客户端大致流程和服务端类似,但是有下面几个注意事项:

1、客户端不需要日志系统,直接输出即可。

2、客户端创建sockfd后不用显示的调用bind绑定自己的ip和端口号,当客户端第一次发送消息后会由OS自动将客户端sockfd和本台主机的ip绑定,并随机分配一个没有被使用的端口号,所以客户端直接发消息即可,因为对于客户端来说,会有各种app,如微信、抖音、支付宝,这些公司不会互相协商对端口号的绑定规则,所以如果让客户端程序显示绑定端口号,就有可能两个进程同时绑定一个端口号,势必会造成一个进程无法启动。而对于服务端来说,可以显示的绑定自己的ip和端口号,因为服务端都是属于同一家公司,公司内部可以对端口号的使用做分配。

3、客户端中一般会内置服务端的ip和端口号,这里我们也用命令行参数来传递服务端的ip和端口号。

源码

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>

void Usage(std::string proc)
{
    //proc表示类似于 ./client
    std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    //提取命令行参数
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

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

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    //用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line); //读取客户端输入并转化为字符串
        //写
        sendto(sockfd, line.c_str(), line.size(), 0,
         (struct sockaddr *)&server, sizeof(server));
        //读
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
         (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            //读成功
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

测试代码注意事项

1、现在我们客户端和服务端都写完了,测试服务端时命令行参数ip填127.0.0.1,这是本地环回主机ip,专门用来本地测试和本地通信,端口号随便填。

2、当我们启动服务器后,想验证服务器是否启动可以用 netstat 命令,用来查看网络状态,-n表示查udp服务器,-ua表示查所有udp服务器,-n表示把能显示成数字的信息显示为数字,-p表示显示更多进程信息。

3、云服务器禁止用户bind公网ip,即使是能bind公网ip或任意ip,最佳实践还是不建议服务端bind固定ip! 而是任意ip bind,操作如下所示:

cpp 复制代码
local.sin_addr.s_addr = htonl(INADDR_ANY); 

这样只要是发给这台主机的8080端口号的信息(如该主机的环回ip 127.0.0.1 和 该主机的公网ip如1.95.35.127),服务端就都能接收,如果服务端绑定了固定ip,那么该服务端就只能收到发给该固定ip的消息。所以服务端不用再通过命令行参数传递ip了,服务端的只用接收端口号即可。

完整源码

cpp 复制代码
//udpserver.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include "Logger.hpp"

static const int gdefaultsockfd = -1;

class UdpServer
{
public:
    UdpServer(const int port) 
    :_port(port)
    ,_sockfd(gdefaultsockfd)
    {

    }
    void Init()
    {
        // 1、创建套接字fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            //创建套接字失败
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
        // 2、bind
        // 2.1 填充ip和port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        //用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip bind
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // 2.2sockaddr_in和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; //清空缓冲区
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
             (struct sockaddr *)&peer, &len);
            if(n > 0)
            {
                //获取客户端ip和端口号
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);

                buffer[n] = 0; //给接收到的字符串的末尾字符的下一个位置写/0
                LOG(LogLevel::DEBUG) << "[" << clientip << ":" <<
                 clientport << "]# " << buffer;
                std::string echo_string = "server echo# ";
                echo_string += buffer;
                sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0,
                 (struct sockaddr *)&peer, len);
            }
        }
        _isrunning = false; //以防服务器自己跑出来
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~UdpServer()
    {}

private:
    int _sockfd;
    uint16_t _port;
    //std::string _ip; //

    bool _isrunning;
}; 
cpp 复制代码
//udpserver.cc
#include "UdpServer.hpp"
#include <iostream>
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localpoat" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    EnableConsoleLogStrategy();
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);
    usvr->Init();
    usvr->Start();
    return 0;
}
cpp 复制代码
//udpclient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>

void Usage(std::string proc)
{
    //proc表示类似于 ./client
    std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    //提取命令行参数
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

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

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    //用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line); //读取客户端输入并转化为字符串
        //写
        sendto(sockfd, line.c_str(), line.size(), 0,
         (struct sockaddr *)&server, sizeof(server));
        //读
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
         (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            //读成功
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

二、V2 版本 - DictionaryServer

EchoServer主要解决的是io问题,现在我们要给服务端添加一些业务,可以完成翻译功能,可以把客户端发送给服务端的信息做翻译。

1、首先我们要理解服务端DictServer本身只用承担发送数据和接收数据的功能,具体业务逻辑是需要另外写一段程序的,然后由服务端回调执行后再返回,所以我们要把软件进行分层实现,Dictionary.hpp负责翻译,DictServer.hpp负责网络IO。

源码

cpp 复制代码
//Dictionary.hpp
#pragma once

#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"

//定义分隔符
const static std::string sep = ":";

class Dictionary
{
private:
    //加载_path中的配置文件并初始化unordered_map
    void LoadConf()
    {
        //以只读模式打开 _path 路径对应的文件,并将其绑定到 in 这个输入流对象上,
        //后续就可以通过 in 像 std::cin 一样读取文件内容
        std::ifstream in(_path);
        if(!in.is_open())
        {
            LOG(LogLevel::ERROR) << "open file error: " << _path;
            return;
        }
        std::string line;
        while(std::getline(in, line))
        {
            LOG(LogLevel::DEBUG) << "load dict nessage: " << line;
            //拆分字符串line,分别插入unordered_map: dog:狗
            auto pos = line.find(sep);
            if(pos == std::string::npos)
            {
                //没找到分隔符
                LOG(LogLevel::ERROR) << "format error:" << line;
                continue; //只是这一行字符串没有分隔符,还需要继续加载后续的字符串
            }
            std::string word = line.substr(0, pos); //起始位置,截取长度
            std::string value = line.substr(pos + sep.size()); //不传第二个参数,默认截取到结尾
            if(word.empty() || value.empty())
            {
                LOG(LogLevel::ERROR) << "format error, word or value is empty: " << line;
                continue;
            }
            _dict.insert({word, value});
        }

        in.close();
    }

public:
    Dictionary(const std::string &path)
    :_path(path)
    {
        LOG(LogLevel::INFO) << "construct dictionary obj";
        LoadConf();
    }
    
    std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
    {
        //该接口不负责收发数据,故不用参数whoip和whoport
        (void)whoip;
        (void)whoport;
        auto iter = _dict.find(word);
        if(iter == _dict.end())
        {
            //没找到
            return "unknown";
        }
        //找到了
        return iter->first + "->" + iter->second;
    }

    ~Dictionary()
    {}
private:
    std::string _path;
    std::unordered_map<std::string, std::string> _dict;
};
cpp 复制代码
//DictServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "Logger.hpp"

using callback_t = std::function<std::string (const std::string &word,
     const std::string &whoip, uint16_t whoport)>;

static const int gdefaultsockfd = -1;

class DictServer
{
public:
    DictServer(const int port, callback_t cb) 
    :_port(port)
    ,_sockfd(gdefaultsockfd)
    ,_cb(cb)
    {

    }
    void Init()
    {
        // 1、创建套接字fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            //创建套接字失败
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;
        // 2、bind
        // 2.1 填充ip和port
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        //用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = htonl(INADDR_ANY); //任意ip bind
        //local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // 2.2sockaddr_in和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; //清空缓冲区 
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
             (struct sockaddr *)&peer, &len);
            if(n > 0)
            {
                buffer[n] = 0; //转C语言风格字符串
                //获取客户端ip和端口号
                uint16_t clientport = ntohs(peer.sin_port);
                std::string clientip = inet_ntoa(peer.sin_addr);

                std::string word = buffer;
                LOG(LogLevel::DEBUG) << "用户查找: " << word;
                std::string result = _cb(word, clientip, clientport);


                // buffer[n] = 0; //给接收到的字符串的末尾字符的下一个位置写/0
                // LOG(LogLevel::DEBUG) << "[" << clientip << ":" <<
                //  clientport << "]# " << buffer;
                // std::string echo_string = "server echo# ";
                // echo_string += buffer;

                sendto(_sockfd, result.c_str(), result.size(), 0,
                 (struct sockaddr *)&peer, len);
            }
        }
        _isrunning = false; //以防服务器自己跑出来
    }

    void Stop()
    {
        _isrunning = false;
    }

    ~DictServer()
    {}

private:
    int _sockfd;
    uint16_t _port;
    //std::string _ip; //
    callback_t _cb;
    bool _isrunning;
}; 
cpp 复制代码
//DictServer.cc
#include "Dictionary.hpp" //负责翻译
#include "DictServer.hpp" //负责网络IO
#include <iostream>
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localpoat" << std::endl;
}

// std::string Translate(const std::string &word, const std::string &whoip, uint16_t whoport)
//     {
//         return "哈哈";
//     }

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    EnableConsoleLogStrategy();

    Dictionary dict("./dict.txt");

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<DictServer> usvr = std::make_unique<DictServer>(port,
         [&dict](const std::string &word, const std::string &whoip, uint16_t whoport)->std::string{
            return dict.Translate(word, whoip, whoport);
    });
    usvr->Init();
    usvr->Start();
    return 0;
}
cpp 复制代码
//DictClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>

void Usage(std::string proc)
{
    //proc表示类似于 ./client
    std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}

int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    //提取命令行参数
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

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

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    //用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line); //读取客户端输入并转化为字符串
        //写
        sendto(sockfd, line.c_str(), line.size(), 0,
         (struct sockaddr *)&server, sizeof(server));
        //读
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
         (struct sockaddr*)&temp, &len);
        if(m > 0)
        {
            //读成功
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
    return 0;
}

三、V3 版本 - ChatSystemDemo

前面我们实现的V2版本的服务器是单进程版本,效率比较低,我们先实现更高效的服务器我们使用线程池,服务器将任务打包派发给线程池,然后由线程池中的现场完成任务,这样就可以实现多个线程一边处理任务,服务器一边从客户端读取数据,使得生产和消费并发运行。

但是我们接下来不再实现多线程的DictServer,而是实现一个多用户聊天系统。

cpp 复制代码
//ChatServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <strings.h>
#include <cstdlib>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"
#include "Route.hpp"

//此处必须message和add必须拷贝传参,因为他们本质是临时变量
using callback_t = std::function<void (int sockfd,
     std::string message, InetAddr &addr)>; 

static const int gdefaultsockfd = -1;

class ChatServer
{
public:
    ChatServer(const int port, callback_t cb) 
    :_port(port)
    ,_sockfd(gdefaultsockfd)
    ,_cb(cb)
    {

    }
    void Init()
    {
        // 1、创建套接字fd
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if(_sockfd < 0)
        {
            //创建套接字失败
            LOG(LogLevel::FATAL) << "create socket error";
            exit(1);
        }
        LOG(LogLevel::INFO) << "create socket sucess: " << _sockfd;

        // 2、bind
        // 2.1 填充ip和port
        InetAddr local(_port);

        // 2.2sockaddr_in和socketfd进行bind
        int n = bind(_sockfd, (struct sockaddr*)&local, local.Lenth());
        if(n < 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind socket sucess: " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while(_isrunning)
        {
            char buffer[1024];
            buffer[0] = 0; //清空缓冲区,默认当前buffer为空
            sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer), 0,
             (struct sockaddr *)&peer, &len);
            if(n > 0)
            {
                //约定:buffer中是聊天消息
                buffer[n] = 0; //转C语言风格字符串
                //获取对应Client
                InetAddr Clientaddr(peer);
                LOG(LogLevel::DEBUG) << "get a client info: # "
                    << Clientaddr.Ip() << " - " << Clientaddr.Port() << ": "
                    << buffer;

                std::string message = buffer;
                
                //回调(将Clientaddr发来的message通过_sockfd转发给线程池中每一个线程)
                _cb(_sockfd, message, Clientaddr);


                // 服务器目前只需收消息然后上层回调,回消息由线程池完成
                // sendto(_sockfd, result.c_str(), result.size(), 0,
                //  (struct sockaddr *)&peer, len);
            }
        }
        _isrunning = false; //以防服务器自己跑出来
    }

    void Stop()
    {
        _isrunning = false;
    }

    ChatServer()
    {}

private:
    int _sockfd;
    uint16_t _port;
    callback_t _cb;
    bool _isrunning;
}; 

封装 sockaddr_in,简化 Linux 网络编程的地址处理:

cpp 复制代码
//InetAddr.hpp
#pragma once

// 用来描述client socket信息的类
// 方便后续用来管理客户端

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

//定义一个宏
#define Conv(addr) ((struct sockaddr*)&addr)

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        _ip = inet_ntoa(_addr.sin_addr);
    }

    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
    }

public:
    InetAddr(const sockaddr_in &addr)
        : _addr(addr)
    {
        Net2Host();
    }
    // 重载构造函数
    // ip缺省值为"0.0.0.0",经过inet_addr转化后就是INADDR_ANY
    InetAddr(uint16_t port, const std::string ip = "0.0.0.0")
        : _port(port), _ip(ip)
    {
        Host2Net();
    }
    //get方法
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr* Addr()
    {
        return Conv(_addr);
    }
    socklen_t Lenth()
    {
        return sizeof(_addr);
    }
    //用于比较两个用户地址是否相等
    bool operator==(const InetAddr &addr)
    {
        return (_ip == addr._ip && _port == addr._port);
    }

    //for debug _addr信息转为字符串
    std::string ToString()
    {
        return _ip + "-" + std::to_string(_port);
    }

    ~InetAddr()
    {
    }

private:
    // 网络风格地址
    sockaddr_in _addr;
    // 主机风格地址
    std::string _ip;
    uint16_t _port;
};

管理在线用户 + 把一个人发的消息转发给所有人:

cpp 复制代码
//Route.hpp
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"

// 用于维护所有在线用户
class Route
{
private:
    //用于判断用户是否存在
    bool IsExists(const InetAddr &addr)
    {
        for(auto &user : _online_user)
        {
            if(user == addr)
            {
                return true;
            }
        }
        return false;
    }

    void AddUser(const InetAddr &addr)
    {
        if(!IsExists(addr))
            _online_user.push_back(addr);
    }

    void SendMessageToAll(int sockfd, std::string &message, InetAddr addr)
    {
        for(auto &user : _online_user)
        {
            LOG(LogLevel::DEBUG) << "route [" <<  message << "] to " << user.ToString();

            std::string info = addr.ToString();
            info += "# ";
            info += message;
            sendto(sockfd, info.c_str(), info.size(), 0, user.Addr(), user.Lenth());
        }
    }

    void DeleteUser(std::string &message, InetAddr addr)
    {
        //权宜之计,后续用协议判断才是最佳实践
        //这里应该用message和"QUIT"进行比较,而不是message.c_str()
        if(message == "QUIT") 
        {
            //!这里不能用范围for,在范围for里erase会导致迭代器失效!
            auto iter = _online_user.begin();
            //迭代器遍历应该用!=,而不是<=
            for(; iter != _online_user.end(); iter++)
            {
                if(*iter == addr)
                {
                    _online_user.erase(iter);
                    break;
                }
            }
        }
    }

public:
    Route()
    {

    }

    void RouteMessageToAll(int sockfd, std::string &message, InetAddr addr)
    {
        AddUser(addr);
        SendMessageToAll(sockfd, message, addr);

        DeleteUser(message, addr);

    }

    // void RouteMessageToOne(int sockfd, std::string &message, InetAddr addr)
    // {

    // }

    ~Route()
    {

    }
private:
    //临界资源
    std::vector<InetAddr> _online_user;
};
cpp 复制代码
//ServerMain.cc
#include "ThreadPool.hpp"
#include "Route.hpp"
#include "ChatServer.hpp" 
#include <iostream>
#include <memory>

void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localpoat" << std::endl;
}

//线程池任务默认无参无返回值
using task_t = std::function<void()>;

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    EnableConsoleLogStrategy();

    uint16_t port = std::stoi(argv[1]);

    //1、消息转发功能
    std::unique_ptr<Route> r = std::make_unique<Route>();

    //2、定义线程池对象
    ThreadPool<task_t> *tp = ThreadPool<task_t>::GetInstance();


    std::unique_ptr<ChatServer> usvr = std::make_unique<ChatServer>(port,
        [&r, &tp](int sockfd, std::string message, InetAddr &addr){
            task_t task = std::bind(&Route::RouteMessageToAll, r.get(),
             sockfd, message, addr);
            tp->Enqueue(task);
    });

    usvr->Init();
    usvr->Start();
    return 0;
}

修改客户端:

目前客户端存在一个问题,因为客户端是单线程的,所以当用户不发消息时客户端线程会在sendto处被阻塞,那么就无法通过recvfrom读取其他用户发的消息,所以需要对服务器进行改进,就算客户端用户不发消息也能看到其他用户的消息。

解决方案是在客户端也引入多线程,让线程并发执行写sendto和读recvfrom,这就是udp的全双工应用表现。

这里我们用C++内部的thread来实现客户端多线程。

cpp 复制代码
//ChatClient.cc
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <thread>

void Usage(std::string proc)
{
    // proc表示类似于 ./client
    std::cerr << "Usage: " << proc << "localip localpoat" << std::endl;
}

int sockfd = -1;
std::string serverip;
uint16_t serverport;

void InitClient(const std::string &serverip, uint16_t serverport)
{

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cout << "create socket error" << std::endl;
    }
}

void recver()
{
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int m = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                         (struct sockaddr *)&temp, &len);
        if (m > 0)
        {
            // 读成功
            buffer[m] = 0;
            std::cout << buffer << std::endl;
        }
    }
}

void sender()
{
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    // 用来告诉操作系统:这个套接字使用IPv4地址格式来进行网络通信
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        std::cout << "Please Enter@ ";
        std::string line;
        std::getline(std::cin, line); // 读取客户端输入并转化为字符串
        // 写
        sendto(sockfd, line.c_str(), line.size(), 0,
               (struct sockaddr *)&server, sizeof(server));
    }
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    // 提取命令行参数
    serverip = argv[1];
    serverport = std::stoi(argv[2]);

    // 创建客户端
    InitClient(serverip, serverport);

    // 创建两个线程
    std::thread recv(recver);
    std::thread send(sender);

    // 线程等待
    recv.join();
    send.join();
    
    return 0;
}

四、windows和linux之间进行通信

我们前面已经实现了用linux做客户端和服务端进行通信,但是我们现实中还是windows做客户端和linux做服务端更常见,所以我们接下来介绍windows和linux之间如何进行网络通信。

其实实现windows和linux之间的网络通信很简单,因为我们知道windows和linux共享同一套网络协议栈,所以我们可以提前下结论,当两个平台进行通信时,它们的socket接口基本是一样的。


以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
yyuuuzz2 小时前
国际云服务器的技术特性与使用场景
运维·服务器
代码中介商2 小时前
Linux多线程编程进阶:fork与锁的交互及网络编程入门
linux·运维·服务器
我不是立达刘宁宇2 小时前
权限提升-前置基础-linux
linux·运维·服务器
IOT.FIVE.NO.12 小时前
claude code desktop cowork报错解决和记录Workspace..The isolated Linux environment ...
linux·服务器·数据库
TOWE technology2 小时前
EN32/G2401FCI——32A大功率,24位国标输出的高密度配电方案
linux·服务器·网络·科技·数据中心·pdu·智能pdu
青梅橘子皮11 小时前
Linux---基本指令
linux·运维·服务器
REDcker11 小时前
Linux信号机制详解 POSIX语义与内核要点 sigaction与备用栈实践
linux·运维·php
cui_ruicheng12 小时前
Linux进程间通信(三):System V IPC与共享内存
linux·运维·服务器
蚰蜒螟12 小时前
深入 Linux 内核同步机制:从 futex 到 spinlock 的完整旅程
linux·windows·microsoft