【Linux】UDP编程【下】{三版本服务器/编程常见问题}

服务器 0

文章目录

  • 3.linux网络涉及到的协议栈
  • 4.三个版本的服务器
    • 4.1响应式
    • 4.2命令式
    • 4.3交互式
      • 1.启动程序
      • 2.运行结果

3.linux网络涉及到的协议栈

Linux网络协议栈是一个复杂而强大的系统,它负责处理网络通信的各种细节。下面是对Linux网络协议栈的详细介绍:

套接字层(Socket Layer):

这是用户空间和内核空间之间的接口,提供了对底层网络通信的抽象。应用程序通过调用套接字API(如socket(), bind(), connect(), send(), recv()等)来与协议栈进行交互。
套接字层支持多种类型的套接字,如流式套接字(TCP)、数据报套接字(UDP)和原始套接字(直接访问网络层协议)。
传输层(Transport Layer):

传输层负责在源端和目的端之间建立可靠的或不可靠的数据传输。主要的传输层协议包括TCP(传输控制协议)和UDP(用户数据报协议)。
TCP提供面向连接的、可靠的、字节流的服务,通过序列号、确认和重传机制确保数据的完整性和顺序性。
UDP则提供无连接的、不可靠的数据报服务,不保证数据的顺序性和完整性,但具有较低的开销和较高的传输效率。
网络层(Network Layer):

网络层负责将数据包从源主机路由到目的主机。主要的网络层协议包括IP(互联网协议)和ICMP(互联网控制消息协议)。
IP协议负责在主机之间传输数据包,通过路由算法确定数据包的最佳路径。
ICMP协议用于在主机和路由器之间传递控制消息,如错误报告和路由查询。
数据链路层(Data Link Layer):

数据链路层负责在相邻节点之间传输数据帧。主要的协议包括以太网、PPP(点对点协议)等。
数据链路层还负责处理错误检测和流量控制等问题,确保数据帧在物理层上的可靠传输。
物理层(Physical Layer):

物理层负责将数据帧转换为电信号或光信号,以便在物理介质(如光纤、双绞线等)上进行传输。
物理层还负责处理信号的编码、解码和调制等问题,以确保数据的正确传输。
除了上述各层之外,Linux网络协议栈还包括一些辅助模块和子系统,如网络设备驱动程序、网络地址转换(NAT)、防火墙等,它们共同协作以实现复杂的网络通信功能。

在Linux内核中,网络协议栈的实现涉及大量的数据结构和算法,如套接字数据结构、路由表、缓冲区管理等。此外,Linux还提供了丰富的配置和调试工具,如ifconfig、netstat、tcpdump等,以帮助开发人员和运维人员更好地理解和控制网络行为。

总之,Linux网络协议栈是一个功能强大、灵活且可扩展的系统,它为用户提供了高效、可靠的网络通信服务。

协议栈和tcp/ip及osi模型有区别吗?

协议栈、TCP/IP模型以及OSI模型在网络通信中各自扮演着重要的角色,但它们之间确实存在一些关键的区别。

协议栈,又称协议堆叠,是计算机网络协议套件的一个具体的软件实现。每个协议模块通常都要和上下两个其他协议模块通信,可以想象成是协议栈中的层。协议栈反映了网络中文件传输的过程,并使得每个网络层能够处理特定的通信任务。

TCP/IP模型是一种网络通信协议模型,由传输控制协议(TCP)和互联网协议(IP)组成,是互联网通信的基础。它将网络通信分为四个层次:网络接口层、网络层、传输层和应用层。每一层都有其特定的功能和协议,共同协作以实现数据的可靠传输。

OSI(开放式系统互连)模型,即OSI参考模型,是ISO组织在1985年研究的网络互连模型。它定义了网络互连的七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。OSI模型提供了一个对网络通信的全面描述,并为不同厂商和开发者提供了一个统一的开发标准。

从区别上来看:

TCP/IP模型与OSI模型的主要区别在于层数不同以及第三层的协议支持不同。TCP/IP模型仅支持IP协议,而OSI模型支持所有网络层协议。此外,TCP/IP协议栈支持跨层封装,这在某些情况下可以加快收敛速度和提高效率。
协议栈与TCP/IP模型和OSI模型的关系在于,协议栈是实现这些模型的具体软件形式。也就是说,TCP/IP模型和OSI模型为网络通信提供了理论框架和分层结构,而协议栈则是这些模型在实际网络环境中的软件实现。
总的来说,协议栈、TCP/IP模型和OSI模型各有其特点和功能,它们共同构成了现代网络通信的基础。在实际应用中,这些模型和协议栈相互协作,以实现高效、可靠的网络通信。

在Linux下进行网络编程时,先调用socket,然后bind,然后调用其他接口就能进行服务器和客户端通信了。原理是什么?socket,bind调用完之后发生了什么?为什么这样就能通信?

在Linux下进行网络编程时,确实需要按照特定的顺序调用函数来设置和建立服务器与客户端之间的通信。这个过程涉及到底层的网络协议和操作系统的支持。下面我将解释socket和bind调用后发生的事情,以及为什么它们能够支持网络通信。

socket调用
当你调用socket函数时,你实际上是在请求操作系统为你创建一个新的套接字(socket)。这个套接字是一个数据结构,它包含了进行网络通信所需的所有信息,比如协议类型(TCP、UDP等)、本地和远程的地址等。

创建套接字后,操作系统会为你分配一个唯一的文件描述符,用于后续对套接字的操作。这个文件描述符就像一个句柄,你可以通过它来读写套接字上的数据。

bind调用
bind函数用于将套接字与本地的一个地址(IP地址和端口号)关联起来。对于服务器来说,这一步是必须的,因为服务器需要监听一个特定的端口来接收客户端的连接请求。

当你调用bind时,操作系统会检查你指定的地址是否可用,并将其与套接字关联起来。这样,当客户端发送连接请求或数据时,操作系统就能知道应该将请求或数据发送到哪个套接字上。

其他接口调用
在socket和bind之后,服务器通常会调用listen来开始监听连接请求。当有客户端连接时,服务器会调用accept来接受连接,并返回一个新的套接字用于与客户端的通信。

对于客户端来说,它会在socket之后调用connect来尝试连接到服务器。一旦连接建立成功,客户端和服务器就可以通过各自的套接字来发送和接收数据了。

原理与通信能力
之所以能够通信,是因为操作系统和网络协议栈在背后默默地做了大量的工作。当你调用这些网络编程接口时,你实际上是在与操作系统的网络协议栈进行交互。

网络协议栈:它负责实现各种网络协议(如TCP、UDP、IP等),并将你的数据按照协议格式进行封装和解封装。这样,数据就能在网络中正确地传输和识别。
路由与转发:操作系统还负责根据目标地址选择合适的路径来发送数据。这涉及到路由表的查询和数据的转发。
错误处理与重传:对于可靠的协议(如TCP),网络协议栈还负责处理各种错误情况,比如数据丢失或乱序。它会自动进行数据的重传和排序,以确保数据的完整性和顺序性。
因此,当你调用socket和bind等函数时,你实际上是在利用操作系统和网络协议栈的功能来建立和管理网络通信。这些函数为你提供了高层次的接口,使得你可以更加方便地进行网络编程,而不需要关心底层的细节。

udp的全双工是如何实现的?

UDP(User Datagram Protocol,用户数据报协议)是一个无连接的、不可靠的传输层协议,它提供了全双工通信的能力。全双工通信意味着数据可以在同一时刻双向流动,即发送方和接收方都可以同时发送和接收数据。UDP实现全双工通信主要依赖于以下几个关键点:

无连接性:UDP不需要在通信双方之间建立连接。因此,发送方和接收方都可以随时开始发送数据,无需等待对方的确认或建立连接的过程。这种无连接特性使得UDP非常适合于需要快速响应和低延迟的应用场景。

独立的数据报:UDP将每个数据报视为独立的单元,每个数据报都包含源端口号、目的端口号、数据长度和校验和等信息。这意味着每个数据报都可以独立地路由和传输,无需与其他数据报保持顺序或关联。因此,发送方和接收方可以同时发送和接收多个数据报,实现全双工通信。

