c++从零实现reactor高并发服务器!!!

环境准备

  1. linux虚拟机
  2. 安装升级c/c++编译器
  • gcc/g++ 选项 源代码文件1 源代码文件2 ... 源代码文件n
  • -o指定输出的文件名(不能和源文件同名 默认是a.out)
  • -g调试 -On链接时优化 减小体积(n=1-3) -c只编译 用于生成库
  • -std=c++11 支持c++11标准
  1. 安装man功能
  • man 级别 接口/命令
  • 级别: 1系统命令 2系统接口 3库函数 4设备文件 5文件 9内核
  1. 安装vscode c/c++插件 简体中文插件 Remote-ssh插件

基础知识

静态库动态库

  1. g++ -c -o libxxx.a xxx.cpp 生成了libxxx.a的静态库
  • g++ -o demo demo.cpp -L/path/xxx -lxxx -L指定路径 -l指定静态库名
  • 用静态库和用源代码是一样的,好处是可以隐藏源代码
  1. g++ -fPIC -shared -o libxxx.so xxx.cpp 制作动态库 调用方式同上
  • 用动态库必须先把目录加到LD_LIBRARY_PATH
  • 动态库是编译时不会连接到程序中,而是运行时装入,如果多个程序用到同一静态库,只在内存有一份(代码共享),避免空间浪费

**静态动态库都有 优先使用动态

makefile

每次编译都要g++ xxxx很麻烦,果然懒惰是第一生产力

# 指定编译的目标文件是生成这俩库
all:libxxx.a \
    libxxx.so

# 编译libxxx.a时,如果发现后面这俩文件变化了 重新编译
libxxx.a: main.h main.cpp
	g++ -c -o libxxx.a main.cpp+

# 同上
libxxx.so: main.h main.cpp
	g++ -fPIC -shared -o libxxx.so main.cpp

# make clean命令
clean:
		rm -f libxxx.a libxxx.so
  1. 增量编译,也就是说当前目录下有静态/动态了,就不编译这个了
  2. 用-I指定头文件包含路径
  3. g++前面是个tab,而不是八个空格
  4. main函数第三个是char* envp[] 打印出来效果如同env命令
  5. int setenv(const char* name, const char* value, int override) 环境变量名/值/是否替换 返回0成功-1失败(几乎不失败) 只对当前进程生效 进程终止下次就没有了,对shell无效

gdb调试

  1. yum -y install gdb 安装
  2. 编译时加-g 不要加-On
  3. gdb常用命令
  • set args xx xx xx 设置参数
  • break/b xx 在第某行打断点 (ctrl+g显示行号 或者vi下:set number)
  • run/r 一直运行直到断点
指令 用处 其他说明
set args xx xx 设置参数
break/b 20 在第20行打断点 ctrl+g 或 :set number
run/r 从头一直运行直到断点
next/n 执行当前语句 若为函数调用不进入
step/s 执行当前语句 进入(库函数由于无源码进不去)
continue/c 运行到下一个断点
print/p xx 查看变量/表达式的值 甚至可以p strlen(xx) p xx = 1
set var xx = xx 调试时设置参数
quit/q 退出gdb
  1. 出现段错误时(操作空指针) 程序会被内核强行终止,保存在core文件中(需要先ulimit -a 查看 core file size ulimit -c unlimited更改后才能看到)
  2. gdb demo core.123调试core文件 bt查看函数调用栈
  3. ps -ef|grep demo 查看进程号 gdb -p demo 123 会自动停止

linux

时间 <time.h>

  1. time_t
    typedef long time_t
  2. 获取1970/1/1到现在的秒数
    time_t now = time(0)
    time_t now; time(&now)
  3. tm结构体
    image
  4. 从time_t转tm结构体,注意加_r 线程安全
    localtime_r(&now, &tmnow)
    image
  5. mktime(&tm)把结构体转time_t
  6. gettimeofday(struct timeinterval* tv, struct timezone* tz) 获取1970/1/1到现在的秒数+当前的微秒数
    image
  7. sleep(秒) usleep(微秒)

目录操作<unistd.h>

  1. 获取当前目录
    char* getcwd(char* buf, size_t size)
    char* get_current_dir_name()
  • 相当于pwd,目录最大长度255 getcwd需要初始化一个256长度的字符数组,get_current_dir_name需要接free
  1. 切换目录
    int chdir(const char*path)

  2. 创建目录
    int mkdir(const char*pathname, mode_t mode)

  • mode如0755,不要省略0
  1. 删除目录
    int rmdir(const char*path)

<dirent.h> 读取目录相当于ls -a

DIR* opendir(const char* path); //打开目录
struct dirent*readdir(DIR* dirp); //读取目录
int closedir(DIR* dirp);        //关闭目录

image

  • 其中 d_type = 8 是文件,= 4 是子目录
    a
  1. 判断文件是否有某个权限,有返回0 没有返回-1
    int access(const char* path, int mode)

image

  1. stat结构体,有很多成员,比ls列出的还多
    int stat(const char*path, struct stat*buf)
    image

  2. 修改目录或文件的时间
    int utime(const char* path,const struct utimbuf* time)

  3. rename库函数 相当于mv
    int rename(const char* old, const char* new)

  4. remove库函数 相当于rm
    int remove(const char* path)

