【Linux】网络编程套接字

发布于:2024-12-20 ⋅ 阅读:(193) ⋅ 点赞:(0)

上一节我们对网络有了一个基础了解,接下来我们直接编写代码,来看看有什么效果

关于网络编程,我发现了一位博客大佬总结的非常详细,我把大佬的博客网址和网络套接字的知识博客贴在下面,方便大家学习,我就直接进行套接字的编程,后续的网络部分的知识都会转载大佬的博客内容(因为小编也在学习,等后续学习结束会总结重点的知识点出来)

注意:套接字是一种特殊的文件描述符

大佬的个人中心

大佬的网络套接字博客


1、UDP协议编程

makefile:

cc=g++
.PHONY:all
all:udpClient udpServer

udpClient:udpClient.cc
	$(cc) -o $@ $^ -std=c++11
udpServer:udpServer.cc
	$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f udpClient udpServer

dict.txt:

apple:苹果
banana:香蕉
orange:橘子
dbb:董斌斌
hello:你好
goodman:好人

1-1、服务端

udpServer.cc

#include "udpServer.hpp"
#include <memory> //智能指针头文件
#include <unordered_map>
#include <fstream>
#include <signal.h>

using namespace Server;

// static void Usage(string proc)//使用手册
// {
//     cout<<"\nUsag:\n\t"<<proc<<"local_ip loacl_port\n\n";
// }
// int main(int argc ,char* argv[])
// {
//     if(argc!=3)
//     {
//         Usage(argv[0]);
//         exit(Usage_ERR);
//     }
//     uint16_t port = atoi(argv[2]);//argv是一个指针数组,里面存的是指针,指向字符串
//     string ip = argv[1];
//     std::unique_ptr<udpServer> usvr(new udpServer(port,ip));
//     usvr->initServer();
//     usvr->start();
//     return 0;
// }

const string dictTxt = "./dict.txt";
unordered_map<string, string> dict;

static bool cutString(const string &line, string *key, string *value, const string sep)
{
    auto pos = line.find(sep);
    if (pos == string::npos)
        return false;
    *key = line.substr(0, pos);
    // pos + sep.size()————我们的sep分割符,不排除是":::","  :::   ","  ###  "这些多种多样的,所以说+ sep.size()才是准确的
    *value = line.substr(pos + sep.size()); // 后面不写substr表示截取到字符串末尾
    return true;
}
static void DictPrint()
{
    for (auto &e : dict)
    {
        cout << e.first << "::::" << e.second << endl;
    }
}
static void initDict() // 小词典
{
    ifstream in(dictTxt, std::ios::binary); // 文件操作
    if (!in.is_open())
    {
        cerr << "open file error" << errno << strerror(errno) << endl;
        exit(OPEN_ERR);
    }
    string line;
    string key, value;
    while (getline(in, line))
    {
        cout << line << endl;
        if (cutString(line, &key, &value, ":"))
        {
            dict.insert(make_pair(key, value));
        }
    }
    in.close();
    cout << "load dict success" << endl;
}

// demo1
void handlerMassage(int sockfd, string clientip, uint16_t clientport, string message)
{
    // 对message进行特殊的业务逻辑处理,不关心message怎么来的——————————server通信和业务逻辑解耦!
    string response_message;
    auto iter = dict.find(message);
    if (iter == dict.end())
        response_message = "The word you searched for was not found";
    else
        response_message = iter->second;

    // 业务处理完成,开始返回
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);

    sendto(sockfd, response_message.c_str(), response_message.size(), 0, (struct sockaddr *)&client, sizeof(client));
}

// 要实现其他业务不需要改其他的,只需要改方法就行//这就是解耦的好处!!
// demo2
void execCommand(int sockfd, string clientip, uint16_t clientport, string cmd)
{
    if (cmd.find("rm") != string::npos || cmd.find("rmdir") != string::npos || cmd.find("mv") != string::npos)
    {
        cerr << "!!!" << clientip << ":" << clientport << "#" << "用户正在做一个非法行为!" << endl;
        return;
    }
    string response;
    FILE *fp = popen(cmd.c_str(), "r"); // 读取出内容
    if (fp == nullptr)
        response = cmd + "exec error";
    // 这里就是popen成功了
    char line[gunm];
    while (fgets(line, sizeof(line), fp))
    {
        response += line;
    }
    pclose(fp);

    // 业务处理完成,开始返回
    struct sockaddr_in client;
    bzero(&client, sizeof(client));
    client.sin_family = AF_INET;
    client.sin_addr.s_addr = inet_addr(clientip.c_str());
    client.sin_port = htons(clientport);

    sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&client, sizeof(client));
}

void reload(int signo) // 捕捉到2号信号之后,重新initDict();
{
    (void)signo;
    initDict();
}
// 因为udpServer.hpp里面有默认ip缺省值0.0.0.0,所以服务端就不需要bind固定的ip
// 服务端直接默认bind默认0.0.0.0或者INADDR_ANY,就可以保证任何访问我服务器8080端口的请求全部接收然后交给8080端口对应的进程处理
static void Usage(string proc) // 使用手册
{
    cout << "\nUsag:\n\t" << proc << "loacl_port\n\n";
}
int main(int argc, char *argv[]) //./udpServer loacl_port
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    // signal(2, reload); ///捕捉2号信号,实现简单的热加载————我们添加新数据到dict.txt里面之后,我们不需要重启服务端,只需要ctrl+c客户端,就能够成功了
    // initDict();
    //  DictPrint();

    uint16_t port = atoi(argv[1]); // argv是一个指针数组,里面存的是指针,指向字符串
    // std::unique_ptr<udpServer> usvr(new udpServer(handlerMassage, port));
    std::unique_ptr<udpServer> usvr(new udpServer(execCommand, port)); // 这里把回调函数一改,就相当于改了上层的业务逻辑!!

    usvr->initServer();
    usvr->start();
    return 0;
}

udpServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;
namespace Server
{

    typedef function<void(int, string, uint16_t, string)> func_t;

    const static string defaultIP = "0.0.0.0";
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        OPEN_ERR
    };
    const static int gunm = 1024;
    class udpServer
    {
    public:
        udpServer(const func_t &cb, const uint16_t &port, const string &ip = defaultIP)
            : _callback(cb), _port(port), _ip(ip), _sockfd(-1)
        {
        }
        void initServer()
        {
            // 1、创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET表示ipv4,SOCK_DGRAM表示面向数据报,也就是udp协议,有了前面两个参数,最后一个可以默认为0
            if (_sockfd == -1)
            {
                cerr << "socket error" << errno << ":" << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success : " << _sockfd << endl;

            // 2、绑定ip+port
            // 下面最重要的是bind端口!因为未来该服务的端口号是一直固定的!是一个明确的端口号,不能随意改变!所以需要用户显示bind
            struct sockaddr_in local;      //_in就是_inet,是网络套接字
            bzero(&local, sizeof(local));  // 将内容初始化为0
            local.sin_family = AF_INET;    // 协议家族
            local.sin_port = htons(_port); // 我们双方要都是大端才能通信,所以要进行主机转网络,进行序列转换才能通信
            // ip地址问题 1、ip要从string转换成为uint32_t类型;2、我们ip也是主机序列要转成大端——htonl()——32字节所以是l不是s
            // inet_addr直接完成了上面的两个工作——字符串转整数,主机序列转网络序列
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这里第一次写就用这种方法,因为上面默认bind0.0.0.0
            // ______如果我们上面没有默认0.0.0.0,那么后续我们在服务端输入ip和端口号绑定的时候,只有与该ip进行通信才能被服务端获取,其他ip通信会丢包

             实际ip地址绑定
             我们服务端不需要绑定自己实际的ip地址,只需要绑定0.0.0.0和端口号就行
             因为服务端直接默认bind默认0.0.0.0或者INADDR_ANY,就可以保证任何访问我服务器8080端口的请求全部接收然后交给8080端口对应的进程处理
             local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址bind,服务器的真实写法
             所以,我们以后服务端的ip进行bind都是INADDR_ANY,这样只要是任何访问的端口的服务我都交给对应进程处理,不会出现丢包的情况

            // 开始绑定,上面都是我们用户的操作,操作系统根本不知道,所以需要调用bind函数来绑定
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n == -1)
            {
                cerr << "bind error" << errno << ":" << strerror(errno) << endl;
                exit(BIND_ERR);
            }
        }
        void start()
        {
            // 服务器的本质就是一个死循环——也就是“常驻内存的进程”
            char buffer[gunm]; // 我们自己定义的缓冲区
            while (1)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                // 这里sizeof(buffer)-1是我们把接收的数据认为是字符串,字符串以'\0'为结束标志
                //(struct sockaddr*)&peer保存是谁发来的数据,传输是数据类型是struct sockaddr*的,所以保存的时候要转换一下
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 返回接收到的字节数

                // 1、数据是什么
                // 2、数据是谁发的
                if (s > 0)
                {
                    // peer里面存放了客户端的信息,但是是网络序列的,我们要进行转换
                    buffer[s] = {0};
                    string clientip = inet_ntoa(peer.sin_addr); // 这里不需要.s_addr,只需要struct in_addr结构体
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;
                    cout << clientip << "[" << clientport << "]#" << message << endl;
                    _callback(_sockfd, clientip, clientport, message);
                }
            }
        }
        ~udpServer()
        {
        }

    private:
        uint16_t _port;   // 端口号
        string _ip;       // ip地址——实际上,一款网络服务器我们不建议指明一个ip,因为我们可能一个机器有多个网卡,客户端可以通过多个ip地址找到服务端机器,然后找到8080类似的端口
        int _sockfd;      // 文件描述符
        func_t _callback; // 回调
    };

}

