TCP套接字编写

目录

TCP服务器编写

服务器类的设计

构造函数

初始化函数

启动服务器

服务器的函数调用

TCP客户端编写

版本一

大小写转换服务

版本二

英译汉服务


TCP套接字编写相对于UDP较难一些,目前我们还是只认为发送的数据就是字符串。

下面我们看一下TCP套接字的编写。

  • 我们准备些一个服务器,一个客户端。

  • 服务器采用封装方式,客户端还是使用面向过程的方式。

  • 我们这次的TCP套接字编程会写几个版本:

    1. 就是采用单进程的方式,来实现

    2. 采用多进程的方式来实现

    3. 采用多线程的方式来实现

    4. 采用线程池的方式来实现

  • 单进程版本到多线程版本都是客户端发什么,服务器返回什么!

  • 到了线程池版本,我们可以写其他的一些方案:

    1. 大小写转换(将小写转大写)

    2. 英译汉

  • 为了方便讲解,我们的代码已经写好了(到了最终版本,线程池版本),所以在说前面的代码时候,主要是讲如何写!主要是为了了解TCP的系统调用以及和之前学习过的系统编程联系起来。

TCP服务器编写

我们前面说了,我们服务器编写采用的是封装好了,然后我们调用!

其实服务器的编写和UDP的接口基本一样:

  1. 创建一个TcpServer对象

  2. initServer 然后就是初始化服务器里面的数据

  3. start 服务器(start 就是让服务器开始运行,也就是从网络中读取数据)

上面就是服务器的接口,下面我们就来实现这些接口,以及这些接口所需要的函数!

服务器类的设计

服务器类中需要哪些成员变量呢?

  1. 因为是网络通信,那么一定是需要 sockfd 套接字的

  2. 既然需要网络通信,那么座位一款服务器,也是一定需要端口号和IP地址的

目前,我们先说这么多,我们后面说到对应的部分的时候,我们在把需要的加进去。

服务器中需要哪些成员函数呢?还有静态的函数呢?

  1. 我们前面说了一定是需要一个构造函数的,构造函数里面主要就是初始化对象中的部分成员变量

  2. 还需要一个初始化函数,该函数就需要创建套接字绑定,以及其他的工作等...

  3. 当套接字等预备工作准备好后,是不是应该让服务器开始读取网络中的数据了?所以需要一个start函数,让服务器开始读取数据

目前我们就说这些,当我们还需要其他的函数,我们在说!

类的结构

class TcpServer
{
public:
    TcpServer()
    {
    }
​
    void initServer()
    {
    }
​
    void start()
    {
    }
private:
    std::string _ip;
    uint16_t _port;
    int _sock;
    static Log log;
};
​
Log TcpServer::log;

上面就是服务器的结构,我们一边说一边介绍里面还需要什么函数等

上面多了一个 static 的成员变量, log,这个我们之前说过了,没有什么目的,知识一个日志类,可以帮助我们打印日志。

那么类的结构就是这了,那么我们如何编写类里面的这些函数呢?

构造函数

构造函数其实不需要说什么,只需要把里面成员变量中能初始化的数据给初始化即可!

那么什么可以初始化呢?当我们启动一个服务器的时候,想要连接的端口和IP都是我们自己设置的,所以这个IP和端口是需要我们自己初始化的,而 sock 是当我们创建套接字的时候才需要初始化,又因为套接字其实就是文件描述符,所以我们初始化为 -1 即可!

    TcpServer(uint16_t port, const std::string ip = "0.0.0.0")
        : _ip(ip), _port(port), _sock(-1)
    {}

初始化函数

初始化函数就是把服务器里面的套接字创建好,还需要绑定,如果是UDP的话,那么这样就以及初始化好了,但是我们现在是TCP,那么我们还需要做什么呢?

我们前面提过一下TCP与UDP的特性:

  1. TCP:面向连接的

  2. UDP:无连接的

所以如果是TCP的话,那么还是面向连接的,所以我们需要多做一些工作,那么是什么呢?

