网络和Linux网络_2(套接字编程)socket+UDP网络通信代码

目录

1. 预备知识

1.1 源IP地址和目的IP地址

1.2 端口号port和套接字socket

1.3 网络通信的本质

1.4 TCP和UDP协议

1.5 网络字节序

2. socket套接字

2.1 socket创建套接字

2.2 bind绑定

2.3 sockaddr结构体

3. UDP网络编程

3.1 server的初始化服务器

3.2 server的数据处理Start

3.3 客户端udp_client.cc

3.4 多线程收发数据

本篇完。


1. 预备知识

1.1 源IP地址和目的IP地址

通过上一篇我们知道,在网络通信中,存在两套地址,一套是IP地址,另一套是MAC地址。

IP地址:标识计算机在网络中的唯一性。

而IP地址又分为源IP地址和目的IP地址:

  •  源IP地址:标识网络通信发起方。
  •  目的IP地址:标识网络通信的接收方。

在网络通信的报文中,其中报头包含着源IP和目的IP


1.2 端口号port和套接字socket

如上图所示,报文从用户A的计算机传送到了用户B的计算机,但是网络通信的目的就是将报文从一台计算机传送到另一台计算机吗?

将数据从计算机A传送到计算机B是手段,并不是网络通信的目的。

真正进行通信的是用户A和用户B,也就是计算机A上的某个应用程序和计算机B上的某个应用程序之间在通信

网络通信的目的就是让两台计算机上的两个进程在进行通信。

IP地址可以标识两台计算机的唯一性,但是每台计算机上会存在大量的进程,如何保证计算机A某个进程发送的数据能让计算机B指定的进程接收到呢?

换句话说,如何标识一台计算机上进程的唯一性呢?

采用端口号port来标识计算机上进程的唯一性。

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程,告诉操作系统要把数据交给哪一个进程。
  • 一个端口号只能被一个进程占用。

现在我们有用来标识计算机在网络中唯一性的IP地址,又有用来标识进程在计算机中唯一性的端口号port。

  •  全网唯一进程 = IP地址(全网主机唯一性) + 端口号port(该主机上进程唯一性)

socket(套接字) = IP地址 + 端口号port。

所以要想两个进程间实现通信,必须各自有各自的套接字。


1.3 网络通信的本质

网络通信实际上是两台计算机或者多台计算机上的进程之间在通信,和我们之前Linux学习的进程间通信的区别在于进程位于不同的计算机上。

网络通信的本质:进程间通信。

  • 要实现进程间通信,必须有共享资源,而网络通信中的网络就是共享资源。
  • 网络通信其实就是在做IO,我们上网的所有行为就两种:①把数据发出去。 ②把数据读回来。

Linux下一切皆文件,所以网络在系统中也是一个"文件",也有struct结构体,也有文件描述符。

我们知道,每个进程都有一个pid来标识它在当前计算机上的唯一性,为什么网络中还需要一个端口号port来标识进程的唯一性呢?不能用pid吗?

在技术实现上是完全可以用pid的,所以就需要考虑为什么不用pid,用了端口号port?:

  • 系统是系统,网络是网络,系统使用pid,网络使用port来标识进程的唯一性,实现了系统与网络的解耦
  • 不是所有进程都提供网络服务或者网络请求的,但是所有的进程都需要pid,只有需要网络的进程才会分配一个port。
  • 客户端需要能够直接找到服务器的进程,服务器进程的唯一性不能做任何改变。

比如平时使用的QQ,手机上的QQ都是客户端,打开QQ使用都是在向服务器上的QQ进程发起网络请求,而这个服务器位腾讯公司,服务进程根据用户的网络请求再做出对应的反馈交给用户。

下载了某个应用程序以后,该程序里就绑定了服务端对应进程的IP地址和端口号。

所以使用应用程序的时候,就能精准的和服务端上对应的进程进行网络通信。

服务器的IP地址并不会随意变化,为了保证客户端每次都能找到服务端的进程,服务端的port也不能变化。  

如果使用pid来代替端口号的话,服务器每重启一次,服务进程的pid值就会改变,客户端就无法找到服务进程了。

绑定了port的进程PCB会被维护在一个哈希表中,port就是key值,操作系统能够根据key值找到对应的PCB,然后再执行它。


1.4 TCP和UDP协议

这两个协议的具体原理和细节在后面会详细讲解,这里仅需要大概了解一下特性即可。

UDP协议:(User Datagram Protocol 用户数据报协议)。

  • 传输层协议。
  • 不需要通信双方建立连接,直接发生即可。
  • 不可靠传输,可能会发生丢包等问题。
  • 面向数据报。

TCP协议:(Transmission Control Protocol 传输控制协议)。

  • 传输层协议。
  • 需要通信双方建立连接。
  • 是一种可靠传输,不会发生丢包等问题。
  • 面向字节流。

