【网络】tcp_socket


一、tcp_server与udp_server一样的部分

#pragma once

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h> 
#include "Log.hpp"

Log lg;

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const uint16_t defaultport = 8080;

enum
{
    SocketERR=2,
    BINDERR=3
};

class TcpServer
{
public:
    TcpServer(const uint16_t& port = defaultport, const std::string& ip = defaultip)
        : _socketfd(defaultfd)
        , _port(port)
        , _ip(ip)
    {}
    void InitServer()
    {
        _socketfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_socketfd < 0)
        {
            lg(Fatal, "create socket err, errno: %d, errst: %s", errno, strerror(errno));
            exit(SocketERR);
        }
        lg(Info, "create socket successful, sockfd:%d", _socketfd);
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        inet_aton(_ip.c_str(), &(local.sin_addr));
        int n = bind(_socketfd, (struct sockaddr*)&local, sizeof(local));
        if (n < 0)
        {
            lg(Fatal, "bind socket err, errno: %d, errst: %s", errno, strerror(errno));
            exit(BINDERR);
        }
        lg(Info, "bind sucessful");
        // 监听套接字  -- 因为tcp是要等待别人来连接的,所以要有个监听套接字进行监听等待别人来连接
    }
    void RunServer()
    {

    }
    ~TcpServer(){}
private:
    int _socketfd;
    uint16_t _port;
    std::string _ip;
};

这里我们用inet_aton将本地序列转化成为网络序列。
在这里插入图片描述

二、listen接口(监听)

在这里插入图片描述

启动服务器,状态是listen:
在这里插入图片描述

三、accept接收套接字

在这里插入图片描述

1、为什么还要多一个套接字(明明已经有了个socket套接字文件了,为什么要多一个accept套接字文件?)

在这里插入图片描述
各司其职呗~~_socketfd是接客用的,面对随时来的新连接先接客到底层去,而accept的返回值才真正是服务的套接字,也就是I/O端口进行服务的,从底层拿出来进行服务!所以_socketfd只有一个,而accept返回值却有多个!(一个接客,多个服务员)
在这里插入图片描述

修改一下socket套接字为listen套接字:
在这里插入图片描述
在这里插入图片描述

2、底层拿到新连接并根据连接进行通信

在这里插入图片描述

在这里插入图片描述

3、类比理解监听套接字和连接套接字的区别

相当于我们去一家饭店,监听套接字是外面迎客的人,把人都迎进来,里面肯定有服务员吧,服务员就是连接套接字,服务员去服务,迎客的人去迎客。我们目前实现的是迎客连接一条龙,也就是来一群人,一个个迎客,再进来一个个服务,太慢了,所以我们的目标是实现迎客和服务两条线,来了人和迎客互不耽误,两者并发式的运行,就需要我们用多线程版本,但是会出现很多问题我们在下面一一进行讲解。

四、服务端提供服务(向客户端回消息)

那我们就写一个Server函数进行封装来将服务端进行提供服务!我们传参传accept从底层拿到的套接字和拿到的套接字的ip地址和port,我们找到ip地址用的是inet_ntop函数接口。
在这里插入图片描述
在这里插入图片描述

小问题:我上来通信的字符串和数字等难道到网络中不考虑大小端问题?我地址需要转大端,难道通信的字符串不用转吗?答案是肯定要转的,但是它网络里面自动帮忙转了。

在这里插入图片描述

我们用简单的Server函数中的代码为接收到消息,拼接一下再返回给服务器:
在这里插入图片描述
在这里插入图片描述

五、tcp_client客户端编写

1、框架

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

void Usage(const std::string& proc)
{
    std::cout << "\n\rUsages: " << proc << "serverip serverport\n" << std::endl;
}

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

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);
    if (socketfd < 0)
    {
        std::cerr << "socket create error " << std::endl; 
        return 1;
    }

    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));

    // tcp客户端要绑定,不需要显示绑定,os随机分配
    // 向网络服务器请求连接,客户端发起connect的时候,进行自动随机绑定
    int n = connect(socketfd, (struct sockaddr*)&server, sizeof(server));
    if (n < 0)
    {
        std::cerr << "connect err " << std::endl;
        return 2;
    }
    // 连接成功
    std::string message;
    while (true)
    {
        std::cout << "Please Enter@ ";
        std::getline(std::cin, message);
        char inbuffer[4096];
        write(socketfd, message.c_str(), message.size()); // 写进socketfd

        int n = read(socketfd, inbuffer, sizeof(inbuffer)); // 从socketfd读入inbuffer
        if (n <= 0)
        {
            std::cerr << "read err " << std::endl;
            return 3;
        }
        inbuffer[n] = 0;
        std::cout << inbuffer << std::endl;
    }
    close(socketfd);
    return 0;
}

在这里插入图片描述

2、客户端卡退了,服务端怎么处理?(read返回值为0)

