【Linux网络编程五】Tcp套接字编程(四个版本服务器编写)

news2024/12/27 13:28:14

【Linux网络编程五】Tcp套接字编程(四个版本服务器编写)

  • [Tcp套接字编程]
  • 一.服务器端进程:
    • 1.创建套接字
    • 2.绑定网络信息
    • 3.设置监听状态
    • 4.获取新连接
    • 5.根据新连接进行通信
  • 二.客户端进程:
    • 1.创建套接字
    • 2.连接服务器套接字
    • 3.连接成功后进行通信
  • 三.version1:单进程版
  • 四.version2:多进程版
  • 五.version3:多线程版
  • 六.version4:线程池版
  • 七.解决细节问题:完善服务器和客户端
    • 1.write写入会存在偶发性失败,进程被信号杀死
    • 2.客户端每次请求都需要重新发起连接
    • 3.服务器端出现问题,客户端需要尝试自救
    • 4.复用ip地址和端口号

[Tcp套接字编程]

在这里插入图片描述
一般使用网络套接字编程时都需要引用以上头文件。

一.服务器端进程:

我们首先先进行服务器端的编写:服务器端肯定是需要知道有自己的ip地址和端口号的。
在这里插入图片描述

1.创建套接字

跟Udp套接字编程一样,通信前需要创建套接字,创建套接字的本质就是打开一个网络文件。

要注意的是Tcp是面向字节流的,创建套接字时,按照字节流形式创建。

        //服务器端启动之前创建套接字,绑定。
        //一开始的这个套接字是属于监听套接字
       _listensock=socket(AF_INET,SOCK_STREAM,0);
       if(_listensock<0)
       {
         lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));
         exit(SockError);
       }
       //创建套接字成功
       lg(Info,"sock create sucess listensock:%d",_listensock);

2.绑定网络信息

创建完套接字后,也就是打开一个网络文件,我们需要绑定该服务器的网络信息,比如ip地址和端口号等。这样该套接字才能找到服务器端。客户端往该套接字写入时,服务器端就能从该套接字里读取到。
服务器往该套接字里写入时,连接该套接字的客户端就能接收到。

 //创建成功后就要绑定服务器的网络信息
       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);
       //填充完毕,真正绑定
       if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0)
       {
        lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));
        exit(BindError);
       }
       lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功

将字符粗类型转int类型使用inet_aton()调用,简单的那个存在线程安全。在这里插入图片描述

3.设置监听状态

Tcp与Udp不同之处在于,Tcp是面向连接的,什么意思呢?就是在真正通信前,需要先建立连接才能正式通信。比如Udp在绑定完网络信息后,就可以直接进行通信了,但Tcp不行,Tcp在绑定完网络信息后,还需要建立连接。

Tcp相比较Udp比较被动,在通信之前,需要随时随地地等待别人连接上去才能进行通信。所以服务器要一直处于一个等待到来连接的状态。
而等待到新连接需要一种能力,这个能力就是监听!只有我们将套接字设置为监听状态,该套接字才能获取到新连接。不然获取不到别人的连接。
在这里插入图片描述

 //udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前
 //需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。
       //需要将套接字设置成监听状态
       listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连

4.获取新连接

以上都是服务器的一些网络信息初始化步骤,接下来才是启动服务器。服务器启动服务呢?
首先服务器在通信之前需要获取到客户端的连接。这样才可以和客户端进行通信。那么如何获取到客户端的连接呢?

1.前提是该服务器的套接字已经设置为监听状态,只有套接字设置为监听状态才可以获取到客户端发起的连接。
2.利用accept函数就可以获取到客户端发起的连接。
在这里插入图片描述
3.accept函数和recvfrom函数类似,它从套接字里获取到连接,不仅仅可以获取到客户端发起的连接,还可以获取到客户端的网络信息。而获取到客户端的网络信息是通过两个输出型参数带出来的。
4.注意accept函数的返回值是一个文件描述符。为什么呢?也就是accept返回的也是一个套接字文件。accept从一个套接字文件里获取到连接然后再返回套接字文件?什么意思呢?

【问题】:为什么会有两个套接字?
accept从我们创建的套接字里获取连接,结果又返回一个套接字文件,为什么呢?这个新的套接字是干啥的?
在这里插入图片描述

1.也就是曾经被创建的,被绑定网络信息的,设置为监听状态的套接字是专门用来获取客户端发起的连接的,然后将该连接返回给一个新的套接字,让服务器通过新的套接字与客户端通信,而原来的套接字就又处于监听状态,继续来监听客户端发起的新连接。
2.其实就是为了提高通信效率,一个套接字既去获取连接,又去通信,效率比较低,而让一个套接字一直处于监听状态,获取到新连接后,就转让给另一个套接字,服务器其实是通过accept返回的套接字与连接端的客户进行通信的。
3.未来,获取的连接会越来越多,也就是返回的套接字会越来越多,而监听的套接字只有一个。

     lg(Info,"tcpserver is running");
       while(true)
       {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        //将套接字设置成监听状态后,就可以获取新连接
        int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);
        //获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。
        if(sockfd<0)
        {
          lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));
          exit(AcceptError);
        }
        //获取新连接成功
        //将客户端端网络信息带出来
        uint16_t clientport=ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));
        }

将客户端网络信息带出来注意需要将网络字节序转换成用户字节序。
在这里插入图片描述
还有我们的传输的内容会自动转换的。在这里插入图片描述

5.根据新连接进行通信

一旦服务器端获取到新连接成功后,就可以与对端的客户通过返回的套接字进行通信。

而Tcp是面向字节流的,面向字节流就可以直接使用文件操作,因为文件操作也是面向字节流的,也就是文件操作里的read,write就可以往套接字里读取和写入。