可靠和不可靠传输并没有相对的好坏,比如可靠传输付出的代价就比较大,具体这些特点是什么意思,后面会讲解到,这里只需要记住以上内容即可。


1.5 网络字节序

计算机分为大端机和小端机,不同的电脑型号就不一样,两台计算机大小端不同,接收到的数据解释出来意义也不同。

规定:网络中的字节序都采用大端

如果你的计算机是大端机,那么就可以直接向网络中发数据和从网络中接收数据,不用做转换。

如果你的计算机是小端机,那么在向网络中发送数据时,需要先将数据转换成大端,再发送到网络中。从网络中接收下来的数据,需要先转换成小端再使用。

此时就存在两个问题:

  • 自己的电脑是大端还是小端?还需要自己去判断一下。
  • 如果自己的电脑是小端,需要自己去将数据转换成大端。

这两个问题虽然我们自己能解决,但是比较繁琐,而且很容易出错,所以操作系统提供了相应的接口来进行大小端转换

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

#include <arpa/inet.h> // 必须包含的头文件
// 主机序列转网络序列
uint32_t htonl(uint32_t hostlong); // 将主机上unsigned int类型的数据转换成对应网络字节序
uint16_t htons(uint16_t hostshort); // 将主机上unsigned short类型的数据转换成对应网络字节序

// 网络序列转主机序列
uint32_t ntohl(uint32_t netlong); // 将从网络中读取的unsigned int类型的数据转换成当前计算机字节序
uint16_t ntohs(uint16_t netshort); // 将从网络中读取的unsigned short类型的数据转换成当前计算机字节序
  • 这些函数名很好记,h表示host,代表着主机,n表示network,代表着网络,s表示unit16_t,l表示uint32_t。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回。
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

2. socket套接字

2.1 socket创建套接字

man socket:

返回值 int

成功返回一个int类型的值,其实就是一个文件描述符sockfd。

失败返回-1,并且设置错误码errno。

socket系统调用专门用来创建套接字,在创建的时候指定使用哪种通信协议。看看参数:

int domain

这是地址族,用来指定创建的套接字进行的是网络通信还是本地通信。

该参数可以填上图所示中的任何一个,经常使用的是AF_INET表示使用IPv4的网络套接字进行网络通信

int type

这是用来指定socket提供的能力类型,比如是面向字节流还是面向用户数据报。

该参数可以使用上图中的任何一个,其中常用的是画红色框的是面向字节流和面向用户数据报,也就是TCP和UDP。

int protocol

该参数是用来指定具体的协议名的,比如指定TCP或者DUP,但是根据前两个参数就可以确定使用哪个协议了,这个一般设置为0即可。


2.2 bind绑定

man 2 bind:

bind用来将IP地址和端口号port创建的socket套接字绑定,也就是将IP地址和端口号port和系统绑定。

返回值int

成功返回0,失败返回-1,并且设置错误码errno。

int sockfd

使用socket()返回的文件描述符sockfd,用来指定绑定哪个套接字。

const struct sockaddr * addr

struct sockaddr是一个结构体。

socklen_t addrlen

这个参数是表示sockaddr结构体大小的,单位是字节,socklen_t本质是unsigned int类型的32位变量。

其它接口:

// 开始监听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);

这几个接口的是TCP协议才会用到,后面再详细讲解。


2.3 sockaddr结构体

套接字有很多种类型,常见的有三种:

  • 网络套接字:用户跨主机之间的通信,也能支持本地通信。
  • 原始套接字:可以跨过传输层(TCP/UDP)访问底层的数据。
  • 域间套接字:只能在本地通信。

这些套接字的应用场景完全不同,所以不同种类的套接字就对应一套系统调用接口,所以三套就会对应三套不同的接口。

网络套接字:

struct sockaddr_in {
    short int sin_family;           // 地址族,一般为AF_INET
    unsigned short int sin_port;    // 端口号,网络字节序
    struct in_addr sin_addr;        // IP地址
    unsigned char sin_zero[8];      // 用于填充,使sizeof(sockaddr_in)等于16
};

通过sockaddr_in结构体,将IP地址,端口号,以及网络通信AF_INET通过系统调用bind与系统绑定,从而进行网络通信。等下我们写代码用的就是sockaddr_in结构体,用之前先清零,看个接口,man bzero:

这是一个库函数,需要包含头文件<strings.h>,该函数的作用和memset一样,不同之处在于bzero只能清零,第一个参数是目标地址,第二个参数是要清零的字节数。

在填充sockaddr_in结构体的时候,将地址类型sin_family填充为AF_INET表示网络通信。

在填充端口号sin_port的时候,需要使用htons()函数,将主机字节序转换成网络字节序,然后再进行填充。


域间套接字:

struct sockaddr_un {
    sa_family_t sun_family;       /* AF_UNIX */
    char sun_path[108];    /* 带有路径的文件名 */
};

sockaddr_un只有域间通信方式AF_UNIX以及域间通信的路径名。


设计者为了方便使用,无论是网络通信还是域间通信,都使用一套接口,通过设置不同参数来解决所有通信场景。

sockaddr_insockaddr_un是用于网络通信和域间通信两个不同的通信场景,它们的区别就在于结构体起始处的16位地址类型不同,网络通信使用AF_INET,域间通信使用AF_UNIX

但由于要使用一套接口,所以此时无论哪种通信,都使用sockaddr结构体。

  • 在填充IP地址,端口号,以及地址类型的时候,仍然是对sockaddr_in进行填充。
  • 在使用bind系统调用时,将sockaddr_in强转成sockaddr类型,在函数内部它会根据前两个字节自行判断是什么类型的通信,然后再强制转回去。

可以将sockaddr看成是基类,把sockaddr_in和sockaddr_un看出是派生类,此时就构成了多态体系。


3. UDP网络编程

网络通信一定是双方的,一端是服务端(Server)接收数据,另一端是客户端(Client)发送数据。

3.1 server的初始化服务器

我们在服务端server建个server.hpp,客户端就不建头文件了,把以前的日志拷过来,先放一部分代码:

Makefile

.PHONY:all
all:udp_client udp_server

udp_client:udp_client.cc
	g++ -o $@ $^ -std=c++11 -lpthread
udp_server:udp_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f udp_client udp_server

log.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>
#include <ctime>
#include <string>

// 日志是有日志级别的
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

// 完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名)
void logMessage(int level, const char *format, ...)  // 可变参数
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) 
    {
        return;
    }
#endif
    char stdBuffer[1024]; // 标准日志部分
    time_t timestamp = time(nullptr); // 获取时间戳
    // struct tm *localtime = localtime(&timestamp); // 转化麻烦就不写了
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024]; // 自定义日志部分
    va_list args; // 提取可变参数的 -> #include <cstdarg> 了解一下就行
    va_start(args, format);
    // vprintf(format, args);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args); // 相当于ap=nullptr
    
    printf("%s%s\n", stdBuffer, logBuffer);

    // FILE *fp = fopen(LOGFILE, "a"); // 追加到文件,这里写好了就不演示了
    // fprintf(fp, "%s%s\n", stdBuffer, logBuffer);
    // fclose(fp);
}

udp_server.hpp(建议复制到VSCode跟着注释看)

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include "log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") 
        : _port(port)
        , _ip(ip)
        , _sock(-1)
    {}

    bool initServer() // 初始化服务器
    {
        // 从这里开始,就是新的系统调用,来完成网络功能
        // 1. 创建套接字(返回值是文件描述符(套接字))
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0
        if (_sock < 0) // 创建套接字失败,打印日志并退出
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 2. udp -> bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
        // "192.168.110.132" -> 点分十进制字符串风格的IP地址 -> 给用户看的
        // 上面每一个区域取值范围是[0-255]: 1字节 -> 4个区域,理论上,表示一个IP地址,其实4字节就够了
        // 点分十进制字符串风格的IP地址 <-互相转化-> 4字节

        struct sockaddr_in local; // -> 四个字段,有一个字段清零了不用管了
        bzero(&local, sizeof(local)); // 清零结构体
        local.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数

        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
        local.sin_port = htons(_port); // 考虑大小端 -> 主机序列转成网络序列,短整数

        // 1. 同上,先要将点分十进制字符串风格的IP地址 ->  转成4字节
        // 2. 4字节主机序列 -> 转成网络序列
        // 有一套接口,可以一次帮我们做完这两件事情,,让服务器在工作过程中,可以从任意IP中获取数据->inet_addr
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {   // bind: 将ip+prot和进程强关联,参数:套接字 + 清零的结构体 + 结构体字段的长度
            logMessage(FATAL, "%d:%s", errno, strerror(errno)); // 小于零绑定失败就打日志和退出
            exit(3);
        }

        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        return true;
    }

    void Start()
    {}

    ~UdpServer()
    {}

protected:
    uint16_t _port; // 一个服务器,一般必须需要ip地址和port(16位的整数)
    std::string _ip;
    int _sock;
};

#endif

udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc) // usage:使用手册,proc:程序名称
{
    std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}

// 运行服务端的方式 ./udp_server ip port // 云服务器的问题 bug
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));
    
    svr->initServer();
    svr->Start();
    return 0;
}

udp_client.cc

#include <iostream>

int main()
{
    return 0;
}

此时就能运行起来了。


3.2 server的数据处理Start

在预备工作做好以后,还需启动服务器,服务器进程是一个常驻内存的进程,也就是一个while(1)的死循环,在这个循环中进行网络数据的接收,处理,以及写回数据。

