【计算机网络】socket网络编程 --- 实现简易UDP网络程序之字符串回响

news2024/11/16 2:17:35

在这里插入图片描述

👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍


前言

在上篇博客中,我们简单认识了socket套接字的相关API等其它内容。这篇博客我将带领大家如何使用socket套接字,来从零实现一个简单的UDP网络程序 — 字符串回响。即接收到的数据内容要原样返回给客户端。里面会掺杂点格外知识,希望大家耐心看完 ~

说明:建议大家先看完上篇博客,再来学习本篇博客的相关知识。 👉点击跳转

目录

  • 前言
  • 一、封装服务端
      • 1.1 创建套接字
      • 1.2 绑定套接字
      • 1.3 启动服务端操作
      • 1.4 回响操作
  • 二、一个关于ip的问题 --- INADDR_ANY
  • 三、一个关于端口号的问题
  • 四、引入命令行参数
  • 五、编写客户端
  • 六、本地测试
  • 七、网络测试
  • 八、相关代码

一、封装服务端

1.1 创建套接字

我们把服务端封装成一个类,当我们定义出一个服务器对象后需要马上初始化服务器,而初始化服务器需要做的第一件事就是创建套接字

首先在Udpserver.cc内搭出大体的框架

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

using namespace std;

int main()
{
    // 创建udp服务器对象
    unique_ptr<UdpServer> svr(new UdpServer());

    // 初始化服务器
    svr->Init();

    // 启动服务器
    svr->Run();

    return 0;
}

创建套接字所需要用到的是socket函数

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);

说明:

  • domain:指定协议家族。常用的值有:

    • AF_INET:用于IPv4网络。注意:PF_INETAF_INET等价。
    • AF_INET6:用于IPv6网络。
    • AF_UNIX / AF_LOCAL:用于本地进程间通信。(这不是本章的重点)
  • type:指定套接字类型。常用的值有:

    • SOCK_STREAM:面向连接的套接字,通常用于TCP
    • SOCK_DGRAM:无连接的套接字,通常用于UDP。(dgrm有数据报的意思)
    • SOCK_RAW:原始套接字,用于访问更底层的协议。
  • protocol:指定协议。设置成0 表示使用默认协议。但该字段一般直接设置为0就可以了,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议。

  • 返回值:成功时返回一个套接字描述符(本质就是文件描述符)。失败时返回 -1,并设置 errno 以指示错误原因。

socket函数属于什么类型的接口?

在这里插入图片描述

网络协议栈是分层的,按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。

而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码,因此,我们调用的实际是下三层的接口。其中传输层和网络层是操作系统的一部分,并由内核负责管理,也就意味着我们在应用层调用的接口都叫做系统调用接口

说明:socket套接字通常用于传输层接口。尽管socket也可以在网络层(如原始套接字)进行操作,但大多数常见的应用程序使用的是传输层的接口,如TCPUDP

socket函数的返回值(回顾文件:点击跳转)

socket函数是被进程所调用的,当进程运行起来,操作系统就要为该进程创建PCB。当进程打开文件时,操作系统会在内核中创建数据结构来描述这个已打开的文件对象(和PCB类似)。这个数据结构通常被称为file或其他类似的名字,该结构体里包含比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*write*)在内核当中就是由struct file_operations结构体来维护的。

而进程可以打开多个文件,那进程PCB结构体对象就要存储哪些文件是由哪一个进程打开的。因此,每个进程PCB对象都要和打开的文件建立关系!所以进程PCB对象其实有一个指针struct files_struct* files,这个指针指向结构体files_struct,而这个结构体包含一个指针数组struct file* fd_array[],这个数组我们可以称之为文件描述符表。数组中的每个元素都是指向当前进程所打开文件的指针(地址)

所以,本质上文件描述符就是指针数组的下标(索引)。所以,只要拿着文件描述符,就可以找到对应的文件。其中数组中的012下标依次对应的就是标准输入、标准输出以及标准错误。


