Bootstrap

(udp)网络编程套接字Linux(整理)

 源IP地址和目的IP地址

唐僧例子1

  • 在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址.
  • 思考: 我们光有IP地址就可以完成通信了嘛? 想象一下发qq消息的例子, 有了IP地址能够把消息发送到对方的机器上,但是还需要有一个其他的标识来区分出, 这个数据要给哪个程序进行解析.

认识端口号

5aa466f7741c43a6a19bacf33aabd297.png

理解 "端口号" 和 "进程ID"

我们之前在学习系统编程的时候, 学习了 pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程. 那么这 两者之间是怎样的关系?

10086例子

另外, 一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定;fe650b7d258a451db453b5bfa3ed4483.png216566e140c14ef1a5b81f7c0b77b650.png

理解源端口号和目的端口号

唐僧例子2

送快递例子

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要 发给谁";

IP:Port()

ip地址4字节  端口号两个字节

跨主机 

7f46fb8563ff448bbee7b5773dd222f6.png

看到的公共部分       是网络

e63b13bff86441209aa88a58a35e44a9.png

在公网上:
IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程
IP:Port=标识全网唯一的一个进程

9ed020d12d3c41fea1c69616d95c77b8.pngfe650b7d258a451db453b5bfa3ed4483.png216566e140c14ef1a5b81f7c0b77b650.png

认识TCP协议

68e77c3741a24c249575602bfb9c54e6.png

认识UDP协议

cb6e6a1c88d24b698fd2049578f21457.png

网络字节序

ip地址4字节  端口号两个字节

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

a8346459c5d84253b9f3e6010f9f7c41.png

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

2c926d5138e3446ba4c12cb6e023c15c.png

480c8a69229b485982c3aef641c8087d.png

网络字节序和主机字节序的转换

主机转网络

服务端

客户端 

 网络转主机(后面ntoa就不建议使用了)

在 C 或 C++ 中,网络字节序和主机字节序的转换非常重要,特别是在进行网络编程时,因为不同的平台可能有不同的字节序(Endianness)。网络字节序通常是大端字节序(Big Endian),而主机字节序可能是大端或小端(Little Endian)。

网络字节序和主机字节序的转换函数

  1. **htons()**:将主机字节序(Host)转换为网络字节序(Network),适用于 16 位数据(short)。
  2. **htonl()**:将主机字节序(Host)转换为网络字节序(Network),适用于 32 位数据(long)。
  3. **ntohs()**:将网络字节序(Network)转换为主机字节序(Host),适用于 16 位数据(short)。
  4. **ntohl()**:将网络字节序(Network)转换为主机字节序(Host),适用于 32 位数据(long)。

示例代码

#include <stdio.h>
#include <arpa/inet.h>  // 包含字节序转换函数(适用于 Linux 和 UNIX)

int main() {
    uint16_t host_short = 0x1234;  // 主机字节序的 16 位数据
    uint32_t host_long = 0x12345678;  // 主机字节序的 32 位数据

    // 主机字节序转网络字节序
    uint16_t network_short = htons(host_short);
    uint32_t network_long = htonl(host_long);

    // 网络字节序转主机字节序
    uint16_t converted_short = ntohs(network_short);
    uint32_t converted_long = ntohl(network_long);

    // 输出结果
    printf("Host short: 0x%04X, Network short: 0x%04X, Converted back: 0x%04X\n",
           host_short, network_short, converted_short);
    printf("Host long: 0x%08X, Network long: 0x%08X, Converted back: 0x%08X\n",
           host_long, network_long, converted_long);

    return 0;
}

解释

  • **htons()**:将主机字节序的 16 位数据(host_short)转换为网络字节序。
  • **htonl()**:将主机字节序的 32 位数据(host_long)转换为网络字节序。
  • **ntohs()**:将网络字节序的 16 位数据(network_short)转换为主机字节序。
  • **ntohl()**:将网络字节序的 32 位数据(network_long)转换为主机字节序。

输出示例

Host short: 0x1234, Network short: 0x3412, Converted back: 0x1234
Host long: 0x12345678, Network long: 0x78563412, Converted back: 0x12345678

适用平台

这些字节序转换函数通常用于 Linux 和 UNIX 系统中,通过包含 arpa/inet.h 头文件来访问。对于 Windows 系统,提供了类似的转换函数,但位于 winsock2.h 中,使用方法相同。

总结

  • htons() 和 **htonl()**:用于将主机字节序转换为网络字节序(适用于 16 位和 32 位数据)。
  • ntohs() 和 **ntohl()**:用于将网络字节序转换为主机字节序。

这些转换函数在进行网络编程时非常重要,可以确保不同平台和字节序的主机之间进行正确的数据传输和处理。

网络转主机序列,主机序列转网络序列

发送到网络里的要转网络序列,从网络里拿的要转主机序列

地址转换函数(字符串传4字节IP)

本节只介绍基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址

但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串转in_addr的函数:

in_addr转字符串的函数:

其中inet_pton和inet_ntop不仅可以转换IPv4的in_addr,还可以转换IPv6的in6_addr,因此函数接口是void *addrptr。 代码示例:

关于inet_ntoa

inet_ntoa这个函数返回了一个char*, 很显然是这个函数自己在内部为我们申请了一块内存来保存ip的结果. 那么是 否需要调用者手动释放呢

man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放.

那么问题来了, 如果我们调用多次这个函数, 会有什么样的效果呢? 参见如下代码:

运行结果如下:

因为inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.

  • 思考: 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢?
  • 在APUE中, 明确提出inet_ntoa不是线程安全的函数;
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁;
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

套接字编程的种类:

1.域间套接字编程2.原始套接字编程3.网络套接字编程

d64c03e49b54459a82f8462e3fa411a3.png

udp代码示例

udp服务器

ca4091b5be82448daecdf81c49d6f1dd.png

socket(创建一个套接字)

socket创建套接字

第一个参数是一个域

socket 常见API

// 创建 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);

第一个参数:创建一个套接字的域,什么叫做域呢

可以理解为我们所要的那个套接字他是属于什么AF_INET,将来是使用IPv4还是IPv6网络通信的,还是有叫本地通信也叫作域间通信,第二个参数是socket对应的类型,udp是面向用户数据报(SOCK_DGRAM)tcp是面向字节流的第三个参数不用填,协议类型,创建一个套接字的本质就是打开一个文件,必须要告诉服务器,端口号,然后再绑定套接字端口号

8d653c031bf046d5aa810558107d2b7f.png

不同类型(第一个参数的) 

8716260b5598498d938bbcbd53f4783a.png

第二个:定义的套接字的类型

30f91d16c7da430a8bff696543150763.png

第三个写0就可以

71ccbde87cf1426295bbaf65c70b00e8.png

#pragma once
#include<iostream>
#include <sys/types.h>          
#include <sys/socket.h>
using namespace std;
class UdpSever
{
public:
    UdpSever()
    {}
    void Init()
    {
        sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    }
    void Run()
    {}
    ~UdpSever()
    {}
private:
    int sockfd;//网络文件描述符
};

把日志功能拷贝进来

99144e4d1376478fa96baba7ecefd7b8.png

创建一个Log对象

8b5540f7b349445d89640824c509a900.png

740be8267ae442d49fd83f34c6ec5278.png

 std::unique_ptr3d9a64f6500d4915b22a6f79297d3eb5.png

c04034a05b5349a4b2afd45c2866be76.png

C++11中开始提供更靠谱的unique_ptr

文档:https://cplusplus.com/reference/memory/unique_ptr/

unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份UniquePtr来了解它的原 理

/ C++11库才更新智能指针实现

// C++11出来之前,boost搞除了更好用的scoped_ptr/shared_ptr/weak_ptr // C++11将boost库中智能指针精华部分吸收了过来

// C++11->unique_ptr/shared_ptr/weak_ptr // unique_ptr/scoped_ptr // 原理:简单粗暴 -- 防拷贝

测试:

3b54eddb23834907af8c46e9fd808acc.png

ada8892b420b4ebc89d44d8d5485ce29.png

绑定bindc4c721283d374ddf95eddc091e2fabe0.png

ba21e2d038d646c2ad598624e655f350.png

绑定端口号

sockaddr结构

3355b591bfbc4a36ae4e65a4883999d1.png9f566ed4b506466d93ba183f949e7e0a.png

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16 位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址, 不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好 处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构 de0c98c98fdf4063a5893be71629af69.png

sockaddr_in 结构 

e610f6c82eff42288335ea95c0ed089f.png 虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主 要有三部分信息: 地址类型, 端口号, IP地址.

in_addr结构 

012fc56492ba47a8845ceffec8cb05ff.png in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

bzero把上面的结构体清空

5c80c4518d9f4ade955e831260500fbf.png

f051463c366842ca9deedd4019d2984b.png

INET

6b5b0f5524774deb843fc63d04ab5c15.png

addr,ip地址(32位的),port端口号,family所用的域或者家族(AF_INET)

c0525808bd5142f8855f07c36eb7734a.png

int,uint16_t,uint32_t的区别及其各自的用法

intuint16_t 和 uint32_t 都是表示整数类型的数据类型,它们在表示范围和用途上有所不同。下面是它们的区别和常见的用法:

1. int

  • 定义int 是C/C++中的标准整数类型,通常用于表示整数。它的大小和范围依赖于平台(操作系统和硬件)。

  • 大小和范围

    • 在大多数现代平台上,int 通常是 32 位的(4 字节),但是也有一些平台会使用 16 位(2 字节)或 64 位(8 字节)来表示。

    • 在 32 位系统上,int 的范围通常是:-2,147,483,648 到 2,147,483,647

    • 在 64 位系统上,int 的范围一般相同,但可能会有所不同,具体取决于编译器和操作系统。

  • 用途:适用于一般整数的存储,特别是当你不需要指定特定位数的整数时。通常是默认的整数类型。

2. uint16_t

  • 定义uint16_t 是一个固定宽度的无符号整数类型,表示一个 16 位(2 字节)的无符号整数。它是通过 C99 标准中的 <stdint.h> 头文件定义的。

  • 大小和范围

    • 它的大小是固定的 16 位(2 字节)。

    • 范围是 0 到 65535(即 2^16 - 1)。

  • 用途:用于需要 16 位无符号整数的场景,适用于存储比较小的正整数值,节省内存空间,常用于协议数据、传感器数据、颜色值等场景。

3. uint32_t

  • 定义uint32_t 是一个固定宽度的无符号整数类型,表示一个 32 位(4 字节)的无符号整数。它同样是通过 <stdint.h> 头文件定义的。

  • 大小和范围

    • 它的大小是固定的 32 位(4 字节)。

    • 范围是 0 到 4,294,967,295(即 2^32 - 1)。

  • 用途:用于需要 32 位无符号整数的场景,常用于表示较大范围的正整数,如文件大小、内存地址、网络协议等。

总结对比:

数据类型

大小

范围

用途

int

32 位(常见)

-2,147,483,648 到 2,147,483,647

通用整数类型,平台相关

uint16_t

16 位

0 到 65535

用于小范围无符号整数,如协议、传感器数据

uint32_t

32 位

0 到 4,294,967,295

用于大范围无符号整数,如文件大小、网络地址

选择依据:

  • 如果你知道需要存储的数值范围,并且希望节省内存,可以选择 uint16_t 或 uint32_t

  • 如果不关心存储大小且需要处理负数,int 是默认选择。

  • 需要无符号整数时,uint16_t 和 uint32_t 是更为合适的选择。

在网络中为什么用uint16_t,不用int

在网络通信中,通常使用 uint16_t 而不是 int 主要有以下几个原因:

1. 一致性和标准化

  • uint16_t 是一个 无符号 16 位整数,大小和范围是固定的,明确且可移植。无论在哪个平台或操作系统上,uint16_t 都始终表示一个 16 位的正整数,范围为 0 到 65535,没有任何平台差异。

  • 相反,int 的大小和范围依赖于编译器、操作系统以及硬件架构(通常是 32 位或 64 位),这使得其在不同平台间可能有不同的表现。例如,32 位平台上的 int 范围是 -2,147,483,648 到 2,147,483,647,而 64 位平台可能会有所不同。

在网络协议中,数据交换需要确保不同系统间能正确地理解数据。如果每个平台的 int 范围不同,可能会导致解析数据时出错。因此,使用 uint16_t 等固定宽度的数据类型可以避免这种问题。

2. 明确的数值表示

  • 网络通信中传输的很多数据(例如端口号、长度、标识符等)都是 非负整数,并且其数值范围通常是有限的。uint16_t 正好满足这些需求,它能够表示 0 到 65535 范围的正整数,这对于大多数网络协议中的字段来说已经足够。

  • 如果使用 int,会引入不必要的负数范围。例如,某些协议字段如长度或计数信息是不需要负值的,因此使用 uint16_t 可以更好地表达数据的本意。

3. 节省带宽和存储空间

  • 网络传输中的数据结构通常是非常紧凑的,节省带宽和存储空间是设计网络协议时的重要考虑因素。uint16_t 是一个固定的 16 位类型,比 int(通常是 32 位)节省了一半的空间。

  • 如果某个字段本来只需要表示 0 到 65535 的值,使用 int 会浪费空间(特别是在带宽受限的网络环境中,数据包大小需要尽量精简),而 uint16_t 刚好满足需求。

4. 与协议格式一致

  • 网络协议(如 IP、TCP、UDP 等)中通常定义了固定宽度的字段,例如端口号、数据长度等。协议规范中往往会指定使用 16 位(uint16_t)或 32 位(uint32_t)等类型来表示数据。

    • 例如,TCP/IP 协议中,端口号使用的是 16 位的无符号整数(uint16_t),而 数据包长度等字段也通常是无符号整数。

  • 使用 uint16_t 可以确保与协议规范一致,使得协议解析更加标准化,避免不同平台或编译器之间的数据解释差异。

总结:

  • uint16_t 在网络协议中比 int 更加适用,因为它的大小和范围是固定的,明确表示非负整数,且可以确保不同平台之间的一致性。

  • int 的范围和大小是依赖于平台的,可能导致不必要的负数值,并且会占用更多的内存空间,因此不适合在需要精确控制数据结构大小的网络通信中使用。

因此,使用 uint16_t 可以提高协议的跨平台兼容性,保证数据的准确传输,并且更高效地使用带宽和存储。

##的作用

68b6861ef85849c191d84188843e7b5c.png

主机序列转成网络序列

端口号只有两个字节

2e1123cc704443a89ac8020a108d16dd.png

如果机器本来是大端机那么就什么都不做,如果是小端机,会转换成大端

c0013a9e5f304bd0a67d911c6e4700b2.png

main中这样传ip

031c972e5a004cceba22a99899d0867d.png

如何快速的将整数IP<->字符串IP

自己写的话如果实现

8bce586b9dc94543a33d6f6f738bf6ac.png

uint8_t一个字节

0866c11933ab45bd920e7f66f37ebca7.png

这样就把4字节的ip直接转换成了字符串风格的ip

30c12eb315a04f03be782e49ac883f8f.png

uint32_t是4个字节

6a7aef0bf67942d4b7348a1c709ac482.png

b4c24c62eb58476fa417510dbef10f76.png

把字符串风格的ip地址转化为四字节(网络风格的四字节)

d85398646f664206b6a2ff827f3687e8.png

c48fdcff3a2742518d1cf5fa61676d16.png

61cfae9004764b0cb3dc622db1682eff.png

f0bb917ae08545628eca39d895680b63.png

local在用户栈上,这样赋值只是把变量给赋值了,并没有把local变量和内核的网络套接字相关联,还没有进行绑定

7c694700f12042a4a0ccd7e494bd661d.png

再进行bind绑定,绑定的本质,把这个参数设置进内核指定的套接字内部

35591e052180455dae3a6b5d96c5da19.png

要强转

fb3ad359bdab4c69a0afc0efbbc7d94e.png

ac04fbe9a19146bda9ecee5d9a7d6168.png

以上udp服务器核心的启动代码基本已经完成

思路精华

111911d2542b40fd9bfd284fbc7258d6.png

ed4bef38062240b2b8a11e6330b5263e.png

d67907c3a0984066af87965d37ad6549.png

测试:

#pragma once
#include<iostream>
#include <sys/types.h>          
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>
#include <stdio.h>
#include "log.hpp"
using namespace std;

extern Log1 log1;//日志器声明

enum{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int Size = 1024;
class UdpSever
{
public:
    UdpSever(const uint16_t& port = defaultport, const string& ip = defaultip):sockfd_(0), 
                                    port_(port), ip_(defaultip), isrunning_(false)
    {}
    void Init()
    {
        //创建udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            log1(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCKET_ERR);
        }
        log1(Info, "socket create success, sockfd:%d", sockfd_);
        //2.bind socket(绑定端口号)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  uint32_t必须是网络序列

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local))< 0)
        {
            log1(Fatal, "bind error, error:%d, err string:%s", errno, strerror(errno));   
            exit(BIND_ERR);
        }
        log1(Info, "bind success, error:%d, err string:%s", errno, strerror(errno));   
    }
    void Run()
    {
        isrunning_ = true;
        //const int size = 1024;
        char inbuffer[Size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                log1(Warning, "recvfrom error, error:%d, err string:%s", errno, strerror(errno));   
                continue;
            }
            inbuffer[0] = 0;
            //充当了一次数据的处理
            string info = inbuffer;
            string echo_string = "sever echo#" + info;
            //发送回给客户端
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }
    ~UdpSever()
    {
        if(sockfd_ > 0)
        close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_;//表明服务器进程端口号
    string ip_;
    bool isrunning_;
};
#include "udpServer.hpp"
#include<memory>
using namespace std;
Log1 log1;

//"120.78.126.148"点分十进制字符串风格的IP地址
int main()
{
    unique_ptr<UdpSever> svr(new UdpSever());//云服务器IP
    svr->Init();
    svr->Run();

    return 0;
}

 6bcd49d626e34e37a4f6dfa0cfb4342b.png

服务器应该周而复始的运行

isrunning是是否在运行

150800fad9c94a69a315ee9192635a1f.png

8a83c9c5d62548dd8dc2cac8698ed6a3.png

从udp中读取数据

read,write是面向字节流的,而udp面向数据报

recv

d1a78aee60254924b3ac3601c6d929f7.png

收消息

分别是客户端结构体和这个结构体的大小socklen_t

客户发的数据,客户是谁153620d9ea404721a99033d7c27f7f79.png

失败了继续重收 

ea4486a754e14257ae3442347e98fab9.png

整理一下 

e29881e551024a14bbd5f7e1e82e84d9.png

发送回给客户端

8f97adcdc495448b845dd4cce2d8fa18.pngdda9202234de4fbb993d2d2aa256a70f.png

查看端口号和IP地址(netstat -naup)c9e1d760820c41d991b813024c5b6a47.png

d372d6f6b5c2493aa678d4d213888329.png

05306a773a6b4561b7fac031b2167244.png

不带n 

1f56c163b5a04742824cfce48e59b8f4.png 07afb60366fa463896b4c667d7470b5d.png

测试,ip地址是云服务器的,8080端口号

云服务器最好就直接不写ip地址就好了,直接默认的0

绑定成功

e3acc093e21e4e76afa8edf00b8151f1.png

关于port的问题(端口号)

eaa3d49e3acf4cb384e85605338afb66.png

sudo就可以了        

41242b5685484a40bf686c471ba0e522.png

0b444476a1834d979ba3e2ff713e43a8.png

0-1023不让绑定,最好绑定1024的端口,有的3306也不可以 

74ec78ea3b9345c595cb1024f10ba246.png

写成命令行./udpServer+port

IP默认0

#include "udpServer.hpp"
#include<memory>
using namespace std;
Log1 log1;

//"120.78.126.148"点分十进制字符串风格的IP地址
void Usage(string proc)
{
    cout<< "\n\rUsage" << proc << "port[1024+]\n" << endl;
}

//./udpServer+port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpSever> svr(new UdpSever(port));//云服务器IP
    svr->Init();
    svr->Run();

    return 0;
}

349b0712e5bc4549b88b71809e32b67c.png

写一个客户端

makefile里要同时执行两个程序

39bfe10913934f53b0f80b9c571f3bda.png

.PHONY:all
udpServer:main.cc
	g++ -o $@ $^ -std=c++11
udpClient:udpClient.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f udpServer udpClient

创建套接字

#include<iostream>
#include <sys/types.h>          
#include <sys/socket.h>
using namespace std;

int main()
{
    //                  IPv4      用户数据报
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    if(sockfd < 0)
    {
        cout<< "socker error" << endl;
        return 1;
    }

    return 0;
}

记得close网络文件

6e4b021ef1cb4fbc888a5fc6b5e3dd61.pngf433e03ec6594cdea8f457e928e194b9.png

客户端要bind吗

一个端口号只能被一个进程绑定,一个程序可以绑定多个端口号,所以要OS自由随机选择,防止定义重复造成冲突,如果两个进程同一个端口号,那么只能启动一个程序。。。客户端的端口号保持唯一性就可以了

cfb91714b41b43a8aa309a35dd9aef4e.png

服务器的端口号要确定是因为,客户端是要访问服务端的,需要知道确定的端口号和ip

客户端写成命令行./udpServer+ip+port

void Usage(string proc)
{
    cout<< "\n\rUsage:" << proc << "severip severport\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string severip = argv[1];
    uint16_t severport = stoi(argv[2]);
    //                  IPv4      用户数据报
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    if(sockfd < 0)
    {
        cout<< "socker error" << endl;
        return 1;
    }

    close(sockfd);
    return 0;
}

1.数据  2.发给谁(sendto)

89095d2b795e45889c16f4a61f2ab46e.png

sendto有可能向不同的服务器发消息 

整理一下。主机转网络(服务器信息)

端口号主机转网络 

edee9a1c029a4dfabf26675b1e7878cd.png

32fd402a42ad4f05ab9430baefffcfc9.png

接收一下(recvfrom)

4f570cad89da4618886b1e7a20735943.png

收到的信息放进buffer

代码示例

#include <iostream>
#include <sys/types.h>          
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
using namespace std;
void Usage(string proc)
{
    cout<< "\n\rUsage:" << proc << "severip severport\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string severip = argv[1];
    uint16_t severport = stoi(argv[2]);

    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(severport);
    server.sin_addr.s_addr = inet_addr(severip.c_str());
    bzero(&server, sizeof(server));
    socklen_t len = sizeof(server);//长度

    //                  IPv4      用户数据报
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        cout<< "socker error" << endl;
        return 1;
    }
    string message;
    char buffer[1024];
    while(true)
    {
        cout<< "Please Enter@ "; 
        getline(cin, message);//空格不会作为结束标识符

        //1.数据  2.发给谁
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            cout<< buffer << endl;
        }

    }

    close(sockfd);
    return 0;
}

测试(客户端向服务器发信息)

035c24ea907a433c8ecc9fe6e90b521f.png

客户端怎么知道服务器ip地址和端口号呢,日常生活中是不会知道的,端口号是固定的,约定好的,服务器也要遵守这样的规则,把服务器ip地址拿过来,也就是云服务器的ip(101.34.66.193)(需要开放端口)5dc7ea4203ca4b529dd2ba0faeeb5179.png

6ab8d3ca4a604fe0bd999e350c8dfdfa.png

(无法使用要用127.0.0.1)

37df3e5955794755909f9b17bac768ba.png

84b2360d3ba142ac920ad7aee14774b7.png

udpServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "log.hpp"
using namespace std;

using fun_t = function<string(const string&)>;
//typedef function<string(const string&)> fun_t;

extern Log1 log1;//日志器声明

enum{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int Size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = defaultport, const string& ip = defaultip):sockfd_(0), 
                                    port_(port), ip_(ip), isrunning_(false)
    {}
    void Init()
    {
        //创建udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            log1(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCKET_ERR);
        }
        log1(Info, "socket create success, sockfd:%d", sockfd_);
        //2.bind socket(绑定端口号)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  uint32_t必须是网络序列

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local))< 0)
        {
            log1(Fatal, "bind error, error:%d, err string:%s", errno, strerror(errno));   
            exit(BIND_ERR);
        }
        log1(Info, "bind success, error:%d, err string:%s", errno, strerror(errno));   
    }
    void Run(fun_t func)//加了fun_t func本质是对代码进行分层
    {
        isrunning_ = true;
        //const int size = 1024;
        char inbuffer[Size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //收到数据
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                log1(Warning, "recvfrom error, error:%d, err string:%s", errno, strerror(errno));   
                continue;
            }
            //收到了一个数据
            inbuffer[n] = 0;

            //充当了一次数据的处理
            //处理数据放在外面去处理
            string info = inbuffer;
            //string echo_string = "sever echo#" + info;
            string echo_string = func(info);//在外面去处理  
            cout<< echo_string <<endl;  // 这个你刚刚注释了

            //发送回给客户端
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0)
        close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_;//表明服务器进程端口号
    string ip_;
    bool isrunning_;
};

udpClient.cc

#include <iostream>
#include <sys/types.h>          
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
using namespace std;
void Usage(string proc)
{
    cout<< "\n\rUsage:" << proc << "severip severport\n" << endl;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);//长度

    //                  IPv4      用户数据报
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        cout<< "socker error" << endl;
        return 1;
    }
    string message;
    char buffer[1024];
    while(true)
    {
        cout<< "Please Enter@ "; 
        getline(cin, message);//空格不会作为结束标识符
        //cout<< message <<endl;

        //1.数据  2.发给谁
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            cout<< buffer << endl;
        }

    }

    close(sockfd);
    return 0;
}

main.cc 

#include "udpServer.hpp"
#include<memory>
using namespace std;
Log1 log1;

//"120.78.126.148"点分十进制字符串风格的IP地址
void Usage(string proc)
{
    cout<< "\n\rUsage" << proc << "port[1024+]\n" << endl;
}

//处理字符串----udpServer.hpp里的fun_t
string Handler(const string& str)
{
    //这里收到客户发来的请求
    string res = "Server get a message: ";
    res += str;
    return res;
}

// bool SafeCheck(const std::string &cmd)
// {
//     int safe = false;
//     std::vector<std::string> key_word = {
//         "rm",
//         "mv",
//         "cp",
//         "kill",
//         "sudo",
//         "unlink",
//         "uninstall",
//         "yum",
//         "top",
//         "while"
//     };
//     for(auto &word : key_word)
//     {
//         auto pos = cmd.find(word);
//         if(pos != std::string::npos) return false;
//     }

//     return true;
// }

// std::string ExcuteCommand(const std::string &cmd)
// {
//     std::cout << "get a request cmd: " << cmd << std::endl;
//     if(!SafeCheck(cmd)) return "Bad man";

//     FILE *fp = popen(cmd.c_str(), "r");
//     if(nullptr == fp)
//     {
//         perror("popen");
//         return "error";
//     }
//     std::string result;
//     char buffer[4096];
//     while(true)
//     {
//         char *ok = fgets(buffer, sizeof(buffer), fp);
//         if(ok == nullptr) break;
//         result += buffer;
//     }
//     pclose(fp);

//     return result;
// }

//./udpServer+port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpServer> svr(new UdpServer(port));//云服务器IP
    svr->Init();
    svr->Run(Handler);

    return 0;
}

makefile

.PHONY:all
all:udpServer udpClient
udpServer:main.cc
	g++ -o $@ $^ -std=c++11
udpClient:udpClient.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f udpServer udpClient

 多台机器网络信息传递测试

先编译一下程序,再进行如图所示操作

919f897b73df429aa3453fbb3fe7f17f.png

然后把这个文件在另一台电脑打开

0a236f6da2494c86894920838bce5b2c.png

再在另一个电脑上打开云服务器创建一个目录,rz一个把文件加入目录

0f217bcc7ce44eb19aa79a4c3505e630.png

默认是不可执行的

ee4ad5565c2e4f41adf91390090a347b.png

+x就可以了

7bbf7a29e09549819d160f41ba26e9f1.png

然后

./udpClient IP(服务器IP) port(服务器启动时的端口号)

62cad6d655b446a7a07b2c4fd70160fd.png

此时,在自己的机器上启动服务器,在另一台机器启动客户端,并且发送信息,在服务器就可以看到了

网络处理数据和接收数据耦合度太高了

把网络通信功能和处理数据的功能进行解耦

两个同理(返回类型是string,参数类型是string&)

705345a312a6414b90ac60a8a275eceb.png

服务器外面给接收到的数据做处理 

05a0baa0feba41ec8e816cc09d367230.png

修改一下,放在外部处理,这样的意思就是希望服务器这边给服务端做什么处理

3883840adcab42c1ac78f0645f1e8da3.png

bedcfffd806f4aa9a645bee0e9d02d23.png

服务器收到了消息,收到了消息然后回调是的去调用传进来的func方法,把这个数据做加工,处理完之后把结果返回

#include "udpServer.hpp"
#include<memory>
using namespace std;
Log1 log1;

//"120.78.126.148"点分十进制字符串风格的IP地址
void Usage(string proc)
{
    cout<< "\n\rUsage" << proc << "port[1024+]\n" << endl;
}

//处理字符串----udpServer.hpp里的fun_t
string Handler(const string& str)
{
    //这里收到客户发来的请求
    string res = "Server get a message: ";
    res += str;
    return res;
}

//./udpServer+port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpServer> svr(new UdpServer(port));//云服务器IP
    svr->Init();
    svr->Run(Handler);

    return 0;
}
#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "log.hpp"
using namespace std;

using fun_t = function<string(const string&)>;
//typedef function<string(const string&)> fun_t;

extern Log1 log1;//日志器声明

enum{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int Size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = defaultport, const string& ip = defaultip):sockfd_(0), 
                                    port_(port), ip_(ip), isrunning_(false)
    {}
    void Init()
    {
        //创建udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            log1(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCKET_ERR);
        }
        log1(Info, "socket create success, sockfd:%d", sockfd_);
        //2.bind socket(绑定端口号)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  uint32_t必须是网络序列

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local))< 0)
        {
            log1(Fatal, "bind error, error:%d, err string:%s", errno, strerror(errno));   
            exit(BIND_ERR);
        }
        log1(Info, "bind success, error:%d, err string:%s", errno, strerror(errno));   
    }
    void Run(fun_t func)//加了fun_t func本质是对代码进行分层
    {
        isrunning_ = true;
        //const int size = 1024;
        char inbuffer[Size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //收到数据
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                log1(Warning, "recvfrom error, error:%d, err string:%s", errno, strerror(errno));   
                continue;
            }
            //收到了一个数据
            inbuffer[n] = 0;

            //充当了一次数据的处理
            //处理数据放在外面去处理
            string info = inbuffer;
            //string echo_string = "sever echo#" + info;
            string echo_string = func(info);//在外面去处理
            //cout<< echo_string <<endl;

            //发送回给客户端
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0)
        close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_;//表明服务器进程端口号
    string ip_;
    bool isrunning_;
};

如果发来的是命令呢

跟我们平时链接xshellf发命令类似

POPEN

13de6b82158f4a4abfb17810b21724d4.png

#include "udpServer.hpp"
#include<memory>
using namespace std;
Log1 log1;

//"120.78.126.148"点分十进制字符串风格的IP地址
void Usage(string proc)
{
    cout<< "\n\rUsage" << proc << "port[1024+]\n" << endl;
}

//处理字符串----udpServer.hpp里的fun_t
string Handler(const string& str)
{
    //这里收到客户发来的请求
    string res = "Server get a message: ";
    res += str;
    return res;
}

// bool SafeCheck(const std::string &cmd)
// {
//     int safe = false;
//     std::vector<std::string> key_word = {
//         "rm",
//         "mv",
//         "cp",
//         "kill",
//         "sudo",
//         "unlink",
//         "uninstall",
//         "yum",
//         "top",
//         "while"
//     };
//     for(auto &word : key_word)
//     {
//         auto pos = cmd.find(word);
//         if(pos != std::string::npos) return false;
//     }

//     return true;
// }

// std::string ExcuteCommand(const std::string &cmd)
// {
//     std::cout << "get a request cmd: " << cmd << std::endl;
//     if(!SafeCheck(cmd)) return "Bad man";

//     FILE *fp = popen(cmd.c_str(), "r");
//     if(nullptr == fp)
//     {
//         perror("popen");
//         return "error";
//     }
//     std::string result;
//     char buffer[4096];
//     while(true)
//     {
//         char *ok = fgets(buffer, sizeof(buffer), fp);
//         if(ok == nullptr) break;
//         result += buffer;
//     }
//     pclose(fp);

//     return result;
// }

//./udpServer+port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpServer> svr(new UdpServer(port));//云服务器IP
    svr->Init();
    svr->Run(Handler);

    return 0;
}

本地环回127.0.0.1

本地环回主要用于客户端和服务端的测试

测试,并绑定127.0.0.1的ip地址

本地环回地址(Loopback Address)在网络技术中指的是一种特殊的IP地址,用于网络软件测试以及本地机器上的服务访问,而不会将数据包发送到本地机器之外的任何地方。最常用的本地环回地址是 127.0.0.1,它对应于IPv4协议,而在IPv6中,环回地址通常表示为 ::1

以下是本地环回地址在网络中的含义和用途:

  1. 自我测试

    • 环回地址允许网络应用程序在不涉及物理网络接口的情况下发送和接收数据包。这对于测试网络软件非常有用,因为它不需要真实的网络连接。
  2. 服务本地访问

    • 服务器应用程序可以在本地机器上运行并通过环回地址进行访问,这允许开发者在服务部署到生产环境之前在本地进行开发和测试。
  3. 不占用网络带宽

    • 通过环回地址发送的数据不会离开主机,因此不会占用任何网络带宽。
  4. 系统内部通信

    • 操作系统内部的不同服务和应用程序可以通过环回地址进行通信,而无需通过网络接口。
  5. 配置和诊断工具

    • 网络配置和诊断工具经常使用环回地址来检查网络堆栈是否正常工作。
  6. 安全

    • 因为数据不会离开主机,所以使用环回地址进行通信被认为是安全的,不会暴露给外部网络。
  7. 标准化

    • 环回地址是网络协议标准的一部分,几乎所有的TCP/IP实现都支持环回地址。

当数据包被发送到环回地址时,操作系统网络堆栈会立即将该数据包返回给发送者,而不进行任何网络传输。这个过程不涉及物理网络接口,因此不会产生任何网络流量。尽管环回地址看起来像是网络中的一个“虚拟”接口,但在网络协议栈中,它与任何其他网络接口一样被处理。

 客户端怎么知道服务器ip地址和端口号呢,日常生活中是不会知道的,端口号是固定的,约定好的,服务器也要遵守这样的规则,把服务器ip地址拿过来,也就是云服务器的ip(101.34.66.193)(需要开放端口)5dc7ea4203ca4b529dd2ba0faeeb5179.png

6ab8d3ca4a604fe0bd999e350c8dfdfa.png

(无法使用要用127.0.0.1)

37df3e5955794755909f9b17bac768ba.png

84b2360d3ba142ac920ad7aee14774b7.png

在服务器查看发信的客户端的端口号和IP

网络转主机拿到端口号和IP

            uint16_t port = ntohs(client.sin_port);

网络转主机就可以打印出来了d55c0255addb4b5a96e9f51fe9a67ac6.png

13da7e54168d49908fc96d7661c6eaeb.png

95c05c683e614a5094a913a891808961.png

改变一下处理数据方式,打印出IP和port

83366a71a18245a28105445d70b4c731.png

6151b051ba774cd293414d6edd48b1b7.png

代码示例:

server

#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"
using namespace std;

using fun_t = function<string(const string&, string &, uint16_t)>;
//typedef function<string(const string&)> fun_t;

extern Log1 log1;//日志器声明

enum{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int Size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = defaultport, const string& ip = defaultip):sockfd_(0), 
                                    port_(port), ip_(ip), isrunning_(false)
    {}
    void Init()
    {
        //创建udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            log1(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCKET_ERR);
        }
        log1(Info, "socket create success, sockfd:%d", sockfd_);
        //2.bind socket(绑定端口号)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  uint32_t必须是网络序列

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local))< 0)
        {
            log1(Fatal, "bind error, error:%d, err string:%s", errno, strerror(errno));   
            exit(BIND_ERR);
        }
        log1(Info, "bind success, error:%d, err string:%s", errno, strerror(errno));   
    }

    //检查用户是不是新用户
    void CheckUser(const struct sockaddr_in& client)
    {

    }

    void Run(fun_t func)//加了fun_t func本质是对代码进行分层
    {
        isrunning_ = true;
        //const int size = 1024;
        char inbuffer[Size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //收到数据
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                log1(Warning, "recvfrom error, error:%d, err string:%s", errno, strerror(errno));   
                continue;
            }
            //接收端口号和ip
            uint16_t clientport = ntohs(client.sin_port);
            string clientip = inet_ntoa(client.sin_addr);


            CheckUser(client);

            //收到了一个数据
            inbuffer[n] = 0;

            //充当了一次数据的处理
            //处理数据放在外面去处理
            string info = inbuffer;
            //string echo_string = "sever echo#" + info;
            string echo_string = func(info, clientip, clientport);//在外面去处理  
            cout<< echo_string <<endl;  

            //发送回给客户端
            sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0)
        close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_;//表明服务器进程端口号
    string ip_; //任意地址 0
    bool isrunning_;
    unordered_map<string, struct sockaddr_t> online_user;
};

main.cc

#include "udpServer.hpp"
#include<memory>
using namespace std;
Log1 log1;

//"120.78.126.148"点分十进制字符串风格的IP地址
void Usage(string proc)
{
    cout<< "\n\rUsage" << proc << "port[1024+]\n" << endl;
}

//处理字符串----udpServer.hpp里的fun_t
string Handler(const string& info, string &clientip, uint16_t clientport)
{
    cout<< "[clientip: " << clientip << " " << clientport << "]#" << endl;
    //这里收到客户发来的请求
    string res = "Server get a message: ";
    res += info;
    return res;
}

//./udpServer+port
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpServer> svr(new UdpServer(port));//云服务器IP
    svr->Init();
    svr->Run(Handler);

    return 0;
}

测试:

3013153250d94bf1b4a19a4bbf5bece2.png

只允许一个客户端连接/判断是不是一个新用户(kv)

检查用户是不是新用户

bc4f321c7fcc47a7a19e5e722297e2e3.png

dff1e337f7a045b2aa2f15d7bdbe9e30.png

6c4c089958f84f618e590fc8f5901c5d.png

4fd111e7084f427ba5ac96171960cdf7.png

代码

#pragma once
#include <iostream>
#include <string>
#include <strings.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <unordered_map>
#include "log.hpp"
using namespace std;

using fun_t = function<string(const string&, string &, uint16_t)>;
//typedef function<string(const string&)> fun_t;

extern Log1 log1;//日志器声明

enum{
    SOCKET_ERR = 1,
    BIND_ERR
};

uint16_t defaultport = 8080;
string defaultip = "0.0.0.0";
const int Size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t& port = defaultport, const string& ip = defaultip):sockfd_(0), 
                                    port_(port), ip_(ip), isrunning_(false)
    {}
    void Init()
    {
        //创建udp socket
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if(sockfd_ < 0)
        {
            log1(Fatal, "socket create error, sockfd:%d", sockfd_);
            exit(SOCKET_ERR);
        }
        log1(Info, "socket create success, sockfd:%d", sockfd_);
        //2.bind socket(绑定端口号)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);//需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str());//1.string -> uint32_t  uint32_t必须是网络序列

        if(bind(sockfd_, (const struct sockaddr*)&local, sizeof(local))< 0)
        {
            log1(Fatal, "bind error, error:%d, err string:%s", errno, strerror(errno));   
            exit(BIND_ERR);
        }
        log1(Info, "bind success, error:%d, err string:%s", errno, strerror(errno));   
    }

    //检查用户是不是新用户
    void CheckUser(const struct sockaddr_in& client, string &clientip, uint16_t clientport)
    {

        auto iter = online_user_.find(clientip);//kv结构检查在不在
        if(iter == online_user_.end())//如果不在,那就添加
        {
            online_user_.insert({clientip, client});
        }
        cout<< "[clientip: " << clientip << " " << clientport << "] add to online user" << endl;
    }

    //广播给所有人,所有人都在unordered_map里
    void Broadcast(const string &info, string &clientip, uint16_t clientport)
    {
        for(auto& user : online_user_)
        {

            string message = "[clientip:";
            message += clientip;
            message += " ";
            message += clientport;
            message += "]#";
            message += info;
            socklen_t len = sizeof(user.second); 
            sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
        }
    }
    
    void Run(/*fun_t func*/)//加了fun_t func本质是对代码进行分层
    {
        isrunning_ = true;
        //const int size = 1024;
        char inbuffer[Size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            //收到数据
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if(n < 0)
            {
                log1(Warning, "recvfrom error, error:%d, err string:%s", errno, strerror(errno));   
                continue;
            }
            //接收端口号和ip
            // uint16_t clientport = ntohs(client.sin_port);
            // string clientip = inet_ntoa(client.sin_addr);
            uint16_t clientport = ntohs(client.sin_port);
            string clientip = inet_ntoa(client.sin_addr);

            CheckUser(client, clientip, clientport);

            //把消息广播给所有人
            string info = inbuffer;
            Broadcast(info, clientip, clientport);

            // //收到了一个数据
            // inbuffer[n] = 0;

            // //充当了一次数据的处理
            // //处理数据放在外面去处理
            // string info = inbuffer;
            // //string echo_string = "sever echo#" + info;
            // string echo_string = func(info, clientip, clientport);//在外面去处理  
            // cout<< echo_string <<endl;  

            // //发送回给客户端
            // sendto(sockfd_, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&client, len);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0)
        close(sockfd_);
    }
private:
    int sockfd_;//网络文件描述符
    uint16_t port_;//表明服务器进程端口号
    string ip_; //任意地址 0
    bool isrunning_;
    unordered_map<string, struct sockaddr_in> online_user_;
};

客户端整改

#include <iostream>
#include <sys/types.h>          
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
using namespace std;
void Usage(string proc)
{
    cout<< "\n\rUsage:" << proc << "severip severport\n" << endl;
}

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

    //                  IPv4      用户数据报
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);//长度


    if(sockfd < 0)
    {
        cout<< "socker error" << endl;
        return 1;
    }
    string message;
    char buffer[1024];
    while(true)
    {
        cout<< "Please Enter@ "; 
        getline(cin, message);//空格不会作为结束标识符
        //cout<< message <<endl;

        //1.数据  2.发给谁
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            cout<< buffer << endl;
        }

    }

    close(sockfd);
    return 0;
}

登录qq时,不发送信息也能收到信息,但目前的客户端没法做到

会在getline阻塞住

发送完再回收到,就又在getline阻塞了

cdb648adb1074feb8c0ec5bdd8829905.png

客户端改成多线程,一个线程输入一个线程显示

udp的sockfd是可以同时被读写的

是线程安全的

 983f9c94299e496b9d17ac15e6a5bebf.png

8690bddac3ec495f808eb5955ab10b35.png

bb888ba0845240fc8f1fa9fa16411dff.png

abd5fccd76294417aeb34d94e0ef1fc1.png

a4f075b63ef74c32ad0b1b2427fbd293.png

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <pthread.h>
using namespace std;
void Usage(string proc)
{
    cout << "\n\rUsage:" << proc << "severip severport\n"
         << endl;
}

//线程的数据
struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
};

// 收
void *recv_message(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);//安全的类型转换
    char buffer[1024];    
    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }
}

// 发 就要知道客户端的套接字信息 
void *send_message(void *args)
{
    ThreadData* td = static_cast<ThreadData*>(args);//安全的类型转换
    string message;
    socklen_t len = sizeof(td->server); // 长度

    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message); // 空格不会作为结束标识符
        // cout<< message <<endl;

        // 1.数据  2.发给谁
        sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
    }
}

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

    //创建线程的数据 对象
    struct ThreadData td;

    //                  IPv4      用户数据报
    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());

    // 创建一个收线程一个发线程
    pthread_t recvr, sender;
    pthread_create(&recvr, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    if (td.sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }


    pthread_join(recvr, nullptr);
    pthread_join(sender,nullptr);

    close(td.sockfd);
    return 0;
}

思路和测试结果73db3ddc08824520bf6c6fb12cac1d0e.png

制作简单的聊天室类似群聊

首先查看自己打开的几个终端

正好3个

上面的这个终端是0

这是1

这是2

将客户端的收消息,用标准错误显示出来,文件描述符是2

覆盖问题

这个是因为是在往/dev/pts/4这个里面写入,你可以将其理解为一个文件,本来文件中有一行内容为aaaaaaaa,每次显示,然后现在再从头输入bbbb,这个bbbb覆盖bbbbaaaa,还是会输出bbbbaaaa

;