看看几个用到的接口,man recvfrom:

上图所示的系统调用recvfrom()用来接收网络中发过来的数据,也就是从套接字中接收。

  • 第一个参数是sockfd,是创建套接字时返回的文件描述符fd
  • 第二个参数buf是用来存储从网络中读取下来的数据的缓冲区。
  • 第三个参数是buf缓冲区的大小。
  • 第四个参数flags是读取的方式,一般设置为0,即阻塞读取数据。
  • 第五个参数sockaddr* src_addr是一个输出型参数,同样传参sockaddr_in结构体,系统会自动对这个结构体进行填充,可以获取数据的来源,包括发送方的地址类型,端口号port以及IP地址。
  • 返回值ssize_t,返回读取到的数据个数,单位是字节,如果读取失败则返回-1。

sendto() 函数是向服务器主机发送数据的:

man sendto:

  • 第一个参数sockfd是创建的套接字的文件描述符。
  • 第二个参数buf是要发送的数据所在的缓冲区。
  • 第三个参数len是要发生的数据个数,以字节为单位。
  • 第四个参数flags是发送方式,一般设置为0,表示阻塞发送。
  • 第五个参数dest_addr是存放服务器IP地址和端口号port的sockaddr_in结构体变量,在传参的时候需要强转为struct sockaddr*
  • 第六个参数,是第五个参数中结构体变量的大小,以字节为单位。

上面udp_server.hpp的Start函数:

    void Start()
    {
        // 作为一款网络服务器,永远不退出的
        // 服务器启动-> 常驻进程 -> 永远在内存中存在,除非挂了 -> 小心内存泄漏
        // 目前类似echo server: client给我们发送消息,我们原封不动返回
        char buffer[SIZE];
        while(true)
        {
            //  注意:peer,纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            // 输入: peer 缓冲区大小
            // 输出: 实际读到的peer
            socklen_t len = sizeof(peer);
            // start. 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 读取数据成功
            {
                buffer[s] = 0; // 先不考虑协议问题,目前数据当做字符串
                // 1. 输出发送的数据信息
                // 2. 输出是谁发送的信息
                uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的 -> 网络序列转成主机序列
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);
            }
            // 分析和处理数据,TODO
            // end. 写回数据,类似recvfrom,后两个参数是把数据写给谁
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }

编译运行:

使用指令netstat -nuap可以查看当前服务器上的网络进程,就看见有个17602的服务器运行了。

至此服务端的工作就做完了,只要客户端发送数据,服务端就可以收到。


3.3 客户端udp_client.cc

这里在客户端就不写头文件封装了,根据上面基础,直接放代码:udp_client.cc:

#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static void usage(std::string proc) // usage:使用手册,proc:程序名称
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

// ./udp_client 127.0.0.1 7070
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    // client要不要bind?要,但是一般client不会显示的bind,程序员不会自己bind
    // 因为client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢?
    // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数
    server.sin_addr.s_addr = inet_addr(argv[1]); 
        // 1.先要将点分十进制字符串风格的IP地址 ->  转成4字节
        // 2. 4字节主机序列 -> 转成网络序列
    server.sin_port = htons(atoi(argv[2])); // 考虑大小端 -> 主机序列转成网络序列,短整数

    char buffer[1024];
    while (true)
    {
        std::cerr << "请输入你的信息# "; // 标准错误 2打印
        std::getline(std::cin, message);
        // 下面向服务器发送消息,当client首次发送消息的时候,OS会自动给client bind他的ip和port
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));

        if (message == "quit")
            break;
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo# " << buffer << std::endl;
        }
    }

    close(sock);
    return 0;
}

运行客户端程序,发送数据,可以看到,客户端新收到的数据中,端口号变了,这是因为客户端的端口号是由操作系统分配的,并不是自己指定的,所以每次运行时端口号都不一样。

(此时的ip地址已经不能乱传了,上面传的是127.0.0.1是本地环回(client和server发送数据只在本地协议栈中进行数据流动,不会把我们的数据发送到网络中本地网络服务器的测试),常常在server中一般不自己传ip,而是设置成0,可以自己改一改,或者直接传INADDR_ANY即0.0.0.0(可以接收任意ip发来的数据),你把udp_client发给其他人,再链接阿里云的ip,就能收到其他人发的信息了,这里就不演示了)