当调用socket函数创建套接字时,因为在Linux中一切皆文件,实际相当于我们打开了一个网卡文件。打开后在内核层面上就形成了一个对应的struct file结构体,同时该结构体被连入到了该进程对应的文件描述符表,并将该结构体的首地址填入到了fd_array数组当中下标为3的位置,此时fd_array数组中下标为3的指针就指向了这个打开的网络文件,最后3号文件描述符作为socket函数的返回值返回给了用户。(后面会向大家验证)

因此,未来我们用户想要对网卡文件进行网络通信(对网卡读写),就必须使用该函数的返回值。

对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的网络文件来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。

在这里插入图片描述

UdpServer.hpp

  • 因为我们要进行的是网络通信,创建套接字时我们第一个需要填入的参数就是AF_INETAF_INET6也是可以的。
  • 我们需要的服务类型就是SOCK_DGRAM,因为我们现在编写的UDP服务器
  • 第三个参数之间设置为0即可
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"

log lg;

enum
{
    SOCKET_ERR = 1
};

class UdpServer
{
public:
    UdpServer()
    	: _socketfd(-1)
    {}

    ~UdpServer()
    {
        if (_socketfd >= 0)
        {
            close(_socketfd);
        }
    }

    // 初始化UDP服务器
    void Init()
    {
		// 创建套接字
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socketfd < 0) // 套接字创建失败
        {
            lg.logmessage(Fatal, "socket create error, socketfd: %d", _socketfd);
            exit(SOCKET_ERR);
        }
        lg.logmessage(Info, "socket create success, socketfd: %d", _socketfd);
    }

private:
    int _socketfd; // 网络文件描述符
};

说明:

  • 以上代码我引入了往期博客写的一个日志小插件,方便打印出我们想要的信息。
  • 以上析构函数写不写都没什么问题。因为文件的生命周期通常与进程的生命周期相关联。在 Unix/Linux系统中,文件描述符(如用于网络套接字的文件描述符)在进程的生命周期内有效。一旦进程结束,这些文件描述符会自动被释放。

UdpServer.cc服务器源文件。这里我们可以做一个简单的测试,看看套接字是否创建成功。

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

using namespace std;

int main()
{
    // 创建udp服务器对象
    unique_ptr<UdpServer> svr(new UdpServer());

    // 初始化服务器
    svr->Init();
	
	// 运行服务器
	// svr->Run();

    return 0;
}

【程序结果】

在这里插入图片描述

运行程序后可以看到套接字是创建成功的,对应获取到的文件描述符就是3。这也很好理解,因为012默认被标准输入流、标准输出流和标准错误流占用了,此时最小的、未被利用的文件描述符就是3

1.2 绑定套接字

现在套接字已经创建成功了,但作为一款服务器来讲,如果只是把套接字创建好了,那我们也只是在系统层面上打开了一个文件,操作系统将来并不知道是要将数据写入到磁盘还是写到网卡,此时该文件还没有与网络关联起来。

而我们现在编写的是不面向连接的UDP服务器,所以创建完套接字后的第二件事就是绑定套接字即将套接字绑定到一个特定的IP地址和端口port上,让套接字能够在网络中接收数据

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

说明:

  • sockfd:套接字的文件描述符,通常是由 socket 函数返回的。

  • addrsockaddr结构体是一个套接字的通用结构体,实际我们在进行网络通信时,还是要定义sockaddr_insockaddr_un这样的结构体,只不过在传参时需要将该结构体的地址类型进行强转为sockaddr*

    • sockaddr_in结构体:用于跨网络通信(更详细细节下面说)
    • sockaddr_un结构体:是用于本地通信
    • 注意:使用以上结构体需要加上头文件<netinet/in.h>
  • addrlenaddr 结构体的大小,以字节为单位。

  • 返回值:成功返回 0;失败返回 -1,并设置errno以指示错误原因。

在这里插入图片描述

首先对于bind函数,第一个参数和第三个参数没的说。主要是第二个参数,我们可以先对struct sockaddr_in结构体进行一个清空初始化处理。可以先使用 bzero函数,它的作用是一个用于将内存区域的字节设置为零的函数。

#include <strings.h>

void bzero(void *s, size_t len);

当然你也可以使用memset来实现。memset函数的原型定义在<string.h>头文件中,其用法如下:

#include <string.h>
void *memset(void *s, int c, size_t len);
// s是指向要填充的内存区域的指针
// c是要填充的值
// len是要填充的字节数

