「网络编程」第二讲:网络编程socket套接字(二)_ 简单UDP网络通信程序的实现

news2024/9/24 11:24:55

「前言」文章是关于网络编程的socket套接字方面的,上一篇是网络编程socket套接字(一),下面开始讲解!

「归属专栏」网络编程

「笔者」枫叶先生(fy)

「座右铭」前行路上修真我

「枫叶先生有点文青病」

「每篇一句」

我认为,每个人都有一个觉醒期,但觉醒的早晚决定个人的命运。 
 ——路遥《平凡的世界》

目录

三、简单的UDP网络程序

3.1 服务端创建

3.1.1 创建套接字 

3.1.2 绑定端口

3.1.3 sockaddr_in结构体

3.1.4 字符串IP和整数IP说明

3.1.5 绑定好端口号的服务端代码

3.1.6 服务端代码

3.2  客户端创建

3.2.1 关于客户端的绑定问题 

3.2.2 客户端代码

3.3 服务端和客户端测试


三、简单的UDP网络程序

接下来进行编写socket套接字代码,先使用的是UDP,边写代码边讲一下socket的接口,还有一些原理

3.1 服务端创建

首先明确,这个简单的UDP网络程序分客户端和服务端,所以我们要生成两个可执行程序,一个是客户端的,另一个是服务端的,服务端充当的是服务器,暂时实现的功能是客户端和服务端简单进行通信,服务端要可以收到客户端发送给服务端的信息,目前就先简单实现这样的功能

下面进行编写服务端的代码

3.1.1 创建套接字 

先介绍创建套接字的函数socket

socket函数

socket函数的作用是创建套接字,TCP/UDP 均可使用该函数进行创建套接字,man 2 socket查看:

 create an endpoint for communication:创建通信端点,即创建通信的一端 

函数:socket

头文件:
        #include <sys/types.h>
        #include <sys/socket.h>

函数原型:
        int socket(int domain, int type, int protocol);

参数:
    第一个参数domain:创建套接字的域,即创建套接字的类型
    第二个参数type:创建套接字时提供的服务类型
    第三参数protocol:创建套接字的协议类别

返回值:
    套接字创建成功返回一个文件描述符,创建失败返回-1,错误码被设置

 下面介绍socket函数的参数

(1)socket函数的第一个参数是domain,用于创建套接字的类型,该参数就相当于 struct sockaddr结构体的前16位,即2字节

该domain参数的选项已经设置好了,我们直接选用即可。该参数的选项很多,我们常用的也就几个:

  • 如果要选择本地通信,则选择 AF_UNIX
  • 如果要选择网络通信,则选择 AF_INET(IPv4)或者 AF_INET6(IPv6)
  • "inet" 是Internet Protocol(IP)的简写

(2)socket函数的第二个参数是type,用于创建套接字时提供的服务类型

该参数的选项也是已经设置好了,我们直接选用即可。该参数的选项很多,我们常用的也就几个:

  • 如果是基于UDP的网络通信,我们采用的就是 SOCK_DGRAM,套接字数据报,提供的用户数据报服务(对应UDP的特点:面向数据报)
  • 如果是基于TCP的网络通信,我们采用的就是 SOCK_STREAM,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)
  • SOCK_DGRAM对应的英文:socket datagram
  • SOCK_STREAM对应的英文:socket stream
  • 至于第四个 SOCK_RAW 是原始套接字

(3)socket函数的第三个参数是protocol,用于创建套接字的协议类别。

  • 可以指明为TCP或UDP,但该字段一般直接设置为0就可以了。
  • 设置为0表示的就是默认,此时会根据传入的前两个参数自动推导出你最终需要使用的是哪种协议

socket函数返回值问题

套接字创建成功返回一个文件描述符,创建失败返回-1,同时错误码被设置

解释套接字创建成功返回一个文件描述符的问题

  • 当我们调用socket函数创建套接字时,实际相当于我们打开了一个“网络文件”,这个网络文件就是“网卡”
  • 文件描述符下标0、1、2依次被标准输入、标准输出以及标准错误占用,
  • 如果程序没有打开其他文件,当套接字创建成功时,文件描述符下标为3的指针就指向了这个打开的 “网络文件”
  • 我们读取、发送数据,就从这个 “网络文件” 进行读取和发送
  • 所以操作网络就像操作文件一般,这个“网络文件”就是一个缓冲区