Linux系统错误 <errno.h>

  1. 获取错误代码的详细信息
    char* strerror(int errnum)
    int strerror_r(int errnum, char* buf, size_t buflen)

  2. 控制台显示最近一次系统错误的详细信息
    void perrpr(const char*s)

  • 不是系统调用的函数,不会设置errorno!!!!
  • 相当于出现error时,printf打印一下,但是error不会自动清零,所以一般是判断if (ret!=0) 也就是执行失败再去看错误

进程控制和进程同步

linux信号

可以用默认的信号操作(通常会终止进程) 也可以用signal函数自定义处理方式,但是有的信号不可被捕获、忽略 如9

sighandler_t signal(int signum, sighandler_t func)
void (*sighandler_t)(int);

  • 说明信号处理函数返回值void 入参int
  • func传入 SIG_IGN 表示忽略这个值的信号 SIG_DFL表示恢复默认
  • alarm(5); signal(14,func); 用于定时五秒发送闹钟信号(14)然后执行func函数~~ 注意 func中需要有alarm(5) 不然就只会处理一次咯!!

进程终止

  1. main函数中,return返回
  2. 任意函数调用exit, _exit() , Exit()
  • exit()不会调用局部变量的析构,但是会调用全局变量的析构
  • _exit() 和 Exit() 直接退出,不会进行任何操作
  1. 退出线程:pthread_exit() 线程主函数return

  2. abort()异常终止、接收到信号、最后一个线程对取消请求做出响应

终止的状态就是main中 return 几

  1. exit(5) 可以把状态变成5 退出后,用echo $?查看
  2. 异常终止的话,状态非零
  3. 用atexit(func)注册退出函数,当正常退出/exit()退出,会调用,最多注册32个!!!

调用可执行程序

  1. system() 成功返回0或非零(正常退出,但是不等于退出状态),失败返回非零
  2. exec函数族

新进程和原进程pid相同,代码段数据段 堆栈都取而代之!!也就是说 exec()后面的函数都不会执行!!
int execl("/bin/ls", "/bin/ls", "-lt", "/tmp", 0)

  • 为什么用/bin/ls 而不是直接ls呢,因为可能没包含bin的环境变量
  • 为什么是-lt 是因为-l详细显示,-t按修改时间排序
    int execv("/bin/ls", args)
  • 其中char* args[] 表示args是一个数组,每个元素都是一个字符串,args[len-1] = 0!!!很重要

创建进程

  1. linux的进程是树形结构 0号祖先创建1号systemd(内核初始化和系统配置) 2号kthreadd(线程调度管理)
  • pstree查看
  1. fork()创建子进程,fork()后面的代码子进程父进程都执行,调用一次但返回两次,子进程的返回值是0 父进程是子进程的pid
  • 所以写了pid_t i = fork() 后 通过判断返回值分别执行父子进程的代码
  • 子进程是用的父进程副本,而不是共享,父子进程执行顺序不确定!!
  1. 父子进程共享文件偏移量,先后的写不会被覆盖
  2. vfork()创建一个新进程,且立即调用exec,可保证子进程先运行!!

僵尸进程

子进程结束,父进程没结束,所以就没处理子进程的信息,叫僵尸进程

  1. 父进程结束,子进程没结束就被1号进程托管,这其实是一种运行在后台的方式!!(原始的方式是./demo &)
    if(fork()>0) return 0
  • 这样可以让父进程退出,子进程挂给一号进程继续运行!!
  1. 僵尸进程危害:系统的可用pid有限,僵尸进程太多影响性能
  2. 避免僵尸进程:子进程退出,内核会通知父进程SIGCHLD,如果父进程忽略这个信号,表示“我早知道他要退出了” 那么子进程退出后立即释放!
    signal(SIGCHLD, SIG_IGN)
  • 也可以wait,waitpid()...
  • 当然也可以选择捕获SIGCHLD信号,并在处理函数中wait() waitpid()

发送信号

  1. kill(pid_t pid, int sig) pid>0指定发送 pid=-1系统内前进程组的成员,一般用于父进程发给子进程(自己也会收到,所以父进程的处理函数一般要加一句忽略信号,防止递归进入信号处理函数,,,)

父进程收到kill(默认15) 或ctrl+c,退出,并在退出函数中向所有子进程发送15信号,子进程捕获15信号并处理!

int main(){
    for(int i = 0; i < 64; i++) signal(i,SIG_IGN);
    signal(SIGTERM, FathEXIT);
    signal(SIGINT, FathEXIT);
    while(1)
    {
    //父进程,每五秒创建一个子进程
        if(fork() > 0){
            sleep(5);
            continue;
        }
    else{
        signal(SIGTERM, ChildEXIT);
        signal(SIGINT, SIG_IGN);
		while(1)
		{
		    cout<<"子进程运行中..."<<endl;
			sleep(3);
			continue;
		}
    }
  }
}
void FathEXIT(int sig)
{
    signal(SIGTERM, SIG_IGN);
    signal(SIGINT, SIG_IGN);
    cout<<"父进程退出,sig="<<sig<<endl;
    kill(0, SIGTERM);
    exit(0);
}
void ChildEXIT(int sig)
{
    cout<<"子进程"<<getpid()<<"退出,sig="<<sig<<endl;
    exit(0);
}

