Linux网络套接字(二)

news2024/12/26 23:46:01

学习任务:

继网络套接字(一),继续学习套接字socket编程接口(已经学习了socket和bind),实现TCP客户端/服务器(单连接版本, 多进程版本, 多线程版本,进程或线程池版本),并且理解tcp服务器建立连接, 发送数据, 断开连接的流程。

1.socket编程接口

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

接口解析

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

函数功能:由于服务器需要周而复始地等待客户端和自己建立连接,因此该函数由服务器使用,功能是等待用户连接,进行系统侦听请求。

第一个参数sockefd:由socket接口创建的套接字fd,不过需要注意

第二个参数backlog:套接字排队的最大连接个数(建议5~10),即申请连接的客户端的个数。

返回值:成功返回0,错误返回-1。

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);

函数功能:接收用户连接请求,并返回一个新的套接字描述符用于与客户端通信。

  

第一个参数sockfd:由socket接口创建的套接字fd。

第二个参数addr:用于保存客户端的进程协议地址的结构体。

第三个参数addrlen:addr的大小。

返回值:返回一个新的套接字描述符。

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

函数功能:建立连接。

第一个参数sockfd:由accept接口创建的套接字描述符。

第二个参数addr:套接字地址结构的指针。

第三个参数addrlen:addr的大小。

  

返回值:成功返回0。

单进程版本TCP(客户端/服务器)

单进程版本没有人会去使用,因为这种版本只能是一对一的连接,很明显不能符合业务要求的,就好比我们打开一个学习软件去学习,同学A先打开了,那么同学B、C和更多的其他同学都不能打开了。这里我们借助单进程版本来学习。

首先是写出服务器的代码,代码的思路是这样的:

①首先为服务器创建套接字,因为这个是TCP协议,TCP是面向连接的,因此服务器是需要进入监听状态才能让客户端连接,所以使用socket接口创建出来的套接字是属于监听套接字,负责绑定IP和端口号,负责监听的。

②创建完监听套接字后,开始绑定IP和端口号。先创建出服务器的sockaddr_in结构(因为使用AF_INET协议),然后填充结构体,填充的是使用何种协议域,IP和端口号。在填充IP的时候,选择任意绑定IP。

③设置监听状态,监听状态的服务器,通俗地来解释就是服务器进入监听状态,就是告诉客户端我可以被连接了,来吧!

④使用accept接口,创建出提供服务的套接字。这里需要建立另外一个sockaddr_in结构体,这个结构体是保存客户端的ip和端口号。

⑤最后就是提供服务,由于TCP是面向字节流的,跟文件操作一样,因此我们可以使用文件操作进行读写。

注意:

在bind方法中的sockaddr结构体里面填充的是服务端的ip地址和端口号,bind就把服务器的ip地址和端口号和前面的监听套接字结合起来了。而accept方法中的socketaddr结构体保存的是客户端的ip地址和端口号信息。

代码如下:

#include<iostream>
#include<sys/socket.h> 
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<" port"<<std::endl;
}

void ServerceIO(int new_sock)
{
  while(true)
    {
      //tcp是面向字节流的,如同文件一样,可以进行正常的读写
      char buffer[1024];
      memset(buffer,0,sizeof(buffer));
      //将读到的数据放入buffer中
      ssize_t s = read(new_sock,buffer,sizeof(buffer)-1);
      if(s > 0)//读取成功
      {
          buffer[s] = 0;//将获取的内容当成字符串
          std::cout<<"client#  "<<buffer<<std::endl;
          std::string echo_string =">>>server<<<, ";
          echo_string += buffer;

          write(new_sock,echo_string.c_str(),echo_string.size());
      }
      else if(s==0)
      {
        std::cout<<"client quit..."<<std::endl;
        break;
      }   
      else
      {
        std::cerr<<"read err"<<std::endl;
        break;
      }
    }
}

