首页 > 系统相关 >Linux 网络编程之UDP套接字

Linux 网络编程之UDP套接字

时间:2024-11-23 19:31:20浏览次数:7  
标签:std sockaddr UDP ip Linux sockfd 接字 include 客户端

前言

前面我们对网络的发展,网络的协议、网路传输的流程做了介绍,最后,我们还介绍了 IP端口号ip + port 叫做 套接字 socket, 本期我们就来介绍UDP套接字编程!

目录

1、预备知识

1.1 传输层协议: TCP/UDP

1.2 网络字节序

1.3 socket 接口

1.4 sockaddr

2、echo_server

2.1 核心功能分析

2.2 服务端设计

2.2.1 创建套接字

2.2.2 绑定ip和端口号

2.2.3 启动服务

2.2.4 服务端的全部源码

2.3 客户端设计

2.3.1 初始化客户端

2.3.2 启动客户端

2.3.3 客户端全部源码

2.3.4 解决遗留问题

2.4 优化和验证

 3、简单的英译汉

3.1 实现Dict 类

3.2 完成服务端的修改

3.3 主函数完善

4、多人聊天室

4.1 服务端改造

4.1.1 Route类实现

4.1.2 服务端主函数修改

4.2 客户端改造

4.2.1 客户端的主函数改造

5、地址函数补充

5.1 字符串转整数

5.2 整数转字符串


1、预备知识

1.1 传输层协议: TCP/UDP

前面我们结合系统了解了网络协议栈是基于OS的,而我们知道传输层是属于内核的,那么要通过网络协议栈进行通信,必定要调用系统调用!

简单认识 TCP

TCP (Transmission Control Protocol) 传输控制协议,它是一个传输层协议,特点有:

• 面向连接

• 可靠传输

• 面向字节流

这里简单的了解一下就可以,后面会详细介绍它的工作原理和机制的!例如:常考的三次握手四次挥手

简单认识 UDP

UDP (User Datagram Protocol)用户数据报协议,它也是一个传输层协议,特点有:

• 无连接

• 不可靠传输

• 面向数据报

关于 可靠性
TCP 的可靠传输并不意味着它可以将数据百分百递达,而是说它在数据传输过程中,如果发生了传输失败的情况,它会通过自己独特的机制,重新发送数据,确保对端百分百能收到数据;至于 UDP 就不一样,数据发出后,如果失败了,也不会进行重传,好在 UDP 面向数据报,并且没有很多复杂的机制,所以传输速度很快

总结起来就是:TCP 用于对数据传输要求较高的领域,比如金融交易、网页请求、文件传输等,至于 UDP 可以用于短视频、直播、即时通讯等对传输速度要求较高的领域

如果不知道该使用哪种协议,优先考虑 TCP,如果对传输速度又要求,可以选择 UDP

1.2 网络字节序

我们知道,在计算机系统中,多字节数据的存储方式有大端(Big-Endian)和小端(Little-Endian)之分。这种区别不仅存在于内存中,还影响到磁盘文件和网络数据流的字节序。

内存中的字节序

大端字节序:高字节(MSB)存储在低的内存地址上。

小端字节序:低字节(LSB)存储在低的内存地址上。

网络数据流的字节序:

发送过程:发送主机按照内存地址从低到高的顺序将数据发出。即,先发送的数据存储在发送缓冲区的低地址,后发送的数据存储在高地址。

接收过程:接收主机从网络上接收到的字节依次保存在接收缓冲区中,也是按照内存地址从低到高的顺序。

• 因此,网络数据流的地址是:主机先发出的数据是低地址,主机后发出的数据室高地址

• TCP/IP 协议为了确保不同架构的主机之间能够正确解析数据,规定网络数据流采用大端字节序,即 低地址,高字节

• 所以,不管当前主机是大端还是小端,网络收发数据都必须要使用大端字节序

关于大小端我们在《C语言数据在内存中的存储》中详细的介绍过!

那现在的问题是:如果我当前的机子是大端机器那还好。但是如果是小端呢?我是不是还得自己手动的转换?

OK,你想到的人家设计网络的人也是想到了,为了让网络程序具有可移植性,使用同样的C语言代码在大端和小端在计算机上编译后都能正常运行,所以就提供了网络序列和主机序列的转换函数

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

这写函数的, h 表示 host,n 表示 network, l 表示 32 位长整数,s 表示 16 位 短整数。

如果主机是大端机器那就不做任何转换直接返回即可,如果是小端机器,转为大端然后返回!

1.3 socket 接口

socket 套接字提供了下面这一批常用接口,用于实现网络通信

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

// 创建socket文件描述符(TCP/UDP	服务器/客户端)
int socket(int domain, int type, int protocol);

// 绑定端口号(TCP/UDP	服务器)
int bind(int socket, const struct sockaddr* address, socklen_t address_len);

// 开始监听socket (TCP	服务器)
int listen(int socket, int backlog);

// 接收连接请求 (TCP	服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);

// 建立连接 (TCP	客户端)
int connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

可以看到在这一批 API 中,频繁出现了一个结构体类型 sockaddr该结构体支持网络通信,也支持本地通信

socket 套接字就是用于描述 sockaddr 结构体的字段,复用了文件描述符的解决方案

1.4 sockaddr

socket  这套网络通信标准隶属于 POSIX  通信标准,该标准设计的初衷是为了实现 可移植性,程序可以直接在使用该标准的不同机器中运行,但是有的是网络通信,有的是本地通信, socket 套接字为了能同时兼顾这两种通信方式, 提供了 sockaddr 结构体

由  sockaddr 结构体衍生出了两个不同的结构体:sockaddr_in 网络套接字, sockaddr_un 域间套接字 ,前者是网络通信, 后者是本地通信

• 后面可以提取 sockaddr 的头部的 16 位地址类型判断是网络通信,还是本地通信

• 在进行网络通信时,需要提供 ip 地址, 端口号 等,而本地通信时,只需要提供一个路径名即可,通过读写同一个文件的形式进行通信(类似于命名管道)

•  socket 提供的接口参数为 sockaddr*类型,我们既可以传入 &sockaddr_in 进行网络通信,也可以传入 &sockaddr_un 进行本地通信,传参时将参数进行强制类型转换即可,这是使用 C语言 实现 多态 的典型做法,确保该标准的通用性