1-2、客户端

udpClient.cc

#include "udpClient.hpp"
#include <memory>//智能指针头文件
using namespace std;
using namespace Client;
static void Usage(string proc)//使用手册
{
    cout<<"\nUsag:\n\t"<<proc<<"server_ip server_port\n\n";
}
int main(int argc ,char* argv[])//./udpClient server_ip server_port
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    uint16_t serverport = atoi(argv[2]);//argv是一个指针数组,里面存的是指针,指向字符串
    string serverip = argv[1];
    std::unique_ptr<udpClient> ucli(new udpClient(serverip,serverport));
    ucli->initClient();
    ucli->run();
    return 0;
}

udpClient.hpp

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

#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;

namespace Client
{
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR
    };
    const static int gunm = 1024;
    class udpClient
    {
    public:
        udpClient(const string &serverip, const uint16_t &serverport)
            : _sockfd(-1), _serverip(serverip), _serverport(serverport), _quit(false)
        {
        }
        void initClient()
        {
            // 1、创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error" << errno << ":" << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success : " << _sockfd << endl;

            // 2、客户端要不要bind[必须要];要不要明确bind,需不需要程序员自己bind[不需要]
            // 写服务端是一家公司,写客户端是无数家公司——由os自动帮客户端绑定端口
        }
        void run()
        {
            string message;
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);
            char buff[gunm];
            while (!_quit)
            {
                cout << "Please Enter#";
                // getline(cin, message);
                fgets(buff, sizeof(buff), stdin);
                message = buff;
                sendto(_sockfd, message.c_str(), message.length(), 0, (struct sockaddr *)&server, sizeof(server));

                char buffer[gunm];
                struct sockaddr_in temp;
                socklen_t len = sizeof(temp);
                ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
                if (n > 0)
                    buffer[n] = 0;
                cout << "翻译结果:" << buffer << endl;
            }
        }
        ~udpClient()
        {
        }

    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverport;
        bool _quit;
    };

}

上面的业务有两个,一个翻译,一个解析返回命令行结果,接下来我们再写一个业务——在线群聊

1-3、在线群聊

其实就多个人在一个聊天室里面聊天,每一个人发的消息都被广播所有人都可以看到

onlineUser.hpp

#pragma once

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

using namespace std;

class User
{
public:
    User(const string &ip, const uint16_t &port) : _ip(ip), _port(port)
    {
    }
    uint16_t port()
    {
        return _port;
    }
    string ip()
    {
        return _ip;
    }
    ~User()
    {
    }

private:
    uint16_t _port;
    string _ip;
};

class OnlineUser
{
public:
    OnlineUser() {}
    ~OnlineUser() {}
    // port是整形,要与字符串相加需要转换成为字符串
    void addUser(const string &ip, const uint16_t &port) // 注册
    {
        string id = ip + "-" + to_string(port);
        users.insert(make_pair(id, User(ip, port))); // unordered_map在insert的时候,有key值就不添加了
    }
    void delUser(const string &ip, const uint16_t &port) // 注销
    {
        string id = ip + "-" + to_string(port);
        users.erase(id);
    }
    bool isOnline(const string &ip, const uint16_t &port) // 判断是否在线
    {
        string id = ip + "-" + to_string(port);
        return users.find(id) != users.end();
        // return users.find(id)==users.end() ? false : true;
    }
    void broadcastMessage(int sockfd, const string &message, const string &ip, const uint16_t &port) // 广播给所有在线用户
    {
        for (auto &user : users) // for循环依次发给每一个用户
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));
            client.sin_family = AF_INET;
            client.sin_addr.s_addr = inet_addr(user.second.ip().c_str()); // 广播的地址是每一个客户的ip地址,用户的ip地址是在unordered_map的第二个参数里面
            client.sin_port = htons(user.second.port());

            string s = ip + "-" + to_string(port)+"#";//标识每条消息是谁说的!!!要使用每个客户自己的ip+port
            s+=message;
            sendto(sockfd, s.c_str(), s.size(), 0, (struct sockaddr *)&client, sizeof(client));
        }
    }

private:
    // 第一个参数用于存放用户id,第二个参数存放用户的ip+port
    unordered_map<string, User> users; // 所有在线用户
};

udpServer.cc

#include "udpServer.hpp"
#include "onlineUser.hpp"
#include <memory> //智能指针头文件
#include <unordered_map>
#include <fstream>
#include <signal.h>

using namespace Server;
//-----------------------------------聊天室-------------------------------------
OnlineUser onlineuser;
void routeMessage(int sockfd, string clientip, uint16_t clientport, string message)
{
    if (message == "online")
        onlineuser.addUser(clientip, clientport); // 当服务端接收到“online”时,用户进入聊天室
    if (message == "offline")
        onlineuser.delUser(clientip, clientport);  // 当服务端接收到“offline”时,用户退出聊天室
    if (onlineuser.isOnline(clientip, clientport)) // 判断用户是否在线,只有在线用户才能进行聊天
    {
        // 消息的路由
        onlineuser.broadcastMessage(sockfd, message, clientip, clientport);
    }
    else // 用户没有上线,客户端提醒工作
    {
        struct sockaddr_in client;
        bzero(&client, sizeof(client));
        client.sin_family = AF_INET;
        client.sin_addr.s_addr = inet_addr(clientip.c_str());
        client.sin_port = htons(clientport);
        string response = "你还没有上线,请先上线,执行命令:online";
        sendto(sockfd, response.c_str(), response.size(), 0, (struct sockaddr *)&client, sizeof(client));
    }
}
//-----------------------------------聊天室-------------------------------------
// 因为udpServer.hpp里面有默认ip缺省值0.0.0.0,所以服务端就不需要bind固定的ip
// 服务端直接默认bind默认0.0.0.0或者INADDR_ANY,就可以保证任何访问我服务器8080端口的请求全部接收然后交给8080端口对应的进程处理
static void Usage(string proc) // 使用手册
{
    cout << "\nUsag:\n\t" << proc << "loacl_port\n\n";
}
int main(int argc, char *argv[]) //./udpServer loacl_port
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    uint16_t port = atoi(argv[1]); // argv是一个指针数组,里面存的是指针,指向字符串
    // std::unique_ptr<udpServer> usvr(new udpServer(handlerMassage, port));
    // std::unique_ptr<udpServer> usvr(new udpServer(execCommand, port));
    std::unique_ptr<udpServer> usvr(new udpServer(routeMessage, port)); // 这里把回调函数一改,就相当于改了上层的业务逻辑!!

    usvr->initServer();
    usvr->start();
    return 0;
}

服务端的hpp没有变化,和上面的1-1,1-2是一样的

udpServer.hpp

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

using namespace std;
namespace Server
{

    typedef function<void(int, string, uint16_t, string)> func_t;