// ./tcp_server port
int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    Usage(argv[0]);
    return 1;
  }

  //tcp_server
 //1.创建套接字,此套接字为监听套接字,用于绑定和监听
  int listen_sock = socket(AF_INET,SOCK_STREAM,0);
  if(listen_sock < 0)
  {
    std::cerr<<"socket error"<<std::endl;
    return 2;
  }
  //tcp是流式的,与文件强相关,因此与文件操作接口一起使用
  //2.bind绑定IP和端口号
  //填充结构体
  struct sockaddr_in local;
  memset(&local,0,sizeof(local));//初始化为0
  local.sin_family = AF_INET;//协议域
  local.sin_port =htons(atoi(argv[1])); //端口号
  local.sin_addr.s_addr = INADDR_ANY;//IP,不能指的IP,需要任意绑定IP;
  //绑定
  if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
  {
    
    std::cerr<<"bind error" <<std::endl;
    return 3;  
  }


  // 3.因为tcp是面向连接的,因此在通信前需要建立连接。UDP是不需要连接的。
  //建立连接的时候,就一定有人会主动建立连接,有人被动接受连接
  //而主动建立连接的是客户端,就是需要服务的一方
  //而被动接受连接的是服务器,就是提供服务的一方
  //这里的是server,被动接受连接的一方,需要周而复始地不间断地等待客户到来,比如我们半夜打开某app看剧一样
  //因此作为服务器,需要给用户提供一个建立连接的功能
  //因此,设置服务器的套接字为Listen监听状态,本质是允许用户连接
  
  const int back_log = 5;
  if(listen(listen_sock,back_log)<0)
  {
    std::cerr<<"listen error"<<std::endl;
    return 4;
  }

  signal(SIGCHLD,SIG_IGN);//父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

  //提供服务
  //使用accept接口,该接口会返回一个套接字,提供服务的套接字
  for(;;)
  {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
    if(new_sock<0)
    {
      continue;
    }

    std::cout<<"get a new link..."<<new_sock<<std::endl;

    
    // //单进程版,是没有人会去使用的。
    // //这里提供服务是死循环,只能对一个客户进行连接,因为其它客户端无法进行循环
    // //只能串行了
    ServerceIO(new_sock);
    
  }
  return 0;
}

客户端代码,代码思路如下:

①创建套接字。

②客户端不需要显示绑定ip和端口号。注意,是不需要显示绑定,并非不需要绑定,因为在客户端连接服务器的时候,操作系统会自动地绑定ip和端口号。如果固定地绑定,如果其它客户端随机绑定,随机到了我这个客户端的端口号此时我这个客户端就不能启动了。客户端关心的是连接处于监听状态的服务器。

③发起连接。先创建保存服务器ip地址和端口号信息的socketaddr结构体,然后使用connect方法进行连接。

④连接成功就可以开始通信了。

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <strings.h>

// ./tcp_client server_ip  server_port 
void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<" server_ip  server_port"<<std::endl;
}
int main(int argc,char *argv[])
{
  if(argc!=3)
  {
    Usage(argv[0]);
    return 1;
  }

  //ip和端口号
  std::string server_ip = argv[1];
  uint16_t server_port = (uint16_t)(atoi(argv[2]));

  //1.创建套接字
  int sock = socket(AF_INET,SOCK_STREAM,0);
  if(sock<0)
  {
    std::cerr<<"socket err"<<std::endl;
    return 2;
  }
  //2.绑定bind
  //客户端需要绑定,但是客户端不需要显示绑定,TCP和UDP都一样,操作系统会自动绑定端口号和ip
  //因为不能固定地绑,如果其它客户端随机绑定,随机到了我这个客户端的端口号
  //此时我这个客户端就不能启动了
  //客户端关心的是连接处于监听状态的服务器

  //创建服务器的结构并且填充信息
  struct sockaddr_in server;
  memset(&server,0,sizeof(server));
  server.sin_family = AF_INET;
  //填完,不需要显示绑定,需要连接

  //inet_addr 该函数做两件事情
  //1.将点分十进制的字符串风格的ip转化为4字节IP
  //2. 将四字节IP由主机序列转化为网络序列
  server.sin_addr.s_addr = inet_addr(server_ip.c_str());
  server.sin_port = htons(server_port);
  //发起连接
  if(connect(sock,(struct sockaddr*)&server,sizeof(server))<0)
  {
    std::cerr<<"connect err"<<std::endl;
    return 3;
  }

  std::cout<<"connect success!"<<std::endl;

  //进行业务请求,开始通信
  while(true)
  {
    std::cout<<"Please Enter# ";
    char buffer[1024];
    //先将数据写到buffer中
    fgets(buffer,sizeof(buffer)-1,stdin);
    //然后将buffer中的数据发出去
    write(sock,buffer,strlen(buffer));

    //信息返回
    ssize_t s = read(sock,buffer,sizeof(buffer)-1);
    if(s>0)
    {
      buffer[s]=0;
      std::cout<<"server echo# "<<buffer<<std::endl;
    }
  }

  return 0;
}