为什么不将参数设置为 void* ?
因为在该标准设计时,C语言还不支持 void* 这种类型,为了确保向前兼容性,即便后续支持后也不能进行修改了

关于 socketaddr_in 结构和上述 socket API 的更多详细信息放到后面写代码时再细谈


2、echo_server

接下来我们将实现三个基于UDP的网络程序,分别是:字符串回响简易的汉译英多人聊天室

2.1 核心功能分析

分别实现客户端和服务端,客户端向服务端发送请求,服务端接收到请求之后,直接回响给客户端,和我们之前介绍的 echo 指令 类似

所以,我们还是基于上面的先来搭建一个框架出来:

UdpServer.hpp

#pragma once

#include <iostream>


class UdpServer
{
public:
    // 构造
    UdpServer()
    {}
    // 初始化服务器
    void InitServer()
    {}
    // 启动服务器
    void Start()
    {}
    // 析构
    ~UdpServer()
    {}
private:
    //属性字段
};

UdpServerMain.cc

无论在服务端,还是在客户端Main函数都将采用智能指针管理资源

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

int main()
{
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>();// C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}

UdpClient.hpp

#pragma once

#include <iostream>

class UdpClient
{
public:
    // 构造
    UdpClient()
    {}
    // 初始化客户端
    void UdpClient()
    {}
    // 启动客户端
    void Start()
    {}
    // 析构
    ~UdpClient()
    {}
private:
    // 属性字段
};

UdpClientMain.cc

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

int main()
{
    std::unique_ptr<UdpClient> uclt = std::make_unique<UdpClient>();// C++14
    uclt->InitClient();
    uclt->Start();
    return 0;
}

Makefile

.PHONY: all
all : udpserver udpclient

udpserver : UdpServerMain.cc
	g++ -o $@ $^ -std=c++14
udpclient : UdpClientMain.cc
	g++ -o $@ $^ -std=c++14

.PHONY:clean
clean:
	rm -rf udpserver udpclient

2.2 服务端设计

服务端做的事情无非三个:

1、接受客户端的请求

2、处理客户端请求

3、响应给客户端

上面的三个操作,对应着的就是接收消息、处理消息、发送消息。如实现?当然是使用 socket 套接字接口喽!

2.2.1 创建套接字

创建套接字使用的是 socket 函数

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

// 创建套接字(TCP/UDP	服务器/客户端)
int socket(int domain, int type, int protocol);

参数

domain 创建套接字用于哪一种通信(网络/本地)

type 选择数据传输类型(流式/数据报)

protocol 选择协议类型(支持根据type自动推到,所以一般直接写0)

返回值

成功,返回一个文件描述符(套接字);失败,返回-1

我们这里因为是UDP协议实现的网络通信,所以,参数 domain 选择 AF_INET(基于IPv4标准),参数2 type 选择 SOCK_DGRAM(数据报传输),参数3 protocol 协议直接设置为 0,他会根据参数2自动推导

我们可以在服务端的初始化函数中,创建套接字!为了代码的可读性,我们在引入我们的日志,其次为了不让我们的服务端进行拷贝,我们可以把拷贝构造和赋值拷贝给禁用掉,也可以单独写一个类继承下来(建议),最后因为后面存在大量的判断退出的情况,我们把退出的码单独使用枚举放在一个Common.hpp中,后面谁用直接引用即可

nocopy.hpp

class nocopy
{
public:
    nocopy(){}
    ~nocopy(){}
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

这样写的好处就是,后面可以直接复用!

Common.hpp

#pragma once

enum
{
    SOCKET_ERROR = 1
};

目前公共的这里没有啥,只有一个 socket 的创建错误,后面了会加!

#pragma once

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

#include "nocopy.hpp"
#include "Common.hpp"
#include "Log.hpp"

using namespace LogModule;

class UdpServer : public nocopy
{
public:
    // 构造
    UdpServer()
    {}
    // 初始化服务器
    void InitServer()
    {
        // 创建 socket 套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);// IPv4 面向数据报
        if(_sockfd < 0)
        {
            LOG(FATAL, "socket create failed\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "socket create success, sockfd is %d\n", _sockfd);// 预期 3
    }
    // 启动服务器
    void Start()
    {}
    // 析构
    ~UdpServer()
    {}
private:
    int _sockfd;
};

我们来编译一下,此时 sockfd 应该是3,因为 【文件描述符】那里介绍过,0、1、2被占用了

没有问题,这也证明了 套接字的本质就是 文件描述符,不过它用于描述网络资源

2.2.2 绑定ip和端口号

两台主机通信的本质是两台主机上的两个进程在通信,即进程间通信

• 如何在网络中标识不同主机?IP地址

• 网络中如何在一台主机上标识唯一的一个进程?端口号

所以,我们只要知道对方的 ip+port 就可以唯一在网络中确定一个进程,然后就可以通信了,所以我们在客户端和服务端都需要进行绑定 ip+port ;

注意:目前这里我的是云服务器,云服务器不建议绑定一个特定的ip,而客户端也是不用,用户显示的绑定的(OS自动绑定)!

这一点后面解释!

使用 bind 函数进行绑定

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

// 绑定IP地址和端口号(TCP/UDP	服务器)
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);

参数

• sockfd    创建成功的套接字

• addr       通信信息的结构体

• addrlen  通信信息结构体的大小

这里第一个参数,就是上面刚刚创建 socket 成功的返回值,主要介绍的是第二个 addr 

上面说了, socket 套接字通信标准为了兼顾 网络通信 和 本地通信 所以提供了 struct sockaddr ,而实际网络通信用的是 struct sockaddr_in(本地通信用的是 struct sockaddr_un)这里是基于 UDP的网络通信所以,我们重点关注一下 struct sockaddr_in

网络通信本质是进程间通信,而ip标识不同网络中的主机,port标识主机中的不同进程,所以,双方通信前得知道对方的ip和port,而它两就会存储在struct sockaddr_in 中,下面这就是 sockaddr_in 结构体中的字段

struct sockaddr_in
{
  __SOCKADDR_COMMON(sin_);
  in_port_t sin_port;      /* Port number.  */
  struct in_addr sin_addr; /* Internet address.  */

  /* Pad to size of `struct sockaddr'.  */
  unsigned char sin_zero[sizeof(struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof(in_port_t) - sizeof(struct in_addr)];
};

  __SOCKADDR_COMMON(sin_);

这就是一个 短整数(16位),标识是 网络通信 还是 本地通信,这里其实就是一个,他的原型如下:

typedef unsigned short int sa_family_t;

/* This macro is used to declare the initial common members
   of the data types used for socket addresses, `struct sockaddr',
   `struct sockaddr_in', `struct sockaddr_un', etc.  */

#define	__SOCKADDR_COMMON(sa_prefix) \
  sa_family_t sa_prefix##family

#define __SOCKADDR_COMMON_SIZE	(sizeof (unsigned short int))

其中 sa_family_t 本质就是一个 短整数,而 sa_prefix 就是我们传递的通信类型即 sin_(网络/本地),这里发现 sa_prefix 和 family 给用 ## 连接了,这在C语言介绍过,这表示将传入的 sin_ 和 family 拼接为一个新的类型 sa_family_t 即标识通信类型/方式的16位

sin_port 就是端口号,其中 in_port_t 就是 uint16_t 的短整数

typedef uint16_t in_port_t;

sin_addr 表示的是 IP 地址,他这里又是一个结构体 in_addr 类型的变量 其实,这里面就一个整型变量:

/* Internet address.  */
typedef uint32_t in_addr_t;
struct in_addr
{
  in_addr_t s_addr;
};

这里明显的看到(IPv4)的时候是一个32位的整数,也就是说网络序列中存储ip使用的是一个整数!此时,你肯定好奇,不对呀!我平时用的是这种:127.0.0.1的字符串啊,其实这种叫做 点分十进制,方便用户看,真实的网络序列采用的是上面的整数存储的!

那是不是我也需要把这种点分十进制的字符串手动的转成整数呢?

是的!但是不需要你自己写函数转,人家已经写好了!例如:inet_addr 还有其他的,我们最后会补充的!目前暂时用这个就OK


介绍到这里我们就可以将 sockfd  和 存储 ip 和 port 信息的结构体进行绑定了!

我们打算未来在启动的时候,可以让外部动态的指定端口,所以我们可以把端口利用构造暴露出去,而ip上面说了,不需要绑定固定的ip所以不用管

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>

#include "nocopy.hpp"
#include "Common.hpp"
#include "Log.hpp"

using namespace LogModule;

static const int g_socket = -1;

class UdpServer : public nocopy
{
public:
    // 构造
    UdpServer(uint16_t port)
        : _sockfd(g_socket), _port(port)
    {
    }
    // 初始化服务器
    void InitServer()
    {
        // 创建 socket 套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IPv4 面向数据报
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket create failed\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "socket create success, sockfd is %d\n", _sockfd); // 预期 3
        // 创建存储 服务端主机的 ip 和 端口号信息的 struct sockaddr_in 的结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));       // 清空
        local.sin_family = AF_INET;         // 网络通信
        local.sin_port = htons(_port);      // 将主机转为网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 与服务器主机不绑定固定的ip而是任意的ip
        // 将 socket 套接字和 struct sockaddr_in 绑定
        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind failed\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");
    }
    // 启动服务器
    void Start()
    {
    }
    // 析构
    ~UdpServer()
    {
        if (_sockfd > 0)
            ::close(_sockfd);
    }

private:
    int _sockfd; // socket 套接字
    uint16_t _port;// 端口号
    // std::string _ip;// ip
};

使用 struct sockaddr_in 需要包含头文件

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

AF_INET 表示网络通信,当然也可以写成 PF_INET

INADDR_ANY 表示绑定任意 IP 地址

bzero 时 <cstring> 中的一个设置初始值函数,和 memset 类似

2.2.3 启动服务

上面我们已经把当前主机某个服务(进程)的信息(ip和port)进行了和socket的绑定,此时我们就可以启动服务端,进行 接收处理 用户的请求了!

首先,服务端是得先收到客户端的请求,然后在处理,最后返回给客户端!

服务端收到请求的时候,也得知道是谁发给你的,所以也得用 sockaddr_in 结构体的存储客户端的 ip 和 port 等信息;

• 使用 recvfrom 函数进行 接收客户端的请求

处理请求,这里的处理就是将收到的信息响应给用户即可

• 使用 sendto 响应给刚刚请求的客户端即可!

这里只需要介绍完这里两个函数,就可以启动服务了

recvfrom

作用:从 sockfd 中接收数据

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

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

参数

• sockfd     创建成功的 socket 套接字

• buf           一个接受数据用的缓冲区,

• len           缓冲区的大小

• flag          读取方式(阻塞/非阻塞)

• src_addr  表示客户端请求的 ip 和 port 信息

• addrlen    客户端信息结构体的大小

所以,我们在接受客户端的请求前,先得有一个 sockaddr_in 的结构体来记录,请求客户端的信息

返回值

成功,返回收到的字节数,失败,返回-1( ssize_t 就是 long int

sendto

作用:通过 sockfd 给指定的 dest_addr 发送数据

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

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);

参数

• sockfd     创建成功的 socket 套接字

• buf           一个接受数据用的缓冲区,

• len           缓冲区的大小

• flag          读取方式(阻塞/非阻塞)

• dest_addr  表示发送给客户端的 ip 和 port信息

• addrlen    客户端信息结构体的大小

返回值

成功,返回发送的字节数;失败,返回-1

下面就是 启动服务的代码

// 启动服务器
void Start()
{
    _isrunning = true;
    while (_isrunning)
    {
        // 创建客户端的 ip 端口信息的结构体
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 接受客户端的请求
        char buffer[1024];
        ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (n > 0)
        {
            buffer[n] = 0;
            // 处理请求
            std::string message = "[echo_server say]# ";
            message += buffer;
            // 响应给客户端
            ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, len);
            if (m < 0)
            {
                LOG(WARNING, "sendto failed\n");
            }
        }
        else
        {
            LOG(FATAL, "recvfrom failed\n");
        }
    }
    _isrunning = false;
}

注意在析构的时候,需要将 sockfd 给关掉

// 析构
~UdpServer()
{
    if (_sockfd > 0)
        ::close(_sockfd);
}

2.2.4 服务端的全部源码

#pragma once

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>

#include "nocopy.hpp"
#include "Common.hpp"
#include "Log.hpp"

using namespace LogModule;

static const int g_socket = -1;

class UdpServer : public nocopy
{
public:
    // 构造
    UdpServer(uint16_t port)
        : _sockfd(g_socket), _port(port), _isrunning(false)
    {
    }
    // 初始化服务器
    void InitServer()
    {
        // 创建 socket 套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // IPv4 面向数据报
        if (_sockfd < 0)
        {
            LOG(FATAL, "socket create failed\n");
            exit(SOCKET_ERROR);
        }
        LOG(INFO, "socket create success, sockfd is %d\n", _sockfd); // 预期 3
        // 创建存储 服务端主机的 ip 和 端口号信息的 struct sockaddr_in 的结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));       // 清空
        local.sin_family = AF_INET;         // 网络通信
        local.sin_port = htons(_port);      // 将主机转为网络序列
        local.sin_addr.s_addr = INADDR_ANY; // 与服务器主机不绑定固定的ip而是任意的ip
        // 将 socket 套接字和 struct sockaddr_in 绑定
        int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(FATAL, "bind failed\n");
            exit(BIND_ERROR);
        }
        LOG(INFO, "bind success\n");
    }
    // 启动服务器
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 创建客户端的 ip 端口信息的结构体
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 接受客户端的请求
            char buffer[1024];
            ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                buffer[n] = 0;
                // 处理请求
                std::string message = "[echo_server say]# ";
                message += buffer;
                // 响应给客户端
                ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&peer, len);
                if (m < 0)
                {
                    LOG(WARNING, "sendto failed\n");
                }
            }
            else
            {
                LOG(FATAL, "recvfrom failed\n");
            }
        }
        _isrunning = false;
    }
    // 析构
    ~UdpServer()
    {
        if (_sockfd > 0)
            ::close(_sockfd);
    }

