网络入门---TCP通信实现

目录标题

  • 前言
    • 准备工作
  • tcpserver.hpp
    • 构造函数
    • 初始化函数(listen)
    • 运行函数(accept)
  • tcpserver.cc
  • tcpclient.hpp
    • 构造函数
    • 初始化函数
    • 运行函数(connect)
  • tcpclient.cc
  • 问题测试
  • 改进一:多进程
  • 改进二:多线程
  • 改进三:线程池
  • 完整代码

前言

在前面的文章中我们知道了如何使用UDP协议来进行通信,并且在单向通信基础上实现了单词翻译,指令执行,群聊系统,那么在本篇文章中我们将介绍该协议的好兄弟TCP协议,这两个协议都是传输层协议但是他们有着不同的性质,我们说TCP协议有链接的,UDP协议是无连接的,那这个有无连接体现在哪里呢?我们说TCP协议是面向字节流的而UDP协议是面向数据报的,那这两个协议在接收数据和发送数据的时候有没有什么区别呢?我们说UDP协议是不可靠的而TCP协议是可靠的,那这个可靠是通过更加复杂的实现换来的,那这个复杂体现在哪里呢?那本篇文章就将带着大家看看TCP协议是如何来实现网络通信,讲法与之前保持一致采用边实现程序边介绍TCP函数,因为UDP和TCP在实现通信的时候用很多的地方是相同的,所以在这些地方本篇文章不会详细介绍,建议大家先阅读一下之前的文章再来阅读本篇文章:UDP协议介绍。

准备工作

本篇文章要实现的程序就是用户端向服务端发送数据,然后服务端将数据打印在屏幕上并向客户端再发送一个数据作为回复,所以我们这里就创建4个文件,两个文件负责装载main函数用来形成可执行程序,两个文件用来装载描述客服端和服务端通信有关的类:
网络入门---TCP通信实现_第1张图片
在客户端和服务端类中分别有构造函数析构函数初始化函数和运行函数,因为服务端对任意ip的消息都进行接受,所以服务端类只需要一个整形来接收套接字和一个16位的无符号整形来接收端口号,而客户端需要知道自己要向谁发送数据,所以服务端中得添加string类型的对象来保存客户端的ip地址还需要对应存储端口号的变量和套接字的变量,那么这两个文件中的内容就如下:

class tcpserver
{
public:
    tcpserver()
    {}
    void inittcpserver(){}
    void start(){}
    ~tcpserver(){}
private:
    int _listensock;
    uint16_t _port;
};

class tcpclient
{
public:
    tcpclient(){}
    void inittcpclient(){}
    void start(){}
    ~tcpclient(){}
private:
    int _sock;
    uint16_t _serverport;
    string _serverip;
};

那么接下来我们首先就要实现tcpserver.hpp文件中的内容。

tcpserver.hpp

构造函数

因为服务端中有一个变量用来存储端口号,而socket套接字是使用函数socket函数来创建的,所以构造函数就只需要一个参数用来获取用户传递的端口号即可:

tcpserver(uint16_t port)
{}

那么在构造函数里面就将_port变量初始化一下,将套接字初始化为-1即可,那么这里的代码如下:

tcpserver(uint16_t port)
:_port(port)
,_listensock(-1)
{}

初始化函数(listen)

这里的逻辑大致与UDP协议是一样的,首先使用socket函数创建套接字:
在这里插入图片描述
第一个参数传递AF_INET表示使用网络通信,UDP在传递第二个参数的时候回传递SOCK_DGRM表示用户数据报套接字,那么TCP在传递第二个参数的时候则应该传递SOCK_STREAM表示流式套接字,第三个参数直接传递为0即可,然后返回值就是创建好的套接字也就是一个文件描述符,我们将返回值赋值给类中的套接字即可,因为该函数创建套接字的时候可能成功也可能失败,所以在赋值完成之后我们应该使用if语句检查一下,如果返回值为-1就表示创建套接字失败,我们就打印一下错误并使用exit函数退出程序:

void inittcpserver()
{
	 //创建端口号
	 _listensock=socket(AF_INET,SOCK_STREAM,0);
	 if(_listensock==-1)
	 {
	     cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;
	     exit(SOCKET_ERR);//枚举常量
	 }
	 cout<<"create socket success"<<endl;
}

套接字创建成功之后就该将主机的ip地址和对应的端口号与套接字绑定起来,那么这里就得使用bind函数:
在这里插入图片描述
在使用bind函数之前我们得先创建一个sockaddr_in对象,然后将对应的ip地址和端口号填写进去,再通过强制转换的方式将该对象的地址传递给bind函数,因为bing函数可能会出现失败,所以我们这里就创建一个变量用来记录bind函数的返回值,如果返回值为-1就说明创建失败我们就打印错误并退出程序:

void inittcpserver()
 {
     //创建端口号
     _listensock=socket(AF_INET,SOCK_STREAM,0);
     if(_listensock==-1)
     {
         cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;
         exit(SOCKET_ERR);
     }
     cout<<"create socket success"<<endl;
     //初始化信息
     struct sockaddr_in local;
     local.sin_family=AF_INET;
     local.sin_port=htons(_port);
     local.sin_addr.s_addr=INADDR_ANY;       
     //开始bind这些信息
     int n=bind(_listensock,(struct sockaddr*)&local,sizeof(local));
     if(n==-1)
     {
         cout<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl;
         exit(BIND_ERR);
     }
     cout<<"bind success"<<endl;
 }

udp协议的初始化函数在写到这里的时候就已经结束了,但是tcp协议还需要做一件事情就是将当前套接字的状态设置为监听状态,处于监听状态的套接字可以收到客户端发来的链接请求反之则不能,这就好比客服上班了你才能咨询人工客服,客服下班你就只能咨询傻瓜机器人,那么这上班的状态就相当于是监听状态,那么这里用到的函数就是listen函数:
在这里插入图片描述
第一个参数表示将哪个套接字设置为监听状态,第二个参数大家展示不需要管传递一个不大的数字即可比如说5,同样的道理该设置监听状态也可能会出现的不成功的时候,那么就可以通过返回值来进行判断如果等于-1就表示设置监听状态不成功,那么该函数完整的代码如下:

    void inittcpserver()
    {
        //创建端口号
        _listensock=socket(AF_INET,SOCK_STREAM,0);
        if(_listensock==-1)
        {
            cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
        cout<<"create socket success"<<endl;
        //初始化信息
        struct sockaddr_in local;
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=INADDR_ANY;       
        //开始bind这些信息
        int n=bind(_listensock,(struct sockaddr*)&local,sizeof(local));
        if(n==-1)
        {
            cout<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl;
            exit(BIND_ERR);
        }
        cout<<"bind success"<<endl;
        //将客户端设置为监听状态
        if(listen(_listensock,5)==-1)
        {
            cout<<"listen socket error"<<endl;
            exit(LISTEN_ERR);
        }
        cout<<"listen socket success"<<endl;
    }

运行函数(accept)

listen函数的作用是将当前的套接字设置为监听状态,位于监听状态的套接字可以获取服务端发来的链接请求,但是listen函数是用来设置状态的不是用来接收链接请求的,要想获取连接就得使用函数accept:
在这里插入图片描述
该函数的第一个参数表示获取哪个套接字的链接请求,第二个参数和第三个参数就和recvfrom一样用来存储是哪个ip地址上的哪个端口号向这个套接字(第一个参数)发起了链接请求,如果该函数运行成功则该函数的返回值是一个文件描述符,如果该函数运行失败也就是获取链接失败则该函数返回-1,那么这里就存在一个问题在学习UDP的时候我们说套接字本身就是一个文件描述符,并且通过该文件描述符和recvfrom,sendto函数相结合就可以做到接收消息和发送消息的功能,那TCP这里的accept函数为什么还要返回一个文件描述呢?这个文件描述符又有什么用呢?那么这里我们就通过餐厅的例子来带着大家进行理解,首先餐厅是有很多的服务员的,有些服务员在餐厅内部进行工作他们负责给客户上菜倒水让客户吃的开心吃的舒心,但是有些服务员却是站在门口的,这些服务员往往长的都比较好看声音也非常的好听,那么她们干的事情就是招揽客户让客户前往自家店来进行用餐,我们把门外的服务员称为A,把餐厅内的服务员称为B,那么这里就存在一个生活现象,当客户被A招揽进餐厅到4号餐桌上吃饭时,是A来负责4号餐桌上的上下菜和卫生打理吗?很明显不是的,A将客户招揽进餐厅后往往都是叫B前来服务,当你在吃饭的时候B一直在服务你,而A则会去干她原本的事情也就是招揽其他的用户前来用餐,所以这里的服务员A就相当于之前设置为监听状态的套接字,而accept返回的文件描述符就相当于服务员B,所以参数中的套接字就负责获取向我申请链接的主机信息,一旦获取成功了就让返回的文件描述与其专门进行通信,而自己则继续获取其他想要和我链接的主机信息,那么这就是accept函数参数介绍,有了这个函数之后我们就可以尝试实现运行函数,首先该函数没有参数:

void start()

然后在函数里面我们就可以使用accept函数来获取链接,因为获取链接可能成功也可能失败,失败的话accept函数就会返回-1,所以这里就可以使用if语句来进行判断,又因为申请链接的客户端可能会有多个,所以这里就采用死循环的方式来不停的获取链接,有主机申请链接accept就会返回,没有的话就会阻塞等待,那么这里的代码如下:

void start()
{
    //服务端不停的获取新的链接
    for(;;)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
        if(sock==-1)
        {
            cout<<"accept error"<<endl;
            continue;
        }
        cout<<"accept a new link success"<<endl;
        cout<<"sock : "<<sock<<endl;
    }
}