多进程版本TCP(客户端/服务器)

由于单进程版没有人会去使用,所以我们通过单进程版本来学习一下简单的代码实现操作,接着我们对单进程版本改造(服务器的代码),变成多进程版本。

代码思路:让父进程创建子进程,子进程去执行网络通信,执行完后就把fd关掉。同时,进入到父进程,表示了子进程已经拿到了用于通信的套接字,那么父进程就可以它关闭掉。为了避免产生僵尸进程,使用自定义信号,让父进程忽略子进程退出的信号,让子进程自动退出并释放资源。

#include<iostream>
#include<sys/socket.h> 
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<" port"<<std::endl;
}

void ServerceIO(int new_sock)
{
  while(true)
    {
      //tcp是面向字节流的,如同文件一样,可以进行正常的读写
      char buffer[1024];
      memset(buffer,0,sizeof(buffer));
      //将读到的数据放入buffer中
      ssize_t s = read(new_sock,buffer,sizeof(buffer)-1);
      if(s > 0)//读取成功
      {
          buffer[s] = 0;//将获取的内容当成字符串
          std::cout<<"client#  "<<buffer<<std::endl;
          std::string echo_string =">>>server<<<, ";
          echo_string += buffer;

          write(new_sock,echo_string.c_str(),echo_string.size());
      }
      else if(s==0)
      {
        std::cout<<"client quit..."<<std::endl;
        break;
      }   
      else
      {
        std::cerr<<"read err"<<std::endl;
        break;
      }
    }
}

// ./tcp_server port
int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    Usage(argv[0]);
    return 1;
  }

  //tcp_server
 //1.创建套接字,此套接字为监听套接字,用于绑定和监听
  int listen_sock = socket(AF_INET,SOCK_STREAM,0);
  if(listen_sock < 0)
  {
    std::cerr<<"socket error"<<std::endl;
    return 2;
  }
  //tcp是流式的,与文件强相关,因此与文件操作接口一起使用
  //2.bind绑定IP和端口号
  //填充结构体
  struct sockaddr_in local;
  memset(&local,0,sizeof(local));//初始化为0
  local.sin_family = AF_INET;//协议域
  local.sin_port =htons(atoi(argv[1])); //端口号
  local.sin_addr.s_addr = INADDR_ANY;//IP,不能指的IP,需要任意绑定IP;
  //绑定
  if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
  {
    
    std::cerr<<"bind error" <<std::endl;
    return 3;  
  }


  // 3.因为tcp是面向连接的,因此在通信前需要建立连接。UDP是不需要连接的。
  //建立连接的时候,就一定有人会主动建立连接,有人被动接受连接
  //而主动建立连接的是客户端,就是需要服务的一方
  //而被动接受连接的是服务器,就是提供服务的一方
  //这里的是server,被动接受连接的一方,需要周而复始地不间断地等待客户到来,比如我们半夜打开某app看剧一样
  //因此作为服务器,需要给用户提供一个建立连接的功能
  //因此,设置服务器的套接字为Listen监听状态,本质是允许用户连接
  
  const int back_log = 5;
  if(listen(listen_sock,back_log)<0)
  {
    std::cerr<<"listen error"<<std::endl;
    return 4;
  }

  signal(SIGCHLD,SIG_IGN);//父进程忽略子进程的SIGCHLD信号,子进程会自动退出释放资源

  //提供服务
  //使用accept接口,该接口会返回一个套接字,提供服务的套接字
  for(;;)
  {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
    if(new_sock<0)
    {
      continue;
    }
    //显示是哪给客户端连接了服务器:ip和端口号
    //ntohs:由网络字节序转为主机序列
    uint16_t cli_port = ntohs(peer.sin_port);//端口号
    //inet_ntoa:将四字节转化为点分十进制的字符串风格,并且把网络序列转化为主机序列
    std::string cli_ip = inet_ntoa(peer.sin_addr);//ip

    std::cout<<"get a new link -> : ["<<cli_ip<<":"<<cli_port<<"]# "<<new_sock<<std::endl;

    //版本二:
    //来一个客户端的一个新连接,那就创建一个子进程,子进程进行客户端的通信
    //通信完子进程退出,父进程不管
    pid_t id = fork();
    if(id < 0)
    {
      continue;
    }
    else if(id == 0)//子进程会继承父进程的文件描述符:代码共享,数据写时拷贝
    {//子进程
      //为了不让子进程对监听套接字进行不小心的操作,要在子进程中将其关掉
      close(listen_sock);
      //当子进程退出的时候,会给父进程发送信号
      ServerceIO(new_sock);
      //关闭套接字,也就是关闭fd
      //如果忘记关闭不需要的文件描述符,导致文件描述符泄漏
      close(new_sock);
      exit(0);
    }
    else
    {
      //父进程
      //因为这个用于通信的套接字已经被子进程拿过去了,因此这个套接字对父进程来说没有用了
      //可以把它关掉
      close(new_sock);
    }
  }
  return 0;
}

