0


【网络】套接字编程——UDP通信

作者:დ旧言~
座右铭:松树千年终是朽,槿花一日自为荣。

目标:UDP网络服务器简单模拟实现。

毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

专栏选自:网络

望小伙伴们点赞👍收藏✨加关注哟💕💕

​​

一、前言

前面我们已经学习了网络的基础知识,对网络的基本框架已有认识,算是初步认识到网络了,如果上期我们的学习网络是步入基础知识,那么这次学习的板块就是基础知识的实践,我们今天的板块是学习网络重要之一,学习完这个板块对虚幻的网络就不再迷茫!!!

** 二主体**

学习【网络】套接字编程——UDP通信咱们按照下面的图解:

2.1 概述

    在使用TCP编写的应用程序和使用UDP编写的应用程序之间存在一些本质的差异,其原因在于这两个传输层之间的差别:UDP是无连接不可靠的数据报协议,非常不同于TCP提供的面向连接的可靠字节流。
     以下给出了典型的UDP客户/服务器的函数调用。客户不与服务器建立连接,而是只管使用sendto函数给服务器发送数据报,其中必须指定目的地(即服务器)的地址作为参数。类似的,服务器不接受来自客户端的连接,而是只管调用recvfrom函数,等待来自某个客户的数据到达。recvfrom将接受与所接受的数据报一道返回客户的协议地址,因此服务器可以把响应的发送给正确的客户。

2.2 Udp Server(服务端)

接下来接下来实现一批基于

UDP

协议的网络程序,本节只介绍基于IPv4的socket网络编程。

2.2.1 核心功能

说明:

分别实现客户端与服务器,客户端向服务器发送消息,服务器收到消息后,回响给客户端,有点类似于

echo

指令,该程序的核心在于 **使用

socket

套接字接口,以

UDP

协议的方式实现简单网络通信。**

2.2.2 核心结构

程序由server.hpp server.cc client.hpp client.cc 组成,大体框架如下:

**创建

server.hpp

服务器头文件:**

#pragma once
 
#include <iostream>
 
namespace nt_server
{
    class UdpServer
    {
    public:
        // 构造
        UdpServer()
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {}
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        // 字段
    };
}

**创建

server.cc

服务器源文件:**

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer());//使用智能指针创建了一个UdeServer对象
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}

**创建

client.hpp

客户端头文件:**

#pragma once
 
#include <iostream>
 
namespace nt_client
{
    class UdpClient
    {
    public:
        // 构造
        UdpClient() 
        {} 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {}
 
        // 启动客户端
        void StartClient() 
        {}
 
    private:
        // 字段
    };
}

**创建

client.cc

客户端源文件:**

#include <memory>
#include "client.hpp"
 
using namespace std;
using namespace nt_client;
 
int main()
{
  unique_ptr<UdpClient> usvr(new UdpClient());
 
  // 初始化客户端
  usvr->InitClient();
  
  // 启动客户端
  usvr->StartClient();
 
  return 0;
}

**创建

Makefile

文件:**

.PHONY:all
all:server client
 
server:server.cc
    g++ -o $@ $^ -std=c++11
 
    
client:client.cc
    g++ -o $@ $^ -std=c++11
 
.PHONY:clean
clean:
    rm -rf server client

2.2.3 Udp Server 端代码


2.2.3.1 socket - 创建套接字

语法:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

解释说明:

  • domain:协议域(协议族)。该参数决定了 socket 的地址类型。在通信中必须采用对应的地址,如 AF_INET 决定了要用 IPv4 地址(32位的)与端口号(16位)的组合,AF_UNIX 决定了要用一个绝对路径名作为地址,AF_INET6(IPv6)。
  • type:指定了 socket 的类型,如 SOCK_STREAM(流式套接字)、SOCK_DGRAM(数据报式套接字)等等。
  • protocol:指定协议,如 IPPROTO_TCP(TCP传输协议)、PPTOTO_UDP(UDP 传输协议)、IPPROTO_SCTP(STCP 传输协议)、IPPROTO_TIPC(TIPC 传输协议)。
  • 返回值:一个文件描述符,创建套接字的本质其实就是打开一个文件。