// -------------------------
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr)); // 将 addr 结构体的所有字节设置为零

初始化之后,我们需要设置struct sockaddr_in成员变量,比如端口号和IP地址之类的。首先可以在vscode中查看sockaddr_in结构体的相关成员:

在这里插入图片描述

  • sin_family:表示协议家族。必须使用与socket创建时相同的协议家族。例如,如果你使用AF_INET 创建了套接字,bind时也应使用AF_INET的地址。
  • sin_port:表示端口号,是一个16位的整数。注意:端口号需要转换为网络字节序。因为只要进行网络通信,端口号一定是双方来回传输的数据,因此为了保证双方能够正常解析数据,需要将其转换为网络字节序。可以使用htons函数
  • sin_addr:其中sin_addr的类型是struct in_addr,实际该结构体当中就只有一个成员,该成员就是一个32位的整数,就是IP地址。

但是我们用户最直观的就是输入类似于192.168.1.1这种字符串形式的IP地址,可是这里sin_addr的类型是32位无符号整数,那么我们要把字符串转化为32位无符号整数

并且在网络通信中,IP地址和端口号一样,也是双方来回传输的数据,也要保证双方能够正常解析数据

因此,这里可以使用 函数inet_addr 将字符串形式的IP地址转换为32位的无符号整数,并且返回的整数已经是网络字节序。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

in_addr_t inet_addr(const char *cp);
  • 剩下的那一个字段不用管,这也就是为什么一开始我们要将结构体struct sockaddr_in的所有成员清空的原因。

注意:以上设置端口号和IP的行为都是在用户层面上的,最后我们还是需要使用bind函数,将指定的IP地址和端口号绑定到套接字上,最终由操作系统内核负责管理。这意味着内核会确保指定的地址和端口号与套接字关联,并在网络通信中处理数据的接收和发送。

补充Init接口

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <cstring>

log lg;

#define DEFAULT_IP "0.0.0.0" // 默认IP地址
#define DEFAULT_PORT 8080    // 默认端口号

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

class UdpServer
{
public:
    UdpServer(const std::string &ip = DEFAULT_IP, const uint16_t &port = DEFAULT_PORT)
        : _socketfd(-1), _ip(ip), _port(port)
    {
    }

    ~UdpServer()
    {
        if (_socketfd >= 0)
        {
            close(_socketfd);
        }
    }

    // 初始化UDP服务器
    void Init()
    {
		// 创建套接字
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socketfd < 0) // 套接字创建失败
        {
            lg.logmessage(Fatal, "socket create error, socketfd: %d", _socketfd);
            exit(SOCKET_ERR);
        }
        lg.logmessage(Info, "socket create success, socketfd: %d", _socketfd);

		// 绑定套接字
        struct sockaddr_in local;
        
        // 初始化struct sockaddr_in对象(清空)
        bzero(&local, sizeof(local));
        // 填充struct sockaddr_in对象字段
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);                  // 细节
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 细节
        
        // 绑定
        int n = bind(_socketfd, (const struct sockaddr *)&local, sizeof(local));
        if (n == -1) // 绑定失败
        {
            lg.logmessage(Fatal, "bind error, errno: %d, describe: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg.logmessage(Info, "bind success, errno: %d, describe: %s", errno, strerror(errno));
    }

private:
    int _socketfd;   // 网络文件描述符
    uint16_t _port;  // UDP服务器的端口号
    std::string _ip; // UDP服务器的IP地址
};

【运行结果】

在这里插入图片描述

1.3 启动服务端操作

UDP服务器的初始化就只需要创建套接字和绑定就行了,当服务器初始化完毕后我们就可以启动服务器了。

服务器实际上就是在周而复始的为我们提供某种服务,比方说你无论什么时候你都可以刷抖音。服务器之所以称为服务器,是因为服务器运行起来后就不会退出。因此,服务器实际执行的是一个死循环代码

注意:UDP服务器是不面向连接的,因此只要UDP服务器启动后,就可以直接接收客户端发来的数据。注意:服务器是被动的,只有数据发来才会做事。

recvfrom函数

recvfrom是一个用于接收数据的网络编程函数,通常用于接收来自网络套接字的数据。