private:
    int _sockfd;     // socket 套接字
    uint16_t _port;  // 端口号
    // std::string _ip; // ip 云服务器不需要绑定指定 ip
    bool _isrunning; // 服务端的状态
};

2.3 客户端设计

客户端的任务分为:1、向服务端发送请求 2、接收来自服务端的响应

收发数据都是基于 sockfd,以及 ip 、port 的所以我们先把这些字段加上,并在构造函数初始化

static const int g_sockfd = -1;

class UdpClient
{
public:
    // 构造
    UdpClient(std::string &ip, uint16_t port)
        : _sockfd(g_sockfd), _ip(ip), _port(port)
    {
    }
    // 初始化客户端
    void InitClient()
    {
    }
    // 启动客户端
    void Start()
    {
    }
    // 析构
    ~UdpClient()
    {
    }

private:
    int _sockfd;                // socket 套接字
    uint16_t _port;             // 端口号
    std::string _ip;            // ip 地址
    struct sockaddr_in _server; // 存储服务端的IP、port的结构体
};

2.3.1 初始化客户端

这里我们创建 socket 套接字,然后将存储 服务端 的结构体的内容填充即可

// 初始化客户端
void InitClient()
{
    // 创建套接字
    _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(1);
    }
    // 填充服务器端的数据
    memset(&_server, 0, sizeof(_server));             // 初始值设置为 0
    _server.sin_family = AF_INET;                     // 网络通信
    _server.sin_port = htons(_port);                  // 主机转网络
    _server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将点分十进制的ip转为整数
    // 注意这里客户端不需要 显示 的绑定自己的 ip 和 端口,OS 会自动绑定
}