功能说明:

  • socket函数打开一个网络通讯端口,如果成功的话就像open一样返回一个文件描述符,应用程序可以像读写文件一样read/write在网络上收发数据。
  • 好了socket函数学完了,接下来在 server.hpp 的 InitServer() 函数中创建套接字,并对创建成功/失败后的结果做打印。

代码呈现:

#pragma once
 
#include <iostream>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
 
namespace nt_server
{
    // 错误码
    enum
    {
        SOCKET_ERR = 1
    };
 
    class UdpServer
    {
    public:
        // 构造
        UdpServer()
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)//创建失败
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            // 创建成功
            std::cout << "Create Success Socket: " << sock_ << std::endl;
        }
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        int sock_; // 套接字
    };
}

总结说明:

因为这里是使用 UDP 协议实现的 网络通信,参数1 domain 选择 AF_INET(基于 IPv4 标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3设置为 0,可以根据 SOCK_DGRAM 自动推导出使用 UDP 协议。

2.2.3.2 bind - 将套接字与一个 IP 和端口号进行绑定

语法:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

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

解释说明:

  • sockfd: 通过socket函数得到的文件描述符
  • addr: 需要绑定的socket地址,这个地址封装了ip和端口号的信息
  • addrlen: 第二个参数结构体占的内存大小

详细了解一下这个sockaddr_in结构体:

struct sockaddr_in {
     short            sin_family;    // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
     unsigned short   sin_port;      // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
     struct in_addr   sin_addr;      // 4 字节 ,32位IP地址
     char             sin_zero[8];   // 8 字节 ,不使用
};
struct in_addr {
     unsigned long s_addr;          // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};

获得一个干净可用的 sockaddr_in 结构体后,可以正式绑定 IP 地址 和 端口号了。

代码呈现:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    class UdpServer
    {
    public:
        // 构造
        UdpServer(const std::string ip, const uint16_t port = default_port)
            :port_(port), ip_(ip)
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            // 创建成功
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.绑定IP地址和端口号
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 置0
 
            // 填充字段
            local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            local.sin_port = htons(port_); // 主机序列转为网络序列
            local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
 
            // 绑定IP地址和端口号
            int n = bind(sock_, (const sockaddr*)&local, sizeof(local));
            if(n<0)
            {
                std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
 
            // 绑定成功
            std::cout << "Bind IP&&Port Success" << std::endl;
        }
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        int sock_; // 套接字
        uint16_t port_; // 端口号
        std::string ip_; // IP地址(后面需要删除)
    };
}

总结说明:

  • 端口号会在网络里互相转发,需要把主机序列转换为网络序列,可以使用 htons 函数。
  • 需要把点分十进制的字符串,转换为无符号短整数,可以使用 inet_addr 函数,这个函数在进行转换的同时,会将主机序列转换为网络序列(因为IP地址需要在网络里面发送)。
  • 绑定IP地址和端口号这个行为并非直接绑定到当前主机中,而是在当前程序中,将创建的 socket 套接字,与目标IP地址与端口号进行绑定,当程序终止后,这个绑定关系也会随之消失。
2.2.3.3 地址转换函数 - 字符串和struct in_addr互相转换

我们这里为什么要使用字符串来表示IP地址:

首先大部分用户习惯使用的IP是点分十进制的字符串,就像下面这个这样。

128.11.3.31

我们的IP地址就是下面的第3个成员:

​struct sockaddr_in {
     short            sin_family;    // 2 字节 ,地址族,e.g. AF_INET, AF_INET6
     unsigned short   sin_port;      // 2 字节 ,16位TCP/UDP 端口号 e.g. htons(3490),
     struct in_addr   sin_addr;      // 4 字节 ,32位IP地址
     char             sin_zero[8];   // 8 字节 ,不使用
};
struct in_addr {
     unsigned long s_addr;          // 32位IPV4地址打印的时候可以调用inet_ntoa()函数将其转换为char *类型.
};

点分十进制的IP地址不好输入,我们往往先用更好输入的字符串来存储IP地址,然后将字符串版的IP地址转换为struct in_addr版的IP地址(也就是点分十进制版的)。

**字符串转

in_addr

结构体:**

语法:

#include <arpa/inet.h>
 
in_addr_t inet_addr(const char *cp);

说明:

  • 该函数将点分十进制的字符串表示的IPv4地址转换为网络字节序的32位整数。返回的是in_addr_t类型,通常用于填充sin_addr.s_addr字段。
  • ** 这个函数是更通用的函数,支持IPv4和IPv6地址的转换。**第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。第二个参数 src 是输入的字符串表示的IP地址,第三个参数 dst 是输出的二进制表示的IP地址。

使用:

#include <arpa/inet.h>
 
struct in_addr ipv4Address;
const char *ipString = "192.168.1.1";
inet_pton(AF_INET, ipString, &(ipv4Address.s_addr));

in_addr 结构体转字符串:

语法:

#include <arpa/inet.h>
 
char *inet_ntoa(struct in_addr in);

说明:

  • 第一个参数 af 表示地址族,常用的是 AF_INET(IPv4)和 AF_INET6(IPv6)。
  • 第二个参数 src 是输入的二进制表示的IP地址。
  • 第三个参数 dst 是输出的字符串表示的IP地址的缓冲区。第四个参数 size 是缓冲区的大小。

使用:

#include <arpa/inet.h>
 
struct in_addr ipv4Address;
ipv4Address.s_addr = inet_addr("192.168.1.1");
char ipString[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(ipv4Address.s_addr), ipString, INET_ADDRSTRLEN);

**我们让IP绑定到 0.0.0.0,

0.0.0.0

表示任意IP地址:**

#pragma once
//.....
namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
    const std::string="0.0.0.0";//注意这里
 
    class UdpServer
    {
    public:
        // 构造
        UdpServer(const std::string ip=defaultip, const uint16_t port = default_port)
            :port_(port), ip_(ip)
        {} 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
           //。。。。
 
            // 2.绑定IP地址和端口号
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 置0
 
            // 填充字段
            local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            local.sin_port = htons(port_); // 主机序列转为网络序列
            local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
 
           //。。。。
        }
 
        // 启动服务器
        void StartServer()
        {}
 
    private:
        int sock_; // 套接字
        uint16_t port_; // 端口号
        std::string ip_; // IP地址
    };
}