明确一点

  • 按照TCP/IP四层模型来说,自顶向下依次是应用层、传输层、网络层和数据链路层。
  • 而我们现在所写的代码都叫做用户级代码,也就是说我们是在应用层编写代码
  • 因此我们调用的实际是下三层的接口,而传输层和网络层都是在操作系统内完成的,也就意味着我们在应用层调用的接口都叫做系统调用接口

 

服务端创建套接字编写代码暂时如下:

udpServer.hpp 

#pragma once

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
using namespace std;

// 错误类型枚举
enum
{
    SOCKET_ERR = 2
};

const static string defaultIp = "0.0.0.0";

class udpServer
{
public:
    udpServer(const uint16_t &port, const string ip = defaultIp)
        : _port(port), _ip(ip)
    {}

    // 初始化服务器
    void initServer()
    {
        // 1.创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
        // 2.绑定端口
    }

    // 运行服务器
    void start()
    {}

    ~udpServer()
    {}

private:
    uint16_t _port; // 端口号
    string _ip;     // ip地址
    int _socket;    // 文件描述符
};

 udpServer.cc

#include "udpServer.hpp"
#include <memory>

int main()
{
    std::unique_ptr<udpServer> usvr(new udpServer()); // TODO
    usvr->initServer();                               // 初始化服务器
    usvr->start();                                    // 运行服务器
    return 0;
}

下面进行绑定端口

3.1.2 绑定端口

 绑定端口的函数是bind函数

bind函数

 bind函数的作用是绑定端口号,TCP/UDP 均可使用进行该函数绑定端口,man 2 bind查看:

 bind a name to a socket:将名称绑定到套接字

函数:bind
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
         int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 
参数:
    第一个参数sockfd:文件描述符
    第二个参数addr:网络相关的属性信息
    第三参数addrlen:传入的addr结构体的长度
 
返回值:
    绑定成功返回0,绑定失败返回-1,同时错误码会被设置

  下面介绍bind函数的参数

(1)bind函数的第一个参数是sockfd,用于绑定套接字创建成功返回的文件描述符

(2)bind函数的第二个参数是addr,用于填充网络相关的属性信息,比如IP地址、端口号等

该参数addr的类型是:struct sockaddr *,也就是如图的结构体:

我们要做的工作就是:定义一个 sockaddr_in 的结构体,也就是上图的第二个结构体,然后对该结构体进行内容填充,填完就把给结构体传给 第二个参数addr,需要强制类型转换

3.1.3 sockaddr_in结构体

我们看一下  sockaddr_in 结构体的定义:

可以看到,sockaddr_in 有以下几个成员类型:

  • __SOCKADDR_COMMON (sin_):(sin_family)表示协议家族
  • sin_port:表示端口号,是一个16位的整数
  • sin_addr:表示IP地址,是一个32位的整数

剩下的字段不关注

其中 __SOCKADDR_COMMON 是一个宏

#define	__SOCKADDR_COMMON(sa_prefix) sa_family_t sa_prefix##family
  •  sa_prefix 代表外部 sin_ 传进来的参数
  • sa_prefix##family,##是进行拼接,意思是 sa_prefix 与 family 进行拼接,
  • sa_prefix就是 sin_,拼接之后就是 sin_family
  • sa_family_t 是16位整数

如图:

 其实就是这个 16位地址类型

sockaddr_in结构体的成员变量 sin_port 是端口号,类型是 in_port_t,16位的整数

sockaddr_in结构体的成员变量 sin_addr,sin_addr里面的内容是32位的整数。sin_addr 自己就是一个结构体,sin_addr 结构体类型是 in_addr

实际就是想说明 IP的类型直接就可以用 int 接收, 端口号需要用 uint16_t 接收

3.1.4 字符串IP和整数IP说明

  • 我们人一般使用的是字符串IP,也就是点分十进制的,比如:“123.2.33.200”,每一位的取值都是 0~255,这种的优点就是方便我们人观看使用,
  • 但是在网络传输中使用的是整数IP,用一个32位的整数来表示IP地址

为什么网络传输中使用的是整数IP??

  • 网络传输数据是寸土寸金的,在网络传输时直接以基于字符串的点分十进制IP的形式进行IP地址的传送,那么此时一个IP地址至少就需要15个字节,
  • 但实际并不需要耗费这么多字节,而整数IP地址只需要4个字节,即12字节。
  • 所以网络传输中使用的是整数IP

但是我们人看一串数字又不方便,比如:123002033200,所以我们人一般使用的是字符串IP

即存在需要把字符串IP转整数IP,整数IP转字符串IP

这些工作不用我们自己做,调用库函数即可

字符串IP和整数IP相互转换的方式

字符串IP转换成整数IP

 inet_addr函数

in_addr_t inet_addr(const char *cp);

只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP

函数做了两件工作:

  1. 字符串IP转换成整数IP
  2. 把整数IP转换成网络字节序

整数IP转换成字符串IP

inet_ntoa函数

char *inet_ntoa(struct in_addr in);

需要注意的是,传入 inet_ntoa函数的参数类型是 in_addr ,因此我们在传参时不需要选中 in_addr结构当中的32位的成员传入,直接传入in_addr 结构体即可 

这两个函数的头文件都是:

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

  

3.1.5 绑定好端口号的服务端代码

网络字节序与主机字节序之间的转换函数(上一节已经谈过)

#include <arpa/inet.h>
 
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

 服务端代码暂时如下:

udpServer.hpp  

#pragma once

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

// 错误类型枚举
enum
{
    UAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

const static string defaultIp = "0.0.0.0";

class udpServer
{
public:
    udpServer(const uint16_t &port, const string &ip = defaultIp)
        : _port(port), _ip(ip)
    {}

    // 初始化服务器
    void initServer()
    {
        // 1.创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
        // 2.绑定端口
        // 2.1 填充 sockaddr_in 结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));                   // 把 sockaddr_in结构体全部初始化为0
        local.sin_family = AF_INET;                     // 未来通信采用的是网络通信
        local.sin_port = htons(_port);                  // htons(_port)主机字节序转网络字节序
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
        // 2.2 绑定
        int n = bind(_socket, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
        if (n == -1)
        {
            cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
            exit(BIND_ERR);
        }
        //UDP server 预备工作完成
    }

    // 启动服务器
    void start()
    {
        // 服务器的本质就是一个死循环
        for(;;)
        {
            sleep(1);
        }
    }

    ~udpServer()
    {}

private:
    uint16_t _port; // 端口号
    string _ip;     // ip地址
    int _socket;    // 文件描述符
};

udpServer.cc

#include "udpServer.hpp"
#include <memory>

// 使用手册
// ./udpServer port ip
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << "local_ip local_port\n\n";
}

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

    uint16_t port = atoi(argv[2]); // string to int
    string ip = argv[1];
    std::unique_ptr<udpServer> usvr(new udpServer(port, ip));
    usvr->initServer(); // 初始化服务器
    usvr->start();      // 启动服务器

    return 0;
}

 暂时可以进行编译了

运行

需要按照手册使用,IP随便填一个,端口号暂时用8080, 端口号有讲究的,后面再说

报错:无法分配请求的地址,说明 IP 也不是可以乱填的

下面进行介绍几个IP 

ifconfig显示和配置网络接口的信息

 注:ifconfig全称:interface configuration接口配置

  • 第一个IP:inet 10.0.4.14,这个IP是内网IP
  • 第二个IP: inet 127.0.0.1,这个IP是本地环回,用于本地测试

注:"inet" 是Internet Protocol(IP)的简写

先说第二个 IP,什么是本地环回?? 

我们写的代码在应用层,使用该IP进行通信贯穿不了物理层,通信只在物理层以上进行环回,只能进行本主机通信。通常用这个 IP用于同一台计算机上运行客户端和服务器程序进行通信测试

内网IP到 IP协议再解释

我们暂时先使用本地环回,进行简单测试

服务端已经可以跑起来了

使用命令进行查看该服务端的信息

 netstat 命令

netstat是一个用于显示网络连接、路由表和网络接口信息的命令行工具

netstat:network statistics网络统计

常用选项:

  • -a:all (显示所有连接和监听端口)
  • -t:tcp (仅显示TCP连接)
  • -u:udp (仅显示UDP连接)
  • -n:numeric (以数字形式显示IP地址和端口号)
  • -p:program (显示与连接关联的进程信息)
  • -r:route (显示路由表信息)
  • -s:statistics (显示网络统计信息)

netstat -nuap 进行查看

Foreign Address:(外部地址)是指与本地计算机建立网络连接的远程计算机的IP地址和端口号,也就是客户端连服务器

0.0.0.0:* 表示任意IP地址、任意的端口号的程序都可以访问当前进程

netstat -uap 进行查看 (不以数字显示)

如果我们想让别人可以连到我们的服务端,服务端需要给全网提供服务,IP就要使用公网IP(连云服务器的那个IP)

注:云服务器是虚拟化的服务器,不能直接绑你的公网IP,如果是虚拟机或独立的Linux则可以

需要去到云服务器的控制台打开相应的UDP端口

依旧是绑定失败,所以云服务器是不支持的绑定公网IP的,使用虚拟机或者独立Linux操作系统,那个IP地址就是支持你绑定的

实际上,一款网络服务器,不建议指明绑定一个IP,上面的服务端指定绑定一个IP是错误的用法

比如你运行服务端的机器上有几个网卡,意味着你的服务端上有多个IP, 一台服务器上端口号为8080的服务只有一个。这台服务器在接收数据时,这里的多张网卡在底层实际都收到了数据,如果这些数据也都想访问端口号为8080的服务。此时如果服务端在绑定的时候是指明绑定的某一个IP地址,那么此时服务端在接收数据的时候就只能从绑定IP对应的网卡接收数据 

如果你只绑定指明的一个,其他IP收到的数据包就被丢弃的,这不是我们想要的

实际上,服务器绑定的IP是:INADDR_ANY,这是一个宏,代表 0.0.0.0,叫做任意地址绑定。绑定了该IP,只要是发送给端口号为8080的服务的数据,系统都会可以将数据自底向上全部交给该服务端

修改代码

udpServer.hpp   

#pragma once

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

// 错误类型枚举
enum
{
    UAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

const static string defaultIp = "0.0.0.0";

class udpServer
{
public:
    udpServer(const uint16_t &port, const string &ip = defaultIp)
        : _port(port), _ip(ip)
    {}

    // 初始化服务器
    void initServer()
    {
        // 1.创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
        // 2.绑定端口
        // 2.1 填充 sockaddr_in 结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));  // 把 sockaddr_in结构体全部初始化为0
        local.sin_family = AF_INET;    // 未来通信采用的是网络通信
        local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
        // 绑定IP方法1:INADDR_ANY
        // local.sin_addr.s_addr = INADDR_ANY;//服务器的真实写法
        // 绑定IP方法2:把外部的构造函数传参去掉,使用我们自己定义的string defaultIp = "0.0.0.0";
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
        // 2.2 绑定
        int n = bind(_socket, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
        if (n == -1)
        {
            cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
            exit(BIND_ERR);
        }
        // UDP server 预备工作完成
    }

    // 启动服务器
    void start()
    {
        // 服务器的本质就是一个死循环
        for (;;)
        {
            sleep(1);
        }
    }

    ~udpServer()
    {}

private:
    uint16_t _port; // 端口号
    string _ip;     // ip地址
    int _socket;    // 文件描述符
};

udpServer.cc

#include "udpServer.hpp"
#include <memory>

// 使用手册
// ./udpServer port
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << " local_port\n\n";
}

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

    uint16_t port = atoi(argv[1]); // string to int
    //不需要传IP了
    std::unique_ptr<udpServer> usvr(new udpServer(port));
    usvr->initServer(); // 初始化服务器
    usvr->start();      // 启动服务器
    
    return 0;
}

编译运行,端口号8080

netstat -nuap 进行查看

任意地址已经绑定成功,此时我们的服务器才能够被外网访问,意味着该UDP服务器可以在本地主机上,读取发送给端口8080的任何一张网卡里面的数据

3.1.6 服务端代码

接下来就是补充完整服务端的代码了。

服务端要接收客户端发送的消息,接收信息的函数是recvfrom

recvfrom函数

 recvfrom函数的作用是接收信息

receive a message from a socket:从套接字接收消息

