1.tcp服务器实现的过程

1.1 创建套接字 socket

       

当一个进程使用了socket函数,就会创建一个套接字,并返回一个指向这个套接字的文件描述符,而这个文件描述符比较神奇,就是它不再是像其他文件描述符一样在内存中读取数据,而是在传输层的发送缓冲区中读取数据,向传输层的接收缓冲区中读取数据;

①domain :协议族  分别有三个

  • AF_INET:IPv4 协议(最常用,比如 192.168.1.1 这种地址)
  • AF_INET6:IPv6 协议
  • AF_UNIX:本地进程间通信(Unix 域套接字,不用网络)

而这些参数未来是会被写到网络层的版本号(Version)字段 里的;

② type :传输的方式

SOCK_STREAM:流式套接字,对应 TCP 协议(可靠、有序、面向连接,打电话一样) SOCK_DGRAM:数据报套接字,对应 UDP 协议(不可靠、无连接,发快递一样)

所谓的流式套接字其实就是面向字节流的传输的套接字,这也是我们今天要学习的,tcp协议使用的传输方式,当我们的参数选择的式SOCK_STREAM的时候,数据的传输天然就有了三次握手建立连接、确认应答(ACK)、 超时重传 、快重传 / 拥塞控制、 滑动窗口流量控制、 乱序重组、去重;这些特性。

③protocal :具体协议

这个参数用来指定通信的具体协议,大部分时候填 0 就行,让系统根据前两个参数自动匹配:

1.2 绑定地址和端口号  bind

首先bind的作用是给刚才创建的socket绑定特定的ip和端口号,底层做了什么呢?把你指定的端口号注册到内核的全局端口监听表(在内核中)里,标记为 “被这个 socket 占用”,其他进程不能再绑定同一个端口,绑定 IP 地址:告诉内核,这个 socket 只接收发给这个 IP 地址的数据包(比如服务器有多张网卡时,可以指定只监听某一张网卡的 IP)。关联 socket 与地址:把 addr 里的 IP 和端口信息,写入 socket 内核结构体中,让后续的 listen()、accept() 知道该监听哪个地址。

   值得注意的是,一个进程可以绑定多个端口号,但是一个端口号只能被一个进程绑定;

参数:

① int sockfd
你用 socket() 创建出来的文件描述符,就是要被绑定的那个套接字。

② const struct sockaddr *addr
指向一个通用地址结构体的指针,里面存着你要绑定的IP 地址和端口号。实际写代码时,一般会用更具体的 struct sockaddr_in(IPv4)来填充,再强转成 sockaddr* 传给 bind。

③ socklen_t addrlen
地址结构体 addr 的大小,告诉内核你传进去的地址数据有多长。

为什么服务端必须 bind,客户端可以不用?

服务端:需要固定的 IP 和端口,客户端才能找到它,所以必须 bind 明确地址。

客户端:只需要主动连接服务端,内核会自动分配一个临时端口(ephemeral port),所以不用手动 bind。

1.3  标记套接字为监听状态 listen

       干什么呢?为啥要把套接字改成listen状态呢?怎么改的呢?

首先listen之后都做了什么呢?

  • 内核通过fd找到对应的 socket 对象
  • 把这个 socket 对象的状态改成TCP_LISTEN
  • 为它创建半连接队列和全连接队列
  • 把它注册到内核的端口监听表中

半连接队列:半连接队列,就是内核为处于 TCP 三次握手过程中、尚未完成连接的客户端请求开辟的等待队列,也叫 SYN 队列。

          全连接队列:是内核为完成TCP三次握手过程的客户端开辟的存储队列,等待被accept()取走;

backlog参数: 直接限制的是已完成三次握手、等待 accept() 取走的连接队列(全连接队列) 的长度 ;

值得注意的是一旦服务器监听到一个客户端,并且这个客户端完成了三次握手,OS就会为这个客户端创建一个socket,当accept的时候就会返回这个客户端的文件描述符,为什么要给创建一个新的文件socket结构体呢?

       首先我们前面创建的socket只是用来监听的,它不能用于通信,所以每连上一个客户端,内核就要单独创建独立发送缓冲区、独立接收缓冲区、独立 TCP 状态、序号、重传机制这些都打包就成一个新 socket 内核对象;这个socket专门用于和与它建立连接的客户端通信,而这个新的socket对象的创建本身是创建了一个新的进程;因为这样这个服务器才能让客户端访问服务器中的对应资源;但是对于每次建立一个连接就创建一个进程未免太吃资源了,所以可以使用线程,并且线程天生就可以共享进程的资源;

1.4 接收连接函数 accept( )

作用:从全连接队列中取出完成三次握手的客户端连接,也就是套接字;

参数:

① int sockfd:

告诉内核 “我要从这个监听套接字的全连接队列里,取出一个已完成三次握手的客户端连接”

② struct sockadd *_Nullable restrict addr:

含义:一个指向 struct sockaddr 结构体的指针,用来接收内核返回的客户端地址信息(IP 地址 + 端口号)。

常见用法:实际写代码时,一般会用更具体的 struct sockaddr_in(IPv4)或 sockaddr_in6(IPv6)来接收数据,再强制转换成 struct sockaddr* 传给 accept

特殊说明:参数上的 _Nullable 表示这个指针可以传 NULL。如果你不关心客户端的地址信息,直接传 NULL 就行。

③ socklen_t * _Nullable restrict addlen:

一个指向 socklen_t 类型变量的指针,用来告诉内核 addr 缓冲区的大小,同时内核会把实际写入的地址信息长度写回这个变量里。

用法细节:

输入时:你要把变量初始化为 sizeof(struct sockaddr_in),告诉内核缓冲区的大小。

输出时:内核会把实际客户端地址的长度更新到这个变量里(一般和初始值相同,但对变长协议族可能不同)。 特殊说明:同样可以传 NULL,前提是 addr 也传了 NULL。

1.5 客户端发起连接connect( )

这个我觉得应该就很好理解吧;connect之后就会触发服务器一直等待连接的监听套接字的三次握手;而udp也有connect函数,但是它不会进行三次握手,而是直接和目标服务器进行通信;

① int sockfd

客户端自己创建的套接字用来和服务器进行通信;

②const struct sockaddr * addr

指向一个 struct sockaddr 结构体的指针,里面存着你要连接的服务器 IP 地址和端口号。

2.TCP服务器的实现

server.hpp

#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/wait.h>
#include <pthread.h>
#include <functional>
#include "log.hpp"
#include "conmmen.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

using namespace LogModule;
using namespace ThreadPoolModule; 

const int gloalsockfd = -1;
static const uint16_t gport = 8080;
#define BACKLOG 8
using handler_t =std::function<std::string (std::string)>;
class Tcpserver
{
  using task_t =std::function<void()>;
  struct ThreadData
  {
      int sockfd;
      Tcpserver * self;
  };

public:
  Tcpserver(handler_t handler, uint16_t port = gport)
      : _handler(handler),_listensockfd(gloalsockfd), _port(port), _isrunning(false)

  {
  }

  void InitServer()
  {
    // 1.创建套接字
    _listensockfd = ::socket(AF_INET, SOCK_STREAM, 0);
    if (_listensockfd < 0)
    {
      LOG(loglevel::FATAL) << "socket:" << strerror(errno);
      Die(SOCKET_ERR);
    }
    LOG(loglevel::INFO) << "create socket success";

    // 2.绑定套接字到网络
    struct sockaddr_in local;
    memset(&local,0,sizeof(local));
    local.sin_family=AF_INET;
    local.sin_port=htons(gport);
    local.sin_addr.s_addr=INADDR_ANY;
    int n = ::bind(_listensockfd, cast(&local),sizeof(local));
    if (n < 0)
    {
      LOG(loglevel::FATAL) << "bind:" << strerror(errno);
      Die(BIND_ERR);
    }
    LOG(loglevel::INFO) << "bind success";

    // 3.监听
    n = ::listen(_listensockfd, BACKLOG);
    if (n < 0)
    {
      LOG(loglevel::FATAL) << "listen:" << strerror(errno);
      Die(LISTEN_ERR);
    }
    LOG(loglevel::INFO) << "listen success";
    signal(SIGCHLD, SIG_IGN); //忽略子进程退出信号,os自动回收不再不要wait了
  }

  void HandlerRequest(int sockfd)
  {
     LOG(loglevel::INFO)<<"HandlerRequest ,sockfd is:"<<sockfd;
     char inbuffer[4096];
     while(true)
     {
       //ssize_t n = ::read(sockfd,inbuffer,sizeof(inbuffer)-1);//读取不完善
       ssize_t n = ::recv(sockfd,inbuffer,sizeof(inbuffer)-1,0);  
       if(n>0)
       {
          inbuffer[n]=0;
          // std::string echo_str="server echo #";
          // echo_str+=inbuffer;

          std::string result_str = _handler(inbuffer);    //回调方法出去调用完还是会回来的
         // ::write(sockfd,echo_str.c_str(),echo_str.size()); 写入也是不完善的
           ::sendto(sockfd,result_str.c_str(),result_str.size(),0,nullptr,0 );
       }
       else if(n==0)
       {
            LOG(loglevel::INFO)<<"client close the connection"<< sockfd;
            break;  
       }
       else
       {
          break;
       }
     }

     ::close(sockfd);
  }

  static void * threadEntry(void * args)
  {
      pthread_detach(pthread_self());
      ThreadData * data =(ThreadData*)args;
      data->self->HandlerRequest(data->sockfd); 
      return nullptr;
  }