    const static string defaultIP = "0.0.0.0";
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        OPEN_ERR
    };
    const static int gunm = 1024;
    class udpServer
    {
    public:
        udpServer(const func_t &cb, const uint16_t &port, const string &ip = defaultIP)
            : _callback(cb), _port(port), _ip(ip), _sockfd(-1)
        {
        }
        void initServer()
        {
            // 1、创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0); // AF_INET表示ipv4,SOCK_DGRAM表示面向数据报,也就是udp协议,有了前面两个参数,最后一个可以默认为0
            if (_sockfd == -1)
            {
                cerr << "socket error" << errno << ":" << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success : " << _sockfd << endl;

            // 2、绑定ip+port
            // 下面最重要的是bind端口!因为未来该服务的端口号是一直固定的!是一个明确的端口号,不能随意改变!所以需要用户显示bind
            struct sockaddr_in local;      //_in就是_inet,是网络套接字
            bzero(&local, sizeof(local));  // 将内容初始化为0
            local.sin_family = AF_INET;    // 协议家族
            local.sin_port = htons(_port); // 我们双方要都是大端才能通信,所以要进行主机转网络,进行序列转换才能通信
            // ip地址问题 1、ip要从string转换成为uint32_t类型;2、我们ip也是主机序列要转成大端——htonl()——32字节所以是l不是s
            // inet_addr直接完成了上面的两个工作——字符串转整数,主机序列转网络序列
            local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这里第一次写就用这种方法,因为上面默认bind0.0.0.0
            // ______如果我们上面没有默认0.0.0.0,那么后续我们在服务端输入ip和端口号绑定的时候,只有与该ip进行通信才能被服务端获取,其他ip通信会丢包

             实际ip地址绑定
             我们服务端不需要绑定自己实际的ip地址,只需要绑定0.0.0.0和端口号就行
             因为服务端直接默认bind默认0.0.0.0或者INADDR_ANY,就可以保证任何访问我服务器8080端口的请求全部接收然后交给8080端口对应的进程处理
             local.sin_addr.s_addr = htonl(INADDR_ANY);//任意地址bind,服务器的真实写法
             所以,我们以后服务端的ip进行bind都是INADDR_ANY,这样只要是任何访问的端口的服务我都交给对应进程处理,不会出现丢包的情况

            // 开始绑定,上面都是我们用户的操作,操作系统根本不知道,所以需要调用bind函数来绑定
            int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
            if (n == -1)
            {
                cerr << "bind error" << errno << ":" << strerror(errno) << endl;
                exit(BIND_ERR);
            }
        }
        void start()
        {
            // 服务器的本质就是一个死循环——也就是“常驻内存的进程”
            char buffer[gunm]; // 我们自己定义的缓冲区
            while (1)
            {
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer);
                // 这里sizeof(buffer)-1是我们把接收的数据认为是字符串,字符串以'\0'为结束标志
                //(struct sockaddr*)&peer保存是谁发来的数据,传输是数据类型是struct sockaddr*的,所以保存的时候要转换一下
                ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); // 返回接收到的字节数

                // 1、数据是什么
                // 2、数据是谁发的
                if (s > 0)
                {
                    // peer里面存放了客户端的信息,但是是网络序列的,我们要进行转换
                    buffer[s] = {0};
                    string clientip = inet_ntoa(peer.sin_addr); // 这里不需要.s_addr,只需要struct in_addr结构体
                    uint16_t clientport = ntohs(peer.sin_port);
                    string message = buffer;
                    cout << clientip << "[" << clientport << "]#" << message << endl;
                    _callback(_sockfd, clientip, clientport, message);
                }
            }
        }
        ~udpServer()
        {
        }

    private:
        uint16_t _port;   // 端口号
        string _ip;       // ip地址——实际上,一款网络服务器我们不建议指明一个ip,因为我们可能一个机器有多个网卡,客户端可以通过多个ip地址找到服务端机器,然后找到8080类似的端口
        int _sockfd;      // 文件描述符
        func_t _callback; // 回调
    };

}

这里的客户端cc文件也没有变化
udpClient.cc

#include "udpClient.hpp"
#include <memory>//智能指针头文件
using namespace std;
using namespace Client;
static void Usage(string proc)//使用手册
{
    cout<<"\nUsag:\n\t"<<proc<<"server_ip server_port\n\n";
}
int main(int argc ,char* argv[])//./udpClient server_ip server_port
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    uint16_t serverport = atoi(argv[2]);//argv是一个指针数组,里面存的是指针,指向字符串
    string serverip = argv[1];
    std::unique_ptr<udpClient> ucli(new udpClient(serverip,serverport));
    ucli->initClient();
    ucli->run();
    return 0;
}

udpClient.hpp

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

#include <cstdlib>
#include <cstring>
#include <strings.h>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>

using namespace std;

namespace Client
{
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR
    };
    const static int gunm = 1024;
    class udpClient
    {
    public:
        udpClient(const string &serverip, const uint16_t &serverport)
            : _sockfd(-1), _serverip(serverip), _serverport(serverport), _quit(false)
        {
        }
        void initClient()
        {
            // 1、创建套接字
            _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
            if (_sockfd == -1)
            {
                cerr << "socket error" << errno << ":" << strerror(errno) << endl;
                exit(SOCKET_ERR);
            }
            cout << "socket success : " << _sockfd << endl;

            // 2、客户端要不要bind[必须要];要不要明确bind,需不需要程序员自己bind[不需要]
            // 写服务端是一家公司,写客户端是无数家公司——由os自动帮客户端绑定端口
        }
        static void *recvMessage(void *args) // 该线程只需要读,读出来然后打印就行
        {
            int sockfd = *(static_cast<int *>(args)); // args是void类型,下面需要整形
            pthread_detach(pthread_self());           // 线程分离,主线程做自己的事情,别管我了
            while (1)                                 // 死循环执行
            {
                char buffer[gunm];
                struct sockaddr_in temp;
                socklen_t len = sizeof(temp);
                ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
                if (n > 0)
                    buffer[n] = 0;
                // cout << "翻译结果:" << buffer << endl;
                cout << buffer << endl;
            }
            return nullptr;
        }
        void run()
        {
            pthread_create(&_reader, nullptr, recvMessage, (void *)&_sockfd); // 创建线程,该线程进行读
            // pthread_create(&_writer,nullptr,recvMessage,nullptr);//创建线程,本来是创建两个线程,一个读,一个写。现在直接让主线程进行写

            string message;
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            server.sin_port = htons(_serverport);
            char buff[gunm];
            while (!_quit)
            {
                // cout << "Please Enter#";

                // getline(cin, message);
                // cerr << "#";//用cerr,通过管道fifo,读到2号文件里面
                fprintf(stderr, "Enter#");
                fflush(stderr);
                fgets(buff, sizeof(buff), stdin);//----------------------输入online,offline会读取末尾的\n
                buff[strlen(buff) - 1] = 0;------------解决\n问题
                message = buff;
                sendto(_sockfd, message.c_str(), message.length(), 0, (struct sockaddr *)&server, sizeof(server));

                // char buffer[gunm];
                // struct sockaddr_in temp;
                // socklen_t len = sizeof(temp);
                // ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);
                // if (n > 0)
                //     buffer[n] = 0;
                // //cout << "翻译结果:" << buffer << endl;
                // cout << buffer << endl;
            }
        }
        ~udpClient()
        {
        }

    private:
        int _sockfd;
        string _serverip;
        uint16_t _serverport;
        bool _quit;
        pthread_t _reader;
        // pthread_t _writer;
    };

}

2、TCP协议编程

我们tcp协议的代码编程要多写几个版本,因为类似于单进程这样的写法是存在漏洞的。多个客户端对服务端发起连接请求,因为服务端是accept连接之后是死循环执行,所以单进程只有一个客户端能够与一个服务端对应通信,其他的客户端就不行

我们下面不同版本的tcp协议编程就只把代码的不同部分写出来

2-1、单进程版

makefile:

cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpserver:tcpserver.cc
	$(cc) -o $@ $^ -std=c++11
tcpclient:tcpclient.cc
	$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f tcpclient tcpserver

log.hpp:

#pragma once

#include <iostream>
#include <string>

#define DEBUG 0   // 表示调试
#define NORMAL 1  // 表示正常
#define WORNING 2 // 表示警告
#define ERROR 3   // 表示错误,但是不影响我后续代码继续运行,只是报错
#define FATAL 4   // 致命错误
void logMessage(int level, const std::string &message)
{
    // 日志格式————[日志等级][时间/时间戳][pid][message]
    std::cout << message << std::endl;
}

tcpserver.hpp:

#pragma once

