【Linux网络编程】Socket编程--TCP:echo server | 多线程远程命令执行

在这里插入图片描述

????个人主页: 南桥几晴秋
????C++专栏: 南桥谈C++
????C语言专栏: C语言学习系列
????Linux学习专栏: 南桥谈Linux
????数据结构学习专栏: 数据结构杂谈
????数据库学习专栏: 南桥谈MySQL
????Qt学习专栏: 南桥谈Qt
????菜鸡代码练习: 练习随想记录
????git学习: 南桥谈Git

????????????????????????????????????????????????????????????????????????????????????????????????????????????
本科在读菜鸡一枚,指出问题及时改正

前言

在学习本章之前,先看【Linux网络编程】Socket编程–UDP:实现服务器接收客服端的消息 | DictServer简单的英译汉的网络字典 | 简单聊天室】,里面详细介绍函数的使用方法,小编在这篇文章不再具体介绍。

TCP echo server

服务端

创建套接字 | 绑定套接字

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

int socket(int domain, int type, int protocol);

在TCP中,第二个参数,指定套接字类型应该为SOCK_STREAM,其余的和UDP中一样。
在这里插入图片描述


绑定套接字:

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

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

将创建的套接字sockfdstruct addr绑定。


//创建
_listensockfd=::socket(AF_INET,SOCK_STREAM,0);
if(_listensockfd<0)
{
    exit(SOCKET_ERROR);
    LOG(FATAL,"socket create error\n");
}
LOG(INFO,"socket create sussedd,listensockfd: %d\n",_listensockfd); //3

struct sockaddr_in local;
memset(&local,0,sizeof(local));

local.sin_family=AF_INET;
local.sin_port=htons(_port);  //网络序列
local.sin_addr.s_addr=INADDR_ANY;
//绑定:sockfd 和 socket addr
if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local))<0)
{
    LOG(FATAL,"bind error\n");
    exit(BAND_ERROR);
}

listen–设置监听状态

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

int listen(int sockfd, int backlog);
  • int sockfd创建的套接字
  • int backlog是连接队列的长度

由于TCP是面向连接的,因此TCP需要不断地能够做到获取连接,所以设置成监听状态,让套接字准备好,随时准备等待别人来连网。

const static int gblcklog=8;
if(::listen(_sockfd,gblcklog)<0)
{
    LOG(FATAL,"listen error\n");
    exit(LISTEN_ERROR);
}

在这里插入图片描述

accetp–获取连接

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

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

  • int sockfd设置为listen状态的套接字
  • 后面俩输出型参数用来获取 client 端的套接字信息
  • 返回值:返回值是一个文件描述符
    在这里插入图片描述

如何理解这里的文件描述符_sockfd

  • _sockfd套接字是用来获取新的连接,accept返回的文件描述符套接字是用来给客户提供服务的,随这_sockfd套接字获取的连接增多,accept返回的文件描述符套接字会越来越多。

  • _sockfd称之为监听套接字