还可以改一下udp_server.hpp的Statr,改成传命令的版本:

    void Start()
    {
        // 作为一款网络服务器,永远不退出的
        // 服务器启动-> 常驻进程 -> 永远在内存中存在,除非挂了 -> 小心内存泄漏
        // 目前类似echo server: client给我们发送消息,我们原封不动返回
        char buffer[SIZE];
        while(true)
        {
            //  注意:peer,纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            // 输入: peer 缓冲区大小
            // 输出: 实际读到的peer
            socklen_t len = sizeof(peer);

            char result[256];
            std::string cmd_echo; // 读取的是指令的话
            // start. 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 读取数据成功
            {
                buffer[s] = 0; // 先不考虑协议问题,目前数据当做字符串
                // 1. 输出发送的数据信息
                // 2. 输出是谁发送的信息
                // uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的 -> 网络序列转成主机序列
                // std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);

                if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
                {
                    std::string err_message = "被禁止的指令";
                    std::cout << err_message << buffer << std::endl;
                    sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
                    continue;
                }
                FILE *fp = popen(buffer, "r");
                if (nullptr == fp)
                {
                    logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                    continue;
                }
                while (fgets(result, sizeof(result), fp) != nullptr)
                {
                    cmd_echo += result;
                }
                fclose(fp);
            }
            // 分析和处理数据,TODO
            // end. 写回数据,类似recvfrom,后两个参数是把数据写给谁
            // sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);
        }
    }

到这也和上面做点处理也能让其他人操作你的机器了。


3.4 多线程收发数据

在上面的基础上对client.cc改成多线程的,把以前写的thread.hpp拷过来:

#pragma once
#include <iostream>
#include <string>
#include <cstdio>

// typedef std::function<void* (void*)> fun_t;
typedef void *(*fun_t)(void *); // 定义函数指针->返回值是void*,函数名是fun_t,参数是void*->直接用fun_t

class ThreadData // 线程数据
{
public:
    void *_args; // 真实参数
    std::string _name; // 名字
};

class Thread // 封装的线程
{
public:
    Thread(int num, fun_t callback, void *args) 
        : _func(callback) // 回调函数
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num); // 格式化到nameBuffer
        _name = nameBuffer;

        _tdata._args = args; // 线程构造时把参数和名字带给线程数据
        _tdata._name = _name;
    }
    void start() // 启动线程
    {
        pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 传入线程数据
    }
    void join() // join自己
    {
        pthread_join(_tid, nullptr);
    }
    std::string name() // 返回线程名
    {
        return _name;
    }
    ~Thread() // 析构什么也不做
    {}

protected:
    std::string _name; // 线程名字
    pthread_t _tid; // 线程tid
    fun_t _func; // 线程要执行的函数
    ThreadData _tdata; // 线程数据
};

client.cc

#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>
#include <memory>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "thread.hpp"

uint16_t serverport = 0;
std::string serverip;

static void usage(std::string proc) // usage:使用手册,proc:程序名称
{
    std::cout << "\nUsage: " << proc << " serverIp serverPort\n" << std::endl;
}

// 下面的两个接口,一个线程调用一个
// 无论是多线程读还是写,用的sock都是一个,sock代表就是文件,UDP是全双工的-> 可以同时进行收发而不受干扰
static void *udpSend(void *args) // 发送数据
{
    int sock = *(int *)((ThreadData *)args)->_args;
    std::string name = ((ThreadData *)args)->_name;

    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数
    server.sin_port = htons(serverport); // serverport和serverip是全局的
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while (true)
    {
        std::cerr << "请输入你的信息# "; //标准错误 2打印
        std::getline(std::cin, message);
        if (message == "quit")
            break;
        // 当client首次发送消息给服务器的时候,OS会自动给client bind他的IP和PORT
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof server);
    }

    return nullptr;
}

static void *udpRecv(void *args) // 接收数据
{
    int sock = *(int *)((ThreadData *)args)->_args;
    std::string name = ((ThreadData *)args)->_name;

    char buffer[1024];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout  << buffer << std::endl;
        }
    }
}

// ./udp_client 127.0.0.1 7070
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    int sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0
    if (sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }
    // // client要不要bind?要,但是一般client不会显示的bind,程序员不会自己bind
    // // 因为client是一个客户端 -> 普通人下载安装启动使用的-> 如果程序员自己bind了->
    // // client 一定bind了一个固定的ip和port,万一,其他的客户端提前占用了这个port呢?
    // // client一般不需要显示的bind指定port,而是让OS自动随机选择(什么时候做的呢?)
    // std::string message;
    // struct sockaddr_in server;
    // memset(&server, 0, sizeof(server));
    // server.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数
    // server.sin_addr.s_addr = inet_addr(argv[1]); 
    //     // 1.先要将点分十进制字符串风格的IP地址 ->  转成4字节
    //     // 2. 4字节主机序列 -> 转成网络序列
    // server.sin_port = htons(atoi(argv[2])); // 考虑大小端 -> 主机序列转成网络序列,短整数

    // char buffer[1024];
    // while (true)
    // {
    //     std::cerr << "请输入你的信息# "; // 标准错误 2打印
    //     std::getline(std::cin, message);
    //     // 下面向服务器发送消息,当client首次发送消息的时候,OS会自动给client bind他的ip和port
    //     sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr *)&server, sizeof(server));
    //     if (message == "quit")
    //         break;
    //     // 下面是接收消息
    //     struct sockaddr_in temp;
    //     socklen_t len = sizeof(temp);
    //     ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
    //     if (s > 0)
    //     {
    //         buffer[s] = 0;
    //         std::cout << "server echo# " << buffer << std::endl;
    //     }
    // }

    // 一个接收的线程,一个读取的线程
    serverport = atoi(argv[2]);
    serverip = argv[1];
    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void *)&sock));
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void *)&sock));
    sender->start();
    recver->start();

    sender->join();
    recver->join();
    close(sock);
    return 0;
}