既然TCP是面向连接的,那么当有很多人来连接TCP的时候,如果TCP只有一个套接字,那么如果对方一直连着怎么办呢?那么此时来了其他人还能连接上吗?不能了!

所以在TCP里面需要将创建的第一个套接字设置为监听套接字,然后这个套接字主要是为了接收有哪些人来连接该服务,当有客户端想要连接的时候,那么就会向该套接字发送连接,然后服务器可以通过 accept 接收到一个连接请求,然后accept函数就会返回一个新创建的文件描述符,此时返回的这个文件描述符才是用来通信的文件描述符!

下面再初始化函数里面用到了 listen 函数,下面看一下该函数:

NAME
       listen - listen for connections on a socket
​
SYNOPSIS
       #include           /* See NOTES */
       #include 
​
       int listen(int sockfd, int backlog);
  • 该函数就是将套接字设置为监听套接字(设置为监听套接字是为了接收想逃连接该主机的连接请求)

  • 第一个参数:想要设置为监听套接字的套接字

  • 第二个参数:接听套接字的等待队列

   void initServer()
    {
        // 1. 创建套接字
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            log(FATAL, "sockfd 创建失败! errno: %d, %s", errno, strerror(errno));
            exit(errno);
        }
        log(INFO, "sockfd 创建成功 sockfd: %d", _sock);
​
        // 2. bind
        struct sockaddr_in local;
        local.sin_family = AF_INET;                     // 套接字域网络
        local.sin_port = htons(_port);                  // 端口——主机转网络
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP——点分十进制——主机转网络
        int r = bind(_sock, (struct sockaddr *)&local, sizeof local);
        if (r < 0)
        {
            log(FATAL, "bind 失败! errno: %d, %s", errno, strerror(errno));
            exit(errno);
        }
        log(INFO, "bind 成功~");
​
        // 3. 设置监听套接字  ?
        // TCP 协议是面向连接的,所以需要连接
        // 设置监听套接字,第一个参数就是想要设置的套接字,第二个参数是等待队列...
        r = listen(_sock, 0);
        if (r < 0)
        {
            log(FATAL, "listen 失败! errno: %d, %s", errno, strerror(errno));
            exit(errno);
        }
        log(INFO, "设置监听套接字成功~");
    }

启动服务器

当上面的工作做完之后,服务器就可以开始接收想要连接服务器的主机了,也就是可以从监听套接字里面读取想要连接的服务器的主机,当读取到后,然后服务器可以和客户端建立连接,连接成功后服务器使用新返回的套接字来通讯。

那么服务器如何接收想要连接服务器的主机呢?

使用系统调用 accept 函数,下面介绍 accept 函数:

NAME
       accept, accept4 - accept a connection on a socket
​
SYNOPSIS
       #include           /* See NOTES */
       #include 
​
       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 该函数就是从网络中读取想要链接的客户端,然后链接一个新的套接字

  • 第一个参数:就是从哪一个监听套接字里面接收想要链接的客户端

  • 第二个参数:是一个输出型参数,就是通过该函数来接收对端客户端的IP和端口

  • 第三个参数:是一个输入输出型参数,输入表示第二个参数的大小,输出表示返回的第二个参数的大小

就可以使用该函数来接收监听套接字里面想要链接的客户端。

我们既然想要通信,那么我们就来看一下通信函数,当我们接收到链接后,我们就调用这个函数,通信函数里面没有什么可以说的,不过这里我们还是要说一下,可以将系统和网络关联再一起,我们使用的是TCP套接字,TCP是面向链接且是字节流的,那么我们就可以采用文件的那一套接口来读取TCP套接字里面的数据,所以我们通信的时候可以采用 read/write 函数来读写。