链接获取成功之后就可以根据sock变量来进行通信,而通信可以看成一个单独的功能,所以这里又可以将通信的过程放到一个函数里面,未来我们想要不同的功能就调用的函数即可

void serverio(int sock)

因为这里是服务端所以首先创建一个数组作为缓冲区,然后创建一个死循环来不停的读取数据和发送数据:

void serverio(int sock)
{
    char buffer[1024];
    while(true)
    { 
    }
}

UDP是面向数据报的所以他可以使用recvfrom函数读取数据使用sendto函数来发送数据,而这里的TCP是面向字节流并且文件的读写也是面向字节流的,所以这里可以采用与文件相同的方式来进行读取和发送也就是使用函数read和write,那么这里首先就是使用read函数从文件描述符sock中读取数据放到buffer中:
在这里插入图片描述
如果read函数读取成功该函数就会返回读取的字节,如果出现其他的情况导致了读取失败那么该函数就会返回-1,如果读到了文件的结尾该函数就会返回0比如说写段关闭,所以我们就可以根据该函数的返回值来判断客户端的链接是否中断:

void serverio(int sock)
{
    char buffer[1024];
    while(true)
    {
        //把读取的数据当成字符串
        ssize_t n=read(sock,buffer,sizeof(buffer)-1);
        if(n>0)
        {
        }
        else if(n==0)
        {
            cout<<"client quit, me to"<<endl;
            break;
        }
    }
}

那么在if语句里面首先将下表为n的元素赋值为0以防读取到其他数据,然后我们就可与将缓冲区中的数据打印出来,创建一个string对象将其初始化为缓冲区中的内容并添加一句话表示这是客户端发来的消息,然后我们就可以通过write函数将string对象中的数据发送给sock对应的文件:

在这里插入图片描述

void serverio(int sock)
{
    char buffer[1024];
    while(true)
    {
        //把读取的数据当成字符串
        ssize_t n=read(sock,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<"recv message: "<< buffer <<endl;
            string outbuffer =buffer;
            outbuffer+=" server[echo] ";
            write(sock,outbuffer.c_str(),outbuffer.size());
        }
        else if(n==0)
        {
            cout<<"client quit, me to"<<endl;
            break;
        }
    }
}

那么这就是通信函数的内容,在start函数里面就可以直接调用该函数,该函数结束就表明此次网络通信也跟着结束了,那么这个时候就可以使用close函数将套接字sock对应的文件进行关闭,那么start函数完整的内容如下:

void start()
{
    //服务端不停的获取新的链接
    for(;;)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
        if(sock==-1)
        {
            cout<<"accept error"<<endl;
            continue;
        }
        cout<<"accept a new link success"<<endl;
        cout<<"sock : "<<sock<<endl;
        serverio(sock);
        close(sock);//下表的个数存在上限
    }
}

tcpserver.cc

该文件用来生成可执行程序,并且在生成可执行程序的时候得端口号,所以main函数得添加两个参数,然后在main函数开始的时候我们首先得判断一下参数的个数是否正确,如果不正确我们就执行一个函数来告诉使用者如何正确的执行该程序:

static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverport\n\n";
}
int main(int args,char* argv[])
{
    if(args!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
}

然后就可以通过main函数的参数来获取端口号,并通过智能指针创建一个udpserver对象,然后就是调用初始化函数和start运行函数来执行程序:

#include"tcpserver.hpp"
#include
#include
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverport\n\n";
}
int main(int args,char* argv[])
{
    if(args!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
    uint16_t port=atoi(argv[1]);
    unique_ptr<tcpserver> tcil(new tcpserver(port));
    tcil->inittcpserver();
    tcil->start();
    return 0;
}

tcpclient.hpp

构造函数

运行客户端的时候得告诉用户你要连接的端口号和ip地址是多少,所以构造函数得有两个参数构造函数的内容就是初始化类中变量的值,原理和之前的一致,代码如下:

tcpclient(string serverip,uint16_t serverport)
:_serverport(serverport)
,_serverip(serverip)
,_sock(-1)
{}

初始化函数

有了前面的经验我们知道初始化函数的第一步就是创建套接字,并将socket函数的返回值赋值给类内成员,因为套接字可能会创建失败,所以这里得判断一下:

void inittcpclient()
{
    _sock=socket(AF_INET,SOCK_STREAM,0);
    if(_sock==-1)
    {
        cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;
        exit(SOCKET_ERR);
    }
}

在服务端我们创建套接字之后还会通过函数bind将本主机的信息与套接字绑定起来,那这里需要吗?答案是需要的但是这件事不需要我们亲自来做而是操作系统替我们完成,因为操作系统做的话就可以避免端口号冲突的问题,所以我们这里就不需要调用bind函数,那这里我们需要将套接字设置为监听状态吗?答案也是不需要,因为这里实现的是客户端,客户端是用来申请链接的不是接收链接的,那你设置为监听状态有何用呢?对吧!所以到这里初始化函数就已经结束了。

运行函数(connect)

在发送消息之前我们首先得通过创建sockaddr_in对象的方式来告诉操作系统你要向哪个主机进行通信

void start()
{
    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()); 
}

