Linux:UDP协议的socket套接字

目录

UDP服务端

构造方法

Init初始化方法

start启动方法

main()

UDP客户端

构造方法

Init初始化方法

start启动方法

main()

扩展:英->中翻译词典

扩展:远程命令行

扩展:聊天室

Windows下socket编程


socket套接字一般分为网络套接字Unix域间套接字原始套接字三种

网络套接字用于TCP/IP协议的跨设备网络数据传输 (调用传输层的系统调用接口);Unix域间套接字用于本地通信 ,类似于命名管道;原始套接字可以绕过传输层,直接访问网络层、数据链路层的数据 ,它一般不用于应用层开发,可以用于一些网络抓包,网络侦测等。而本篇所介绍的是网络套接字

由于套接字的种类有三大类,那么对应的接口就会有三套,这样用起来会非常难受,因为很多功能可以被合并为一个接口,因此套接字最终只有一套接口,通过传入不同类型的参数,接口内判断是那种类型,从而解决所有场景下通信

socket常用API:

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

可以看到大部分接口都有一个struct sockaddr *address 参数,该参数是一个sockaddr 结构体类型的指针,内有2字节的地址族14字节的地址数据

对于网络套接字(原始套接字)、Unix域间套接字,在调用socket接口时,都需要传入自己的一套结构体指针(如下图),但接口指定的struct sockaddr又是什么呢?

该结构体是一个只有2字节的地址族和14字节地址数据的结构体,当网络套接字或Unix域间套接字想要传入参数时,需要先将自己的结构体指针强转为 struct sockaddr* 类型,在接口内通过判断第一个字段sa_famliy的值来决定再强转成哪个结构体类型指针(例如网络套接字为AF_INET,域间套接字为AF_UNIX / AF_LOCAL)

sockaddr_in的8字节填充是为了与sockaddr对齐,防止内存越界

原始套接字可以和网络套接字用同一个结构体,当用原始套接字时,第一个字段sa_family为AF_PACKET/AF_ROUTE

而这可以看做是C语言实现的多态机制 ,++sockaddr为基类,sockaddr_in/sockaddr_un为派生类,通过传入不同的派生类来执行不同的方法++

下面通过用UDP实现一个简单的服务器和客户端通信来认识socket接口具体使用方法

ps:以下代码均在此基础上进行:

cpp 复制代码
//颜色控制
#define BLACK "\033[0;30;1m" //黑色
#define RED "\033[0;31;1m"   //红色
#define GREEN "\033[0;32;1m" //绿色
#define YELLOW "\033[0;33;1m"//黄色
#define BLUE "\033[0;34;1m"  //蓝色
#define PURPLE "\033[0;35;1m"//紫色
#define CYAN "\033[0;36;1m"  //青色
#define WHITE "\033[0;37;1m" //白色
#define ED "\033[0m"

enum SevEx//错误码
{
    USAGE_ERR = 1,//传入命令行参数错误
    SOCKET_ERR = 2,//socket()失败
    BIND_ERR,//bind()失败
    INETPN_ERR//inet_pton/inet_ntop失败
};

UDP服务端

将服务器定义为一个类,在类中实现具体方法:

cpp 复制代码
class UdpServer
{
public:
    UdpServer()
    {}
    void InitServer()
    {}
    void start()
    {}
    ~UdpServer()
    {}

private:
    std::string _ip = "0.0.0.0";//ip,一般为0.0.0.0
    uint16_t _port;//端口号
    int _SocketFd = -1;//socket文件描述符

}

对于服务器而言,需要绑定对应的端口号,当后续客户端要向服务器发送数据时,就要指定该端口号

ip为服务器接收的ip地址 ,例如,若定义为127.0.0.1(回环地址),表示只有当客户端向127.0.0.1(前提是在一台服务器上)发送数据时该服务器才会接收,若向其他地址(例如公网IP)发送,就会忽略掉 。(一台云服务器可能会有个ip地址)

为了能接收发送给该云服务器的所有数据包,因此这里需要指定为0.0.0.0 ,意为接收任意IP地址

_SocketFd下面会讲

构造方法

虽然ip一般默认为0.0.0.0,但为了也可以自己传入ip,显式定义了两个构造,分别为只传端口号和传ip+端口号的构造

