tcp服务器的实现

Posted ych9527

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了tcp服务器的实现相关的知识,希望对你有一定的参考价值。

1.服务端需要绑定,客户端不需要

1.1服务器为什么需要绑定

1.一般服务器端是固定的,是总所周知的,ip和port不需要也不能轻易的更改。比如:http对应的端口号是80 https:443 ssh:22

2.服务器面对的客户很多,服务器一旦改了,客户端立马找不到,就无法访问服务器了。比如在日常生活中110对应的就是报警,报警的第一反应就是110

3.服务器是一对多的,一旦发生变化,很多客户端都要做出相对的变动

1.2客户端为什么不需要绑定

1.客户有很多客户端,如果绑定,就需要让不同的公司商量一下,什么软件用什么端口,端口是标识进程的,一个端口只能对应一个进程,如果多个进程使用同一个端口,就会导致绑定是失败。并且这种让不同的公司进行沟通进行约定,是很不现实的。

如果进行了bind很容易发生端口冲突,导致客户端无法启动

2.客户端需要唯一性,不需要明确是那个端口,但是需要IP和port。

客户端使用udp服务器进行数据的交互之时,系统会自动进行Ip和端口号的绑定(OS最清楚端口号情况)。端口号是16位,是有限的,OS是需要管理它的,因此OS清楚哪些端口没有被使用。而自己去绑定是不确定的,只能是"碰运气",看那个端口没有被绑定

2.什么是本地环回

IP:127.0.0.1 叫做本地环回 -> 通常用来进行网络通信代码的测试,一般本地跑通说明本地环境以及代码基本没有大问题
image-20210516082709294

3.INADDR_ANY

**[注]**云服务器有些地方是不需要端口的,这是考虑到安全问题,因此在绑定公网IP的时候可能会失败

网络地址为INADDR_ANY,这个宏表示本地的任意ip地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个ip地址,这样设置可以在所有的IP地址上进行监听,直到与某个客户建立了链接时才确下来到底用哪个ip地址

绑定的是INADDR_ANY该主机的所有IP层收到的数据都会交给主机,否则只有绑定的IP才会将数据交给主机

image-20210517085737386
image-20210517091305619

4.四字节和点分十进制的相互转换

image-20210517102642628

#include <iostream>
using namespace std;
#include <string>
#include <vector>


typedef union IP
{
  unsigned ip;//四字节ip
  struct{
    unsigned part1:8;
    unsigned part2:8;
    unsigned part3:8;
    unsigned part4:8;
  }ip_seg;
}IP;

void GetString(string & str,IP &ip)//从part之中读取1个个字节的整形转换成字符串
{
  str+=to_string(ip.ip_seg.part1);
  str+='.';
  str+=to_string(ip.ip_seg.part2);
  str+='.';
  str+=to_string(ip.ip_seg.part3);
  str+='.';
  str+=to_string(ip.ip_seg.part4);
}

void GetNum(string &str,IP &ip)//将字符串之中的内容填入ip,读取part
{
  string temp[4];
  int sub=0;
  for(int i=0;i<(int)str.size();i++)
  {
    if(str[i]!='.')
      temp[sub]+=str[i];
    else
    {
      sub++;
    }
  }
  ip.ip_seg.part1=stoi(temp[0]);
  ip.ip_seg.part2=stoi(temp[1]);
  ip.ip_seg.part3=stoi(temp[2]);
  ip.ip_seg.part4=stoi(temp[3]);
  
}

int main()
{
//将整形ip转换成点分十进制
  IP ip;//定义一个联合结构体,用来转换字符串
  ip.ip=987654321;
  string str;
  GetString(str,ip);
  cout<<ip.ip<<"->"<<str<<endl; 
 //将点分十进制转换成整形四字节ip
  cout<<"________________"<<endl;
  IP ip2;//定义一个联合结构体,用来转换成整形
  GetNum(str,ip2);
  cout<<str<<"->"<<ip2.ip<<endl;
  return 0;
  }