#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len 
				, int flags, struct sockaddr *src_addr
				, socklen_t *addrlen);

说明:

  • sockfd: 套接字的文件描述符,通常由 socket 函数创建。
  • buf: 指向接收缓冲区的指针。数据将被读入这个缓冲区。
  • len: 缓冲区的大小,以字节为单位。
  • flags: 读取的方式。一般设置为0,表示阻塞读取。

收到消息后,服务器需要知道是谁发来的,为了后序是否做回响做准备。

  • src_addr: 发送方相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen: 调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。
  • 返回值:读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。
#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include "log.hpp"
#include <netinet/in.h>
#include <strings.h>
#include <arpa/inet.h>
#include <cstring>

log lg;

#define DEFAULT_IP "0.0.0.0" // 默认IP地址
#define DEFAULT_PORT 8080    // 默认端口号

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

class UdpServer
{
public:
    UdpServer(const std::string &ip = DEFAULT_IP, const uint16_t &port = DEFAULT_PORT)
        : _socketfd(-1), _ip(ip), _port(port), _isRun(false)
    {
    }

    ~UdpServer()
    {
        if (_socketfd >= 0)
        {
            close(_socketfd);
        }
    }

    // 初始化UDP服务器
    void Init()
    {
        // ...
    }

    // 运行服务端服务器
    void Run()
    {
        _isRun = true;
        char buffer[1024];
        while (true)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // -1是为了去掉'\n'
            ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1
            					, 0, (struct sockaddr *)&client, &len);

            if (n < 0) // 接收失败
            {
                // UDP是不可靠的,接收失败不是什么很大的问题,日志级别给个警告就好
                lg.logmessage(Warning, "recvfrom error, errno: %d, describe: %s", errno, strerror(errno));
                continue; // 继续运行即可
            }
            // 来到这,发送方的数据都在buffer里了
            buffer[n] = 0; // 将buffer当做字符串

            // 加工:服务端个性化回显
            std::string message = buffer;
            std::string echo_string = "server echo# " + message;
            std::cout << echo_string << std::endl;

            // 服务端发送回给客户端(未完成) ???
        }
    }

private:
    int _socketfd;   // 网络文件描述符
    uint16_t _port;  // UDP服务器的端口号
    std::string _ip; // UDP服务器的IP地址
    bool _isRun;     // 服务器是否在运行
};

1.4 回响操作

客户端发送数据后,希望确认服务器是否成功接收了这些数据。服务器的回响消息(回显)可以作为接收确认,告诉客户端数据已经到达。

sendto函数

#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags
			, const struct sockaddr *dest_addr, socklen_t addrlen);

说明:

  • sockfd: 套接字的文件描述符,通常由 socket 函数创建。
  • buf: 指向要发送数据的缓冲区的指针。
  • len: 要发送的数据长度,以字节为单位。
  • flags: 控制发送操作的标志。和recvfrom函数一样,填0即可。
  • dest_addr: 指向 sockaddr 结构体的指针,指定数据的目标地址(在无连接协议中,目标地址是必需的)。
  • addrlen: 目标地址结构的大小。通常设置为 dest_addr 对象的大小。
void Run()
{
   	// ....

    // 服务端发送回给客户端(未完成) 
    sendto(_socketfd, echo_string.c_str(), echo_string.size(), 0, (const sockaddr *)&client, len);  
}

二、一个关于ip的问题 — INADDR_ANY

客户端的代码我们已经写完了,那如何知道服务器已经运行情况呢?

我们可以通过以下命令

netstat -naup
  • -n: 显示数字格式的地址和端口号,而不是解析为主机名或服务名。
  • -a: 显示所有连接和监听的套接字。
  • -u: 显示UDP连接。
  • -p: 显示哪个进程在使用每个套接字

在这里插入图片描述

注意:以上能启动纯属巧合!!!

以上服务端使用的IP地址默认是0.0.0.0,端口号也是默认的8080。那如果我的端口号不变,将IP地址改为我的云服务器的IP地址175.178.46.38

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

using namespace std;