cpp 复制代码
UdpServer(const uint16_t& port = 1024):_port(port){}//无需给定ip时
UdpServer(const std::string& ip, const uint16_t& port = 1024)//给定ip时
:_ip(ip)
,_port(port)
{}

为什么端口号默认给定1024?

因为0~1023都被系统内定了,真正用户能用的是从1024~65535

Init初始化方法

需要先为UDP创建一个网络套接字,此时就要用到socket(),返回值为一个文件描述符 ,即传入类中的**_SocketFd**成员变量

  • domain为要创建的套接字的类型,由于我们需要的是ipv4的网络套接字,因此为AF_INET
  • type为传输方式,SOCK_STREAM 意为字节流传输 (TCP协议),SOCK_DGRAM 意为数据报传输(UDP协议)。

因为这里用的是UDP协议,所以传输方式为SOCK_DGRAM

  • protocol为协议类型,由于在type时已经指定了数据包类型,因此这里一般为0(根据type自动选取协议)(若为原始套接字,则需要显式选择)

socket创建成功后,需要将ip和端口号绑定到该socket上,此时需要用到bind()接口

  • sockfd即为socket文件描述符
  • addr为要绑定的套接字类型+ip+port信息,因为我们用的是网络套接字,因此传参时传的是**(sockaddr*)sockaddr_in***类型
  • addrlen为addr的长度
cpp 复制代码
void InitServer()
{
    //创建socket
    _SocketFd = socket(AF_INET, SOCK_DGRAM, 0);//网络套接字,数据报形式(UDP)
    if(_SocketFd == -1)//创建套接字失败则退出
    {
        std::cerr << RED "socket error" ED " -> "  << errno << ": " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    std::cout << "创建socket成功,socket_fd: " << _SocketFd << std::endl;

    //绑定ip+port
    sockaddr_in local;
    memset(&local,0,sizeof(local));//初始化置零
    local.sin_family = AF_INET;//网络套接字
    local.sin_port = htons(_port);//主机序列转网络序列(short)
    int n = inet_pton(AF_INET, _ip.c_str(),&(local.sin_addr));//IP: 点分十进制转4字节整数,并主机序列转网络序列(long)
    if(n != 1)
    {
        std::cerr << RED "inet_pton error" ED " -> "  << errno << ": " << strerror(errno) << std::endl;
        exit(INETPN_ERR);
    }
    //local.sin_addr.s_addr = INADDR_ANY;//允许接收任何ip的数据包,想当于0.0.0.0

    n = bind(_SocketFd,(sockaddr*)&local,sizeof(local));//给服务器绑定IP和端口
    if(n == -1)//绑定失败报错退出
    {
        std::cerr << RED "bind error" ED " -> "  << errno << ": " << strerror(errno) << std::endl;
        exit(BIND_ERR);
    }
}

htons() 为主机序列转换网络序列(short类型) ,由于主机可能是大端机也可能是小端机,但发送到网络的序列需要统一大端序,因此该函数会自动将数据转为大端序 (若本来就是大端序则不做修改)。

主机序列 <--> 网络序列 转换的接口:

cpp 复制代码
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//主机序列转网络序列(long)
uint16_t htons(uint16_t hostshort);//主机序列转网络序列(short)
uint32_t ntohl(uint32_t netlong);//网络序列转主机序列(long)
uint16_t ntohs(uint16_t netshort);//网络序列转主机序列(short)

inet_pton() 可以将点分十进制的IP字符串转换成网络二进制序列

  • af为套接字类型,这里就填入AF_INET表示网络套接字(ipv4)
  • src为点分十进制IP字符串
  • dst为输出型参数,例如sockaddr_in,就传入sockaddr_in::sin_addr结构体地址,会自动填入转换完的二进制网络序列IP

除此之外,还有inet_ntop() ,可以将网络二进制序列IP转换成点分十进制的IP字符串

  • af为套接字类型,这里就填入AF_INET表示网络套接字(ipv4)
  • src为网络序列IP地址,例如sockaddr_in,就传入sockaddr_in::sin_addr结构体地址
  • dst为输出型参数,自动填入转换完的点分十进制字符串
  • size为dst字符串的大小

start启动方法

接收(读取)数据需要用到recvfrom() ,该系统调用适用于UDP协议传输

  • sockfd为socket文件描述符
  • buf为读取数据的缓冲区;len为buf的大小(最多读入len个字符)
  • flags为读取的方式,这里设为0,表示默认,阻塞式读取
  • src_addr为输出型参数,用于接收客户端方的ip,port
  • addrlen为输入输出型参数,输入时为src_addr的长度 (因为src_addr传入时统一强转成sockaddr,因此并不知道真实长度),输出时为修改后的src_addr长度(一般来说不变)
cpp 复制代码
void start()
{
    char buffer[1024];//接收数据的缓冲区
    while(1)
    {
        struct sockaddr_in peer;//用于接收客户端的addr数据
        socklen_t len = sizeof(peer);
        
        ssize_t n = recvfrom(_SocketFd, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&peer, &len);//读取数据
        if(n > 0)//数据个数不为0
        {
            buffer[n] = 0;//字符串结束标志
            char ip_str[64];//为了适配C接口inet_ntop
            const char* ClientIp = inet_ntop(AF_INET, &peer.sin_addr, ip_str, sizeof(ip_str));//网络序列IP转点分十进制IP
            if(!ClientIp)//若为NULL,代表ntop失败,退出
            {
                std::cerr << RED "inet_ntop error" ED " -> "  << errno << ": " << strerror(errno) << std::endl;
                exit(INETPN_ERR);
            }
            uint16_t ClientPort = ntohs(peer.sin_port);//网络序列转主机序列(short)
            //输出数据
            std::cout << RED << ClientIp << '[' << ClientPort << "]# " ED << GREEN << buffer << ED << std::endl;
        }
    }
}

main()

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include "UdpServer.hpp"
using namespace std;
using namespace Server;

void usage(char* proc)//使用手册
{
    cout << "使用方法:" GREEN << proc << " [port]" ED << endl;
}

//./UdpServer port
int main(int argc,char* argv[])
{
    if(argc != 2)//若没有按照格式运行,则退出
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);//字符串转整型

    UdpServer server(port);

    server.InitServer();
    server.start();
    
    return 0;
}