异步传输:UDP的异步传输特性意味着数据的发送和接收是异步进行的,即发送方发送数据后不需要等待接收方的确认或响应,可以继续发送其他数据。同样地,接收方也可以随时接收数据,无需等待发送方的进一步指示。这种异步性使得UDP能够支持并发通信和实时交互。

需要注意的是,虽然UDP提供了全双工通信的能力,但它并不保证数据的可靠传输。由于UDP是无连接的,它不提供流量控制、数据排序或重传机制。因此,在使用UDP进行通信时,应用程序需要自行处理这些问题,以确保数据的完整性和正确性。

总之,UDP通过其无连接性、独立的数据报和异步传输特性实现了全双工通信。这使得UDP成为许多需要快速响应和低延迟的应用场景的首选传输层协议。然而,在使用UDP时,应用程序需要仔细考虑如何处理数据的可靠性和完整性。

回顾静态成员函数

静态成员函数是C++中的一个重要特性,它们与普通的成员函数有一些显著的差异。静态成员函数属于类本身,而不是类的实例,这意味着它们可以在没有创建类的对象的情况下被调用。静态成员函数只能访问静态成员变量和其他静态成员函数,不能访问类的非静态成员。

静态成员函数的应用场景、目的和意义主要包括以下几个方面:

应用场景:

工具函数:当某些函数仅与类有关,但并不涉及类的具体实例时,可以将其设计为静态成员函数。例如,一个表示数学运算的类可能有一个静态成员函数来计算两个数的和,这个函数并不依赖于类的任何特定实例。
访问静态成员:静态成员函数常常用于访问和修改类的静态成员变量。由于静态成员变量不属于任何特定的对象实例,因此需要通过静态成员函数进行访问。
工厂方法:在设计模式中,静态成员函数经常用作工厂方法,用于创建类的实例。这种方法允许我们控制对象的创建过程,例如实现单例模式或实现对象的池化。
目的:

减少耦合:通过将某些功能设计为静态成员函数,可以减少类与其使用者之间的耦合,使得类的使用更加灵活和独立。
提供类级别的操作:静态成员函数允许我们在不创建类实例的情况下执行某些操作,这对于那些只需要在类级别执行的操作非常有用。
意义:

提高代码复用性:静态成员函数可以被类的所有实例共享,提高了代码的复用性。
简化代码设计:通过将某些功能设计为静态成员函数,可以简化类的设计,使得类的职责更加清晰和明确。
增强代码可读性:通过合理使用静态成员函数,可以使代码的结构更加清晰,提高代码的可读性。
总的来说,静态成员函数是C++中一种强大的工具,它们允许我们在类级别执行操作,提高代码的复用性和可读性,并简化代码设计。然而,我们也需要注意合理使用静态成员函数,避免过度使用导致代码结构混乱或增加不必要的复杂性。

4.三个版本的服务器

4.1响应式