还有,既然是通信,那么就是需要不断的从套接字里面读取数据,然后返回数据,所以是不能断开的,所以是一个死循环!

    static void commun(int sockfd, struct sockaddr_in &peer, socklen_t len)
    {
        // 缓冲区
        char buffer[1024];
        while (true)
        {
            // 从 sockfd 套接字中读取数据
            int s = read(sockfd, buffer, 1024 - 1);
            if (s > 0)
            {
                // 读取成功后,将读取到的数据当作字符串,然后显示出来
                buffer[s] = 0;
                std::cout << "server recv# " << buffer << std::endl;
            }
            else if (s == 0)
            {
                // 对端关闭连接
                log(INFO, "ip: %s port: %d 关闭连接, server 也关闭", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
                break;
            }
            else
            {
                log(ERROR, "读取数据错误");
                break;
                // 读取失败
            }
            std::string message = name + ": " + buffer;
            // 发送数据给对端主机
            write(sockfd, message.c_str(), message.size());
        }
    }

单进程版本

那么启动服务器该怎么写呢?

我们这一次认为,启动服务器客户端给服务器发什么,那么服务器就显示到显示器上什么!并且回显给客户端!

既然是服务器,那么一定是死循环的处理任务,既然我们想要做客户端给服务器发什么,那么服务器就返回什么,那么我们可以使用一个函数,这个函数里面就是服务器一直在死循环的读取数据,读取到数据后就返回数据!

    void start()
    {
        _tPool_ptr->run();
        // 服务器,死循环
        while (true)
        {
            // 接受套接字,通过 _sock(监听套接字) 如果有想要连接的客户端,那么就会发送到监听套接字,然后通过 accept 来接受
            struct sockaddr_in peer;
            socklen_t len = sizeof peer;
            // 再监听套接字里面接收想要链接的主机,接收后创建一个套接字并返回,采用新的套接字通信
            int sockfd = accept(_sock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                log(ERROR, "accept 失败!");
                // 重新接收
                continue;
            }
            log(INFO, "成功获取到一个连接 sockfd: %d", sockfd);
            // 接收成功, 获得了新的通信套接字 sockfd
​
​
            // version_1 单进程版本
            // 通信函数
            commun(sockfd, peer, len);
            // 通信完成,服务器关闭该文件描述符
            close(sockfd);
        }
    }

但是如果这样写的话(单进程版本),那么是有一个缺点的,单进程版本的话如果当一个链接到达,那么就会 accept 到一个新的套接字,此时使用该套接字进程通信,但是由于是单进程,所以此时该执行流就会处理通信了,我们前面也说了,通信就是一个死循环,这样的话该执行流就会再通信函数里面,如果此时后面还有链接到达,那么就是有问题的,就处理不了其他到达的链接了。

多进程版

多进程版本其实该的地方并不多,只有几行代码!

那么要怎么改呢?我们可以使用父进程来一直处理监听套接字的消息,如果有链接到达,那么就创建新的套接字,然后再创建子进程,让子进程去处理通信函数,这样父进程就可以继续处理其他的到达的链接。

但是如果创建子进程的话,那么只要创建好就不用管了吗?我们再进程的时候说过,如果创建好的子进程不等待的话,那么就会造成僵尸进程,僵尸进程就会发生内存泄露,所以创建的子进程是需要等待的,但是我们要怎么等待呢?

有三种等待方法:

  1. 直接等待,也就是使用 wait/waitpid 函数等待,但是如果这样等待的话一定需要非阻塞轮询,但是这样的话就麻烦的很,因为等待的时候不能妨碍父进程处理链接到达的时候,所以就需要使用循环的方式将所有的代码包进去!那么为什么不使用阻塞的方式等待呢?如果使用阻塞的方式那么就是和父进程的单进程版本没什么不同,如果阻塞的话,那么父进程也是阻塞到 wait/waitpid 函数里面,也处理不了新到来的链接,所以不能使用阻塞等待。

  2. 我们再学习信号的时候,还学习了 SIGCHLD 信号,该信号就是当子进程死亡的时候,会发SIGCHLD信号给父进程,那么我们可以捕捉该信号,然后对该信号捕捉,再捕捉函数里面可以采用非阻塞循环的方式等待,如果等待失败那么捕捉函数就结束。

  3. 第三种,也就是我们想要采用的方法!我们既然学习了信号,那么我们知道信号有三种处理动作:默认、忽略、自定义(捕捉),如果对一个信号忽略的话,那么进程对该信号的处理动作就成了忽略,那么对SIGCHLD信号忽略的话,子进程给父进程发送SIGCHLD信号就会被忽略掉,而此时子进程也就不会进入到僵尸,子进程会自动释放掉僵尸状态,这样我们也就不用等待子进程了。

  
  void start()
    {
        // 设置对SIGCHLD信号的忽略
        signal(SIGCHLD, SIG_IGN);
        _tPool_ptr->run();
        // 服务器,死循环
        while (true)
        {
            // 接受套接字,通过 _sock(监听套接字) 如果有想要连接的客户端,那么就会发送到监听套接字,然后通过 accept 来接受
            struct sockaddr_in peer;
            socklen_t len = sizeof peer;
            int sockfd = accept(_sock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                log(ERROR, "accept 失败!");
                // 重新接收
                continue;
            }
            log(INFO, "成功获取到一个连接 sockfd: %d", sockfd);
            // 接收成功, 获得了新的通信套接字 sockfd
​
​
            // version_2 多进程版本
            if(fork() == 0)
            {
                // 子进程关闭监听套接字
                close(_sock);
                commun(sockfd, peer, len);
                exit(0);
            }
             close(sockfd);
        }
    }

多线程版

创建一个进程需要操作系统做哪些工作呢?

我们知道每一个进程都是独立的,而创建一个进程也就是需要创建一个新的 task_struct(PCB),还有每个进程都有自己的地址空间 mm_struct 还有每个进程虚拟到物理的映射,也就是需要页表来完成,以及还有其他的内核数据结构,所以创建一个进程对操作系统的消耗还是比较大的,那么我们怎么办呢?

我们可以创建线程,我们知道线程的创建时要比进程要轻的多的,因为创建线程只需要创建一个控制块,还有就是维护一下线程私有的数据即可,所以我们可以使用多线程来代替多进程!

线程的控制我们再前面的时候也已经说过了,那么我们怎么写这部分代码呢?

当主线程获取到一个新的连接后,主线程就创建一个新线程,然后让新线程去执行通信函数,即可,然后主线程继续去获取新的连接!

但是这里再写的时候时需要注意很多细节的,比如如果我们写的传给线程的函数是一个类内的成员函数,那么成员函数里面是有一个隐层的 this 指针的,虽然我们没有写这个this指针,但是实际上它是有的,所以我们看起来是一个 void*(*)void* 的函数,实际上是一个 void*(this*, *)void* 的类型,所以我们写类内的函数是需要写成 static 的。

还有就是如果需要传参的话,那么是不能传栈上开辟空间的数据的,因为栈上开辟空间的数据会在出了作用域后销毁,而主线程创建的栈上的数据是一下就销毁了,所以需要主线程去在堆上开辟空间,但是入果传给线程的话,那么线程里面的函数是需要记得释放的。

    void start()
    {
        _tPool_ptr->run();
        // 服务器,死循环
        while (true)
        {
            // 接受套接字,通过 _sock(监听套接字) 如果有想要连接的客户端,那么就会发送到监听套接字,然后通过 accept 来接受
            struct sockaddr_in peer;
            socklen_t len = sizeof peer;
            int sockfd = accept(_sock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                log(ERROR, "accept 失败!");
                // 重新接收
                continue;
            }
            log(INFO, "成功获取到一个连接 sockfd: %d", sockfd);
            // 接收成功, 获得了新的通信套接字 sockfd
​
            // version_3 多线程版
            pthread_t tid;
            // 因为只需要传入两个参数,所以这里将 pair 作为传入的类型,pair里面有一个int用来存放 sockfd 还有一个 struct sockaddr_in 类型的
            std::pair* td = new std::pair({sockfd, peer});
            pthread_create(&tid, nullptr, routine, (void*)td);
​
            // 如果使用多线程,那么这里就不能关闭套接字了,那么就需要在函数执行结束后关闭该套接字,也就是在 routine 函数执行后调用
            // close(sockfd);
        }
    }
    
    // routine 函数里面调用通信函数, args 变量里面就是通信函数里面需要的参数
    static void *routine(void *args)
    {
        // 线程将自己分离
        pthread_detach(pthread_self());
​
        std::pair *td = (std::pair *)args;
        commun(td->first, td->second, (socklen_t)sizeof(td->second));
        // 关闭套接字
        close(td->first);
        delete td;
    }

线程池版

线程版本的其实已经可以了,但是我们想一下,入锅一瞬间来了大量的连接,那么操作系统也需要一瞬间创建大量的线程,那么服务器的压力还是很大的,那么有没有什么办法可以环境缓解一瞬间需要创建大量的连接呢?

有办法:线程池,我们可以提前创建好一大批线程,然后当来了连接后就交给这个线程处理,如果没有连接到来的时候,我们就可以让这些线程等待即可。

那么线程池版本的怎么写呢?

我们在前面写过一个线程池,我们就基于前面的线程池该即可!

前面的线程池为了可以处理任意类型的数据,所以线程池是一个米板类型的,也就是可以处理任意类型的数据,所以我们需要将通信封装成一个任务,而且线程池的任务类需要提供一个仿函数,只要调用仿函数,那么就可以处理这个任务没所以我们还需要将通信封装到一个类里面,然后该类调用仿函数,那么仿函数就会调用通信函数,所以我们就可以将通信函数所需要的参数作为成员变量初始化到任务类中,然后当线程池里面的线程处理任务的时候,直接调用仿函数,然后仿函数里面会将成员变量传给通信函数,然后调用通信函数。

那么线程池哪里来呢?

我们可以将线程池的对象加到TCP服务器的成员变量中,然后构造的时候,我们就构造这个线程池,当服务器开始运行的时候,我们就让该线程池 run 起来。

所以我们的成员变量和构造函数也需要加一点东西。

// 所有的成员变量
private:
    std::string _ip;
    uint16_t _port;
    int _sock;
    threadPool *_tPool_ptr;
    static Log log;
// 新的构造函数
    TcpServer(uint16_t port, const std::string ip = "0.0.0.0")
        : _ip(ip), _port(port), _sock(-1), _tPool_ptr(threadPool::getObj(10))
    { }

还有就是在服务器 start 的时候,我们调用让服务器 run 起来的函数。我们前面在说线程池的时候已经说过了。

还有就是那么当主线程接收到连接现在该怎么办呢?

当主线程接收到连接后,我们需要将连接封装成一个任务然后 push 到线程池中,我们后面在看任务类怎么写,我们先将服务器的启动函数里面的内容写好(也就是接收到连接后的内容)。

    void start()
    {
        _tPool_ptr->run();
        // 服务器,死循环
        while (true)
        {
            // 接受套接字,通过 _sock(监听套接字) 如果有想要连接的客户端,那么就会发送到监听套接字,然后通过 accept 来接受
            struct sockaddr_in peer;
            socklen_t len = sizeof peer;
            int sockfd = accept(_sock, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                log(ERROR, "accept 失败!");
                // 重新接收
                continue;
            }
            log(INFO, "成功获取到一个连接 sockfd: %d", sockfd);
            // 接收成功, 获得了新的通信套接字 sockfd
​
            // version_4 线程池版
            task *date = new task(sockfd, peer, len, commun);
            _tPool_ptr->push_task(*date);
​
            // 如果使用多线程,那么这里就不能关闭套接字了
            // close(sockfd);
        }
    }

因为通信函数里面需要通信的套接字,还需要 struct sockaddr_in 的对象,还有该对象的长度 len,还有就是因为我们的任务就是通信,所以我们需要将通信的函数作为任务给放进去,让线程执行这个任务(通信),而其他的只是作为该函数的参数传进去。

所以我们看一下任务怎么写,我们任务很简单就是成员变量里面有四个字段,套接字、struct sockaddr_in的对象、长度、通信函数,我们还需要一个默认构造函数和一个构造函数,还需要一个仿函数,仿函数里面就是调用通信函数即可,然后将参数传入。

#include 
#include 
#include 
​
typedef std::function tCallBack_t;
​
class task
{
public:
    task()
    {
    }
    task(int sockfd, struct sockaddr_in &peer, socklen_t len, tCallBack_t callBack)
        : _sockfd(sockfd), _peer(peer), _len(len), _callBack(callBack)
    {
    }
​
    int operator()(const std::string &name)
    {
        _callBack(_sockfd, _peer, _len, name);
    }
​
private:
    int _sockfd;
    struct sockaddr_in _peer;
    socklen_t _len;
    tCallBack_t _callBack;
};

如果是这样,那么就结束了吗?我们在多进程那里父进程在创建子进程后就关闭掉新获取到的套接字,所以不会造成文件描述符泄露,在多线程那里是在 routine 函数里面调用完通信函数后关闭了套接字,所以也不会造成文件描述符泄露,那么在这里需要在哪里关闭呢?在仿函数里面可以吗?当然可以,但是我们写的是一个一个的模块,我们并不希望任务和业务耦合在一起,所以我们并不建议在仿函数里面(任务类)里面关闭,所以我们可以在通信函数调用完后关闭。

所以在线程池这里我们调用通信函数,我们是需要在通信函数里面的最后关闭套接字的。

服务器的函数调用

服务器类我们已经基本编写完毕了,那么我们需要怎么调用这些函数呢?

我们在最开始的时候其实就已经说了我们想要如何写:

  1. 我们先创建一个服务器的对象,也就是调用构造函数

  2. 我们调用初始化函数,来完成服务器里面的数据的初始化例如:套接字创建、绑定、设置监听套接字

  3. 我们启动服务器,也就是调用 start 函数,该函数会接收到连接,然后返回一个新的套接字,然后使用该套接字通信。

上面就是我们打算如何写的,所以我们也就是这样调用即可:

void usage(const std::string& proc)
{
    std::cout << "\nUse: " << proc << " port\n" << std:: endl;
}
​
int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        usage(argv[0]);
        exit(0);
    }