共享内存

  1. 共享内存没提供锁的机制,一边读一边写容易出错!!!

  2. 创建/获取(如已创建)共享内存
    int shmget(key_t key, size_t size, int shmflg)

  • key说白了是unsigned int 一般用十六进制
  • size以字节为单位
  • 访问权限,0666表示全部用户可读写 IPC_CREAT表示不存在时创建
  • 成功返回共享内存id(正整数),失败(内存不足或无权限)返回-1
  1. ipcs -m 查看 ipcrm -m xxx 手动删除

  2. 进程中使用共享内存
    void* shmat(int shmid, const void* shmaddr, int shmflg)

  • shmget() 返回的shmid
  • 第二个填0,让系统选择在那块地址共享 第三个填0
  • 然后就可以对返回的指针进行操作了,相当于在操作这个内存!!
  1. 把共享内存从当前进程里分离出去
    shmdt(const void* shmaddr)
  • 参数填shmat()返回值
  1. 所有进程都不用了 就删除共享内存
    int shmctl(int id, int command, shmid_ds* buf)
  • 要删除共享内存,第二个填IPC_RMID宏 第三个填0
  • root用户创建的,任何普通用户都删不掉!!
  • 不能用STL容器!!因为STL会在堆区动态分配内存,堆不属于共享内存!

循环队列

队列是一种实现共享内存的数据结构

  1. 循环队列,队头出 队尾进
  2. 循环队列用的数组实现,所以当满了之后,就要从第一个位置进入,并且把队尾指针指向第一个!!
  • 也就是说,比如用数组长度n的模拟,尾指针索引是m,每次插入就是(m+1)%n

信号量

  1. P操作减一 V操作加一
  2. ipcs -s查看系统中的信号量 ipcrm sem id 删除信号量

网络编程

linux文件描述符

分配规则是找到最小的,没被占用的!!

  1. /proc/进程id/fd 存放了这个进程的所有文件描述符(open返回值)!!
  2. 一个进程默认存在三个,标准输入cin-0 标准输出cout-1 标准错误cerr-2 也就是0--键盘 1--显示器
  • 可以用close(0/1/2)关闭标准输入输出错误等。。
  • 文件和socket是一个东西,,所以socket也是文件描述符。。。
  • send可以改成write!!! recv()可以改成read!!!

SOCKET

  1. 创建套接字
    int socket(int domain, int type, int protocol)
  • 第一个协议族,常用PF_INET--ipv4 AF_INET6--ipv6 AF_LOCAL--本地
  • 第二个传输类型,常用SOCK_STREAM--面向连接 SOCK_DGRAM--无连接
  • 第三个填0,会根据是STREAM填入IPPROTO_TCP,DREAM填入UDP
  • ulimit -a 查看open files,就是最大的文件描述符数量!!
  1. 数据占用大于一字节时,操作系统的存储方式就有两种:大端小端
  • 比如存0x12345678(每个数用四位01表示 0-F ) 所以一共是4*8=32位,8位一字节,也就是四字节,每个字节存在一个地址上!!
  • 大端:0x12属于高位,存在0x00000001 , 0x34存在0x00000002!!
  • 小端反过来,也就是低位(右边)存在低地址!!!!
  • 网络字节序是大端!!!!!!!!!!!
uint16_t htons()
uint16_t htonl()
uint16_t ntohs()
uint16_t ntohl()
// h主机 to转换 n网络 s short-2字节 l long-4字节
// 最终都是用网络序传输!!!
  1. 结构体们:
  • sockaddr结构体,bind和connect要用到
    image

  • 我们发现,用14字节表示端口和地址,我不会表示啊,所以定义了sockaddr_in结构体,可以强转为sockaddr
    image

为啥不能直接用sockaddr_in呢,因为32位地址对于ipv6不够用!!

  • 16位的端口号可以htons(8080)
  • 32位的ip地址需要gethostbyname("192.168.1.1"), 返回hostent结构体
struct sockaddr_in s;
struct hostent* h = gethostbyname("192.168.1.1");
memcpy(&s.sin_addr, h->h_addr, h-h_length);
  • 32位的ip地址这个参数,也可以直接用库函数
服务器端可以写:
s.sin_addr.s_addr = htonl(INADDR_ANY); 表示全部ip可被用
s.sin_addr.s_addr = inet_addr("192.168.1.1");表示只有这个网段的可以用
  • 总结:inet_addr把字符串ip转大端序 inet_ntoa反之!!

多进程服务端

主要是采用新来一个连接,fork一个进程处理的方式,父进程只负责处理连接,子进程只负责收发数据

 while(true)
    {
      if (!server.start()) {
          cout << "连接失败" << endl;
          return -1;
      }
      int pid = fork();
      if (pid == -1){
          cout << "系统资源不足" << endl;
          return -1;
      }
      //父进程回到while(true)第一行,继续处理connect
      if (pid > 0) {
        continue;
        server.closeclient();
      }

      cout << "客户端" + server.getCip() + "已连接" << endl;

      server.closelisten();
      string buffer;
      for(int i = 0; i < 5; i++)
      {
          if (!server.recv(buffer, 1024)) {
              cout << "接收失败!" << endl;
              break;
          }
          cout << "收到: " << buffer.c_str() << endl << endl;

          buffer = "这是服务器的第 " + to_string(i + 1) + " 条消息";
          if (!server.send(buffer)) {
              cout << "发送失败!" << endl;
              break;
          }
          cout << "发送:" << buffer << endl << endl;
      }
    }
    return 0;
}