**

server.cc

服务器源文件:**

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer());
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}
2.2.3.3 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);

解释说明:

  • buf:接收缓冲区。
  • len:接收缓冲区的大小。
  • flags:默认设置为 0,表示阻塞。
  • src_addr:输出型参数,获取客户端的套接字信息,也就是获取客户端的 ip 和端口号信息。因为是 udp 网络通信,所以这里传入的还是 struct sockaddr_in 类型的对象地址。
  • addrlen:这里就是 struct sockaddr_in 对象的大小。
  • 返回值:成功会返回获取到数据的字节数;失败返回 -1。

使用示例:

struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
char buffer[1024];
 
int bytes_received = recvfrom(sockfd, buffer, sizeof(buffer), 0,
                              (struct sockaddr*)&sender, &sender_len);
if (bytes_received < 0) {
    perror("recvfrom failed");
    // handle error
}

代码呈现:**

server.hpp

服务器头文件 **

namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    class UdpServer
    {
    //.....
 
        // 启动服务器
        void StartServer()
        {
            // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
            char buff[1024]; // 缓冲区
            while (true)
            {
                // 1. 接收消息
                struct sockaddr_in peer;      // 客户端结构体
                socklen_t len = sizeof(peer); // 客户端结构体大小
 
                // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
                // 传入 0 表示当前是阻塞式读取
                ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
 
                if (n > 0)
                    buff[n] = '\0';
                else
                    continue; // 继续读取
 
                // 2.处理数据
                std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
                uint16_t clientPort = ntohs(peer.sin_port);      // 获取端口号
                printf("Server get message from [%s:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
 
                // 3.回响给客户端
                // ...
            }
        }
 
   //.....
    };
}
2.2.3.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:当前服务器的套接字,发送网络数据本质就是先向该主机的网卡(本质上就是文件)中进行写入。
  • buf:待发送的数据缓冲区。
  • len:数据缓冲区的大小。
  • flags:默认设置为 0。
  • dest_addr:接收方的套接字信息,这里也就是客户端的套接字信息。
  • addrlen: struct sockaddr_in 对象的大小。