​
    std::unique_ptr srv(new TcpServer(atoi(argv[1])));
    srv->initServer();
    srv->start();
    return 0;
}

这里我们还使用了一个只能指针,这样的话,我们就可以不用害怕服务器对象忘记释放了。

TCP的服务器就基本写完了,那么我们看一下如何调用这些函数,还有就是我们前面说的该服务器还可以进程大小写转换,还有英译汉,我们等客户端也编写好后,然后我们进行说明。

TCP客户端编写

客户端我们就不进行封装了,我们下面说一下准备如何写客户端!

  1. 客户端和服务器一样,既然想要使用网络通信,那么就需要创建套接字

  2. 套接字创建好后,我们需要绑定吗?不需要,我们在UDP编写的时候已经说了。

  3. 那么需要设置监听套接字吗?不需要,因为客户端是去连接别人的,不需要别人来连接自己,所以不需要设置监听套接字。

  4. 既然不需要设置监听套接字,那么也就不需要 accept。

  5. 那么客户端需要什么?我们知道TCP是面向连接的,所以客户端是需要连接别人的,所以客户端需要 connect 别人(服务器)

  6. 所以当客户端连接到服务器后就可以开始通信了。

版本一

而通信也很简单,就是向服务器发送数据,然后接收数据即可!

Log log;
​
void usage(const char *proc)
{
    std::cout << "\nUse: " << proc << " serverIP  serverPort\n"
              << std::endl;
}
​
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(0);
    }
​
    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        log(FATAL, "client create sockfd error! errno: %d ,%s", errno, strerror(errno));
        exit(errno);
    }
    log(INFO, "client create sockfd success~ sockfd: %d", sockfd);
​
    // 客户端不需要 bind
    // 客户端不需要设置监听套接字
    // 客户端不需要 accept
    // 但是客户端需要 connect
    struct sockaddr_in peer;
    peer.sin_family = AF_INET;
    peer.sin_port = htons(atoi(argv[2]));
    inet_aton(argv[1], &(peer.sin_addr));
​
    // 这里是返回客户端信息的客户端
    // connect
    int r = connect(sockfd, (struct sockaddr *)&peer, (socklen_t)sizeof(peer));
    if (r < 0)
    {
        log(FATAL, "connect error! erron: %d, %s", errno, strerror(errno));
        exit(errno);
    }
    log(INFO, "connect success~");
    // 通信
    char buffer[1024];
    while (true)
    {
        std::cerr << "please Enter> ";
        std::string message;
        std::getline(std::cin, message);
        // 输入成功
        write(sockfd, message.c_str(), message.size());
        // 读取 server 返回
        int s = read(sockfd, buffer, 1024 - 1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << buffer << std::endl;
        }
        else if(s == 0)
        {
            log(INFO, "对端关闭连接,我也关闭~");
            break;
        }
        else
        {
            log(ERROR, "读取失败!");
            break;
        }
    }
    // 关闭套接字
    close(sockfd);
    return 0;
}