改一下udp_server.hpp的Start,这里直接放udp_server.hpp了。

#ifndef _UDP_SERVER_HPP
#define _UDP_SERVER_HPP

#include "log.hpp"
#include <iostream>
#include <unordered_map>
#include <cstdio>
#include <string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h> // 网络四件套
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <queue>

#define SIZE 1024

class UdpServer
{
public:
    UdpServer(uint16_t port, std::string ip = "") 
        : _port(port)
        , _ip(ip)
        , _sock(-1)
    {}

    bool initServer() // 初始化服务器
    {
        // 从这里开始,就是新的系统调用,来完成网络功能
        // 一. 创建套接字(返回值是文件描述符(套接字))
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 域 + 类型 + 0
        if (_sock < 0) // 创建套接字失败,打印日志并退出
        {
            logMessage(FATAL, "%d:%s", errno, strerror(errno));
            exit(2);
        }

        // 二. udp -> bind: 将用户设置的ip和port在内核中和我们当前的进程强关联
        // "192.168.110.132" -> 点分十进制字符串风格的IP地址 -> 给用户看的
        // 上面每一个区域取值范围是[0-255]: 1字节 -> 4个区域,理论上,表示一个IP地址,其实4字节就够了
        // 点分十进制字符串风格的IP地址 <-互相转化-> 4字节

        struct sockaddr_in local; // -> bind的第二个参数,四个字段,有一个字段清零了不用管了
        bzero(&local, sizeof(local)); // 清零结构体
        local.sin_family = AF_INET; // 协议解锁->AF_INET上面sock的第一个参数

        // 服务器的IP和端口未来也是要发送给对方主机的 -> 先要将数据发送到网络
        local.sin_port = htons(_port); // 考虑大小端 -> 主机序列转成网络序列,短整数

        // 1. 同上,先要将点分十进制字符串风格的IP地址 ->  转成4字节
        // 2. 4字节主机序列 -> 转成网络序列
        // 有一套接口inet_addr,可以一次帮我们做完这两件事情,让服务器在工作过程中,可以从任意IP中获取数据->任意IP:INADDR_ANY
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        if (bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {   // bind: 将ip+prot和进程强关联,参数:套接字 + 清零的结构体 + 结构体字段的长度
            logMessage(FATAL, "%d:%s", errno, strerror(errno)); // 小于零绑定失败就打日志和退出
            exit(3);
        }

        logMessage(NORMAL, "init udp server done ... %s", strerror(errno));
        return true;
    }

    void Start()
    {
        // 作为一款网络服务器,永远不退出的
        // 服务器启动-> 常驻进程 -> 永远在内存中存在,除非挂了 -> 小心内存泄漏
        // 目前类似echo server: client给我们发送消息,我们原封不动返回
        char buffer[SIZE];
        while(true)
        {
            //  注意:peer,纯输出型参数
            struct sockaddr_in peer;
            bzero(&peer, sizeof(peer));
            // 输入: peer 缓冲区大小
            // 输出: 实际读到的peer
            socklen_t len = sizeof(peer);

            char result[256];
            char key[64]; // key存ip和port
            std::string cmd_echo; // 读取的是指令的话
            // start. 读取数据
            ssize_t s = recvfrom(_sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0) // 读取数据成功
            {
                buffer[s] = 0; // 先不考虑协议问题,目前数据当做字符串
                // 1. 输出发送的数据信息
                // 2. 输出是谁发送的信息
                uint16_t cli_port = ntohs(peer.sin_port); // 从网络中来的 -> 网络序列转成主机序列
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 4字节的网络序列的IP->本主机的字符串风格的IP,方便显示
                // printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer);

                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 127.0.0.1-8080
                logMessage(NORMAL, "key: %s", key);
                auto it = _users.find(key);
                if (it == _users.end())
                {
                    logMessage(NORMAL, "add new user : %s", key);
                    _users.insert({key, peer});
                }

                // // 下面是指令版本
                // if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
                // {
                //     std::string err_message = "被禁止的指令";
                //     std::cout << err_message << buffer << std::endl;
                //     sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr *)&peer, len);
                //     continue;
                // }
                // FILE *fp = popen(buffer, "r");
                // if (nullptr == fp)
                // {
                //     logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
                //     continue;
                // }
                // while (fgets(result, sizeof(result), fp) != nullptr)
                // {
                //     cmd_echo += result;
                // }
                // fclose(fp);
            }

            // 分析和处理数据
            // end. 写回数据,类似recvfrom,后两个参数是把数据写给谁
            // sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            // sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len);

            for (auto &iter : _users)
            {
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 此时消息就类似:127.0.0.1-1234# 你好
                logMessage(NORMAL, "push message to %s", iter.first.c_str());
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0, (struct sockaddr*)&(iter.second), sizeof(iter.second));
            }
        }
    }