函数: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:文件描述符,从哪个套接字去读数据
    第二个参数buf:代表读上来的数据放到哪个缓冲区里面
    第三参数len:缓冲区的长度
    第四个参数flags:读取方式,0代表阻塞式读取
    第五个参数src_addr:下面解释
    第六个参数addrlen:src_addr结构体的长度
 
返回值:
    成功返回接收到的字节数,失败返回-1,同时错误码会被设置。对等方执行有序关闭后,返回值将为0

socklen_t  是一个32位的无符号整数

recvfrom函数的第五个参数src_addr,src_addr是一个结构体,类型是 struct sockaddr *

第五个参数src_addr 和第六个参数addrlen 是一个输入输出型参数。

第五个参数src_addr用于返回发送数据一方的信息,比如IP、端口号等。就好比别人发消息给你,你得知道对方是谁

我们要做的也是定义一个 sockaddr_in 的结构体,初始化该结构体,把结构体传给第五个参数src_addr,需要强制类型转换

服务端代码如下

udpServer.hpp    

#pragma once

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

// 错误类型枚举
enum
{
    UAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

const static string defaultIp = "0.0.0.0";
const static int gnum = 1024;

class udpServer
{
public:
    udpServer(const uint16_t &port, const string &ip = defaultIp)
        : _port(port), _ip(ip)
    {}

    // 初始化服务器
    void initServer()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(SOCKET_ERR);
        }
        cout << "socket success: " << _sockfd << endl;
        // 2.绑定端口
        // 2.1 填充 sockaddr_in 结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));  // 把 sockaddr_in结构体全部初始化为0
        local.sin_family = AF_INET;    // 未来通信采用的是网络通信
        local.sin_port = htons(_port); // htons(_port)主机字节序转网络字节序
        // 绑定IP方法1:INADDR_ANY
        // local.sin_addr.s_addr = INADDR_ANY;//服务器的真实写法
        // 绑定IP方法2:把外部的构造函数传参去掉,使用我们自己定义的string defaultIp = "0.0.0.0";
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)
        // 2.2 绑定
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); // 需要强转,(struct sockaddr*)&local
        if (n == -1)
        {
            cerr << "bind error: " << errno << " : " << strerror(errno) << endl;
            exit(BIND_ERR);
        }
        // UDP server 预备工作完成
    }

    // 启动服务器
    void start()
    {
        // 服务器的本质就是一个死循环
        char buffer[gnum];
        for (;;)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 接收成功
            {
                buffer[s] = 0;
                // 发消息对方的IP
                string clientip = inet_ntoa(peer.sin_addr); // 直接传sin_addr结构体,整数IP 转 字符串IP(点分十进制IP)
                // 发消息对方的端口号
                uint16_t clientport = ntohs(peer.sin_port); // ntohs:网络字节序转主机字节序
                // 发送的消息
                string message = buffer;

                // 打印
                cout << clientip << "[" << clientport << "]" << "# " << message << endl;
            }
        }
    }

    ~udpServer()
    {}

private:
    uint16_t _port; // 端口号
    string _ip;     // ip地址
    int _sockfd;    // 文件描述符
};

 udpServer.cc 没有发生改变

3.2  客户端创建

客户端的功能是可以发送消息给服务端,目前就先简单实现这样的功能

3.2.1 关于客户端的绑定问题 

客户端在初始化时只需要创建套接字就行了,而不需要进行显示绑定操作 

客户端代码如下

udpClient.hpp 

class udpClient
{
public:
    udpClient(const string &serverip, const uint16_t serverport)
        : _ip(serverip), _port(serverport), _socket(-1)
    {}

    // 初始化客户端
    void initClient()
    {
        // 1.创建套接字
        _socket = socket(AF_INET, SOCK_DGRAM, 0);
        if (_socket == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(2);
        }
        // 2.绑定
        // 客户端必须要进行bind绑定,但是不需要我们自己bind,OS帮我们完成
    }

    // 启动客户端
    void run()
    {}
    
    ~udpClient()
    {}

private:
    uint16_t _port; // 端口号
    string _ip;     // ip地址
    int _socket;    // 文件描述符
};

udpClient.cc

#include "udpClient.hpp"
#include <memory>