效果演示:
image-20210517102507568

5.udp字典服务器

#pragma once

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <map>

class udpServer
{
  private:
    string ip;
    int port;
    int sock;
    map<string,string> dict;

  public:
    udpServer(int _port)
      :port(_port)
    {
      dict.insert(pair<string,string>("apple","苹果"));
      dict.insert(pair<string,string>("banana","香蕉"));
      dict.insert(pair<string,string>("school","学校"));
    }


    void initServer()//初始化服务器
    {
      sock=socket(AF_INET,SOCK_DGRAM,0);
      cout<<sock<<endl;

      //当前套接字文件里面只有文件信息
      //这个文件对应的是网络文件、因此需要加入ip、port信息->绑定
      
      //传入的ip、port是在用户层的,下面需要绑定在内核里面
      //因此需要填充结构体,sockaddr_in(ipv4)
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口->转成网络字节序
      local.sin_addr.in_addr::s_addr = INADDR_ANY;//绑定本机上的任意IP

      if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
      {
        cerr<<"bind error"<<endl;//往显示器上打印,但是对应的文件描述符不一样
        exit(1);//绑定失败终止整个进程
      }
    }

    //服务器启动之后、永不退出
    //写一个回应服务器
    void start()
    {
      while(1)
      {
        struct sockaddr_in end_point;//保存是谁送人信息->ip 和port
        socklen_t len=sizeof(end_point);//发送人结构体大小

        char buff[88];//接收消息缓冲区
        buff[0]='\\0';//清空缓冲区

        ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&end_point,&len);//接收消息
        
        if(size>0)//收到消息了
        {
          string cli=inet_ntoa(end_point.sin_addr);//,将整形转换成str,获取客户端的点分十进制ip
          cli+=":";
          cli+=to_string(ntohs(end_point.sin_port));//获取客户端的port,获得的是整形,需要转换成字符串
          cli+=':';


           buff[size]='\\0';//在末尾添加\\0
           cout<<cli<<buff<<endl;//打印客户端发送过来的消息
           
          // string echo_string = buff;//回显字符串
          // echo_string+="-> server echo";
          //
           
          string echo_string;
           map<string,string>::iterator it=dict.find(buff);//通过key值查找
           if(it==dict.end())//没找到
             echo_string+="not find";
           else
            echo_string+=it->second;

          sendto(sock,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&end_point,len);

        }
      }
    }

    ~udpServer()
    {
      close(sock);//关闭文件描述符
    }

};

#include "udpServer.hpp"

void Usage()
{
  cout<<"Usage:"<<"please enter server ip and port"<<endl;
}

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

  udpServer *up=new udpServer(atoi(argv[1]));
  up->initServer();//初始化
  up->start();//运行起来

  return 0;
}

#pragma once

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <string>
using namespace std;
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <arpa/inet.h>

class udpClient
{
  private:
    string ip;
    int port;
    int sock;

  public:
    //连接服务器->所以填写server的ip和端口-> 下载软件的时候就是下载客户端
    udpClient(string _ip,int _port)
      :ip(_ip)
       ,port(_port)
  {}


    void initClient()//初始化客户端
    {
      sock=socket(AF_INET,SOCK_DGRAM,0);
      cout<<sock<<endl;

      //客户端不需要绑定
    }

    //服务器启动之后、永不退出
    //写一个回应服务器
    void start()
    {
      string send_string;
      //发送给谁,填充信息
      struct sockaddr_in peer;
      peer.sin_family=AF_INET;
      peer.sin_port=htons(port);
      peer.sin_addr.s_addr=inet_addr(ip.c_str());//将字符串转成4字节再转成网络字节序

      
      
      char buff[88];

      while(1)
      {
        cout<<"please enter your message#"<<endl;
        getline(cin,send_string);

        sendto(sock,send_string.c_str(),send_string.size(),0,(struct sockaddr*)&peer,sizeof(peer));
        
        buff[0]='\\0';
        ssize_t size=recvfrom(sock,buff,sizeof(buff)-1,0,nullptr,nullptr);
        if(size>0)
        {
          buff[size]='\\0';
          cout<<"Serve:"<<buff<<endl;
        }

      }

    }