这里是引用
1.我们就可以根据获取到的套接字与客户端进行通信。
2.我们可以将该工作分离成一个函数service。与客户端通信需要知道连接的套接字和客户端的网络信息。

void Service(int &sockfd,const std::string &clientip,uint16_t &clientport)
     {
         char inbuffer[1024];
         while(true)
         {
          ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client say# "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string="tcpserver加工处理数据:";
            echo_string+=inbuffer;

            //将加工处理的数据发送会去
            write(sockfd,echo_string.c_str(),echo_string.size());
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);
            break;
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
         }
     }

1.当服务器获取到连接后,read读取时,如果读取成功就会返回读取的个数大小。
2.当服务器获取到连接后,read读取之前或者连接又全部断掉,那么read就会读取到0。也就是在read的时候,没有用户连接了,那么服务器就没有必要再从该套接字里读取了。那么服务器读取的工作就退出,不再读取了。后续更需要将该套接字关闭起来。因为没有人使用了,就需要关闭起来。
3.read读取失败时就会返回小于0.
在这里插入图片描述

二.客户端进程:

服务器端基本工作已经准备好,接下来就是客户端,客户端在创建完套接字后需要绑定网络信息吗?

1.需要!但不需要显示绑定,操作系统会帮我们自动绑定。
在这里插入图片描述
2.客户端在发起连接时,系统就会帮我们自动绑定。

客户端想和服务器连接,那么用户肯定是知道该服务器端的套接字信息的,不然无法进行通信。所以在用户进行通信之前,我们首先将服务器端的网络信息给处理下。

比如将服务器的ip字符粗类型转换成int类型
有一下三个函数可以使用:
在这里插入图片描述

    std::string serverip=argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    struct sockaddr_in server;
    socklen_t len=sizeof(server);
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);
    inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);

1.创建套接字

不管是服务器端还是客户端要想网络通信都需要创建套接字,根据通信类型,创建Tcp套接字还是Udp套接字根据选择。我们这里创建Tcp套接字,按照面向字节流创建:

 //创建套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        std::cout<<"create sockfd err "<<std::endl;
    }
    //创建套接字成功,创建完套接字后该干什么?

2.连接服务器套接字

创建完套接字后该干什么呢?因为用户已经知道服务器端的套接字信息,所以我们就根据服务器端的套接字信息来向服务器端发起连接。

因为Tcp是面向连接的,所以必须先发起连接,对方获取到连接才能进行通信。

 //连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的
    int n=connect(sockfd,(struct sockaddr*)&server,len);
    if(n<0)
    {
        std::cout<<"connect sock err..."<<std::endl;
        exit(2);
    }
    //连接成功
    //连接成功后,就可以直接通信了,就可以直接给对方写消息了。

3.连接成功后进行通信

连接成功后,就可以与连接的服务器进行通信了,就可以通过套接字进行数据的传输了。

在这里插入图片描述
1.往套接字里写入就使用write,从套接字里读取就使用read。
2.注意使用完该套接字后,就要将套接字关闭。

  std::string message;
    while(true)
    {
        std::cout<<"Please enter#";
        getline(std::cin,message);
        //往套接字里写
        write(sockfd,message.c_str(),message.size());

        char outbuffer[1024];
        //接收服务器发送的加工处理消息

        int n=read(sockfd,outbuffer,sizeof(outbuffer));
        if(n>0)
        {
            outbuffer[n]=0;
            std::cout<<outbuffer<<std::endl;
        }
        
    }
    close(sockfd);

三.version1:单进程版

Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{
    SockError=2,
    BindError,
    AcceptError,
};
class Tcpserver
{
public:
     Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip)
     {}

     void Init()
     {
        //服务器端启动之前创建套接字,绑定。
        //一开始的这个套接字是属于监听套接字
        _listensock=socket(AF_INET,SOCK_STREAM,0);
       if(_listensock<0)
       {
         lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));
         exit(SockError);
       }
       //创建套接字成功
       lg(Info,"sock create sucess listensock:%d",_listensock);
       //创建成功后就要绑定服务器的网络信息
       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);
       //填充完毕,真正绑定
       if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0)
       {
        lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));
        exit(BindError);
       }
       lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功
       //udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前
       //需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。
       //需要将套接字设置成监听状态
       listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接
      }
      
     void START()
     {
      lg(Info,"tcpserver is running");
       while(true)
       {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        //将套接字设置成监听状态后,就可以获取新连接
        int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);
        //获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。
        if(sockfd<0)
        {
          lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));
          exit(AcceptError);
        }
        //获取新连接成功
        //将客户端端网络信息带出来
        uint16_t clientport=ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));
        
        //根据新连接进行通信

        lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);
        //-----------version1 单进程版本
        Service(sockfd,clientip,clientport); 
        close(sockfd);//不用了就关闭
       }
      
     }
  
     void Service(int &sockfd,const std::string &clientip,uint16_t &clientport)
     {
         char inbuffer[1024];
         while(true)
         {
          ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client say# "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string="tcpserver加工处理数据:";
            echo_string+=inbuffer;

            //将加工处理的数据发送会去
            write(sockfd,echo_string.c_str(),echo_string.size());
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);
            break;
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
         }
     }
     ~Tcpserver()
     {}