#ifndef _UDP_SERVER_HPP#define _UDP_SERVER_HPP#include "Log.hpp"#include <iostream>#include <unordered_map>#include <cstdio>#include <string>#include <cerrno>#include <cstring>#include <cstdlib>#include <strings.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <queue>#define SIZE 1024class UdpServer{public:    // 这里填空串 下面三目运算分配INADDR_ANY;填指定串 server以指定串作为ip    UdpServer(uint16_t port, std::string ip = "127.0.0.1")        : _ip(ip),          _port(port),          _socketFd(-1)    {    }    bool initServer()    {        // 新系统调用完成网络功能        // 1. socket创建套接字 协议用IPv4 套接字类型用数据报套接字        _socketFd = socket(AF_INET, SOCK_DGRAM, 0); // #define AF_INET PF_INET 二者相同        if (_socketFd < 0)        {            logMsg(FATAL, "socket::%d:%s", errno, strerror(errno));            exit(2); // 正规写法:规定每个退出码代表什么意思        }        // 2. bind绑定端口号 将一个套接字绑定到一个特定的ip和port        struct sockaddr_in svr_sockAddr;        bzero(&svr_sockAddr, sizeof(svr_sockAddr));        svr_sockAddr.sin_family = AF_INET;        // #define INADDR_ANY ((in_addr_t) 0x00000000) 字符串--32位整数--网络字节序        svr_sockAddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());        svr_sockAddr.sin_port = htons(_port); //主机字节序--网络字节序        // int bind(int __fd, const sockaddr *__addr, socklen_t __len)        if (bind(_socketFd, (struct sockaddr *)&svr_sockAddr, sizeof(svr_sockAddr)) < 0)        {            logMsg(FATAL, "bind::%d:%s", errno, strerror(errno));            exit(2);        }        logMsg(NORMAL, "init udp server done ... %s", strerror(errno));        return true;    }    void Start()    {        // 响应式服务器:原封地不动返回client发送的消息        char buffer[SIZE];        while (true)        {            // clt_sockAddr 纯输出型参数            struct sockaddr_in clt_sockAddr;            bzero(&clt_sockAddr, sizeof(clt_sockAddr));            // clint_addrlen: 输入输出型参数            socklen_t clint_addrlen = sizeof(clt_sockAddr); // unsigned int            // 读取数据            ssize_t bytes_read = recvfrom(_socketFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&clt_sockAddr, &clint_addrlen);            if (bytes_read == -1)            {                logMsg(FATAL, "recvfrom::%d:%s", errno, strerror(errno));                exit(3);            }            // 谁 发送的 什么信息            if (bytes_read > 0)            {                buffer[bytes_read] = 0; // 数据当做字符串使用                uint16_t client_port = ntohs(clt_sockAddr.sin_port);   // 网络字节序 --> 主机字节序                std::string cli_ip = inet_ntoa(clt_sockAddr.sin_addr); // 4字节的网络序列的IP->本主机字符串风格的IP                printf("[%s:%d]# %s/n", cli_ip.c_str(), client_port, buffer);            }            // 回显数据            sendto(_socketFd, buffer, strlen(buffer), 0, (struct sockaddr *)&clt_sockAddr, clint_addrlen);        }    }    ~UdpServer()    {        if (_socketFd >= 0)            close(_socketFd);    }private:    // ip和port    std::string _ip;    uint16_t _port;    int _socketFd;};#endif

4.2命令式