改进:添加signal函数

文件传输

主要利用了ifs ofs流、二进制传输

服务端代码


#include <iostream>
#include <fstream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <signal.h>
using namespace std;

class Server {
public:
    Server(): m_listenfd(-1),m_clientfd(-1){}

    bool init(const unsigned short port, const unsigned int num){
        m_listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (m_listenfd == -1) return false;

        m_port = port;
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_addr.s_addr = INADDR_ANY;
        servaddr.sin_port = htons(m_port);

        if(bind(m_listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
            closelisten();
            m_listenfd = -1;
            return false;
        }
        if(listen(m_listenfd, num) != 0) {
            closelisten();
            m_listenfd = -1;
            return false;
        }
        return true;
    }

    bool start(){
        struct sockaddr_in clientaddr;
        socklen_t clen = sizeof(clientaddr);
        m_clientfd = accept(m_listenfd, (struct sockaddr *)&clientaddr, &clen);
        if (m_clientfd == -1) return false;

        m_cip = inet_ntoa(clientaddr.sin_addr);
        return true;
    }

    const string getCip() {
        return m_cip;
    }


    bool send(const string &buffer){
        //未连接 无需发送
        if (m_clientfd == -1) return false;

        if(::send(m_clientfd, buffer.data(), buffer.size(), 0) == -1) return false;
        return true;
    }

    //接收字符串
    bool recv(string &buffer, const unsigned int max_len){
        //未连接 无需接收
        if (m_clientfd == -1) return false;

        //这里的buffer不是const,说明他是string对象,可以用库函数!
        buffer.clear();
        buffer.resize(max_len);
        int n = ::recv(m_clientfd, &buffer[0], buffer.size(), 0);
        if(n == -1) return false;
        buffer.resize(n);

        return true;
    }

    //接收二进制 结构体
    bool recv(void *buffer, const unsigned int max_len){
        int n = ::recv(m_clientfd, buffer, max_len, 0);
        if(n == -1) return false;

        return true;
    }


    bool recvfile(const string &name, const unsigned int size){
        ofstream fout;
        fout.open(name, ios::binary);
        if(fout.is_open() == false){
            cout << "打开" << name << "失败" << endl;
            return false;
        }
        int len; //每次写入的长度
        int write;//已写入的长度
        char buffer[10]; //每次写十个

        while(true)
        {
            memset(buffer, 0, sizeof(buffer));
            int buf_len = sizeof(buffer) / sizeof(buffer[0]);
            len = (size - write) > buf_len ? buf_len : (size - write);

            if(!recv(buffer, len)) return false;
            fout.write(buffer, len);

            if((write +=len) == size) break;
        }
        return true;
    }
    bool closelisten(){
        if (m_clientfd == -1) return false;
        ::close(m_listenfd);
        m_listenfd = -1;
        return true;
    }

    bool closeclient(){
        if (m_clientfd == -1) return false;
        ::close(m_clientfd);
        m_clientfd = -1;
        return true;
    }

    ~Server() {
        closelisten();
        closeclient();
    }


private:
    int m_listenfd;
    int m_clientfd;
    unsigned short m_port;
    string m_cip;
};

Server server;
void FatherEXIT(int sig){
    signal(SIGTERM, SIG_IGN);
    signal(SIGINT, SIG_IGN);
    cout<<"父进程退出,sig="<<sig<<endl;
    kill(0, SIGTERM);
    server.closelisten();
    exit(0);
}

void ChildEXIT(int sig){
    cout<<"子进程"<<getpid()<<"退出,sig="<<sig<<endl;
    server.closeclient();
    exit(0);
}
int main(int argc, char *argv[]) {
    if (argc != 3) {
        cout << "用法: ./file_server port 存放目录" << endl;
        cout << "示例: ./file_server 8080 /files" << endl << endl;
        return -1;
    }

    for(int i = 1; i <= 64; i++) signal(i, SIG_IGN);

    //父进程收到 2/15 进入退出处理
    signal(SIGINT, FatherEXIT);
    signal(SIGTERM, FatherEXIT);

    if (!server.init(atoi(argv[1]), 5)) {
        cout << "初始化失败,可能是创建socket、已初始化过、监听端口失败!!" << endl;
        return -1;
    }
    cout << "服务器已启动,等待客户端连接..." << endl << endl;
    while(true)
    {

        if (!server.start()) {
            cout << "连接失败" + server.getCip() + "可能是端口被占用!!" << endl;
            return -1;
        }
        int pid = fork();
        if (pid == -1){
            cout << "系统资源不足" << endl;
            return -1;
        }
      //pid > 0 是父进程,父进程只需受理客户端连接,无需处理数据收发,关闭client sock!!!
        if (pid > 0) {
        server.closeclient();
        continue;
        }
        //子进程无需受理客户端连接,只负责收发数据,关闭监听sock!!!!
        server.closelisten();

        //子进程收到 15 进入退出处理   收到2忽略!
        signal(SIGTERM, ChildEXIT);
        signal(SIGINT, SIG_IGN);

        cout << "客户端" + server.getCip() + "已连接" << endl << endl;

        //接收文件名 文件大小信息 (以结构体形式,也就是二进制文件)
        struct file_info{
            char file_name[256];
            unsigned int file_len;
        }fi;
        memset(&fi, 0, sizeof(fi));

        if (!server.recv(&fi, sizeof(fi))) {
            cout << "接收文件信息失败" << endl;
            return -1;
        }
        cout << "接收:" << fi.file_name << "(" << fi.file_len << ")" << endl << endl;

        //回复客户端可以开始发了
        if (!server.send("OK")) {
            cout << "发送文件信息失败" << endl;
            return -1;
        }
        cout << "已发送OK" << endl << endl;
        //接收文件

        if(!server.recvfile(argv[2], fi.file_len)){
            cout << "接收文件失败" << endl;
        }
        cout << "已接收完毕" << endl << endl;
        //收完了 回复客户端已处理完成
        if (!server.send("OVER")) {
            cout << "发送文件信息失败" << endl;
            return -1;
        }
        cout << "已发送over" << endl << endl;
        return 0;
    }
}

客户端


#include <iostream>
#include <fstream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;

class Client{
public:
    Client():m_sockfd(-1){}