    ~udpClient()
    {
      close(sock);//关闭文件描述符
    }

};

#include "udpClient.hpp"

void Usage()
{
  cout<<"Usage:"<<"please enter Server ip and port"<<endl;
}

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

  udpClient *uc =new udpClient(argv[1],atoi(argv[2]));
  uc->initClient();
  uc->start();

  return 0;
}

image-20210517102829539

6.TCP协议

6.1监听套接字 1listen

让套接字处理监听状态listen:允许任何时刻客户端来链接我(udp可以不链接,ip+port是表示发给谁)

什么是监听状态呢?
好比餐馆,老板就处于监听你的状态,只要你去了就有饭吃

image-20210516084205266

6.2建立链接accpet

image-20210516131347579

此时的返回值也是一个文件描述符,它和socket的返回值有什么区别呢?

sock:专注于底层获取链接(监听套接字)

accept返回值:专注于进程之间的通信

比如:一个餐馆,有在外面迎宾的和在里面招待客人的,他们就是两种套接字。迎宾的只负责将客人(链接)带进餐馆,带进餐馆之后他就不管了,之后与客人进行沟通,则是在里面的服务员了 -> 类比 sock(迎宾,负责获取链接)、accept返回值(服务员,负责进程之间的通信)

6.3 文件和流的概念

一般的文件都是基于流的 -> 比如通过open、fopen、pipe等方式打开的文件都有对应的概念 stream -> 在c/c++之中打开一个文件都叫做打开一个文件流。

tcp sock也是流的概念,因此读取sock文件时,也可以用read和write,但是常用的是recv

image-20210520100127018

image-20210520100221638

6.4代码实现tcp服务器和客户端

6.4.1服务器

#ifndef _TCPSERVER_HPP_
#define _TCPSERVER_HPP_

#include <iostream>
using namespace std;
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#endif
#define BACKLOCK 5

class tcpServer
{
  private:
    int port;
    int lsock;//监听套接字
  public:
    tcpServer(int _port)
      :port(_port)
       ,lsock(-1)
    {}

    void initServer()
    {
      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock<0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK)<0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
    }

    void service(int sock)
    {
      char buff[200];
      while(true)
      {
        size_t size=recv (sock,buff,sizeof(buff)-1,0);
        if(size>0)//读取到了
        {
          buff[size]='\\0';//末尾添加\\0
          cout<<"client:"<<buff<<endl;

          send(sock,buff,strlen(buff),0);//不需要+1,\\0是语言的标准,文件不认识
        }
      }
    }

    void stat()
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
      while(true)
      {
        //udp直接通信,tcp第一件事是获取链接
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        if(sock<0)
        {
          cerr<<"accpet error"<<endl;
          continue;
        }
        cout<<"get a new accept"<<endl;
        service(sock);//封装一个服务接口
        
      }
    }
  ~tcpServer()
  {
	  close(lsock);
  }
};


#include "tcpServer.hpp"

void Usage()
{
  cout<<"please enter your port"<<endl;
}
int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    Usage();
    exit(1);
  }
      tcpServer *tp=new tcpServer(stoi(argv[1]));//传输的是字符串 -> int
      tp->initServer();
      tp->stat();

    delete tp;
    return 0;
  }
 

image-20210517141351799

6.4.2客户端

知识点预知:

1.客户端链接服务器接口:
image-20210520102420393

2.服务器如何知道客户端退出了:
image-20210520102431694

3.将一个进程放置后台,拿到前台:
image-20210520103001199
image-20210520103936236
image-20210520104036504
4.效果展示:
image-20210520104919103

5.代码:

#ifndef _TCP_CLIENT_CPP_
#define _TCP_CLIENT_CPP_

#include <stdio.h>
using namespace std;
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#endif

class tcpClient
{
  private:
    string ip;
    int port;
    int sock;

  public:
    tcpClient(string _ip,int _port)
    :ip(_ip)
     ,port(_port)
     ,sock(-1)
    {}

    void initClient()
    {
      sock =socket(AF_INET,SOCK_STREAM,0);//绑定套接字
      if(sock==-1)
      {
        cerr<<"client sock error"<<endl;
        exit(1);
      }

    //链接服务器 -> udp不需要连接诶
      struct sockaddr_in peer;
      peer.sin_family=AF_INET;
      peer.sin_port=htons(port);//主机转成网络序列
      peer.sin_addr.s_addr=inet_addr(ip.c_str());//将字符串转成网络整形

      if(connect(sock,(struct sockaddr*)&peer,sizeof(peer))!=0)//链接服务器
      {
        cerr<<"connect fail"<<endl;
        exit(2);
      }

    }

  //链接成功...... 
  
    void start()
    {
      char msg[100];
      while(true)
      {
        cout<<"please your message"<<endl;
        size_t s=read(0,msg,sizeof(msg)-1);//从标准输入读取
       if(s>0)
       {
         msg[s-1]=0;//read会将换行读进去
         send(sock,msg,s,0);//给服务器发送信息
       
         size_t ss=recv(sock,msg,sizeof(msg)-1,0);
         if(ss>0)//表示读取到了信息
         {
           msg[ss]=0;
           cout<<"server:"<<msg<<endl;
         }
       }
      }
    }

    ~tcpClient()
    {
      close(sock);
    }
};


#include "tcpClient.hpp"
#include <stdio.h>
using namespace std;
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>

void Usage(char *arg)
{
  cout<<"usage:"<<arg<<":"<<"please enter yout ip and port"<<endl;
}

int main(int argc,char* argv[])
{
  if(argc!=3)
  {
    Usage(argv[0]);
    exit(-1);
  }
  tcpClient *tc=new tcpClient(argv[1],atoi(argv[2]));
  tc->initClient();
  tc->start();
  delete tc;

  return 0;
}

6.5tcp服务器优化

6.5.1多进程版本

#ifndef _TCPSERVER_HPP_
#define _TCPSERVER_HPP_

#include <iostream>
using namespace std;
#include <stdlib.h>
#include <signal.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>

#endif

#define BACKLOCK 5


class tcpServer
{
  private:
    int port;
    int lsock;//监听套接字
  public:
    tcpServer(int _port)
      :port(_port)
       ,lsock(-1)
    {}

    void initServer()//初始化,创建套接字
    {
      signal(SIGCHLD,SIG_IGN);//父进程忽略子进程发送过来的信号,即子进程自己清理资源

      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock<0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK)<0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
    }

    void service(int sock)//执行任务函数
    {
      char buff[200];
      while(true)
      {
        size_t size=recv (sock,buff,sizeof(buff)-1,0);
        if(size>0)//读取到了
        {
          buff[size]='\\0';//末尾添加\\0
          cout<<"client:"<<buff<<endl;

          send(sock,buff,strlen(buff),0);//不需要+1,\\0是语言的标准,文件不认识
        }
        else if(size==0)
        {
          cout<<"client quit..."<<endl;
          close(sock);//关闭对应的套接字
          break;
        }
        else//读取错误
        {
          cerr<<"read error"<<endl;
          close(sock);
          break;
        }
      }
    }

    void stat()
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
      while(true)
      {
        //udp直接通信,tcp第一件事是获取链接
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        if(sock<0)
        {
          cerr<<"accpet error"<<endl;
          continue;
        }

        string cl;//客户端信息
        cl+=inet_ntoa(end_point.sin_addr);//网->主,整->字符串
        cl+=':';
        cl+=to_string(ntohs(end_point.sin_port));


        cout<<"get a new accept:"<<cl<<"-"<<"sock:"<<sock<<endl;
        
        //创建多进程,可以有多个执行流
        pid_t id=fork();

        if(id==0)//子进程提供服务
        {
          //子进程不关心lsock
          close(lsock);//子进程拷贝了父进程的文件描述表
          service(sock);//封装一个服务接口
          exit(0);//执行完任务退出
        }
        //父进程的主要任务是获取链接
        //然后链接越多,对应的为服务创建的sock文件描述符也越多
        //父进程可用的资源就少了,因此不如关闭
        close(sock);//父进程不关心后面的服务
      }
    }
  ~tcpServer()
  {
  	close(lsock);
  }

};