客户端卡退了,服务端怎么办?
服务端保存好消息,其余不管,也就是我们的服务器read的返回值为0,也就是从底层拿到的连接的文件描述符值为0的时候,表示客户端退出。
在这里插入图片描述
在这里插入图片描述

3、一个有趣的现象–两个一样的客户端去连接客户端?(单进程服务)

在这里插入图片描述
理由是单进程版,得等一个进程搞好退出后才能实现另一个进程的使用。

4、方法1:子进程关listensock,父进程关sockfd

因为子进程会有很多没必要的listensock套接字,父进程会有很多没必要的sockfd套接字,子进程是进行监听,父进程是进行连接,其套接字本质不一样,子进程负责监听,父进程负责连接,所以把这些没必要的套接字都关了。
在这里插入图片描述
但这种情况父进程用waitpid还是有很大问题,因为父进程得等子进程退出!所以跟单进程没什么区别了,下面我们介绍怎么解决这个问题:

5、处理waitpid问题:孙子进程处理机制或者signal忽略信号

在这里插入图片描述

因为父进程等待子进程是阻塞的方式,导致的使父进程要一直等待子进程退出,子进程退出需要一定的时间,并且连的子进程多了,子进程就会一直运行,等待一个运行后再等下一个运行,时间太久了,所以我们使用一下子进程创建孙子进程的方法,子进程创建完立马退出,告诉父进程我退出了,父进程就能够执行下一步操作,而孙子进程去跑服务,并且孙子进程给操作系统进行托孤,孙子进程不受爷爷进程控制,并发的去跑进程。

但上面这个方法还是有很大的问题的,因为子进程的创建代价太大了,要有进程地址空间等很多需要创建的东西,很麻烦,所以我们用下面的这种方法:

6、方法2:多线程版本

在这里插入图片描述

在这里插入图片描述
上面的做法仍然有不合理之处,就是假如说是几亿个用户连接,那岂不是要几亿个线程,所以我们用线程池版本来解决!

7、方法3:线程池版本

(1)线程池代码ThreadPool.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 5;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