private:
    int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字
    std::string _ip;
    uint16_t _port;
};
Main.cc
#include "tcpserver.hpp"
#include <iostream>
#include <memory>
#include <cstdio>
void Usage(std::string proc)
{
    std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpserver port
int main(int args,char*argv[])
{
     if(args!=2)
     {
        Usage(argv[0]);
        exit(-1);
     }
    //定义一个服务器对象
    uint16_t port=std::stoi(argv[1]);
    std::unique_ptr<Tcpserver> tcpsvr(new Tcpserver(port));
    tcpsvr->Init();
    tcpsvr->Run();
}

【存在问题】上面属于单进程版本,只有一个进程在执行,只能服务一个客户端,其他客户端要想再使用服务器需要等当前客户端使用完服务器退出才能使用。这也太low了。
所以我们想改进为多进程版本。
单进程就是因为当前进程获取到连接后,该进程就去服务客户端端了,其他客户端就无法再使用该进程了。所以我们可以这样改进:让父进程获取新连接,然后创建子进程,让子进程去提供服务。子进程在服务的过程中,父进程就一直在等待新的连接。
在这里插入图片描述
在这里插入图片描述

四.version2:多进程版

【处理文件描述符】
多进程版我们首先需要处理一下文件描述符,也就是被打开的套接字。
当获取到新连接后,我们的进程就可以创建子进程,让子进程去跟客户端进行通信。而父进程继续去等待新连接。

因为创建子进程,子进程会继承父进程的文件描述符表。也就是父子进程具有相同的文件描述符表。
1.父进程的主要任务是获取新连接,然后将获取到的套接字给子进程使用。所以它是不需要获取到的套接字的,所以父进程需要将获取到的新套接字关闭掉。
2.而子进程的主要任务是根据新连接服务客户端,它是需要监听套接字的,所以需要将监听套接字关闭掉。父子进程相互关闭文件描述符是不会影响对方的。

在这里插入图片描述
【父子进程并发执行】
因为单进程中就是因为进程只能在处于要么在监听中,要么在服务中,不能处于既在监听中,又在服务中。所以我们需要让父进程去获取连接,子进程去服务客户端。而如何做到呢?
父进程是需要等待子进程的,如果子进程不退出,那么它就要一直在阻塞等待。那么它就无法再去获取新连接了。这就跟单进程一样了。
所以我们需要让父进程非阻塞等待子进程。或者通过这样的方法:
让子进程再创建一个子进程,当创建成功时,就直接退出,那么父进程就会等待成功。就会继续往下执行回去新的连接。
而让孙子进程去服务客户端。
在这里插入图片描述

Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{
    SockError=2,
    BindError,
    AcceptError,
};

class Tcpserver
{
public:
     Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip)
     {}

     void Init()
     {
        //服务器端启动之前创建套接字,绑定。
        //一开始的这个套接字是属于监听套接字
        _listensock=socket(AF_INET,SOCK_STREAM,0);
       if(_listensock<0)
       {
         lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));
         exit(SockError);
       }
       //创建套接字成功
       lg(Info,"sock create sucess listensock:%d",_listensock);
       //创建成功后就要绑定服务器的网络信息
       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);
       //填充完毕,真正绑定
       if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0)
       {
        lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));
        exit(BindError);
       }
       lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功
       //udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前
       //需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。
       //需要将套接字设置成监听状态
       listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连
      }
     void START()
     {
      lg(Info,"tcpserver is running");
       while(true)
       {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        //将套接字设置成监听状态后,就可以获取新连接
        int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);
        //获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。
        if(sockfd<0)
        {
          lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));
          exit(AcceptError);
        }
        //获取新连接成功
        //将客户端端网络信息带出来
        uint16_t clientport=ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));
        
        //根据新连接进行通信

        lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);
        //-----------version1 单进程版本
        //Service(sockfd,clientip,clientport); 
        //close(sockfd);//不用了就关闭
      
        //-----------version2 多进程版本
         pid_t id=fork();
         if(id==0)//子进程,用来处理服务,父进程用来获取新连接
         {
           close(_listensock);//子进程不需要该文件关闭
          if(fork()>0)exit(0);//再创建一个子进程,然后让该进程退出,让孙子进程执行下面的服务,子进程就退出,父进程就等待成功就会重新获取连接
          Service(sockfd,clientip,clientport);
          close(sockfd);
          exit(0);
         }
         //父进程只负责用来获取新连接,获取完毕后就交给子进程,自己是不用的,所以关闭
         close(sockfd);
         pid_t rid=waitpid(id,nullptr,0);//阻塞等待
         (void)rid;
       }
      
     }
  
     void Service(int &sockfd,const std::string &clientip,uint16_t &clientport)
     {
         char inbuffer[1024];
         while(true)
         {
          ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client say# "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string="tcpserver加工处理数据:";
            echo_string+=inbuffer;

            //将加工处理的数据发送会去
            write(sockfd,echo_string.c_str(),echo_string.size());
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);
            break;
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
         }
     }
     ~Tcpserver()
     {}

private:
    int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字
    std::string _ip;
    uint16_t _port;
};

在这里插入图片描述

五.version3:多线程版

【存在问题】当客户端发起一个连接,服务器获取一个连接时,服务器才忙着创建子进程呢,而创建一个进程的成本是很高的!
所以我们想当在服务器端,获取到一个新连接后,不创建进程而创建线程,让子线程去服务客户端。而主线程继续去获取新的连接。

1.所以只要一旦获取到一个新连接,我们只要当前进程创建线程,将获取的连接的套接字信息以函数参数的形式发送给线程。
2.要让线程去服务连接的客户端,线程不仅需要知道客户端的套接字,还需要知道是要服务谁,也就是客户端的网络信息。
所以我们还需要构建一个线程参数数据结构体,包含线程所需要的客户端的套接字,网络信息等。

在这里插入图片描述

【问题1】主线程需要等待子线程吗?
这个问题又回归到刚刚的多进程和单进程中的存在的问题了,如果主线程阻塞等待子线程,那么主线程就无法再重新获取新的连接了。
所以主线程不等待子线程,让子线程自己分离即可。也就是只要获取到一个新连接,那么主线程就创建子线程,创建完后,就不管子线程了,就继续回去获取新的连接。
子线程将自己分离后,就直接去通信即可。
在这里插入图片描述
在这里插入图片描述