另外一种写法___________________________________
pid_t id=fork();
if(id==0)
{
if(fork()>0)//子进程退出
	exit(0);
}
	//接下来的这一段是孙子进行在运行
          close(lsock);//子进程拷贝了父进程的文件描述表
          service(sock);//封装一个服务接口
          exit(0);//执行完任务退出

wait(id,null,0);//父进程立马可以等待子进程
-> 不推荐这样写,fork是有成本的,创建进程越多,代价越大

效果展示:
image-20210520140146228

6.5.2多线程版本

#ifndef _TCPSERVER_HPP_
#define _TCPSERVER_HPP_

#include <iostream>
using namespace std;
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>

#endif

#define BACKLOCK 5


class tcpServer
{
  private:
    int port;
    int lsock;//监听套接字
  public:
    tcpServer(int _port)
      :port(_port)
       ,lsock(-1)
    {}

    void initServer()
    {
      signal(SIGCHLD,SIG_IGN);//父进程忽略子进程发送过来的信号,即子进程自己清理资源

      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock<0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK)<0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
    }

    static void service(int sock)
    {
      char buff[200];
      while(true)
      {
        size_t size=recv (sock,buff,sizeof(buff)-1,0);
        if(size>0)//读取到了
        {
          buff[size]='\\0';//末尾添加\\0
          cout<<"client:"<<buff<<endl;

          send(sock,buff,strlen(buff),0);//不需要+1,\\0是语言的标准,文件不认识
        }
        else if(size==0)
        {
          cout<<"client quit..."<<endl;
          close(sock);//关闭对应的套接字
          break;
        }
        else//读取错误
        {
          cerr<<"read error"<<endl;
          close(sock);
          break;
        }
      }
    }


    static void *sevrun(void *arg)//只能有一个参数,因此必须是static
    {
      pthread_detach(pthread_self());//线程分离,不用再进行等待
      cout<<"create a new prhread"<<endl;
      int *p=(int*)arg;
      int sock=*p;
      service(sock);//执行任务
      delete p;
      return NULL;
    }

    void stat()
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
      while(true)
      {
        //udp直接通信,tcp第一件事是获取链接
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        if(sock<0)
        {
          cerr<<"accpet error"<<endl;
          continue;
        }

        string cl;//客户端信息
        cl+=inet_ntoa(end_point.sin_addr);//网->主,整->字符串
        cl+=':';
        cl+=to_string(ntohs(end_point.sin_port));

        cout<<"get a new accept:"<<cl<<"-"<<"sock:"<<sock<<endl;

        pthread_t tid;
        
        //防止新线程还未执行,老线程又创建了新的链接,sock就被改变了
        //new出来一段空间,只要传参就发生了拷贝,所以不会有影响
        int *p=new int(sock);

        pthread_create(&tid,nullptr,sevrun,(void*)p);//创建线程
      }
    }

  ~tcpServer()
  {
    close(lsock);
  }

};

//makefile
FLAG= -lpthread

.PHONY:all 
all:tcpClient tcpServer  -lpthread

tcpClient:tcpClient.cpp
	g++ -o $@ $^ $(FLAG)

tcpServer:tcpServer.cpp
	g++ -o $@ $^ $(FLAG)