#include <iostream>
#include <string>
#include <string.h>
#include <cstdlib>
// 网络套接字4剑客
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <unistd.h>
#include "log.hpp"
using namespace std;
namespace server
{
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR,
        OPEN_ERR
    };

    const static uint16_t gport = 8081;
    const static int listen_num = 10;
    class tcpserver
    {

    public:
        tcpserver(const uint16_t &port = gport)
            : _listensock(-1), _port(port)
        {
        }
        void initserver()
        {
            // 1、创建套接字
            _listensock = socket(AF_INET, SOCK_STREAM, 0); // AF_INET表示ipv4协议;SOCK_STREAM表示面向字节流(tcp)
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success");

            // 2、bind绑定自己的网络信息
            struct sockaddr_in local; // 这个local是栈上面的,我们填了数据,但是os不知道,所以我们要交给os需要用到bind
            memset(&local, 0, sizeof(local));
            // bzero(&local,sizeof(local)); // 将每个字节全部置0
            local.sin_family = AF_INET;
            // local.sin_family = PF_INET; //AF和PF是一样的
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");

            ----我们的udp上面两步执行完了就可以进行下面的收发数据操作了,但是tcp还不行

            // 我们tcp是要进行连接的——所以你客户端不能直接和我通信,要先和我建立连接
            // 3、设置socket为监听状态!监听状态让服务端一直监听,获取客户端的新连接
            if (listen(_listensock, listen_num) < 0) // 底层真实的值是listen_num+1
            {
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "listen socket success");
        }
        void start()
        {
            while (1)
            {
                // 4、获取新连接
                struct sockaddr_in peer; // 存放客户端ip+port
                socklen_t len = sizeof(peer);
                // accept返回值也是一个套接字(文件描述符) —— 是真正tcp服务端和客户端进行通信的套接字
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    logMessage(ERROR, "accept socket error , next"); // 我服务端这次连接失败了,但是不影响我下次的连接工作
                    continue;                                        // 服务端继续进行服务!
                }
                logMessage(NORMAL, "accept socket success");
                cout << "accept sock : " << sock << endl;

                // 5、这里就有了套接字sock,未来就是用这个套接字进行通信的。因为tc面向字节流,所以后续全部都是文件操作!
                // version 版本1 单进程
                serverIO(sock);
                close(sock);

                // version 版本2 多进程
            }
        }
        void serverIO(int sock)//------单进程
        {
            char buffer[1024];
            while (1)
            {
                ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
                if (n > 0)
                {
                    // 目前将读取的内容当作字符串
                    buffer[n] = 0;
                    cout << "recv message : " << buffer << endl;

                    string outbuffer = "server[echo]";
                    outbuffer += buffer;
                    write(sock, outbuffer.c_str(), outbuffer.size()); // 服务端回应
                }
                else if (n == 0) // 表示客户端退出了!
                {
                    logMessage(NORMAL, "client quit,me too");
                    break;
                }
            }
        }

        ~tcpserver() {}

    private:
        int _listensock; // 不是用来通信,只是用来监听,获取新连接的
        uint16_t _port;
    };
}

tcpserver.cc:

#include "tcpserver.hpp"
#include <memory>
using namespace std;
using namespace server;


// 启动tcpserver方法和udp的一模一样
// ./tcpserver local_port
static void Usage(string proc) // 使用手册
{
    cout << "\nUsag:\n\t" << proc << "loacl_port\n\n";
}
int main(int argc, char * argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<tcpserver> tsvr (new tcpserver(port));
    tsvr->initserver();
    tsvr->start();
    return 0;
}

tcpclient.hpp:

#pragma once

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

#include <unistd.h>
#include "log.hpp"
using namespace std;

namespace client
{
    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR,
        OPEN_ERR
    };
    class tcpclient
    {
    public:
        tcpclient(const string &ip, const uint16_t &port)
            : _serverip(ip), _serverport(port), _sock(-1)
        {
        }
        void initclient()
        {
            // 1、创建套接字
            _sock = socket(AF_INET, SOCK_STREAM, 0);
            if (_sock < 0)
            {
                cout << "create socket error" << endl;
                exit(2);
            }

            // 2、tcp客户端要bind,但是不需要显示bind,由os自动bind
            // 3、客户端不需要listen!
            // 4、客户端也不需要accept!

            // 5、客户端是要发起连接的!
        }
        void start()
        {
            // 5、发起连接
            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_family = AF_INET;
            server.sin_port = htons(_serverport);
            server.sin_addr.s_addr = inet_addr(_serverip.c_str());
            if (connect(_sock, (struct sockaddr *)&server, sizeof(server)) != 0)
            {
                cout << "socket connent error" << endl;
                exit(3);
            }
            else // 连接成功
            {
                string message;
                while (1)
                {

                    cout << "Enter#";
                    getline(cin, message);

                    write(_sock, message.c_str(), message.size());

                    char buff[1024];
                    int n = read(_sock, buff, sizeof(buff) - 1);
                    if (n > 0)
                    {
                        buff[n] = 0; // 当成字符串
                        cout << "server回显" << buff << endl;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
        ~tcpclient()
        {
            if (_sock >= 0)
                close(_sock);
        }

    private:
        int _sock;
        string _serverip;
        uint16_t _serverport;
    };
}

tcpclient.cc:

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

using namespace std;
using namespace client;
static void Usage(string proc) // 使用手册
{
    cout << "\nUsag:\n\t" << proc << "server_ip server_port\n\n";
}
int main(int argc, char * argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    uint16_t serverport = stoi(argv[2]);
    string serverip = argv[1];
    unique_ptr<tcpclient> tcli (new tcpclient(serverip,serverport));
    tcli->initclient();
    tcli->start();
    return 0;
}

上面就是tcp的单进程代码,我们用的是read和write进行读写,tcp里面有专门的读写数据接口,后面会讲到!
在这里插入图片描述

在这里插入图片描述

2-2、多进程

2-2-1、忽略信号版

void Start()
{
       //忽略SIGCHLD信号
	signal(SIGCHLD, SIG_IGN); 
	while(1){
		//父进程负责不断获取客户端的数据请求
		struct sockaddr_in peer;
		memset(&peer, '\0', sizeof(peer));
		socklen_t len = sizeof(peer);
		int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
           //父进程获取失败,则跳过本次,继续获取
		if (sock < 0){
			std::cerr << "accept error, continue next" << std::endl;
			continue;
		}
		std::string client_ip = inet_ntoa(peer.sin_addr);
		int client_port = ntohs(peer.sin_port);
		std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
		
           //父进程获取成功,则创建子进程去提供服务
		pid_t id = fork();
		if (id == 0) //child
		{
			serverIO(sock, client_ip, client_port);
			close(sock);
			exit(0);   //子进程提供完服务就退出
		}
	}

2-2-2、阻塞等待版

我们大部分代码都没有改变,就是tcpserver.hpp

void start()
{
    while (1)
    {
        // 4、获取新连接
        struct sockaddr_in peer; // 存放客户端ip+port
        socklen_t len = sizeof(peer);
        // accept返回值也是一个套接字(文件描述符) —— 是真正tcp服务端和客户端进行通信的套接字
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
        if (sock < 0)
        {
            logMessage(ERROR, "accept socket error , next"); // 我服务端这次连接失败了,但是不影响我下次的连接工作
            continue;                                        // 服务端继续进行服务!
        }
        logMessage(NORMAL, "accept socket success");
        cout << "accept sock : " << sock << endl;

        // 5、这里就有了套接字sock,未来就是用这个套接字进行通信的。因为tc面向字节流,所以后续全部都是文件操作!
        
        // version 版本1 单进程 —— 缺点明显,单进程死循环执行,只有一个客户端才能与服务端通信
        // serverIO(sock);
        // close(sock);

        // version 版本2 多进程
        pid_t id = fork();
        if (id == 0) // 子进程
        {
            // 子进程共享与父进程的所有文件描述符,但是我们子进程只是用来通信交流,所以要关闭不需要的文件描述符
            // 防止1、文件描述符泄露;2、对文件描述符的误操作
            close(_listensock);
            // 我们上面是爷爷进程

            // 这里再fork一次,返回值大于0,所以说是父进程,父进程直接exit退出!那么我们下面的waitpid会直接等待成功!
            if (fork() > 0)
            {
                exit(0);
            }

            //下面的就是孙子进程!孙子进程进行通信交流,最后退出,但是你孙子进程退出了,管我爷爷进程什么事
            //并且!父进程先退出,孙子进程后退出成为了孤儿进程!!!孤儿进由OS领养,进行管理!
            serverIO(sock);
            close(sock);
            exit(0); // 终止子进程
        }
        // 父进程
        // 两个问题:
        // 1、父进程阻塞等待fork,那不还是一个串行化执行流程吗?那我要多进程干嘛呢?
        // 2、父进程阻塞等待fork,但是未来服务多起来,父进程不可能等待完所有的子进程,那么有的子进程就没有父进程回收了

        // 我们就想要阻塞等待的写法
        pid_t ret = waitpid(id, nullptr, 0);
        if (ret > 0)
        {
            cout << "waitpid success : " << ret << endl;
        }
    }
}

注意:我们这种多进程写法是不太好的,因为OS创建一批多进程压力很大,一旦多起来OS很容易忙不过来。其次主要是上面的写法有一点小设计在里面——“爷孙进程”

在这里插入图片描述

2-3、多线程版

makefile里面要带上线程库

cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpserver:tcpserver.cc
	$(cc) -o $@ $^ -std=c++11 -lpthread
tcpclient:tcpclient.cc
	$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f tcpclient tcpserver

tcpserver.hpp:

class tcpserver;//声明存在
class ThreadDate//包含tcpserver对象和soc套接字
{
public:
    ThreadDate(tcpserver *self, int sock)
        : _self(self), _sock(sock)
    {
    }

public:
    tcpserver *_self;
    int _sock;
};


void start()
{
    // signal(SIGCHLD,SIG_IGN);
    while (1)
    {
        // 4、获取新连接
        struct sockaddr_in peer; // 存放客户端ip+port
        socklen_t len = sizeof(peer);
        // accept返回值也是一个套接字(文件描述符) —— 是真正tcp服务端和客户端进行通信的套接字
        int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
        if (sock < 0)
        {
            logMessage(ERROR, "accept socket error , next"); // 我服务端这次连接失败了,但是不影响我下次的连接工作
            continue;                                        // 服务端继续进行服务!
        }
        logMessage(NORMAL, "accept socket success");
        cout << "accept sock : " << sock << endl;

        // 5、这里就有了套接字sock,未来就是用这个套接字进行通信的。因为tc面向字节流,所以后续全部都是文件操作!
        // version 版本3 多线程
        pthread_t tid;
        // 静态方法无法访问非进程成员,所以需要this
        // 但是我们的threadRoutine还需要主线程的套接字sock,所以我们定义一个结构体,里面包含tcpserver结构体自己(this)
        // 和主线程里面的sock套接字
        ThreadDate *td = new ThreadDate(this, sock);
        pthread_create(&tid, nullptr, threadRoutine, td);

        // pthread_join(tid,nullptr);//父线程阻塞等待,和上面一样多此一举,但是多线程没有非阻塞等待。直接在子线程里面线程分离就行
    }
}
static void *threadRoutine(void *args) // 静态方法无法访问非进程成员,所以需要this
{
    pthread_detach(pthread_self());//线程分离!!主线程就别join子线程了,你跑你的,我跑我的
    ThreadDate *td = static_cast<ThreadDate *>(args);
    td->_self->serverIO(td->_sock);
    close(td->_sock);//子线程与客户端通信完成之后,关闭子线程服务端的socket套接字
    delete td;
    return nullptr;
}

就大体上看,没错是的,多进程多线程的版本挺不错的,但是多进程多线程都是连接来了之后进行创建,每次都要进行创建工作,太麻烦了。另外,我们如果使用进程池,进程池内的进程想将自己的文件描述符给其他进程是比较麻烦的,但是线程池内的线程打开的文件描述符其他线程容易拿到

2-4、线程池版

makefile

cc=g++
.PHONY:all
all:tcpserver tcpclient
tcpserver:tcpserver.cc
	$(cc) -o $@ $^ -std=c++11 -lpthread
tcpclient:tcpclient.cc
	$(cc) -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f tcpclient tcpserver

2-4-1、完善日志文件

1、直接打印日志信息

#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

using namespace std;



#define DEBUG 0   // 表示调试
#define NORMAL 1  // 表示正常
#define WORNING 2 // 表示警告
#define ERROR 3   // 表示错误,但是不影响我后续代码继续运行,只是报错
#define FATAL 4   // 致命错误

const char* levelstr(int level)
{
    switch(level)
    {
        case DEBUG: return "DEBUG";
        case NORMAL: return "NORMAL";
        case WORNING: return "WORNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default:return nullptr;
    }
}

// 可变参数列表,加上..就可以在调用logMessage的地方进行打印数据
void logMessage(int level, const char *format, ...)
{
    // 日志格式————[日志等级][时间/时间戳][pid][message]
    // 可变参数列表需要:
    //1、va_list自动向后移动;2、va_arg()一次向后面移动多少个字节;3、va_start()指向打印信息的开始;4、va_end()指向空;
#define Num 1024
    char logprefix[Num];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]",levelstr(level),(long int)time(nullptr),getpid());


    char logcontent[Num];//这里是可变的
    va_list arg;
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);
    cout << logprefix << logcontent << endl;
    
}

2、日志信息写入文件

#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

using namespace std;

#define LOG_NORMAL "log.txt"  // 没有错误的日志
#define LOG_ERROR "log.error" // 有错误的日志

#define DEBUG 0   // 表示调试
#define NORMAL 1  // 表示正常
#define WORNING 2 // 表示警告
#define ERROR 3   // 表示错误,但是不影响我后续代码继续运行,只是报错
#define FATAL 4   // 致命错误

const char *levelstr(int level)
{
    switch (level)
    {
    case DEBUG:
        return "DEBUG";
    case NORMAL:
        return "NORMAL";
    case WORNING:
        return "WORNING";
    case ERROR:
        return "ERROR";
    case FATAL:
        return "FATAL";
    default:
        return nullptr;
    }
}

void logMessage(int level, const char *format, ...)
{
#define Num 1024
    char logprefix[Num];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", levelstr(level), (long int)time(nullptr), getpid());

    char logcontent[Num];
    va_list arg;
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);

    ----向文件写入日志!
    FILE *log = fopen("LOG_NORMAL", "wa");
    FILE *err = fopen("LOG_ERROR", "wa");
    if (log != nullptr && err != nullptr)
    {
        FILE *cur = nullptr;
        if (level == DEBUG || level == NORMAL || level == WORNING)
            cur = log;
        if (level == ERROR || level == FATAL)
            cur = err;
        if (cur)
            fprintf(cur, "%s%s\n", logprefix, logcontent);

        fclose(log);
        fclose(err);

    }
}


2-4-2、线程池的代码

任务Task.hpp:

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include <functional>
#include <cstdio>
#include <cstring>

using namespace std;

void serverIO(int sock) // serverIO和服务端连接通信其实没什么关系,通信,业务逻辑处理解耦
{
    char buffer[1024];
    注意:未来我们线程池的业务逻辑处理不能是死循环!不然线程越来越少
    while (1)
    {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            // 目前将读取的内容当作字符串
            buffer[n] = 0;
            cout << "recv message : " << buffer << endl;

            string outbuffer = "server[echo]";
            outbuffer += buffer;
            write(sock, outbuffer.c_str(), outbuffer.size()); // 服务端回应
        }
        else if (n == 0) // 表示客户端退出了!
        {
            logMessage(NORMAL, "client quit,me too");
            break;
        }
        else
        {
            cerr << sock << " read error!" << endl;
            break;
        }
        close(sock);//处理完的话,在内部就把套接字关闭
    }
}