【问题2】在多线程这里需要处理多余的文件描述符吗?
不需要!因为线程所拥有的资源都是共享的!一旦关闭,其他线程就会收到影响。
在这里插入图片描述
【问题3】在类内部创建多线程需要将线程函数设置为静态成员函数,而静态成员函数又无法使用通信函数Service,怎么办呢?
所以我们可以在构造线程数据结构体时,将该服务器类对象的指针也构建进去,当创建完线程,线程就可以根据该类指针找到通信函数Service。
在这里插入图片描述
在创建线程之前,我们首先将线程所需要的参数数据构建初始化好。然后就可以创建线程,让线程去执行通信函数。


#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{
    SockError=2,
    BindError,
    AcceptError,
};
class Tcpserver;

//构建线程所需要的参数数据:获取连接的套接字,客户端端的网络信息,服务器类的指针
class ThreadData
{
public:
  ThreadData(int &fd,const std::string& ip,uint16_t &port,Tcpserver* svr):_sockfd(fd),_ip(ip),_port(port),_svr(svr)
   {}
public:  
    int _sockfd;
    std::string _ip;
    uint16_t _port;
    Tcpserver* _svr;//通过该类指针可以找到类内部函数
};

class Tcpserver
{
public:
     Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip)
     {}

     void Init()
     {
        //服务器端启动之前创建套接字,绑定。
        //一开始的这个套接字是属于监听套接字
        _listensock=socket(AF_INET,SOCK_STREAM,0);
       if(_listensock<0)
       {
         lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));
         exit(SockError);
       }
       //创建套接字成功
       lg(Info,"sock create sucess listensock:%d",_listensock);
       //创建成功后就要绑定服务器的网络信息
       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);
       //填充完毕,真正绑定
       if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0)
       {
        lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));
        exit(BindError);
       }
       lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功
       //udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前
       //需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。
       //需要将套接字设置成监听状态
       listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接
      
      }
     static void* Routine(void *args)//静态成员函数无法使用成员函数,再封装一个服务器对象
     {
      //子线程要和主线程分离,主线程不需要等待子线程,直接回去重新获取新连接
      pthread_detach(pthread_self());
      ThreadData* td=static_cast<ThreadData*>(args);
      //子线程用来服务客户端
      td->_svr->Service(td->_sockfd,td->_ip,td->_port);
      delete td;
      return nullptr;
     }
     void Run()
     {
      //一启动服务器,就将线程池中的线程创建
      ThreadPool<TASK>::GetInstance()->Start();//单例对象
      //静态函数,通过类域就可以使用
      lg(Info,"tcpserver is running");
       while(true)
       {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        //将套接字设置成监听状态后,就可以获取新连接
        int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);
        //获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。
        if(sockfd<0)
        {
          lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));
          exit(AcceptError);
        }
        //获取新连接成功
        //将客户端端网络信息带出来
        uint16_t clientport=ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));
        
        //根据新连接进行通信

        lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);
       
       //-----------version3 多线程版本
          ThreadData *td=new ThreadData(sockfd,clientip,clientport,this);//首先将线程所需要的参数数据初始化好
          pthread_t tid;
          pthread_create(&tid,nullptr,Routine,td);//Routine要设置成静态成员函数
       }
      
     }
  
     void Service(int &sockfd,const std::string &clientip,uint16_t &clientport)
     {
         char inbuffer[1024];
         while(true)
         {
          ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client say# "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string="tcpserver加工处理数据:";
            echo_string+=inbuffer;

            //将加工处理的数据发送会去
            write(sockfd,echo_string.c_str(),echo_string.size());
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);
            break;
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
         }
     }
     ~Tcpserver()
     {}



private:
    int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字
    std::string _ip;
    uint16_t _port;
};

六.version4:线程池版

【服务器端:构建线程池
【存在问题】虽然该多线程版比多进程版要好些,成本要低一些。但还是存在问题:

1.客户端发起连接,服务器端获取到连接成功后,线程才被创建处理,而线程创建也是需要时间的,所以我们想减少这个时间----我们可以通过线程池来完善。线程池中的线程都是早就被创建好的。只要获取到新连接,就将对应的任务投入到线程池里让线程池里的线程去执行。
2.每来一个用户,就会发起一个连接,就要创建一个线程, 如果客户端不退出,那么线程就一直存在。那么就会存在很多线程的情况。这主要原因是我们的服务器的服务是长时间服务,服务器端是主动退出的,所以客户端不退的话,线程就无法退出。------所以我们想让服务器的服务变成短时间服务,服务完就立马关闭套接字,不再服务。线程就会退出。

在这里插入图片描述

所以服务器端只要获取到一个新连接,就将服务客户端的操作让线程池里的线程去执行。
在这里插入图片描述

不过线程池里的线程要执行必须是以任务的形式竞争,所以我们需要将服务客户端的动作封装成一个任务,然后将任务投递到线程池里,线程池里线程就会被唤醒去竞争任务。
在这里插入图片描述

线程池---被封装成单例模式
#pragma once

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

struct ThreadInfo
{
    pthread_t tid;
    std::string name;
};

static const int defalutnum = 10;