#ifndef _UDP_SERVER_HPP#define _UDP_SERVER_HPP#include "Log.hpp"#include <iostream>#include <unordered_map>#include <cstdio>#include <string>#include <cerrno>#include <cstring>#include <cstdlib>#include <strings.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <queue>#define SIZE 1024class UdpServer{public:    UdpServer(uint16_t port, std::string ip = "127.0.0.1")        : _ip(ip),          _port(port),          _socketFd(-1)    {    }    bool initServer()    {        // 新系统调用完成网络功能        // 1. socket创建套接字 协议用IPv4 套接字类型用数据报套接字        _socketFd = socket(AF_INET, SOCK_DGRAM, 0); // #define AF_INET PF_INET 二者相同        if (_socketFd < 0)        {            logMsg(FATAL, "socket::%d:%s", errno, strerror(errno));            exit(2); // 正规写法:规定每个退出码代表什么意思        }        // 2. bind绑定端口号 将一个套接字绑定到一个特定的地址和端口        struct sockaddr_in svr_sockAddr;        bzero(&svr_sockAddr, sizeof(svr_sockAddr));        svr_sockAddr.sin_family = AF_INET;        svr_sockAddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());        svr_sockAddr.sin_port = htons(_port);        // int bind(int __fd, const sockaddr *__addr, socklen_t __len)        if (bind(_socketFd, (struct sockaddr *)&svr_sockAddr, sizeof(svr_sockAddr)) < 0)        {            logMsg(FATAL, "bind::%d:%s", errno, strerror(errno));            exit(2);        }        logMsg(NORMAL, "init udp server done ... %s", strerror(errno));        return true;    }    void Start()    {        char cmdBuf[SIZE];        while (true)        {            // client_addr 纯输出型参数            struct sockaddr_in client_addr;            bzero(&client_addr, sizeof(client_addr));            // clint_addrlen: 输入输出型参数            socklen_t clint_addrlen = sizeof(client_addr); // unsigned int            // 读取数据            ssize_t bytes_read = recvfrom(_socketFd, cmdBuf, sizeof(cmdBuf) - 1, 0, (struct sockaddr *)&client_addr, &clint_addrlen);            if (bytes_read == -1)            {                logMsg(FATAL, "recvfrom::%d:%s", errno, strerror(errno));                exit(3);            }            // 谁 发送的 什么信息            char partMsg[256];            std::string cmdOutput;            if (bytes_read > 0)            {                cmdBuf[bytes_read] = 0; // 数据当做字符串使用                // 不允许客户端执行rm命令                if (strcasestr(cmdBuf, "rm") != nullptr || strcasestr(cmdBuf, "rmdir") != nullptr)                {                    std::string err_msg = "大坏蛋!不准删除!";                    std::cout << "client send:" << cmdBuf << " but is blocked!" << std::endl;                    sendto(_socketFd, err_msg.c_str(), err_msg.size(), 0, (struct sockaddr *)&client_addr, clint_addrlen);                    continue;                }                FILE *fp = popen(cmdBuf, "r");                if (nullptr == fp)                {                    logMsg(ERROR, "popen:%d:%s", errno, strerror(errno));                    continue;                }                //popen把cmdBuf的命令执行的结果存入到fp文件中                while (fgets(partMsg, sizeof(partMsg), fp) != nullptr)                {                    cmdOutput += partMsg;                }                fclose(fp);                sendto(_socketFd, cmdOutput.c_str(), cmdOutput.size(), 0, (struct sockaddr *)&client_addr, clint_addrlen);            }        }    }    ~UdpServer()    {        if (_socketFd >= 0)            close(_socketFd);    }private:    // ip和port    std::string _ip;    uint16_t _port;    int _socketFd;};#endif

4.3交互式

#ifndef _UDP_SERVER_HPP#define _UDP_SERVER_HPP#include "Log.hpp"#include <iostream>#include <unordered_map>#include <cstdio>#include <string>#include <cerrno>#include <cstring>#include <cstdlib>#include <strings.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <unistd.h>#include <queue>#define SIZE 1024class UdpServer{public:    UdpServer(uint16_t port, std::string ip = "")        : _ip(ip),          _port(port),          _socketFd(-1)    {    }    bool initServer()    {        // 新系统调用完成网络功能        // 1. socket创建套接字 协议用IPv4 套接字类型用数据报套接字        _socketFd = socket(AF_INET, SOCK_DGRAM, 0); // #define AF_INET PF_INET 二者相同        if (_socketFd < 0)        {            logMsg(FATAL, "%d:%s", errno, strerror(errno));            exit(2); // 正规写法:规定每个退出码代表什么意思        }        // 2. bind绑定端口号 将一个套接字绑定到一个特定的地址和端口        // 将用户设置的ip和port在内核中和我们当前的进程强关联        struct sockaddr_in svr_socketAddr;        bzero(&svr_socketAddr, sizeof(svr_socketAddr));        svr_socketAddr.sin_family = AF_INET;        svr_socketAddr.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());        svr_socketAddr.sin_port = htons(_port);        // int bind(int __fd, const sockaddr *__addr, socklen_t __len)        if (bind(_socketFd, (struct sockaddr *)&svr_socketAddr, sizeof(svr_socketAddr)) < 0)        {            logMsg(FATAL, "%d:%s", errno, strerror(errno));            exit(2);        }        logMsg(NORMAL, "init udp server done ... %s", strerror(errno));        return true;    }    void Start()    {        char buffer[SIZE];        while (true)        {            // client_addr 纯输出型参数            struct sockaddr_in client_addr;            bzero(&client_addr, sizeof(client_addr));            // clint_addrlen: 输入输出型参数            socklen_t clint_addrlen = sizeof(client_addr); // unsigned int            // 读取数据            ssize_t bytes_read = recvfrom(_socketFd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&client_addr, &clint_addrlen);            if (bytes_read == -1)            {                logMsg(FATAL, "%d:%s", errno, strerror(errno));                exit(3);            }            //一旦收到新用户的信息 就把新用户记录下来            char client[64];            if (bytes_read > 0)            {                buffer[bytes_read] = 0; // 数据当做字符串使用                std::string cli_ip = inet_ntoa(client_addr.sin_addr); // 4字节的网络序列的IP->本主机字符串风格的IP                uint16_t client_port = ntohs(client_addr.sin_port);   // 网络字节序 --> 主机字节序                snprintf(client, sizeof(client), "%s-%u", cli_ip.c_str(), client_port); // 127.0.0.1-8080                logMsg(NORMAL, "client: %s", client);                std::string client_str = client;                auto it = _clients.find(client_str);                if (it == _clients.end())                {                    logMsg(NORMAL, "add new client : %s", client);                    _clients.insert({client_str, client_addr});                }            }                        //svr把一个客户端发送过来的信息发送给了他所记录的所有的client            for (auto &iter : _clients)            {                std::string sendMsg = client;                sendMsg += "# ";                sendMsg += buffer; // 127.0.0.1-8080# 你好                logMsg(NORMAL, "push message to %s", iter.first.c_str());                sendto(_socketFd, sendMsg.c_str(), sendMsg.size(), 0, (struct sockaddr *)&(iter.second), sizeof(iter.second));            }        }    }    ~UdpServer()    {        if (_socketFd >= 0)            close(_socketFd);    }private:    // ip和port    std::string _ip;    uint16_t _port;    int _socketFd;    std::unordered_map<std::string, struct sockaddr_in> _clients;    /*for test: PC模型    多线程服务端: 解耦服务端 -- PC模型    A线程只读数据且把数据推送到队列    B线程只取数据且把数据发给客户端        std::queue<std::string> msgQueue;    如果消息是任务 还可以把任务交给线程池处理    */};#endif