客户端这里我们就直接面向过程写了,也不进行封装了。

那么我们看一下服务器如何进行处理其他的业务,例如:大小写转换、英译汉等...

我们这里只写两个,一个就是大小写转换,还有就是英译汉!

其实这里想要改写很简单,我们现在的服务器不是线程池版本?那么我们如何让线程执行我们想要让线程处理的任务呢?也就是之前的通信函数,我们是不是将通信函数以及它所需要的参数封装成一个任务,然后调用任务的仿函数,仿函数里面调用了我们想要处理的任务,所以我们可不可以将其他的任务封装成任务然后 push 到热线程池里面呢?可以的!

但是我们还是像通信函数那样写是有问题的,如果使用线程池,那么线程池的大小是有上限的,那么如果此时有是个线程,此时有是个用户来了,那么此时的是个线程在为是个用户提供服务,由于服务是长链接的,所以不会断开,也就是以及链接好的用户,那么即使不发消息也是占用一个线程的,所以这是不合理的,那么我们需要怎么做呢?我们可以将任务做成非死循环的,也就是你发一条消息,服务器就处理一条,然后关闭套接字,然后为其他人处理任务,所以这样就可以处理成千上百的人,不会因为只链接而不使用浪费资源。

所以我们的任务就需要改一下,我们就使用大小写转化来模拟:

大小写转换服务

    // 大小写转换
    static void Change(int sockfd, struct sockaddr_in &peer, socklen_t len, std::string name)
    {
        // 缓冲区
        char buffer[1024];
        log(DEBUG, "处理ing...");
​
        int s = read(sockfd, buffer, 1024 - 1);
        log(DEBUG, "读取成功~ s: %d", s);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "需要转化的字符串: " << buffer << std::endl;
            // 大小写转化
            std::string message;
            char *begin = buffer;
            while (*begin != '\0')
            {
                if (islower(*begin))
                    message += (toupper(*begin));
                else
                    message += *begin;
                ++begin;
            }
​
            std::cout << "转换完成: " << message << std::endl;
            write(sockfd, message.c_str(), message.size());
        }
        else if (s == 0)
        {
            // 对端关闭连接
            log(INFO, "ip: %s port: %d 关闭连接, server 也关闭", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
        }
        else
        {
            log(ERROR, "读取数据错误");
            // 读取失败
        }
​
        // 线程池版本需要在这里关闭套接字
        close(sockfd);
    }

这个函数很简单,也就是从套接字里面读取到数据,然后再转换,转换后使用 write 函数写回去即可!

版本二

因为这个函数并不是一直死循环,而是只当有链接到来的时候,就处理一条数据,然后关闭套接字返回,那么这样的话,客户端也需要改写一下,为什么?因为服务器只处理一条消息,然后关闭了套接字,那么客户端是一直向以及链接的套接字里面发送数据,如果服务器关闭的话,那么客户端还继续写,就会收到SIGPIPE信号,导致客户端退出,所以当客户端接收到服务器的返回值之后,是需要重新向服务器发起连接的,但是一定是先输入数据,再发启连接,否则的话,就和服务器是死循环没什么区别了。