UDP客户端

将客户端也定义为一个类,在类中实现具体方法

cpp 复制代码
class UdpClient
{
public:
    UdpClient(const std::string& ServerIp, const uint16_t& ServerPort)
    {}
    void InitClient()
    {}
    void start()
    {}
    ~UdpClient()
    {}
private:
    std::string _ServerIp;//服务端IP地址
    uint16_t _ServerPort;//服务端prot
    int _SocketFd = -1;//socket文件描述符
    sockaddr_in _addr;//发送数据时指定的套接字地址信息
};

客户端向服务端发送数据时,需要指定服务端的IP和端口 ,因此需要ip和port字段

客户端也需要创建socket,因此需要_SocketFd

_addr下面会讲

构造方法

由于客户端向服务端通信必须指定ip + port,因此这里的ip也必须显式定义 ,且port不能设置缺省

cpp 复制代码
UdpClient(const std::string& ServerIp, const uint16_t& ServerPort)
:_ServerIp(ServerIp)
,_ServerPort(ServerPort)
{}

Init初始化方法

客户端同样需要构建socket套接字,用法和服务端时一样

但客户端不需要向服务端一样显式的调用bind对socket绑定ip与端口 ,可以在第一次发送消息(sendto() )时由OS自己选择

  • 对于ip,会根据目标ip来选择最佳的源IP
  • 对于port,会随机选择一个未被使用的port

若显式调用bind,为该客户端指定了一个明确的端口,++如果其他客户端正好也使用了这个明确端口,就会出问题++,因此最好交给OS自行处理