1.启动程序

当你执行以下命令序列时:

./server 0.0.0.0 8080  ./client 127.0.0.1 8080  ./client 127.0.0.1 8080

会发生以下事情:

服务器启动:
./server 0.0.0.0 8080 命令启动了一个服务器程序,该服务器绑定到所有可用的网络接口上的8080端口(由于使用了0.0.0.0地址)。服务器现在准备接收发送到8080端口的UDP数据包。

第一个客户端启动:
./client 127.0.0.1 8080 命令启动了第一个客户端程序。该客户端配置为向本地机器上的8080端口发送UDP数据包,目标地址是127.0.0.1(回环地址)。客户端发送的数据将被路由到同一台机器上运行的服务器。

第二个客户端启动:
几乎同时,./client 127.0.0.1 8080 命令又启动了第二个客户端程序。这个客户端与第一个客户端完全相同,它也向本地机器上的8080端口发送UDP数据包。

现在,服务器上有一个监听在8080端口的套接字,准备接收数据。两个客户端都在尝试向这个端口发送数据。由于UDP是无连接的,所以两个客户端可以独立地、同时地或几乎同时地向服务器发送数据。

服务器将分别接收来自两个客户端的数据包,并根据其程序逻辑处理这些数据。因为UDP是无状态的,所以服务器不会知道或关心数据是从一个客户端还是两个客户端发送的;它只处理到达其套接字的数据包。

需要注意的是,UDP不保证数据包的顺序、可靠性或重复检测,所以可能会有数据包丢失、乱序或重复到达的情况。此外,如果服务器没有设计为并发处理多个客户端的请求,它可能会在处理一个客户端的数据时阻塞另一个客户端的数据,这取决于服务器的实现细节。

在实际应用中,你通常需要确保服务器能够处理来自多个客户端的并发请求,以避免性能瓶颈或数据丢失。此外,如果数据包的内容需要按照特定的顺序处理,或者需要确保数据的可靠性,那么你可能需要考虑使用TCP而不是UDP。

2.运行结果

在这里插入图片描述

也许您对下面的内容还感兴趣: