【Linux取经路】网络套接字编程——TCP篇

news2024/11/24 16:05:50

在这里插入图片描述

文章目录

  • 前言
  • 十、Tcp Server 端代码
    • 10.1 socket、bind
    • 10.1 listen——监听一个套接字
    • 10.2 accept——获取一个新连接
    • 10.3 read——从套接字中读取数据
    • 10.4 write——向套接字中进行写入
    • 10.5 Tcp Service 端完整代码(单进程版)
    • 10.6 Tcp Server 端代码(多进程版)
    • 10.7 Tcp Server 端代码(多线程版)
    • 10.8 Tcp Server 端完整代码(线程池版)——英汉字典
    • 10.9 setsockopt——设置套接字描述符的属性
  • 十一、Tcp Client 端代码
    • 11.1 connect——向服务端发起连接
    • 11.2 write、read——向服务器发送数据、从服务器接收数据
    • 11.3 Tcp Client 端完整代码
  • 十二、基于 TCP 协议的客户端/服务器程序的一般流程
  • 十三、结语

前言

本篇文章接上一篇【Linux取经路】网络套接字编程——UDP篇,所以目录号从十开始。

十、Tcp Server 端代码

10.1 socket、bind

listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (listensockfd_ < 0)
{
    lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));
    exit(SOCKET_ERR);
}
lg(Info, "socket creat successful, listensockfd_: %d", listensockfd_);

// 2. bind
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)); // 将 ip 地址转化成网络序列
local.sin_addr.s_addr = INADDR_ANY;

socklen_t len = sizeof(local);
int ret = bind(listensockfd_, (struct sockaddr *)&local, len);

if (ret < 0)
{
    lg(Fatal, "bind error, errno: %d, error message: %s", errno, strerror(errno));
    exit(BIND_ERR);
}
lg(Info, "socket bind successful");

TCP 这里的创建套接字、绑定代码和 UDP 中的一样。

10.1 listen——监听一个套接字

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

绑定完之后需要将监听套接字设为监听状态。

// 3. listen
// TCP 是面向连接的,服务器一般是比较“被动的”,在没有客户端访问的时候就只能干等,不能退出,服务器需要一直处于一种,一直在等待连接到来的状态
int n = listen(listensockfd_, backlog);
if (n < 0)
{
    lg(Fatal, "bind error, errno: %d, error message: %s", errno, strerror(errno));
    exit(LISTEN_ERR);
}
lg(Info, "socket listen successful");

10.2 accept——获取一个新连接

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

该函数参数和 recvfrom 函数的参数一样,后面俩输出型参数用来获取 client 端的套接字信息。该函数的返回值是一个文件描述符,它和 socket 函数返回的文件描述符有什么区别呢?socket 函数返回的这个套接字用来绑定、监听和获取新的连接,一般把这个套接字叫做监听套接字,它的主要任务就是用来获取新连接,可以把它看成拉客的。accept 函数返回的套接字才是后面进行网络通信时使用的套接字,一个服务器进程,它的监听套接字一般就只有一个,而用来通信的套接字可能会存在多个。

for (;;)
{
    // 4. 获取新连接
    lg(Info, "server is running...");
    struct sockaddr_in client;
    socklen_t len;
    int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
    if (sockfd < 0)
    {
        lg(Warning, "bind error, errno: %d, error message: %s", errno, strerror(errno));
        continue;
    }
    uint16_t client_port = ntohs(client.sin_port);
    char client_ip[32];
    inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
    lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, client_ip, client_port);

    // 5. 根据新连接来进行通信
    // version 1
    Server(sockfd, client_ip, client_port);

    sleep(1);
}

可以在本本机使用 telnet 127.0.0.1 9999 (其中 9999 是端口号,是自定义的)指令来验证服务器是否能成功获取一个连接。

10.3 read——从套接字中读取数据

因为 TCP 是面向字节流的,所以可以直接使用 read 系统调用去读取数据。

void Server(int sockfd, const std::string &client_ip, const uint16_t &client_port)
{
    char buffer[num];
    while(true)
    {
        memset(buffer, 0, sizeof(buffer));
        ssize_t n = read(sockfd, buffer, sizeof(buffer));
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "client asy# <b>" << buffer << std::endl;
            std::string echo_string = "tcpserver echo@ ";
            echo_string += buffer;

            int n = write(sockfd_, echo_string.c_str(), echo_string.size());
            if (n < 0)
            {
                lg(Warning, "write error, errno: %d, strerror: %d", errno, strerror(errno));
            }
        }
        else if(n == 0)
        {
            lg(Info, "%s-%d Client quit... sockfd: %d", client_ip.c_str(), client_port, sockfd);
            break;
        }
        else
        {
            lg(Warning, "read err:, errno: %d, errstr: %s", errno, strerror(errno));
            break;
        }
    }
}

如果客户端退出了,那么 read 会读到0,此时需要把之前 accept 返回的 sockfd 关闭,防止误操作,造成意想不到的结果。

10.4 write——向套接字中进行写入

同理,向套接字中进行写入时,直接使用 write 系统调用即可。服务端在收到客户端的数据后,先进行加工处理,然后再进行写入,上面 if(n > 0) 后面就是写入的代码。唯一需要注意的就是,如果在写入前或者正在写入的过程中,client 端退出了,此时客户端与服务器之间的连接就断了,此时客户端如果进行写入操作可能会导致整个服务端崩掉。这和管道类似,读端关闭,写端继续写,操作系统会给写端发送 13 号信号,将写端 kill 调,为了避免这种情况,我们需要在服务器启动的时候将 13 号新号进行捕捉。

10.5 Tcp Service 端完整代码(单进程版)

#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
#include <string>
#include <unistd.h>

extern Log lg;

#define num 1024

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 一般不要设置的太大

enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    LISTEN_ERR
};

class TcpServer
{
private:
    void Server(int sockfd, const std::string &client_ip, const uint16_t &client_port)
    {
        char buffer[num];
        while(true)
        {
            memset(buffer, 0, sizeof(buffer));
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if(n > 0)
            {
                buffer[n] = 0;
                std::cout << "client asy# <b>" << buffer << std::endl;
                std::string echo_string = "tcpserver echo@ ";
                echo_string += buffer;

                write(sockfd, echo_string.c_str(), echo_string.size()); 
            }
            else if(n == 0)
            {
                lg(Info, "%s-%d Client quit... sockfd: %d", client_ip.c_str(), client_port, sockfd);
                break;
            }
            else
            {
                lg(Warning, "read err:, errno: %d, errstr: %s", errno, strerror(errno));
                break;
            }
        }
    }

public:
    TcpServer(const uint16_t &port, const std::string &ip = defaultip)
        : listensockfd_(defaultfd), port_(port), ip_(ip)
    {
    }

    void Init()
    {
        // 1. 创建套接字
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        lg(Info, "socket creat successful, listensockfd_: %d", listensockfd_);

        // 2. bind
        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)); // 将 ip 地址转化成网络序列
        local.sin_addr.s_addr = INADDR_ANY;

        socklen_t len = sizeof(local);
        int ret = bind(listensockfd_, (struct sockaddr *)&local, len);

        if (ret < 0)
        {
            lg(Fatal, "bind error, errno: %d, error message: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "socket bind successful");

        // 3. listen
        // TCP 是面向连接的,服务器一般是比较“被动的”,在没有客户端访问的时候就只能干等,不能退出,服务器需要一直处于一种,一直在等待连接到来的状态
        int n = listen(listensockfd_, backlog);
        if (n < 0)
        {
            lg(Fatal, "bind error, errno: %d, error message: %s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
        lg(Info, "socket listen successful");
    }

    void Start()
    {
        for (;;)
        {
            // 4. 获取新连接
            lg(Info, "server is running...");
            struct sockaddr_in client;
            socklen_t len;
            int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "bind error, errno: %d, error message: %s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(client.sin_port);
            char client_ip[32];
            inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
            lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, client_ip, client_port);

            // 5. 根据新连接来进行通信
            // version 1
            Server(sockfd, client_ip, client_port);
            close(sockfd);
            sleep(1);
        }
    }

    ~TcpServer()
    {
        close(listensockfd_);
    }

private:
    int listensockfd_; //
    uint16_t port_;    // 端口号
    std::string ip_;   // ip
};

因为服务端是单进程版,所以一但一个 client 与 服务端建立了连接,那么其它的客户端在前一个 client 退出之前就无法再与客户端建立连接,这意味着,服务端同时只能服务一个 client 端。这显然是不符合实际应用场景的。

单进程版 TcpServer

10.6 Tcp Server 端代码(多进程版)

void Start()
{
    for (;;)
    {
        // 4. 获取新连接
        lg(Info, "server is running...");
        struct sockaddr_in client;
        socklen_t len;
        int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
        if (sockfd < 0)
        {
            lg(Warning, "bind error, errno: %d, error message: %s", errno, strerror(errno));
            continue;
        }
        uint16_t client_port = ntohs(client.sin_port);
        char client_ip[32];
        inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
        lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, client_ip, client_port);

        // 5. 根据新连接来进行通信
        // version 1——单进程版
        // Server(sockfd, client_ip, client_port);
        // close(sockfd);
        // sleep(1);

        // version 2——多进程版
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程
            close(listensockfd_); // 子进程不关心这个 socket
            if(fork() > 0) exit(0); // 子进程再创建子进程,然后让子进程直接退出,由孙子进程执行后续代码
            Server(sockfd, client_ip, client_port);
            close(sockfd); // 子进程处理完后将套接字信息关闭
            exit(0); // 因为子进程退出了,所以孙子进程就会被系统领养,最终执行完毕,会被系统直接回收
        }
        close(sockfd); // 子进程已经把当前父进程中 accept 获得的文件描述符继承下去了,所以父进程已经不关心该文件描述符了,可以直接关闭掉,不然到时候父进程中存在大量被打开的文件

        // 父进程
        int rid =  waitpid(id, nullptr, 0);

    }
}

相较于单进程版,多进程的变化是在 Start 中,来一个 client 连接请求,通过创建子进程,让该子进程去为这个 client 端进行服务(获取该 client 端的数据、对数据加工处理、将处理完的数据发送给 client 端),父进程继续去 accept 获取新连接。这里需要注意:子进程是会继承父进程中的所有文件描述符,对子进程来说 listensockfd_ 是没有用的,因为子进程不去拉客(获取新连接),因此子进程代码中第一件事就是将 listensockfd_ 这个文件描述符进行关闭。对父进程来说,获取到一个新连接后会为该连接创建一个新的文件描述符,这个文件描述符被子进程继承下去后父进程就不在关心了,因为父进程要去继续拉客(获取新连接),而不是为当前获取到的连接提供服务。其次父进程需要等待子进程,如果是 waitpid(id, nullptr, 0); 这种等待,那和单进程版的,没有任何区别,因为这是阻塞等待,只有等子进程退出了父进程才能继续执行。解决该问题的办法有很多:上面代码中,采用的是让子进程继续创建孙子进程,然后让孙子进程去提供服务,子进程直接退出,这时,父进程就立刻能将子进程回收,孙子进程会被操作系统领养,最终执行完毕,会被操作系统直接回收。这里再提供两种解决方案:

  • 用一个数据将创建的子进程的 pid 管理起来,然后 waitpid(id, nullptr, WNOHANG); 或者直接 waitpid(-1, nullptr, WNOHANG);非阻塞等待任意子进程。
  • 父进程中对 SIGCHLD 进行捕捉,在捕捉方法里面对退出的子进程进行回收;或者父进程中直接对 SIFCHLD 信号进行 SIG_IGN,此时子进程在退出时会自动被清理掉,不会产生僵尸进程。

多进程版 TcpServer

10.7 Tcp Server 端代码(多线程版)

创建一个新进程相较于创建一个新线程是很“重”的,所以下面引入多线程版。一个进程打开的文件描述符是被所有子线程共享的,子线程可以根据文件描述符去对文件进行读写操作。

// 线程函数的参数
class ThreadData
{
public:
    ThreadData(int sockfd, const std::string &client_ip, const uint16_t &client_port, TcpServer *tcpserver)
        : sockfd_(sockfd), client_ip_(client_ip), client_port_(client_port), tcpserver_(tcpserver)
    {
    }

    ~ThreadData()
    {
    }

public:
    int sockfd_;            // 文件描述符
    std::string client_ip_; // 客户端 ip
    uint16_t client_port_;  // 客户端 port
    TcpServer *tcpserver_;  // 一个 tcpserver 进程
};
static void *Rountine(void *args)
{
    pthread_detach(pthread_self()); // 让子线程分离,这样在主线程中不用进行 join 了
    ThreadData *td = static_cast<ThreadData *>(args);

    td->tcpserver_->Server(td->sockfd_, td->client_ip_, td->client_port_);
    close(td->sockfd_);
}

void Start()
{
    for (;;)
    {
        // 4. 获取新连接
        lg(Info, "server is running...");
        struct sockaddr_in client;
        socklen_t len;
        int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
        if (sockfd < 0)
        {
            lg(Warning, "bind error, errno: %d, error message: %s", errno, strerror(errno));
            continue;
        }
        uint16_t client_port = ntohs(client.sin_port);
        char client_ip[32];
        inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
        lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, client_ip, client_port);

        // 5. 根据新连接来进行通信

        // version 3——多线程版
        ThreadData *td = new ThreadData(sockfd, client_ip, client_port, this);
        pthread_t tid;
        pthread_create(&tid, nullptr, Rountine, td);
    }
}

注意要把线程函数设置成 static

多线程版 TcpServer

10.8 Tcp Server 端完整代码(线程池版)——英汉字典

上面三个版本的服务端都提供的是常服务,什么事常服务呢?就是只要一个 client 发起了连接,服务端这边就一直 while 循环式的为该客户端提供服务,直到客户端退出,这样实际也是不合理的,正确做法是,服务端在收到一个连接请求后,一次处理完后就应该返回了。所以这里我们引入线程池,将一次客户端的连接当做一个任务,将该任务放到线程池里面去,让线程池里面的线程为这一次连接服务(获取客户端发来的数据、对数据加工处理、将结果返回给客户端)。

// TcpServer.hpp——服务端
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <iostream>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "log.hpp"
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <pthread.h>
#include "ThreadPool.hpp"
#include "Task.h"

extern Log lg;

#define num 1024

const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 一般不要设置的太大

enum
{
    SOCKET_ERR = 1,
    BIND_ERR,
    LISTEN_ERR
};

class TcpServer;

class ThreadData
{
public:
    ThreadData(int sockfd, const std::string &client_ip, const uint16_t &client_port, TcpServer *tcpserver)
        : sockfd_(sockfd), client_ip_(client_ip), client_port_(client_port), tcpserver_(tcpserver)
    {
    }

    ~ThreadData()
    {
    }

public:
    int sockfd_;            // 文件描述符
    std::string client_ip_; // 客户端 ip
    uint16_t client_port_;  // 客户端 port
    TcpServer *tcpserver_;  // 一个 tcpserver 进程
};

class TcpServer
{
public:
    TcpServer(const uint16_t &port, const std::string &ip = defaultip)
        : listensockfd_(defaultfd), port_(port), ip_(ip)
    {
    }

    void Init()
    {
        // 1. 创建套接字
        listensockfd_ = socket(AF_INET, SOCK_STREAM, 0);
        if (listensockfd_ < 0)
        {
            lg(Fatal, "socket create error, errno: %d, error message: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        lg(Info, "socket creat successful, listensockfd_: %d", listensockfd_);

        // 2. bind
        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)); // 将 ip 地址转化成网络序列
        local.sin_addr.s_addr = INADDR_ANY;

        socklen_t len = sizeof(local);
        int ret = bind(listensockfd_, (struct sockaddr *)&local, len);

        if (ret < 0)
        {
            lg(Fatal, "bind error, errno: %d, error message: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(Info, "socket bind successful");

        // 3. listen
        // TCP 是面向连接的,服务器一般是比较“被动的”,在没有客户端访问的时候就只能干等,不能退出,服务器需要一直处于一种,一直在等待连接到来的状态
        int n = listen(listensockfd_, backlog);
        if (n < 0)
        {
            lg(Fatal, "bind error, errno: %d, error message: %s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
        lg(Info, "socket listen successful");
    }

    void Start()
    {
        signal(SIGPIPE, SIG_IGN);
        ThreadPool<Task>::GetInstance()->start();
        for (;;)
        {
            // 4. 获取新连接
            lg(Info, "server is running...");
            struct sockaddr_in client;
            socklen_t len;
            int sockfd = accept(listensockfd_, (struct sockaddr *)&client, &len);
            if (sockfd < 0)
            {
                lg(Warning, "bind error, errno: %d, error message: %s", errno, strerror(errno));
                continue;
            }
            uint16_t client_port = ntohs(client.sin_port);
            char client_ip[32];
            inet_ntop(AF_INET, &(client.sin_addr), client_ip, sizeof(client_ip));
            lg(Info, "get a new link, sockfd: %d, clientip: %s, clientport: %d", sockfd, client_ip, client_port);

            // 5. 根据新连接来进行通信
            // version 4——线程池版本
            Task t(sockfd, client_ip, client_port);
            ThreadPool<Task>::GetInstance()->push(t);
        }
    }

    ~TcpServer()
    {
        close(listensockfd_);
    }

private:
    int listensockfd_; //
    uint16_t port_;    // 端口号
    std::string ip_;   // ip
};
// ThreadPool.hpp——线程池
#pragma once

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

struct ThreadInfo
{
    pthread_t tid_;    // 线程 ID
    std::string name_; // 线程的名字
};

template <class T>
class ThreadPool
{
    static const int defaultnum = 5;

public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }

    void Weakup()
    {
        pthread_cond_signal(&cond_);
    }

    void Sleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }

    bool IsTaskQueueEmpty()
    {
        return tasks_.empty();
    }

    T PopTasks()
    {
        T task = tasks_.front();
        tasks_.pop();
        return task;
    }

    const std::string &GetThreadName(pthread_t tid)
    {
        return um_[tid];
    }

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

            T task = tp->PopTasks();
            tp->Unlock();
            task(); // 此时这个任务已经属于该线程私有的了,所以对任务的处理工作可以在解锁之后进行
            // printf("%s is running----%s\n", name.c_str(), task.result_to_string().c_str());
        }
    }

    void start()
    {
        for (int i = 0; i < thread_num_; i++)
        {
            threads_[i].name_ = "Thread-" + std::to_string(i);
            pthread_create(&(threads_[i].tid_), nullptr, Routine, this);
            um_[threads_[i].tid_] = threads_[i].name_;
        }
    }

    void push(const T &task)
    {
        Lock();
        tasks_.push(task);
        Weakup();
        Unlock();
    }

    static ThreadPool<T> *GetInstance() // 指正通过该接口获取一个单例对象
    {
        if (ptp_ == nullptr)
        {
            pthread_mutex_lock(&smutex_);
            if (ptp_ == nullptr)
            {
                lg(Info, "singleton creat done first!");
                ptp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&smutex_);
        }
        return ptp_;
    }

private:
    ThreadPool(int thread_num = defaultnum)
        : threads_(thread_num), thread_num_(thread_num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &tp) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;

private:
    std::vector<ThreadInfo> threads_;               // 一批线程
    int thread_num_;                                // 线程池中的线程的数量
    std::queue<T> tasks_;                           // 任务
    pthread_mutex_t mutex_;                         // 定义一把锁,让所有线程保持互斥
    pthread_cond_t cond_;                           // 定义一个条件变量,让线程同步
    std::unordered_map<pthread_t, std::string> um_; // 一个 map 用来快速查找一个线程的name

    static ThreadPool<T> *ptp_;     // 单例
    static pthread_mutex_t smutex_; // 定义一把静态的锁
};

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

template <class T>
pthread_mutex_t ThreadPool<T>::smutex_ = PTHREAD_MUTEX_INITIALIZER;
// Task.hpp——任务
#include <iostream>
#include <string>
#include "log.hpp"  
#include "ECDictionary.hpp"

ECDictionary ecd;

extern Log lg;

#define num 1024



class Task
{
public:
    Task(int sockfd, const std::string &client_ip, const uint16_t &client_port)
        : sockfd_(sockfd), client_ip_(client_ip), client_port_(client_port)
    {
    }

    void run()
    {
        char buffer[num];
        memset(buffer, 0, sizeof(buffer));
        ssize_t n = read(sockfd_, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client asy# <b>" << buffer << std::endl;
            std::string echo_string = "tcpserver echo@ ";
            echo_string += ecd.GetTranslation(buffer);

            write(sockfd_, echo_string.c_str(), echo_string.size());
        }
        else if (n == 0)
        {
            lg(Info, "%s-%d Client quit... sockfd: %d", client_ip_.c_str(), client_port_, sockfd_);
        }
        else
        {
            lg(Warning, "read err:, errno: %d, errstr: %s", errno, strerror(errno));
        }

        close(sockfd_);
    }

    void operator()()
    {
        run();
    }
private:
    int sockfd_;
    std::string client_ip_;
    uint16_t client_port_;
};
// ECDictionary.hpp——字典
#include <fstream>
#include <string>
#include <string.h>
#include <unordered_map>
#include "log.hpp"

#define SEP ":"

extern Log lg;

const std::string path = "./dictionary.txt";

class ECDictionary
{
private:
    static void Split(const std::string &line, std::string *english, std::string *chinese)
    {
        size_t pos = line.find(SEP);
        if(pos == std::string::npos) return;
        *english = line.substr(0, pos);
        *chinese = line.substr(pos+1);
        return;
    }

public:
    ECDictionary()
    {
        std::ifstream di(path.c_str());
        if(!di.is_open())
        {
            lg(Fatal, "open %s Dictionary errror, errno: %d, errstr: %s", path.c_str(), errno, strerror(errno));
            exit(1);
        }

        std::string line;
        while(std::getline(di, line))
        {
            std::string english, chinese;
            Split(line, &english, &chinese);
            dir_.insert({english, chinese});
        }
        di.close();
    }

    std::string GetTranslation(const std::string &english)
    {
        auto it = dir_.find(english);
        if(it == dir_.end()) return "There is no such word";

        return it->second;
    }

private:
    std::unordered_map<std::string, std::string> dir_;
};

线程池版 TcpServer

由于我们当前是短连接,即对于一个客户端发起的一次连接请求,服务器只处理一次,就把相应的文件描述符进行了关闭。所以一个客户端启动后只能翻译一次。

10.9 setsockopt——设置套接字描述符的属性

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
int opt = 1;
setsockopt(listensockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt)); // 防止偶发性的服务器无法进行立即重启

十一、Tcp Client 端代码

和 Udp Client 端一样,Tcp Client 在创建完套接字之后,不需要我们自己编写代码去显示的绑定 ip 地址和端口号,而是在 connect 的时候,由操作系统随机的为我们绑定端口号。

11.1 connect——向服务端发起连接

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

后两个参数用来标识要连接服务端的套接字信息。返回值:只要绑定成功并且获取连接成功,0被返回;失败-1被返回。

11.2 write、read——向服务器发送数据、从服务器接收数据

和服务端一样,客户端也是通过 writeread 接口来发送数据和读取数据。

11.3 Tcp Client 端完整代码

因为服务端对一个连接只会处理一次,然后就会把服务端对应的文件描述符给关闭了,此时相当于连接就断了,所以客户端需要循环去发起连接请求,并且每连接一次,得到服务器端的结果后需要把创建的套接字进行关闭,因为服务端已经关了,你不关还想干啥??所以客户端需要每次重新去创建套接字,发起连接。

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

using namespace std;

void Usage(const char *command)
{
    std::cout << "\n\tUsage: " << command << " port[1024+]" << std::endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    string server_ip = argv[1];
    uint16_t server_port = stoi(argv[2]);

    sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &(server.sin_addr));
    socklen_t len = sizeof(server);

    while (true)
    {
        int cnt = 5; // 重连次数
        bool needreconnect = true;
        // 1. 创建套接字
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            perror("sockfd");
            exit(2);
        }

        do // 重连模块
        {
            // 2. bind —— 客户端不需要我们显示去 bind,是由操作系统帮我们进行绑定的

            // 3. 发起连接

            int result = connect(sockfd, (struct sockaddr *)&server, len);
            if (result < 0)
            {
                needreconnect = true;
                std::cerr << "The client is reconnecting cnt: " << cnt << std::endl;
                cnt--;
                sleep(2);
            }
            else
            {
                needreconnect = false; // 连接成功,不需要重连
                break;
            }
        } while (cnt && needreconnect);

        if (cnt == 0)
        {
            std::cerr << "The user is disconnected..." << std::endl;
            break;
        }

        // 4. 向server发送消息
        string message;
        std::cout << "Please Enter: ";
        getline(cin, message);

        ssize_t n = write(sockfd, message.c_str(), message.size());
        if (n < 0)
        {
            std::cerr << "write err, errno: " << errno << ", strerror: " << strerror(errno) << std::endl;
            continue;
        }

        char buffer[4096];
        int ret = read(sockfd, buffer, sizeof(buffer));
        if (ret > 0)
        {
            buffer[ret] = 0;
            printf("%s\n", buffer);
        }
        close(sockfd);
    }
    return 0;
}

image-20240320161648846

image-20240320162932709

十二、基于 TCP 协议的客户端/服务器程序的一般流程

image-20240321114434136

服务器初始化

  • 调用 socket ,创建文件描述符

  • 调用 bind ,将当前的文件描述符和 ip、port 绑定在一起;如果端口号被其他进程占用就会 bind 失败。

  • 调用 listen ,声明当前这个文件描述符作为一个服务器的文件描述符,为后面的 accept 做好准备

  • 调用 accept,并阻塞,等待客户端连接过来。

建立连接的过程

  • 调用 socket ,创建文件描述符

  • 调用 connect,向服务器发起连接请求

  • connect 会发出 SYN 段并阻塞,等待服务器应答(第一次)

  • 服务器收到客户端的 SYN ,会应答一个 SYN-ACK 段表示“同意建立连接”(第二次)

  • 客户端收到 SYN-ACK后会从 connect() 返回,同时应答一个 ACK 段(第三次)

这个建立连接的过程,通常成为三次握手

数据传输的过程

  • 建立连接后,TCP 协议提供全双工的通信服务;所谓全双工,就是在同一条连接中,同一时刻,通信双方可以同时写数据。当创建一个 TCP 套接字的时候,我们虽然只获得了一个文件描述符,但是在底层操作系统会为给文件描述符创建两个缓冲区,一个是发送缓冲区,另一个接收缓冲区;相对的概念是半双工,同一条连接在同一时刻,只能由一方来写数据。

  • 服务器从 accept 返回后,立刻调用 read()socket 就像读管道一样,如果没有数据到达就阻塞等待。

  • 这时客户端调用 write() 发送请求给服务器,服务器收到后从 read() 返回,对客户端的请求进行处理,在此期间客户端调用 read() 阻塞等待服务器的应答。

  • 服务器调用 write() 将处理结果发回给客户端,再次调用 read() 阻塞等待下一条请求。

  • 客户端收到后从 read() 返回,发送下一条请求,如此循环下去。

断开连接的过程

  • 如果客户端没有更多的请求了,就调用 close() 关闭连接,客户端会向服务器发送 FIN段(第一次)

  • 此时服务器收到 FIN 后,会回应一个 ACK ,同时 read() 会返回 0(第二次)

  • read() 返回之后,服务器就知道客户端关闭了连接,也调用 close() 关闭连接,这个时候服务器会向客户端发送一个 FIN (第三次)

  • 客户端收到 FIN,再返回一个 ACK 给服务器(第四次)