.PHONY:clean
clean:
	rm -rf tcpClient tcpServer

效果展示:
image-20210520144636998

6.5.3线程池版本

#pragma once
#include <iostream>
#include <math.h>
#include <pthread.h>
#include <queue>
#include <unistd.h>
#include <map>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>

using namespace std;
#define NUM 5

class Task
{
  private:
    int sock;
    map<string,string>dict;
  public:
    Task()
    {}

    Task(int _sock)
       :sock(_sock)
    {
      dict.insert(make_pair("banana","香蕉"));
      dict.insert(make_pair("apple","苹果"));
    }

    void Run()//来一个链接,执行一个任务(打印语句)
    {
      cout<<"Task is run....."<<endl;
      
      char buff[100];
      size_t size=recv (sock,buff,sizeof(buff)-1,0);
      if(size>0)//读取到了
      {
        buff[size]='\\0';//末尾添加\\0
        string key=buff;

        send(sock,dict[key].c_str(),dict[key].size(),0);//不需要+1,\\0是语言的标准,文件不认识
      }
      else if(size==0)
      {
        cout<<"client quit..."<<endl;
        close(sock);//关闭对应的套接字
      }
      else//读取错误
      {
        cerr<<"read error"<<endl;
        close(sock);  
      }
    }

    ~Task()
    {
      close(sock);
    }
};

class Pool
{
  private:
    int _max;//线程个数
    queue<Task*> q;//存指针,减少数据拷贝消耗
    pthread_mutex_t lock;
    pthread_cond_t cond;

  public:
    
    Pool(int max=NUM)
      :_max(max)
    {}

    void TaskLock()
    {
      pthread_mutex_lock(&lock);
    }

    void TaskUnlock()
    {
      pthread_mutex_unlock(&lock);
     }

    void ThreadWait()
    {
      pthread_cond_wait(&cond,&lock);
    }

    void ThreadWakeUp()
    {
      pthread_cond_signal(&cond);//一次唤醒一个
    }

    void AllWakeUp()
    {
      pthread_cond_broadcast(&cond);//全部唤醒
    }


    bool IsEmpty()
    {
      return q.size()==0;
    }

    void Put(Task &data)//外部放入数据
    {
      TaskLock();
      q.push(&data);
      TaskUnlock();
      ThreadWakeUp();//唤醒一个线程
    }

    void Get(Task **data)//内部线程池完成任务
    {
      Task *t=q.front();
      q.pop();
      *data=t;
    }

public:

    //必须设置为静态的,因为成员函数会有this形参
    static void *RunTask(void *arg)//处理任务函数
    {
      Pool* p_this=(Pool*)arg;
      pthread_detach(pthread_self());//分离

      while(true)
      {
         p_this->TaskLock();
         //不退出才进行判断
         while(p_this->IsEmpty())//没有任务则挂起
         {
           p_this->ThreadWait();
         }

         //在这里一定不为空,将任务拿出来
         Task *t;
         p_this->Get(&t);

         p_this->TaskUnlock();
         t->Run();
         delete t;
      }
    }
    
    //尽量不要在构造函数内做有风险的事情
    void Init()
    {
      //锁和环境变量初始化
      pthread_mutex_init(&lock,nullptr);
      pthread_cond_init(&cond,nullptr);

      //线程池的创建
      pthread_t tid;
      for(int i=0;i<_max;i++)
      {
        pthread_create(&tid,nullptr,RunTask,this);//每个线程都去处理任务,不关心线程ID
      }
    }

      ~Pool()
      {
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&cond);
      }

};


#ifndef _TCPSERVER_HPP_
#define _TCPSERVER_HPP_

#include "pool.hpp"
#include <iostream>
using namespace std;
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <netinet/in.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <unistd.h>

#endif

#define BACKLOCK 5

class tcpServer
{
  private:
    int port;
    int lsock;//监听套接字
    Pool *pl;
  
  public:
    tcpServer(int _port)
      :port(_port)
       ,lsock(-1)
       ,pl(nullptr)
    {}