// 使用手册
// ./udpClient ip port
static void Uage(string proc)
{
    cout << "\nUage:\n\t" << proc << " server_ip server_port\n\n";
}

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

    // 客户端需要服务端的 IP 和 port
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]); // string to int
    std::unique_ptr<udpClient> ucli(new udpClient(serverip, serverport));
    ucli->initClient(); // 初始化服务器
    ucli->run();        // 启动服务器

    return 0;
}

 明确一点

  • 客户端是需要服务端的IP和端口号的,没有这些客户端就连不上服务端
  • 也就是说服务端的 IP 和端口号是不能轻易改变的,否则用户端不知道就会连不上服务端
  • 所以现在我们写的需要手动服务端的IP和端口号

关于客户端的绑定问题  

  • 由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号
  • 只不过服务端需要进行端口号的绑定,而客户端不需要显示绑定端口号
  • 服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,并且端口号和IP不能轻易改变
  • 客户端在通信时虽然也需要端口号,但客户端一般是不进行显示绑定的,客户端访问服务端的时候,端口号只要是唯一就可以了
  • 如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。
  • 所以客户端的端口只要保证唯一性就行了,这个工作由OS完成,操作系统会自动给当前客户端生产一个唯一的端口号并且进行绑定
  • 也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要客户端的端口号没有被耗尽,客户端就永远可以启动

3.2.2 客户端代码

接下来就是补充完整客户端的代码了。

客户端要发送消息给服务端,发送消息的函数是sendto

sendto函数

 sendto函数的作用是发送消息

send a message on a socket:在套接字上发送消息

函数: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:文件描述符,从哪个套接字去发送消息
    第二个参数buf:待写入数据的存放位置
    第三参数len:写入数据的长度
    第四个参数flags:写入方式,0代表阻塞式写入
    第五个参数dest_addr:下面解释
    第六个参数addrlen:dest_addr结构体的长度
 
返回值:
    成功返回写入的字节数,失败返回-1,同时错误码会被设置

socklen_t  是一个32位的无符号整数 

第五个参数dest_addr和第六个参数addrlen 是一个输入型参数

第五个参数dest_addr用于发送客户端的IP、端口号数据,发给服务端

我们要做的工作也是定义一个 sockaddr_in 的结构体,然后对该结构体进行内容填充,填完就把给结构体传给第五个参数dest_addr,需要强制类型转换 

客户端代码 

udpClient.hpp 

#pragma once

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

class udpClient
{
public:
    udpClient(const string &serverip, const uint16_t serverport)
        : _serverip(serverip), _serverport(serverport), _sockfd(-1),  _quit(false)
    {}

    // 初始化客户端
    void initClient()
    {
        // 1.创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd == -1)
        {
            cerr << "socket error: " << errno << " : " << strerror(errno) << endl;
            exit(2);
        }
        cout << "socket success: " << _sockfd << endl;
        // 2.绑定
        // 客户端必须要进行bind绑定,但是不需要我们自己bind,OS帮我们完成
    }

    // 启动客户端
    void run()
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(_serverport);//主机转网络序列
        server.sin_addr.s_addr = inet_addr(_serverip.c_str());// 1.string类型转int类型 2.把int类型转换成网络字节序 (这两个工作inet_addr已完成)

        string message;
        while ((!_quit))
        {
           cout << "Please Enter# ";
           cin >> message;
           sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
        }
        
    }

    ~udpClient()
    {}

private:
    uint16_t _serverport; // 端口号
    string _serverip;     // ip地址
    int _sockfd;    // 文件描述符
    bool _quit;
};

注:后面全部改了一下_socket 的命名(_sockfd) 

udpClient.cc 没有发生改变

然后进行整体编译,编译没有问题

3.3 服务端和客户端测试

新建窗口,先运行服务端,再启动客户端,客户端先用本地环回进行测试,测试成功

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

关掉客户端,再次运行,发送消息给服务端,发现客户端的端口号已经发送改变,也就证明了操作系统会自动给当前客户端生产一个唯一的端口号并且进行绑定

网络测试 

可以将生成的客户端的可执行程序发送给你的其他朋友,进行网络测试,也就是跨主机通信

先使用 sz命令将该客户端可执行程序下载到本地