class Task
{
    using func_t = std::function<void(int)>; // 回调函数,上面的serverIO

public:
    Task()
    {
    }
    Task(int sock, func_t func)
        : _sock(sock), _callback(func)
    {
    }
    void operator()()
    {
        _callback(_sock);
    }
    ~Task()
    {
    }

private:
    int _sock;
    func_t _callback;
};

Thread.hpp:

#pragma once

#include <iostream>
#include <pthread.h>
#include <string>
#include <cstring>
#include <functional>
#include <cassert>
#include <unistd.h>

// class Thread;

// class Context // 上下文,将pthread_create中类内的第4个参数和this合并
// {
// public:
//     Context()
//         : _this(nullptr),
//           _args(nullptr)
//     {
//     }
//     ~Context()
//     {
//     }
//     Thread *_this; // 调用函数的this指针(线程当前的对象)
//     void *_args;   // 线程当前执行函数的参数
// };

// class Thread
// {
// public:
//     // using func_t = std::function<void*(void*)>;作用同下
//     typedef std::function<void *(void *)> func_t;
//     const int num = 1024;
//     //这样改就可以像C++一样,直接构造一个线程,然后传线程执行函数就行,不需要传线程函数参数和线程编号了!
//     //Thread(func_t func, void *args = nullptr, int number = 0)
//     Thread(func_t func, void *args, int number)
//         : _func(func),
//           _args(args)
//     {
//         // _name = "thread : ";
//         // _name += std::to_string(number);//和下面snprintf作用相同
//         char buffer[num];
//         snprintf(buffer, sizeof buffer, "thread : %d", number);
//         _name = buffer;

//         //void start()//可以直接把start函数拿进来
//         //{
//         Context *cnt = new Context();
//         cnt->_args = _args;
//         cnt->_this = this;
//         // 这里直接把cnt进行传参,直接把start_routine的参数和this指针都传过去了
//         int n = pthread_create(&_tid, nullptr, start_routine, cnt); // 这里_func会报错,C接口不能直接掉C++的函数对象
//         assert(0 == n);                                             // 线程创建成功函数返回值为0
//         (void)n;                                                    // 编译器在release版本会注释掉assert。会发现我们没有使用n,有的编译器就会报存在未使用变量n的警告,我们这里取消这个警告
//         //}
//     }

//     // 在类内创建线程,想让线程执行对应的方法,需要将方法设置为static(静态方法) —— 因为static类内函数没有this指针!