    ~UdpServer()
    {
        if (_sock >= 0)
            close(_sock);
    }

protected:
    uint16_t _port; // 一个服务器,一般必须需要ip地址和port(16位的整数)
    std::string _ip;
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _users; // first存ip和prot,second存消息
    std::queue<std::string> messageQueue; // 用户层与网络的解耦->多线程->生产者消费者模型(这里就不改了)
};

#endif

顺便把udp_server.cc放出来:

#include "udp_server.hpp"
#include <memory>
#include <cstdlib>

static void usage(std::string proc) // usage:使用手册,proc:程序名称
{
    std::cout << "\nUsage: " << proc << " ip port\n" << std::endl;
}

// 运行服务端的方式 ./udp_server ip port // 云服务器的问题 bug
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    std::string ip = argv[1];
    uint16_t port = atoi(argv[2]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port, ip));
    
    svr->initServer();
    svr->Start();
    return 0;
}

编译运行:(左边是服务端,中上是A用户发的消息,右上是A用户收到的消息,中下是B用户发的消息,右下是B用户收到的消息)

如果你把client发给其他人,就能实现类似群聊的效果了。


本篇完。

加上代码两万多字了,不过放的代码有重复的,可以自己试着敲一下。

下一篇:网络和Linux网络_3(套接字编程)TCP网络通信(多个版本)。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java小游戏之——贪吃蛇

今天详细讲解写贪吃蛇的遇到的问题 代码&#xff1a; Main类 GrameStart类 GamePanel类 启动main方法 在写贪吃蛇的时候&#xff0c;我接触到了两个新东西&#xff1a; 1.定时器Timer类。 2.paint&#xff08;&#xff09;绘图方法。第一次出现在java.awt.Component类中&…

云性能监控的五大重要性

在当今数字化时代&#xff0c;企业越来越依赖云服务来支持其IT基础设施和业务运营。为了确保这些云服务的稳定性和性能&#xff0c;云性能监控变得至关重要。本文将探讨云性能监控的重要性。 一、实时可见性 云性能监控提供了对云基础设施和应用程序性能的实时可见性。这意味着…

探索arkui(2)--- 布局(列表)--- 1(列表数据的展示)

前端开发布局是指前端开发人员宣布他们开发的新网站或应用程序正式上线的活动。在前端开发布局中&#xff0c;开发人员通常会展示新网站或应用程序的设计、功能和用户体验&#xff0c;并向公众宣传新产品的特点和优势。前端开发布局通常是前端开发领域的重要事件&#xff0c;吸…

【LeetCode刷题-滑动窗口】--487.最大连续1的个数II

487.最大连续1的个数II 方法&#xff1a;滑动窗口 维护一个区间&#xff0c;使得该区间中只包含一个0 class Solution {public int findMaxConsecutiveOnes(int[] nums) {int n nums.length;int left 0,right 0,sum 0;int zero 0; //计算0的个数while(right < n){if(…

基于JavaWeb+SpringBoot+Vue房屋租赁系统微信小程序系统的设计和实现

基于JavaWebSpringBootVue房屋租赁系统微信小程序系统的设计和实现 源码获取入口前言主要技术系统设计功能截图Lun文目录订阅经典源码专栏Java项目精品实战案例《500套》 源码获取 源码获取入口 前言 21世纪是信息的时代&#xff0c;是网络的时代&#xff0c;进入信息社会高速…

(C++)把字符串转换成整数

把字符串转换成整数_牛客题霸_牛客网 愿所有美好如期而遇 思路 看到这个题目我们首先应该想到的就是去处理第一个字符&#xff0c;但是第一个字符也可能是数字字符&#xff0c;所以我们需要对他单独处理&#xff0c;如果他不符合条件&#xff0c;直接return&#xff0c;符合条…

java实现钉钉机器人消息推送

项目开发中需要用到钉钉机器人发送任务状态&#xff0c;本来想单独做一个功能就好&#xff0c;但是想着公司用到钉钉机器人发送项目挺多的。所以把这个钉钉机器人抽离成一个组件发布到企业maven仓库&#xff0c;这样可以给其他同事用提高工作效率。 1.目录结构 2.用抽象类&…

