【Linux】UDP的服务端 + 客户端

文章目录

  • 前言
  • 1. TCP和UDP
  • 2. 网络字节序
    • 2.1 大小端字节序:
    • 2.2 转换接口:
  • 3. socket接口
    • 3.1 sockaddr结构:
    • 3.2 配置sockaddr_in:
    • 3.3 inet_addr:
    • 3.4 inet_ntoa:
    • 3.5 bind绑定:
  • 4. 服务端start
    • 4.1 recvfrom:
    • 4.2 sendto:
  • 5. 客户端
  • 6. 测试
  • 7. Windows客户端

前言

从上一章开始我们正式进入Linux网络编程的学习,上回中我们对网络有了大概的认识,宏观上了解了网络的传输过程,对局域网广域网以及Mac地址和IP地址有了初步的认识。
本章我们正式进入网络编程,用代码来实现网络间的通信,学习认识相关的接口……


1. TCP和UDP

为了完成通信,传输层有两个重要的协议。

  • TCP:Transmission Control Protocol 传输控制协议
  • 传输层协议:
    • 在网络通信中负责提供端到端的数据传输服务的协议。
  • 有链接:
    • 在数据传输之前,发送方和接收方需要建立一个可靠的连接。
  • 可靠传输:
    • 通过一系列的机制和算法来确保数据能够完整、准确地传输到目标主机,并且按照正确的顺序进行重组和接收。
  • 面向字节流:
    • 是一种数据传输的方式,其中数据被视为一连串的字节序列。
  • UDP:User Datagram Protocol 用户数据报协议
  • 传输层协议:
    • 在网络通信中负责提供端到端的数据传输服务的协议。
  • 无连接:
    • 无连接指的是数据传输时不需要先建立连接再进行通信的方式,比如所有人都能给你的电子邮箱发送邮件。
  • 不可靠传输:
    • 在不可靠传输中,数据传输过程中不进行可靠性保证的方式,发送方将数据发送给接收方,但不对数据的正确性和完整性进行确认和修复。
  • 面向数据报:
    • 数据在传输过程中被划分为独立的数据报进行传输,每个数据报(也称为包、帧等)都包含了完整的源地址、目标地址和其他必要的信息,使得每个数据报都能够独立地进行路由和处理。

可靠与不可靠传输,更多的标明的是一种通信特征。不能说tcpudp哪个更好,只能说哪个更合适。


2. 网络字节序

我们之前学过C语言都知道,有大端机和小端机,那么不同的计算机的字节序,要向网上发,其他计算结接收时,不知道发过来的数据是按照大端还是小端字节序来读,所有必须要有统一的规定。

规定网络字节序列是一种大端序列。

  • 要保证发到网络中的序列必须是大端的。
  • 无论是发送方还是接收方,都要直接或者间接的将自己的数据由主机序列转成网络序列,或者由网络序列转成主机序列。

2.1 大小端字节序:

  • 大端字节序(Big-endian):
  • 是指将高位字节存储在低地址,低位字节存储在高地址的方式。
  • 例如,十六进制数0x12345678在大端字节序下的内存存储方式为0x12 0x34 0x56 0x78
  • 小端字节序(Little-endian):
  • 是指将低位字节存储在低地址,高位字节存储在高地址的方式。
  • 例如,十六进制数0x12345678在小端字节序下的内存存储方式为0x78 0x56 0x34 0x12

2.2 转换接口:

【Linux】UDP的服务端 + 客户端_第1张图片
如果主机就是大端机,这些函数什么都不会做。主机是小端机,则会将主机字节序转换成网络字节序(大端字节序),网络字节序转主机字节序也是同样的道理。


3. socket接口

【Linux】UDP的服务端 + 客户端_第2张图片
【Linux】UDP的服务端 + 客户端_第3张图片

  • Socket(套接字)是一种用于网络通信的编程接口和抽象概念,它使得应用程序可以通过标准的流式或数据报式方式发送和接收数据包,实现不同计算机之间的通信。
  • 在计算机网络中,一个 Socket 由 IP 地址和端口号两部分组成
    • IP 地址用于标识网络上的一个主机,而端口号则用于标识该主机上的一个进程。
    • Socket 通过 IP 地址和端口号来唯一确定一个网络上的进程,并且可以通过多种协议(如 TCP、UDP)进行数据传输。
  • 使用 Socket 接口,应用程序可以直接访问网络协议栈,与其他主机建立连接并进行数据交互。因此,Socket 是实现各种协议和服务的基础,如 HTTP、SMTP、FTP、Telnet 等。