//     static void *start_routine(void *args) // 写一个函数,方便我们下面pthread_create第3个参数使用
//     {
//         // 很不幸,下面还是不能直接使用start_routine函数,因为start_routine是类内函数,有缺省参数!
//         // 也就是说start_routine有两个参数,第一个参数是Thread* this指针,第二个参数才是void* args
//         // return _func(args);

//         // 这里就又出问题了,静态函数只能调用静态方法和静态成员,不能调用类内成员方法和成员变量!
//         // 所以得换一种写法

//         Context *cnt = static_cast<Context *>(args);
//         void *ret = cnt->_this->run(cnt->_args); // 这里调用下面的run函数
//         delete cnt;
//         return ret;
//     }
//     // void start()//这里把start放外面,调用的时候要让线程调用start
//     // {
//     //     Context *cnt = new Context();
//     //     cnt->_args = _args;
//     //     cnt->_this = this;
//     //     // 这里直接把cnt进行传参,直接把start_routine的参数和this指针都传过去了
//     //     int n = pthread_create(&_tid, nullptr, start_routine, cnt); // 这里_func会报错,C接口不能直接掉C++的函数对象
//     //     assert(0 == n);                                             // 线程创建成功函数返回值为0
//     //     (void)n;                                                    // 编译器在release版本会注释掉assert。会发现我们没有使用n,有的编译器就会报存在未使用变量n的警告,我们这里取消这个警告
//     // }

//     void join()
//     {
//         int n = pthread_join(_tid, nullptr);
//         assert(n == 0);
//         (void)n;
//         //printf("%s\n", strerror(n));
//     }

//     void *run(void *args) // 给上面start_routine来用的
//     {
//         return _func(args);
//     }

//     ~Thread()
//     {
//     }

// private:
//     std::string _name; // 我们想直接看线程名字,比如线程1,线程2这种
//     pthread_t _tid;//线程的tid
//     func_t _func; // 线程未来执行的函数
//     void *_args;  // 线程执行函数的参数
// };

namespace my_thread
{
    typedef std::function<void *(void *)> func_t;
    const int num = 1024;
    class Thread
    {
        // 在类内创建线程,想让线程执行对应的方法,需要将方法设置为static(静态方法) —— 因为static类内函数没有this指针!
        static void *start_routine(void *args)
        {
            Thread *_this = static_cast<Thread *>(args);
            return _this->callback();
        }

    public:
        Thread()//构造不传参数
        {
            char namebuffer[num];
            snprintf(namebuffer, sizeof namebuffer, "thread : %d", threadnum++);
            _name = namebuffer;
        }
        void start(func_t func, void *args = nullptr)
        {
            _func = func;
            _args = args;
            int n = pthread_create(&_tid, nullptr, start_routine, this); // 这里直接传this指针
            assert(0 == n);
            (void)n;
        }
        // Thread(func_t func, void *args = nullptr) // 不要number了
        //     : _func(func),
        //       _args(args)
        // {
        //     char namebuffer[num];
        //     snprintf(namebuffer, sizeof namebuffer, "thread : %d", threadnum++);
        //     _name = namebuffer;
        // }
        // void start()
        // {
        //     int n = pthread_create(&_tid, nullptr, start_routine, this); // 这里直接传this指针
        //     assert(0 == n);
        //     (void)n;
        // }
        void join()
        {
            int n = pthread_join(_tid, nullptr);
            assert(n == 0);
            (void)n;
        }
        void *callback() // 不用传参数,回调函数自动调用类内成员变量 —— 下面的_args
        {
            return _func(_args); // 回调函数自动调用类内成员变量
        }
        std::string threadname() // 拿取线程名称
        {
            return _name;
        }
        ~Thread()
        {
        }

    private:
        std::string _name;    // 我们想直接看线程名字,比如线程1,线程2这种
        pthread_t _tid;       // 线程的tid
        func_t _func;         // 线程未来执行的函数
        void *_args;          // 线程执行函数的参数
        static int threadnum; // 这里为了避免麻烦就不加锁了
    };
    int Thread::threadnum = 1;
}

LockGuard.hpp:

#pragma once

#include <iostream>
#include <pthread.h>

using std::cout;
using std::endl;

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr)
        : _lock_p(lock_p)
    {
    }
    void lock()
    {
        if (_lock_p) // 锁不为空,才表示要设置锁
            pthread_mutex_lock(_lock_p);
    }
    void unlock()
    {
        if (_lock_p) // 锁不为空,表示有锁需要我们解锁
            pthread_mutex_unlock(_lock_p);
    }
    ~Mutex() {}

private:
    pthread_mutex_t *_lock_p; //我们这里没有锁,需要外面传锁进来
};

class LockGuard
{
public:
    LockGuard(Mutex mutex)
        : _mutex(mutex)
    {
        _mutex.lock(); // 在构造函数中加锁
    }
    ~LockGuard()
    {
        _mutex.unlock(); // 在析构函数中解锁
    }

private:
    Mutex _mutex;
};

ThreadPool.hpp:

#pragma once

#include "Thread.hpp"
#include "LockGuard.hpp"
#include "log.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
#include <mutex>

using namespace my_thread;

using std::cerr;
using std::cin;
using std::cout;
using std::endl;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
const int gnum = 5;

template <class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadData(ThreadPool<T> *tp, const std::string &name)
        : _threadpool(tp),
          _name(name)
    {
    }
    ~ThreadData()
    {
    }

public:
    ThreadPool<T> *_threadpool;
    std::string _name;
};

template <class T>
class ThreadPool
{
public:
    void mylock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void myunlock()
    {
        pthread_mutex_unlock(&_mutex);
    }
    bool myQueueEmpty()
    {
        return _task_queue.empty();
    }
    void myThreadWait()
    {
        pthread_cond_wait(&_cond, &_mutex);
    }
    T Pop()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }
    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

private:
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = static_cast<ThreadData<T> *>(args);
        while (true)
        {
            T t;
            {
                LockGuard lockguard(td->_threadpool->mutex()); // RAII —— 要构建临时对象!!!
                while (td->_threadpool->myQueueEmpty())
                {
                    td->_threadpool->myThreadWait();
                }
                t = td->_threadpool->Pop();
            }
            t();//回调serverIO
        }
        delete td;
        return nullptr;
    }

    // ThreadPool(const int &num = gnum) 构造函数要放到private里面 —————— 单例模式还有是个单例呢,不是没有例了!!!
    //     : _num(num)
    // {
    //     pthread_mutex_init(&_mutex, nullptr);
    //     pthread_cond_init(&_cond, nullptr);
    //     for (size_t i = 0; i < _num; ++i)
    //     {
    //         _threads.push_back(new Thread());
    //     }
    // }
    // // 赋值拷贝还有许多地方要进行研究:支不支持连等,返回值是什么类型的,允不允许自己赋值自己
    // void operator=(const ThreadPool &) = delete; // 赋值语句要取消!!!!
    // // 拷贝构造没有void
    // ThreadPool(const ThreadPool &) = delete; // 拷贝构造要取消!!!!

public:
    ThreadPool(const int &num = gnum) 构造函数要放到private里面 —————— 单例模式还有是个单例呢,不是没有例了!!!
        : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (size_t i = 0; i < _num; ++i)
        {
            _threads.push_back(new Thread());
        }
    }
    void run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            //printf("%s run... \n", t->threadname().c_str());
            logMessage(DEBUG,"%s run...",t->threadname().c_str());
        }
    }
    void Push(const T &in)
    {
        LockGuard lockguard(&_mutex); // RAII —— 要构建临时对象!!!
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }
    ///一般一个类内函数,既属于类又属于对象;我们这里加上static,使得函数只属于类,不属于对象!!!
    static ThreadPool<T> *getInstance() // 单例模式这里可以传参的
    {
        if (tp == nullptr) // 增加并发度,不要每一次进来都直接面对申请锁
        {
            _singlock.lock();
            if (tp == nullptr)
            {
                tp = new ThreadPool<T>();
            }
            _singlock.unlock();
        }
        return tp;
    }

private:
    int _num;
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;

    static ThreadPool<T> *tp; // 要构建一个静态的指针
    static std::mutex _singlock;
};

template <class T> // 静态指针tp的定义
ThreadPool<T> *ThreadPool<T>::tp = nullptr;

template <class T>
std::mutex ThreadPool<T>::_singlock;

tcpserver.hpp:

#pragma once

#include <iostream>
#include <string>
#include <string.h>
#include <unistd.h>
#include <cstdlib>
// 网络套接字4剑客
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "log.hpp"

// 线程池
#include "Task.hpp"
#include "ThreadPool.hpp"
using namespace std;
namespace server
{

    enum
    {
        Usage_ERR = 1,
        SOCKET_ERR,
        BIND_ERR,
        LISTEN_ERR,
        OPEN_ERR
    };

    const static uint16_t gport = 8081;
    const static int listen_num = 10;