template <class T>
class ThreadPool
{
public:
    void Lock()
    {
        pthread_mutex_lock(&mutex_);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&mutex_);
    }
    void Wakeup()
    {
        pthread_cond_signal(&cond_);
    }
    void ThreadSleep()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    bool IsQueueEmpty()
    {
        return tasks_.empty();
    }
    std::string GetThreadName(pthread_t tid)
    {
        for (const auto &ti : threads_)
        {
            if (ti.tid == tid)
                return ti.name;
        }
        return "None";
    }

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

            while (tp->IsQueueEmpty())
            {
                tp->ThreadSleep();
            }
            T t = tp->Pop();
            tp->Unlock();

            t();
        }
    }
    void Start()
    {
        int num = threads_.size();
        for (int i = 0; i < num; i++)
        {
            threads_[i].name = "thread-" + std::to_string(i + 1);
            pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);
        }
    }
    T Pop()
    {
        T t = tasks_.front();
        tasks_.pop();
        return t;
    }
    void Push(const T &t)
    {
        Lock();
        tasks_.push(t);
        Wakeup();
        Unlock();
    }
    static ThreadPool<T> *GetInstance()
    {
        if (nullptr == tp_) // ???
        {
            pthread_mutex_lock(&lock_);
            if (nullptr == tp_)
            {
                std::cout << "log: singleton create done first!" << std::endl;
                tp_ = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&lock_);
        }

        return tp_;
    }

private:
    ThreadPool(int num = defalutnum) : threads_(num)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    ThreadPool(const ThreadPool<T> &) = delete;
    const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:
    std::vector<ThreadInfo> threads_;
    std::queue<T> tasks_;

    pthread_mutex_t mutex_;
    pthread_cond_t cond_;

    static ThreadPool<T> *tp_;
    static pthread_mutex_t lock_;
};

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

template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
构建任务,准备投递到线程池里的任务队列里
#pragma once
#include "Log.hpp"
#include <iostream>
#include "Init.hpp"
Init init;//刚加载程序时,文件里的内容就加载到map里了。
extern Log lg;
class TASK//构建任务,就是一旦获取到连接后,就将客户端的网络信息存到任务里,让服务器根据这个信息去服务客户端
{
    
public:    
    TASK(int &sockfd,const std::string& ip,uint16_t& port):_sockfd(sockfd),_clientip(ip),_clientport(port)
    {}
    void run()
    {
         char inbuffer[1024];
        // while(true)
         
          ssize_t n=read(_sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client key: "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string=init.translation(inbuffer);

            //将加工处理的数据发送会去
            n=write(_sockfd,echo_string.c_str(),echo_string.size());//写入过程也可能会失败,操作系统会发送信号将该进程杀死的,我们不想被杀死,就要忽略这个信息
            if(n<0)
            {
              //失败了,我们将信号忽略,但将日志打印出来
              lg(Fatal,"write error :%d ,errstring :%s",errno,strerror(errno));
            }
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",_clientip.c_str(),_clientport,_sockfd);
            
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
    }
    void operator()()
    {
        run();
    }
    ~TASK()
    {
    }
public:
   int _sockfd;
   std::string _clientip;
   uint16_t _clientport;
};
线程池版本
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;


const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{
    SockError=2,
    BindError,
    AcceptError,
};
class Tcpserver;

class ThreadData
{
public:
  ThreadData(int &fd,const std::string& ip,uint16_t &port,Tcpserver* svr):_sockfd(fd),_ip(ip),_port(port),_svr(svr)
   {}
public:  
    int _sockfd;
    std::string _ip;
    uint16_t _port;
    Tcpserver* _svr;
};
class Tcpserver
{
public:
     Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip)
     {}

     void Init()
     {
        //服务器端启动之前创建套接字,绑定。
        //一开始的这个套接字是属于监听套接字
        _listensock=socket(AF_INET,SOCK_STREAM,0);
       if(_listensock<0)
       {
         lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));
         exit(SockError);
       }
       //创建套接字成功
       lg(Info,"sock create sucess listensock:%d",_listensock);
       //创建成功后就要绑定服务器的网络信息
       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);
       //填充完毕,真正绑定
       if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0)
       {
        lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));
        exit(BindError);
       }
       lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功
       //udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前
       //需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。
       //需要将套接字设置成监听状态
       listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接
       
       
      }
     static void* Routine(void *args)//静态成员函数无法使用成员函数,再封装一个服务器对象
     {
      //子线程要和主线程分离,主线程不需要等待子线程,直接回去重新获取新连接
      pthread_detach(pthread_self());
      ThreadData* td=static_cast<ThreadData*>(args);
      //子线程用来服务客户端
      td->_svr->Service(td->_sockfd,td->_ip,td->_port);
      delete td;
      return nullptr;
     }
     void Run()
     {
      //一启动服务器,就将线程池中的线程创建
      ThreadPool<TASK>::GetInstance()->Start();//单例对象
      //静态函数,通过类域就可以使用
      lg(Info,"tcpserver is running");
       while(true)
       {
        struct sockaddr_in client;
        socklen_t len=sizeof(client);
        //将套接字设置成监听状态后,就可以获取新连接
        int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);
        //获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。
        if(sockfd<0)
        {
          lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));
          exit(AcceptError);
        }
        //获取新连接成功
        //将客户端端网络信息带出来
        uint16_t clientport=ntohs(client.sin_port);
        char clientip[32];
        inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));
        
        //根据新连接进行通信

        lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);
      
       //----------version4 线程池版本
       
       //当获取到新连接时,就构建任务
       TASK t(sockfd,clientip,clientport); 
       //将任务放进线程池里,线程就会到线程池里去执行任务。
       ThreadPool<TASK>::GetInstance()->Push(t);
       }
     }
  
     void Service(int &sockfd,const std::string &clientip,uint16_t &clientport)
     {
         char inbuffer[1024];
         while(true)
         {
          ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client say# "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string="tcpserver加工处理数据:";
            echo_string+=inbuffer;

            //将加工处理的数据发送会去
            write(sockfd,echo_string.c_str(),echo_string.size());
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);
            break;
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
         }
     }
     ~Tcpserver()
     {}