多线程版本TCP(客户端/服务器)

代码思路:让主线程创建新线程,然后让新线程分离,这样就可以不用线程等待,并且分离出去的线程带着用于通信的套接字去进行通信,通信完后就关闭fd。

#include<iostream>
#include<sys/socket.h> 
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
#include<pthread.h>
void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<" port"<<std::endl;
}

void ServerceIO(int new_sock)
{
  while(true)
    {
      //tcp是面向字节流的,如同文件一样,可以进行正常的读写
      char buffer[1024];
      memset(buffer,0,sizeof(buffer));
      //将读到的数据放入buffer中
      ssize_t s = read(new_sock,buffer,sizeof(buffer)-1);
      if(s > 0)//读取成功
      {
          buffer[s] = 0;//将获取的内容当成字符串
          std::cout<<"client#  "<<buffer<<std::endl;
          std::string echo_string =">>>server<<<, ";
          echo_string += buffer;

          write(new_sock,echo_string.c_str(),echo_string.size());
      }
      else if(s==0)
      {
        std::cout<<"client quit..."<<std::endl;
        break;
      }   
      else
      {
        std::cerr<<"read err"<<std::endl;
        break;
      }
    }
}

void* HandlerRequest(void* args)
{
  pthread_detach(pthread_self());//线程脱离
  int sock = *(int*)args;
  delete (int*)args;

  ServerceIO(sock);//进行通信
  close(sock);//通信结束后关闭文件描述符
}

// ./tcp_server port
int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    Usage(argv[0]);
    return 1;
  }

  //tcp_server
 //1.创建套接字,此套接字为监听套接字,用于绑定和监听
  int listen_sock = socket(AF_INET,SOCK_STREAM,0);
  if(listen_sock < 0)
  {
    std::cerr<<"socket error"<<std::endl;
    return 2;
  }
  //tcp是流式的,与文件强相关,因此与文件操作接口一起使用
  //2.bind绑定IP和端口号
  //填充结构体
  struct sockaddr_in local;
  memset(&local,0,sizeof(local));//初始化为0
  local.sin_family = AF_INET;//协议域
  local.sin_port =htons(atoi(argv[1])); //端口号
  local.sin_addr.s_addr = INADDR_ANY;//IP,不能指的IP,需要任意绑定IP;
  //绑定
  if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
  {
    
    std::cerr<<"bind error" <<std::endl;
    return 3;  
  }


  // 3.因为tcp是面向连接的,因此在通信前需要建立连接。UDP是不需要连接的。
  //建立连接的时候,就一定有人会主动建立连接,有人被动接受连接
  //而主动建立连接的是客户端,就是需要服务的一方
  //而被动接受连接的是服务器,就是提供服务的一方
  //这里的是server,被动接受连接的一方,需要周而复始地不间断地等待客户到来,比如我们半夜打开某app看剧一样
  //因此作为服务器,需要给用户提供一个建立连接的功能
  //因此,设置服务器的套接字为Listen监听状态,本质是允许用户连接
  
  const int back_log = 5;
  if(listen(listen_sock,back_log)<0)
  {
    std::cerr<<"listen error"<<std::endl;
    return 4;
  }

  
  //提供服务
  //使用accept接口,该接口会返回一个套接字,提供服务的套接字
  for(;;)
  {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
    if(new_sock<0)
    {
      continue;
    }
    //显示是哪给客户端连接了服务器:ip和端口号
    //ntohs:由网络字节序转为主机序列
    uint16_t cli_port = ntohs(peer.sin_port);//端口号
    //inet_ntoa:将四字节转化为点分十进制的字符串风格,并且把网络序列转化为主机序列
    std::string cli_ip = inet_ntoa(peer.sin_addr);//ip

    std::cout<<"get a new link -> : ["<<cli_ip<<":"<<cli_port<<"]# "<<new_sock<<std::endl;
    //版本3  多线程版本
    //新线程会跟主线程的文件描述符共享,因此不需要关闭监听套接字
    //选择新线程分离,就不需要等待了
    //新线程分离后,就拿着提供服务的套接字走,去进行通信, 通信完毕关闭这个套接字即可
    pthread_t tid;
    int * pram = new int(new_sock);
    pthread_create(&tid,nullptr,HandlerRequest,pram);
  }
  return 0;
}