    class tcpserver; // 声明存在
    class ThreadDate // 包含tcpserver对象和soc套接字
    {
    public:
        ThreadDate(tcpserver *self, int sock)
            : _self(self), _sock(sock)
        {
        }

    public:
        tcpserver *_self;
        int _sock;
    };

    class tcpserver
    {

    public:
        tcpserver(const uint16_t &port = gport)
            : _listensock(-1), _port(port)
        {
        }
        void initserver()
        {
            // 1、创建套接字
            _listensock = socket(AF_INET, SOCK_STREAM, 0); // AF_INET表示ipv4协议;SOCK_STREAM表示面向字节流(tcp)
            if (_listensock < 0)
            {
                logMessage(FATAL, "create socket error");
                exit(SOCKET_ERR);
            }
            logMessage(NORMAL, "create socket success : %d",_listensock);

            // 2、bind绑定自己的网络信息
            struct sockaddr_in local; // 这个local是栈上面的,我们填了数据,但是os不知道,所以我们要交给os需要用到bind
            memset(&local, 0, sizeof(local));
            // bzero(&local,sizeof(local)); // 将每个字节全部置0
            local.sin_family = AF_INET;
            // local.sin_family = PF_INET; //AF和PF是一样的
            local.sin_port = htons(_port);
            local.sin_addr.s_addr = INADDR_ANY;
            if (bind(_listensock, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                logMessage(FATAL, "bind socket error");
                exit(BIND_ERR);
            }
            logMessage(NORMAL, "bind socket success");

            ----我们的udp上面两步执行完了就可以进行下面的收发数据操作了,但是tcp还不行

            // 我们tcp是要进行连接的——所以你客户端不能直接和我通信,要先和我建立连接
            // 3、设置socket为监听状态!监听状态让服务端一直监听,获取客户端的新连接
            if (listen(_listensock, listen_num) < 0) // 底层真实的值是listen_num+1
            {
                logMessage(FATAL, "listen socket error");
                exit(LISTEN_ERR);
            }
            logMessage(NORMAL, "listen socket success");
        }
        void start()
        {

            4、线程池初始化
            ThreadPool<Task>::getInstance()->run();
            // signal(SIGCHLD,SIG_IGN);
            while (1)
            {
                // 4、获取新连接
                struct sockaddr_in peer; // 存放客户端ip+port
                socklen_t len = sizeof(peer);
                // accept返回值也是一个套接字(文件描述符) —— 是真正tcp服务端和客户端进行通信的套接字
                int sock = accept(_listensock, (struct sockaddr *)&peer, &len);
                if (sock < 0)
                {
                    logMessage(ERROR, "accept socket error , next"); // 我服务端这次连接失败了,但是不影响我下次的连接工作
                    continue;                                        // 服务端继续进行服务!
                }
                logMessage(NORMAL, "accept socket success,get new sock : %d",sock);
                

                // 5、这里就有了套接字sock,未来就是用这个套接字进行通信的。因为tc面向字节流,所以后续全部都是文件操作!
                // version 版本4 线程池
                ThreadPool<Task>::getInstance()->Push(Task(sock, serverIO));
                
            }
        }
        ~tcpserver() {}

    private:
        int _listensock; // 不是用来通信,只是用来监听,获取新连接的
        uint16_t _port;
    };
}

3、守护进程

3-1、守护进程相关概念

1、守护进程的概念

守护进程也叫做精灵进,是运行在后台的一种特殊进程(本质上是一个孤儿进程)。它独立于控制终端并且可以周期性的执行某种任务或者处理某些发生的事件。
守护进程是非常有用的进程,在Linux当中大多数服务器用的就是守护进程。比如,web服务器http等,同时守护进程完成很多系统的任务。当Linux系统启动的时候,会启动很多系统服务,这些进程服务是没有终端的,也就是你把终端关闭了,这些系统服务是不会停止的,它们一直运行着。它们有一个名字,就叫做守护进程。
一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则,一直会在运行。)

2、进程组和会话
进程组的相关概念:

进程除了有进程PID之外,还有一个进程组,进程组是一个进程或者多个进程组成。通常他们与同一作业相关联,可以收到同一终端的信号;
每个进程组有唯一的进程组ID,每一个进程组有一个进程组组长。如何判断一个进程是不是这个进程组的组长?通常进程ID等于进程该进程组ID,那么该进程就是该进程组的组长。进程组中第一个被创建出来的就是组长

会话组的相关概念:

会话是有一个或者多个进程组组成的集合
一个会话可以有一个终端,建立与控制终端连接的会话首进程被成为控制进程,一个会话的几个进程组可以分为前台进程和后台进程,而这些进程组的控制终端相同,也就是sesion
id是一样的。当用户使用Ctrl + c 产生SIGINT信号时,内核会发送信号给相应的前台进程组的所有进程。
如果运行一个程序,我想把它放到后台运行,可以在可执行程序后面加一个&; 如果想把后台进程提到前台,可以使用fg
jobs指令可以查看当前会话的后台进程 将前台进程放到后台,Ctrl + z | bg + 任务编号

一个会话只有1个前台程序和n个后台程序,作业之间可以前后台转换。这样的让我可能会收到用户登录和注销被清理

现象:
在这里插入图片描述
意义:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

所以

我们在命令行中启动一个进程,现在就可以叫做在会话中启动一个进程组,来完成某种任务;
所有会话内的进程fork创建子进程,一般而言依旧属于当前会话。

样例:像平时,当我们觉得Windows卡顿的时候,我们可能会重新注销一下。注销就是让用户退出登录后再重新登陆,那么此时就相当于给你新建一个会话。卡顿是因为你本次登陆过程中启动了很多任务,且都属于同一个会话,注销本质就是把你内部会话的所有进程组删掉。

注意:

1、在登录的状态时,新起了一个网络服务器,创建好之后,在派生的子进程也属于当前会话,所以我们就不能让这个网络服务器属于这个会话内容,要不然它会受到用户的登录和注销的影响。
2、所以,当我们有个网络服务的时候,应该脱离这个会话,让它独立的在计算机里自成进程组,自成新会话。这样在两个用户同时登录的时候,形成的两个会话是独立的,在操作各自的bash不会相互影响。
3、像这种自成进程组,自成新会话,而且周而复始的进程称为守护进程(精灵进程)。

3-2、守护进程的方式

守护进程自成进程组,自成会话
在这里插入图片描述
谁都管不了,除非用户登录kill

我们这里有三种方式让自己的进程守护进程化:

自己写daemon函数,推荐使用这种方式
用系统的daemon函数
nohup命令

1、TCP网络程序(守护进程化)
之前的TCP网络程序是在前台运行的,但是实际上服务器并不是在前台运行的,而是在后台运行的。所以现在对TCP网络程序的代买进行修改,加上一个小组件,使其守护进程化,让服务器在后台运行。编写daemon.hpp文件完成守护进程的主要逻辑,具体如下:

1、忽略一些不需要的异常信号,防止进程被信号杀死,如:调用signal函数忽略SIGPIPE信号;
2、更改进程的工作目录(选做);
3、 fork创建子进程,exit让父进程退出。让执行服务的进程不是进程组组长,从而保证后续不会再和其他终端相关联;
4、调用setsid函数设置自己是一个独立的会话(setsid不能设置进程组组长的进程);
5、将标准输入、标准输出、标准错误重定向到/dev/null(一种文件,不能写也不能读);

进程守护化需要调用setsid()函数,注意点如下:

SETSID(2)      
#include <unistd.h>
pid_t setsid(void);

用途:使用setsid可以使调用进程成为一个新的会话领导者,并且不会受到终端的控制。这对于守护进程和后台进程非常有用,因为它们需要在后台运行,并且不希望受到终端的影响。调用setsid后,调用进程的进程组ID将变为新会话的组ID,该进程成为新会话的领导进程,并且不再有控制终端。进程组的组长不能调用setsid()函数来创建一个新的会话。但是,进程调用完setsid(),从不是组长变成了组长!

参数:无
调用成功返回值:返回新会话的会话ID
调用失败返回值:失败返回-1

daemon.hpp:

#pragma once
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstdlib>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define DEV "/dev/null"//数据黑洞,向它写入的数据会被吃掉,读取数据什么都读不到(不会使进程退出)
void DaemonSele(const char* currrPath=nullptr)
{
    //1、让调用进程屏蔽异常的信号
    //SIGPIPE信号会在进程向一个已经关闭的socket连接写数据时产生,如果不处理这个信号,进程会被强制退出。通过忽略SIGPIPE信号,可以避免进程因为这个信号而退出。
    signal(SIGPIPE,SIG_IGN);
    //2、让自己不是组长,调用setsid
    if(fork()>0) exit(0);
    //子进程——守护进程也称精灵进程,本质就是一个孤儿进程
    pid_t n=setsid();//进程不是组长的时候才能调用setsid,但是调用完之后,进程成为了组长
    assert(n!=-1);//失败返回-1
    //3、守护进程脱离终端,所以要关闭或重定向进程默认打开的文件及文件描述符
    int fd=open(DEV,O_RDWR);//以读写的方式打开文件黑洞
    if(fd>=0)//创建成功:重定向
    {
        dup2(fd,0);//将fd覆盖标准输入
        dup2(fd,1);
        dup2(fd,2);
        close(fd);
    }
    else//创建失败:手动关闭文件描述符
    {
        close(0);
        close(1);
        close(2);
    }
    //4、进程执行路径更改(可改可不改)
    if(currrPath)
    {
        chdir(currrPath);
    }
}

问题:进程变成守护进程后,打印的日志信息不见了,要怎么办?
解决:日志持久化,将日志输出追加打印到log.txt文件中!

在这里插入图片描述

最后:我们只需要在服务端的main函数命令行参数信息处理后调用此daemon函数即可:
在这里插入图片描述

测试结果:

现在我们运行服务端,通过下面的监控脚本辅助观察信息:
[xzy@ecs-333953 tcp]$ ps axj | head -1 && ps axj | grep serverTcp
[xzy@ecs-333953 tcp]$ ps axj | head -1 && ps axj | grep sshd

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

Linux自带生成守护进程的接口:

DAEMON(3)  
#include <unistd.h>
int daemon(int nochdir, int noclose);

用途:该函数的意义在于将进程转变为守护进程,守护进程是在后台运行的进程,通常不与控制台交互,而是在后台执行某些任务,如网络服务器等。通过调用该函数,可以实现以下功能:

将当前进程的父进程置为init进程(进程id为1),从而脱离原有的进程组和会话。
将当前进程的工作目录切换到根目录下,以避免守护进程因为当前工作目录被卸载等原因导致崩溃。
关闭标准输入、输出和错误输出,以避免守护进程输出信息到控制台,从而影响用户体验。

参数:
nochdir:是否改变当前工作目录。如果为0,则将当前工作目录切换到根目录下,否则保持不变。
noclose:是否关闭标准输入、标准输出、标准错误的文件描述符。如果为0,则不关闭,否则关闭。
调用成功返回值:返回0
调用失败返回值:失败返回-1,并设置errno变量。

3-3、守护进程的使用

tcpserver.cc

#include "tcpserver.hpp"
#include "daemon.hpp"
#include <memory>
using namespace std;
using namespace server;


// 启动tcpserver方法和udp的一模一样
// ./tcpserver local_port
static void Usage(string proc) // 使用手册
{
    cout << "\nUsag:\n\t" << proc << "loacl_port\n\n";
}
int main(int argc, char * argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(Usage_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<tcpserver> tsvr (new tcpserver(port));
    tsvr->initserver();
    DaemonSele();//运行代码,进行守护进程化
    tsvr->start();//启动服务器之前,守护进程化
    return 0;
}

在这里插入图片描述
客户端随便访问,但是服务端在后台给我们提供服务,我们看不我们退出xshell也是可以继续访问服务端的

4、补充知识点(重点)

1、进程已经有pid了,为什么还要有port呢?

a.系统是系统,网络是网络,单独设置——系统与我那个解耦
b.需要客户端每次都能找到服务器进程——服务器的唯一性不能做任何改变——IP+port——port不能随意更改,不能使用会轻易改变的值
c.不是所有的进程都要提供网络服务或者请求,但是所有的进程都需要pid

2、我们在网络通信的过程中,IP+PORT标识唯一性,client ->server,除了数据,要把自己的ip和port发给对方吗?是的需要,我们还要发回来——未来发送数据的时候,一定会“多发”一部分数据以协议的形式呈现

3、我们第一次通信,怎么知道对方端口号呢?

那是因为:我们服务端和客户端是一家公司做出来的!提前在服务端或者客户端内内嵌好了对方的端口号,所以我们现实是不需要输入端口号的(我们访问各种知名网站(百度,网易,腾讯等等),都不需要我们输入端口号),答案就是我们已经有那些访问服务端的端口号了

4、快速识别大小端

低字节/低权值数据——放在低地址处——就是小端——口诀“小小小“
反之则是大端

5、服务端在bind的时候,如果不需要指明固定ip,那么都是bind默认的INADDR_ANY或者0.0.0.0,因为这样我们可以收到任何的ip发送的内容,然后交给对应端口的进程,不会丢包。如果我们显示的bind固定的ip,那么除了与该ip通信的内容会被接收以外,其他的ip发送的内容将会被丢弃!!!

所以,一般服务端除非在指明了要bind某ip的特殊用途以外,都是bind默认0.0.0.0或者INADDR_ANY

简单例子:
1、我服务端收到了各地的客户端的数据,来了这么一堆数据,通过知识点5的结论,我服务端不管目的地址了,只关心端口号,所有访问我8080端口的全部交给8080,访问我8081的交给我8081…就不需要关注服务端的网卡ip地址了
2、我服务器有多个网卡,多个ip地址,但是今天我这个8080的相关进程就bind了我机器的一个ip地址,那么如果是服务端其他ip地址收到了给8080端口的数据,服务端不会接收到!会直接丢弃!

注意:只是ip地址的bind有默认值了,端口号port没有默认值的情况下还是要手动绑定的

6、一个端口只能绑定一个进程,一个进程可以绑定多个端口

7、服务端在bind过程中,最重要的是bind端口!因为未来该服务的端口号是一直固定的!是一个明确的端口号,不能随意改变!所以需要用户显示bind

所以说客户端都是由os自动帮客户端绑定端口——在客户端第一次sendto发送数据的时候,os会帮我们生成随机bind一个端口

客户端虽然也需要端口号,但一般并不需要进行绑定,只需在访问服务端时,客户端的端口号是唯一的即可,无需与特定的客户端进程强相关。如果客户端绑定了某个端口号,那么这个端口号以后就只能给这一个客户端使用,而如果这个客户端没有启动,这个端口号也就无法分配给别人,另外,如果这个端口号被别人使用了,这个客户端也就无法启动了。因此,客户端的端口号只需要保证唯一性而无需进行绑定,这样,客户端的端口号可以动态地进行设置(一般是调用类似于 sendto() 这样的接口,操作系统会自动给当前客户端分配一个唯一的端口号)。也就是说,客户端每次启动时使用的端口号可能是不同的,且只要系统中的端口号没有被耗尽,客户端就永远可以启动。

简单例子:我们手机或者电脑上面有许多app,每一个app启动之后都需要一个端口号,如果说,今天我微信启动,显示绑定1234端口,不然不启动,我抖音也是要绑定1234端口…多个app都绑定了同一个端口,那么就导致,只有一个app在运行,其他的app都卡住了。
所以客户端的端口号不重要,只需要有唯一性就行,保证多个app能同时在手机或者电脑运行

8、getline函数知识点

1、getline参数1表示从哪里读,参数2表示存放到哪里
2、getline的返回值是一个流,但是编译器做了各种类型转换(就是函数重载),所以可以直接用在while循环里面做bool值的判断

9、FILE *popen(const char *command, const char *type);
   int pclose(FILE *stream);

popen = pipe+fork+exec*

command——是我们linux中的命令,如ls -a -l…
type——是我们文件操作方式,如rwa,读写追加…

样例:

string response;
FILE *fp = popen(cmd.c_str(), "r"); // 读取出内容
if (fp == nullptr)
	response = cmd + "exec error";
// 这里就是popen成功了
char line[gunm];
while (fgets(line, sizeof(line), fp))
{
	response += line;
}
pclose(fp);

10、在我们上面的udp和tcp协议的编程中,我们IO类的接口都不需要进行主机转网络,网络转主机(htons,inet_addr/inet_ntoa…),是因为我们IO类函数内部已经帮我们完成了,只不过函数进行了封装,我们看不到,但是我们将数据拿到本地之后(peer等结构体里面),我们想要拿出来打印或者进行业务处理,就需要进行网络转主机操作了

11、我们tcp在listen的时候,监听了一个套接字,但是accept这个套接字之后,又返回了一个套接字:

我们listen时的套接字,是专门用来监听是否有新连接来到的
而accept返回的套接字,是服务端用来与客户端进行通信的
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
返回值的套接字才是tpc服务端与客户端进行通信的套接字
而参数的套接字是用来获取下层listen监听到的客户端连接请求

所以tcp的服务端与客户端进行通信的套接字是accept返回的文件描述符

5、总结

本章内容主要是了解udp和tcp的一些编程接口和使用,然后完成了基于两种协议的基础通信功能