返回值:

在这里插入图片描述

Linux下的一切皆文件包括了socket接口。每个打开的文件(包括socket)都会被分配一个文件描述符(file descriptor),它是一个非负整数。

  • 使用socket API 创建一个socket时,会返回一个socket文件描述符(socket fd),我们可以通过这个socket fd来进行网络的发送和接收操作。实际上,发送和接收数据就像对文件写入和读取数据一样操作。
  • 发送数据时,我们可以使用类似于写入文件的操作,使用write()函数将数据写入到socket fd中。
  • 接收数据时,我们可以使用类似于读取文件的操作,使用read()函数从socket fd中读取数据。
  • 此外,还可以使用其他文件操作函数,如open()、close()、select()等,对socket进行更复杂的操作。

API:

API 是 Application Programming Interface 的缩写,翻译为应用程序编程接口。是一组定义了不同软件组件之间,相互通信和交互的规范和工具集合。它允许不同的软件系统应用程序或服务之间进行数据传递、功能调用和交互操作。

那么socket是打开了一个文件吗:

  • 在Linux中,socket并不是打开一个文件,而是提供了一种抽象的接口,用于进行网络通信。
  • 尽管在编程上可以将socket看作是一个文件描述符,但实际上并没有打开一个物理文件。
  • 当我们调用socket()函数创建一个socket时,操作系统会为该socket分配资源,并返回一个文件描述符(socket fd)。
  • 这个文件描述符是一个整数值,用于标识该socket。

3.1 sockaddr结构:

Socket 是一种抽象层,提供了一种通用的应用程序编程接口(API),允许应用程序通过网络或本地主机之间进行通信。它可以用于不同协议的网络通信,包括 TCP、UDP 等。

除了在网络通信中使用外,它还可以用于同一台计算机上的应用程序之间的通信,例如进程间通信、线程间通信等。

那么一个接口干两件事,如何区分呢?

  • sockaddr,用来接收目标信息,这个值的参数可以是sockaddr_in/scokaddr_un/sockadd_in6之中的任意一个(需要强转指针)。

sockaddr是一个通用的地址结构体,它主要用于在网络编程中传递和表示套接字地址。在实际使用中,我们通常会使用sockaddr的具体派生结构体,例如sockaddr_in(IPv4)或sockaddr_in6(IPv6)或scokaddr_un,它们在sockaddr的基础上添加了特定的字段,以方便使用不同类型的套接字地址。
【Linux】UDP的服务端 + 客户端_第4张图片
sockaddr_in、sockaddr_un和sockaddr_in6sockaddr结构体的几个具体实现,用于在网络编程中表示不同类型的套接字地址:

  • 其中,sockaddr_in结构体用于表示IPv4地址,包括一个16位端口号和一个32位IP地址。该结构体 “继承” 自sockaddr结构体,并且增加了专门存储端口号和IP地址的字段。
  • sockaddr_un结构体用于表示UNIX域套接字的地址,包括UNIX域套接字的路径名。该结构体同样 “继承” 自sockaddr结构体,并且增加了存储路径名的字段。
  • sockaddr_in6结构体用于表示IPv6地址,该结构体也同样 “继承” 自sockaddr结构体,而其增加了专门存储IPv6地址和端口号的字段。
  • 在使用这些结构体时,可以根据需要将它们强制转换为sockaddr结构体使用,以便在函数调用中进行传递。

补充:

  • 相同起始成员: sockaddr结构体和这些特定结构体都有名为 “sa_family” 的成员变量,用于指示地址家族。这个成员在不同的特定结构体中具有相同的位置和作用。
  • 强制类型转换: 因为这些特定结构体的首部成员与sockaddr结构体的首部成员相同,并且只有首部成员是重要的,所以可以通过将特定结构体的指针强制转换为sockaddr结构体的指针,并传递给需要sockaddr结构体参数的函数。

3.2 配置sockaddr_in:

因为用的是ipv4的网络通信,所以这里需要初始化一个sockaddr_in类型的结构体:

// 绑定网络信息,指明ip + port
// 先填充基本信息到 struct sockaddr_in
struct sockaddr_in local;
bzero(&local, sizeof(local));// 清空操作