image-20240322172927274

同一时刻可能会有多个客户端来连接服务器,所以在服务端一定会存在多个连接,服务端是需要把这多个连接管理起来的,管理一定是通过先描述再组织的方式。我们调用 write 接口本质上是将数据写入到 TCP 的发送缓冲区,至于写入的数据什么时候发送到对端的接收缓冲区,以及发送多少,出错了怎么办,完全是由 TCP 协议自主控制的, 所以 TCP 也叫做传输控制协议。我们使用的这些接口 writereadsendtorecvfrom 本质上都是在用户和内核之间进行数据拷贝。用户把数据交给操作系统,本质上就是把数据交给操作系统。

image-20240322180614425

对于读方来说,可能 TCP 将发送方多次 write 的数据一次打包发了过来,接收方通过read 一次就多上来了多份数据,也可能 TCP 将发送方一次 write 的数据,分了好几次发送,read 每次读取到的都是部分数据。所以对接收方来说,它读上来的数据就存在很大的不确定性,它不清楚自己当前读到的数据是否是一个完整数据,也不清楚自己是否读到了多份数据。为了解决这个问题,就需要引入协议的概念,接收方可以根据协议来确定自己是否读取到了一份完整的数据。

十三、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

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

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

相关文章

C++ 11【右值引用】

&#x1f493;博主CSDN主页:麻辣韭菜&#x1f493;   ⏩专栏分类&#xff1a;C修炼之路⏪   &#x1f69a;代码仓库:C高阶&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多C知识   &#x1f51d;&#x1f51d; 1.C 11 简介 目录 1.C 11 简介 2. 统一的列表…

JVM之【类的生命周期】

首先&#xff0c;请区分Bean的声明周期和类的声明周期。此处讲的是类的声明周期 可以同步观看另一篇文章JVM之【类加载机制】 概述 在Java中数据类型分为基本数据类型和引用数据类型 基本数据类型由虚拟机预先定义&#xff0c;引用数据类型则需要进行类的加载 按照]ava虚拟机…

如何打造不一样的景区文旅VR体验馆项目?

近年来影院类产品迅速火爆&#xff0c;市面上的产品越来越多&#xff0c;投资者可以说是挑花了眼。为了助力投资者实现持续盈利&#xff0c;今天来给大家分析目前普乐蛙大爆新品悬空球幕飞行影院与其他5D/7D影院有哪些区别&#xff0c;给大家的创业投资之路避避雷~ 那我们正式开…

Android精通值Fragment的使用 —— 不含底层逻辑(五)

文章目录 1. Fragment1.1 Fragment的特性1.2 Fragment的基本使用步骤1.3 动态添加Fragment基本步骤1.4 Fragment与Activity的通信原生方案&#xff1a;Bundle类深入方案&#xff1a;java类与类通信的方案&#xff1a;接口Activity从Fragment获取消息Fragment从Activity获取消息…

德国80%的统计学教授都会答错的6个与P值有关的问题!

小编阅读了一篇发表于2002年关于P值的一项问卷调查研究 [1]&#xff0c;作者在6所德国大学中邀请了3组不同的受试者&#xff0c;分别为: 心理学专业的学生(n 44)&#xff1b;主要从事科学研究但不进行统计相关教学的教授和讲师(n 39)&#xff1b;进行统计相关教学的教授和讲师…

05-控制流(分支结构)

05-控制流(分支结构) 一、二路分支 程序中某一段代码需要满足一定的条件才会被执行。 if 语句&#xff1a;用于表达一种条件&#xff0c;如果条件满足则执行某个代码块。if-else 语句&#xff1a;用于表达一种条件&#xff0c;如果条件满足则执行某个代码块&#xff0c;否则…

微信小程序bindgetphonenumber获取手机号阻止冒泡触发

问题&#xff1a;点击手机号弹出微信的手机号验证组件&#xff0c;这是可以的。但是我点击车牌号&#xff0c;也弹出来了&#xff0c;这就郁闷了。 以下是解决方法 点击手机号时&#xff0c;弹出选择手机号 解决&#xff1a; <view style"display: flex;justify-conte…

Facebook开户|Facebook广告设计与测试优化

早上好家人们~今天Zoey给大家伙带来的是Facebook广告设计与测试优化&#xff0c;需要的家人们看过来啦&#xff01; 一、避免复杂用图和过多的文字 根据Facebook的数据显示&#xff0c;用户平均浏览一个贴文的时间在手机上仅花1.7秒、在电脑上则为2.5秒。因此&#xff0c;广告…