注意:客户端这里,我们是需用户显示要绑定 ip 和 port 的(最后解释原因)

2.3.2 启动客户端

启动客户端这里的任务主要是,向服务器发送数据,然后结束反馈,显示给用户即可

// 启动客户端
void Start()
{
    // 长服务
    while (true)
    {
        // 客户端需要发送的消息
        std::string message;
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        // 向服务端发送请求
        int n = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));
        if (n > 0) // 发送成功,接受响应
        {
            // 记录服务端的ip和port等信息
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 创建接收数据的 缓冲区
            char buffer[1024];
            // 接收响应
            ssize_t m = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (m > 0)
            {
                buffer[m] = 0;
                std::cout << buffer << std::endl;// 接收到的数据,显示给用户
            }
            else
            {
                std::cerr << "recvfrom error" << std::endl;
                break;
            }
        }
        else
        {
            std::cerr << "sendto error" << std::endl;
            break;
        }
    }
}

注意,析构的时候需要将sockfd 给关掉

// 析构
~UdpClient()
{
    if(_sockfd > 0)
        ::close(_sockfd);
}

2.3.3 客户端全部源码

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

static const int g_sockfd = -1;

class UdpClient
{
public:
    // 构造
    UdpClient(std::string &ip, uint16_t port)
        : _sockfd(g_sockfd), _ip(ip), _port(port)
    {
    }
    // 初始化客户端
    void InitClient()
    {
        // 创建套接字
        _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            std::cerr << "socket error" << std::endl;
            exit(1);
        }
        // 填充服务器端的数据
        memset(&_server, 0, sizeof(_server));             // 初始值设置为 0
        _server.sin_family = AF_INET;                     // 网络通信
        _server.sin_port = htons(_port);                  // 主机转网络
        _server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将点分十进制的ip转为整数
        // 注意这里客户端不需要 显示 的绑定自己的 ip 和 端口,OS 会自动绑定
    }
    // 启动客户端
    void Start()
    {
        // 长服务
        while (true)
        {
            // 客户端需要发送的消息
            std::string message;
            std::cout << "Please Enter@ ";
            std::getline(std::cin, message);
            // 向服务端发送请求
            int n = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));
            if (n > 0) // 发送成功,接受响应
            {
                // 记录服务端的ip和port等信息
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                // 创建接收数据的 缓冲区
                char buffer[1024];
                // 接收响应
                ssize_t m = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                if (m > 0)
                {
                    buffer[m] = 0;
                    std::cout << buffer << std::endl;// 接收到的数据,显示给用户
                }
                else
                {
                    std::cerr << "recvfrom error" << std::endl;
                    break;
                }
            }
            else
            {
                std::cerr << "sendto error" << std::endl;
                break;
            }
        }
    }
    // 析构
    ~UdpClient()
    {
        if(_sockfd > 0)
            ::close(_sockfd);
    }

private:
    int _sockfd;                // socket 套接字
    uint16_t _port;             // 端口号
    std::string _ip;            // ip 地址
    struct sockaddr_in _server; // 存储服务端的IP、port的结构体
};

主函数修改

前面设计的时候,说了我们需要动态的指定服务端的 port 以及客户端的 ip+port,我们这里采用以前系统部分介绍的命令行参数来实现

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

// ./udpserver 8888
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage " << argv[0] << " port" << std::endl;
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);// C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}
#include "UdpClient.hpp"
#include <memory>
// ./udpclient 127.0.0.1 8888
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        std::cerr << "Usage " << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    std::unique_ptr<UdpClient> uclt = std::make_unique<UdpClient>(ip,port);// C++14
    uclt->InitClient();
    uclt->Start();
    return 0;
}

我们先来测试一下效果,再来解决上面的遗留问题

是没有问题的!下面这是我云服务器的公网ip,这也证明了服务端可以接收不同ip的请求

2.3.4 解决遗留问题

为什么云服务器上服务器端不能绑定指定的ip?

云服务器如果绑定了一个特定的ip 就只能接收来自该ip+端口的客户端的请求, 该ip的请求都请求不了,一般我们采用的是将服务端的 ip 设置为 INADDR_ANY (0.0.0.0)他表示任意地址!这就意味着服务器接受 任意 ip+服务器端口的客户端 访问该服务!

举个例子:一般服务器会开放一个端口供客户端访问,我们把这个端口比作一个小区的警务室!警务室有很多的通信设备,有线电话、手机、对讲机等;这些设备就是ip,如果这个警务室指定了有线电话接受外界的通信请求的话,外界就只能用电话!手机、对讲机等都打不通!而如果不指定通信设备的话,外界就可以使用任意的同学设备给警务室通信!而不指定具体的通信设备就是 IP 中的INADDR_ANY

为什么客户端不需要 显示 的绑定ip和端口?

首先注意的是,客户端不是不需要绑定ip和端口,只是不用用户显示的绑定!而是由OS随机选择一个端口进行绑定的!原因是,端口号资源是有限的,而客户端主机的进程不止一个,众多进程/服务无法做到客户端的端口号不冲突,所以如果客户端显示的绑定,就会造成同一主机不同进程的端口冲突,这就很不好!

例如:快手 觉得 8888 这个端口号,给他的客户端显示的绑定了,隔壁 抖音 觉的8888也很好,也给他的客户端绑定了,此时它两冲突了,造成的后果就是,你一旦打开抖音就开不了快手!显然我们平时不是这样的!

现在的问题是,端口号既然是OS随机选择的,什么时候选择的?如何选择?

当客户端第一次向服务端发送请求的时候,OS会自动的给客户端选择一个没被用的端口,进行和本机ip绑定!而这个被随机选择的端口被称为临时端口/客户端端口!至于客户端ip也是由OS和网络配置决定的,用户不用操作!

2.4 优化和验证

我们发现,上面的客户端可以请求服务端了,服务端也可以将收到的请求处理并返回给客户端了!但是不够优雅!为我们期望看到的是,服务端回显客户端的的ip和port的信息!这样我么可以验证上面说的,客户端的端口是OS随机选择的!

由于服务端拿到的是网络序列,所以我们进行小优化一下,在服务端拿到主机序列,所以我们写一个类专门完成这个事情:

#pragma once
#include <string>
#include <netinet/in.h>
#include <arpa/inet.h>

class Int_Addr
{
private:
    void ToHost()
    {
        // 将网络序列转为本机序列
        _ip = inet_ntoa(_addr.sin_addr);
        _port = ntohs(_addr.sin_port);
    }

public:
    Int_Addr(const struct sockaddr_in &peer)
        : _addr(peer)
    {
        ToHost();
    }

    std::string Ip()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    ~Int_Addr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _addr;
};

此时我们把他给加到服务端:

现在来看一下效果:

此时服务端就可以看到来自客户端请求的ip和端口了,而且我们客户端是没有指定端口的,这里他们的端口是不一样的!

OK,因为这里第一个UDP网络程序很多东西的细节以前没有介绍过,所以这里就很详细!以后我们不在介绍这么详细了!就直接用了!

请点击我的gitee链接查看全部源码:echo_server全部源码


 3、简单的英译汉

这个网络程序,打算实现的功能是:客户端输入一个单词,服务端进行翻译成汉语

仔细一分析,要实现这个样的程序好像不难,直接在上面的 echo_server  的基础上加业务即可!这个业务就是实现将一个单词翻译成汉语

为了降低耦合度,我们可以将执行翻译的这部分单独封装成一个类,利用包装器包装成一个可调用对象,给服务端这个可调用对象,服务端只需要调用即可!这样服务端并不关心处理业务在干啥,只需要调指定的可调用函数即可!做到了完美的解耦,优雅~!

3.1 实现Dict 类

我们将采用文件级别的英汉映射,所以提前得准备一个文件,外面在初始化对象的时候,只需要把这个文件的路径传过来就可以,然后构造的时候去加载该文件中的,单词和汉语将他们放到哈希表中,最后向外部提供翻译的接口 Transform 即可!

#pragma once

#include <iostream>
#include <string>
#include <unordered_map>

#include "Log.hpp"
#include "Common.hpp"

using namespace LogModule;

const std::string sep = ":";

class Dict
{
private:
    void Load()
    {
        // 打开文件
        std::ifstream in(_path);
        if (!in.is_open())
        {
            LOG(FATAL, "file: %s open failed\n", _path);
            exit(FILE_OPEN);
        }

        // 读取每一行
        std::string line;
        while (std::getline(in, line))
        {
            LOG(DEBUG, "load %s success\n", line.c_str());
            if (line.empty()) // 空行
                continue;
            auto pos = line.find(sep);
            if (pos == std::string::npos) // 最后只有分隔符
                continue;
            std::string key = line.substr(0, pos);
            if (key.empty()) //       :xxxx
                continue;
            std::string value = line.substr(pos + sep.size());
            if (value.empty()) // xxxxx:
                continue;

            _dict.insert(std::make_pair(key, value));
        }
        LOG(DEBUG, "load done\n");
        in.close();
    }

public:
    Dict(const std::string &path)
        : _path(path)
    {
        Load();
    }

    std::string Transform(std::string word)
    {
        if (word.empty())
            return "None";
        auto iter = _dict.find(word);
        return iter == _dict.end() ? "None" : iter->second;
    }

    ~Dict()
    {
    }

private:
    std::string _path;                                  // 文件的路径
    std::unordered_map<std::string, std::string> _dict; // 映射
};

因为这个逻辑不复杂所以直接就给源码了!

3.2 完成服务端的修改

其实服务端和上面的 echo_server 几乎是一样的,不同的是加了一个可调用对象的属性:

包装一个参数string ,返回值 string 的 func_t 的函数对象

这个对象是外部启动服务器的时候传过来的,服务器只是调用它!

仅仅是和上面的 echo_server 就多了一个 调用 _func  此时,服务器也不知道 _func 在干嘛,服务器只负责收发数据!

3.3 主函数完善

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

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage " << argv[0] << " port" << std::endl;
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);

    Dict dict("./dict.txt");
    func_t transfrom = std::bind(&Dict::Transform, &dict, std::placeholders::_1);
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, transfrom);// C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}

客户端只是创建了一个 Dict 对象,然后将它里面的 Transform 函数进行了绑定,然后给了服务器对象!关于std::bind 这里就是将 Dict 类中的Transform函数和&dict对象强关联了,此时就是返回值 string 参数string  的 可调用对象了!std::bind是解耦的神器,后面经常看到~

注意:这里客户端是不用动的!

请点击我的gitee链接查看全部源码:dict 全部源码


4、多人聊天室

最后一个UDP套接字的网络程序,我们打算实现一个简单的在线多人聊天室

主要功能是:不同主机的客户端都可以向服务端发送消息,服务端在将这些消息转给所有在线的客户端,即实现了多人聊天

其实这里和上面的翻译程序很相似,但是我们这里要采用多线程的转发!什么意思呢?

我们服务端的主线程负责收消息,向每个在线的用户转发的事情将采用线程转发!

由于转发是给每一个在线的用户转发的,所以我们需要在维护一个类Route专门负责维护在线用户和转发的,它里面提供一个转发的函数,外部主函数进行包装,主线程直接执行这个可调用的函数对象即可,即降低了耦合度

Route的转发函数内部将采用线程池当主线程调用时,只需要将转发的任务包装成线程池的任务类型,放到线程池即可,之后线程池中的线程就可以调用可!这里的线程池我们也采用我们自己以前手撕的那个~!

4.1 服务端改造

我们这个程序还是基于 echo_server 的,所以直接在它上面改造即可!

UdpServer.hpp 服务端 只需要把之前的转发消息换成_func即可

_func 是主函数启动服务器时给的,上面包装的类型是 server_t 是因为func_t 和线程池中的冲突了!这里因为要转发消息,所以得把 sockfd、转发的消息都给过去,最后给 Int_Addr  的addr是因为转发时得判断当前的用户是否在用户列表中!

而服务端主函数给的可调用对象是,包装的Route中的Forward的,所我们先介绍Route类

4.1.1 Route类实现

Route类的主要作用是:向所有的在线用户转发消,介绍到的消息

所以,得维护一个在线的用户列表,可以使用 vector,类型就是 Int_Addr主线程进来先检查该用户是否存在,然后判断是不是要退出(消息是:Q或者QUIT代表退出),如果是直接将从vector中删除即可!如果不删除,就包装线程池中线程的回调的函数,然后获取线程池的句柄(我们以前实现的是单例的),将任务放到线程池即可

#pragma once

#include <iostream>
#include <vector>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>

#include "Inet_Addr.hpp"
#include "Log.hpp"



class Route
{
private:
    void Check(Int_Addr &who)
    {
    }

    void Offline(Int_Addr &who)
    {
    }
    
    // 线程池中的线程回调的任务函数
    void ForwardHelper(int sockfd, std::string message, Int_Addr &who)
    {
    }


public:
    Route()
    {
    }

    void Forward(int sockfd, std::string message, Int_Addr &who)
    {
        // 1、检查用户是否在,在线列表 ---》a.不在:添加  b.在:啥都不做
        Check(who);
        // 2、用户需要离线离线
        if (message == "QUIT" || message == "Q")
        {
            Offline(who);
        }
        // 3、向你在线的用户转发消息
        //    让线程去转发
        // 包装函数
        // 获取句柄,推送任务
        
    }

    ~Route()
    {
    }

private:
    std::vector<Int_Addr> _online_users; // 在线用户列表
};

这就是大概的框架,先来实现一下,上面的检查用户在线、是否下线、线程池中的回调(转发消息)的函数:

Check

void Check(Int_Addr &who)
{
    for (auto &user : _online_users)
    {
        if (user == who)
            return; // 用户已经存在
    }
    // 不存在,添加
    _online_users.emplace_back(who);
    LOG(DEBUG, "%s add success\n", who.AddrStr().c_str());
}

这里使用到了 == Int_Addr中没有实现,所以得加一下:

Offline

void Offline(Int_Addr &who)
{
    auto iter = _online_users.begin();
    for (; iter != _online_users.end(); iter++)
    {
        if (*iter == who)
        {
            LOG(DEBUG, "%s remove success\n", who.AddrStr().c_str());
            _online_users.erase(iter);
            break; // 避免迭代器失效
        }
    }
}

 ForwardHelper

void ForwardHelper(int sockfd, std::string message, Int_Addr &who)
{
    std::string send_message = "[" + who.AddrStr() + "]# " + message;
    // 转发
    for (auto &user : _online_users)
    {
        struct sockaddr_in peer = user.Addr(); // 发给谁
        ::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
        LOG(INFO, "%s forward a message is %s\n", who.AddrStr(), send_message.c_str());
    }
}

OK,接下来就是包装 线程的可调用对象了,转发需要的就是 sockfd 和 message 、who也就是和上面的 ForWardHelper 绑死,后面线程直接调用即可,无需参数!

void Forward(int sockfd, std::string message, Int_Addr &who)
{
    // 1、检查用户是否在,在线列表 ---》a.不在:添加  b.在:啥都不做
    Check(who);
    // 2、用户需要离线离线
    if (message == "QUIT" || message == "Q")
    {
        Offline(who);
    }
    // 3、向你在线的用户转发消息
    //    让线程去转发
    task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);
    ThreadPool<task_t>::getInstance()->PushTask(t);
}

OK,到这里我们的Route的类基本上就设计好了!但是还差最后一口气:这里的检查和下线是主线程做的,向所有的在线用户转发时新线程做的,而他们的操作都会操作同一个vector,可能造成线程安全的问题,所以得保护,如何保护?加锁!

#pragma once

#include <iostream>
#include <vector>
#include <functional>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>

#include "Inet_Addr.hpp"
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "LockGuard.hpp"

using namespace LogModule;

using task_t = std::function<void(void)>; // 包装一个线程的任务函数

class Route
{
private:
    void Check(Int_Addr &who)
    {
        LockGuard lockguard(&_mutex);
        for (auto &user : _online_users)
        {
            if (user == who)
                return; // 用户已经存在
        }
        // 不存在,添加
        _online_users.emplace_back(who);
        LOG(DEBUG, "%s add success\n", who.AddrStr().c_str());
    }

    void Offline(Int_Addr &who)
    {
        LockGuard lockguard(&_mutex);
        auto iter = _online_users.begin();
        for (; iter != _online_users.end(); iter++)
        {
            if (*iter == who)
            {
                LOG(DEBUG, "%s remove success\n", who.AddrStr().c_str());
                _online_users.erase(iter);
                break; // 避免迭代器失效
            }
        }
    }

    void ForwardHelper(int sockfd, std::string message, Int_Addr &who)
    {
        LockGuard lockguard(&_mutex);
        std::string send_message = "[" + who.AddrStr() + "]# " + message;
        // 转发
        for (auto &user : _online_users)
        {
            struct sockaddr_in peer = user.Addr(); // 发给谁
            ::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
            LOG(INFO, "%s forward a message is %s\n", who.AddrStr(), send_message.c_str());
        }
    }

public:
    Route()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }

    void Forward(int sockfd, std::string message, Int_Addr &who)
    {
        // 1、检查用户是否在,在线列表 ---》a.不在:添加  b.在:啥都不做
        Check(who);
        // 2、用户需要离线离线
        if (message == "QUIT" || message == "Q")
        {
            Offline(who);
        }
        // 3、向你在线的用户转发消息
        //    让线程去转发
        task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who);
        ThreadPool<task_t>::getInstance()->PushTask(t);
    }

    ~Route()
    {
        pthread_mutex_destroy(&_mutex);
    }

private:
    std::vector<Int_Addr> _online_users; // 在线用户列表
    pthread_mutex_t _mutex;              // 互斥锁
};

这样就好了!最后还差服务端的主函数的传递给服务端可调用对象了

4.1.2 服务端主函数修改

只需要创建 Route 对象,然后绑定一个可接受三个参数的函数对象即可

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

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage " << argv[0] << " port" << std::endl;
        exit(1);
    }
    uint16_t port = std::stoi(argv[1]);

    Route route;
    server_t forward = std::bind(&Route::Forward, &route, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, forward);// C++14
    usvr->InitServer();
    usvr->Start();
    return 0;
}

4.2 客户端改造

我们期望客户端使用两个不用的线程分别进行收发消息,所以我们需要将原来的客户端的 Start函数进行改造成两个函数,供两个线程分别调用:

// 收消息
void RecvMsg(const std::string &name)
{
    while (true)
    {
        // 记录服务端的ip和port等信息
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        // 创建接收数据的 缓冲区
        char buffer[1024];
        // 接收响应
        ssize_t m = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
        if (m > 0)
        {
            buffer[m] = 0;
            std::cerr << buffer << std::endl; // 接收到的数据,显示给用户
        }
        else
        {
            std::cerr << "recvfrom error" << std::endl;
            break;
        }
    }
}

void SendMsg(const std::string &name)
{
    // 长服务
    std::string cli_profix = name + "# ";
    while (true)
    {
        // 客户端需要发送的消息
        std::string message;
        std::cout << cli_profix;
        std::getline(std::cin, message);
        // 向服务端发送请求
        int n = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server));
        if (n < 0)
        {
            std::cerr << "sendto error" << std::endl;
            break;
        }
    }
}

4.2.1 客户端的主函数改造

客户端的主函数就需要创建两个线程来分别执行,收消息和发消息的任务

#include "UdpClient.hpp"
#include "Thread.hpp"
#include <memory>

using namespace ThreadModule;

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        std::cerr << "Usage " << argv[0] << " ip port" << std::endl;
        exit(1);
    }
    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);
    std::unique_ptr<UdpClient> uclt = std::make_unique<UdpClient>(ip, port); // C++14

    // 创建两个线程分别负责 收发消息
    Thread sender("sender-thread", std::bind(&UdpClient::SendMsg, uclt.get(), std::placeholders::_1));
    Thread recver("recver-thread", std::bind(&UdpClient::RecvMsg, uclt.get(), std::placeholders::_1));

    sender.start();
    recver.start();

    sender.join();
    recver.join();
    return 0;
}

注意:这里bind时因为这里uclt是智能指针对象,所以需要获取原生对象的指针就需要使用get方法,这点我们在【智能指针】介绍过!

OK,测试一下:

这里我是在一台主机上使用 本地环回 公网ip 进行模拟的,为了让页面看起来不那么混乱,我用了 管道重定向 分离,因为客户端收到的消息是使用 std::cerr 打印的,也就是文件描述符是2,可以利用重定向将他们分开,将输出的cerr的内容都放到管道,一个专门的终端读取即可每个客户端都有一个管道和读取管道的终端,正如你的微信有一个输入区域(就是这里的下面的那个终端),有显示群消息的(上面的那个终端),且每个微信都是有各自的这两个的!

请点击我的gitee链接查看全部源码:chat全部源码


5、地址函数补充

sockaddr_in 中的成员 sin_addr.s_addr 表示一个32位的整数的 ip 地址,但是我们通常使用点分十进制的字符串表示 ip 地址,以下函数可以在 字符串整数的ip 之间进行转换

5.1 字符串转整数

// 将 cp 字符串转为 整数
int inet_aton(const char *cp, struct in_addr *inp);

// typedef uint32_t in_addr_t;
in_addr_t inet_addr(const char *cp);// cp 字符串转为 整数 返回

// af : IpV4和IPv6的哪一个  src : 表示字符串ip    dst :被转换之后的整数struct sockaddr_in 的 sin_addr.s_addr
int inet_pton(int af, const char *src, void *dst);

5.2 整数转字符串

// 将整数转为 整数 ip 返回
char *inet_ntoa(struct in_addr in);

// af : IpV4和IPv6的哪一个 src:代表整数ip    dst:用户指定的字符串缓冲区, size 缓冲区大小
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