int main()
{
    // 创建udp服务器对象
    unique_ptr<UdpServer> svr(new UdpServer("175.178.46.38", 8080));

    // 初始化服务器
    svr->Init();

    // 启动服务器
    svr->Run();

    return 0;
}

【运行结果】

在这里插入图片描述

我们发现,绑定竟然失败?错误的原因是无法分配请求的地址?地址错误?为什么?

现在我来解释一下,如果你是虚拟机,运行以上代码就不会发生错误;如果你是云服务器,禁止直接绑定公网ip,那为什么不允许我们绑定呢?

如果你在服务器上绑定特定的公网IP地址,那么只有在这个公网IP地址上访问的请求才会被接受,也就是说,只有通过这个特定 IP 地址发出的请求会被接受,其他 IP 地址的请求会被忽略。但服务器一般是可以接受任意的IP地址的。比方说,任何一个人都可以访问抖音的服务器刷抖音。

因此,如果需要让任意IP地址访问,直接将IP绑定为0,即0.0.0.0。系统也提供宏INADDR_ANY,它对应的值就是0

绑定INADDR_ANY的好处

一台服务器底层可能会有多个网卡设备,此时这台服务器就可能会有多个IP地址,但一台服务器上比如端口号为8080的应用服务只有一个。

因此,这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据。如果这些数据也都想访问端口号为8081的服务。而此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据。

在这里插入图片描述

而如果服务端绑定的是INADDR_ANY,那么只要是发送给端口号为8080的服务的数据,系统都会可以将数据自底向上交给该服务端进行响应。

在这里插入图片描述

因此,服务端绑定INADDR_ANY这种方案也是强烈推荐的方案,所有的服务器具体在操作的时候用的也就是这种方案

只需修改Init函数即可

// 初始化UDP服务器
void Init()
{
    // 创建套接字
    // 略 ...
    
    // 绑定套接字
    struct sockaddr_in local;
    // 初始化struct sockaddr_in对象(清空)
    bzero(&local, sizeof(local));
    // 填充struct sockaddr_in对象字段
    local.sin_family = AF_INET;
    local.sin_port = htons(_port); // 细节
    // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 细节
    local.sin_addr.s_addr = INADDR_ANY; // 动态绑定INADDR_ANY
    // 绑定
    int n = bind(_socketfd, (const struct sockaddr *)&local, sizeof(local));
    if (n == -1) // 绑定失败
    {
        lg.logmessage(Fatal, "bind error, errno: %d, describe: %s", errno, strerror(errno));
        exit(BIND_ERR);
    }
    lg.logmessage(Info, "bind success, errno: %d, describe: %s", errno, strerror(errno));
}

【运行结果】

在这里插入图片描述

此时当我们再重新编译运行服务器时就不会绑定失败了,并且此时当我们再用netstat命令查看时会发现,该服务器的本地IP地址变成了0.0.0.0,这就意味着该UDP服务器可以在本地读取任何一张网卡里面的数据。

另外,服务器的IP地址也可以绑定成了127.0.0.1,这是本地环回地址,也称为localhost。它只能用于在同一个计算机上测试网络程序,确保数据在同一台机器上发送和接收,不经过网络接口。即只能进行本地通信。这个会在【本地测试】中为大家演示

三、一个关于端口号的问题

如果我将原本的端口号8080改为80又会是什么结果呢?

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

using namespace std;

int main()
{
    // 创建udp服务器对象
    unique_ptr<UdpServer> svr(new UdpServer("175.178.46.38", 80));

    // 初始化服务器
    svr->Init();

    // 启动服务器
    svr->Run();

    return 0;
}

在这里插入图片描述

我们发现还是绑定的问题,而且是权限问题?什么?绑定也有权限问题?

那我们提升权限来试试看

在这里插入图片描述

我们发现提升权限后就绑定成功了。

这里我想说的是,端口号范围[0, 1023]被称为知名端口,这些端口号是由互联网分配号码管理局IANA注册并分配给特定的应用层协议和服务的。常见的知名端口号包括:

  • HTTP:80
  • HTTPS:443
  • FTP:21
  • SSH:22
  • SMTP:25
  • ...

这些端口通常被操作系统和网络设备保留用于特定的服务或应用。系统和网络管理员通常避免在这些端口上运行其他服务,以避免与已知服务发生冲突。因此,我们绑定端口号建议选择[1024,正无穷)