当你的朋友收到这个客户端的可执行程序后,可以通过 rz命令或拖拽的方式将这个可执行程序上传到他的云服务器上,然后通过 chmod命令给该文件加上可执行权限 

加可执行权限 chmod +x 文件名 

然后运行客户端,给服务端发送消息,客户端需要服务端的IP和端口号

简单的测试就成功了

注:云服务器的端口默认都是关闭的,需要手动打开,在控制台里面

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2023.6.18
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

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

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

相关文章

chatgpt赋能python:Python遍历文章的SEO指南

Python遍历文章的SEO指南 Python是一种高度灵活的编程语言&#xff0c;因其易于学习和使用而为许多程序员、数据科学家和SEO专业人士所青睐。在这篇文章中&#xff0c;我们将探讨Python如何遍历文章和对SEO优化的最佳实践。 什么是Python遍历文章&#xff1f; 遍历文章是指采…

【论文阅读】Segment Anything(SAM)——可分割一切的CV大模型

【前言】随着ChatGPT席卷自然语言处理&#xff0c;Facebook凭借着Segment Anything在CV圈也算扳回一城。迄今为止&#xff0c;github的star已经超过3万&#xff0c;火的可谓一塌糊涂。作为AI菜鸟&#xff0c;可不得自己爬到巨人肩膀上瞅一瞅~ 论文地址&#xff1a;https://arxi…

NDK编译C++源码生成Android平台so文件(opencv_android)

1.准备CPP文件 编写CMakeLists.txt 编写 mk文件 android-8 is unsupported. Using minimum supported version android-16 APP_PLATFORM android-16 is higher than android:minSdkVersion 1 in ./AndroidManifest.xml 修改Application.mk中的APP_PLATFORM为 android-21 builg…

59、基于51单片机多机 NRF24L01 无线温湿度 DHT11报警系统设计(程序+原理图+PCB源文件+参考论文+开题报告+任务书+元器件清单等)

摘 要 温湿度在工农业生产中占有很重要的地位&#xff0c;是工农业生产的重要组成数据。温湿度过高会造成粮食发霉长芽&#xff0c;还会引起大棚蔬菜一系列的病害。因此对其适时准确的测量就显得尤为重要。而一般的测量过程较为复杂繁琐&#xff0c;误差还大。比如现在所使…

MySQL-SQL存储过程/触发器详解(上)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

Python 循环与判断(详解)

❄️作者介绍&#xff1a;奇妙的大歪❄️ &#x1f380;个人名言&#xff1a;但行前路&#xff0c;不负韶华&#xff01;&#x1f380; &#x1f43d;个人简介&#xff1a;云计算网络运维专业人员&#x1f43d; 前言 在Python中&#xff0c;循环语句有两个&#xff0c;一个是fo…

Redis缓存穿透-击穿-雪崩详细分析加解决办法

Redis 缓存穿透 问题描述-如图 缓存穿透的原因 key 对应的数据在数据源并不存在&#xff0c;每次针对此key 的请求从缓存获取不到&#xff0c;请求都会压到数据源, 可能压垮数据源比如: 用一个不存在的用户id 获取用户信息&#xff0c;不论缓存还是数据库都没有&#xff0c;…

Spring Boot 集成 Redisson分布式锁(拿来即用版)

Redisson 是一种基于 Redis 的 Java 驻留集群的分布式对象和服务库&#xff0c;可以为我们提供丰富的分布式锁和线程安全集合的实现。在 Spring Boot 应用程序中使用 Redisson 可以方便地实现分布式应用程序的某些方面&#xff0c;例如分布式锁、分布式集合、分布式事件发布和订…

JSON5的作用、安装及使用

JSON5是对JSON的扩展&#xff0c;让人可以更容易手工编写和维护&#xff0c;用来减少一些JSON的限制&#xff0c;诸如json语法不支持注释&#xff0c;不支持字符串换行&#xff0c;所有的key都必须双引号&#xff0c;末尾不能有多余的逗号…等等&#xff0c;一大堆极其严格的要…

chatgpt赋能python:Python的退役与SEO

Python的退役与SEO 随着Python编程语言的流行和普及&#xff0c;越来越多的人开始使用它来开发各种类型的应用程序。但是&#xff0c;就像我们所知道的&#xff0c;所有技术都会发生变化&#xff0c;包括编程语言。因此&#xff0c;Python程式员可能会感到困惑和担忧&#xff…