private:
    int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字
    std::string _ip;
    uint16_t _port;
};

【服务器端:构建短服务----->翻译字典】

在这里插入图片描述

服务器端的服务功能是交给线程池里的线程执行的,所以最好是短服务,如果是长服务的话,那么就会存在很多线程不退出,客户端不主动退出,线程就不退出的场景。我们想要让服务器只服务一次,服务完后,就将对应的服务的套接字关闭,不再服务。那么线程服务完一次就结束了。

所以我们可以设计一个翻译字典的功能,让客户端用户发送字母,服务器端给客户端翻译字母,再将字母的中文发送回去。服务器端服务完一次后,就将监听套接字获取到的新的套接字关闭,不再服务。
如果客户端想要再次请求服务,就需要重新发起连接,让服务器端的监听套接字重新获取到新的套接字。因为原来获取到的套接字已经被关闭。

翻译字典功能
#pragma once 
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
const std::string dictname="./dict.txt";
const std::string sep=":";//默认的分割符
extern Log lg;
class Init//相当于加载配置文件的动作,当创建出来,文件内容就已经被加载到map里了
{

public:
    void Spilt(std::string& line,std::string *part1,std::string* part2)
    {
        auto pos=line.find(sep);
        if(pos==std::string::npos)return ;
        *part1=line.substr(0,pos);
        *part2=line.substr(pos+1);
        return ;
    }
    Init()
    {
        std::ifstream in(dictname);//定义一个文件流,成功就打开该文件
        if(!in.is_open())
        {
           lg(Fatal,"ifstream open %s error",strerror(errno));
           exit(1);
        } 
        //读取文件里的内容
        std::string line;
        //按行读取。读取到line里
        while(std::getline(in,line))
        {
          std::string part1,part2;
          Spilt(line,&part1,&part2);
          //然后分割到map里
          dic.insert({part1,part2});
        }
        
        in.close();
    }
    std::string translation(const std::string& key)
    {
        auto iter=dic.find(key);//返回对应的该值的的迭代器
        if(iter==dic.end())return "Unkonwn";
        else return iter->second;
    }
private:
    std::unordered_map<std::string,std::string> dic; 
};
dict。txt对应的配置文件
apple:苹果
yellow:黄色
red:红色
bule:蓝色
man:男人
woman:女人
hello:你好

让服务器端的翻译字典只服务服务一次,就将对应的套接字关闭:

#pragma once
#include "Log.hpp"
#include <iostream>
#include "Init.hpp"
Init init;//刚加载程序时,文件里的内容就加载到map里了。
extern Log lg;
class TASK//构建任务,就是一旦获取到连接后,就将客户端的网络信息存到任务里,让服务器根据这个信息去服务客户端
{
    
public:    
    TASK(int &sockfd,const std::string& ip,uint16_t& port):_sockfd(sockfd),_clientip(ip),_clientport(port)
    {}
    void run()
    {
         char inbuffer[1024];
        // while(true)
         
          ssize_t n=read(_sockfd,inbuffer,sizeof(inbuffer));
          if(n>0)
          {
            inbuffer[n]=0;
            std::cout<<"client key: "<<inbuffer<<std::endl;
            //加工处理一下
            std::string echo_string=init.translation(inbuffer);

            //将加工处理的数据发送会去
            n=write(_sockfd,echo_string.c_str(),echo_string.size());//写入过程也可能会失败,操作系统会发送信号将该进程杀死的,我们不想被杀死,就要忽略这个信息
            if(n<0)
            {
              //失败了,我们将信号忽略,但将日志打印出来
              lg(Fatal,"write error :%d ,errstring :%s",errno,strerror(errno));
            }
          }
          else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了
          {
            lg(Info,"%s:%d quit, server close sockfd: %d",_clientip.c_str(),_clientport,_sockfd);
            
          }
          else
          {
            lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));
          }
          close(_sockfd);//任务只处理一次,服务器端处理完任务后,就会将该套接字关闭,线程池里的线程就不会一直在执行
         //客户端如果想再使用服务,需要重新连接,而该套接字已经被关闭,客户端也需要重新创建
    }
    void operator()()
    {
        run();
    }
    ~TASK()
    {
    }
public:
   int _sockfd;
   std::string _clientip;
   uint16_t _clientport;
};

在这里插入图片描述

七.解决细节问题:完善服务器和客户端

1.write写入会存在偶发性失败,进程被信号杀死

write写入功能也是会存在失败的,这种失败是偶发性的,比较小,但也是会发生。比如说,服务器端刚接收到客户端发送的消息,正在处理的过程中,客户端又将连接关闭,那么服务器向往这个套接字里写入数据时,就会写入失败。
而这种失败,就会类似于管道里的,读端关闭,写端再往里写时,系统就会觉得没有用,就会发送信号将该进程杀死。所以这里如果出现了写入失败,也会被操作系统杀死的。而我们不希望服务器端被杀死。因为只是出现了偶发性的写入失败。
在这里插入图片描述

所以我们当出现写入失败时,我们应该忽略操作系统发送的信号。防止被杀死。
在这里插入图片描述
所以服务器端不仅要对读取做处理,还要对写入做处理。

2.客户端每次请求都需要重新发起连接

在这里插入图片描述
因为服务器端,当监听套接字获取到新连接时,就会返回一个新的套接字给线程使用去服务客户端。而线程只服务一次,就会将该套接字关闭。关闭完后,对端的客户端就无zsxxxxxxxxxx法再使用服务了。虽然它对应的连接还在,但是服务器端的连接被关闭了。
所以客户端端再次发送请求时,服务器就接收不到。
【解决方案】
在这里插入图片描述
所以客户端在被服务完后,想要再次获取服务,就必须要重新发起连接,这样服务器端才可以获取到对应的连接。并且还需要重新创建套接字,然后再发起连接。因为原来的套接字已经被服务器关闭,不能再使用了。

