• 【Linux进阶之路】Socket —— “UDP“ && “TCP“


    • 温馨提示:文章较长(代码较多),请收藏起来慢慢观看。

    一、再识网络

    1. 端口号

     在上文我们大致理解了,网络传输的基本流程和相关概念,知道ip地址可以标识唯一的一台主机,但是主机获取到信息的最终目的是为了呈现给上层的用户,即我们所熟知的抖音等APP,既然有很多的APP,具体给哪一个APP呢?

    • 说明:
    1. APP,具体指的是运行起来的程序,即一个一个的进程。
    2. 网络通信的本质是进程之间借助网卡(共享资源)进行通信。
    • 概念
    • 端口号:
    1. 指的是用于标记进程或者服务的逻辑地址
    2. 范围为0 到 65535,分有大致三类:
    1. 系统端口:系统端口范围是从 0 到 1023,这些端口通常被一些众所周知的网络服务占用,比如 HTTP(端口 80)、HTTPS(端口 443)、FTP(端口 21)、SSH(端口 22)等。通常需要root权限才能够进行使用。
    2. 注册端口:注册端口范围是从 1024 到 49151,这一范围的端口通常被一些应用程序或服务占用。普通用户也可进行使用
    3. 动态/私有端口:动态/私有端口范围是从 49152 到 65535,也被称为私有端口或暂时端口。这些端口通常被客户端应用程序使用,用于临时通信。

    疑问:既然进程的pid能标识唯一的进程,那为什么不直接捡现成的用呢?

    答: 进程pid VS 端口号:

    • 从概念上看:
    1. pid:操作系统管理进程使用。
    2. 端口号:网络通信以及为应用程序提供服务。
    3. 两者实现的解耦合的关系。
    • 从使用形式上看:
    1. pid: 进程创建时才拥有。
    2. 端口号:固定一段范围,0 到 65535。
    • 从用法来看:
    1. pid: 一个进程只能有一个pid。
    2. 端口号:一个进程能有多个端口号,为用户提供不同的服务。
    3. 联系:pid和端口号都是只能对应一个进程。且通过端口号可找到进程pid,从而找到进程。

    • 总结:
    1. 通过IP地址标识唯一的一台主机。
    2. 通过端口号标识唯一的一个进程。
    3. 进而我们可以实现网络之间的通信。

    拓展:在实际进行通信的过程中,一般是由客户端访问服务器,由服务器再提供对应的服务。

    • 说明:
    1. 客户端要想访问服务器,首先得知道服务器的ip地址和对应服务的端口号。这些工作早已经由开发人员做好,因此无需担心。
    2. 服务器的ip地址和端口号一般是不能发生变化的,否则客户端就无法访问。因为客户端的载入的服务器的端口号和ip一般是固定的。
    3. 客户端的端口号是动态变化的。这是因为多个app的开发厂商并不互通,因此可能存在端口号冲突的现象,因此要动态绑定端口号,而且这样做更加灵活,安全,高效。
    4. 服务器要对大量用户提供服务,而且用户的IP地址是随机变化的,这也间接的导致了,服务器要在"客户端做一些手脚", 即固定服务器的ip地址和端口号。

    2. 网络字节序列

     关于数据用大端还是用小端,就跟鸡蛋先吃大头还是先吃小头一样,没有实际的争论意义,因此我们看到电脑既有大端机,也有小端机。

    在这里插入图片描述

    • 说明:big - endian 为大端机的数据,little - endian为小端机的数据。
    • 速记:大同小异反着记——大 “异” 小 “同”。
    • 但是网络里面传输数据,不可能即传输大端数据也传输小端,因此规定统一在网络里面传输大端数据,到对应的主机里面再进行统一的转换,大端不用变,小端再转换一下即可。

    • 相关的接口:

    #include 
    
    uint32_t htonl(uint32_t hostlong);
    
    uint16_t htons(uint16_t hostshort);
    
    uint32_t ntohl(uint32_t netlong);
    
    uint16_t ntohs(uint16_t netshort);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 速记:h(host) to n(net) l(long),即将long类型的数据从主机转网络。其余类似。

    3.TCP 与 UDP

    传输方式:

    • TCP:面向字节流。
    • UDP:面向数据报。

    这是最本质的差别,下面我们就这两点进行分析:

    • 面向字节流
    1. 将数据视为连续的字节流进行传输和处理。发送方将数据拆分为字节流并逐个发送,接收方按照接收到的顺序重新组装数据。
    2. 提供了可靠的传输,保证数据按顺序、无差错地传输。它使用基于确认和重传的机制来确保数据的可靠性。
    3. 基于连接的通信方式,需要在发送方和接收方之间建立一个持久的连接。连接的建立和维护需要一定的开销,但可以确保数据的有序传输。
    4. 适用于需要可靠传输和有序性的应用,如文件传输、视频流传输等。

    总结:TCP协议,面向字节流,因此可靠,有连接。适合文件和视频等信息的传输。

    • 面向数据报
    1. 将数据划分为独立的数据,即数据报,每个数据报都携带了完整的信息,可以独立地发送和接收。
    2. 不保证数据的可靠性,每个数据报都是独立传输的,可能会发生丢失、重复或乱序。
    3. 无连接的通信方式,每个数据报都是独立的,不需要事先建立连接。
    4. 对实时性要求较高的应用,如实时音频、视频通信等,因为它可以提供更低的延迟。

    总结:UDP协议,面向数据报,因此不可靠,无连接。适用于对实时性要求高的应用。

    • 说明:这里的可靠和不可靠是一个中性词。不可靠意味着较低的成本,实现更加简单,可靠意味着实现需要较大的代价。因此没有谁好谁坏。

    下面我们实现是更为简单的UDP套接字。

    • 在开始之前我们先来解决一个前置问题,主要是服务器的端口问题,一般默认有些端口是禁掉的,不能用于网络之间的通信,因此我们需要开放一些端口供我们之间通信使用。
    • 实现步骤:
    1. 登录所在云服务的官网。(我的是阿里云的)
    2. 点击控制台。
    3. 点击云服务器ESC/轻量级服务器/云服务器,找到对应的云服务器。(我的是轻量级云服务器)
    4. 如果是云服务器ESC/服务器就找到安全组,点击安全组ID进行编辑即可。如果是轻量级服务器就在服务器一栏找到实例id点击,再点击防火墙进行编辑即可。
    • 具体步骤——阿里云轻量级云服务器
      • 第一步:
        在这里插入图片描述
      • 第二步:
        在这里插入图片描述
      • 第三步:
        在这里插入图片描述
      • 第四步:
        在这里插入图片描述

    二、套接字

    1.sockaddr结构

    • 这是一层抽象化的结构,设计之初是为了统一网络套接字的接口使用,是一套通用的网络套接字,而对应的具体的套接字有 网络套接字 与 域间套接字

    图解:
    在这里插入图片描述

    • 类似多态的思想,即从抽象到具体。在使用过程中我们可以通过传入通用的套接字类型,并且指定对应的套接字大小,从而说明其对应的具体类型,也就是我们说的多态。

    • 我们实现的是网络编程,使用的是:struct sockaddr_in

    • 具体结构:
      在这里插入图片描述
    1. sin_family_t sin_family; 所属家族协议类型,一般设置为AF_INT/PF_INT,即ipv4类型的协议。
    2. in_port_t sin_port; 端口号。
    3. struct in_addr sin_addr; ip地址。
    • 注意:端口号和ip地址的数据都为网络序列。

    2.UDP

    • Log.hpp(记录日志信息)
    #pragma once
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    using namespace std;
    #define SIZE (4096)
    
    //事件的等级
    #define EMRGE 1
    #define ALERK 2
    #define CRIT  3
    #define ERRO  4
    #define WARNNING 5
    #define NOTICE   6
    #define INFORE   7
    #define DEBUG    8
    #define NONE     9
    
    //输出方向
    #define DEFAULTFILE 1
    #define CLASSFILE 2
    #define SCREAN 0
    //说明:一般我们在传参时一般都是以宏的方式进行传参的,
    //如果需要打印出字符串可以用KV类型进行映射转换。
    map<int,string> Mode = {
        {1,"EMERG"},{2,"ALERK"},{3,"CRIT"},
        {4,"ERRO"},{5,"WARNING"},{6,"NOTICE"},
        {7,"INFOR"},{8,"DEBUG"},{9,"NONE"}
    };
    
    //分类文件处理的后缀。
    map<int,string> file = {
        {1,"emerg"},{2,"alerk"},{3,"crit"},
        {4,"erro"},{5,"warning"},{6,"notice"},
        {7,"infor"},{8,"debug"},{9,"none"}
    };
    class Log
    {
    public:
        void operator()(int level,const char* format,...)
        {
            //将输入的字符串信息进行输出。
            va_list arg;
            va_start(arg,format);
            char buf[SIZE];
            vsnprintf(buf,SIZE,format,arg);
            va_end(arg);
    
            //获取时间
            time_t date = time(NULL);
            struct tm* t = localtime((const time_t *)&date);
            char cur_time[SIZE] = {0};
            snprintf(cur_time,SIZE,"[%d-%d-%d %d:%d:%d]",\
            t->tm_year + 1900,t->tm_mon + 1,
            t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
    
            //输入再进行合并
            string Log = "[" + Mode[level] + "]" + \
            cur_time + string(buf) + "\n";
            
            //处理输出方向
            PrintClassFile(level,where,Log);
        }
        void PrintDefaultFILE(string& file_name,const string& mes)
        {
            int fd = open(file_name.c_str(),O_CREAT | O_WRONLY \
            | O_APPEND,0666);
            write(fd,mes.c_str(),mes.size());
            close(fd);
        }
        //将文件进行分类进行输出。
        void PrintClassFile(int level,int where,const string& mes)
        {
            if(where == SCREAN)
                cout << mes;
            else
            {
                string file_name = "./log.txt";
                if(where == CLASSFILE)
                    file_name += ("." + file[level]);
                PrintDefaultFILE(file_name,mes);
            }
        }
        void ReDirect(int wh)
        {
            where = wh;
        }
    private:
        int where = SCREAN;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    说明:在【Linux进阶之路】进程间通信有所提及,具体这个小组件是用来帮助我们显示出日志的时间,等级,出错内容等信息。

    1.server端

    • 基本框架:
    //所用容器
    #include
    #include
    
    //与内存相关的头文件
    #include
    #include
    
    //网络相关的头文件
    #include
    #include
    #include
    #include 
    
    //包装器
    #include
    
    //日志头文件
    #include "Log.hpp"
    
    //枚举常量,用于失败退出进程的退出码
    enum
    {
        SOCKET_CREAT_FAIL = 1,
        SOCKET_BIND_FAIL,
    };
    class UdpServer
    {
    public:
        UdpServer(uint16_t port,string ip)
        :_port(port),_ip(ip),_sockfd(0)
        {}
        ~UdpServer()
        {}
        void Init()
        {}
        void Run()
        {}
    private:
        int _sockfd;
        string _ip;
        uint16_t _port;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    1.1 构造函数
    1. 一般我们使用1024以上的端口号即可,此处我们默认使用8080端口。
    2. 云服务器,禁止直接绑定公网ip。
    • . 解决方法——因此我们绑定的时候使用0.0.0.0即任意ip地址绑定即可,即接收所有云服务器地址的发来的信息。
    • . 方法优点——服务可以在服务器上的所有IP地址和网络接口上进行监听,从而提供更广泛的访问范围。
    • 因此在构造函数里,我们给出两个缺省值即可。
    //全局定义:
    uint16_t default_port = 8080;
    string default_string = "0.0.0.0";
    
    //类内
    UdpServer(uint16_t port = default_port,string ip = default_string)
    :_port(port),_ip(ip),_sockfd(0)
    {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1.2 Init
    1. 创建套接字
    • 接口
    //头文件:
    #include 
    #include 
    //函数声明:
    int socket(int domain, int type, int protocol);
    /*
    参数:
    	1:指定通信域,使用AF_INT即可,即IPV4的ip地址。
    	2: SOCKET_DGRAM,即使用的套接字类型,指的是UDP类型的套接字。
    	3: 指定协议,一般设为0,根据前两个参数系统会自动选择合适的协议。
    返回值:
    	1.成功返回对应的文件描述符,网络对应的是网卡文件。
    	2.失败返回-1。
    
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 绑定套接字
    • 接口:
    //头文件:
    #include 
    #include 
    //函数声明:
    int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    
    /*
    参数:
    	1.网络的文件描述符。
    	2.sockaddr具体对象对应的地址,为输入型参数。
    	3.具体对象对应的大小,为输入型参数。
    说明:在传参之前,sockaddr对象应初始化完成。
    
    返回值:
    	1.成功返回 0。
    	2.失败返回 -1,并设置合适的错误码。
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 说明:在传入sockaddr具体对象对应的地址时,需要再强转为sockaddr*类型的,因为也传进去了具体对象的大小,所以内部会再识别出具体的对象,再进行处理。
    • 这里的IP地址的形式为字符串类型的,便于用户进行识别,而在网络当中是usiged int 类型的,中间需要转换一下。

    • 实现代码:

    #include
    #include
    using std::string;
    using std::cout;
    using std::endl;
    
    struct StrToIp
    {
    	unsigned int str_to_ip(const string& str)
    	{
    		int ssz = str.size();
    		int begin = 0;
    		int index = 3;
    		for (int i = 0; i <= ssz; i++)
    		{
    			if (str[i] == '.' || i == ssz)
    			{
    				string tmp = str.substr(begin,i);
    				begin = i + 1;
    				unsigned char n = stoi(tmp);
    				if (index < 0) return 0;
    				part[index--] = n;
    			}
    		}
    		//auto p = (unsigned char*)&ip;
    		//for (int i = 0; i < 4; i++)
    		//{
    		//	*(p + i) = part[i];
    		//}
    		//return ip;
    
    		return *((unsigned int*)part);
    	}
    	unsigned char part[4] = { 0 };
    
    	//unsigned int ip = 0;
    
    };
    
    int main()
    {
    	StrToIp s;
    	cout << s.str_to_ip("59.110.171.160") << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    1. 我们将字符串分为四部分,然后转换为char类型的四个变量,存储即可。
    2. 这四个部分我们存放在数组或者单独存都可以,这里我采用数组便于操作。
    3. 如果为数组,具体转换为int变量时,应注意四个部分的存储顺序。
    • 运行结果:

    在这里插入图片描述

    • 说明:
    1. 我所在的机器为小端机,数据是低位放在低地址处,所以应该倒着存每一段。
    2. 如果为大端机,数据是高位放在低地址处,所以应该正着存每一段。
    3. 最后强转取数据即可。
    • 补充: 指针指向的是对象的低地址处。

    在实际编程的过程中,相应的接口已经准备好,不需要手动的写,但相应的原理还是要清楚的。

    • 字符串转地址的网络序列接口:
    //头文件
    #include 
    #include 
    #include 
    
    int inet_aton(const char *cp, struct in_addr *inp);
    
    /*
    参数:
    	1.转化的ip地址的字符串。
    	2.输出型参数,in_addr的变量。
    返回值:
    	1.成功返回非零值,通常为1.
    	2.失败返回零值。
    */
    in_addr_t inet_addr(const char *cp);
    /*
    参数:
    	1.转化的ip地址的字符串。
    返回值:
    	1.成功返回对应的ip值。
    	2.失败返回INADDR_NONE,其定义为 (in_addr_t) -1。
    */
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 主机ip地址转字符串的接口:
    //头文件
    #include 
    #include 
    #include 
    char *inet_ntoa(struct in_addr in);
    
    /*
    
    参数:存放主机序列的ip地址
    返回值:字符串形式的ip。
    
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    • 实现代码:
        void Init()
        {
            //1.创建套接字,即创建文件描述符
            _sockfd = socket(AF_INET,SOCK_DGRAM,0);
            if(_sockfd < 0)
            {
                lg(CRIT,"socket fail,error message is %s,error \
                number is %d ",strerror(errno),errno);
                exit(SOCKET_CREAT_FAIL);
            }
            lg(INFORE,"socket fd is %d,socket success!",_sockfd);
            //2.绑定套接字
            /*
                注意:主机序列都要转成网络序列。
            */
            
            // 2.1初始化域间套接字
            struct sockaddr_in server_mes;
            bzero(&server_mes,sizeof(server_mes));
            server_mes.sin_family = AF_INET;
            server_mes.sin_port = htons(_port); 
            server_mes.sin_addr.s_addr = inet_addr(_ip.c_str());
            socklen_t len = sizeof(server_mes);
            
            // server_mes.sin_addr.s_addr = INADDR_ANY;; 
            
            //任意地址转网络序列
            
            // 2.2 绑定域间套接字
            int ret = bind(_sockfd,(const sockaddr*)&server_mes,len);
            if(ret < 0)
            {
                lg(CRIT,"bind fail,error message is %s,\
                error number is %d ",strerror(errno),errno);
                exit(SOCKET_BIND_FAIL);
            }
            lg(INFORE,"ret is %d,bind success!",ret);
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    1.3 Run
    1. 等待客户发信息

    接口

    //头文件
    #include 
    #include 
    
    
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
    struct sockaddr *src_addr, socklen_t *addrlen);
    
    /*
    参数:
    	1.文件描述符。
    	2.缓存区,读取用户发来的信息。
    	3.缓存区的大小。
    	4.一般使用默认值0即可。
    	5.src_addr变量的地址,用于接收用户的网络信息,输出型参数。
    	6.addrlen用于接受用户的src_addr具体对象的长度,输入输出型参数。
    
    返回值:
    	1.成功,返回接受的字节个数。
    	2.连接关闭,返回0。
    	3.错误返回-1.设置合适的错误码。
    */
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    1. 给客户提供服务

    接口:

    //头文件:
    #include 
    #include 
    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
    const struct sockaddr *dest_addr, socklen_t addrlen)	
    参数:
    	1.文件描述符。
    	2.缓存区,存放发给用户的信息。
    	3.缓存区的大小。
    	4.一般使用默认值0即可。
    	5.src_addr变量的地址,用于存放用户的网络信息。
    	6.addrlen用于存放用户的src_addr具体对象的长度。
    
    返回值:
    	1.成功,返回实际发送的字节个数。
    	3.错误返回-1.设置合适的错误码。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 实现代码:
     void Run()
     {
         for(;;)
         {
         	 //存放用户消息的缓存区
             char buffer[1024] = {0};
             //用于存放用户的网络信息
             struct sockaddr_in client_mes; 
             socklen_t len;
             //收消息
             ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1\
             ,0,(sockaddr*)&client_mes,&len);
             if(n < 0)
             {
                 lg(WARNNING,"recvfrom message fail,waring \
                 message is %s",strerror(errno));
                 continue;
             }
             buffer[n] = 0;
             //uint_32_t 转 string
             string cip = inet_ntoa(client_mes.sin_addr);
             
             uint16_t cport = ntohs(client_mes.sin_port);
             string echo_mes =  "[" + cip + ":" + to_string(cport)\
              + "]@" + buffer;
             cout << echo_mes << endl;
             //发消息:
             n = sendto(_sockfd,echo_mes.c_str(),echo_mes.size(),\
             0,(sockaddr*)&client_mes,len);
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 这里只是简单的使用接口,因此完成收发消息即可。

    • server.hpp
    #pragma once
    //所用容器
    #include
    #include
    
    //与内存相关的头文件
    #include
    #include
    
    //网络相关的头文件
    #include
    #include
    #include
    #include 
    
    //包装器
    #include
    
    //日志头文件
    #include "Log.hpp"
    //枚举常量,用于失败退出进程的退出码
    enum
    {
        SOCKET_CREAT_FAIL = 1,
        SOCKET_BIND_FAIL,
    };
    
    Log lg;
    uint16_t default_port = 8888;
    string default_string = "0.0.0.0";
    class UdpServer
    {
    public:
        UdpServer(uint16_t port = default_port,string ip \
        = default_string)
        :_port(port),_ip(ip),_sockfd(0)
        {}
        ~UdpServer()
        {
            if(_sockfd > 0) 
            {
                close(_sockfd);
            }
        }
        void Init()
        {
            //1.创建套接字,即创建文件描述符
            _sockfd = socket(AF_INET,SOCK_DGRAM,0);
            if(_sockfd < 0)
            {
                lg(CRIT,"socket fail,error message is %s,error \
                number is %d ",strerror(errno),errno);
                exit(SOCKET_CREAT_FAIL);
            }
            lg(INFORE,"socket fd is %d,socket success!",_sockfd);
            //2.绑定套接字
            /*
                注意
                1.主机序列转成网络序列。
            */
            struct sockaddr_in server_mes;
            bzero(&server_mes,sizeof(server_mes));
            server_mes.sin_family = AF_INET;
            server_mes.sin_port = htons(_port); 
            server_mes.sin_addr.s_addr = inet_addr(_ip.c_str());
            socklen_t len = sizeof(server_mes);
    
            // server_mes.sin_addr.s_addr = INADDR_ANY;; 
            //任意地址转网络序列
            
            int ret = bind(_sockfd,(const sockaddr*)&server_mes,len);
            if(ret < 0)
            {
                lg(CRIT,"bind fail,error message is %s,error number is\
                 %d ",strerror(errno),errno);
                exit(SOCKET_BIND_FAIL);
            }
            lg(INFORE,"ret is %d,bind success!",ret);
    
        }
        void Run()
        {
            for(;;)
            {
                char buffer[1024] = {0};
                struct sockaddr_in client_mes; //用户的网络信息
                socklen_t len = sizeof(client_mes);
                //收消息
                ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1\
                ,0,(sockaddr*)&client_mes,&len);
                if(n < 0)
                {
                    lg(WARNNING,"recvfrom message fail,waring message \
                    is %s",strerror(errno));
                    continue;
                }
                buffer[n] = 0;
                string cip = inet_ntoa(client_mes.sin_addr);
                uint16_t cport = ntohs(client_mes.sin_port);
                string echo_mes =  "[" + cip + ":" + to_string(cport)\
                 + "]@" + buffer;
                cout << echo_mes << endl;
                //发消息:
                n = sendto(_sockfd,echo_mes.c_str(),echo_mes.size()\
                ,0,(sockaddr*)&client_mes,len);
            }
        }
    private:
        int _sockfd;
        string _ip;
        uint16_t _port;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • server.cc
    #include
    #include
    #include"udpserver.hpp"
    void Usage(char* pragma_name)
    {
        cout << endl << "Usage: " << pragma_name \
        << " + port[8000-8888]" << endl << endl; 
    }
    int main(int argc,char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 1;
        }
        uint16_t port = stoi(argv[1]);
        UdpServer* ser = new UdpServer(port);
        ser->Init();
        ser->Run();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 当服务器启动成功时,我们可以使用 netstat -naup 查看对应的服务器。
    • 说明:-n表示 net,-a 表示 all,-u 表示 udp, -p 表示 process, 即显示出所有的udp套接字的信息。
    • 图解:在这里插入图片描述

    2.客户端

    1.Linux
    • 实际上只有四步:
    1. 创建网络套接字。

    2. 输入要发送的消息。

    3. 发消息。

    4. 收消息。

    • 注意:
    1. 在客户端,我们并不需要主动bind端口号,而是应该由系统自动分配端口号,这样即避免了不同应用程序之间端口号的冲突,也变向的提高了安全性,灵活性。
    2. 端口号在调用sento函数时,自动进行绑定。
    • client.hpp
    #pragma once
    //容器
    #include
    
    //内容接口
    #include
    #include
    
    //网络相关的接口
    #include
    #include
    #include
    #include 
    
    //线程
    #include
    //日志
    #include "Log.hpp"
    using std::string;
    Log lg;
    enum
    {
        SOCKET_CREAT_FAIL = 1,
        SOCKET_BIND_FAIL,
    };
    string default_ip = "59.110.171.164";
    uint16_t default_port = 8888;
    struct UdpClient
    {
    public:
        UdpClient(uint16_t port = default_port,string ip = default_ip)
        :_ip(ip),_port(port)
        {}
         ~UdpClient()
        {
            if(_sockfd > 0) 
            {
                close(_sockfd);
            }
        }
    
        void Init()
        {
            _sockfd = socket(AF_INET,SOCK_DGRAM,0);
            if(_sockfd < 0)
            {
                lg(CRIT,"socket create fail,error message is %s,error \
                is %d",strerror(errno),errno);
                exit(SOCKET_CREAT_FAIL);
            }
            lg(INFORE,"socket create success, socketfd is %d",_sockfd);
            //在发送消息的时候会自动进行绑定。
        }
    
        void Run()
        {
            struct sockaddr_in server_mes;
            bzero(&server_mes,sizeof(server_mes));
            server_mes.sin_addr.s_addr = inet_addr(_ip.c_str());
            server_mes.sin_family = AF_INET;
            server_mes.sin_port = htons(_port);
            socklen_t len = sizeof(server_mes);
            while(true)
            {
                string str;
                cout << "please enter@";
                getline(cin,str);
                ssize_t n = sendto(_sockfd,str.c_str(),str.size(),0,\
                (sockaddr*)&server_mes,len);
                if(n < 0)
                {
                    lg(WARNNING,"send message fail,error message is \
                    %s,error is %d",strerror(errno),errno);
                    continue;
                }
                char buffer[SIZE] = {0};
                n = recvfrom(_sockfd,buffer,SIZE - 1,0,\
                (sockaddr*)&server_mes,&len);
                buffer[n] = '\0';
                cout << buffer << endl;
    
            }
        }
        int _sockfd;
        string _ip;
        uint16_t _port;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • client.cc
    #include "udpclient.hpp"
    void Usage(char* pragma_name)
    {
        cout << endl << "Usage: " << pragma_name << " + ip " << \
        " + port[8080-8888]" << endl << endl; 
    }
    int main(int argc,char* argv[])
    {
        if(argc != 3)
        {
            Usage(argv[0]);
            return 1;
        }
        string ip = argv[1];
        uint16_t port = stoi(argv[2]);
        UdpClient* client = new UdpClient(port,ip);
        client->Init();
        client->Run();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 示例:
      在这里插入图片描述
    2.Windows
    • WindowsClient.hpp
    //定义此宏是为了屏蔽inet_addr这个错误。
    #define _WINSOCK_DEPRECATED_NO_WARNINGS 1
    
    #include
    #include
    
    #include
    //此头文件应该包含于Windows.h之上,可能的原因是重复包含相同的声明,
    //也就是没有写#pragma once之类的。
    
    #include
    
    #include
    #include
    
    #pragma comment(lib,"ws2_32.lib")//包含一个库
    
    using std::string;
    using std::cin;
    using std::cout;
    using std::endl;
    
    
    enum
    {
    	START_FAIL = 1,
    	SOCKET_FAIL = 2,
    };
    
    string default_ip = "59.110.171.164";
    uint16_t default_port = 8080;
    
    struct UdpClient
    {
    public:
        UdpClient(uint16_t port = default_port, string ip = default_ip)
            :_ip(ip), _port(port),_sockfd(0)
        {
            WSADATA wsd;
            int ret = WSAStartup(MAKEWORD(2, 2), &wsd);
            if (ret != 0)
            {
                perror("WSAStartup");
                exit(START_FAIL);
            }
        }
    
    
    
        ~UdpClient()
        {
            if(_sockfd > 0) 
            {
                close(_sockfd);
            }
            WSACleanup();
        }
        void Init()
        {
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd < 0)
            {
                perror("socket");
                exit(SOCKET_FAIL);
            }
        }
        void Run()
        {
            struct sockaddr_in server_mes;
            server_mes.sin_addr.S_un.S_addr = inet_addr(_ip.c_str());
            server_mes.sin_port = htons(_port);
            server_mes.sin_family = AF_INET;
            int len = sizeof(server_mes);
    
            while (true)
            {
                string str;
                cout << "please enter@";
                getline(cin, str);
    
                int ret = sendto(_sockfd, str.c_str(), str.size(), \
                0, (const sockaddr*)&server_mes, len);
                if (ret < 0)
                {
                    cout << ret << endl;
                    perror("sento");
                    continue;
                }
                char buffer[1024] = { 0 };
                int size = recvfrom(_sockfd, buffer, \
                sizeof(buffer) - 1, 0, (sockaddr*)&server_mes, &len);
                if (size < 0)
                {
                    perror("recvfrom");
                    continue;
                }
                buffer[size] = '\0';
                cout << buffer << endl;
            }
        }
        int _sockfd;
        string _ip;
        uint16_t _port;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • Client.cc
    #define _CRT_SECURE_NO_WARNINGS 1
    #include"WindowsClient.hpp"
    //智能指针所在头文件
    #include
    int main()
    {
    	//使用unique_ptr确保只有一台服务器。
    	std::unique_ptr<UdpClient> cp(new UdpClient());
    	cp->Init();
    	cp->Run();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述

    • Linux与Windows的代码相比之下,其实就构造和析构多了一点东西,其余基本完全相同。

    • 上述实现代码我们称之为版本1。

     基本的收发消息我们是可以完成的,而且我们通过端口号和ip标识了唯一的一台主机的唯一进程,这样我们可以基于此简单的实现一个基于网络的聊天室:

    • 客户端:
    1. 收消息和发消息应该用不同的线程进行执行,因此我们需要在客户端创建两个线程,收消息和发消息。
    2. 具体的实现操作我们可分为两步:
    1. 将收消息的线程输出到错误流中。
    2. 在运行时将错误流再重定向到指定的终端文件当中。
    • 查看终端文件:ls /dev/pts
      在这里插入图片描述
    • 客户端更改代码:
        static void* SendMessage(void* args)
        {
            auto threadptr = static_cast<pair<UdpClient*,\
            struct sockaddr_in*>*>(args);
            
            UdpClient* cptr = threadptr->first;
            sockaddr* sptr = (sockaddr*)threadptr->second;
            //发消息
            while(true)
            {
                string str;
                cout << "please enter@";
                getline(cin,str);
                ssize_t ret = sendto(cptr->_sockfd,str.c_str()\
                ,str.size(),0,sptr,sizeof(sockaddr_in));
                if(ret < 0)
                {
                    lg(WARNNING,"send message fail,error message\
                     is %s,error is %d",strerror(errno),errno);
                    continue;
                }
            }   
        }
        static void* ReceiveMessage(void* args)
        {
            auto threadptr = static_cast<pair<UdpClient*,\
            struct sockaddr_in*>*>(args);
            
            UdpClient* cptr = threadptr->first;
            sockaddr* sptr = (sockaddr*)threadptr->second;
            socklen_t len = sizeof(sockaddr_in);
            //收消息
            while(true)
            {
                char buffer[SIZE] = {0};
                int ret  = recvfrom(cptr->_sockfd,buffer,SIZE - 1,\
                0,sptr,&len);
                cerr << buffer << endl;
            }
        }
        void Run()
        {
            struct sockaddr_in server_mes;
            bzero(&server_mes,sizeof(server_mes));
            server_mes.sin_addr.s_addr = inet_addr(_ip.c_str());
            server_mes.sin_family = AF_INET;
            server_mes.sin_port = htons(_port);
            socklen_t len = sizeof(server_mes);
            pair<UdpClient*,struct sockaddr_in*> thread_ptr = \
            {this,&server_mes};
            pthread_t rtid,wtid;
            pthread_create(&rtid,nullptr,SendMessage,&thread_ptr);
            pthread_create(&wtid,nullptr,ReceiveMessage,&thread_ptr);
            pthread_join(rtid,nullptr);
            pthread_join(wtid,nullptr);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 服务端:
    1. 上述代码我们已经用ip地址和端口号标识了全网的唯一 一个进程。
    2. 因此我们可以用此来认证用户和给指定用户收发消息。
    3. 具体采用unordered_map的结构进行实现。
    • server.hpp增删代码:
    //类外:
    #include
    
    #include
    using fun_t = function<string(string,string,uint16_t)>;
    
    
    //类内:
        void CheckUser(const sockaddr_in& user)
        {
            string cip = inet_ntoa(user.sin_addr);
            uint16_t port = ntohs(user.sin_port);
            string key = cip + to_string(port);
            auto it = users.find(key);
            if(it == users.end())
            {
                cout << "add a new user[" << cip << "]" << endl;
                users.insert({key,user});
            }
        }
        void BroadCast(const string& mes)
        {
            int cnt = 0;
            for(auto& user : users)
            {
                sockaddr_in& client_mes = user.second;
                socklen_t len = sizeof(sockaddr_in);
                ssize_t n = sendto(_sockfd,mes.c_str(),mes.size()\
                ,0,(sockaddr*)&client_mes,len);
                if(n < 0)
                {
                    lg(WARNNING,"send message fail,waring message\
                     is %s",strerror(errno));
                    continue;
                }
                cnt++;
            }
        }
        void Run(fun_t cal_back)
        {
            for(;;)
            {
                char buffer[1024] = {0};
                struct sockaddr_in client_mes; //用户的网络信息
                // socklen_t len = sizeof(client_mes);
                socklen_t len;
    
                //收消息
                ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) \
                - 1,0,(sockaddr*)&client_mes,&len);
                if(n < 0)
                {
                    lg(WARNNING,"recvfrom message fail,waring message \
                    is %s",strerror(errno));
                    continue;
                }
                buffer[n] = 0;
                string cip = inet_ntoa(client_mes.sin_addr);
                uint16_t cport = ntohs(client_mes.sin_port);
                string echo_mes = cal_back(buffer,cip,cport);
                //1.检查用户是否已经上线
                CheckUser(client_mes);
                //2.广播给所有用户
                BroadCast(echo_mes);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • server.cc——更新代码
    //添加此函数。
    string Print(const string& mes,string ip,uint16_t port)
    {
        string infor = "[" + ip + ":" + to_string(port) + "]@" \
        + mes;
        return infor; 
    }
    
    void Usage(char* pragma_name)
    {
        cout << endl << "Usage: " << pragma_name \
        << " + port[8000-8888]" << endl << endl; 
    }
    int main(int argc,char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 1;
        }
        uint16_t port = stoi(argv[1]);
        UdpServer* ser = new UdpServer(port);
        ser->Init();
        
        //更新此处
        ser->Run(Print);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 效果:
      在这里插入图片描述
    • 以上我们使用终端来重定向输出,看起来是不太漂亮的,使用图形库的知识我们可以将效果做的更为逼真,更加真实。
    • 上述更新代码我们称之为版本2,基于版本1更改。
    • 在运行可执行程序时,我们将标准错误流重定向到指定的终端文件即可。

    其次,既然能收发消息,我们还可以将消息当做指令进行处理,就类似与我们使用ssh登录云服务器的功能类似:

    • 相关接口:
    //头文件
     #include 
    
     FILE *popen(const char *command, const char *type);
    /*
    
    参数:
    	1.要执行的命令。
    	2.打开文件的类型,这里我们设置为"r" 模式即可。
    返回值:
    	1.失败返回空指针。
    	2.成功返回对应的文件指针。
    */
     int pclose(FILE *stream);
     
    /*
     参数:要关闭的文件指针。
     返回值:
     	1.失败返回-1。
     	2.成功返回0.
    */
    
    char *fgets(char *s, int size, FILE *stream);
    /*
    
    参数:
    	1.存放的缓存区的地址
    	2.缓存区的大小。
    	3.读取的文件指针
    返回值:
    	1.成功返回读取到的内容的地址。
    	2.失败返回空指针。 	
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • server.cc
    #include
    #include
    #include"udpserver.hpp"
    
    //过滤掉一下关键词。
    bool SafeCheck(const string& buf)
    {
        vector<string> key_words = 
        {
            "rm","cp","mv","yum","top","while",
        };
        for(string& word : key_words)
        {
            auto it = buf.find(word);
            if(it != string::npos) return true;
        }
        return false;
    }
    //主要功能函数:
    string HandlerCommand(const string& buf,string ip,uint16_t port)
    {
        if(SafeCheck(buf)) return "Bad Man!";
        FILE* res = popen(buf.c_str(),"r");
        if(res == nullptr)
        {
            lg(CRIT,"run a command fail,error message is %s,\
             error is %d",strerror(errno),errno);
            exit(-1);
        }
        string ret;
        //从执行的命令的结果中读取内容
        while(true)
        {
            char buffer[1024] = {0};
            if(fgets(buffer,sizeof(buffer),res) == nullptr)
                break;
            ret += buffer;
        }
        int n = pclose(res);
        return ret;
    }
    void Usage(char* pragma_name)
    {
        cout << endl << "Usage: " << pragma_name \
        << " + port[8000-8888]" << endl << endl; 
    }
    int main(int argc,char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 1;
        }
        uint16_t port = stoi(argv[1]);
        UdpServer* ser = new UdpServer(port);
        ser->Init();
        ser->Run(HandlerCommand);
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • server.hpp
    #include
    using fun_t = function<string(string,string,uint16_t)>;
    
    //类内:
        void Run(fun_t cal_back)
        {
            for(;;)
            {
                char buffer[1024] = {0};
                struct sockaddr_in client_mes; //用户的网络信息
                // socklen_t len = sizeof(client_mes);
                socklen_t len;
    
                //收消息
                ssize_t n = recvfrom(_sockfd,buffer,sizeof(buffer) \
                - 1,0,(sockaddr*)&client_mes,&len);
                if(n < 0)
                {
                    lg(WARNNING,"recvfrom message fail,waring message \
                    is %s",strerror(errno));
                    continue;
                }
                buffer[n] = 0;
                string cip = inet_ntoa(client_mes.sin_addr);
                uint16_t cport = ntohs(client_mes.sin_port);
                string echo_mes = cal_back(buffer,cip,cport);
    
                n = sendto(_sockfd,echo_mes.c_str(),echo_mes.size()\
                ,0,(sockaddr*)&client_mes,len);
                if(n < 0)
                {
                    lg(WARNNING,"send message fail,waring message\
                     is %s",strerror(errno));
                    continue;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 上述实现代码我们称之为版本3。基于版本1进行拓展。

    • 总结:
    1. 版本1—— 简单使用套接字编程,并使用服务器和客户端完成简单的收发消息。
    2. 版本2—— 基于版本1,实现了一个简单的聊天室,使用多线程和重定向使收消息和发信息完成并发。
    3. 版本3—— 基于版本1,实现了客户端远程控制服务器,并执行对应发送的命令。

    3.TCP

    1. 基本接口

    因为TCP是可靠的,那么必然得多做一些准备工作,具体以接口的形式呈现,下面我们先介绍TCP的服务端和客户端多做的工作。

    服务端:

    1. socket时,我们需要设置第二个选项为SOCKET_STREAM,即基于字节流的形式的协议。
    2. 服务器在bind之后需要监听客户端的连接。
    //头文件
    #include
    #include
    
    //函数声明:
    int listen(int sockfd, int backlog);
    /*
    参数:
    	1.SOCKET_STREAM,即TCP类型的套接字文件描述符。
    	2.请求队列的最大长度,设置为5即可,可以理解为待处理的客户端的最大连接数。
    返回值:
    	1.成功返回 0.
    	2.失败返回-1,设置合适的错误码。
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. 服务器在listen之后,如果有客户端连接需要接收。
    //头文件
    #include
    #include
    //函数声明:
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    /*
    参数:
    	1.TCP类型的套接字文件描述符。
    	2.输出型参数,即客户端的信息。
    	3.输入输出型参数,具体的套接字地址结构体的大小变量的地址。
    返回值:
    	1.成功返回对应客户端的套接字文件描述符。
    	2.失败返回-1,设置合适的错误码。
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 疑问:socket 不是已经有文件描述符了,accept还要返回文件描述符呢?
    • 解释:
    1. 我们最开始创建的描述符是用来接收客户端连接的,并不用与客户端通讯。
    2. 这就好比一家餐厅,有出门接客的服务员,有餐桌上提供实际服务的服务员,开始创建的描述符就好比出门接客的服务员,而accept返回的套接字描述符就是实际提供服务的服务员。
    3. 出门引客的文件描述符在引完" 客人",又回到店门口去引客了,因此只有一个,又因为店里可能同时有多人在吃饭,所以实际服务的文件描述符可能有多个。
    4. 在提供服务的描述符服务完之后,需要关闭描述符,即清理餐桌,等待为下一位客人提供服务。
    • 说明:因为要保证可靠,因此实际服务一次只能服务一位。

    客户端:只需要在一些基础工作之上,与服务器建立连接即可。

    //头文件
    #include
    #include
    //函数声明:
    int connect(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);
    /*
    参数:
    	1.TCP类型的文件描述符。
    	2.输出型参数,即客户端的信息。
    	3.输入输出型参数,具体的套接字地址结构体的大小。
    返回值:
    	1.成功返回0,表示连接成功。
    	2.失败返回-1,表示连接失败。
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 接口的功能基本了解之后,我们可以开始实现一个简单的服务端与客户端。

    • 实现代码文件基本框架:
      在这里插入图片描述

    2. 客户端

    • client.cc
    #include"tcpclient.hpp"
    #include
    void Usage(char* pragma_name)
    {
        cout << endl << "Usage: " << pragma_name \
        << "+ ip + port[8000-8888]" << endl << endl; 
    }
    int main(int argc,char* argv[])
    {
        if(argc != 3)
        {
            Usage(argv[0]);
            return 1;
        }
        string ip = argv[1];
        uint16_t port = stoi(argv[2]);
        std::unique_ptr<TcpClient> tc(new TcpClient(ip,port));
        // tc->Init();
        tc->Run();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • tcpclient.hpp
    #pragma once
    
    #include 
    #include 
    #include 
    
    #include 
    #include 
    
    #include 
    #include 
    
    #include 
    #include "../Tools/Log.hpp"
    
    using std::string;
    
    enum
    {
        CREATE_FAIL = 1,
        TOIP_FAIL,
        BIND_FAIL,
        LISTEN_FAIL,
        CONNET_FAIL,
        IPTONET_FAIL,
    };
    using std::string;
    uint16_t defaultport = 8080;
    string defaultip = "59.110.171.164";
    int defaultbacklog = 5;
    class TcpClient
    {
    public:
        TcpClient(string ip = defaultip,uint16_t port = defaultport)
            : _port(port), _ip(ip), _sockfd(0)
        {}
        void Run()
        {
    
            //server端
            sockaddr_in server;
            memset(&server,0,sizeof(server));
            socklen_t len = sizeof(server);
            if(inet_aton(_ip.c_str(), &server.sin_addr) < 0)
            {
                lg(CRIT,"inet_atoncreat fail,reason is %s,errno \
                is %d", strerror(errno), errno);
                exit(IPTONET_FAIL);
            }
            server.sin_family = AF_INET;
            server.sin_port = htons(_port);
            while(true)
            {
                _sockfd = socket(AF_INET, SOCK_STREAM, 0);
                if (_sockfd < 0)
                {
                    lg(CRIT, "socket creat fail,reason is %s,errno \
                    is %d", strerror(errno), errno);
                    exit(CREATE_FAIL);
                }
                bool reconect = false;
                int cnt = 10;
                do
                {
                    int ret = connect(_sockfd, (sockaddr *)&server,\
                     sizeof(server));
                    if (ret < 0)
                    {
                        reconect = true;
                        lg(WARNNING, "connect creat fail,reason \
                        is %s,errno is %d", strerror(errno), errno);
                        sleep(2);
                    }
                    else
                    {
                        reconect = false;
                    }
                } while (reconect && cnt--);
                
                //发消息
                string str;
                cout << "please enter@";
                getline(cin,str);
                ssize_t n = sendto(_sockfd,str.c_str(),str.size()\
                ,0, (sockaddr *)&server, sizeof(server));
                if(n < 0)
                {
                    lg(WARNNING, "sendto fail,reason is %s,\
                    errno is %d", strerror(errno), errno);
                    continue;
                }
                char buffer[1024] = {0};
                n = recvfrom(_sockfd,buffer,sizeof(buffer) - 1,\
                0, (sockaddr *)&server, &len);
                if(n < 0)
                {
                    lg(WARNNING, "recvfrom fail,reason is %s,\
                    errno is %d", strerror(errno), errno);
                    continue;
                }
                cout << buffer;
                close(_sockfd);
            }
    
        }
    
    private:
        int _sockfd;
        uint16_t _port;
        string _ip;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 套接字文件描述符,在提供一次服务之后立马关闭,重新连接。
    • 重新连接时,可能会连接失败,因此提供了10次重连功能,重连失败跳出循环,重连成功继续享受服务器的服务。

    3. 服务端

    • server.cc:
    #include"tcpserver.hpp"
    #include
    using std::unique_ptr;
    void Usage(char* pragma_name)
    {
        cout << endl << "Usage: " << pragma_name \
        << " + port[8000-8888]" << endl << endl; 
    }
    int main(int argc,char* argv[])
    {
        if(argc != 2)
        {
            Usage(argv[0]);
            return 1;
        }
        uint16_t port = stoi(argv[1]);
        unique_ptr<TcpServer> tp(new TcpServer(port));
        tp->Init();
        tp->Run();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1.版本1
    • server.hpp
    #pragma once
    #include
    #include
    #include
    #include
    #include
    
    #include
    #include
    
    #include
    #include
    
    #include"../Tools/Log.hpp"
    enum
    {
        CREATE_FAIL = 1,
        TOIP_FAIL,
        BIND_FAIL,
        LISTEN_FAIL,
    };
    using std::string;
    uint16_t defaultport = 8080;
    string defaultip = "0.0.0.0";
    int defaultbacklog = 5;
    
    class TcpServer
    {
    
    public:
        TcpServer(uint16_t port = defaultport,string ip = defaultip\
        ,int backlog = defaultbacklog)
        :_port(port),_ip(ip),_sockfd(0),_backlog(5)
        {
            lg.ReDirect(CLASSFILE);
        }
        ~TcpServer()
        {
            if(_sockfd > 0)
            {
                close(_sockfd);
            }
        }
        void Init()
        {
            _sockfd = socket(AF_INET,SOCK_STREAM,0);
            if(_sockfd < 0)
            {
                lg(CRIT,"socket creat fail,reason is %s,errno\
                 is %d.",strerror(errno),errno);
                exit(CREATE_FAIL);
            }
                    
            //补充:防止服务器偶发性无法重启,即端口号无法重复的进行使用。
            int opt = 1;
            setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,\
            &opt,sizeof(opt));
    
            //初始化服务器信息
            sockaddr_in server_mes;
            socklen_t len = sizeof(server_mes);
            memset(&server_mes,0,sizeof(server_mes));
            server_mes.sin_port = htons(_port);
            server_mes.sin_family = AF_INET;
            if(!inet_aton(_ip.c_str(),&server_mes.sin_addr))
            {
                lg(CRIT,"address change fail,reason is %s,errno \
                is %d.",strerror(errno),errno);
                exit(TOIP_FAIL);
            }
            //绑定
            if(bind(_sockfd,(sockaddr*)&server_mes,len) == -1)
            {
                lg(CRIT,"bind fail,reason is %s,errno \
                is %d.",strerror(errno),errno);
                exit(BIND_FAIL);
            }
            //监听
            if(listen(_sockfd,_backlog) < 0)
            {
                lg(CRIT,"listen fail,reason is %s,errno \
                is %d.",strerror(errno),errno);
                exit(LISTEN_FAIL);
    
            }
    
            lg(INFORE,"sockfd is %d,TcpServer Init Success!",_sockfd);
        }
    
    
        void Run()
        {
            for(;;)
            {
                sockaddr_in client_mes;
                memset(&client_mes,0,sizeof(client_mes));
                socklen_t len = sizeof(client_mes);
                int fd = accept(_sockfd,(sockaddr*)&client_mes,&len);
                if(fd < 0)
                {
                    lg(WARNNING,"accept fail,reason is %s,\
                    errno is %d,fd is %d",strerror(errno),errno,fd);
                    break;
                }
                lg(INFORE,"add a client, fd is %d.",fd);
                // version 1 —— 单进程版,即一次只能服务一个人。
                Service(client_mes,len,fd); 
            }
        }
        void Service(const sockaddr_in& client,socklen_t len,int fd)
        {
            //收消息
            while(true)
            {
                char buffer[1024] = {0};
                ssize_t n = read(fd,buffer,sizeof(buffer) - 1);
                if(n < 0)
                {
                    lg(WARNNING,"read fail,reason is %s,\
                    errno is %d.",strerror(errno),errno);
                    continue;
                }
                //细节1:读不到要跳出/返回。
                else if(n == 0)
                {
                    break;
                }
                buffer[n] = '\0';
                string ip = inet_ntoa(client.sin_addr);
                uint16_t port = ntohs(client.sin_port);
                string echo_mes = "[" + ip + ":" + to_string(port)\
                 + "]@" + string(buffer);
                n = write(fd,echo_mes.c_str(),echo_mes.size());
                if(n <= 0)
                {
                    lg(WARNNING,"sendto fail,reason is %s,\
                    errno is %d.",strerror(errno),errno);
                    continue;
                }
            }
        }
    private:
        int _sockfd;
        uint16_t _port;
        string _ip;
        int _backlog;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147

    效果:

    • 这里我们介绍一个小工具——telnet,用与简单的收发消息。
    • 使用:
    1. telnet 【IP地址】 【端口号】
    2. 连接成功即可接收和发送信息。
    3. 输入 ctrl 加 ],跳转到命令端口。
    4. 输入回车退出命令窗口,输入quit退出telnet工具。
    • 效果在这里插入图片描述

    但是这个服务器有一个缺陷,就是一次只能服务一位客户,因为Service调用完之后,才能给下一个用户提供服务,那么我们可以将Run函数的Service变成如下情况,实现多进程版的并发服务。

    2.版本2
    • 更改Run函数的代码:
    
        void Run()
        {
            for(;;)
            {
                sockaddr_in client_mes;
                memset(&client_mes,0,sizeof(client_mes));
                socklen_t len = sizeof(client_mes);
                int fd = accept(_sockfd,(sockaddr*)&client_mes,&len);
                if(fd < 0)
                {
                    lg(WARNNING,"accept fail,reason is %s,\
                    errno is %d,fd is %d",strerror(errno),errno,fd);
                    break;
                }
                lg(INFORE,"add a client, fd is %d.",fd);
    
                // version 2 —— 使用孙子进程进行托孤,即让操作系统接管。
                pid_t pid = fork();
                if(pid == 0)
                {
                    //子进程
                    if(fork() > 0) exit(0);
    
                    //孙子进程
                    Service(client_mes,len,fd); 
                    exit(0);
                }
    
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 这里如果我们使用子进程的话,下面势必还有父进程等待回收子进程。
    • 因此我们创建子进程之后,再创建一个孙子进程,并退出子进程。
    • 这样孙子进程就变为了孤儿进程,由系统进行管理,而父进程不受影响。

    但是多进程势必会占用更多的资源,因此我们还可以创建线程,从而达到资源方面的优化作用。

    3.版本3
    • 在服务端增加与更改代码:
    //类外
    
    //声明
    class TcpServer;
    
    struct  PthreadData
    {
        PthreadData(TcpServer* const ptr,socklen_t leng,\
        sockaddr_in client_mes,int sockfd)
        :tp(ptr),len(leng),client(client_mes),fd(sockfd)
        {}
        TcpServer* tp;
        socklen_t len;
        sockaddr_in client;
        int fd;
    };
    
    //类内
        static void * Routine(void *args)
        {
            //为了省略之后的join,因此直接分离线程即可。
            pthread_detach(pthread_self());
    
            auto p = static_cast<PthreadData*>(args);
            sockaddr_in client = p->client;
            socklen_t len = p->len;
            TcpServer* tp = p->tp;
            int fd = p->fd;
            tp->Service(client,len,fd);
            close(fd);
            return nullptr;
        }
    
        void Run()
        {
            for(;;)
            {
                sockaddr_in client_mes;
                memset(&client_mes,0,sizeof(client_mes));
                socklen_t len = sizeof(client_mes);
                int fd = accept(_sockfd,(sockaddr*)&client_mes,&len);
                if(fd < 0)
                {
                    lg(WARNNING,"accept fail,reason is %s,\
                    errno is %d,fd is %d",strerror(errno),errno,fd);
                    break;
                }
                lg(INFORE,"add a client, fd is %d.",fd);
    
                // version 3 —— 多线程版
                pthread_t tid;
                PthreadData PD(this,len,client_mes,fd);
                pthread_create(&tid,nullptr,Routine,&PD);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 除此之外,线程的开辟和销毁也是有一定的损耗的,在此基础上,我们还可以进一步的进行优化。
    • 可以利用池化技术,即一次申请够线程,然后一直用着,这就所谓的线程池。
    4.版本4
    • 线程池:threadpool.hpp
    #pragma once
    #include
    #include
    #include "Task.hpp"
    using std::vector;
    using std::queue;
    using std::cout;
    using std::endl;
    
    typedef void(*cal)();
    class ThreadPool
    {
    public:
        static ThreadPool* GetInstance()
        {
            if(tpool == nullptr)
            {
                tpool = new ThreadPool();
            }
            return tpool;
        }
        void Lock()
        {
            pthread_mutex_lock(&_t_mutex);
        }
        void UnLock()
        {
            pthread_mutex_unlock(&_t_mutex);
        }
        ~ThreadPool()
        {
            for(int i = 0; i < _capacity; i++)
            {
                pthread_join(tids[i],nullptr);
            }
        }
        void start()
        {
            for(int i = 0; i < _capacity; i++)
            {
                pthread_create(&tids[i],nullptr,handler,this);
            }
        }
        void Push(const Task& data)
        {
            //push这里只有主线程在push,因此没必要加锁。
            _que.push(data);
            pthread_cond_broadcast(&_t_cond);
        }
        static void* handler(void* args)
        {
            ThreadPool* ptr = static_cast<ThreadPool*>(args);
            ptr->_handler();
        }
        void _handler()
        {
            while(true)
            {
                Lock();
                while(_que.empty())
                {
                    pthread_cond_wait(&_t_cond,&_t_mutex);
                }
                _que.front()();_que.pop();
                UnLock();
            }
        }
    private:
        static ThreadPool* tpool;
        ThreadPool(int num = defaultnum)
        :_capacity(num),tids(num)
        {
            pthread_mutex_init(&_t_mutex,nullptr);
            pthread_cond_init(&_t_cond,nullptr);
        }
        const static int defaultnum = 5; 
        //线程的锁和条件变量
        pthread_cond_t _t_cond;
        pthread_mutex_t _t_mutex;
        queue<Task> _que; //任务的场所
        vector<pthread_t> tids;
        int _capacity;
        int cnt = 0;
    };
    ThreadPool* ThreadPool::tpool = nullptr;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 除此之外,我们还需封装对应的Task任务,供线程池调用,以及服务端进行推送对应的任务。
    • 而且我们还可以基于此写一个网络版本的简单的翻译词典:
      1. 生成一个dict.txt,放入以 : 作为分割符的中译英的单词。
      2. 写一个类,用于读取文件内容生成词典,具体可用unordered_map。
      3. 根据此类写一个Task任务。

    • Dict.cc
    #include
    #include
    #include
    using namespace std;
    struct Dict
    {
    	Dict(string dir = "/home/shun_hua\
    	/linux_-code/test_2024/2/Dict/dict.txt")
    	{
    		const char* _dir = dir.c_str();
            string str;
            std::ifstream fs(_dir,ios_base::in);
            while(getline(fs, str))
            {
                int pos = str.find(':');
                string prev = str.substr(0, pos);
                string suf = str.substr(pos + 1);
                dict[prev] = suf;
            }
    	}
        string translate(const string& word)
        {
            if(dict[word] == "") return "unknow";
    
            return dict[word];
        }
    	unordered_map<string, string> dict;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 单词可以用ChatGpt生产,这里就不再列出了。

    • Task.hpp
    #pragma once
    #include
    #include
    #include
    #include
    
    #include
    #include
    
    #include
    #include
    #include
    #include
    
    #include"Log.hpp"
    #include"../Dict/dict.hpp"
    struct  Task
    {
        Task(const sockaddr_in& client,socklen_t len,int fd)
        :_client(client),_len(len),_fd(fd)
        {}
        void Service()
        {
            //收消息
            while(true)
            {
                char buffer[1024] = {0};
                ssize_t n = read(_fd,buffer,sizeof(buffer) - 1);
                if(n < 0)
                {
                    lg(WARNNING,"read fail,reason is %s,errno is %d,\
                    fd is %d.",strerror(errno),errno,_fd);
                    continue;
                }
                //细节1:读不到要跳出/返回。
                else if(n == 0)
                {
                    break;
                }
                buffer[n] = '\0';
                cout << buffer << endl;
                Dict dic;
                string ip = inet_ntoa(_client.sin_addr);
                uint16_t port = ntohs(_client.sin_port);
                string echo_mes = "[" + ip + ":" + to_string(port)\
                 + "]@" + dic.translate(buffer) +  "\n";
                cout << echo_mes;
                n = write(_fd,echo_mes.c_str(),echo_mes.size());
                if(n <= 0)
                {
                    lg(WARNNING,"sendto fail,reason is %s,errno \
                    is %d.",strerror(errno),errno);
                    continue;
                }
            }
        }
        void operator()()
        {
            Service();
            close(_fd);
        };
        sockaddr_in _client;
        socklen_t _len;
        string user;
        int _fd;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • Server.hpp
    #pragma once
    #include
    #include
    #include
    #include
    #include
    
    #include
    #include
    
    #include
    #include
    
    #include"../Tools/Log.hpp"
    #include"../Tools/threadpool.hpp"
    #include"../Tools/Task.hpp"
    #include"../Tools/daemon.hpp"
    
    enum
    {
        CREATE_FAIL = 1,
        TOIP_FAIL,
        BIND_FAIL,
        LISTEN_FAIL,
    };
    using std::string;
    uint16_t defaultport = 8080;
    string defaultip = "0.0.0.0";
    int defaultbacklog = 5;
    
    //声明
    class TcpServer;
    
    struct  PthreadData
    {
        PthreadData(TcpServer* const ptr,socklen_t leng\
        ,sockaddr_in client_mes,int sockfd)
        :tp(ptr),len(leng),client(client_mes),fd(sockfd)
        {}
        TcpServer* tp;
        socklen_t len;
        sockaddr_in client;
        int fd;
    };
    
    ThreadPool* thp = ThreadPool::GetInstance();
    class TcpServer
    {
    
    public:
        TcpServer(uint16_t port = defaultport,\
        string ip = defaultip,int backlog = defaultbacklog)
        :_port(port),_ip(ip),_sockfd(0),_backlog(5)
        {
            // lg.ReDirect(CLASSFILE);
        }
        ~TcpServer()
        {
            if(_sockfd > 0)
            {
                close(_sockfd);
            }
        }
        void Init()
        {
            _sockfd = socket(AF_INET,SOCK_STREAM,0);
            if(_sockfd < 0)
            {
                lg(CRIT,"socket creat fail,reason is %s\
                ,errno is %d.",strerror(errno),errno);
                exit(CREATE_FAIL);
            }
                    
            //补充:防止服务器偶发性无法重启,即端口号无法重复的进行使用。
            int opt = 1;
            setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT\
            ,&opt,sizeof(opt));
    
            //初始化服务器信息
            sockaddr_in server_mes;
            socklen_t len = sizeof(server_mes);
            memset(&server_mes,0,sizeof(server_mes));
            server_mes.sin_port = htons(_port);
            server_mes.sin_family = AF_INET;
            if(!inet_aton(_ip.c_str(),&server_mes.sin_addr))
            {
                lg(CRIT,"address change fail,reason is %s\
                ,errno is %d.",strerror(errno),errno);
                exit(TOIP_FAIL);
            }
            //绑定
            if(bind(_sockfd,(sockaddr*)&server_mes,len) == -1)
            {
                lg(CRIT,"bind fail,reason is %s,\
                errno is %d.",strerror(errno),errno);
                exit(BIND_FAIL);
            }
            //监听
            if(listen(_sockfd,_backlog) < 0)
            {
                lg(CRIT,"listen fail,reason is %s,\
                errno is %d.",strerror(errno),errno);
                exit(LISTEN_FAIL);
    
            }
    
            lg(INFORE,"sockfd is %d,TcpServer Init Success!",_sockfd);
        }
        void Run()
        {
            thp->start();
            for(;;)
            {
                sockaddr_in client_mes;
                memset(&client_mes,0,sizeof(client_mes));
                socklen_t len = sizeof(client_mes);
                int fd = accept(_sockfd,(sockaddr*)&client_mes,&len);
                if(fd < 0)
                {
                    lg(WARNNING,"accept fail,reason is %s,errno is \
                    %d,fd is %d",strerror(errno),errno,fd);
                    break;
                }
                lg(INFORE,"add a client, fd is %d.",fd);
                pthread_create(&tid,nullptr,Routine,&PD);
                //version 4 —— 线程池版本
                thp->Push(Task(client_mes,len,fd));
            }
        }
    private:
        int _sockfd;
        uint16_t _port;
        string _ip;
        int _backlog;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135

    三、守护进程

    • 在之前的信号这篇文章,我们对前台和后台有了基本的认识,两者的区分在于是否接收键盘的信息,前台接收,后台不接收
    • 相关命令:
    1. jobs 查看后台进程
    2. 可执行程序 &,将程序变为后台。
    3. fg 任务号,将程序变为前台。
    4. Ctrl Z,将程序停止。
    5. bg 任务号,将程序变为后台。
    • 效果:使sleep 200 为可执行程序,这里是为了演示具体无意义。
      在这里插入图片描述
    • 会话

      • 那么我们所谓前台和后台运行的交替,从而达成信息的交互的过程,我们称之为会话。

      • 一般我们在登录服务器成功时会有一个会话,这个会话在前台显示的是bash进程,用于与用户实现交互,后台是一些系统的进程在运行,其中bash进程随着用户的登录而出现,随着用户的退出而消失。

      • 因此当登录时,会话的id与bash进程的ID保持一致,而且我们称这个进程为会话的领导者,也就是没人变为前台,那么bash就变为前台。
        在这里插入图片描述

    • 守护进程

      • 一个进程自成一个会话,且无终端,即不与用户交流,但可以将信息导入到文件中。
      • 要想变为守护进程自身不能是会话的领导者。
    • 实现代码:

    #pragma once
    #include
    #include
    #include
    #include
    #include
    #include
    using std::string;
    string ddir = "/dev/null";
    void Daemon(string dir = "")
    {
        //对一些信号进行忽略,确保守护进程能够不受影响的正常运行。
        signal(SIGPIPE,SIG_IGN);
        signal(SIGCHLD,SIG_IGN);
        //创建进程,父进程退出使子进程变为孤儿进程。
        if(fork() > 0) exit(0);
        //子进程
        setsid();
        //更改当前的工作目录
        if(dir != "")
        {
            chdir(dir.c_str());
        }
        //将输出输入标准错误设置到 /dev/null
        int fd = open(ddir.c_str(),O_WRONLY);
        if(fd < 0) return;
        dup2(fd,1);
        dup2(fd,2);
        dup2(fd,0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 系统调用接口:
    //头文件:
    #include 
    //函数:
    int daemon(int nochdir, int noclose);
    /*
    参数:
    	1.如果为0,则改变当前进程的工作目录为根目录,否则不做变化。
    	2.如果为0,则改变stdin,stdout,stderror为/dev/null,否则不做变化。
    	说明:这里的/dev/null看做一个 “黑洞”,即输入东西没有反应。
    返回值:
    	1.成功返回0。
    	2.失败返回-1,并且设置合适的错误码。
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 我们可以将我们写的进程守护进程化,即7 * 24小时不间断的运行,并把日志信息重定向到文件中,方便出错时进行查看。

    尾序

     本篇文章主要是对于Socket套接字的实战:

    • 对于UDP实现了服务端,两个系统的客户端,实现了一个简单版本的网络聊天室和输入命令远端控制。

    • 对于TCP实现了客户端,基于资源利用率和要求实现了四个版本的服务端,并在此基础上写了一个简单版本的网络之间的单词翻译。

    • 再此基础上介绍了守护进程,可以将我们写的服务端守护进程化,即7*24小时不停的运作。

     希望本篇文章对各位C友有所帮助,下篇文章将进入自定义协议章节。


    我是舜华,期待与你的下一次相遇!

  • 相关阅读:
    美创科技获2023浙江民营企业数字化转型“弄潮榜”上榜服务商
    [vue3] 统一页面导航标签
    NFT入门:部署示例等
    基于瞬时无功功率ip-iq的谐波信号检测Simulink仿真
    P5194 [USACO05DEC]Scales S
    【一】Mac 本地部署大模型
    040_小驰私房菜_MTK平台,添加camera客制化size
    自定义RBAC(1)
    redis非关系型数据库(缓存型数据库)——中间件
    4.基于SpringBoot3实现全局异常处理与统一返回封装
  • 原文地址:https://blog.csdn.net/Shun_Hua/article/details/136123143