首先是把协议家族设置为IPV4,端口配置为代码所在函数参数中传的端口号:

// 填充协议家族,域
local.sin_family = AF_INET;
// 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
local.sin_port = htons(port_);

这个local.sin_family就是前16位,确定是本地通信还是网络通信,也可以用PF_INET是一样的。

然后配置IP:

local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());
  • 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制,4字节IP,uint32_t ip
  • INADDR_ANY(0):程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法。

3.3 inet_addr:

inet_addr()函数可以将一个点分十进制的IPv4地址转换为网络字节序,下的32位二进制整数,即4个字节的IP地址。

in_addr_t inet_addr(const char *cp);

因为对于网络来说并不认识字符串类型的ip,只认识网络字节流规定的ip。

IPV4地址是由四个十进制数组成(每个十进制数的数值是8位二进制数的数值),每个数组表示一个字节,范围从0~255,用点分十进制表示。

  • 例如,123.123.0.1是一个IPV4地址。
  • 每个区域都是8个比特位一字节的数据。

in_addr_t转到定义就是uint32_t
在这里插入图片描述

ip第四个字节,用的是位段来存储的:

// 示例
struct ip
{
    uint32_t part1:8;
    uint32_t part2:8;
    uint32_t part3:8;
    uint32_t part4:8;
}

3.4 inet_ntoa:

inet_ntoa()函数将网络请求中的IP地址转换为字符串类型,接受一个struct in_addr类型的参数,该类型表示一个IPv4地址。

从网络请求中获取到的IP地址转换为struct in_addr类型,然后再使用inet_ntoa函数将其转换为字符串类型。

char *inet_ntoa(struct in_addr in);

很多同学不知道struct in_addr是什么类型,我们不妨在vscode中点开struct sockaddr_in类型定义来看看:

【Linux】UDP的服务端 + 客户端_第5张图片
所以我们在传参时,只需要将struct sockaddr_in类的对象的成员传过去就好了。

返回值是个char*类型的,那么字符串在哪呢?

  • inet_ ntoa这个函数返回了一个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果。
  • 那么这块内存需要我们手动释放吗?答案是不需要!
  • 它会返回一个静态申请的buffer
  • man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区,这个时候不需要我们手动进行释放。
#include 
#include 
#include 

int main()
{
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    
    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);

    std::cout << "ptr1: " << ptr1 << " " << "ptr2: " << ptr2 << std::endl;

    return 0;
}

【Linux】UDP的服务端 + 客户端_第6张图片

在APUE中, 明确提出inet_ntoa不是线程安全的函数。但是在Centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。

3.5 bind绑定:

【Linux】UDP的服务端 + 客户端_第7张图片
该接口是指定socketsockaddr进行绑定,第三个参数是addr参数的大小。

// bind 网络信息 -- 将数据填入到操作系统里
if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
{
    logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
    exit(2);
}

在之前的初始化struct sockaddr_in时,我们提到过INADDR_ANY

  • INADDR_ANY(0):程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法。

【Linux】UDP的服务端 + 客户端_第8张图片
INADDR_ANY:转到定义上去看,我们发现它就是0。
在这里插入图片描述
关于端口号,不要绑定,0到1023以前的端口号,是服务器或者特定服务用的,一绑定可能就出错了:

【Linux】UDP的服务端 + 客户端_第9张图片
绑定完之后,我们的服务器就配置成功了!

小结一下:(个人理解)

  • sockaddr存储着套接字的信息,bindsockaddrsocket绑定起来,然后socket函数去处理套接字。

具体代码如下:

// udp服务器,只需要,1. 创建套接字 2. 填充信息之后做绑定,绑定完成之后就算完成
void init()
{
    // 1. 创建socket套接字
    sockfd_ = socket(AF_INET, SOCK_DGRAM, 0); // 就是打开了一个文件
    if (sockfd_ < 0)
    {
        logMessage(FATAL, "socket:%s:%d", strerror(errno), sockfd_);
        exit(1);
    }

    // 日志
    logMessage(DEBUG, "socket create success: %d", sockfd_);

    // 2. 绑定网络信息,指明ip + port
    // 2.1 先填充基本信息到 struct sockaddr_in

    struct sockaddr_in local;     // local在哪里开辟的空间? 用户栈,就是临时变量,我们要将其写入内核中
    bzero(&local, sizeof(local)); // 也可以用memset

    // 填充协议家族,域
    local.sin_family = AF_INET;   // 这个family就是前16位,确定是本地通信还是网络通信,也可以用PF_INET是一样的

    // 填充服务器对应的端口号信息,一定是会发给对方的,port_一定会到网络中
    local.sin_port = htons(port_);

    // 服务器都必须具有IP地址,"xx.yy.zz.aaa",字符串风格点分十进制 -> 4字节IP -> uint32_t ip
    // INADDR_ANY(0): 程序员不关心会bind到哪一个ip, 任意地址bind,强烈推荐的做法,所有服务器一般的做法
    // inet_addr: 指定填充确定的IP,特殊用途,或者测试时使用,除了做转化,还会自动给我们进行 h—>n (主机转网络)
    local.sin_addr.s_addr = ip_.empty() ? htonl(INADDR_ANY) : inet_addr(ip_.c_str());

    // 2.2 bind 网络信息 -- 将数据填入到操作系统里
    if (bind(sockfd_, (const struct sockaddr *)&local, sizeof(local)) == -1)
    {
        logMessage(FATAL, "bind: %s:%d", strerror(errno), sockfd_);
        exit(2);
    }

    logMessage(DEBUG, "socket bind success: %d", sockfd_);
}

4. 服务端start

4.1 recvfrom:

【Linux】UDP的服务端 + 客户端_第10张图片
recvfrom函数用于接收UDP协议的数据报,它从指定的文件描述符处读取数据,并将数据保存在指定的缓冲区buf中,同时将发送方的地址信息存储在addr参数所指向的结构体中。

返回值:

在这里插入图片描述

  • 如果接收成功,并且收到了数据,则返回接收到的数据的字节数。
  • 如果连接关闭,即对方套接字(socket)关闭连接,则返回0。
  • 如果发生错误,返回-1,并且可以使用errno变量获取具体的错误码。

start具体实现:

void start()
{
    char inbuffer[1024];  // 将来读取到的数据,都放在这里
    char outbuffer[1024]; // 将来发送的数据,都放在这里

    // 服务器设计的时候,服务器都是死循环
    while (true)
    {
        // 远端
        struct sockaddr_in peer;      // 输出型参数
        socklen_t len = sizeof(peer); // 输入输出型参数

        // demo2
        // UDP是无连接的
        // 对方给你发了消息,你想不想给对方回消息?
        // 要的!后面的两个参数是输出型参数,发消息的一方会将属性写到对应的peer和len当中
        // 不断地从网络当中进行数据读取:
        ssize_t s = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,
                             (struct sockaddr *)&peer, &len);

        // 数据已经读到了吧
        if (s > 0)
        {
            inbuffer[s] = 0; // 当做字符串
        }
        else if (s == -1)
        {
            logMessage(WARINING, "recvfrom: %s:%d", strerror(errno), sockfd_);
            continue;
        }

        // 谁发的消息,将对方的信息提取出来:
        // 读取成功的,除了读取到对方的数据,你还要读取到对方的网络地址[ip:port]
        std::string peerIp = inet_ntoa(peer.sin_addr);  // 拿到了对方的IP
        uint32_t peerPort = ntohs(peer.sin_port);       // 拿到了对方的port

        // 打印出来客户端给服务器发送过来的消息
        logMessage(NOTICE, "[%s:%d]# %s", peerIp.c_str(), peerPort, inbuffer);

        for (int i = 0; i < strlen(inbuffer); i++)
        {
            if(isalpha(inbuffer[i]) && islower(inbuffer[i]))
                outbuffer[i] = toupper(inbuffer[i]);
            else
                outbuffer[i] = toupper(inbuffer[i]);
        }

        // 谁给我发消息,立马转回去
        sendto(sockfd_, outbuffer, strlen(outbuffer), 0, (struct sockaddr*)&peer, len);
    }
}

4.2 sendto:

【Linux】UDP的服务端 + 客户端_第11张图片
sendto函数用于通过UDP协议发送数据报。它可以将指定的缓冲区中的数据发送到目标地址。

在我们上述start函数中,我们还实现了一个功能就是将收到的信息处理之后,再发回出去。客户端可以再用recvfrom接到消息,再显示出来。

服务类的成员变量和main函数:

// 使用手册
static void Usage(const std::string porc)
{
    std::cout << "Usage:\n\t" << porc << " port [ip]" << std::endl;
}