所以每次客户端想要服务时,都必须重新创建套接字然后发起连接。

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


void Usage(std::string proc)
{
    std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{
    if(args!=3)
    {
     Usage(argv[0]);
     exit(1);
    }
    std::string serverip=argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    struct sockaddr_in server;
    socklen_t len=sizeof(server);
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);
    inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);

    while(true)
    {
    //创建套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        std::cout<<"create sockfd err "<<std::endl;
    }
    //创建套接字成功,创建完套接字后该干什么?
    //连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的
    connect(sockfd,(struct sockaddr*)&server,len);
    std::string message;
   
    std::cout<<"Please enter#";
    getline(std::cin,message);
    //往套接字里写
    int n=write(sockfd,message.c_str(),message.size());
    if(n<0)//服务器端会将该套接字关闭,然后就写不进去了。需要重新创建套接字连接
    {
        std::cerr<<"write error..."<<std::endl;
        continue;
    }
    char outbuffer[1024];
    //接收服务器发送的加工处理消息

    n=read(sockfd,outbuffer,sizeof(outbuffer));
    if(n>0)
    {
       outbuffer[n]=0;
       std::cout<<outbuffer<<std::endl;
    }
    close(sockfd);
    }
    return 0;
 
}

3.服务器端出现问题,客户端需要尝试自救

在这里插入图片描述
所以当服务器出现问题时,比如挂掉了。客户端首先要尝试重新连接,看能不能恢复正常。因为服务器端出现问题,那么客户端的写入和读取就可能会出现问题,甚至连接部分会出现问题,这些问题一旦出现,就要尝试重新连接。多次连接,如果服务器又恢复连接了,那么就可以重新连接成功。
否则还是失败,失败了就重连接失败,用户就退出吧不玩了。

1.只是连接部分或者写入读取部分出现错误,客户端创建的套接字并没有问题,所以不需要重新创建。只需要循环连接部分即可。
2.重新连接模块是客户端应该具备的功能。在这里插入图片描述

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


void Usage(std::string proc)
{
    std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{
    if(args!=3)
    {
     Usage(argv[0]);
     exit(1);
    }
    std::string serverip=argv[1];
    uint16_t serverport = std::stoi(argv[2]);
    struct sockaddr_in server;
    socklen_t len=sizeof(server);
    server.sin_family=AF_INET;
    server.sin_port=htons(serverport);
    inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);

    while(true)
    {
    //创建套接字
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        std::cout<<"create sockfd err "<<std::endl;
    }
    //创建套接字成功,创建完套接字后该干什么?
    //连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的
    int cnt=10;
    bool isreconnect=false;
    do
    { 
       int n=connect(sockfd,(struct sockaddr*)&server,len);
      if(n<0)//服务器关闭了,肯定会连接失败
      {
        isreconnect=true;
        cnt--;
        std::cout<<"connect sock err...,cnt: "<<cnt<<std::endl;
        sleep(12);
      }
      else//重连成功了
      {
        break;
      }
    }while(cnt&&isreconnect);
    //连接成功
    //连接成功后,就可以直接通信了,就可以直接给对方写消息了。
    if(cnt==0)
    {
        std::cerr<<"user offline.."<<std::endl;
        break;//用户直接不玩了
    }
    std::string message;
   
    std::cout<<"Please enter#";
    getline(std::cin,message);
    //往套接字里写
    int n=write(sockfd,message.c_str(),message.size());
    if(n<0)//服务器端会将该套接字关闭,然后就写不进去了。需要重新创建套接字连接
    {
        std::cerr<<"write error..."<<std::endl;
        continue;
    }
    char outbuffer[1024];
    //接收服务器发送的加工处理消息

    n=read(sockfd,outbuffer,sizeof(outbuffer));
    if(n>0)
    {
       outbuffer[n]=0;
       std::cout<<outbuffer<<std::endl;
    }
    close(sockfd);
    }
    return 0;
 
}

4.复用ip地址和端口号

在这里插入图片描述
将套接字设置成允许复用ip地址和端口号。

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

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

相关文章

2.15练习

选择题 1. B2. B3. C4. D5. A6. B7. C8. B9. D10. B11. A12. B 填空题 1. a b c 2.string s: I like it. String t:A like it. 3. 30 10 30 n Learning

云计算基础-虚拟机迁移原理

什么是虚拟机迁移 虚拟机迁移是指将正在运行的虚拟机实例从一个物理服务器&#xff08;或主机&#xff09;迁移到另一个物理服务器&#xff08;或主机&#xff09;的过程&#xff0c;而不会中断虚拟机的运行。 虚拟机拟机迁移分类虚 热迁移&#xff1a;开机状态下迁移 冷迁…

“分布式透明化”在杭州银行核心系统上线之思考

导读 随着金融行业数字化转型的需求&#xff0c;银行核心系统的升级改造成为重要议题。杭州银行成功上线以 TiDB 为底层数据库的新一代核心业务系统&#xff0c;该实践采用应用与基础设施解耦、分布式透明化的设计开发理念&#xff0c;推动银行核心系统的整体升级。 本文聚焦…

H12-821_44

44.如图所示的网络,R1设备配置路由渗透,那么R1设备把Level-2的LSP发送给R3,使R3可以获知全网路由。 A.正确 B.错误 答案&#xff1a;B 注释&#xff1a; 感觉题目描述有两个问题&#xff1a; 1. R3是Level-1-2路由器&#xff0c;本来就可以学习到Level-2的路由。题目中的R3应该…