四、引入命令行参数

未来服务端启动我们想以这样的形式./xxx 端口号

#include <iostream>
#include "UdpServer.hpp"
#include <memory>
#include <string>
using namespace std;

void Usage(string proc)
{
    cout << "\n\tUsage: " << proc << " 端口号1024+" << endl
         << endl;
}

int main(int argc, char *argv[])
{
    // 如果命令行参数的个数不是2,那么就要提示用户
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }

    uint16_t port = stoi(argv[1]);

    // 创建udp服务器对象
    unique_ptr<UdpServer> svr(new UdpServer("1.1.1.1", port));

    // 初始化服务器
    svr->Init();

    // 启动服务器
    svr->Run();

    return 0;
}

在这里插入图片描述

五、编写客户端

关于客户端的绑定bind问题

对于服务器来说,服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,这些绑定操作确保服务端能够在指定的网络接口和端口上监听和接收数据。具体来说:

  • IP 地址:动态绑定INADDR_ANY,即设置为0.0.0.0
  • 端口号:服务端必须绑定到一个特定的端口号,以便客户端能够通过该端口号连接到服务端。

客户端同样需要绑定IP地址和端口号,因为服务端也要能够正确地将数据回传给客户端。只不过客户端通常不需要我们显式地绑定IP地址和端口号。大多数情况下,客户端的操作系统会自动处理这一过程

  • IP地址:客户端一般不需要指定IP地址。操作系统会自动选择合适的本地网络接口IP地址来发起连接请求。

  • 端口号:客户端也不需要显式地绑定到一个特定的端口号。操作系统会为客户端应用程序随机分配一个可用的端口号,以便建立与服务端的连接。

那么操作系统为什么要自己处理呢?

如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端(进程)使用,即使这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。所以客户端的端口只要保证唯一性就行了,因此客户端端口可以动态的进行设置,并且客户端的端口号不需要我们来设置,当我们调用类似于sendto这样的接口时,操作系统会自动给当前客户端获取一个唯一的端口号。

客户端这里就不为大家进行封装了,直接一个Udpclient.cc文件,即能跑起来就行

注意:作为一个客户端,它必须知道它要访问的服务端的IP地址和端口号,我们这里使用命令行参数来实现就行了

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

void Usage(string proc)
{
    cout << "\n\tUsage: " << proc << " serverip serverport" << endl
         << endl;
}

int main(int argc, char *argv[])
{
    // 如果命令行参数的个数不是3,那么就要提示用户
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string server_ip = argv[1];           // 服务端IP地址
    uint16_t server_port = stoi(argv[2]); // 服务端端口号
	
	// 服务端IP地址和端口
    struct sockaddr_in server;
    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());
    socklen_t len = sizeof(server);

    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "client socket create error" << endl;
        return 1;
    }

    string message;
    char buffer[1024];
    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);
		
		// 发送消息给服务端
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
       	// 接收服务端的消息
        ssize_t s = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
        
        if (s > 0)
    	{
        	buffer[s] = 0;
        	cout << buffer << endl;
    	}
    }

    close(sockfd);
    return 0;
}

六、本地测试

在这里插入图片描述

注意:

  • 如果以上代码在你本地上试的时候没有回显,那么可能你就要打开你的云服务器的特定端口(开放端口行为),因为我们云服务器为了保证自己的安全,很多的端口默认都是禁掉的,不准任何一个人访问,包括你。
  • 我将服务器的IP地址绑定成了127.0.0.1,这是本地环回地址,也称为localhost。它用于在计算机上测试网络程序,确保数据在同一台机器上发送和接收,不经过网络接口。只能进行本地通信。

七、网络测试

我们可以将生成的客户端的可执行程序发送给你的其他朋友,进行网络级别的测试。注:在你朋友执行可执行程序之前,你需要先将服务端运行起来。还要保证你的朋友使用的端口号是你服务器上的端口号。

操作略…

八、相关代码

Gitee链接: 👉点击跳转

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2073600.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【北京仁爱堂】脖子歪斜,拉扯疼痛怎么办?规律的生活让痉挛性斜颈的恢复事半功倍!