我们前面说TCP协议是有链接的在通信之前得先建立链接,所以服务端中先使用函数accept将套接字设置为监听状态再使用accept函数来获取链接,那么理所当然在客户端中就应该存在一个函数来向服务端申请链接,这个函数就是connect函数
在这里插入图片描述
第一个参数传递你要通过哪个套接字进行通信,第二个参数表示你要向哪个客户端申请链接,第三个参数就表示长度,如果链接申请成功该函数就会返回0,如果链接申请失败该函数就会返回-1:

void start()
{
    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<<" socker connect error "<<endl;
    }
    else
    {
    }
}

链接申请成功之后我们就可以使用套接字来进行通信,那么这里也是和客户端相同的道理先创建一个循环,然后通过write函数和套接字_sock将信息发送出去,再使用read函数和套接字_sock读取服务端发来的数据,那么该函数完整的代码如下:

void start()
{
    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<<" socker connect error "<<endl;
    }
    else
    {
        string message;
        while(true)
        {
            cout<<"please enter# ";
            getline(cin,message);
            write(_sock,message.c_str(),message.size());
            char buffer[1024];
            int n=read(_sock,buffer,sizeof(buffer)-1);
            if(n>0)
            {
                buffer[n]=0;
                cout<<"server回显: "<<buffer<<endl;
            }
            else if(n==0)
            {
                break;
            }
        }     
    }
}

tcpclient.cc

服务端的main函数和客户端的差不多这里就不多说了,直接看代码:

#include"tcpclient.hpp"
#include
#include
static void Usage(string proc)
{
    cout << "\nUsage:\n\t" << proc << " serverip serverport\n\n";
}
int main(int args,char*argv[])
{
    if(args!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string ip=argv[1];
    uint16_t port=atoi(argv[2]);
    unique_ptr<tcpclient> tcil(new tcpclient(ip,port));
    tcil->inittcpclient();
    tcil->start();
    return 0;
}

问题测试

首先创建两个渠道一个客户端一个服务端:
网络入门---TCP通信实现_第2张图片
在这里插入图片描述
然后客户端发送一个hello,我们就可以看到服务端也显示了一个hello,并且客户端还接收到了服务端发来的回应:
网络入门---TCP通信实现_第3张图片
在这里插入图片描述
通过运行结果我们不难发现在一个服务端场景下上面的实现是没有问题的,那么接下来我们再创建一个渠道让其运行服务端看看会有什么现象:
网络入门---TCP通信实现_第4张图片
可以看到这里让我们输入消息但是消息发送之后没有得到任何的回应,并且客户端上也没有显示2号服务端发送的消息:
网络入门---TCP通信实现_第5张图片
我们将一号客户端退出再看看会有什么样的现象:
网络入门---TCP通信实现_第6张图片
退出之后可以看到服务端上立马显示出来之前2号客户端上发送的消息:
网络入门---TCP通信实现_第7张图片
并且2号客户端上也立马显示出来服务端发送过来消息回应:
网络入门---TCP通信实现_第8张图片
那么这就说明当前写的代码面对多个服务端时会出现问题,而这个问题就出现在服务端的start函数:

void start()
{
   //服务端不停的获取新的链接
   for(;;)
   {
       struct sockaddr_in peer;
       socklen_t len=sizeof(peer);
       int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
       if(sock==-1)
       {
           cout<<"accept error"<<endl;
           continue;
       }
       cout<<"accept a new link success"<<endl;
       cout<<"sock : "<<sock<<endl;
       serverio(sock);
       close(sock);//下表的个数存在上限
   }
}
void serverio(int sock)
{
   char buffer[1024];
   while(true)
   {
       //把读取的数据当成字符串
       ssize_t n=read(sock,buffer,sizeof(buffer)-1);
       if(n>0)
       {
           buffer[n]=0;
           cout<<"recv message: "<< buffer <<endl;
           string outbuffer =buffer;
           outbuffer+=" server[echo] ";
           write(sock,outbuffer.c_str(),outbuffer.size());
       }
       else if(n==0)
       {
           cout<<"client quit, me to"<<endl;
           break;
       }
   }
}

服务端通过死循环不停的获取客户端发过来的链接请求,获取链接之后就会运行serverio函数来完成通信,可是该函数内部又是一个死循环来不停的接收消息和发送消息,所以这就导致了外层循环运行了一次之后就再也无法运行第二次除非之前链接成功的客户端主动断开,导致read函数返回0进而退出死循环,那么这就是上面出现问题的原因,所以接下来我们就采用三种方式来改进这个问题。

改进一:多进程

在学习操作系统的时候我们提到过fork函数,该函数可以帮助我们创建子进程然后通过fork函数的不同返回值从而让父子进程执行不同的内容,那么这里我们就有了一个思路,我们可以创建子进程让子进程执行serverio函数,然后父进程继续获取新链接这样两者不就互不耽误了嘛!首先使用fork创建子进程再使用if语句让父子进程执行不同的内容:

pid_t id =fork();
if(id==0)
{
    //子进程执行
}
//父进程执行

fork在创建子进程的时候会将PCB,虚拟地址空间,页表等等等都拷贝一边,所以子进程也继承了很多父进程的内容,其中就包括了父进程打开的_listensock文件描述符,但是子进程是不需要这个文件描述符的,所以这里最好将其关闭,以免影响该文件的正常关闭从而导致资源浪费,_listensock关闭之后就可以调用serverio函数调用完成之后就可以关闭sock套接字,然后调用exit该函数进行退出,那么这就是子进程要执行的代码

pid_t id =fork();
if(id==0)
{
    //子进程执行
    close(_listensock);
    serverio(sock);
    close(sock);
    exit(0);
}
//父进程执行

因为父进程不需要通过sock套接字来完成通信,所以父进程首先关闭该套接字,然后对子进程的信息进行回收,这里的回收就可以使用waitpid函数,但是在回收的时候就会出现问题,是采用阻塞等待还是采用非阻塞式等待呢?如果是阻塞等待话我们上面的操作就好像脱裤子放屁多此一举啊,对吧!为了不让主进程等待客户端断开链接,所以我们创建了父子进程,如果采用阻塞等待话这里的父进程不就又要等了嘛!等的话又怎么能建立新链接呢?所以有小伙伴就想到了使用非阻塞等待,可是这样的话又会出现一个新的问题,一次等待不可能将所有的进程全部回收掉那如果在次之后再也没有建立新的链接的话,accept函数不就一直没有返回了吗?那这个时候非阻塞等待也失去了意义,所以不管采用哪个都存在问题,所以我们这里的解决方法就是让子进程再创建一个孙子进程,让孙子进程执行serverio函数然后子进程直接退出,这时的孙子进程就是一个孤儿进程,当他运行结束的时候会由操作系统来进行回收,这样就解决了父进程等待的问题,那么这里的代码如下:

    void start()
    {
        //服务端不停的获取新的链接
        for(;;)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sock==-1)
            {
                cout<<"accept error"<<endl;
                continue;
            }
            cout<<"accept a new link success"<<endl;
            cout<<"sock : "<<sock<<endl;
            pid_t id =fork();
            if(id==0)
            {
                //子进程执行
                close(_listensock);
                if(fork()>0)
                {
                    //子进程
                    exit(0)
                }
                //孙子进程
                serverio(sock);
                close(sock);
                exit(0);
            }
            //父进程执行
            close(sock);//下表的个数存在上限
        }
    }

那么接下来就可以进行测试,首先将服务端运行起来再将两个客户端运行起来,我们就可以看到下面这样的现象:
在这里插入图片描述
在这里插入图片描述
网络入门---TCP通信实现_第9张图片
然后两个客户端同时输入消息也不会出现问题

网络入门---TCP通信实现_第10张图片
网络入门---TCP通信实现_第11张图片
网络入门---TCP通信实现_第12张图片
那么这是多进程的一种解决方法,跟这相同道理的还有这样写的:signal(SIGCHLD,SIG IGN);子进程在结束的时候会发送型号SIGCHID,signal(SIGCHLD, SIG_IGN) 中的 SIGCHLD 是代表子进程状态变化的信号,SIG_IGN 是一个宏,表示忽略该信号。这行代码告诉操作系统在父进程中忽略(不处理)收到的子进程结束的信号。通常情况下,父进程可能会调用 wait() 或 waitpid() 来等待子进程的终止,并获取其状态,但使用 SIG_IGN 则表示父进程不关心子进程的终止状态,子进程结束后会被操作系统回收资源,那么这里的写法就变成下面这样:

void start()
{
    signal(SIGCHLD,SIG_IGN);
    //服务端不停的获取新的链接
    for(;;)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
        if(sock==-1)
        {
            cout<<"accept error"<<endl;
            continue;
        }
        cout<<"accept a new link success"<<endl;
        cout<<"sock : "<<sock<<endl;
        pid_t id =fork();
        if(id==0)
        {
            //子进程执行
            close(_listensock);
            serverio(sock);
            close(sock);
            exit(0);
        }
        //父进程执行
        close(sock);//下表的个数存在上限
    }
}

改进二:多线程

既然可以采用多进程的方式来进行改进,那么同样的道理也可以采用多线程的方式来进行改进,首先创建一个pthread_t类型的变量,然后再使用pthread_create函数创建线程,因为创建线程的时候得告诉新线程你要执行的函数是哪个?所以我们这里创建一个返回值为void参数为void类型的函数,在该函数里面执行serverio函数,在主线程里面就对新线程进行join回收,因为join是阻塞等待并且他没有非阻塞等待,所以这里就采用分离线程的方式来进行处理,新线程运行结束之后就自动被操作系统回收无需父进程处理,所以这里还得用到detach函数,但是这里还存在一个问题类中静态函数是无法访问类中非静态函数的,这一pthread_create函数在传递参数的时候可以传递this指针过去这样在函数里面就可以强制类型转换然后就可以正常的访问调用了,可是这里还存在一个问题函数的调用是需要传递参数的,而这个参数是之前start函数中通过accept函数返回的也就是变量sock,那如何将这个参数也传递过去呢?所以这里就可以再创建一个名为ThreadData的类,该类中有两个变量专门用来存储通信所用的文件描述符和this指针,那么这里的代码就如下:

class tcpserver;
class ThreadData
{
public:
    ThreadData(tcpserver* self,int& sock)
    :_self(self)
    ,_sock(sock)
    {}
private:
    tcpserver *_self;
    int _sock;
};

新线程执行的函数threadRoutine的实现就是先进行线程分离,再进行参数转换,然后通过参数调用serverio函数,最后关闭文件描述符释放之前创建的ThreadData对象,那么这里的代码如下:

 static void* threadRoute(void* args)
 {
     pthread_detach(pthread_self());
     ThreadData* td=static_cast<ThreadData*>(args);
     td->_self->serverio(td->_sock);
     close(td->_sock);
     delete td;
     return nullptr;
 }

那么start函数的内容就如下:

void start()
{
    signal(SIGCHLD,SIG IGN);
    //服务端不停的获取新的链接
    for(;;)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
        if(sock==-1)
        {
            cout<<"accept error"<<endl;
            continue;
        }
        cout<<"accept a new link success"<<endl;
        cout<<"sock : "<<sock<<endl;
        pthread_t tid;
        ThreadData* td=new ThreadData(this,sock);
        pthread_create(&tid,nullptr,threadRoute,td);
    }
}

那么这就是多线程的修改方式。

改进三:线程池

在前面的文章中我们实现过一个线程池,这里就直接将代码拷贝到当前路径下:

#include "Thread.hpp"
#include "LockGuard.hpp"
#include "log.hpp"
#include 
#include 
#include 
#include 
#include 
using namespace ThreadNs;
const int gnum = 10;
template <class T>
class ThreadPool;
template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool;
    std::string name;

public:
    ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n)
    {}
};
template <class T>
class ThreadPool
{
private:
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = (ThreadData<T> *)args;
        while (true)
        {
            T t;
            {
                LockGuard lockguard(td->threadpool->mutex());
                while (td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); // pop的本质,是将任务从公共队列中,拿到当前线程自己独立的栈中
            }
            t();
        }
        delete td;
        return nullptr;
    }

    ThreadPool(const int &num = gnum) : _num(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }

    void operator=(const ThreadPool &) = delete;
    ThreadPool(const ThreadPool &) = delete;

public:
    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_mutex); }
    bool isQueueEmpty() { return _task_queue.empty(); }
    void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
    T pop()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }
    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