    bool init(const string &ip, const unsigned short port){
        //已经初始化过 不要重复初始化
        if (m_sockfd != -1)  return false;

        m_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (m_sockfd == -1) return false;

        m_ip = ip;
        m_port = port;

        return true;
    }
    bool connect(){
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(m_port);
        servaddr.sin_addr.s_addr = inet_addr(m_ip.c_str());
        if (::connect(m_sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1){
            close();
            return false;
        }
        return true;
    }

    //发送字符串
    bool send(const string &buffer){
        //未连接 无需发送
        if (m_sockfd == -1) return false;

        if(::send(m_sockfd, buffer.data(), buffer.size(), 0) == -1) return false;
        return true;
    }

    //发送结构体  这里之所以要max_len 是因为sizeof(void*)不准确!!
    bool send(void* buffer, const unsigned int max_len){
        //未连接 无需发送
        if (m_sockfd == -1) return false;

        if(::send(m_sockfd, buffer, max_len, 0) == -1) return false;
        return true;
    }


    bool recv(string &buffer, const unsigned int max_len){
        //未连接 无需接收
        if (m_sockfd == -1) return false;

        //这里的buffer不是const,说明他是string对象,可以用库函数!
        buffer.clear();
        buffer.resize(max_len);
        int n = ::recv(m_sockfd, &buffer[0], buffer.size(), 0);
        if(n == -1) return false;
        buffer.resize(n);

        return true;
    }


    bool sendfile(const string& name, const unsigned int size){
        ifstream fin(name, ios::binary);
        if(fin.is_open() == false){
            cout << "打开" << name << "失败" << endl;
            return false;
        }
        int len; //每次读len
        int read; //已读过的
        char buffer[10];
        while(true)
        {
            memset(buffer, 0, sizeof(buffer));
            int buf_len = sizeof(buffer) / sizeof(buffer[0]);
            len = (size - read) > buf_len ? buf_len: (size - read);

            fin.read(buffer, len);
            if (!send(buffer, len)) return false;
            cout<<read+len<<endl;
            if((read += len) == size) break;
        }
        return true;
    }
    bool close(){
        if (m_sockfd == -1) return false;

        ::close(m_sockfd);
        m_sockfd = -1;
        return true;
    }
    ~Client(){close();}

private:
    int m_sockfd;
    unsigned short m_port;
    string m_ip;
};
int main(int argc, char *argv[])
{
    if (argc != 5)
    {
        cout << "用法: ./file_client ip port 文件名 文件大小" << endl;
        cout << "示例: ./file_client 192.168.15.186 8080 xsl.txt 1024" << endl << endl;
        return -1;
    }
    Client client;
    if (!client.init(argv[1], atoi(argv[2])))
    {
      cout << "初始化失败!!! 可能是已初始化或系统socket已满!!!" << endl;
      return -1;
    }

    if (!client.connect())
    {
      cout << "连接失败!!! " << endl;
      return -1;
    }

    cout << "连接成功!" << endl;

    //发送文件名 文件信息
    struct file_info{
        char file_name[256];
        unsigned int file_len;
    }fi;
    memset(&fi, 0, sizeof(fi));
    strcpy(fi.file_name, argv[3]);
    fi.file_len = atoi(argv[4]);

    if(!client.send(&fi, sizeof(fi))){
        cout << "发送文件信息失败" << endl;
        return -1;
    }

    cout << "发送:" << fi.file_name << "(" << fi.file_len << ")" << endl;

    //等待服务器响应

    string buffer = "";
    if(!client.recv(buffer, 1024)){
        cout << "接收ok失败" << endl;
        return -1;
    }
    if(buffer != "OK"){
        cout << "服务端校验文件信息fail" << endl;
        return -1;
    }
    cout << "服务端已回复OK" << endl << endl;

    //发送文件

    if(!client.sendfile(fi.file_name, fi.file_len)){
        cout << "发送文件失败!" << endl;
        return -1;
    }
    //等待服务器响应
    buffer = "";
    if(!client.recv(buffer, 1024)){
        cout << "接收over失败" << endl;
        return -1;
    }
    if(buffer != "OVER"){
        cout << "服务端接收文件fail" << endl;
        return -1;
    }
    cout << "服务端已回复OVER" << endl << endl;

    return 0;
}

TCP