cpp 复制代码
void InitClient()
{
    //创建socket
    _SocketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if(_SocketFd == -1)//创建套接字失败则退出
    {
        std::cerr << RED "socket error" ED " -> "  << errno << ": " << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    //无需显式bind
    //初始化sockaddr_in addr成员
    memset(&_addr, 0, sizeof(_addr));//初始化置零
    _addr.sin_family = AF_INET;//网络套接字类型(ipv4)
    int n = inet_pton(AF_INET, _ServerIp.c_str(), &(_addr.sin_addr));//字符串转二进制网络序列
    if(n != 1)
    {
        std::cerr << RED "inet_pton error" ED " -> "  << errno << ": " << strerror(errno) << std::endl;
        exit(INETPN_ERR);
    }
    _addr.sin_port = htons(_ServerPort);//主机序列转网络序列(short)
}

start启动方法

客户端向服务端发送数据需要用到sendto() ,该系统调用适用于UDP协议传输

  • sockfd即为socket文件描述符
  • buf为要发送的数据;len为发送的数据长度
  • flags为写入的方式,这里设为0,表示默认,阻塞式写入
  • dest_addr为要传给服务端的客户端套接字地址结构体,例如网络套接字就传入 **(sockaddr*)sockaddr_in***类型;addrlen为dest_addr的长度
cpp 复制代码
void start()
{
    std::string message;
    while(1)
    {
        std::cout << "请输入文本# ";
        std::getline(std::cin,message);//按行读取,防止以空格/tab作为分隔
        sendto(_SocketFd, message.c_str(), message.size(), 0, (struct sockaddr*)&_addr, sizeof(_addr));
    }
}

main()

cpp 复制代码
#include <iostream>
#include "UdpClient.hpp"

using namespace std;
using namespace Client;

void usage(char* proc)//使用手册
{
    cout << "使用方法:" GREEN << proc << " [ip] [port]" ED << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)//若参数不规范则退出
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }

    uint16_t serport = atoi(argv[2]);//字符串转为整数
    
    UdpClient Client(argv[1],serport);
    
    Client.InitClient();
    Client.start();
    return 0;
}

在UdpServer启动后,就可以通过 netstat 命令查看该服务器上启动的网络连接进程

简单介绍一下这几个选项:

  • n表示以数字形式显示地址和端口
  • u表示仅显示udp协议的连接
  • a表示显示所有连接和监听端口
  • p表示显示关联进程的pid和程序名

扩展:英->中翻译词典

上面程序中只有客户端对服务端的发送,但在真实的业务逻辑中往往是需要再由服务端处理数据后返回给客户端。

下面就实现一个简单的词典:客户端向服务端发送英语单词 ,服务端通过数据处理后向客户端返回中文翻译

在上面代码的基础上,为UdpServer对象新加一个**_callback 回调方法成员** ,在服务端接收数据后,交给_callback处理,而_callback由实例化UdpServer时传入函数指针 ,这样就完成了接收数据和处理数据的解耦

cpp 复制代码
typedef std::function<void(int, sockaddr_in, std::string)> func_t; // 重命名function对象

class UdpServer
{
public:
    UdpServer(const func_t &callback, const uint16_t &port = 1024) : _port(port), _callback(callback) {} // 无需给定ip时
    UdpServer(const func_t &callback, const std::string &ip, const uint16_t &port = 1024)                // 给定ip时
        : _ip(ip), _port(port), _callback(callback)
    {}

    //......