/// @brief  我们想写一个简单的udpSever
/// 云服务器有一些特殊情况:
/// 1. 禁止你bind云服务器上的任何确定IP, 只能使用INADDR_ANY,如果你是虚拟机,随意
class UdpServer
{
public:
    UdpServer(int port, std::string ip = "") : port_((uint16_t)port), ip_(ip), sockfd_(-1)
    {
    }
    ~UdpServer()
    {
    }
    
    // .........
    
private:
    // 服务器必须得有端口号信息
    uint16_t port_;
    // 服务器必须得有ip地址
    std::string ip_;
    // 服务器的socket fd信息
    int sockfd_;
    // onlineuser
    std::unordered_map<std::string, struct sockaddr_in> users;
};

// ./udpServer port [ip]
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3) // 反面:argc == 2 || argc == 3
    {
        Usage(argv[0]);
        exit(3);
    }

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

    if (argc == 3)
    {
        ip = argv[2];
    }

    UdpServer svr(port, ip);
    svr.init();
    svr.start();

    return 0;
}

5. 客户端

有了上述知识,客户端的实现就一马平川了。

struct sockaddr_in server;

static void Usage(std::string name)
{
    std::cout << "Usage:\n\t" << name << " server_ip server_port" << std::endl;
}

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

// ./udpClient server_ip server_port
// 如果一个客户端要连接server必须知道server对应的ip和port
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    // 1. 根据命令行,设置要访问的服务器IP
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);

    // 2. 创建客户端
    // 2.1 创建socket,服务器是udp的已经跑起来了,客户端也要想办法去连接服务器
    // 所以客户端也必须得有套接字信息
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd > 0);

    // 2.2 填写服务器对应的信息
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 创建一个线程就可以了
    // pthread_t t;
    // pthread_create(&t, nullptr, recverAndPrint, (void *)&sockfd);

    // 3. 通讯过程
    std::string buffer;
    while (true)
    {
        std::cerr << "Please Enter# ";
        std::getline(std::cin, buffer);

        // 发送消息给server:
        // 客户端首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
        sendto(sockfd, buffer.c_str(), buffer.size(), 0,\
               (const struct sockaddr *)&server, sizeof(server)); 

        // 发完消息之后再转发回去
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);

        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl; 
        }
    }

    close(sockfd);

    return 0;
}

客户端需不需要bind???是需要bind的,但是不需要用户自己bind,而是OS自动bind的!!!

  • 所谓的 “不需要” ,指的是:不需要用户自己bind端口信息!因为OS会自动给你绑定,你也最好这么做!
  • 如果我非要自己bind呢???可以!严重不推荐!!!
  • 所有的客户端软件,服务器在进行通信的时候,必须得有 client[ip:port] <-> server[ip:port]为什么呢??
    • 因为client很多,不能给客户端bind指定的port,port可能已经被别的client使用了。
    • 一旦被别的client使用了你的client就无法启动了,因为一个客户端只能被一个进程绑定。
    • 只能让操作系统随机生成端口号,用的时候拿去用,不用了就回收掉,下次客户端再来再把端口号给别的客户端。

那么server凭什么要bind呢??

  • server提供的服务,必须被所有人知道!server不能随便改变!
  • client端口号是多少一点都不重要,只需要保证唯一性就可以,因为没人连它。

在填写好服务端主机的信息之后,客户端直接就可以向服务端发送消息,main函数:

// ./udpServer port [ip]
int main(int argc, char *argv[])
{
    if (argc != 2 && argc != 3) // 反面:argc == 2 || argc == 3
    {
        Usage(argv[0]);
        exit(3);
    }

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

    if (argc == 3)
    {
        ip = argv[2];
    }

    UdpServer svr(port, ip);
    svr.init();
    svr.start();

    return 0;
}

6. 测试

在测试之前,我们先把日志实现一下:

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char *log_level[]={"DEBUG", "NOTICE", "WARINING", "FATAL"};

// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{
    assert(level >= DEBUG);
    assert(level <= FATAL);

    char *name = getenv("USER");

    char logInfo[1024];
    va_list ap; // ap就是一个char*类型
    va_start(ap, format);

    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    va_end(ap); // ap = NULL

    FILE *out = (level == FATAL) ? stderr:stdout;

    fprintf(out, "%s | %u | %s | %s\n", \
        log_level[level], \
        (unsigned int)time(nullptr),\
        name == nullptr ? "unknow":name,\
        logInfo);
}