public:
    void run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            std::cout << t->threadname() << " start ..." << std::endl;
        }
    }
    void push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _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 ThreadPool<T> *getInstance()
    {
        if (nullptr == tp)
        {
            _singlock.lock();
            if (nullptr == tp)
            {
                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>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template <class T>
std::mutex ThreadPool<T>::_singlock;

该线程池是使用单例模式实现的,并且在实现的过程中还用到了我们自己封装的线程和自己封装的锁,所以我们还得将这些代码也复制到当前路径下,这是线程的封装:

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 

namespace ThreadNs
{
    typedef std::function<void *(void *)> func_t;
    const int num = 1024;

    class Thread
    {
    private:
        // 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
        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); // TODO
            assert(n == 0);                                            
            (void)n;
        }

        void join()
        {
            int n = pthread_join(tid_, nullptr);
            assert(n == 0);
            (void)n;
        }

        std::string threadname()
        {
            return name_;
        }

        ~Thread()
        {
            // do nothing
        }
        void *callback() { return func_(args_);}
    private:
        std::string name_;
        func_t func_;
        void *args_;
        pthread_t tid_;

        static int threadnum;
    };
    int Thread::threadnum = 1;
} // end namespace ThreadNs

这是锁的封装:

#pragma once

#include 
#include 
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(pthread_mutex_t *mutex): mutex_(mutex)
    {
        mutex_.lock(); //在构造函数中进行加锁
    }
    ~LockGuard()
    {
        mutex_.unlock(); //在析构函数中进行解锁
    }
private:
    Mutex mutex_;
};

线程池是通过懒汉模式来进行实现的,所以每次都通过getInstance函数来获取对象,所以在函数start里面先通过getInstance函数和run函数让线程池运行起来然后再构建新连接,连接创建成功之后我们就可以通过线程池中的push函数往池中添加任务,所以在通信之前我们还得构建任务,通过线程池中的handlerTask我们也不难发现这个任务内有函数调用的运算符重载所以我们这里就再创建一个文件里面就装有任务构建的类,因为任务就是网络通信,网络通信需要serverio函数和对应的套接字,所以该类就两个成员变量和对应重载函数和构造函数

class Task
{
    using func_t = std::function<void(int)>;

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

因为该文件用来构造方法,而方法又和网络通信有关所以我们这里可以直接将serverio有关,所以这里就直接将serverio也搬到该文件里面:

#pragma once
#include 
#include 
#include 
#include 
void serviceIO(int sock)
{
    char buffer[1024];
    while (true)
    {
        ssize_t n = read(sock, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            // 目前我们把读到的数据当成字符串, 截止目前
            buffer[n] = 0;
            std::cout << "recv message: " << buffer << std::endl;

            std::string outbuffer = buffer;
            outbuffer += " server[echo]";

            write(sock, outbuffer.c_str(), outbuffer.size()); // 多路转接
        }
        else if (n == 0)
        {
            // 代表client退出
            cout<<"client quit, me too!";
            break;
        }
    }
    close(sock);
}

class Task
{
    using func_t = std::function<void(int)>;

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

private:
    int _sock;
    func_t _callback;
};

最后start函数调里面掉用push函数在里面传递临时的任务对象即可,那么start函数的完整代码如下:

void start()
{
    ThreadPool<Task>::getInstance()->run();
    //服务端不停的获取新的链接
    for(;;)
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
        if(sock==-1)
        {
            cout<<"accept error"<<endl;
            continue;
        }
        cout<<"accept a new link success"<<endl;
        cout<<"sock : "<<sock<<endl;
        //线程池中添加任务
        ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));
    }
}

代码的运行结果如下:
网络入门---TCP通信实现_第13张图片
网络入门---TCP通信实现_第14张图片
在这里插入图片描述

完整代码

tcpserver.hpp的代码如下:

#include 
#include           
#include 
#include 
#include 
#include 
#include 
#include
#include
#include
#include
#include"ThreadPool.hpp"
#include"Task.hpp"

