UDP服务器实现

news2024/9/25 9:32:20

目录

一、服务端创建

1.1、创建套接字

1.2、端口绑定

1.3、sockaddr_in结构体

1.4、字符串IP和整数IP说明

1.5、绑定好端口号的服务端代码

1.6、服务端代码

二、客户端创建

2.1、关于客户端的绑定问题 

2.2、客户端代码


一、服务端创建

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

1.1、创建套接字

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

  man 2 socket查看:

函数:socket
 
头文件:
        #include <sys/types.h>
        #include <sys/socket.h>
 
函数原型:
        int socket(int domain, int type, int protocol);
 
参数:
    第一个参数domain:创建套接字的域,即创建套接字的类型
    第二个参数type:创建套接字时提供的服务类型
    第三参数protocol:创建套接字的协议类别
 
返回值:
    套接字创建成功返回一个文件描述符,创建失败返回-1,错误码被设置

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

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

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

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

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

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

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

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

(4)socket函数返回值问题

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

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

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

注意: 

  • 按照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;
}

1.2、端口绑定

bind

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

函数: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,同时错误码会被设置

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

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

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

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

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 接收

1.4、字符串IP和整数IP说明

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

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

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

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;
}

暂时可以进行编译了

运行

./udpserver 1.23.45.67 8080

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

下面进行介绍几个IP 

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

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

  • 第一个IP:inet 10.0.4.14,这个IP是内网IP
  • 第二个IP: inet 127.0.0.1,这个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)

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;
}

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

1.6、服务端代码

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

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

recvfrom函数

 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:文件描述符,从哪个套接字去读数据
    第二个参数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 

#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;
}

二、客户端创建

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和端口号

关于客户端的绑定问题  

1、由于是网络通信,通信双方都需要找到对方,因此服务端和客户端都需要有各自的IP地址和端口号


2、只不过服务端需要进行端口号的绑定,而客户端不需要显示绑定端口号


3、服务器就是为了给别人提供服务的,因此服务器必须要让别人知道自己的IP地址和端口号,并且端口号和IP不能轻易改变


4、客户端在通信时虽然也需要端口号,但客户端一般是不进行显示绑定的,客户端访问服务端的时候,端口号只要是唯一就可以了


5、如果客户端绑定了某个端口号,那么以后这个端口号就只能给这一个客户端使用,就是这个客户端没有启动,这个端口号也无法分配给别人,并且如果这个端口号被别人使用了,那么这个客户端就无法启动了。


6、所以客户端的端口只要保证唯一性就行了,这个工作由OS完成,操作系统会自动给当前客户端生产一个唯一的端口号并且进行绑定


7、也就是说,客户端每次启动时使用的端口号可能是变化的,此时只要客户端的端口号没有被耗尽,客户端就永远可以启动

2.2、客户端代码

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

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

sendto函数

函数: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 

#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;
}

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

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

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

相关文章

ThinkPad T14p Gen1(21J7,21N3)原厂Windows11系统镜像下载

LENOVO联想ThinkPad 系列笔记本电脑原装出厂Win11系统安装包&#xff0c;恢复出厂开箱状态预装OEM系统 适用型号&#xff1a;T14p Gen1【21J7,21N3】 链接&#xff1a;https://pan.baidu.com/s/1bLHdQoQ9zsAeZgd4c0ie4A?pwdxps2 提取码&#xff1a;xps2 联想原装WIN系统自…

QT 布局管理器之QHBoxLayout

文章目录 概述.ui来看看Cmain.cpp运行 小结 概述 QHBoxLayout&#xff0c;在QT中是一个布局文件&#xff0c;而且相对来说还是比较简单的。接下来看下。 .ui 先看下在qt design中是如何用的&#xff0c;如下图&#xff1a; 就是这个布局文件&#xff0c;是一个xml的文件&am…

Redis之golang编程实战

Redis 介绍 官网&#xff1a;Redis - The Real-time Data Platform Redis 可作为数据库、缓存、流引擎和消息代理的开源内存数据存储。被用在不计其数的应用中。Redis 连续 5 年被评为最受欢迎的数据库&#xff0c;是开发人员、架构师和开源贡献者参与社区的中心。 Redis 是…

苹果计划推出付费版Apple Intelligence AI服务,费用高达20美元

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 苹果计划推出付费版Apple Intelligence AI服务 苹果公司正在计划推出一项新的高级人工智能服务&#xff0c;名为Apple Intelligence&#xff0…

关于嵌套循环之深入理解

关于嵌套循环之深入理解 # 外层循环遍历第一维&#xff08;深度&#xff09; for depth in range(len(cube)):# 中层循环遍历第二维&#xff08;行&#xff09;for row in range(len(cube[depth])):# 内层循环遍历第三维&#xff08;列&#xff09;for col in range(len(cube[d…

秒懂C++之进程状态及优先级

