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

news2024/11/27 19:49:50

目录

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网络通信(多个版本)。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用阿里云Logstash,从自建es6.2.4迁移数据至云ES7.10测试

一、购买阿里云Logstash服务 4C16G 40G 高效云盘 二、配置管道 2.1 配置启动管道前置工作 logstash要通源端ES网络及http端口,且加白logstash要通目标端ES网络及http端口,且加白目标端ES加配置自动创建索引配置管道前先安装 logstash-output-file_extend插件目标云ES7.10要…

Java 等后端应用如何获取客户端真实IP —— 筑梦之路

需求说明 现有一套Java开发的应用&#xff0c;需要能获取到用户访问的真实IP地址&#xff0c;以此来过滤到一些不安全的因素。而实际部署的场景中Java服务提供给用户访问需要经过多次代理&#xff0c;默认情况下是无法获取到客户端真实IP地址的&#xff0c;因此要实现该需求&a…

如何实现SSL证书自动部署、自动续费、自动更新?看这里~

SSL证书默认有效期默认为1年&#xff0c;部分免费证书的默认有效期为3个月。您必须在证书到期前的30个自然日内续费并更新证书&#xff0c;才能延长证书的服务时长。证书续费时&#xff0c; 会颁发一个新的证书&#xff0c;您收到新证书后需要手工更新到服务器上。部分证书支持…

乡村电商人才齐聚浙江建德,这场农播氛围值已拉满!

“3、2、1&#xff0c;上链接!” “现场营造了很好的交流氛围&#xff0c;碰撞出了不少合作机会。” “农播让我们有机会为家乡农产品代言&#xff0c;并且通过电商平台&#xff0c;把优质农特产品卖到全国各地。” “就像是一个演员需要一个舞台&#xff0c;一个好产品也需…

[Linux] yum仓库相关

一、yum仓库 1.1 yum简介 yum 是一种基于 RPM 软件包&#xff08;Red-Hat Package Manager 的缩写&#xff09;的软件更新机制&#xff0c;可自动解决软件包之间的依赖关系。这就解决了日常工作中花费大量时间寻找安装包的问题。 为什么会出现依赖 linux 本身就有简化系统的优…

华为鸿蒙开发记录

错误 1No module found. Make sure the project sync is completed successfully and the module is set in Edit Configuration > General 应该是项目建立的是Api是9 &#xff0c;但是 华为远程模拟器是应该建立的是 8的&#xff0c;导致 版本过低。从新建立项目选择APi8就…

前端学习笔记--React

1. 什么是React? React 是一个用于构建用户界面的JavaScript库核心专注于视图,目的实现组件化开发我们可以很直观的将一个复杂的页面分割成若干个独立组件,每个组件包含自己的逻辑和样式 再将这些独立组件组合完成一个复杂的页面。 这样既减少了逻辑复杂度&#xff0c;又实现…

基于vue 2.0的H5页面中使用H5自带的定位,高德地图定位,搜索周边商户,覆盖物标记,定位到当前城市

基于vue的H5页面中使用高德地图定位&#xff0c;搜索周边商户&#xff0c;覆盖物标记 首先安装高德地图插件 npm i amap/amap-jsapi-loader --save地图承载容器 <template><div id"container"></div> </template>地图容器样式 <style…

《QT从基础到进阶·三十四》qobject_cast动态强制转换

qobject_cast()对QObject类执行动态强制转换。 qobject_cast()函数的行为类似于标准c dynamic_cast()&#xff0c;但执行速度比dynamic_cast 更快&#xff0c;且不需要C的RTTI 的支持&#xff0c;但qobject_cast 仅适用于QObject 及其派生类。 如果对象的类型正确(在运行时确定…

Git客户端(TortoiseGit)使用

参考文章&#xff1a; https://www.cnblogs.com/xuwenjin/p/8573603.html 【精选】使用TortoiseGit工具进行开发&#xff08;连接远程仓库进行克隆、拉取、获取、提交、推送、新建/切换/合并分支、解决冲突&#xff09;_tortoisegit连接远程仓库-CSDN博客 tortoise git 拉取…