第七篇【传奇开心果系列】Python微项目技术点案例示例:数据可视化界面图形化经典案例

传奇开心果微博系列 系列微博目录Python微项目技术点案例示例系列 微博目录一、微项目开发背景和项目目标&#xff1a;二、雏形示例代码三、扩展思路介绍四、数据输入示例代码五、数据分析示例代码六、排名统计示例代码七、数据导入导出示例代码八、主题定制示例代码九、数据过…

微信网页版能够使用(会顶掉微信app的登陆)

一、文件结构 新建目录chrome新建icons&#xff0c;其中图片你自己找吧新建文件manifest.json新建文件wx-rules.json 二、文件内容 对应的png你们自己改下 1、manifest.json {"manifest_version": 3,"name": "wechat-need-web","author…

[BUUCTF]-PWN:ciscn_2019_es_1解析(tcachebin duf)

查看保护 再查看ida 大致为alloc创建堆块&#xff0c;free释放堆块&#xff0c;show输出堆块内容 但是要注意一点free没有清空堆块指针 完整exp&#xff1a; from pwn import* from LibcSearcher import* pprocess(./es1) premote(node5.buuoj.cn,26841)def alloc(size,cont…

基于剪贴板的文件传输方案

文章目录 背景原理步骤获取待上传文件的十六进制数据格式转换输出 背景 某次误删了环境上的C库之后想到的 什么都不可用了&#xff0c;但当前的ssh连接还在&#xff0c;echo命令和重定向符还可以使用 这就催生了我的想法&#xff1a;直接用echo -en “\xab\xcd” > file这样…

DataX源码分析-插件机制

系列文章目录 一、DataX详解和架构介绍 二、DataX源码分析 JobContainer 三、DataX源码分析 TaskGroupContainer 四、DataX源码分析 TaskExecutor 五、DataX源码分析 reader 六、DataX源码分析 writer 七、DataX源码分析 Channel 八、DataX源码分析-插件机制 文章目录 系列文章…

HTML | DOM | 网页前端 | 常见HTML标签总结

文章目录 1.前端开发简单分类2.前端开发环境配置3.HTML的简单介绍4.常用的HTML标签介绍 1.前端开发简单分类 前端开发&#xff0c;这里是一个广义的概念&#xff0c;不单指网页开发&#xff0c;它的常见分类 网页开发&#xff1a;前端开发的主要领域&#xff0c;使用HTML、CS…

活用 Composition API 核心函数,打造卓越应用(下)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

C#根据权重抽取随机数

&#xff08;游戏中一个很常见的简单功能&#xff0c;比如抽卡抽奖抽道具&#xff0c;或者一个怪物有多种攻击动作&#xff0c;按不同的权重随机出个攻击动作等等……&#xff09; 假如有三种物品 A、B、C&#xff0c;对应的权重分别是A&#xff08;50&#xff09;&#xff0c…

yolov8源码解读Detect层

yolov8源码解读Detect层 Detect层解读网络各层解读及detect层后的处理 关于网络的backbone,head&#xff0c;以及detect层后处理&#xff0c;可以参考文章结尾博主的文章。 Detect层解读 先贴一下全部代码,下面一一解读。 class Detect(nn.Module):"""YOLOv8 …

【开源】JAVA+Vue.js实现大学计算机课程管理平台

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 实验课程档案模块2.2 实验资源模块2.3 学生实验模块 三、系统设计3.1 用例设计3.2 数据库设计3.2.1 实验课程档案表3.2.2 实验资源表3.2.3 学生实验表 四、系统展示五、核心代码5.1 一键生成实验5.2 提交实验5.3 批阅实…

如何选择一个适合自己的赛道

&#xff08;点击即可收听&#xff09; 最开始一定要先做好定位&#xff0c;也就是你做短视频的目的是什么&#xff1f;当然对大多数人来说&#xff0c;终极目的肯定是赚钱&#xff0c;但赚钱的速度是由定位决定的 如果你资金比较充裕&#xff0c;不急于赚钱&#xff0c;就可以…

配置oracle连接管理器(cman)

Oracle Connection Manager是一个软件组件&#xff0c;可以在oracle客户端上指定安装这个组件&#xff0c;Oracle连接管理器代理发送给数据库服务器的请求&#xff0c;在连接管理器中&#xff0c;我们可以通过配置各种规则来控制会话访问。 简而言之&#xff0c;不同于专用连接…

基于BP算法的SAR成像matlab仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 4.1 BP算法的基本原理 4.2 BP算法的优点与局限性 5.完整工程文件 1.课题概述 基于BP算法的SAR成像。合成孔径雷达&#xff08;SAR&#xff09;是一种高分辨率的雷达系统&#xff0c;能够在各种天气和光…

DS:八大排序之直接插入排序、希尔排序和选择排序

创作不易&#xff0c;感谢三连支持&#xff01;&#xff01; 一、排序的概念及运用 1.1 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起 来的操作。稳定性&…

Java图形化界面编程——五子棋游戏 笔记

2.8.5 五子棋 接下来&#xff0c;我们使用之前学习的绘图技术&#xff0c;做一个五子棋的游戏。 注意&#xff0c;这个代码只实现了五子棋的落子、删除棋子和动画等逻辑实现&#xff0c;并没有把五子棋的游戏逻辑编写完整&#xff0c;比较简单易上手。 图片素材 package…

.NET Core MongoDB数据仓储和工作单元模式实操

前言 上一章节我们主要讲解了MongoDB数据仓储和工作单元模式的封装&#xff0c;这一章节主要讲的是MongoDB用户管理相关操作实操。如&#xff1a;获取所有用户信息、获取用户分页数据、通过用户ID获取对应用户信息、添加用户信息、事务添加用户信息、用户信息修改、用户信息删除…