    void start()
    {
        char message[1024]; // 接收数据的缓冲区
        while (1)
        {
            //......

            _callback(_SocketFd, peer, message); // 数据的处理
        }

private:
    //......
    func_t _callback;            // 数据的处理
};

词典的具体实现在UdpServer.cpp(main文件)中实现:

这里词典为一个unordered_map对象,key用于存英语单词value用于存中文翻译 ,那就需要在服务器启动时将对应的英汉关系加载进词典,这里就用文件实现

cpp 复制代码
const string DictPath = "./dictionary.txt";                                        // 词典文件路径
unique_ptr<unordered_map<string, string>> dict(new unordered_map<string, string>); // 词典

Init初始化函数负责加载词典数据:

cpp 复制代码
bool cutstring(const string &src, string *s1, string *s2, char delimiter) // 将src以delimiter为分隔,分为s1和s2
{
    auto pos = src.find(delimiter);
    if (pos == string::npos)
        return false; // 找不到分隔符无法分隔、

    *s1 = src.substr(0, pos);  //[  )区间
    *s2 = src.substr(pos + 1); // 第二个参数不加默认到最后
    return true;
}

void InitDict() // 初始化词典(加载数据)
{
    ifstream ifs(DictPath, ios::binary); // 打开字典要加载的数据
    if (!ifs.is_open())                  // 打开失败则报错退出
    {
        cerr << RED "oepn file error" ED " -> " << errno << ": " << strerror(errno) << endl;
        exit(OPENFILE_ERR);
    }
    string kv;
    while (ifs >> kv) // 按行读取词典数据中的KV值
    {
        string key, value;
        if (cutstring(kv, &key, &value, ':'))    // 将KV字符串拆分为k/v,以':'做分割
            dict->insert(make_pair(key, value)); // 插入字典
        else                                     // 分隔失败
            cerr << RED "cutstring error" ED " -> " << "格式错误,分隔失败:" << kv << endl;
    }
    ifs.close();
    cout << GREEN GREEN_BL "---词典加载成功---" ED << endl;
}

具体数据处理的方法,也就是要传入_callback的函数,负责查找key的对应value,并发送数据给客户端:

cpp 复制代码
void DictHandler(const int &sockfd, const sockaddr_in &addr/*客户端的ip+port*/, const string &word/*英语单词*/) // 数据处理接口
{
    string chinese;
    auto it = dict->find(word);
    if (it == dict->end()) // 词典中没有
        chinese = "not find";
    else
        chinese = it->second;

    // 返回数据
    sendto(sockfd, chinese.c_str(), chinese.size(), 0, (sockaddr *)&addr, sizeof(addr)); // 向客户端发送对应翻译数据包
}

因为在实际业务中,服务器一旦启动基本不会关闭,所以如果词典文件更新,就需要一个热更新接口来重新加载词典,这里就将某个信号捕捉为热更新接口

cpp 复制代码
void reloding(int sign) // 热加载词典数据
{
    (void)sign;
    InitDict();
}

只需要在main时调用初始化函数并将DictHandler传给UdpServer对象 ,并为3号信号注册热加载方法(属于词典的函数,定义等都在Dictionary命名空间内)

cpp 复制代码
//./UdpServer port
int main(int argc, char *argv[])
{
    //......
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_handler = Dictionary::reloading; // 2. 设置处理函数
    sigemptyset(&sa.sa_mask);              // 显式初始化屏蔽集为空
    sa.sa_flags = 0;                       // 显式关闭所有标志位
    sigaction(SIGQUIT, &sa, nullptr);      // 将3号信号(Ctrl + \)捕捉为热加载
    //signal(3, Dictionary::reloading);

    //......

    Dictionary::InitDict();
    UdpServer server(Dictionary::DictHandler, prt);

    //......

    return 0;
}

对于UDP客户端的start方法,也需要加上发送数据后的接收数据相关动作:

cpp 复制代码
void start()
{
    //......
    while (1)
    {
        //......

        // 接收处理后的数据
        char pro_data[1024] = {0};
        sockaddr_in out;
        memset(&out, 0, sizeof(out));
        socklen_t len = sizeof(len);
        int n = recvfrom(_SocketFd, pro_data, sizeof(pro_data) - 1, 0, (sockaddr *)&out, &len);
        if (n > 0)
            pro_data[n] = 0;
        std::cout << GREEN "翻译结果: " ED << PURPLE << pro_data << ED << std::endl;
    }
}

扩展:远程命令行

再根据上面词典的消息转发逻辑,实现一个远程对服务器机器执行命令的程序

因为我们已经做了接收数据和处理数据的解耦,因此大部分操作只需要在处理数据里修改,修改UdpServer内的_callback回调方法传入的函数即可

因为要远程执行命令,因此就需要再创建一个子进程 ,执行程序替换对应命令,可以用fork+exec系列函数实现,再用管道发送和读取命令的结果

不过本篇不用上面的实现方法,在C的stdio.h库中,有用于创建子进程执行命令的接口:popen

command即为要执行的命令,type为管道文件打开的方式,"r"为读,"w"为写

cpp 复制代码
void WhoCriminal(const sockaddr_in &addr, const string &cmd) // 谁想要执行危险命令?
{
    // 获取到点分十进制ip字符串
    char ip_str[64];
    const char *ClientIp;
    ClientIp = inet_ntop(AF_INET, &addr.sin_addr, ip_str, sizeof(ip_str));
    if (!ClientIp) // ntop失败退出
    {
        cerr << RED "inet_ntop error" ED " -> " << errno << ": " << strerror(errno) << endl;
        exit(INETPN_ERR);
    }
    cerr << RED_BL << ClientIp << '[' << ntohs(addr.sin_port) << "] 妄想进行" << cmd << "操作!!" << ED << endl;
}

void GetFData(FILE *&pipe, string &buffer) // 获取一个文件的全部数据
{
    char line[128];
    while (fgets(line, sizeof(line), pipe))
    {
        buffer += line;
    }
}

void ComdHandler(const int &sockfd, const sockaddr_in &addr, const string &cmd)
{
    string result;
    if (cmd.find("rm") != string::npos || cmd.find("rmdir") != string::npos || cmd.find("mv") != string::npos || cmd.find("su") != string::npos) // 危险命令直接拦截
    {
        WhoCriminal(addr, cmd);
        return;
    }

    // 在命令末尾添加" 2>&1"是为了将stderr和stdout合并(重定向stderr到stdout),使得命令无效时也能读取错误信息
    FILE *pipe = popen((cmd + " 2>&1").c_str(), "r");
    if (!pipe) // 执行失败
        result = "命令执行失败!";

    GetFData(pipe, result); // 获取管道文件中的数据
    pclose(pipe);

    sendto(sockfd, result.c_str(), result.size(), 0, (sockaddr *)&addr, sizeof(addr));
}

ComdHandler为充当_callback参数的接口

扩展:聊天室

上面的远程命令行,当客户端发送数据后,服务端只会对发送消息的这一个客户端转发消息,下面实现一个可以对所有在连接的用户都转发消息的程序,从而实现一个简单的聊天室

由于聊天室需要对所有用户转发,因此需要先实现一个管理在线用户的类

cpp 复制代码
class User
{
public:
    User(const std::string &ip, const uint16_t &port, const std::string &nickname)
        : _ip(ip), _port(port), _nickname(nickname)
    {
    }

