UDP服务器实现

目录

一、服务端创建

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查看:

cpp 复制代码
函数: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

cpp 复制代码
#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

cpp 复制代码
#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查看:

cpp 复制代码
函数: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函数

cpp 复制代码
in_addr_t inet_addr(const char *cp);

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

函数做了两件工作:

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

整数IP转换成字符串IP

inet_ntoa函数

cpp 复制代码
char *inet_ntoa(struct in_addr in);

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

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

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

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

网络字节序与主机字节序之间的转换函数

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
#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函数的作用是接收信息

cpp 复制代码
函数: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

cpp 复制代码
#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

cpp 复制代码
#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

cpp 复制代码
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

cpp 复制代码
#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函数

cpp 复制代码
函数: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

cpp 复制代码
#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

cpp 复制代码
#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;
}

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

相关推荐
孤客网络科技工作室几秒前
VMware 虚拟机使用教程及 Kali Linux 安装指南
linux·虚拟机·kali linux
百事老饼干7 分钟前
Java[面试题]-真实面试
java·开发语言·面试
FeelTouch Labs17 分钟前
Netty实现WebSocket Server是否开启压缩深度分析
网络·websocket·网络协议
可均可可29 分钟前
C++之OpenCV入门到提高004:Mat 对象的使用
c++·opencv·mat·imread·imwrite
。puppy36 分钟前
HCIP--3实验- 链路聚合,VLAN间通讯,Super VLAN,MSTP,VRRPip配置,OSPF(静态路由,环回,缺省,空接口),NAT
运维·服务器
杨荧44 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
颇有几分姿色1 小时前
深入理解 Linux 内存管理:free 命令详解
linux·运维·服务器
白子寰1 小时前
【C++打怪之路Lv14】- “多态“篇
开发语言·c++
小芒果_011 小时前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj1 小时前
C++优选算法十 哈希表
c++·算法·散列表