using namespace std;
enum{
    SOCKET_ERR=1,
    BIND_ERR,
    LISTEN_ERR,
};
// class tcpserver;
// class ThreadData
// {
// public:
//     ThreadData( tcpserver* self, int& sock)
//     :_self(self)
//     ,_sock(sock)
//     {}
//     tcpserver *_self;
//     int _sock;
// };
class tcpserver
{
public:
    tcpserver(uint16_t port)
    :_port(port)
    ,_listensock(-1)
    {}
    void inittcpserver()
    {
        //创建端口号
        _listensock=socket(AF_INET,SOCK_STREAM,0);
        if(_listensock==-1)
        {
            cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
        cout<<"create socket success"<<endl;
        //初始化信息
        struct sockaddr_in local;
        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=INADDR_ANY;       
        //开始bind这些信息
        int n=bind(_listensock,(struct sockaddr*)&local,sizeof(local));
        if(n==-1)
        {
            cout<<"bind error: "<<errno<<" : "<<strerror(errno)<<endl;
            exit(BIND_ERR);
        }
        cout<<"bind success"<<endl;
        //将客户端设置为监听状态
        if(listen(_listensock,5)==-1)
        {
            cout<<"listen socket error"<<endl;
            exit(LISTEN_ERR);
        }
        cout<<"listen socket success"<<endl;
    }
    //多进程写法一
    // void start()
    // {
    //     //服务端不停的获取新的链接
    //     for(;;)
    //     {
    //         struct sockaddr_in peer;
    //         socklen_t len=sizeof(peer);
    //         int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
    //         if(sock==-1)
    //         {
    //             cout<<"accept error"<
    //             continue;
    //         }
    //         cout<<"accept a new link success"<
    //         cout<<"sock : "<
    //         pid_t id =fork();
    //         if(id==0)
    //         {
    //             //子进程执行
    //             close(_listensock);
    //             if(fork>0)
    //             {
    //                 //子进程
    //                 exit(0)
    //             }
    //             //孙子进程
    //             serverio(sock);
    //             close(sock);
    //             exit(0);
    //         }
    //         //父进程执行
    //         close(sock);//下表的个数存在上限
    //     }
    // }
    //多进程写法二
    // void start()
    // {
    //     signal(SIGCHLD,SIG IGN);
    //     //服务端不停的获取新的链接
    //     for(;;)
    //     {
    //         struct sockaddr_in peer;
    //         socklen_t len=sizeof(peer);
    //         int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
    //         if(sock==-1)
    //         {
    //             cout<<"accept error"<
    //             continue;
    //         }
    //         cout<<"accept a new link success"<
    //         cout<<"sock : "<
    //         pid_t id =fork();
    //         if(id==0)
    //         {
    //             //子进程执行
    //             close(_listensock);
    //             serverio(sock);
    //             close(sock);
    //             exit(0);
    //         }
    //         //父进程执行
    //         close(sock);//下表的个数存在上限
    //     }
    // }
    //多线程版
    // void start()
    // {
    //     signal(SIGCHLD,SIG_IGN);
    //     //服务端不停的获取新的链接
    //     for(;;)
    //     {
    //         struct sockaddr_in peer;
    //         socklen_t len=sizeof(peer);
    //         int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
    //         if(sock==-1)
    //         {
    //             cout<<"accept error"<
    //             continue;
    //         }
    //         cout<<"accept a new link success"<
    //         cout<<"sock : "<
    //         pthread_t tid;
    //         ThreadData* td=new ThreadData(this,sock);
    //         pthread_create(&tid,nullptr,threadRoute,td);
    //     }
    // }

    // static void* threadRoute(void* args)
    // {
    //     pthread_detach(pthread_self());
    //     ThreadData* td=static_cast(args);
    //     td->_self->serverio(td->_sock);
    //     close(td->_sock);
    //     delete td;
    //     return nullptr;
    // }
    void start()
    {
        ThreadPool<Task>::getInstance()->run();
        //服务端不停的获取新的链接
        for(;;)
        {
            struct sockaddr_in peer;
            socklen_t len=sizeof(peer);
            int sock=accept(_listensock,(struct sockaddr*)&peer,&len);
            if(sock==-1)
            {
                cout<<"accept error"<<endl;
                continue;
            }
            cout<<"accept a new link success"<<endl;
            cout<<"sock : "<<sock<<endl;
            //线程池中添加任务
            ThreadPool<Task>::getInstance()->push(Task(sock, serviceIO));
        }
    }
    ~tcpserver()
    {

    }
private:
    int _listensock;
    uint16_t _port;
};

tcpclient.hpp的完整代码如下:

#include
#include           /* See NOTES */
#include 
#include 
#include 
#include
#include
#include
using namespace std;
enum{
    SOCKET_ERR=1, 
};
class tcpclient
{
public:
    tcpclient(string serverip,uint16_t serverport)
    :_serverport(serverport)
    ,_serverip(serverip)
    ,_sock(-1)
    {}
    void inittcpclient()
    {
        _sock=socket(AF_INET,SOCK_STREAM,0);
        if(_sock==-1)
        {
            cout<<"create socket error: "<<errno<<" : "<<strerror(errno)<<endl;
            exit(SOCKET_ERR);
        }
    }
    void start()
    {
        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<<" socker connect error "<<endl;
        }
        else
        {
            string message;
            while(true)
            {
                cout<<"please enter# ";
                getline(cin,message);
                write(_sock,message.c_str(),message.size());
                char buffer[1024];
                int n=read(_sock,buffer,sizeof(buffer)-1);
                if(n>0)
                {
                    buffer[n]=0;
                    cout<<"server回显: "<<buffer<<endl;
                }
                else if(n==0)
                {
                    break;
                }
            }     
        }
    }
    ~tcpclient()
    {}
private:
    int _sock;
    uint16_t _serverport;
    string _serverip;
};

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