127.0.0.1是IPv4地址中的本地回环地址。它通常被称为“localhost”,可用于测试计算机或网络设备的网络功能,以及运行并测试网络应用程序等。当计算机尝试连接到127.0.0.1时,它实际上是在尝试与自己本身通信,因此这个地址非常有用。

客户端发的消息,经过网络协议栈,不往网络里发,到了网络协议栈的最底部,再由最底部向上交付。再交付给另一个进程对应的缓冲区里面,让那个进程读到消息。

【Linux】UDP的服务端 + 客户端_第12张图片
加上服务端收到信息后,再将字符窜改为大写再转发回去:

在这里插入图片描述
我们可以创建一个多人聊天室,将所有人的信息(ip + port)都保存在unordered_map中,只要有用户连到主机上,就将其添加到哈希表中。

void checkOnlineUser(std::string &ip, uint32_t port, struct sockaddr_in &peer)
{
    std::string key = ip;
    key += ":";
    key += std::to_string(port);
    auto iter = users.find(key);
    if(iter == users.end())
    {
        users.insert({key, peer});
    }
    else
    {
        // iter->first, iter->second->
        // do nothing
    }
}

并且将收到的信息群发给所有的用户:

void messageRoute(std::string ip, uint32_t port, std::string info)
{
    std::string message = "[";
    message += ip;
    message += ":";
    message += std::to_string(port);
    message += "]# ";
    message += info;

    // 给每个在线用户都发回去
    for(auto &user : users)
    {
        sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), sizeof(user.second));
    }
}

UDP的sendto和recvfrom是阻塞式的,sendto会一直阻塞直到数据成功发送或者发生错误,所以我们要加多线程:

void *recverAndPrint(void *args)
{
    while (true)
    {
        int sockfd = *(int *)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
}

加了多线程之后,客户端有两个线程,主个线程在发消息, 获取用户输入,发消息;新线程在不断地收消息,并且打印到显示器上去。

为了让输出的内容更直观的显示出来,我们将客户端输出的内容写入到命名管道fifo中(注意:命名管道要现将读端打开,所以先要cat < fifo然后再在服务端发送消息)

【Linux】UDP的服务端 + 客户端_第13张图片
客户端的cout 本来是向显示器打印的,结果被重定向到了fifo命名管道中,重定向只是改变了输出流的目标,将输出内容发送到指定的管道文件中,但不会影响管道中已有的内容,所以才会出现上述情况(原有的信息依旧显示的情况)。

备注:

  • 在 shell 命令中,将输出重定向到 FIFO 命名管道时,先前存在于管道中的数据不会被删除或清空,而是会被保留
  • 与重定向到普通文件不同,FIFO 命名管道是一种特殊的文件类型,用于进程间通信。
  • 当写入进程写入数据时,读取进程可以从管道中读取数据。

当将命令的输出重定向到一个 FIFO 命名管道时,它会将输出写入到管道中,并且不会影响管道中现有的任何数据。


7. Windows客户端

#pragma warning(disable:4996)

#include 
#include 
#include 
#include 
#include 

#pragma comment(lib, "Ws2_32.lib")

int server_port = 8080;
std::string server_ip = "xxx.yyy.zz.mm";

int main()
{
    WSADATA data;
    (void)WSAStartup(MAKEWORD(2, 2), &data);
    (void)data;

    SOCKET sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    assert(sockfd > 0);

    // 2.2 填写服务器对应的信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof server);
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 3. 通讯过程
    std::string buffer;
    while (true)
    {
        std::cerr << "Please Enter# ";
        std::getline(std::cin, buffer);

        // 发送消息给server:
        // 客户端首次调用sendto函数的时候,我们的client会自动bind自己的ip和port
        sendto(sockfd, buffer.c_str(), buffer.size(), 0, 
            (const struct sockaddr*)&server, sizeof(server));

        char buffer[1024];
        struct sockaddr_in temp;
        int len = sizeof(temp);
        int s = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);

        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }
    
    closesocket(sockfd);
    WSACleanup();

    system("pause");

    return 0;
}

除了开头一些Windows需要的东西外,其他的与Linux下的代码一模一样,这样我们就可以在Windows端来访问部署在Linux下的服务了。

【Linux】UDP的服务端 + 客户端_第14张图片

你可能感兴趣的:(Linux,linux,udp)