public:
    static void *HandlerTask(void *args)
    {
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
        std::string name = tp->GetThreadName(pthread_self());
        while (true)
        {
            tp->Lock();

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == tp_) // ???
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;

(2)任务代码Task.hpp

#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
extern Log lg;

class Task
{
public:
    Task(int sockfd, const std::string &ipstr, const uint16_t &clientport)
        : _sockfd(sockfd), _clientip(ipstr), _clientport(clientport)
    {
    }
    void run()
    {
        char buffer[4096];
        // 因为是面向字节流的,所以读网络跟读文件一样简单
        // 先读到buffer
        ssize_t n = read(_sockfd, buffer, sizeof(buffer)); // 从套接字信息中读取消息存到buffer中
        if (n < 0)
        {
            lg(Warning, "read err, readip:%s, readport:%d\n", _clientip.c_str(), _clientport);
        }
        else if (n == 0) // 客户端退出
        {
            lg(Info, "%s:%d quit, server close fd:%d", _clientip.c_str(), _clientport, _sockfd);
        }
        else
        {
            buffer[n] = 0;
            std::cout << "client say# " << buffer << std::endl;
            std::string echo_string = "tcpserver echo@ ";
            echo_string += buffer;
            // 再写入
            write(_sockfd, echo_string.c_str(), echo_string.size()); // 处理完的消息写回sockfd文件
        }
        close(_sockfd);
    }
    void operator()()
    {
        run();
    }

private:
    int _sockfd;
    std::string _clientip;
    uint16_t _clientport;
};

(3)代码改进

void RunServer()
    {
        ThreadPool<Task>::GetInstance()->Start(); // 开启线程池的单例模式
        // signal(SIGCHLD, SIG_IGN); // 信号忽略
        lg(Info, "tcp_server is running...");
        while (true)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            // 1.获取新连接
            int sockfd = accept(_listensocketfd, (struct sockaddr*)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "accept err, errno: %d, errst: %s", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            char ipstr[32]; // 自定义的缓冲区
            inet_ntop(AF_INET, &(client.sin_addr), ipstr, sizeof(ipstr));

            // 2.根据新连接来通信
            lg(Info, "get a new link, sockfd:%d, clentip:%s, clientport:%d", sockfd, ipstr, clientport);
            // 3.4 线程池版本
            Task t(sockfd, ipstr, clientport);
            ThreadPool<Task>::GetInstance()->Push(t);
        }
    }

(4)结果

发送一则消息则退出线程。
在这里插入图片描述

六、服务端翻译小程序

Init.hpp:

#pragma once 
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"

extern Log lg;

const std::string dicname = "./dict.txt";
const std::string sep = ":";

static bool Split(std::string &line, std::string* part1, std::string* part2) // line输入性参数 part1/2都是输出型参数
{
    auto pos = line.find(sep);
    if (pos == std::string::npos)
    {
        return false;
    }
    *part1 = line.substr(0, pos);
    *part2 = line.substr(pos + 1);
    return true;
}

class Init
{
public:
    Init()
    {
        std::ifstream in(dicname);
        if (!in.is_open())
        {
            lg(Fatal, "ifstream open %s error", dicname.c_str());
            exit(1);
        }
        std::string line;
        while (std::getline(in, line))
        {
            std::string part1, part2;
            Split(line, &part1, &part2);
            dict.insert({part1, part2});
        }
        in.close();
    }
    std::string Translation(const std::string& key)
    {
        auto it = dict.find(key);
        if (it == dict.end()) return "Unkonw";
        else return it->second;
    }
private:
    std::unordered_map<std::string, std::string> dict;
};

Task.hpp:

#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"
extern Log lg;
Init init;
class Task
{
public:
    Task(int sockfd, const std::string &ipstr, const uint16_t &clientport)
        : _sockfd(sockfd), _clientip(ipstr), _clientport(clientport)
    {
    }
    void run()
    {
        char buffer[4096];
        // 因为是面向字节流的,所以读网络跟读文件一样简单
        // 先读到buffer
        ssize_t n = read(_sockfd, buffer, sizeof(buffer)); // 从套接字信息中读取消息存到buffer中
        if (n < 0)
        {
            lg(Warning, "read err, readip:%s, readport:%d\n", _clientip.c_str(), _clientport);
        }
        else if (n == 0) // 客户端退出
        {
            lg(Info, "%s:%d quit, server close fd:%d", _clientip.c_str(), _clientport, _sockfd);
        }
        else
        {
            buffer[n - 2] = 0;
            std::cout << "client key# " << buffer << std::endl;
            std::string echo_string = init.Translation(buffer);
            // 再写入
            write(_sockfd, echo_string.c_str(), echo_string.size()); // 处理完的消息写回sockfd文件
        }
        close(_sockfd);
    }
    void operator()()
    {
        run();
    }

private:
    int _sockfd;
    std::string _clientip;
    uint16_t _clientport;
};

在这里插入图片描述

七、进化版:出现错误的细节问题

1、向一个已经关闭的文件描述符的文件中进行写入,读端已经关掉了,写端继续写,OS会把客户端进程杀掉

在这里插入图片描述

在这里插入图片描述

所以我们在write的时候都需要用返回值做一层判断,防止向已经关闭掉的文件描述符中写信息。要么就加信号忽略:
在这里插入图片描述

2、重连

tcpclient.cc:
在这里插入图片描述

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

八、在线翻译服务+重连

我们先来这些词汇:
在这里插入图片描述
在这里插入图片描述

九、地址复用

在这里插入图片描述

十、守护进程介绍

守护进程

在这里插入图片描述

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

守护进程的启动bash:
在这里插入图片描述

带上日志文件(日志信息打印到当前路径下):
在这里插入图片描述

在这里插入图片描述

接口:默认00
在这里插入图片描述

十一、tcp的通信原理

在这里插入图片描述

tcp是全双工的:两个人吵架。
在这里插入图片描述

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
错误消息 "Socket operation on non-socket" 表示在非套接字对象上执行了套接字操作。这通常发生在你试图对非套接字对象调用 `modbus_tcp_accept` 函数时。 可能的原因和解决方法如下: 1. 未正确初始化 Modbus 上下文:在调用 `modbus_new_tcp` 创建 Modbus TCP 上下文后,确保上下文对象成功创建并不为 NULL。如果上下文对象为 NULL,则可能是由于内存分配失败或其他错误导致的。你可以检查上下文对象是否为 NULL,并相应地处理错误。 2. 未正确设置套接字选项:在调用 `modbus_tcp_listen` 函数之前,确保已正确设置了套接字选项。例如,检查 IP 地址和端口号是否正确,并确保没有与其他应用程序冲突。 3. 非法的上下文对象:如果 `modbus_tcp_accept` 返回 "Socket operation on non-socket" 错误,可能是因为 `ctx` 参数不是有效的 Modbus 上下文对象。确保传递给 `modbus_tcp_accept` 函数的上下文对象是通过 `modbus_new_tcp` 函数创建的,并且没有被释放或损坏。 4. 网络连接问题:该错误可能是由于网络连接问题导致的。确保从站和主站之间的网络连接正常,并且主站可以访问从站的 IP 地址和端口号。 如果以上方法仍无法解决问题,建议仔细检查你的代码逻辑和调用顺序,确保正确初始化和设置 Modbus 上下文,并在调用 `modbus_tcp_accept` 函数之前进行必要的准备工作。如果问题仍然存在,可以考虑参考 libmodbus 的文档或寻求社区的帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2022horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值