    User()
    {
    }

    ~User()
    {
    }

    std::string ip() const
    {
        return _ip;
    }

    uint16_t port() const
    {
        return _port;
    }

    std::string name() const
    {
        return _nickname;
    }

private:
    std::string _ip;       // 用户的ip
    uint16_t _port;        // 用户的端口号
    std::string _nickname; // 用户昵称
};

class OnlineUser
{
public:
    OnlineUser() = default;
    ~OnlineUser() = default;

    void AddUser(const std::string &ip, const uint16_t port, const std::string &nickname)
    {
        std::string id = ip + "-" + std::to_string(port); // id = "ip-port"
        _users.insert(make_pair(id, User(ip, port, nickname)));
    }

    void DelUser(const std::string &ip, const uint16_t &port)
    {
        std::string id = ip + "-" + std::to_string(port);
        _users.erase(id);
    }

    bool IsOnline(const std::string &ip, const uint16_t &port) // 判断该用户是否在线
    {
        const std::string id = ip + "-" + std::to_string(port);
        return _users.find(id) == _users.end() ? false : true;
    }

    std::string find(const std::string &id)
    {
        return _users[id].name();
    }

    void boardcast(int sockfd, const std::string &self_id, const std::string &message) // 对所有在线用户广播message这条消息
    {
        std::string usrmesg = BLUE + self_id + "# " + ED + message; // 最终要消息转发给所有在线用户的字符串

        sockaddr_in ClientAddr; // 每个用户的套接字地址信息
        for (const auto &usr : _users)
        {
            // 初始化
            memset(&ClientAddr, 0, sizeof(ClientAddr));
            ClientAddr.sin_family = AF_INET;
            // 传IP
            int n = inet_pton(AF_INET, usr.second.ip().c_str(), &ClientAddr.sin_addr);
            if (n != 1) // 转换失败则退出
            {
                std::cerr << RED "boardcast: inet_pton error" ED " -> " << errno << ": " << strerror(errno) << std::endl;
                exit(INETPN_ERR);
            }
            // 传port
            ClientAddr.sin_port = htons(usr.second.port());

            // 向每个在线用户发送该用户发的消息
            n = sendto(sockfd, usrmesg.c_str(), usrmesg.size(), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr));
            if (n == -1)
            {
                std::cerr << RED "boardcast: sendto error" ED " -> " << errno << ": " << strerror(errno) << std::endl;
                exit(SENDTO_ERR);
            }
        }
    }

private:
    std::unordered_map<std::string, User> _users; //<id,User>
};