使用示例:

struct sockaddr_in receiver;
receiver.sin_family = AF_INET;
receiver.sin_port = htons(12345);  // Some port number
inet_pton(AF_INET, "192.168.1.1", &receiver.sin_addr);  // Some IP address
 
char message[] = "Hello, World!";
ssize_t bytes_sent = sendto(sockfd, message, sizeof(message), 0,
                            (struct sockaddr*)&receiver, sizeof(receiver));
if (bytes_sent < 0) {
    perror("sendto failed");
    // handle error
}

代码呈现:**

server.hpp

服务器头文件 **

//。。。
namespace nt_server
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR
    };
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    class UdpServer
    {
    //.....
 
        // 启动服务器
        void StartServer()
        {
            // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
            char buff[1024]; // 缓冲区
            while (true)
            {
                // 1. 接收消息
                struct sockaddr_in peer;      // 客户端结构体
                socklen_t len = sizeof(peer); // 客户端结构体大小
 
                // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
                // 传入 0 表示当前是阻塞式读取
                ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&peer, &len);
 
                if (n > 0)
                    buff[n] = '\0';
                else
                    continue; // 继续读取
 
                // 2.处理数据
                std::string clientIp = inet_ntoa(peer.sin_addr); // 获取IP地址
                uint16_t clientPort = ntohs(peer.sin_port);      // 获取端口号
                printf("Server get message from [%c:%d]$ %s\n", clientIp.c_str(), clientPort, buff);
 
                // 3.回响给客户端
                n = sendto(sock_, buff, strlen(buff), 0, (const struct sockaddr *)&peer, sizeof(peer));
 
                if (n == -1)
                    std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            }
        }
 
   //.....
    };
}

如何证明服务器端正在运行:

可以通过

Linux

中查看网络状态的指令,因为我们这里使用的是

UDP

协议,所以只需要输入下面这条指令,就可以查看有哪些程序正在运行

netstat -nlup

修改 sever.cc 代码:

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
int main()
{
    unique_ptr<UdpServer> usvr(new UdpServer(80));//使用智能指针创建了一个UdeServer对象
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}
2.2.3.5 命令行参数改装服务端

上面的代码中,我们的端口号都是在代码里面指定了的,但是我们不能每次使用的时候都去修改代码吧,我们其实通过命令行参数来指定端口号

server.hpp:

//....
   // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
//.....

server.cc:

#include <memory> // 智能指针相关头文件
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
void Usage(const char* program)
{
  cout << "Usage:" << endl;
  cout << "\t" << program << "  ServerPort" << endl;
}
 
int main(int argc, char* argv[])
{
  if (argc != 2)
  {
    // 错误的启动方式,提示错误信息
    Usage(argv[0]);
    return USAGE_ERR;
  }
 
  //命令行参数都是字符串,我们需要将其转换成对应的类型
  uint16_t port = stoi(argv[1]);//将字符串转换成端口号
 
    unique_ptr<UdpServer> usvr(new UdpServer(port));//使用智能指针创建了一个UdeServer对象
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}

2.3 Udp Client 客户端

因为一个端口号只能被一个进程 bind,客户端的应用是非常多的,如果在客户端采用静态 bind,那可能会出现两个应用同时 bind 同一个端口号,此时就注定了这两个应用一定是不能同时运行的。为了解决这个问题,一般不建议客户端 bind 一个固定的端口,而是由操作系统来进行动态的 bind,这样就可以避免端口号发生冲突。这也间接说明,对一个 client 端的进程来说,它的端口号是几并不重要,只要能够标识该进程在主机上的唯一性就可以。因为,一般都是由 clinet 端主动的向 server 端发送消息,所以 client 一定是能够知道 client 端的端口号。相反,服务器的端口号必须是确定的。因此,在编写客户端的代码时,第一步就是创建套接字,创建完无需进行 bind,直接向服务器发送数据,发送的时候,操作系统会为我们进行动态 bind。