  1. netstat -na 表示 列出数字形式的ip port 、全部 网络连接
  • netstat -natu 可以只显示tcp udp 不显示那一堆烦人的本地socket
  1. bind,普通用户只能用1024+的端口号 root用户任意
  2. listen的参数加一,表示的是establish队列的长度,也就是握手完了还没被accept的
  3. 主动断开的四次挥手,socket状态是time_wait, 一般是2分钟
  • 客户端主动断开,TIME_WAIT无所谓,因为客户端一般就一个socket,而且随机分配的,等就等吧
  • 服务器端主动断开,socket不会立即释放,会占用资源,表现为不能立即重启,会bind失败,2MSL后才能使用这个端口

解决方案:
image

TCP缓存

发送=向发送缓存中写入 接收=从接收缓存中读取

image

#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>

int main() {
    // 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        std::cerr << "Failed to create socket" << std::endl;
        return -1;
    }

    int buf_len;
    socklen_t optlen = sizeof(buf_len);
    // 获取发送缓存区大小
    getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &buf_len, &optlen);
    std::cout << "发送区缓存大小为:" << buf_len << std::endl;

    // 获取接收缓存区大小
    getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &buf_len, &optlen);
    std::cout << "接收区缓存大小为:" << buf_len << std::endl;

    // 关闭socket
    close(sockfd);

    return 0;
}

image

  • 如果我方发送缓冲区满了,或对方接收缓冲区满了,就会阻塞send()
  • 因为缓冲区的存在,所以客户端发送完关闭socket,服务端照样能收到
  • Negle算法定义:任意时刻只能有一小块未被确认的值,否则都大块MSS发送! 除此之外,还有个ack延迟机制,就是发出去包,等待40ms再发送,因为他想40ms内收到ack并连下一包一起发送!!

image

解决方案:

#include <netinet/tcp>
int opt = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));

IO复用

select模型

  1. 读事件:有新的客户端连上来、对端发送报文已到达、对端关闭了链接
  2. 写事件:可以向对端发送报文(缓冲区没满)
  3. 如果没有及时处理事件 也会留存到下一次select被调用 recv没接收完,也会继续下次自动触发事件!!!
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
using namespace std;

int main(int argc, char *argv[]) {
    if (argc != 2) {
        cout << "Usage: ./select port" << endl;
        cout << "Example: ./select 8080" << endl << endl;
        return -1;
    }
    
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        cout << "Error: socket() failed" << endl;
        return -1;
    }
    
    int opt = 1;
    unsigned int len = sizeof(opt);
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, len);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(atoi(argv[1]));
    
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        cout << "Error: bind() failed" << endl;
        close(listenfd);
        return -1;
    }

    if (listen(listenfd, 5) != 0) {
        cout << "Error: listen() failed" << endl;
        close(listenfd);
        return -1;
    }
    
    cout << "服务器已启动,等待客户端连接..." << endl;
    
    //select模型就是不直接accept的意思,select可以监视三种读事件:新客户端来、数据到、对端关
                                             //监视一种写事件:发送缓冲区不满 可以发
    // int clientfd = accept(listenfd, NULL, NULL);
    // if (clientfd < 0) {
    //     cout << "Error: accept() failed" << endl;
    //     return -1;
    // }

    fd_set readfds; //定义监视读事件的socket集合 是int[32] 大小为4*8*32 = 1024位
    FD_ZERO(&readfds);
    FD_SET(listenfd, &readfds);
    int maxfd = listenfd; //maxfd初始化为最小的
    
    while(true)
    {
        timeval tout;
        tout.tv_sec = 10;
        tout.tv_usec = 0; //表示十秒+0微秒
        fd_set tmpfds = readfds;

        int ret = select(maxfd + 1, &tmpfds, NULL, NULL, &tout);
        if (ret < 0){
            cout << "Error: select() failed" << endl;
            break;
        }
        if (ret == 0){
            cout << "TIME OUT!!!" << endl;
            continue;
        }
        // 成功监视到读事件!!ret 就是已发生事件的个数,tmpfds被修改,
        //比如一共有6个描述符,012345 除去012标准 345三个监视,如果5发生事件 就会把bitmap中34清空
        for(int eventfd = 0; eventfd <= maxfd; eventfd++)
        {
            //遍历到最大的maxfd,就可以找到哪个fd有事件!!
            if (FD_ISSET(eventfd, &tmpfds) == 0) continue;
            
            // 发生事件的是listenfd 那一定是新来了链接,accept即可
            if (eventfd == listenfd)
            {
                int clientfd = accept(listenfd, NULL, NULL);
                //accept不成功,继续处理下一个事件
                if (clientfd < 0) {
                    cout << "Error: accept() failed" << endl;
                    continue;
                }

                //更新readfs标志位,这就是为什么要用tmpfds,因为tmpfds会把listenfd设为1 不会管新来的链接
                FD_SET(clientfd, &readfds);
                if (maxfd < clientfd) maxfd = clientfd;
            }
            
            //不是listensock 那一定是收到数据或被断开连接
            
            else
            {
                char buffer[1024];
                memset(&buffer, 0, sizeof(buffer));
                //如果是断开事件
                if (recv(eventfd, buffer, sizeof(buffer), 0) <= 0)
                {
                    cout << "客户端fd: " << eventfd << "已断开连接" << endl;
                    FD_CLR(eventfd, &readfds);
                    close(eventfd);

                    //可能出现0 1 2 3 5 6 8, 目前是8要断开,接下来要更新maxfd=6
                    if (eventfd == maxfd)
                    {
                        for(int i = maxfd; i >= 3; i++)
                        {
                            if (FD_ISSET(i, &readfds) == 0) 
                            {
                                maxfd = i;
                                break;
                            }
                        }
                    }
                }
                //如果是接收到数据
                else
                {
                    cout << "收到:" << buffer << endl << endl;

                    memset(&buffer, 0, sizeof(buffer));
                    strcpy(buffer, "我收到啦");
                    if (send(eventfd, buffer, sizeof(buffer), 0) == -1)
                    {
                        cout << "发送失败" << endl;
                        continue;
                    }
                    cout << "发送回复报文啦!!" << endl << endl;
                }
            }
        }
    }
    return 0;
}

  1. 可以在客户端写一个循环发送数万次,然后写sh脚本多运行几个客户端程序(不要带cout 会影响性能) 测试发现大约7s处理一百万个事件

  2. sock增多 bitmap每次都要拷贝(代码) 然后把副本拷贝到内核态 效率较低