当客户端启动后,用户需要先输入online 昵称,才会被列入在线用户,之后广播消息时才会带上该用户

接下来实现聊天室的数据处理接口(_callback的实参)

cpp 复制代码
OnlineUser onlineuser;

void ChatHandler(const int &sockfd, const sockaddr_in &ClientAddr, const string &message)
{
    // 获取客户端点分十进制ip字符串
    char ip_str[64];
    const char *ClientIp = inet_ntop(AF_INET, &ClientAddr.sin_addr, ip_str, sizeof(ip_str));
    // 获取客户端端口
    const uint16_t ClientPort = ntohs(ClientAddr.sin_port);

    string ChatMess;                      // 要转发给该用户的信息
    if (message.substr(0, 6) == "online") // 上线的信息格式:online nickname
    {
        if (ClientIp == nullptr)
        {
            std::cerr << RED "Chatroom: inet_ntop error" ED " -> " << errno << ": " << strerror(errno) << std::endl;
            exit(INETPN_ERR);
        }
        onlineuser.AddUser(ClientIp, ClientPort, message.substr(7));

        // 发送上线提示信息
        ChatMess = "---已成功上线---";
        sendto(sockfd, ChatMess.c_str(), ChatMess.size(), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr));
    }
    if (message == "offline") // 下线
    {
        if (!onlineuser.IsOnline(ClientIp, ClientPort))
        {
            ChatMess = "---你TM还没上线,下个鸡毛线呢?---";
        }
        else
        {
            onlineuser.DelUser(ClientIp, ClientPort);
            ChatMess = "---下线成功---";
        }
        sendto(sockfd, ChatMess.c_str(), ChatMess.size(), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr));
    }
    else if (onlineuser.IsOnline(ClientIp, ClientPort)) // 此时进行广播消息转发
    {
        std::string idname = ClientIp + std::string("-") + std::to_string(ClientPort); // id = "ip-port"
        idname = onlineuser.find(idname) + "@" + "[" + idname + "]";                   // nickname@[ip-port]
        onlineuser.boardcast(sockfd, idname, message);                                 // 将消息广播给所有在线用户
    }
    else//没上线就想发消息?
    {
        ChatMess = "你还没有上线,请先上线:online [昵称]";
        int n = sendto(sockfd, ChatMess.c_str(), ChatMess.size(), 0, (sockaddr *)&ClientAddr, sizeof(ClientAddr));
        if (n == -1)
        {
            std::cerr << RED "ChatHandler: sendto error" ED " -> " << errno << ": " << strerror(errno) << std::endl;
            exit(SENDTO_ERR);
        }
    }
}

这里贴主写的if else有点难看了,合上之后是这样的逻辑:

服务端搞完了,但以前的客户端是发一条消息收一条消息,且都为阻塞式调用,不符合聊天室的逻辑,因此收消息的过程就需要单独放在子线程/进程中循环运行

cpp 复制代码
class UdpClient
{
public:
    //......

    static void *ReadMess(void *args)
    {
        pthread_detach(pthread_self()); // 分离自己和主线程
        int sockfd = *(static_cast<int *>(args)); // 由于static方法看不到类内成员
        // 接收处理后的数据
        while (true)
        {
            char pro_data[1024] = {0};
            sockaddr_in out;
            memset(&out, 0, sizeof(out));
            socklen_t len = sizeof(out);
            int n = recvfrom(sockfd, pro_data, sizeof(pro_data) - 1, 0, (sockaddr *)&out, &len);
            if (n > 0)
                pro_data[n] = 0;
            std::cout << pro_data << std::endl;
        }
    }

    void start()
    {
        pthread_create(&reder, nullptr, ReadMess, &_SocketFd);
        std::string inmessage;
        while (1)
        {
            std::cerr << RED "# " ED; // 用cerr是为了让它后续输出在自己客户端中而非聊天室(命名管道)中
            
            std::getline(std::cin, inmessage); // 按行读取,防止以空格/tab作为分隔
            inmessage[inmessage.size()] = 0;
            int n = sendto(_SocketFd, inmessage.c_str(), inmessage.size(), 0, (struct sockaddr *)&_addr, sizeof(_addr));
            if (n == -1)
            {
                std::cerr << RED "start: sendto error" ED " -> " << errno << ": " << strerror(errno) << std::endl;
                exit(SENDTO_ERR);
            }
        }
    }

    //......

private:

    //......

    pthread_t reder; // 持续读数据线程的句柄
};

在运行时,客户端需要额外一个窗口来用于创建/查看管道

Windows下socket编程

在Windows下的socket接口使用几乎完全一样,下面用Windows下的VS环境写一个UDP通信的客户端(发一条收一条)

在Windows环境下,要使用socket,需要包含WinSock2

cpp 复制代码
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")

前者提供接口声明,后者确保链接正确性

在main函数中也要先启动socket

cpp 复制代码
WSAData wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
	cout << "WSAStartup Error =" << WSAGetLastError() << endl;
	return 0;
}
else
{
	cout << "WSAStartup Success\n";
}

并且socket套接字的类型是SOCKET(其实本质还是整数)

cpp 复制代码
SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

除此之外,操作与Linux下完全一样!!

下面代码为简单实现的向服务器发送数据且服务器返回原数据的程序

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <string>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main()
{
	WSAData wsd;
	if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
	{
		cout << "WSAStartup Error =" << WSAGetLastError() << endl;
		return 0;
	}
	else
	{
		cout << "WSAStartup Success\n";
	}
	SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
	if (sock == -1) // 创建套接字失败则退出
	{
		std::cerr <<"InitClient: socket error -> " << errno << endl;
		exit(-1);
	}

	uint16_t port = 8080;
	string ip = "180.76.250.209";

	//填写Server端的addr
	sockaddr_in ServerAddr;
	memset(&ServerAddr, 0, sizeof(ServerAddr));

	ServerAddr.sin_family = AF_INET;
	ServerAddr.sin_port = htons(8080);
	int n = inet_pton(AF_INET, ip.c_str(), &ServerAddr.sin_addr);
	if (n != 1)
	{
		cerr <<"Init Client: inet_pton error -> " << errno << endl;
		exit(-2);
	}
	string message;
	while (true)
	{
		//写入
		getline(cin, message);
		n = sendto(sock, message.c_str(), message.size(), 0, (sockaddr*)&ServerAddr, sizeof(ServerAddr));
		if (n == -1)
		{
			std::cerr << "start: sendto error -> " << errno << endl;
			exit(-3);
		}
		//读取
		char buffer[1024] = { 0 };
		sockaddr_in revaddr;
		memset(&revaddr, 0, sizeof(revaddr));
		int len = sizeof(revaddr);
		n = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (sockaddr*)&revaddr, &len);
		if(n > 0)
			buffer[n] = 0;
		cout << buffer << "(Server echo)" << endl;
	}
	
	
	return 0;
}
相关推荐
狮子再回头1 小时前
relhat9.1 yum无法安装问题
linux·运维·centos
杨云龙UP1 小时前
Oracle 19c 单机环境安装目录规划与磁盘永久挂载操作指南_2026-06-15
运维·服务器·数据库·oracle·部署·目录·规划
暮云星影1 小时前
全志linux开发 USB接口设置
linux·arm开发·驱动开发
王二端茶倒水2 小时前
智慧公寓网络运营:从入住开通到退租停用
运维·物联网·架构
翼龙云_cloud2 小时前
阿里云代理商:如何管理CPFS的POSIX客户端挂载点?
运维·阿里云·云计算·阿里云 cpfs
A.说学逗唱的Coke2 小时前
【大模型专题】AIOps + Loop 工程:从智能告警到自愈闭环的实战指南
运维·人工智能·devops
xingyuzhisuan2 小时前
8 卡 / 16 卡 GPU 服务器机架布线与高速互联带宽优化技术详解
运维·服务器·云计算·gpu算力
江华森2 小时前
Linux 系统实战完全指南
linux·运维·服务器
Safeploy安策数据2 小时前
政务云加密太慢?万兆服务器密码机如何破解高并发性能瓶颈
linux·运维·github