Java1.8 vue版家政服务系统成品源码 家政管家系统源码 家政月嫂系统源码 家政保洁系统源码 在线派单,师傅入驻全套商业源码

Java1.8 vue版家政服务系统成品源码 家政管家系统源码 家政月嫂系统源码 家政保洁系统源码 在线派单&#xff0c;师傅入驻全套商业源码 一、系统定义 家政上门服务系统是一种利用互联网技术&#xff0c;将家政服务需求与专业的家政服务人员进行高效匹配的平台。它允许用户通过…

LeetCode-704. 二分查找【数组 二分查找】

LeetCode-704. 二分查找【数组 二分查找】 题目描述&#xff1a;解题思路一&#xff1a;注意开区间和闭区间背诵版&#xff1a;解题思路三&#xff1a; 题目描述&#xff1a; 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xf…

Partially Spoofed Audio Detection论文介绍(ICASSP 2024)

An Efficient Temporary Deepfake Location Approach Based Embeddings for Partially Spoofed Audio Detection 论文翻译名&#xff1a;一种基于部分欺骗音频检测的基于临时深度伪造位置方法的高效嵌入 摘要&#xff1a; 部分伪造音频检测是一项具有挑战性的任务&#xff0…

水电表自动抄表系统

1.简述 水电表自动抄表系统是一种现代化智能化管理系统&#xff0c;它利用先进的物联网&#xff0c;完成了远程控制、即时、零接触的水电表读值收集&#xff0c;大大提升了公共事业服务项目的效率和准确性。该系统不仅减少了人工抄表工作量&#xff0c;还避免了人为失误&#…

【NOIP2020普及组复赛】题3:方格取数

题3&#xff1a;方格取数 【题目描述】 设有 nm 的方格图&#xff0c;每个方格中都有一个整数。现有一只小熊&#xff0c;想从图的左上角走到右下角&#xff0c;每一步只能向上、向下或向右走一格&#xff0c;并且不能重复经过已经走过的方格&#xff0c;也不能走出边界。小熊…

神经网络搭建(1)----nn.Sequential

神经网络模型构建 采用CIFAR10中的数据&#xff0c;并对其进行简单的分类。以下图为例 输入&#xff1a;3通道&#xff0c;3232 ( 经过一个55的卷积) → 变成32通道&#xff0c;3232的图像 (经过22的最大池化) → 变成32通道&#xff0c;1616的图像 ( 经过一个55的卷积) → 变…

电商售后常见的客服快捷语

在电商行业&#xff0c;优质的客户服务体验是留住顾客、建立品牌信誉的关键。面对多样化的售后请求&#xff0c;如何高效、准确地回应顾客&#xff0c;成为每个客服团队必须面对的挑战。今天&#xff0c;我给大家分享一些电商售后常见的客服快捷语&#xff0c;帮助客服人员提高…

AIGC 011-SAM第一个图像分割大模型-分割一切!

AIGC 011-SAM第一个图像分割大模型-分割一切&#xff01; 文章目录 0 论文工作1论文方法2 效果 0 论文工作 这篇论文介绍了 Segment Anything (SA) 项目&#xff0c;这是一个全新的图像分割任务、模型和数据集。SA 项目是一个具有里程碑意义的工作&#xff0c;它为图像分割领域…

网络安全:https劫持

文章目录 参考https原理https窃听手段SSL/TLS降级原理难点缺点 SSL剥离原理发展缺点前端劫持 MITM攻击透明代理劫持 参考 https原理 SNI 浏览器校验SSL证书 https降级 https握手抓包解析 lets encrypt申请证书 https原理 步骤如下&#xff1a; 客户端向服务器发送https请求。…

Java大文件上传、分片上传、多文件上传、断点续传、上传文件minio、分片上传minio等解决方案

一、上传说明 文件上传花样百出&#xff0c;根据不同场景使用不同方案进行实现尤为必要。通常开发过程中&#xff0c;文件较小&#xff0c;直接将文件转化为字节流上传到服务器&#xff0c;但是文件较大时&#xff0c;用普通的方法上传&#xff0c;显然效果不是很好&#xff0c…

docker 拉取不到镜像的问题:拉取超时

error pulling image configuration: download failed after attempts6: dial tcp 31.13.94.10:443: i/o timeout 首先设置国内的镜像源&#xff1a;复制下面直接执行 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF{"registry-mirrors"…

k8s学习--Secret详细解释与应用

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Secret什么是Secret?Secret四种类型及其特点Secret应用案例&#xff08;1&#xff09;将明文密码进行base64编码&#xff08;2&#xff09;编写创建secret的YAML文…