  void Start()
  {
    _isrunning = true;
    while (_isrunning)
    {
      struct sockaddr_in peer;
      socklen_t peerlen = sizeof(peer);  //对端地址的长度必须初始化好,不然会有随机值,参数就会出错 
      int sockfd = ::accept(_listensockfd, cast(&peer), &peerlen);
      if (sockfd < 0)
      {
        LOG(loglevel::WARNING) << "accept:" << strerror(errno);
        continue;
      }
      LOG(loglevel::INFO) << "accecpt success,sockfd:" << sockfd;

      //version 1 多进程模式
      // pid_t id=fork();  //创建子进程
      // if(id == 0)
      // {
      //   //关闭不需要的文件描述符,防止子进程误触发
      //   ::close(_listensockfd); 
      //   if(fork() >0) exit(0);  //子进程退出,孙子进程执行,因为子进程退出了,所以孙子进程成为了孤儿进程,会被系统收养,所以孙子进程也要退出,不然会造成僵尸进程
      //   //子进程处理请求  
      //   HandlerRequest(sockfd); 
      //   exit(0);  //子进程退出,必须退出否则会造成内存泄露,因为子进程会拷贝父进程得文件描述符表
      // }
      // ::close(sockfd);  //sockfd已经交给子进程了,父进程关闭自己不关心的文件描述符,防止误触发
      // //子进程已经退出,父进程不再阻塞
      // int rid =::waitpid(id,nullptr,0);
      // if(id < 0) 
      // {
      //    LOG(loglevel::FATAL)<<"create fork error";
      // }

      //version 2 多线程模式
      //新线程和主线程共享一张文件描述符表
      // pthread_t tid ;
      // ThreadData * data= new ThreadData;  //sockfd是在栈上开辟的空间声明周期很短,int(sockfd)的意思是用sockfd初始化int类型的地址sockfdp;
      // data->sockfd=sockfd;
      // data->self=this;
      // pthread_create(&tid,nullptr,threadEntry,data);

      //version 3 线程池模式
      task_t f=std::bind(&Tcpserver::HandlerRequest,this,sockfd);
      ThreadPool<task_t>::getInstance()->Equeue([this,sockfd](){
        this->HandlerRequest(sockfd);
      });
      
    }
  }

  void Stop()
  {
    _isrunning = false;
  }
  ~Tcpserver()
  {
  }

private:
  int _listensockfd;
  bool _isrunning;
  uint16_t _port;
  handler_t _handler;


};

clientMain.cc

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

int main(int argc,char * argv[])
{
    if(argc !=3)
    {
        std::cout<<"Usage:./client_tcp server_ip server_port"<<std::endl;
        return 1;
    }
    std::string server_ip = argv[1];
    int server_port = std::stoi(argv[2]); 
    int sockfd=::socket(AF_INET,SOCK_STREAM,0);
    if(sockfd<0)
    {
        std::cout<<"create sockfd error"<<std::endl;
        return 2;
    }

    struct sockaddr_in server_addr; 
    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family=AF_INET; 
    server_addr.sin_port=::htons(server_port); 
    server_addr.sin_addr.s_addr=::inet_addr(server_ip.c_str());   

    int n = ::connect(sockfd,(struct sockaddr*)&server_addr,sizeof(server_addr));  //connect之后会自动bind
    if(n<0)
    {
        std::cout<<"connect error"<<std::endl;
        return 3;
    }
    std::string message;
    while(true)
    {
        char inbuffer[1024];
        std::cout<<"input message:"; 
        std::getline(std::cin,message);
        n= ::write(sockfd,message.c_str(),message.size());
        if(n>0)
        {
            //读的是服务器发回来的数据
           int m = ::read(sockfd,inbuffer,sizeof(inbuffer));
           if(m>0)
           {
              inbuffer[m]=0;
              std::cout<<"receive message from server:"<<inbuffer<<std::endl;
           }
           else
                 break;
        }
        else 
              break;    
    }   
    ::close(sockfd);
    return 0;
}

serverMain.cc

#include "server.hpp"
#include "commendExcu.hpp"
#include <memory>

int main()
{
    ENABLE_CONSOLE_LOG();
    commendExcu commend;   //实例化命令执行类   
    std::unique_ptr<Tcpserver> tsvr=std::make_unique<Tcpserver>([&commend](std::string cmdstr){
        return commend.execute(cmdstr);
    });   //用智能指针初始化服务器
    tsvr -> InitServer();   //初始化服务器
    tsvr -> Start();        //启动服务器
    return 0;
}

commentExct.hpp ->实现远程执行命令功能

#pragma once
#include <iostream>
#include <cstdio>
#include <string>   

class commendExcu
{
  public:
    std::string execute(std::string cmdstr)
    {
      FILE * fp = ::popen(cmdstr.c_str(), "r");
      if(fp == nullptr)
      {
         return std::string("Failed to execute command");
      }
      char buffer[1024];
      std::string result;
      while(true)
      {
         char * ret = ::fgets(buffer, sizeof(buffer), fp);
         if(!ret) break;
         result += ret;
      }
      pclose(fp);
      return result.empty()?std::string("done"):result;
     }
};

本篇博客可以搭配"每日遗忘"专栏中的二三篇文章来看;

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