client.hpp代码:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
  class UdpClient
    {
    public:
        // 构造
        UdpClient(const std::string& ip, uint16_t port)
            :server_ip_(ip), server_port_(port)
        {} 
 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_); // 绑定服务器端口号
        }
 
        // 启动客户端
void StartClient() 
{
    char buff[1024];
 
    while(true)
    {
        // 1.发送消息
        std::string msg;
        std::cout << "Input Message# ";
        std::getline(std::cin, msg);
 
        ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
 
        if(n == -1)
        {
            std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            continue; // 重新输入消息并发送
        }
 
        // 2.接收消息
        socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
        n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
 
        if(n > 0)
            buff[n] = '\0';
        else
            continue;
 
        // 可以再次获取IP地址与端口号
        std::string ip = inet_ntoa(svr_.sin_addr);
        uint16_t port = ntohs(svr_.sin_port);
 
        printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
    }
}
 
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

client.cc代码:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
  class UdpClient
    {
    public:
        // 构造
        UdpClient(const std::string& ip, uint16_t port)
            :server_ip_(ip), server_port_(port)
        {} 
 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_); // 绑定服务器端口号
        }
 
        // 启动客户端
void StartClient() 
{
    char buff[1024];
 
    while(true)
    {
        // 1.发送消息
        std::string msg;
        std::cout << "Input Message# ";
        std::getline(std::cin, msg);
 
        ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
 
        if(n == -1)
        {
            std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            continue; // 重新输入消息并发送
        }
 
        // 2.接收消息
        socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
        n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
 
        if(n > 0)
            buff[n] = '\0';
        else
            continue;
 
        // 可以再次获取IP地址与端口号
        std::string ip = inet_ntoa(svr_.sin_addr);
        uint16_t port = ntohs(svr_.sin_port);
 
        printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
    }
}
 
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

2.4 简易公共聊天室

server.hpp代码:

#pragma once
 
#include <iostream>
#include <string>
#include <functional>//注意这个
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include<unordered_map>
 
namespace nt_server
{
 
        // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
 
    // 端口号默认值
    const uint16_t default_port = 8888;
 
    using func_t = std::function<std::string(std::string)>; 
// 可以简单的理解为func_t是一个参数为string,返回值同样为string的函数的类型
 
    class UdpServer
    {
    public:
 
        // 构造
        UdpServer(const func_t& func, uint16_t port = default_port)//注意这里的func_t
            :port_(port)
            ,serverHandle_(func)
//注意serverHandle_的类型已经是一个func_t,就是一个一个参数为string,返回值同样为string的函数的类型
        {}
 
        // 析构
        ~UdpServer()
        {} 
 
        // 初始化服务器
        void InitServer()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            // 创建成功
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.绑定IP地址和端口号
            struct sockaddr_in local;
            bzero(&local, sizeof(local)); // 置0
 
            // 填充字段
            local.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            local.sin_port = htons(port_); // 主机序列转为网络序列
            // local.sin_addr.s_addr = inet_addr(ip_.c_str()); // 点分十进制转为短整数,再将主机序列转为网络序列
            local.sin_addr.s_addr = INADDR_ANY; // 绑定任何可用IP地址
 
            // 绑定IP地址和端口号
            if(bind(sock_, (const sockaddr*)&local, sizeof(local)))
            {
                std::cout << "Bind IP&&Port Fail: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
 
            // 绑定成功
            std::cout << "Bind IP&&Port Success" << std::endl;
        }
 
        //检测是不是新用户
        void CheckUser(const struct sockaddr_in & client,const std::string clientIp_,uint16_t clientPort_)
        {
            auto iter= online_user_.find(clientIp_);
            if(iter==online_user_.end())
            {
                online_user_.insert({clientIp_,client});
                std::cout<<"["<<clientIp_<<":"<<clientPort_<<"] add to oniline user."<<std::endl;
            }
        }
 