警告:

在上面两个版本(多线程和多进程版本)的写法中看,有两个弊端:

①创建进程或线程是无上限的,这给了一些带有恶意的用户一个搞破坏的机会!

②每次都是要等待客户连接来了,才会去创建子进程或者是线程。这毫无疑问浪费了不必要的时间。

因此针对这两个问题,我们可以采用进程池或者是线程池来解决!下面使用线程池版本!

线程池版本TCP(客户端/服务器)

#pragma once

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

namespace ns_threadpool
{
    const int g_num = 5;

    template <class T>
    class ThreadPool
    {
    private:
        int num_;
        std::queue<T> task_queue_; //该成员是一个临界资源

        pthread_mutex_t mtx_;
        pthread_cond_t cond_;

        static ThreadPool<T> *ins;

    private:
        // 构造函数必须得实现,但是必须的私有化
        ThreadPool(int num = g_num) : num_(num)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&cond_, nullptr);
        }
        ThreadPool(const ThreadPool<T> &tp) = delete;
        //赋值语句
        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;

    public:
        static ThreadPool<T> *GetInstance()
        {
            static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
            // 当前单例对象还没有被创建
            if (ins == nullptr) //双判定,减少锁的争用,提高获取单例的效率!
            {
                pthread_mutex_lock(&lock);
                if (ins == nullptr)
                {
                    ins = new ThreadPool<T>();
                    ins->InitThreadPool();
                    std::cout << "首次加载对象" << std::endl;
                }
                pthread_mutex_unlock(&lock);
            }

            return ins;
        }

        void Lock()
        {
            pthread_mutex_lock(&mtx_);
        }
        void Unlock()
        {
            pthread_mutex_unlock(&mtx_);
        }
        void Wait()
        {
            pthread_cond_wait(&cond_, &mtx_);
        }
        void Wakeup()
        {
            pthread_cond_signal(&cond_);
        }
        bool IsEmpey()
        {
            return task_queue_.empty();
        }

    public:
        // 在类中要让线程执行类内成员方法,是不可行的!
        // 必须让线程执行静态方法
        static void *Rountine(void *args)
        {
            pthread_detach(pthread_self());
            ThreadPool<T> *tp = (ThreadPool<T> *)args;

            while (true)
            {
                tp->Lock();
                while (tp->IsEmpey())
                {
                    //任务队列为空,线程该做什么呢??
                    tp->Wait();
                }
                //该任务队列中一定有任务了
                T t;
                tp->PopTask(&t);
                tp->Unlock();

                t.Run();
            }
        }
        void InitThreadPool()
        {
            pthread_t tid;
            for (int i = 0; i < num_; i++)
            {
                pthread_create(&tid, nullptr, Rountine, (void *)this /*?*/);
            }
        }
        void PushTask(const T &in)
        {
            Lock();
            task_queue_.push(in);
            Unlock();
            Wakeup();
        }
        void PopTask(T *out)
        {
            *out = task_queue_.front();
            task_queue_.pop();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&cond_);
        }
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::ins = nullptr;
} // namespace ns_threadpool
#pragma once

#include <iostream>
#include <cstring>
#include <unistd.h>

namespace ns_task
{
    class Task
    {
    private:
        int sock;

    public:
        Task() : sock(-1) {}