while(_isrunning)
{
    struct sockaddr_in client;
    socklen_t len=sizeof(client);
    // 获取新连接
    int sockfd=::accept(_listensockfd,(struct sockaddr*)&client,&len);
    if(sockfd<0)
    {
        //获取连接失败
        LOG(WARING,"accept error\n");
        continue;
    }
    InetAddr addr(client);
    LOG(INFO,"get a new link,client info: %s\n",addr.AddrStr().c_str());
	//提供服务
	Service(sockfd,addr);

read–读取数据 | write–读取数据

TCP是面向字节流的,符合流式的特性,在Linux以及C++中,学过文件流等流式特性,这些都属于文件。UDP无法直接做文件读取,需要使用sentorecvdrom

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

服务端提供信息version–0(单进程版)

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

using namespace log_ns;

enum
{
    SOCKET_ERROR=1,
    BAND_ERROR,
    LISTEN_ERROR
};

const static int gport=8888;
const static int gsock=-1;
const static int gblcklog=8;

class TcpServer
{
public:
    TcpServer(uint16_t port=gport)
        :_port(port)
        ,_listensockfd(gsock)
        ,_isrunning(false)
    {}

    void InitServer()
    {
        //创建
        _listensockfd=::socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd<0)
        {
            exit(SOCKET_ERROR);
            LOG(FATAL,"socket create error\n");
        }
        LOG(INFO,"socket create sussess,listensockfd: %d\n",_listensockfd); //3

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));

        local.sin_family=AF_INET;
        local.sin_port=htons(_port);  //网络序列
        local.sin_addr.s_addr=INADDR_ANY;
        //绑定:sockfd 和 socket addr
        if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local))<0)
        {
            LOG(FATAL,"bind error\n");
            exit(BAND_ERROR);
        }
        LOG(INFO,"bind success\n");

        // 设置监听状态 listen
        if(::listen(_listensockfd,gblcklog)<0)
        {
            LOG(FATAL,"listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO,"listen success\n");
    }

    void Loop()
    {
        _isrunning=true;
        while(_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len=sizeof(client);
            // 获取新连接
            int sockfd=::accept(_listensockfd,(struct sockaddr*)&client,&len);
            if(sockfd<0)
            {
                //获取连接失败
                LOG(WARING,"accept error\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO,"get a new link,client info: %s\n",addr.AddrStr().c_str());
            //提供服务--version 0
            Service(sockfd,addr);
        }
        _isrunning=false;
    }

    //提供服务--version 0
    void Service(int sockfd,InetAddr addr)
    {
        //长服务
        while(true)
        {
            char inbuffer[1024];
            ssize_t n=::read(sockfd,inbuffer,sizeof(inbuffer)-1);
            if(n>0)
            {
                std::string echo_string="[sever echo] # ";
                echo_string+=inbuffer;
                write(sockfd,echo_string.c_str(),echo_string.size());
            }
            else if(n==0)
            {
                LOG(INFO,"client %s quit\n",addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR,"read error: %s\n",addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);
    }

    ~TcpServer()
    {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

服务端提供信息version–1(多进程版)

在上一个版本中,服务器只能接受一个客户端发来的连接,无法接受多个客户端发来的请求,因此称之为单进程版。在这个版本中,使用多进程,实现多个客户端都可以向服务器发起连接。

在这里插入图片描述

上述父进程在创建子进程后,将父进程的数据结构以及文件描述符表都拷贝给了子进程,此时父子进程都指向同样的文件。

如果想让子进程单独处理新获取的连接,对于子进程来说,不关心之前创建的其他连接,只关心子进程本身继承下来的文件描述符,比如4号fd。对于曾经已经打开的文件建议关闭。如果不关,万一将来子进程误对3号fd进行操作。父进程对新获取的连接进行关闭,父进程不需要这个文件,新连接已经交给了子进程,让子进程进行处理。

对于不使用的文件描述符,需要及时关闭,避免文件描述符泄漏

在这里插入图片描述

因此,在多进程版本中,父进程在完成任务后,继续返回到获取连接那里,子进程完成获取到的新连接的任务。这样服务器实现了多进程并发式获取连接。

在这里插入图片描述

子进程创建后未来会结束,一旦结束就会先进入僵尸状态,等待父进程进行wait读取,一旦被读取,僵尸进程才会被释放掉,如果不读取,子进程将会一直处于僵尸状态。当前父进程采用的是阻塞式wait,当子进程被创建,子进程在处理任务时,父进程会一直阻塞等待,如果子进程一直不死,父进程就一直在阻塞等待。此时依旧是等子进程结束,父进程才开始工作,简单来说这不是并发,依然还是一次只能处理一个连接。
解决方案:

  1. 使用信号量,这是最推荐的做法,但是过于简单signal(SIGCHLD,SIG_IGN)
  2. 在子进程中再fork一次,让子进程直接退出,父进程的wait可以立即返回,僵尸状态就可以立即被处理。此时孙子进程的父进程退出了,变成了孤儿进程,孤儿进程被系统领养,退出后,OS会对他进程回收,爷爷进程就不用关心了。
    在这里插入图片描述

完整代码

#pragma once
#include<iostream>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/wait.h>
#include"Log.hpp"
#include"InetAddr.hpp"

using namespace log_ns;

enum
{
    SOCKET_ERROR=1,
    BAND_ERROR,
    LISTEN_ERROR
};

const static int gport=8888;
const static int gsock=-1;
const static int gblcklog=8;

class TcpServer
{
public:
    TcpServer(uint16_t port=gport)
        :_port(port)
        ,_listensockfd(gsock)
        ,_isrunning(false)
    {}

    void InitServer()
    {
        //创建
        _listensockfd=::socket(AF_INET,SOCK_STREAM,0);
        if(_listensockfd<0)
        {
            exit(SOCKET_ERROR);
            LOG(FATAL,"socket create error\n");
        }
        LOG(INFO,"socket create sussess,listensockfd: %d\n",_listensockfd); //3

        struct sockaddr_in local;
        memset(&local,0,sizeof(local));

        local.sin_family=AF_INET;
        local.sin_port=htons(_port);  //网络序列
        local.sin_addr.s_addr=INADDR_ANY;
        //绑定:sockfd 和 socket addr
        if(::bind(_listensockfd,(struct sockaddr*)&local,sizeof(local))<0)
        {
            LOG(FATAL,"bind error\n");
            exit(BAND_ERROR);
        }
        LOG(INFO,"bind success\n");

        // 设置监听状态 listen
        if(::listen(_listensockfd,gblcklog)<0)
        {
            LOG(FATAL,"listen error\n");
            exit(LISTEN_ERROR);
        }
        LOG(INFO,"listen success\n");
    }

    void Loop()
    {
        //signal(SIGCHLD,SIG_IGN);
        _isrunning=true;
        while(_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len=sizeof(client);
            // 获取新连接
            int sockfd=::accept(_listensockfd,(struct sockaddr*)&client,&len);
            if(sockfd<0)
            {
                //获取连接失败
                LOG(WARING,"accept error\n");
                continue;
            }
            InetAddr addr(client);
            LOG(INFO,"get a new link,client info: %s, sockfd is: %d\n",addr.AddrStr().c_str(),sockfd);
            //提供服务--version 0
            //Service(sockfd,addr);

            //提供服务--version 1(多进程版)
            pid_t id=fork();
            if(id==0)
            {
                //child
                ::close(_listensockfd);
                
                if(fork()>0) exit(0);

                Service(sockfd,addr);

                exit(0);
            }
            //fathre
            ::close(sockfd);
            int n=waitpid(id,nullptr,0);
            if(n>0)
            {
                LOG(INFO,"wait child success\n");
            }
        }
        _isrunning=false;
    }

    //提供服务--version 0
    void Service(int sockfd,InetAddr addr)
    {
        //长服务
        while(true)
        {
            char inbuffer[1024];
            ssize_t n=::read(sockfd,inbuffer,sizeof(inbuffer)-1);
            if(n>0)
            {
                std::string echo_string="[sever echo] # ";
                echo_string+=inbuffer;
                write(sockfd,echo_string.c_str(),echo_string.size());
            }
            else if(n==0)
            {
                LOG(INFO,"client %s quit\n",addr.AddrStr().c_str());
                break;
            }
            else
            {
                LOG(ERROR,"read error: %s\n",addr.AddrStr().c_str());
                break;
            }
        }
        ::close(sockfd);
    }

    ~TcpServer()
    {}

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning;
};

在这里插入图片描述

为什么两次都是4号fd?
父进程获取第一次连接后,将4号fd关闭了,此时4号fd是空的,第二次进程连接的时候,依然用这个4号fd。

服务端提供信息version–2(多线程版)

需要让新线程分离,如果在创建一个新线程后,让主线程等待,这不是并发式。
这里需要让新线程分离,这样新线程执行完毕后,会直接被回收。

在多线程中,所有的文件描述符表都是共享的,因此不能对不需要的文件描述符进行关闭。

#pragma once
#include<iostream>
#include<cstring>
#
上一篇:SpringMVC学习笔记(一)-三、SpringMVC中的请求参数绑定


下一篇:Windows Server 搭建DHCP服务器实战