C语言加密字符(ZZULIOJ1064:加密字符)

题目描述 从键盘输入一批字符&#xff0c;以结束&#xff0c;按要求加密并输出。 输入&#xff1a;从键盘输入一批字符&#xff0c;占一行&#xff0c;以结束。 输出&#xff1a;输出占一行 加密规则: 1&#xff09;所有字母均转换为小写。 2&#xff09;若是字母a到y&#xff…

系列七、JVM的内存结构【堆(Heap)】

一、概述 一个JVM实例只存在一个堆内存&#xff0c;堆内存的大小是可以手动调节的。类加载器读取了类文件后&#xff0c;需要把类、方法、常变量放到堆内存中&#xff0c;保存所有引用类型的真实信息&#xff0c;以方便执行器执行&#xff0c;堆内存分为三个部分&#xff0c;即…

EtherCAT从站EEPROM组成信息详解(3):字16-63邮箱、EEPROM信息

0 工具准备 1.EtherCAT从站EEPROM数据&#xff08;本文使用DE3E-556步进电机驱动器&#xff09;1 字10-63邮箱、EEPROM信息 1.1 字10-63组成规范 字10-63虽然包含的空间区域很大&#xff0c;但实际上仅包含引导状态下邮箱配置、标准邮箱配置、EEPROM大小、执行的SII标准版本…

相机突然断电,保存的DAT视频文件如何修复

3-7 本文主要解决因相机突然断电导致拍摄的视频文件损坏的问题。 在平常使用相机拍摄视频&#xff0c;比如用单反相机、无人机拍摄视频的时候&#xff0c;如果电池突然断电&#xff0c;或者突然炸机了&#xff0c;就非常有可能会得到一个损坏的视频文件&#xff0c;比如会产生…

网络安全(黑客技术)—高效自学

前言 前几天发布了一篇 网络安全&#xff08;黑客&#xff09;自学 没想到收到了许多人的私信想要学习网安黑客技术&#xff01;却不知道从哪里开始学起&#xff01;怎么学 今天给大家分享一下&#xff0c;很多人上来就说想学习黑客&#xff0c;但是连方向都没搞清楚就开始学习…

udp多点通信-广播-组播

单播 每次只有两个实体相互通信&#xff0c;发送端和接收端都是唯一确定的。 广播 主机之间的一对多的通信所有的主机都可以接收到广播消息(不管你是否需要)广播禁止穿过路由器&#xff08;只能做局域网通信&#xff09;只有UDP可以广播广播地址 有效网络号全是1的主机号 192.1…

卷积神经网络(CNN)多种图片分类的实现

文章目录 前期工作1. 设置GPU&#xff08;如果使用的是CPU可以忽略这步&#xff09;我的环境&#xff1a; 2. 导入数据3.归一化4.可视化 二、构建CNN网络模型三、编译模型四、训练模型五、预测六、模型评估 前期工作 1. 设置GPU&#xff08;如果使用的是CPU可以忽略这步&#…

ElasticStack日志分析平台-ES 集群、Kibana与Kafka

一、Elasticsearch 1、介绍&#xff1a; Elasticsearch 是一个开源的分布式搜索和分析引擎&#xff0c;Logstash 和 Beats 收集的数据可以存储在 Elasticsearch 中进行搜索和分析。 Elasticsearch为所有类型的数据提供近乎实时的搜索和分析&#xff1a;一旦数据被索引&#…

Python Numpy.einsum

、 见 https://zhuanlan.zhihu.com/p/27739282

Resolume Arena 7.15.0(VJ音视频软件)

Resolume Arena 7是一款专业的实时视觉效果软件&#xff0c;用于创造引人入胜的视频演出和灯光秀。它提供了丰富多样的功能和工具&#xff0c;可以将音频、视频和图像合成在一起&#xff0c;创造出令人惊叹的视觉效果。 Resolume Arena 7支持多种媒体格式&#xff0c;包括视频文…

微信@all bug复现及原理分析

1、复现 条件&#xff1a; 1、Windows PC 端微信 2、自建一个群聊拥有群管权限可以所有人 废话不多说&#xff0c;直接上图 所有人 剪切后&#xff0c;到另一个群中&#xff0c;引用任意一个群里成员的消息&#xff0c;并将刚才剪切的粘贴至此&#xff0c;发送 便可完成非群…

LeetCode之二叉树

发现更多计算机知识&#xff0c;欢迎访问Cr不是铬的个人网站 最近数据结构学到二叉树&#xff0c;就刷了刷力扣&#xff0c;写这篇文章也是辅助记忆。 103二叉树锯齿形遍历 要解出本道题&#xff0c;首先要会层次遍历。层次遍历我们都知道用一个队列去实现就行。但是力扣这里…