Linux网络(C++)——网络套接字(TCP/UDP编程模型)多进程,多线程,线程池服务器开发(画图解析)

Posted 努力学习的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux网络(C++)——网络套接字(TCP/UDP编程模型)多进程,多线程,线程池服务器开发(画图解析)相关的知识,希望对你有一定的参考价值。

目录

一. 套接字基本概念

📚  IP地址

📚 TCP和UDP协议 

📚 端口号

 📚 端口号vs 进程pid

📚 网络字节序

本地字节序转换成网络字节序

网络字节序转换为本地字节序

二. 套接字的基本操作

📚 socket的创建

域(domain)

类型(type)

 协议(Protcol)

返回值

📚 struct socketaddr地址结构

struct sockaddr  结构

 struct sockaddr_in 结构

📚 socket绑定地址(bind函数)

📚 Socket监听连接(listen函数)

 📚 Socket请求连接(connect函数)

📚 Socket接受连接(accept函数)

📚 Socket接受数据 (recvfrom函数)

📚 Socket发送数据(sendto函数)

📚 Socket关闭

三. UDP编程模型

四. Tcp编程模型

 多进程服务器

 多线程服务器

线程池服务器

五. netstate命令


一. 套接字基本概念

所谓套接字(Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象,一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制

📚  IP地址

IP地址是在IP协议中,主要功能是用来标识网络上不同主机的地址。

IP地址是由32位组成,主要由三部分:地址类别,网络号和主机号:

📚 TCP和UDP协议 

TCP/IP协议传输层使用最广泛的两个协议分别是TCP协议和UDP协议,UDP套接口是数据报套接字的一种,而TCP套接口是字节流套接字的一种。

TCP/IP协议传输层的 主要任务是向位于不同的(有时候位于同一主机)上的应用程序提供端到端的通信服务,为了区分应用程序,TCP和UDP引入了端口号的概念,端口本质是一个16位的整数

📚 端口号

端口号(port)是传输层协议的内容

  • 是一个2字节的16位整数
  • 端口号是用来表示进程的,告诉操作系统,当前的数据要交给哪一个进程处理
  • ip地址+端口号能够标识网络上一台主机上的一个进程。
  • 一个端口号只能标识一个进程

任何的网络服务与网络客户端,如果要进行正常的数据通信,必须使用端口号,来唯一标识进程。

公网ip:唯一的标识全网唯一的一台主机

socket通信,本质是进程间通信,跨网络的进程进通信。

 📚 端口号vs 进程pid

在linux操作系统内,进程pid也是唯一标识进程,端口号跟进程pid之间有什么区别?举个例子:
在国家的社会中,每个人都有一个身份证号用来唯一标识一个人,在大学中,每个学生都有一个学生号来唯一标识一个学生,为什么学校不用身份证号来唯一区分一个学生呢?因为使用身份证号的人数多,有14亿左右,并不是每个人都在这个大学中读书,而一个大学中只有几千个人,那么为了能够方便管理和查找,学校再使用一个学号来唯一标识学校中学生的身份。

而进程pid就好比进程的身份证号,而端口号就好比学号,在一台机器上,假设有50个进程在运行,但是只有5个进程进行网络通信,这时候我们只要给这5个进程分配端口号即可。

📚 网络字节序

   由于不同计算机系统采用不同的字节序存储数据,同样一个4字节的32位整数在内存中存储的方式是不同的,这称为本地字节序。字节序列分为大端字节序和小端字节序,Intel处理器大多使用大端字节序,Motoro大多使用小端字节序。小端字节序是指低位字节存放内存的低地址处,大端字节序指的是高位字节存储在内存的低地址处。

     如果进程只在单机环境下运行,并且不和其他进程打交道,我们完全可以忽略字节序的存在,但是,如果进程需要跟其他计算机上的进程进行交换,我们必须考虑字节序的问题。

    1的字节序分别在大端机器和小端机器存储情况,如果大端机器将内存中的数据通过网络传给小端机器,那么小端机器从网络获得的数据放进内存中的数据是相反的。这就是网络字节序问题。

      TCP/IP协议进行网络数据传输时,规定一种数据表示格式,它与具体的cpu和操作系统无关,保证了数据在不同主机传输时能够被正确解释,这就是网络字节序,网络字节序统一采用大端字节序。 

因此,当两台机器进行通信时,必须先将本地字节序转换为网络字节序转化为网络字节序在进行发送,当收到数据之后,应当将网络字节序转化为本地字节序再进行后续使用。

本地字节序转换成网络字节序

       #include <arpa/inet.h>

       //将32位的整型数据从本地转换为网络字节序
       uint32_t htonl(uint32_t hostlong);
       //将16位的整形数据从本地转换为网络字节序
       uint16_t htons(uint16_t hostshort);

网络字节序转换为本地字节序

       #include <arpa/inet.h>

       //将32位的整形网络序列转换为本地序列
       uint32_t ntohl(uint32_t netlong);
       //将16位的整形网络序列转换为本地序列
       uint16_t ntohs(uint16_t netshort);

二. 套接字的基本操作

📚 socket的创建

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

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

域(domain)

域指定了Socket编成模型的地址族,其类型为int,常见的取值如下表所示:

 网络编程中最常用的socket域取值是AF_INET和AF_INET6协议,其中AF_INET6协议是下一代互联网的协议,客服目前IPv4存在可用地址有限的问题,但AF_INET6协议还没有被实际运用,在写代码时一般使用AF_INET.


类型(type)

类型主要指定了通信双方的数据传输格式,比较常见的数据格式类型有三种:

 协议(Protcol)

协议主要指通信双方的约定,如TCP协议UDP协议,但正常情况下,当双方的域(domain)和通信数据类型(type)确定以后,协议就会被唯一确定l,传0表示根据domain和type类型推出协议。所以该参数一般传0即可

返回值

函数socket()的返回值是一个文件描述符。套接字的建立本质是一个进程在内存中打开一个文件,并且这个文件中的数据与网卡互相连接,然后将打开的文件描述符返回给进程,然后进程可以对这个文件可以进行读写数据。

成功返回一个文件描述符(socket描述符),失败返回-1,同时errno被设置成相应的值。

 创建socket本质是一个进程在内存中打开一个文件缓冲区,然后这个文件缓冲区与网卡进行数据交互。

📚 struct socketaddr地址结构

在各种底层网络协议中,如IPv4和IPv6,以及UNIX DOMAIN socket,这些底层网络协议地址格式不同,所以为了兼容这些底层网络协议,Socket API定义了一个通用的struct socketaddr,这使得不同的地址结构可以被bind(),connect,revfrom,sendto()等函数调用。

struct sockaddr  结构

          struct sockaddr 
               sa_family_t sa_family; //地址族 16位
               char        sa_data[14]; //14个字节,包括目标地址和端口号
           

 struct sockaddr_in 结构

struct sockaddr_in     
  __kernel_sa_family_t  sin_family;   // 地址族 16位
  __be16    sin_port;    //端口号,16位,可以看成一个int类型
  struct in_addr  sin_addr;  //ip地址 32位
    
   //填充信息,一般不需要管
  unsigned char   __pad[__SOCK_SIZE__ - sizeof(short int) -    
      sizeof(unsigned short int) - sizeof(struct in_addr)];    
;  

//以整数的形式指定套接字的网络地址
struct in_addr 
  __be32  s_addr; //存放32位ip地址
;

IPv4地址使用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位ip地址。

IPv4IPv6的地址格式定义在netinet/in.h头文件 ,IPv4地址用 sockaddr_in 结构体表示 , 包括 16 位地址类型 , 16 位端口号和32 IP地址.

📚 socket绑定地址(bind函数)

若要使socket也可以被其他进程使用,服务器必须给socket绑定ip地址端口号

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

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

参数sockfd为待绑定的socket描述符,参数addr是本地sockaddr地址结构指针,addrlen为本地sockaddr地址结构的大小。执行成功返回0,失败返回-1.`

 对于绑定IP4套接字,我们不会直接去创建一个struct sockaddr变量,而是直接定义一个struct sockaddr_in变量,然后将ip地址和端口号填充进struct sockaddr_in变量中,最后在传参的时候将struct sockaddr_in*变量强制转换为struct sockaddr*类型,其他套接字也是一样。  

绑定IP4套接字过程如下: 

          struct sockaddr_in s;      //创建IP4网套接字地址数据结构
          memset(&s,'\\0',sizeof(s));   //将s置为0
          s.sin_family=AF_INET;      //设置TCP/IP地址族
          s.sin_port=htons(port);    //设置端口号
          s.sin_addr.s_addr=0;       //系统自动填入本机IP地址
          bind(fd,(struct sockaddr*)&s,sizeof(s) //绑定套接字
  • 创建struct soaddr_in对象时,首先需要用bzero()函数或memset()函数将其置为0. 
  • 在调用bind()函数时一般不要将端口号设置为小于1024的值,因为1~1024是保留端口号,用户可以选择大于1024中任何一个没有被占用的端口号。
  • 在使用bind()函数时,需要将sin_port转换为网络字节序,而sin_addr则不需要转换。
  • 在主机上定义的IP地址一般是“ 112.233.111.111”这种点分式字符串IP,但我们需要将这种字符串IP转换为整数IP填充进in_addr对象中的s_addr中。IP转换函数如下:
     //将字符型IP转换为32位整数型ip
    in_addr_t inet_addr(const char *cp);
     
    //将网络地址转换为字符串
    char *inet_ntoa(struct in_addr in)

  IP地址也可以不用直接填充,使用INADDR_ANY变量,可以直接填入本机IP地址,推荐使用这种方法。在我们的云服务器中,一般使用这种操作填充IP地址,因为云服务器的IP是由云厂商提供的,这个云服务器IP不能直接被绑定。


my_addr.sin_addr.s_addr=INADDR_ANY;   //填入本机IP地址

一般只有服务器才会去绑定IP地址和端口号 ,客户端不需要绑定IP地址和端口号。因为服务器需要不断的监听客户端的请求,服务器的IP地址和端口号它不可以一直改变,而客户端在连接服务器或者给服务器发送数据时,系统会给客户端随机分配一个端口,这个端口号和ip地址会发送给服务器,接下来服务器根据ip地址和端口号就可以给客户回应消息。

📚 Socket监听连接(listen函数)

在成功建立Socket并完成与本地地址绑定后。使用listen()函数来监听客户的连接请求。调用函数listen()函数会创建一个等待队列,在其中存放未处理的客户端请求,其函数原型如下:

       #include <sys/types.h>          
       #include <sys/socket.h>

       int listen(int sockfd, int backlog);

 参数为Socket描述符,参数为backlog为请求队列中允许的最大请求数,系统默认为5。函数listen()执行成功返回0,若执行失败返回-1.同时errno被设置成相应的值。

 📚 Socket请求连接(connect函数)

Tcp协议中,函数connect()用于客户端向服务器发起连接请求;UDP协议是面向无连接的,因此无需使用connect(),其函数原型如下:
        

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

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

sockfd为待绑定Socket的描述符:参数addr是服务器Socket地址结构指针,参数addrlen为addr的大小。

    函数执行成功返回0,失败返回-1,同时errno被设置成相应的值。 

📚 Socket接受连接(accept函数)

服务器进程调用函数listen()创建等待队列之后,调用accept()函数等待并接受客户端的连接请求,函数accept通常从连接等待队列中取出一个未处理的连接请求,其函数

       #include <sys/types.h>          
       #include <sys/socket.h>

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

其中,参数sockfd为Socket的描述符;参数addr为存放客户端Sock地址结构指针,参数addrlen用于存放客户端Socket地址结构指针。

函数accept()执行成功,返回客户端新的套接字描述符;若执行失败,返回-1,同时errno被设置成相应的值。 

📚 Socket接受数据 (recvfrom函数)

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

参数sockfd是Socket描述符,buf指定接受数据存放的缓冲区,len用于指定接受数据的大小,参数用于指定接受数据的标志,一般设置为0.

函数recvfrom中,参数src_addr用于存放数据发送方的网络地址结构(ip4是由sockaddr_in填充对象强转为sockaddr类型),参数addrlen是src_addr的大小。

recv和recvfrom执行成功,返回实际接受的字节数,执行失败,返回-1.

📚 Socket发送数据(sendto函数)

函数send()和sendto()向socket连接中发送数据,函数原型如下:

   ssize_t send(int sockfd, const void *buf, size_t len, int flags);

   ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

sockfd是Socket描述符,参数buf用于指定发送数据的缓冲区,参数len是指定发送数据的大小,参数flags用于指定发送数据标志,一般设置为0.

函数sendto(),dest_addr用于指定接受方的网络地址结构(ip4是由sockaddr_in填充对象强转为sockaddr类型),addrlen是dest_addr的大小。 

函数send()和sendto()发送数据成功,返回实际发送出数据的大小,失败返回-1.

📚 Socket关闭

因为socketfd是文件描述符,所以用户也可以用close()函数来终止服务器与客户端的套接字的连接。

       #include <unistd.h>
       int close(int fd);

close函数执行成功,返回0,失败返回-1.

三. UDP编程模型

UDP编程是不需要连接的,它是面向数据报。UDP socket是不可靠的,报文可能会丢失,重复或达到的顺序与它发送时的顺序不同;第二,当UDP上的socket填满的数据时,再发送数据时则报文会丢弃。

代码:

server.hpp文件

 #include<iostream>
  #include<string>
  #include<sys/types.h>
  #include<sys/socket.h>
  #include<netinet/in.h>
  #include<arpa/inet.h>
  #include<string.h>
  using namespace std;
  
  class server                                                                                         
  
    private:
      int port;
      string ip;
      int fd;
    public:
      server(string _ip,int _port)
W>      :ip(_ip),port(_port)
      
  
      bool initUdpServer()
      
          //创建套接字
          fd=socket(AF_INET,SOCK_DGRAM,0);
          if(fd<0)
          
            cout<<"socket fail\\n"<<endl;
            return false;
          
          cout<<"socket success"<<endl;
          //绑定套接字
          struct sockaddr_in s;
          memset(&s,'\\0',sizeof(s));
          s.sin_family=AF_INET;
          s.sin_port=htons(port);
          s.sin_addr.s_addr=0; 
          if(bind(fd,(struct sockaddr*)&s,sizeof(s))<0)
          
              cerr<<"bind error"<<endl;
              return false;
          
          cout<<"bind success"<<endl;
          return true;
      
  
      void Start()
      
        for(;;)
        
  #define NUM 32
          char buf[NUM];
          struct sockaddr_in peer;
          socklen_t len=0;                                                                             
          ssize_t sz=recvfrom(fd,(void*)buf,sizeof(buf)-1,0,(struct sockaddr*)&peer,&len);
          if(sz>0)
        
             buf[sz]='\\0';
             int _port=ntohs(peer.sin_port);
             string s=inet_ntoa(peer.sin_addr);
             cout<<s<<":"<< _port<<"#"<<buf<<endl;
          
          else
          cerr<<"recvfrom error"<<endl;
          
        
      
  ;                                                                                                   
 

server.cc文件

#include"server.hpp"    
#include<stdlib.h>    
int main(int argc,char* argv[])    
    
  if(argc!=2)    
      
    cout<<"Usage :"<<argv[0]<< "port"<<endl;    
      
  string ip="127.0.1";//127.0.1表示本地环回    
  int port=atoi(argv[1]);    
  server* s=new server(ip,port);    
  s->initUdpServer();    
  s->Start();    
  return 0;                                                                                            
    

 

client.hpp文件

#include<iostream>                                                                                     
#include<string>    
#include<sys/types.h>    
#include<sys/socket.h>    
#include<stdlib.h>    
#include<netinet/in.h>    
#include<string.h>    
#include<arpa/inet.h>    
using namespace std;    
    
class client    
    
  private:    
    int sockfd;    
    int sev_port;    
    string sev_ip;    
  public:    
    client(string _sev_ip,int _sev_port)    
      :sev_port(_sev_port)    
      ,sev_ip(_sev_ip)    
        
    
        
    
    bool InitClient()    
        
      sockfd=socket(AF_INET,SOCK_DGRAM,0);    
      if(sockfd<0)    
          
        cout<<"socket error"<<endl;    
        return false;    
          
     return true;
    

    void start()
    
      struct sockaddr_in dest;
      memset(&dest,'0',sizeof(dest));
      dest.sin_family=AF_INET;
      dest.sin_port=htons(sev_port);
      dest.sin_addr.s_addr=inet_addr(sev_ip.c_str());
      string buf;
      for(;;)
      
        cout<<"Please Enter#"<<endl;
        cin>>buf;
        sendto(sockfd,(void*)buf.c_str(),(size_t)buf.size(),0,(struct sockaddr*)&dest,sizeof(dest));
      
    
;        

client.cc文件

#include"client.hpp"    
    
//./client "127.0.0.1" 8081    
int main(int argc,char* argv[])    
    
  if(argc!=3)    
      
    cout<<"Usuage"<<"server_ip server_port"<<endl;    
    
      
  char* ip=argv[1];    
  int port=atoi(argv[2]);    
  client* c=new client(ip,port);    
  c->InitClient();    
  c->start();                                                                                          
  return 0;    
  

 

四. Tcp编程模型

Tcp协议再两个端点(即应用程序)之间提供了可靠的,面向连接的,双向字节流的通信管道。

 

Tcp协议提供了客户端和服务器之间的连接。Tcp客户端首先给某个定服务器建立一个连接,然后再通过连接与服务器进行数据交换,最后终止这个连接。

Tcp协议保证了数据传输的可靠性,当Tcp一端向另一端发送数据时,它要求另一端返回一个“确认”,如果没有收到“确认”,Tcp就会自动多次传送数据,在数次重传失败后才会放弃。

Tcp协议提供的连接是全双工的,这意味着在一个给定的连接上,一个Tcp端点可以在同一时刻发送数据又接受数据

Tcp协议

  • 传输层协议
  • 有链接
  • 可靠传输
  • 面向字节流

Tcp服务器模型

Tcp服务器和客户端接收和发送数据可以用read和write,首先,前面socket描述符本质是文件描述符,所以可以用文件的操作接口, 其次,服务器在accept()后连接客户端就可以拿到客户端的IP和端口号信息。所以服务器和客户端接收数据和发送数据正常使用read()函数和write()函数。

read()如果大于0,说明读到了信息。如果等于0,说明对端已关闭。

makefile文件

.PHONY:all    
all:tcp_client tcp_server    
    
tcp_client:tcp_client.cc    
  g++ $^ -o $@    
    
tcp_server:tcp_server.cc    
  g++ $^ -o $@    
    
clean:                                                                             
  rm -f tcp_client tcp_server   

服务器头文件

下面是单线程单进程的服务器,一次只能连接一个客户端。

 

 

#pragma once                                                                       
#include<iostream>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<unistd.h>
#include<string>
#include<stdlib.h>
class TcpServer

  private:
    int port;//端口号
    int listen_sock;//监听套接字描述符
  public:
    TcpServer(int _port=8081)
      :port(_port)
      ,listen_sock(-1)
    

    

    void InitServer()
    
      listen_sock=socket(AF_INET,SOCK_STREAM,0);//创建套接字
      if(listen_sock<0)
      
        std::cerr<<"socket fail"<<std::endl;
        return;
      
      std::cout<<"socket success"<<std::endl;
      struct sockaddr_in s;
      memset(&s,'\\0',sizeof(s));
      s.sin_family=AF_INET;
      s.sin_port=htons(port);                                                      
      s.sin_addr.s_addr=INADDR_ANY;
      //绑定套接字
      if(bind(listen_sock,(struct sockaddr*)&s,sizeof(s))<0)
      
        std::cerr<<"bind failing"<<std::endl;
        return;
        
      //让套接字去监听连接
      if(listen(listen_sock,5)<0)
      
        std::cerr<<"listen failing"<<std::endl;
      
    

    void Loop()
    
      struct sockaddr_in sockaddr;                                                 
      for(;;)
      
        socklen_t len=sizeof(sockaddr); 
        //接受监听套接字其中的一个连接,然后在创建新的套接字给这个连接
        //新的套接字进行数据通信
        int sockfd=accept(listen_sock,(struct sockaddr*)&sockaddr,&len);
        if(sockfd<0)
        
          std::cerr<<"accept fail"<<std::endl;
          continue;
        
 
        std::string ip=inet_ntoa(sockaddr.sin_addr);
        int port=ntohs(sockaddr.sin_port);
        std::cout<<"get new link..["<<ip<<"]:"<<port<<std::endl;
        //对新的套接字进行服务
        Server(sockfd,ip,port);
               
    

    void Server(int sock,std::string ip,int port)//服务功能
    
      char buffer[1024];
      while(true)
      
        ssize_t size=read(sock,buffer,sizeof(buffer)-1);
        if(size>0)
        
          buffer[size]='\\0';
          std::cout<<ip<<":"<<port<<"#"<<buffer<<std::endl;
          write(sock,buffer,size);
        
        else if(size==0)
                     
          std::cout<<ip<<":"<<port<<"close"<<std::endl;
          break;
        
        else
          std::cout<<"port error!!"<<std::endl;
        
      
         //新的套接字使用完后,需要关闭文件描述符,防止文件描述符泄漏
        close(sock);
                                                                                  
;

服务器.cc文件

#include"tcp_server.hpp"    
using namespace std;    
//启动服务器的方式:./tcp_server port    
int main(int argv,char* argc[])    
                                                                                  
  if(argv!=2)    
                      
    cout<<"Usage: tcp_server port"<<endl;    
    exit(-1);                                
                      
                       
  TcpServer* ts=new TcpServer(atoi(argc[1]));//创建服务器    
  ts->InitServer();//初始化服务器                                   
  ts->Loop();//启动服务器                                           
  return 0;                                                         
         

客户端头文件

#pragma once
#include<iostream>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/types.h>
#include<string>
#include<sys/socket.h>
#include<unistd.h>

class TcpClient

  private:
    int ser_port;//服务器端口号
    std::string ser_ip;//服务器的ip地址
    int sockfd;//本地套接字描述符
  public:
    TcpClient(std::string _ser_ip,int _ser_port):
      ser_port(_ser_port)                                                          
      ,ser_ip(_ser_ip)
      ,sockfd(-1)
  

  

    void InitTcpClient()
    
      sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字
      if(sockfd<0)
      
        std::cerr<<"socket failing"<<std::endl;
        return;
      
    

      void Start()
      
          struct sockaddr_in peer;
          memset(&peer,'\\0',sizeof(peer));
          peer.sin_family=AF_INET;
          peer.sin_port=htons(ser_port);
          peer.sin_addr.s_addr=inet_addr(ser_ip.c_str());
          //本地套接字去连接服务器
          if(connect(sockfd,(struct sockaddr*)&peer,sizeof(peer))==0)
          
            //连接成功,请求服务
            std::cout<<"connect success..."<<std::endl;
            Request(sockfd);
          
          else
            //fail
            std::cout<<"connect fail"<<std::endl;
          
      
      //请求服务任务接口
      void Request(int sockfd)
      
        std::string message;
        char buffer[1024];
        while(true)
        
          std::cout<<"Please Enter#";
          std::cin>>message;
          //往套接字中输入数据
          write(sockfd,message.c_str(),message.size());

          ssize_t size=read(sockfd,buffer,sizeof(buffer));
          if(size>0)
          
            buffer[size]='\\0';
            std::cout<<"server # "<<buffer<<std::endl;
          
        
      

      ~TcpClient()
                                                                                  
        if(sockfd>0)
          close(sockfd);
      
;

客户端.cc文件

#include"tcp_client.hpp"    
using namespace std;    
#include<stdlib.h>    
// 启动客户端的方式:./tcp_client ip port    
    
int main(int argv,char* argc[])    
    
  if(argv!=3)    
      
    cout<<"Usage : ./tcp_client ip port"<<endl;    
    exit(-1);    
      
      
  TcpClient* tc=new TcpClient(argc[1],atoi(argc[2]));    
  tc->InitTcpClient();    
  tc->Start();//开始运行服务端                                                     
  return 0;                            
  

 运行结果:

    

 因为这是单线程单进程的服务器,所以服务器每次只能接受一个客户端的连接。        

 多进程服务器

 

多进程服务器只需要在server.hpp文件改Loop()函数,其他文件不用改。     

主线程是爷爷进程,爷爷进程不断的去等待队列中接受新连接,接受到一个新连接,就去创建一个爸爸进程,再创建一个儿子进程去服务客户端的连接请求,再退出爸爸进程,如果有多个连接,爷爷进程就会创建多个儿子进程去服务多个连接。为什么要创建儿子进程?如果只创建爸爸进程,那么爸爸进程再退出前,爷爷进程需要去回收爸爸进程的资源,爷爷进程就会阻塞等待,如果再创建一个儿子进程,再退出爸爸进程,那么儿子进程就会被操作系统给“领养",当儿子系统退出时,操作系统会自动释放儿子进程的资源,此时爷爷进程就不会被阻塞,继续去接收新的连接。

 

         

 当创建儿子进程去服务新链接后,爷爷进程就需要关闭新连接的sock描述符,防止爷爷进程的文件描述符表泄漏。

运行结果

 多线程服务器

多线程服务器代码 

 class Pragma
  
    public:
      int sockfd;
      std::string ip;
      int port;
    public:
      Pragma(int _sockfd,std::string _ip,int _port)
        :sockfd(_sockfd)
        ,ip(_ip)
        ,port(_port)
     
    
  ;
 class TcpServer
  
    private:
      int port;
      int listen_sock;
    public:
      TcpServer(int _port)
        :port(_port)
        ,listen_sock(-1)
      
  
      
  
      void InitTcpServer()
      
        listen_sock=socket(AF_INET,SOCK_STREAM,0);
        if(listen_sock<0)
        
          std::cerr<<"socket failing"<<std::endl;
          exit(2);
        
       
        struct sockaddr_in lock;
        lock.sin_family=AF_INET;
        lock.sin_port=htons(port);
        lock.sin_addr.s_addr=INADDR_ANY;
       if(bind(listen_sock,(struct sockaddr*)&lock,sizeof(lock))<0)
        
          std::cerr<<"bind failing"<<std::endl;
          exit(2);
        
  
        if(listen(listen_sock,5)<0)
        
          std::cerr<<"listen failing"<<std::endl;
          exit(3);
        
        std::cout<<"listen success....."<<std::endl;
      
  
      void Loop()
      
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        for(;;)
        
          int sockfd=accept(listen_sock,(struct sockaddr*)&peer,&len);
          if(sockfd<0)
                                                                                                      
            continue;
          
          std::string ip=inet_ntoa(peer.sin_addr);
          int port=ntohs(peer.sin_port);
          std::cout<<"get new link ["<<ip<<"] :"<<port<<std::endl;
          pthread_t id;
          Pragma* p=new Pragma(sockfd,ip,port);
          pthread_create(&id,NULL,Routine,p);
        
      
  
      static void* Routine(void* arg)
      
          Pragma* p=(Pragma*)arg;
          pthread_detach(pthread_self());
          Server(p->sockfd,p->ip,p->port);
          close(p->sockfd);
          delete p;
W>                                                                                                    
      static void Server(int sockfd,std::string ip,int port)
                                                           
        char buffer[1024];
        while(true)       
                  
          buffer[0]='0';
        int sz=read(sockfd,buffer,sizeof(buffer)-1);
       if(sz>0)
          buffer[sz]='\\0';
          std::cout<<ip<<":"<<port<<"#"<<buffer<<std::endl;
        
        else if(sz==0)
          std::cerr<<"client close"<<std::endl;
          break;
        
        else
          break;                                                                                       
        
        
        close(sockfd);
      
      ~TcpServer()
          ~TcpServer()
      
        if(listen_sock>0)
          close(listen_sock);
      
  ;   


 

 

多线程服务器,当主线程接受连接多少个客户端时,主线程会创建相应的子线程去服务客户端,同时主线程和客户端不能随便关闭socket描述符,因为主线程和子线程使用的同一文件描述符表。 

线程池服务器

线程池Tcp服务器在服务器内创建一个线程池,初始化线程池后,服务器的主线程不断的往线程池中的任务队列中塞连接请求任务,线程池中的线程不断在任务队列中取出连接请求任务,通过客户端的套接字描述符,线程池中的线程就可以跟客户端进行通信。

线程池Tcp服务器处理客户端的连接请求的线程是固定的,不会随着连接请求的增多而导致线程不断的增多。

线程池的实现

Task.hpp文件

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>

class Handler
    public:
      Handler()
      
    void operator()(int sockfd,std::string ip,int port)
    
      char buffer[1024];                                                                                  
      while(true)
      
        buffer[0]='0';
      int sz=read(sockfd,buffer,sizeof(buffer)-1);
      if(sz>0)
        buffer[sz]='\\0';
        std::cout<<ip<<":"<<port<<"#"<<buffer<<std::endl;
      
      else if(sz==0)
        std::cerr<<"client close"<<std::endl;
        break;
      
      else
        break;
      
      
      close(sockfd);
    
;

threadpool.hpp头文件(线程池) 

#pragma once    
#include<iostream>    
#include<queue>    
#include<pthread.h>    
#include<unistd.h>    
using namespace std;    
     
#define NUM 2                                                                                             
template<class T>    
class ThreadPool    
    
  private:    
    queue<T> q;//任务队列    
    int thread_num;//线程池的线程数量    
    pthread_mutex_t lock;//互斥锁    
    pthread_cond_t cond;//条件变量    
  public:    
    ThreadPool(int num=NUM)//构造函数    
      :thread_num(num)    
        pthread_mutex_init(&lock,NULL);    
        pthread_cond_init(&cond,NULL);    
        
    
    bool Empty()    
        
      return q.size()==0?true:false;    
        

   static  void* Routine(void* arg)//线程执行流
    
      pthread_detach(pthread_self());//线程分离
      ThreadPool* self=(ThreadPool*)arg;
      while(1)
      
        self->LockQueue();
        while(self->Empty())//任务队列是否为空
        
          self->Wait();
        
        T data;
        self->Pop(data);//取出任务
        self->UnlockQueue();
        cout<<pthread_self()<<"# "; 
        //处理任务
        data.Run();//处理任务
        sleep(1);
      
    
   void ThreadPoolInit()
    
      pthread_t tid;
      for(int i=0;i<thread_num;i++)
      
        pthread_create(&tid,NULL,Routine,(void*)this);
      
    

    void Wait()
    
      pthread_cond_wait(&cond,&lock);
    

    void LockQueue()
    
      pthread_mutex_lock(&lock);
    

    void UnlockQueue()
    
      pthread_mutex_unlock(&lock);
         
    void Push(const T& in)//将任务推进任务队列中
                                               
      LockQueue();
      q.push(in); 
      UnlockQueue();
      SignalThread();
                    
     
    void SignalThread()
                      
      pthread_cond_signal(&cond);
                                
     
    void Pop( T& out)//将任务从队列中删除
                                        
      out=q.front();
      q.pop();      
             
     
    ~ThreadPool()
                
      pthread_mutex_destroy(&lock);
      pthread_cond_destroy(&cond); 
                                 
;   
                                  

 tcp_server.hpp

 #pragma once                                                                                            
  #include<iostream>    
  #include<string.h>    
  #include<arpa/inet.h>    
  #include<netinet/in.h>    
  #include<sys/types.h>    
  #include<sys/socket.h>    
  #include<unistd.h>    
  #include<pthread.h>    
  #include"threadpool.hpp"    
  #include"Task.hpp"    
  class TcpServer    
      
    private:    
      int port;    
      int listen_sock;    
      ThreadPool<Task>* pool;    
    public:    
      TcpServer(int _port)    
        :port(_port)    
        ,listen_sock(-1)    
        ,pool(nullptr)    
          
      
          

    void InitTcpServer()
    
      listen_sock=socket(AF_INET,SOCK_STREAM,0);//创建监听套接字
      if(listen_sock<0)
      
        std::cerr<<"socket failing"<<std::endl;
        exit(2);
      
      
      struct sockaddr_in lock;
      lock.sin_family=AF_INET;
      lock.sin_port=htons(port);
      lock.sin_addr.s_addr=INADDR_ANY;

      //绑定监听套接字
      if(bind(listen_sock,(struct sockaddr*)&lock,sizeof(lock))<0)
      
        std::cerr<<"bind failing"<<std::endl;
        exit(2);
      
      
      //监听连接
      if(listen(listen_sock,5)<0)
      
        std::cerr<<"listen failing"<<std::endl;
        exit(3);                                                                                       
      
     std::cout<<"listen success....."<<std::endl;
     pool= new ThreadPool<Task>();//创建线程池
    

    void Loop()
    
      pool->ThreadPoolInit();//初始化线程池
      struct sockaddr_in peer;
      socklen_t len=sizeof(peer);
      for(;;)
      
        //主线程接受客户端的连接
        int sockfd=accept(listen_sock,(struct sockaddr*)&peer,&len);
        if(sockfd<0)
        
          continue;
        

        std::string ip=inet_ntoa(peer.sin_addr);
        int port=ntohs(peer.sin_port);
        std::cout<<"get new link ["<<ip<<"] :"<<port<<std::endl;
        Task t(sockfd,ip,port);//创建任务
        pool->Push(t);//将任务推到线程池中
      
    

    ~TcpServer()
    
      if(listen_sock>0)                                                                                
        close(listen_sock);
    
;

五. netstate命令

netstate 命令用于显示各种网络相关信息,如网络连接,路由表,接口状态 (Interface Statistics),masquerade 连接,多播成员 (Multicast Memberships) 等等。

  • -a:显示所有选项,默认不显示LISTEN
  • -t(tcp):仅显示tcp相关选项
  • -u(udp):仅显示udp相关选项
  • -n:拒绝显示别名,能显示成数字就显示成数字
  • -p:显示建立相关链接的程序名
  • -r:显示路由信息,路由表
  • -e:显示扩展信息
  • -s:按各个协议进行统计
  • -c:每隔一段时间,执行该netstate命令
  • -l:拒绝显示别名,能显示成数字就显示成数字

 

 

以上是关于Linux网络(C++)——网络套接字(TCP/UDP编程模型)多进程,多线程,线程池服务器开发(画图解析)的主要内容,如果未能解决你的问题,请参考以下文章

用c++完成一个hello/hi的简单的网络聊天程序

C++ 网络编程 01

Socket Programming

通过野兽网络套接字发送二进制数据(在 C++ 中)

是否可以在另一个网络中寻址服务器/客户端套接字?(C++)

C++网络编程