痉挛性斜颈是一种肌张力障碍性疾病&#xff0c;也是一种让人非常痛苦不堪的疾病&#xff0c;他不仅影响患者的外貌&#xff0c;也会对患者的身体和心理造成双重的打击&#xff0c;严重影响正常的生活&#xff0c;社交和工作。 痉挛性斜颈的病因尚不明确&#xff0c;因为做任何仪…

Java Web —— 第八天(登录功能)

基础登录功能 LoginController 类 RestController //用于处理 HTTP 请求 Slf4j //记录日志 RequestMapping("/login") public class LoginController {Autowiredprivate EmpService empService;PostMappingpublic Result login(RequestBody Emp emp){log.info(&quo…

FFmpeg的入门实践系列六(编程入门之常见处理流程)

欢迎诸位来阅读在下的博文~ 在这里&#xff0c;在下会不定期发表一些浅薄的知识和经验&#xff0c;望诸位能与在下多多交流&#xff0c;共同努力 文章目录 前期博客参考书籍一、FFmpeg常见的处理流程复制编解码器的参数完整代码 二、创建并写入音视频文件三、总结附页 前期博客…

《黑神话:悟空》中的实景三维建模

这几天&#xff0c;国产游戏《黑神话:悟空》终于面世&#xff0c;迅速引爆了全球游戏市场。作为一款以《西游记》为背景的3A级动作角色扮演游戏&#xff0c;《黑神话:悟空》不仅在文化表达上极具吸引力&#xff0c;其背后的技术实力更是令人瞩目。本文将深入探讨&#xff0c;3A…

新“冰桶挑战”风靡奥运年,荣耀让科技有温度

巴黎奥运会落幕之后&#xff0c;残奥会即将在8月28日正式开赛&#xff0c;“超越自我&#xff0c;挑战极限”的拼搏精神仍在延续。 而挑战精神&#xff0c;不分场上或台下&#xff0c;存在于生活中的每个角落。 2024年不仅是奥运年&#xff0c;也是“冰桶挑战”的十周年&…

DMP调研(Data Management Platform-数据管理平台)

基础概念 数据资产 CRM、DMP、CDP 定义 Customer Data Platform&#xff08;CDP,客户数据平台&#xff09;&#xff1a;对于企业来说&#xff0c;CDP是作为全链路运营的核心数据系统。是汇集所有客户数据并将数据存储在统一的、可多部门访问的数据平台中&#xff0c;让企业各…

【Remi Pi使用HDMI屏幕显示QT界面】将QT工程在Ubuntu虚拟机交叉编译好拷贝到开发板并运行

开发板和Ubuntu虚拟机ssh连接 可以成功点亮hdmi屏幕&#xff08;默认是运行的mxapp2程序显示如下&#xff09; 可以在开发板上运行自己的qt程序界面 手动配置开发板以太网ip ip addr add 192.168.43.101/24 brd dev eth0 ip link set eth0 &#xff08;2&#xff09;虚拟机…

Vue3.0项目实战(一)——Vue3 大事件管理系统项目搭建

目录 1. 大事件项目介绍和创建 1.1 Vue3 大事件管理系统 1.2 pnpm 包管理器 - 创建项目 1.3 创建项目 2. Eslint 配置代码风格 2.1 环境同步 2.2 Eslint 配置代码风格 3. 基于 husky 的代码检查工作流 3.1 提交前做代码检查 3.2 暂存区 eslint 校验 3.3 总结 4. 目…

i.MX6裸机开发(9):CCM时钟控制模块

本章参考资料&#xff1a;《IMX6ULRM》&#xff08;参考手册&#xff09;。 学习本章时&#xff0c;配合《IMX6ULRM》第18章Clock Controller Module (CCM)&#xff0c;效果会更佳&#xff0c;特别是涉及到寄存器说明的部分。 本章我们主要讲解时钟部分&#xff0c;芯片内部的…

TensorFlow实现Softmax回归

原理 模型 相比线性回归&#xff0c;Softmax只多一个分类的操作&#xff0c;即预测结果由连续值变为离散值&#xff0c;为了实现这样的结果&#xff0c;我们可以使最后一层具有多个神经元&#xff0c;而输入不变&#xff0c;其结构如图所示&#xff1a; 为了实现分类&#xf…