但是如果客户端还是使用之前的那个套接字来连接的话,那么会显示以及连接了,所以我们需要创建新的套接字来连接,所以我们可以将之前的循环去掉,然后将之前的代码放到一个循环就可以了。

Log log;
​
void usage(const char *proc)
{
    std::cout << "\nUse: " << proc << " serverIP  serverPort\n"
              << std::endl;
}
​
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(0);
    }
    while (true)
    {
        // 通信
        char buffer[1024];
​
        std::cerr << "please Enter> ";
        std::string message;
        std::getline(std::cin, message);
        // 输入成功
​
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd < 0)
        {
            log(FATAL, "client create sockfd error! errno: %d ,%s", errno, strerror(errno));
            exit(errno);
        }
        struct sockaddr_in peer;
        peer.sin_family = AF_INET;
        peer.sin_port = htons(atoi(argv[2]));
        inet_aton(argv[1], &(peer.sin_addr));
​
        int r = connect(sockfd, (struct sockaddr *)&peer, (socklen_t)sizeof(peer));
        if (r < 0)
        {
            log(FATAL, "connect error! erron: %d, %s", errno, strerror(errno));
            exit(errno);
        }
​
        write(sockfd, message.c_str(), message.size());
        log(DEBUG, "客户端 发送数据成功~");
​
        // 读取 server 返回
        int s = read(sockfd, buffer, 1024 - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "小写转大写完成: " << buffer << std::endl;
        }
        else if (s == 0)
        {
            log(INFO, "对端关闭连接,我也关闭~");
            break;
        }
        else
        {
            log(ERROR, "读取失败!");
            break;
        }
​
        close(sockfd);
    }
    return 0;
}

所以我们现在就是直接写处理的函数即可!

英译汉服务

下面我们再看一下英译汉的函数,其实和大小写转换一样,包括客户端都一样,只是里面打印的字段可能有些不一样,但是这不影响我们整体的逻辑,而且英译汉也是很简单,我们不多介绍。

    static void *routine(void *args)
    {
        // 线程将自己分离
        pthread_detach(pthread_self());
​
        std::pair *td = (std::pair *)args;
        commun(td->first, td->second, (socklen_t)sizeof(td->second), "thread 1");
        // 关闭套接字
        close(td->first);
        delete td;
    }
​
    static void translation(int sockfd, struct sockaddr_in &peer, socklen_t len, std::string name)
    {
        static std::unordered_map englishDict{
            {"sort", "排序"},
            {"chat", "聊天"},
            {"hello", "你好"},
            {"world", "世界"},
            {"computer", "电脑"},
            {"game", "游戏"},
            {"host", "主机"},
            {"telphone", "手机"},
            {"word", "单词"},
            {"english", "英语"},
            {"chinese", "语文"},
            {"math", "数学"}};
​
        char buffer[1024];
        int s = read(sockfd, buffer, 1024 - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::unordered_map::iterator kv  = englishDict.find(buffer);
            if(kv != englishDict.end())
            {
                std::string message = englishDict[buffer];
                write(sockfd, message.c_str(), message.size());
            }
            else
            {
                std::string message = "找不到该单词";
                write(sockfd, message.c_str(), message.size());
            }
        }
        else if (s == 0)
        {
            // 对端关闭连接
            log(INFO, "ip: %s port: %d 关闭连接, server 也关闭", inet_ntoa(peer.sin_addr), ntohs(peer.sin_port));
        }
        else
        {
            log(ERROR, "读取数据错误");
            // 读取失败
        }
​
        close(sockfd);
    }

如何将大小写转换和英译汉放入到线程池中?

其实这个也很简单,就是如何将通信函数放到任务中,那么就将通信函数替换为其他两个函数放入到任务中,其他的都不变,但是这里是有一个条件的,那就是这三个函数的参数以及返回值是一样的,如果不一样的话,那么就需要使用其他的任务了。

你可能感兴趣的:(tcp/ip,网络协议,网络)