poll模型

  1. poll结构是数组,传入内核后换成链表
  2. 调用一次poll只拷贝一次结构体数组,比select强点
  3. 没有1024限制,但是因为也是遍历所以sock增多性能很差
#include <iostream>
#include <cstring>
#include <cstdio>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <poll.h>
using namespace std;
# define MAX_POLL 2048
int main(int argc, char *argv[]) {
    if (argc != 2) {
        cout << "Usage: ./select port" << endl;
        cout << "Example: ./select 8080" << endl << endl;
        return -1;
    }
    
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd < 0) {
        cout << "Error: socket() failed" << endl;
        return -1;
    }
    
    int opt = 1;
    unsigned int len = sizeof(opt);
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, len);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(atoi(argv[1]));
    
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        cout << "Error: bind() failed" << endl;
        close(listenfd);
        return -1;
    }

    if (listen(listenfd, 5) != 0) {
        cout << "Error: listen() failed" << endl;
        close(listenfd);
        return -1;
    }
    
    cout << "服务器已启动,等待客户端连接..." << endl;

    //定义pollfd数组 清空 设置监听窗口为结构体数组的第一个,后面依次++
    pollfd fds[MAX_POLL];
    for(int i = 0; i < MAX_POLL; i++)
    {
        fds[i].fd = -1;
    }
    
    int number = 0;
    fds[number].fd = listenfd;
    fds[number].events = POLLIN;
    number++;

    int maxnum = number;
    while(true)
    {
        int ret = poll(fds, maxnum, 2000); //10000ms = 10s
        if (ret == -1){
            cout << "Error: poll() failed" << endl;
            break;
        }
        if (ret == 0){
            cout << "TIME OUT!!!" << endl;
            continue;
        }
        // 成功监视到读事件!!ret 就是已发生事件的个数,
        for(int eventnum = 0; eventnum <= maxnum; eventnum++)
        {
            //遍历到最大的maxnum,就可以找到哪个num对应的fd有事件!!
            if (fds[eventnum].fd == -1) continue;
            // listenfd已经事先被绑定到结构体数组第一个了,只需检查它是否有事件
            if (fds[eventnum].fd == listenfd && fds[eventnum].revents&POLLIN)
            {
                int clientfd = accept(listenfd, NULL, NULL);
                //accept不成功,继续处理下一个事件
                if (clientfd < 0) {
                    cout << "Error: accept() failed" << endl;
                    continue;
                }

                fds[number].fd = clientfd;
                fds[number].events = POLLIN;
                number++;
                cout << "客户端fd: " << clientfd << " 已连接" << endl;
                // 这里比如结构体数组下标为 0 1 2 3 4 5 6 7 8 9,3是listenfd 6 8 是clientfd
                //                元素为\ 3 6 8
                maxnum = number;
            }
            
            //不是listensock 那一定是收到数据或被断开连接
            else if (fds[eventnum].fd!=listenfd && fds[eventnum].revents&POLLIN)
            {
                char buffer[1024];
                memset(&buffer, 0, sizeof(buffer));
                //如果是断开事件
                if (recv(fds[eventnum].fd, buffer, sizeof(buffer), 0) <= 0)
                {
                    cout << "客户端fd: " << fds[eventnum].fd << "已断开连接" << endl;
                    fds[eventnum].fd = -1;
                    close(fds[eventnum].fd);

                    //可能出现0 1 2 3 5 6 8,如果是6断开,就不用更新maxnum了,否则检测不到8!
                    if (eventnum == maxnum)
                    {
                        maxnum--;
                        break;
                    }
                }
                //如果是接收到数据
                else
                {
z
                    if (send(fds[eventnum].fd, buffer, sizeof(buffer), 0) == -1)
                    {
                        cout << "发送失败" << endl;
                        continue;
                    }
                    cout << "发送回复报文啦!!" << endl << endl;
                }
            }
        }  
    }
    return 0;
}