    void initServer()
    {
      signal(SIGCHLD,SIG_IGN);//父进程忽略子进程发送过来的信号,即子进程自己清理资源

      lsock=socket(AF_INET,SOCK_STREAM,0);
      if(lsock<0)
      {
        cerr<<"socket fail"<<endl;
        exit(2);
      }
      
      struct sockaddr_in local;
      local.sin_family=AF_INET;//填充协议
      local.sin_port=htons(port);//填充端口,转换成网网络字节序
      local.sin_addr.s_addr=INADDR_ANY;//填写ip
    
      if(bind(lsock,(struct sockaddr*)&local,sizeof(local))<0)//进行绑定
      {
        cerr<<"bind error"<<endl;
        exit(3);
      }
      if(listen(lsock,BACKLOCK)<0)
      {
        cerr<<"listen error"<<endl;
        exit(4);
      }
      pl=new Pool();
      pl->Init();//初始化线程池
    }

    static void service(int sock)
    {
      char buff[200];
      while(true)
      {
        size_t size=recv (sock,buff,sizeof(buff)-1,0);
        if(size>0)//读取到了
        {
          buff[size]='\\0';//末尾添加\\0
          cout<<"client:"<<buff<<endl;

          send(sock,buff,strlen(buff),0);//不需要+1,\\0是语言的标准,文件不认识
        }
        else if(size==0)
        {
          cout<<"client quit..."<<endl;
          close(sock);//关闭对应的套接字
          break;
        }
        else//读取错误
        {
          cerr<<"read error"<<endl;
          close(sock);
          break;
        }
      }
    }


    static void *sevrun(void *arg)//只能有一个参数,因此必须是static
    {
      pthread_detach(pthread_self());//线程分离
      cout<<"create a new prhread"<<endl;
      int *p=(int*)arg;
      int sock=*p;
      service(sock);//执行任务
      delete p;
      return NULL;
    }

    void stat()
    {
      sockaddr_in end_point;//获取客户结构体信息,和udp作用一样
      while(true)
      {
        //udp直接通信,tcp第一件事是获取链接
        socklen_t len=sizeof(end_point);
        int sock=accept(lsock,(struct sockaddr*)&end_point,&len);
        if(sock<0)
        {
          cerr<<"accpet error"<<endl;
          continue;
        }

        string cl;//客户端信息
        cl+=inet_ntoa(end_point.sin_addr);//网->主,整->字符串
        cl+=':';
        cl+=to_string(ntohs(end_point.sin_port));

        cout<<"get a new accept:"<<cl<<"-"<<"sock:"<<sock<<endl;
      
      //此时获得了新的连接 -> 将任务put至线程池
        Task *t= new Task(sock);
        pl->Put(*t);//将任务put进去

      }
    }

  ~tcpServer()
  {
    close(lsock);
  }

};

效果展示:
image-20210520162816130

6.5.4版本特点

1.单进程版本:只有一个执行流,一般不使用,多进程/多线程版本通常小型应用(局域网,少量机)
2.多进程版本:进程之间是独立的,健壮性强,但是比较吃资源,效率低下
3.多线程版本:健壮性不强,较吃资源,效率相对较高
大量的客户端:系统会存在大量的执行流,切换有可能称为效率低下的原因
4.线程池版本:可以减轻一大批客户端来进行链接时,服务器的压力。同时、由于线程池的线程数量有限,可以有效防止外部非法请求攻击、也在一定程度上保护了计算机

以上是关于tcp服务器的实现的主要内容,如果未能解决你的问题,请参考以下文章

Motan在服务provider端用于处理request的线程池

TCP通信的客户端代码实现,TCP通信的服务端代码实现

nginx如何做到TCP的负载均衡

从片段调用 Google Play 游戏服务

Java编程实例-tcp聊天室代码实现

linux epoll机制对TCP 客户端和服务端的监听C代码通用框架实现