        Task(int _sock) : sock(_sock) {}

        int Run()
        {
            //提供服务,短服务
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                buffer[s] = 0; //将获取的内容当成字符串
                std::cout << "client# " << buffer << std::endl;
                std::string echo_string = ">>>server<<<, ";
                echo_string += buffer;
                write(sock, echo_string.c_str(), echo_string.size());
            }
            else if (s == 0)
            {
                std::cout << "client quit ..." << std::endl;
            }
            else
            {
                std::cerr << "read error" << std::endl;
            }
            //服务提供完就关闭文件描述符
            close(sock);
        }
        ~Task() {}
    };
}
#include<iostream>
#include<sys/socket.h> 
#include<sys/types.h>
#include<cerrno>
#include<cstring>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<signal.h>
#include<pthread.h>
#include"thread_pool.hpp"
#include"Task.hpp"
using namespace ns_threadpool;
using namespace ns_task;
void Usage(std::string proc)
{
  std::cout<<"Usage: "<<proc<<" port"<<std::endl;
}
// ./tcp_server port
int main(int argc,char *argv[])
{
  if(argc!=2)
  {
    Usage(argv[0]);
    return 1;
  }

  int listen_sock = socket(AF_INET,SOCK_STREAM,0);
  if(listen_sock < 0)
  {
    std::cerr<<"socket error"<<std::endl;
    return 2;
  }

  struct sockaddr_in local;
  memset(&local,0,sizeof(local));
  local.sin_family = AF_INET;
  local.sin_port =htons(atoi(argv[1])); 
  local.sin_addr.s_addr = INADDR_ANY;
  if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local))<0)
  {
    
    std::cerr<<"bind error" <<std::endl;
    return 3;  
  }
  const int back_log = 5;
  if(listen(listen_sock,back_log)<0)
  {
    std::cerr<<"listen error"<<std::endl;
    return 4;
  }
  for(;;)
  {
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
    if(new_sock<0)
    {
      continue;
    }
    uint16_t cli_port = ntohs(peer.sin_port);
    
    std::string cli_ip = inet_ntoa(peer.sin_addr);//ip

    std::cout<<"get a new link -> : ["<<cli_ip<<":"<<cli_port<<"]# "<<new_sock<<std::endl;
    //版本4:线程池
    //构建任务
    Task t(new_sock);
    //将任务push到后端的线程池
    //获取单例即可
    ThreadPool<Task>::GetInstance()->PushTask(t);

  }
  return 0;
}

TCP协议通信流程

通过上面的代码,我们来总结一下流程:

①创建监听套接字sock。socket()方法本质是打开文件,这是跟系统相关的。

②绑定ip和端口号,使用bind()方法,本质是ip+port+文件信息进行关联。

③服务端设置为监听状态,使用listen()方法,本质是设置该socket文件的状态,允许客户端来连接。

④获取连接,使用accept()方法,连接的本质是描述连接的结构体,由操作系统管理着。

⑤发起链接,使用connect()方法,本质是发起链接,在系统层面就是构建一个申请报文发送过去,在网络层面,发起tcp链接的三次握手!

⑥进行网络通信,使用文件读写的方式,read/wirte的方法。关

⑦闭监听套接字close(fd),本质:a、在系统层面,释放曾经申请的文件资源和连接资源等待。b、在网络层面,通知对方服务端的连接已经关闭。

⑧关闭用于通信的套接字close() && server/client,本质是在网络层面进行四次挥手!

三次握手:

在服务器建立连接的时候:

调用socket, 创建文件描述符。
调用connect, 向服务器发起连接请求。
connect会发出SYN段并阻塞等待服务器应答(第一次)。
服务器收到客户端的SYN, 会应答一个SYN-ACK段表示"同意建立连接"(第二次)。
客户端收到SYN-ACK后会从connect()返回, 同时应答一个ACK段(第三次)。

四次挥手:

如果客户端没有更多的请求了, 就调用close()关闭连接, 客户端会向服务器发送FIN段(第一次);此时服务器收到FIN后, 会回应一个ACK, 同时read会返回0 (第二次);read返回之后, 服务器就知道客户端关闭了连接, 也调用close关闭连接, 这个时候服务器会向客户端发送一个FIN; (第三次)客户端收到FIN, 再返回一个ACK给服务器; (第四次)。