目录 一.进程状态 1.1 进程排队 1.2 进程状态 运行状态 阻塞状态 挂起状态 二.Linux环境下的进程状态 R运行状态 S睡眠状态 D磁盘休眠状态 T停止状态 X死亡状态 Z僵尸进程状态 三.进程优先级 基本概念 查看系统进程 用top命令更改已存在进程的nice 一.进程状态…

【数据结构】十大排序全面分析讲解及其对比分析(排序看懂就这篇!)

【数据结构】十大排序全面分析讲解及其对比分析 &#x1f525;个人主页&#xff1a;大白的编程日记 &#x1f525;专栏&#xff1a;数据结构 文章目录 【数据结构】十大排序全面分析讲解及其对比分析前言一.排序的概念及其运用1.1排序的概念1.2排序的应用 二.插入排序2.1 插入…

Gather:开启绝密社交和收益双重惊喜之旅

在数字时代&#xff0c;我们的隐私信息面临着严重的泄露风险&#xff0c;保护个人隐私变得尤为重要。基于区块链加专利硬件技术&#xff0c;Gather成为全球唯一实现真正绝密社交的DePIN社交产品&#xff0c;带来了划时代的社交体验。而其硬件产品G-BOX&#xff0c;不仅是你的隐…

Vercel Error: (Azure) OpenAI API key not found

题意&#xff1a;Vercel 错误&#xff1a;(Azure) OpenAI API 密钥未找到 问题背景&#xff1a; I implemented openAI API in my Next.js app with the help of langchain library and it works superb on localhost, but in Vercel (ProVersion) it throws an error: 我使用…

服务器磁盘扩容

一、扫描新硬件 如果通过命令&#xff1a; lsblk 没有看到新增的盘&#xff0c;使用如下命令&#xff0c;扫描新硬件 echo "- - -" > /sys/class/scsi_host/host0/scan二、查看磁盘和物理卷 查看新添加的硬盘设备名和物理卷的属性 fdisk -l pvdisplay下面的sdc是…

第四天博客顶顶顶

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 ☁️运维工程师的职责&#xff1a;监…

论软件设计方法及其应写作框架软考高级论文系统架构设计师论文

论文真题 软件设计&#xff08;Software Design&#xff0c;SD)根据软件需求规格说明书设计软件系统的整体结构、划分功能模块、确定每个模块的实现算法以及程序流程等&#xff0c;形成软件的具体设计方案。软件设计把许多事物和问题按不同的层次和角度进行抽象&#xff0c;将…

Spring的设计模式----工厂模式及对象代理

一、工厂模式 工厂模式提供了一种将对象的实例化过程封装在工厂类中的方式。通过使用工厂模式&#xff0c;可以将对象的创建与使用代码分离&#xff0c;提供一种统一的接口来创建不同类型的对象。定义一个创建对象的接口让其子类自己决定实例化哪一个工厂类&#xff0c;…

游乐园智慧向导小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;票务信息管理&#xff0c;门票购买管理&#xff0c;路线介绍管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;票务信息&#xff0c;路线介绍&#…

Spring Boot 3.x Rest API统一异常处理最佳实践

上一篇&#xff1a;Spring Boot 3.x Rest API最佳实践之统一响应结构 在Spring MVC应用中&#xff0c;要对web表示层所抛出的异常进行捕获处理有多种方式&#xff0c;具体的可参考著名国外Spring技术实战网站baeldung上的相关话题。Spring Boot对Spring MVC应用中抛出的异常以…

【算法设计题】判定给定的二叉树是否为二叉排序树,第7题(C/C++)

目录 第7题 判定给定的二叉树是否为二叉排序树 得分点&#xff08;必背&#xff09; 题解&#xff1a;判定给定的二叉树是否为二叉排序树 数据结构定义 判断二叉树是否为二叉排序树 详细解释 1. 空二叉树情况 2. 左右子树都无情况 3. 只有左子树情况 4. 只有右子树情…

【最长递增子序列】python刷题记录

R4-dp 目录 常规方法遇到以下序列时就会变得错误 动态规划的思路 单调栈 ps: class Solution:def lengthOfLIS(self, nums: List[int]) -> int:#最简单的方法nlen(nums)if n<2:return nmx1for i in range(n):max_i1for j in range(i1,n):if nums[i]<nums[j]:nums…

河南萌新联赛2024第(四)场

题目链接&#xff1a;河南萌新联赛2024第&#xff08;四&#xff09;场&#xff1a;河南理工大学_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ 1.小雷的神奇电脑 同或概念&#xff1a; • 如果两个输入位相同&#xff0c;则输出为1 • 如果两个输入位不同&#xff…

连接投影仪/显示器只能扩展不能复制的解决方案

原文章&#xff1a;https://iknow.lenovo.com.cn/detail/121481 故障现象&#xff1a; 笔记本外接投影仪/显示器后&#xff0c;笔记本屏幕有显示&#xff0c;但投影仪却只有背景或没有显示&#xff1b; 原因分析&#xff1a; 此现象多发生在双显卡机型上&#xff0c;笔记本屏…

SpringBoot3热部署

引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope><optional>true</optional> </dependency> 默认就是,无需配置 可以了…