AI编程简介

文章目录 AI 编程的特点常见编程工具copilot的工作原理AI编程常用技巧 AI 编程的特点 AI 编程是指利用人工智能技术来辅助开发过程的一种编程方式。包括但不限于&#xff1a;代码生成、优化、调试、审查&#xff0c;文档生成、测试自动化。 编程能力是大模型各项能力的天花板&…

可用于便携音箱的18V同步升压变换器TPS61288

在音频市场中,便携式音箱因其自带电源、方便携带深受消费者喜爱。便携式音箱通常配备2节可充电锂离子电池,当输出功率要求高于10W时,电池电压不足以为音频功放提供足够的功率,一般需要升压电路将电池电压升压至12V~18V以满足功率要求。 TPS61288是德州仪器公司推出的一款最…

力扣2025.分割数组的最多方案数

力扣2025.分割数组的最多方案数 哈希表 前缀和 用两个哈希表分别存元素(之后会遍历)左侧和右侧的前缀和 typedef long long LL;class Solution {public:int waysToPartition(vector<int>& nums, int k) {int n nums.size(),ans 0;vector<LL> sum(n);unor…

【Redis】Redis编程技巧

Redis编程技巧 一、StringVeiw是什么&#xff1f;二、OptionalString是什么&#xff1f;三、怎么看keys *1、vector配合back_inserter2、set配合inserter 四、chrono_literals技巧 一、StringVeiw是什么&#xff1f; 是一种轻量级的字符串视图类型&#xff0c;通常提供的是一种…

Mora:多智能体框架实现通用视频生成

人工智能咨询培训老师叶梓 转载标明出处 尽管已有一些模型能够生成视频&#xff0c;但大多数模型在生成超过10秒的长视频方面存在局限。Sora模型的出现标志着视频生成能力的一个新时代&#xff0c;它不仅能够根据文本提示生成长达一分钟的详细视频&#xff0c;而且在编辑、连接…

026集—CAD中多段线批量增加折点(相交点)——vba代码实现

当需要批量在多段线中加入顶点&#xff08;与多段线相交的点&#xff09;时&#xff0c;如下图所示&#xff1a;若干条线相交&#xff1a; 我们想在相交处增加折点&#xff0c;可通过vba插件一键完成。 &#xff08;使用方法命令行输入&#xff1a;vbaman,加载插件&#xff0c…

一本读懂数据库发展史的书

数据库及其存储技术&#xff0c;一直以来都是基础软件的主力。数据库系统的操作接口标准&#xff0c;也是应用型软件的重要接口&#xff0c;关系重大。 作为最“有感”的系统软件&#xff0c;数据库的历史悠久、品类繁多、创新活跃。 对数据库历史发展的介绍&#xff0c;有利…

JavaEE(1):web后端开发环境搭建和创建一个Servlet项目

web后端(javaEE)程序需要运行在服务器的&#xff0c;这样前端才可以访问得到 web后端开发&#xff1a; 服务器&#xff1f; 解释1&#xff1a;服务器就是一款软件&#xff0c;可以向其发送请求&#xff0c;服务器会作出一个响应。可以在服务器中部署文件&#xff0c;让他人访问…

MySQL必会知识精华2(了解基础篇)

我们的目标是&#xff1a;按照这一套资料学习下来&#xff0c;大家可以完成数据库增删改查的实际操作。轻松应对面试或者笔试题中MySQL相关题目 上篇文章我们先做一下MySQL学习的准备工作&#xff0c;如安装MySQL 服务&#xff0c;配置MySQL&#xff0c;连接MySQL。本篇文章注重…

大模型学习笔记 - LLM 之RAG

RAG RAG RAG SuveryRAG 简介RAG 范式的演变 1. 初级 RAG2. 高级 RAG3. 模块化的 RAG 介绍 RAG框架简述 检索技术文本生成增强技术简介 RAG 与 微调的区别RAG 模型评估解析RAG 研究的挑战与前景构建 RAG 系统的工具 在学习RAG中, 发现这个网站的内容特别好&#xff0c;也比较…