网络运维Day18

文章目录 环境准备导入数据确认表导入成功练习用表解析表格结构设计 查询语句进阶什么是MySQL函数常用功能函数数学计算流程控制函数查询结果处理 连接查询(联表查询)表关系什么是连接查询连接查询分类笛卡尔积内连接(INNER)外连接 子查询什么是子查询子查询出现的位置子查询练…

Element UI 偶发性图标乱码问题

1. 问题如图所示 2. 原因&#xff1a;sass版本低 sass: 1.26.8 sass-loader: 8.0.2 3. 解决方法 (1) 提高sass版本 (2) 在vue.config.js中添加配置 css: {loaderOptions: {sass: {sassOptions: {outputStyle: expanded}}}},4. 遇到的问题 升级后打包&#xff0c;报错 Syntax…

自动化网络图软件

由于 IT 系统的发展、最近向混合劳动力的转变、不断变化的客户需求以及其他原因&#xff0c;网络监控变得更加复杂。IT 管理员需要毫不费力地可视化整个网络基础设施&#xff0c;通过获得对网络的可见性&#xff0c;可以轻松发现模式、主动排除故障、确保关键设备可用性等。 为…

半平面求交 - 洛谷 - P3256 [JLOI2013] 赛车

欢迎关注更多精彩 关注我&#xff0c;学习常用算法与数据结构&#xff0c;一题多解&#xff0c;降维打击。 往期相关背景点击前往 题目大意 题目链接 https://www.luogu.com.cn/problem/P3194 有一场赛车比赛&#xff0c;每辆车有自己的起位置和速度&#xff0c;比赛时间无…

目标检测一 SSD代码复现

SSD 背景 这是一种 single stage 的检测模型&#xff0c;相比于R-CNN系列模型上要简单许多。其精度可以与Faster R-CNN相匹敌&#xff0c;而速度达到了惊人的59FPS&#xff0c;速度上完爆 Fster R-CNN。 速度快的根本原因在于移除了 region proposals 步骤以及后续的像素采样或…

《持续交付:发布可靠软件的系统方法》- 读书笔记(十五)

持续交付&#xff1a;发布可靠软件的系统方法&#xff08;十五&#xff09; 第 15 章 持续交付管理15.1 引言15.2 配置与发布管理成熟度模型15.3 项目生命周期15.3.1 识别阶段15.3.2 启动阶段15.3.3 初始阶段15.3.4 开发与发布15.3.5 运营阶段 15.4 风险管理流程15.4.1 风险管理…

微星迫击炮b660m使用intel arc a750/770显卡功耗优化方法

bios 优化: 1,开机后持续点击“delete”键直到进入微星bios。 2,点击右上角选择我们熟悉的中文。 3,点击Settings--->高级---> pcie/Pci子系统设置 4,Native PCIE Enable : Enabled Native Aspm:允许

从零开始 通义千问大模型本地化到阿里云通义千问API调用

从零开始 通义千问大模型本地化到阿里云通义千问API调用 一、通义千问大模型介绍 何为“通义千问”&#xff1f; “通义千问大模型”是阿里云推出的一个超大规模的语言模型&#xff0c;具有强大的归纳和理解能力&#xff0c;可以处理各种自然语言处理任务&#xff0c;包括但…

Xrdp+Cpolar实现远程访问Linux Kali桌面

XrdpCpolar实现远程访问Linux Kali桌面 文章目录 XrdpCpolar实现远程访问Linux Kali桌面前言1. Kali 安装Xrdp2. 本地远程Kali桌面3. Kali 安装Cpolar 内网穿透4. 配置公网远程地址5. 公网远程Kali桌面连接6. 固定连接公网地址7. 固定地址连接测试 前言 Kali远程桌面的好处在于…