        //广播给所有人
        void Broadcast(const std::string& respond,const std::string clientIp_,uint16_t clientPort_)
        {
            for(const auto&usr :online_user_)
            {
                std::string message="[";
                message+=clientIp_;
                message+=" : ";
                message+=std::to_string(clientPort_);
                message+="]#";
                message+=respond;
                
                int z = sendto(sock_, respond.c_str(), respond.size(), 0, (const sockaddr*)&usr.second, sizeof(usr.second));
                if(z == -1)
                    std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            }
 
        }
 
        // 启动服务器
        void StartServer()
        {
            // 服务器是不断运行的,所以需要使用一个 while(true) 死循环
            char buff[1024]; // 缓冲区
            while(true)
            {
                // 1. 接收消息
                struct sockaddr_in client; // 客户端结构体
                socklen_t len = sizeof(client); // 客户端结构体大小
 
                // 传入 sizeof(buff) - 1 表示当前传输的是字符串,预留一个位置存储 '\0'
                // 传入 0 表示当前是阻塞式读取
                ssize_t n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&client, &len);
 
                if(n > 0)
                    buff[n] = '\0';
                else
                    continue; // 继续读取
 
                // 2.处理数据
                 std::string clientIp = inet_ntoa(client.sin_addr); // 获取用户的IP地址
            uint16_t clientPort = ntohs(client.sin_port);      // 获取端口号
 
                //2.1.判断是不是新用户,如果是就加入,如果不是就什么也不做
                CheckUser(client,clientIp,clientPort); 
                
 
                //2.2 对数据进行业务处理,并获取业务处理后的结果
                std::string respond = serverHandle_(buff);
                //特别注意这里,业务处理的代码已经放到了这个serverHandle_这个函数了,
 
                // 3.回响给所有在线客户端
                Broadcast(respond,clientIp,clientPort);  
            }
        }
 
    private:
        int sock_; // 套接字
        uint16_t port_; // 端口号
        func_t serverHandle_; // 业务处理函数(回调函数)
        std::unordered_map<std::string,struct sockaddr_in> online_user_;//在线用户列表
    };
}

server.cc代码:

#include <string>
#include <vector>
#include <memory> // 智能指针相关头文件
#include <cstdio>
#include "server.hpp"
 
using namespace std;
using namespace nt_server;
 
//业务处理函数
std::string ExecCommand(const std::string& request)
{
    
    return request;
}
 
void Usage(const char* program)
{
  cout << "Usage:" << endl;
  cout << "\t" << program << "  ServerPort" << endl;
}
 
int main(int argc, char* argv[])
{
  if (argc != 2)
  {
    // 错误的启动方式,提示错误信息
    Usage(argv[0]);
    return USAGE_ERR;
  }
 
  //命令行参数都是字符串,我们需要将其转换成对应的类型
  uint16_t port = stoi(argv[1]);//将字符串转换成端口号
 
    unique_ptr<UdpServer> usvr(new UdpServer(ExecCommand,port));
 
    // 初始化服务器
    usvr->InitServer();
 
    // 启动服务器
    usvr->StartServer();
 
    return 0;
}

client.hpp代码:

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <unistd.h>
#include <arpa/inet.h>
#include<functional>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
    class UdpClient
    {
    public:
     // 构造
        UdpClient(const std::string &ip, uint16_t port)
            : server_ip_(ip), server_port_(port)
        {
        }
 
        // 析构
        ~UdpClient()
        {
        }
 
        
        static void *send_message(void *argc)//传进来的是this指针
        {
            UdpClient*_this =(UdpClient*)argc;//强制转换为类指针
 
            char buff[1024];
            while (1)
            {
                // 1.发送消息
                std::string msg;
                std::cout << "Input Message# ";
                std::getline(std::cin, msg);
 
                ssize_t n = sendto(_this->sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr *)&_this->svr_, sizeof(_this->svr_));
 
                if (n == -1)
                {
                    std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
                    continue; // 重新输入消息并发送
                }
            }
                return (void*)0;
        }
 
        static void *recv_message(void *argc)//传进来的是this指针
        {
            UdpClient*_this =(UdpClient*)argc;
 
            char buff[1024];
            while (1)
            {
                // 2.接收消息
                socklen_t len = sizeof(_this->svr_); // 创建一个变量,因为接下来的参数需要传左值
                int n = recvfrom(_this->sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr *)&_this->svr_, &len);
 
                if (n > 0)
                    buff[n] = '\0';
                else
                    continue;
 
                // 可以再次获取IP地址与端口号
                std::string ip = inet_ntoa(_this->svr_.sin_addr);
                uint16_t port = ntohs(_this->svr_.sin_port);
 
                printf("Client get message from [%s:%d]# %s\n", ip.c_str(), port, buff);
            }
                return (void*)0;
        }
 
 
        // 初始化客户端
        void InitClient()
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if (sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET;                            // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_);                  // 绑定服务器端口号
        }
 
        // 启动客户端
        void StartClient()
        {
            pthread_t recv, sender;
            //只需将类内的线程函数变化为static函数,但是static函数就不能直接访问到我们类内的私有数据,
            //不要忘了这个线程函数的void*参数,我们可以把this指针作为参数传给它,
            //然后就能通过这个this指针来访问到我们的类内的私有数据
            pthread_create(&recv, nullptr, recv_message, (void*)this);
            pthread_create(&sender, nullptr, send_message, (void*)this);
 
            pthread_join(recv,nullptr);
            pthread_join(sender,nullptr);
        }
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;//套接字描述符
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