其中 inet_pton 和 inet_ntop 不仅可以转换 IPv4 的 in_addr,还可以转换 IPv6 的 in6_addr,因此函数接口是 void *

#include <iostream>
#include <netinet/in.h>
#include <arpa/inet.h>

int main()
{       
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    char* ptr1 = inet_ntoa(addr1.sin_addr);
    char* ptr2 = inet_ntoa(addr2.sin_addr);
    printf("ptr1: %s, ptr2: %s\n", ptr1, ptr2);
    return 0;
} 

我们发现,第二次把第一次的给覆盖了!

原因是: inet_ntoa 把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆

盖掉上一次的结果

这里就会有问题:如果有多个线程调用 inet_ntoa 可能会出现异常的情况!

APUE 中, 明确提出 inet_ntoa 不是线程安全的函数

但是在 centos7 上测试了没有出问题,内部可能加了互斥锁(猜测)!

在多线程环境下, 推荐使用 inet_ntop, 这个函数由调用者提供一个缓冲区保存 结果, 可以规避线程安全

由于 centos7 已停止维护所以,我们写一段代码在 ubuntu 上验证一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
void *Func1(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr1: %s\n", ptr);
    }
    return NULL;
}
void *Func2(void *p)
{
    struct sockaddr_in *addr = (struct sockaddr_in *)p;
    while (1)
    {
        char *ptr = inet_ntoa(addr->sin_addr);
        printf("addr2: %s\n", ptr);
    }
    return NULL;
}
int main()
{
    pthread_t tid1 = 0;
    struct sockaddr_in addr1;
    struct sockaddr_in addr2;
    addr1.sin_addr.s_addr = 0;
    addr2.sin_addr.s_addr = 0xffffffff;
    pthread_create(&tid1, NULL, Func1, &addr1);
    pthread_t tid2 = 0;
    pthread_create(&tid2, NULL, Func2, &addr2);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

我大概测试了5次,都没有出错!

但是,我们还是建议在多线程下使用 inet_ntopinet_pton  


OK,好兄弟本期分享就到这里,我是 cp 我们下期再见~!

标签:std,sockaddr,UDP,ip,Linux,sockfd,接字,include,客户端
From: https://blog.csdn.net/m0_75256358/article/details/143721074

相关文章

  • linux操作系统-信号
    目录1.信号概念2.信号捕捉初识2.1通过终端按键产生信号2.2调用系统函数向进程发信号2.3由软件条件产生信号 2.4硬件异常产生信号在Linux操作系统中,信号(Signal)是一种重要的进程间通信机制,它允许一个进程向另一个进程发送异步通知。这些通知可以是简单的消息,如用户按下......
  • linux-进程间通信
    目录1.管道1.1无名管道1.2命名管道2.消息队列3.共享内存4.信号量一个完整的系统离不开各种各样的进程执行不同的任务,不同的任务之间存在着一些联系,为了能够保证进程之间的协调运作,使系统达到的理想的效果,需要进程之间传递信息。进程间通信目的:1.数据传输:一个进程需......
  • Linux常用命令之id命令详解
    id命令详解id命令在Linux和Unix系统中用于显示用户的标识信息,包括用户ID(UID)、组ID(GID)以及用户所属的附加组。这个命令对于系统管理员和开发者来说非常有用,因为它能帮助他们确认运行命令或脚本的用户身份,从而确保正确的权限设置和数据安全。以下是对id命令的详细解释......
  • JetBrains IDE 2024.3 (macOS, Linux, Windows) - 开发者工具
    JetBrainsIDE2024.3(macOS,Linux,Windows)-开发者工具Aqua,CLion,DataGrip,DataSpell,Fleet,GoLand,IntelliJIDEA,PhpStorm,PyCharm,Rider,RubyMine,WebStorm请访问原文链接:https://sysin.org/blog/jetbrains/查看最新版。原创作品,转载请保留出处。作者主......
  • Linux系统中如何排查CPU高占用进程
    如何在Linux系统中排查CPU高占用进程在Linux系统管理中,监控和优化系统的性能是非常重要的任务之一。当系统运行缓慢或者响应时间变长时,可能是由于CPU资源过度占用导致的。本文将介绍几种有效的方法来帮助您排查和解决Linux系统中的CPU高占用问题。1.使用top命令top是......
  • Linux内核USB2.0驱动框架分析--USB传输
    一、USB传输、事务、包的关系USB传输、事务、包是从不同层次上去说明一次数据交互的三个概念。举个例子可能更好些,“某领导和一个早起的程序员进行了一次交流,说了5件事”。OK,其实这里的"这次交流"就相当于USB的一次传输,"说了5件事"就相当于这次传输过程中的5个事务,当然每......
  • Linux——环境变量
        环境变量一般指的是在操作系统重用来指定操作系统运行环境的一些参数,这些参数会被bash使用,而bash是被我们用户使用的,也就是说,这些环境变量间接的也是被我们用户使用的。环境变量通常都有某些特殊的用途,它在系统重通常还具有全局的特性。命令行参数    ......
  • 科普文:软件架构之Linux系列【linux内核数据结构:链表、队列、映射、二叉树】
    概叙科普文:软件架构之Linux系列【linux内核数据结构汇总】-CSDN博客Linux内核提供了许多复杂的数据结构,这些结构被广泛用于各种不同的目的,例如存储设备管理、内存管理、进程管理等。以下是一些常见的数据结构以及它们的简要描述:双向链表(list):实现链表的数据结构,每个节点都......
  • Linux 系统关机后电源无法关闭的解决办法
    如果一些主板在关机之后电源不是自动关闭,需要手动关闭电源,请在grub里加上:引用:#boot=/dev/sdadefault=0timeout=5splashimage=(hd0,7)/boot/grub/splash.xpm.gzhiddenmenutitleFedora(2.6.23.1-42.fc8)   root (hd0,7)  kernel/boot/vmlinuz-2.6.23.1-42.fc8roro......
  • Linux内核调优
    为了让系统能够支持更大的并发,除了必须安装event扩展之外,优化linux内核也是重中之重,以下优化每一项都非常非常重要,请务必按逐一完成。参数解释:max-file:表示系统级别的能够打开的文件句柄的数量。是针对整个OS而言,并不是针对用户的。ulimit-n:表示控制进程级别能够打开的文......