在上面写的代码中,不管是在单进程、多进程多线程还是线程池,我们其实都是在使用系统调用,从零开始编写网络层,而非在使用网络层!

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

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

相关文章

JavaScript(JS)-1.JS基础知识

1.JavaScript概念 (1)JavaScript是一门跨平台&#xff0c;面向对象的脚本语言&#xff0c;来控制网页行为的&#xff0c;它能使网页可交互 (2)W3C标准&#xff1a;网页主要由三部分组成 ①结构&#xff1a;HTML负责网页的基本结构&#xff08;页面元素和内容&#xff09;。 …

SPI协议——同步全双工串行通信方式

文章目录 前言一、简要介绍1、优点2、缺点 二、信号线和连接方式1、四根信号线2、连接方式2.1 多NSS形式2.2 菊花链形式 三、SPI配置1、时钟极性CPOL2、时钟相位CPHA3、四种模式4、数据大小5、其他配置参数 四、通信过程 前言 2023.4.22 阴 一、简要介绍 SPI&#xff1a;Seri…

什么是分组卷积、深度可分离卷积?附上深度可分离卷积代码

文章目录 分组卷积 Group Converlution1、由来和用途2、常规卷积和分组卷积的区别2.1、常规卷积&#xff1a;常规卷积的运算量&#xff1a; 2.2、分组卷积&#xff1a; 3、分组卷积的作用4、深度可分离卷积总结:先做分组卷积&#xff0c;再做1 x 1卷积深度可分离卷积代码 参考博…

【U-Net】训练自己的数据集

代码用的是b导的 整个训练流程也是根据b导的视频来的 源码地址&#xff1a;https://github.com/bubbliiiing/unet-pytorch 博客地址&#xff1a;https://blog.csdn.net/weixin_44791964/article/details/108866828 # 一、准备数据集 1、使用labelme软件标注数据&#xff0c;得…

深度学习 -- 什么是张量 pytorch中张量的几种创建方法

前言 在学习深度学习的过程中&#xff0c;遇到的第一个概念就是张量&#xff0c;张量在pytorch中的计算十分重要&#xff0c;所以本篇博客记录本人学习张量的过程&#xff0c;以及自己的理解。 张量是什么&#xff1f; 张量是一个多维数组&#xff0c;它是标量、向量、矩阵的…

SpringBoot的创建及配置文件

文章目录&#xff1a;一.Spring项目的创建&#xff08;1&#xff09;SpringBoot的含义 &#xff08;2&#xff09;SpringBoot的优点 &#xff08;3&#xff09;项目目录的运行和介绍 二.SpringBoot的配置文件 &#xff08;1&#xff09;配置文件的作用 &#xff08;2&#xff0…

E5--Aurora 8/10Bip核实现光纤接口通信2023-04-22

1.场景 使用两块开发板A和B&#xff0c;通过光纤接口将在A板上ROM中存储的图片数据转发到B板并显示在B板连接的显示屏上&#xff0c;实现光纤接口通信。 具体场景是&#xff0c;由于A735T片上资源有限&#xff0c;因此ROM IP存储了一张1024*600&#xff08;LVDS屏幕&#xff0…

集成光子学在计算领域的机会与挑战【光子学公开课 第133期】

没有听懂&#xff0c;自己浅浅记录一下 背景 深度学习与大模型带来的算力需求&#xff08;需要的算力指数型提高&#xff09; 解决方案 算力池化和算力网络 传统的主机服务&#xff0c;可能会存在闲置资源。 ->改变商业模式&#xff1a;不是出售硬件服务&#xff0c;…

【Makefile通用模板】入门必看篇,超详细

工程目录 假如我们有以下目录结构&#xff1a; . ├── inc │ ├── add.h │ └── sub.h ├── main.c └── src├── add.c└── sub.c文件中的内容如下&#xff1a; //main.c #include <stdio.h> #include "add.h" #include "sub.h&q…

nodejs+vue 智慧餐厅点餐餐桌预订系统

现在社会的生活节奏越来越快&#xff0c;人们对互联网的需求也越来越大&#xff0c;不仅要求使用方便&#xff0c;而且对于功能及扩展性也有了更高的要求&#xff0c;最能达到要求莫过于利用计算机网络&#xff0c;将所需功能要求和计算机网络结合起来&#xff0c;就形成了本智…

FOSSASIA Summit 的参会为 openEuler 全球化注入强心剂

2023年4月15日&#xff0c;亚洲顶级开源盛会FOSSASIA Summit 2023在新加坡落幕。openEuler作为白金赞助级别参会。 自2009年成立以来&#xff0c;除因疫情中断3年之外&#xff0c;FOSSASIA Summit已累计举办11年。作为亚洲年度开源技术旗舰活动&#xff0c;FOSSASIA Summit吸引…

C/C++ 常见编译器说明

文章目录 window下常用的编译器如何获取MSVCMinGW Linux和MaxOS下的编译器gcc和g的区别 window下常用的编译器 window下并不提供原生的类似gcc/g的类unix系统下的C/C编译器。常用的是 MSVC&#xff08;Microsoft Visual C/C&#xff09;编译器&#xff0c;在我们安装完visual …

python 获取脚本所在存储目录

获取.py文件所在目录 问题背景问题原因解决方法 问题背景 项目需要使用到当前脚本所在的目录然后保存文件 我像之前一样&#xff0c;使用了os.getcwd() 去获取脚本目录&#xff0c;保存文件&#xff0c;程序正常运行&#xff0c;但设定路径下没有任何文件&#xff0c;没有头脑…

读 AI学者生存策略

链接&#xff1a;https://arxiv.org/pdf/2304.06035.pdf 作者&#xff1a;Julian Togelius and Georgios N. Yannakakis 随着大模型 和 大数据的出现&#xff0c; AI研究者 都会感到焦虑。 没有计算资源 &#xff0c;没有标注的人力&#xff0c;很难做出突破性的研究。即使很多…

FFmpeg 中 RTSP推流桌面和Android设备延时测试

文章目录 1. FFMPEG 推流&#xff1a;1.1 FFmpeg 源码准备1.2 RTSP 推流服务器 2. 执行流程2.1 启动服务器2.2 执行桌面推流2.3 播放 3. 安卓测试 1. FFMPEG 推流&#xff1a; 1.1 FFmpeg 源码准备 官网&#xff0c;GitHub&#xff0c;CSDN中选一个就好&#xff1a; ● 官网…

在线图片编辑网站汇总

目录 前言一、在线抠图工具1. 佐糖2. remove.bg3. Clipping Magic4. 起兮深深5. BgSub6. 改图神器7. PIXLR 二、其他工具1. 压缩图2. AI人工智能图片放大 前言 本文收录了多个可以在线抠图、在线编辑图片的网站。 一、在线抠图工具 1. 佐糖 官网&#xff1a;链接 特点&#x…

一本通 3.4.4 并查集

1346&#xff1a;【例4-7】亲戚(relation) 【题目描述】 或许你并不知道&#xff0c;你的某个朋友是你的亲戚。他可能是你的曾祖父的外公的女婿的外甥女的表姐的孙子。如果能得到完整的家谱&#xff0c;判断两个人是否是亲戚应该是可行的&#xff0c;但如果两个人的最近公共祖…

企业想注销境外投资备案应该怎么做?

我们今天就来说一下关于境外投资备案的注销&#xff0c;有办理的需求&#xff0c;当然就有注销的需求。如果您当初想去海外投资并购一家公司&#xff0c;因此办理了境外投资备案&#xff0c;但是由于种种原因可能没有办法投资下去了&#xff0c;那么这个时候我们就需要做境外投…

【Linux网络服务】SSH远程访问及控制

一、openssh服务器 1.1ssh协议 SSH&#xff08;Secure Shell&#xff09;是一种安全通道协议&#xff0c;主要用来实现字符界面的远程登录、远程 复制等功能&#xff1b; SSH 协议对通信双方的数据传输进行了加密处理&#xff0c;其中包括用户登录时输入的用户口令&#xff1…

测试月入30K,Soeasy?测试这一行涨薪机制被我摸透了...

1.软件测试如何实现涨薪 首先涨薪并不是从8000涨到9000这种涨薪&#xff0c;而是从8000涨到15K加到25K的涨薪。基本上三年之内就可以实现。 如果我们只是普通的有应届毕业生或者是普通本科那我们就只能从小公司开始慢慢往上走。 有些同学想去做测试&#xff0c;是希望能够日…