client.cc保持不变

#pragma once
 
#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
namespace nt_client
{
    // 退出码
    enum
    {
        SOCKET_ERR = 1,
        BIND_ERR,
        USAGE_ERR
    };
 
  class UdpClient
    {
    public:
        // 构造
        UdpClient(const std::string& ip, uint16_t port)
            :server_ip_(ip), server_port_(port)
        {} 
 
        // 析构
        ~UdpClient() 
        {} 
 
        // 初始化客户端
        void InitClient() 
        {
            // 1.创建套接字
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
 
            if(sock_ == -1)
            {
                std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
 
            std::cout << "Create Success Socket: " << sock_ << std::endl;
 
            // 2.构建服务器的 sockaddr_in 结构体信息
            bzero(&svr_, sizeof(svr_));
            svr_.sin_family = AF_INET; // 设置为网络通信(PF_INET 也行)
            svr_.sin_addr.s_addr = inet_addr(server_ip_.c_str()); // 绑定服务器IP地址
            svr_.sin_port = htons(server_port_); // 绑定服务器端口号
        }
 
        // 启动客户端
void StartClient() 
{
    char buff[1024];
 
    while(true)
    {
        // 1.发送消息
        std::string msg;
        std::cout << "Input Message# ";
        std::getline(std::cin, msg);
 
        ssize_t n = sendto(sock_, msg.c_str(), msg.size(), 0, (const struct sockaddr*)&svr_, sizeof(svr_));
 
        if(n == -1)
        {
            std::cout << "Send Message Fail: " << strerror(errno) << std::endl;
            continue; // 重新输入消息并发送
        }
 
        // 2.接收消息
        socklen_t len = sizeof(svr_); // 创建一个变量,因为接下来的参数需要传左值
        n = recvfrom(sock_, buff, sizeof(buff) - 1, 0, (struct sockaddr*)&svr_, &len);
 
        if(n > 0)
            buff[n] = '\0';
        else
            continue;
 
        // 可以再次获取IP地址与端口号
        std::string ip = inet_ntoa(svr_.sin_addr);
        uint16_t port = ntohs(svr_.sin_port);
 
        printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
    }
}
 
 
    private:
        std::string server_ip_; // 服务器IP地址
        uint16_t server_port_;  // 服务器端口号
        int sock_;
        struct sockaddr_in svr_; // 服务器的sockadder_in结构体信息
    };
}

makefile代码:

.PHONY:all
all:server client
 
server:server.cc
    g++ -o $@ $^ -std=c++11 -lpthread
 
    
client:client.cc
    g++ -o $@ $^ -std=c++11 -lpthread
 
.PHONY:clean
clean:
    rm -rf server client

三、结束语** **

   今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​

标签: 服务器 php 运维

本文转载自: https://blog.csdn.net/AAlykk/article/details/140567878
版权归原作者 დ旧言~ 所有, 如有侵权,请联系我们删除。

“【网络】套接字编程——UDP通信”的评论:

还没有评论