JavaScript对象 (八):对象类型的使用、值类型和引用类型、函数的this指向、工厂方法创建对象、构造函数和类、new创建对象

1. 对象类型的使用 1.1 认识对象类型 基础数据类型可以存储一些简单的值&#xff0c;但是现实世界的事物抽象成程序时&#xff0c;往往比较复杂。 比如一个人&#xff0c;有自己的特性&#xff08;比如姓名、年龄、身高&#xff09;&#xff0c;有一些行为&#xff08;比如跑…

课程17:菜单管理功能实现

🚀前言 本文是《.Net Core从零学习搭建权限管理系统》教程专栏的课程(点击链接,跳转到专栏主页,欢迎订阅,持续更新…) 专栏介绍:以实战为线索,基于.Net 7 + REST + Vue、前后端分离,不依赖任何第三方框架,从零一步一步讲解权限管理系统搭建。 专栏适用于人群:We…

为什么运行时安全性重新流行起来?

容器通过以更高效和可扩展的方式创建、打包和部署应用程序&#xff0c;彻底改变了软件开发过程。 然而&#xff0c;能力越大&#xff0c;责任越大&#xff0c;对“左移安全性”的高度关注为那些在运行时忽视安全性的组织带来了风险。 通过对容器运行时安全采取多层次、全面的…

云服务器ECS_云主机_服务器托管_弹性计算-阿里云

阿里云服务器ECS&#xff08;Elastic Compute Service&#xff09;是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;阿里云提供多种云服务器ECS实例规格&#xff0c;如通用算力型u1、ECS计算型c7、通用型g7、GPU实例等&#xff0c;阿里云服务器网分享阿里云服务器ECS详细介…

Linux文件操作四剑客

目录 一、grep &#xff08;一&#xff09;作用 &#xff08;二&#xff09;格式 &#xff08;三&#xff09;选项 &#xff08;四&#xff09;案例 1、查看/etc目录下所有包含bash的文件名&#xff1a;grep -rl bash /etc 2、查看/var/log目录下所有包含error的文…

读发布!设计与部署稳定的分布式系统(第2版)笔记04_集成点

1. 第一个拥有10亿用户的网站 1.1. 2016年&#xff0c;Facebook宣布其每日活跃用户数量为11.3亿 1.2. 对整个应用程序来说&#xff0c;“五个9”的可靠性远远不够&#xff0c;这每天会让成千上万的用户失望 1.3. 假如按照六西格玛质量标准来衡量&#xff0c;那么Facebook每天…

LIN-网络管理:休眠(Go To Sleep)和唤醒(Wake up)

文章目录 一、LIN总线的两种状态二、休眠模式&#xff08;Go To Sleep&#xff09;①利用诊断帧中的主机请求帧 0x3C 作休眠命令②当总线静默(没有显性和隐性电平之间的切换)4s&#xff5e;10s 时&#xff0c;节点自动进入休眠状态。 三、唤醒模式&#xff08;Wake up&#xff…

智能图片降噪-Topaz Photo AI

今天给各位小伙伴们测试了一款可以使视频智能无损放大的软件——Topaz Photo AI。 小编在很早之前也有了解过Topaz系列的软件&#xff0c;都是通过人工智能处理的&#xff0c;对小白新手们很适用&#xff0c;由于使用人工智能方面的软件或程序对硬件要求都比较高&#xff0c;因…

交通指南系统

一、实验目的 1. 掌握图的基本存储方法&#xff1b; 2. 掌握有关图的操作算法并用高级语言实现&#xff1b; 3. 熟练掌握图的两种搜索路径的遍历方法。 二、实验内容 假设以一个带权有向图表示某一区域的公交线路网&#xff0c;图中顶点代表一些区域中的重要场所&#xff0…

设计模式(二十二):行为型之备忘录模式

设计模式系列文章 设计模式(一)&#xff1a;创建型之单例模式 设计模式(二、三)&#xff1a;创建型之工厂方法和抽象工厂模式 设计模式(四)&#xff1a;创建型之原型模式 设计模式(五)&#xff1a;创建型之建造者模式 设计模式(六)&#xff1a;结构型之代理模式 设计模式…