水平触发 边缘触发

通过epoll_event ev; ev.events = EPOLLIN|EPOLLET边缘触发写

  1. select poll只能水平触发
  2. epoll可以边缘触发 默认水平触发
  3. 水平触发:对于读/写事件 如果没读/写完,再次epoll_wait() 继续读写(缓冲区没满的清空下)
  • 边缘触发:epoll_wait触发后不管有没有读,新数据来之前都不会再次触发,写事件是当满变成不满 才再次触发
  1. 如果是边缘触发,会发现一个问题:如果正在accept时,又来一个连接,那么他可能不会触发,是因为不accept就不会改变缓冲区,就不会边缘触发,那他就没有事件通知,就被忽略了!!!!
  • 解决方案:把accept套在while(true)里 判断accept返回负数且errno=EAGAIN 表示队列中没有socket了 这时候break!!!

epoll

  1. epoll_event是结构体,成员是uint32_t eventsepoll_data_t data的联合体,所谓联合体就是只有一个成员生效,一般用fd,还有void* ptr、u32、u64

阻塞/非阻塞IO

  1. 阻塞指的是在进程/线程中,发起一个调用,调用返回前进/线程会被阻塞等待
  2. 非阻塞指的是,发起调用时立即返回
  3. 会阻塞的函数:accept(暂时无客户端来) connect(三次握手或失败) send(发送区满) recv(暂时无数据来)
  4. IO复用的模型,事件循环不能被阻塞 所以要采用非阻塞IO
  5. connect一般是客户端调用的,可以用fcntl函数设置非阻塞模式
int set_noblock(int sock){
  int flags = fcntl(sock, F_GETFL, 0);
  if(flags < 0){
    return -1;
  }
  return fcntl(sock, F_SETFL, flags|O_NONBLOCK);
}
  • 这样设置后,会导致不管成功还是失败都会返回fail,因为他不是立即返回的,而是会立即设置errno为EINPROGRESS 并退出
if (errno != EINPROGRESS) {
    perror("connect");
    exit(1);
}
  • 比比半天,不还是没法判断到底连上没有?别急,我们可以用poll/select/epoll等库函数,判断socket是否可写,例如:
pollfd fds;
fds.fd = sockfd;
fds.events = POLLOUT;
poll(&fds, 1, -1); // 调用poll函数

if (fds.revents != POLLOUT) {
    perror("connect failed");
    // 连接失败
} else if (fds.revents & POLLOUT) {
    // 连接成功
    // 可以开始发送或接收数据
}
  1. accept recv send 函数,设置非阻塞后 立即返回失败errno=EAGAIN

优化手段

  1. accept改为accept4,最后加一个SOCK_NONBLOCK属性
  2. int listenfd = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, 0);
  3. 成员变量通常返回void 省略不写!!! 但成员函数有返回值的话,一定要记得声明和实现一致!!差一个const也不行!!!
#include "InetAddr.h"

/*
class InetAddr
{
private:
    sockaddr_in addr_;
public:
    InetAddr(const char* ip, uint16_t port);    // 监听socket用这个构造函数
    InetAddr(const struct sockaddr_in& addr);   // 连接socket用这个构造函数
    ~InetAddr();

    const sockaddr*& sockAddr() const;          // 获取sockaddr*
    const char* ip() const;                     // 获取ip
    uint16_t port() const;                      // 获取port
};
*/
InetAddr::InetAddr(const char* ip, uint16_t port)
{
    bzero(&addr_, sizeof(addr_));
    addr_.sin_family = AF_INET;
    addr_.sin_port = htons(port);
    addr_.sin_addr.s_addr = inet_addr(ip);
}

InetAddr::InetAddr(const struct sockaddr_in& addr)
    : addr_(addr)
{
}
// 等价于函数体内直接赋值 addr_ = addr;



const sockaddr *InetAddr::sockAddr() const   // 返回addr_成员的地址,转换成了sockaddr。
{
    return (const sockaddr*)&addr_;
}


const char* InetAddr::ip() const
{
    return inet_ntoa(addr_.sin_addr);
}

uint16_t InetAddr::port() const
{
    return ntohs(addr_.sin_port);
}
  • 封装协议类、TCPsock类、epoll类,简化main函数,踩得坑:
  1. new左边必须是指针,比如Stu *s = new Stu("Jack");
  2. h文件声明时不要带{},不然被视为定义!!!
  3. epoll的data用void* 更方便!

回调函数

  1. include <functional.h>

  2. 常见用法:
class XXX{
    function<void()> cb_;             //回调函数是一个变量
    setcb(function<void()> cb)        //设置回调函数
    abc(int i)                        //假设一个成员函数是回调函数
}

.....

int i = 5;
XXX xxx;
xxx.setcb(std::bind(XXX::abc, xxx, &i))

posted @ 2024-03-12 09:02  __Zed  阅读(412)  评论(0)    收藏  举报