c++从零实现reactor高并发服务器!!!
环境准备
- linux虚拟机
- 安装升级c/c++编译器
- gcc/g++ 选项 源代码文件1 源代码文件2 ... 源代码文件n
- -o指定输出的文件名(不能和源文件同名 默认是a.out)
- -g调试 -On链接时优化 减小体积(n=1-3) -c只编译 用于生成库
- -std=c++11 支持c++11标准
- 安装man功能
- man 级别 接口/命令
- 级别: 1系统命令 2系统接口 3库函数 4设备文件 5文件 9内核
- 安装vscode c/c++插件 简体中文插件 Remote-ssh插件
基础知识
静态库动态库
g++ -c -o libxxx.a xxx.cpp
生成了libxxx.a的静态库
g++ -o demo demo.cpp -L/path/xxx -lxxx
-L指定路径 -l指定静态库名- 用静态库和用源代码是一样的,好处是可以隐藏源代码
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
- 增量编译,也就是说当前目录下有静态/动态了,就不编译这个了
- 用-I指定头文件包含路径
- g++前面是个tab,而不是八个空格
- main函数第三个是char* envp[] 打印出来效果如同env命令
- int setenv(const char* name, const char* value, int override) 环境变量名/值/是否替换 返回0成功-1失败(几乎不失败) 只对当前进程生效 进程终止下次就没有了,对shell无效
gdb调试
yum -y install gdb
安装- 编译时加-g 不要加-On
- 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 |
- 出现段错误时(操作空指针) 程序会被内核强行终止,保存在core文件中(需要先ulimit -a 查看 core file size ulimit -c unlimited更改后才能看到)
gdb demo core.123
调试core文件 bt查看函数调用栈- ps -ef|grep demo 查看进程号 gdb -p demo 123 会自动停止
linux
时间 <time.h>
- time_t
typedef long time_t
- 获取1970/1/1到现在的秒数
time_t now = time(0)
time_t now; time(&now)
- tm结构体
- 从time_t转tm结构体,注意加_r 线程安全
localtime_r(&now, &tmnow)
mktime(&tm)
把结构体转time_tgettimeofday(struct timeinterval* tv, struct timezone* tz)
获取1970/1/1到现在的秒数+当前的微秒数
- sleep(秒) usleep(微秒)
目录操作<unistd.h>
- 获取当前目录
char* getcwd(char* buf, size_t size)
char* get_current_dir_name()
- 相当于pwd,目录最大长度255 getcwd需要初始化一个256长度的字符数组,get_current_dir_name需要接free
切换目录
int chdir(const char*path)
创建目录
int mkdir(const char*pathname, mode_t mode)
- mode如0755,不要省略0
- 删除目录
int rmdir(const char*path)
<dirent.h> 读取目录相当于ls -a
DIR* opendir(const char* path); //打开目录
struct dirent*readdir(DIR* dirp); //读取目录
int closedir(DIR* dirp); //关闭目录
- 其中 d_type = 8 是文件,= 4 是子目录
a
- 判断文件是否有某个权限,有返回0 没有返回-1
int access(const char* path, int mode)
stat结构体,有很多成员,比ls列出的还多
int stat(const char*path, struct stat*buf)
修改目录或文件的时间
int utime(const char* path,const struct utimbuf* time)
rename库函数 相当于mv
int rename(const char* old, const char* new)
remove库函数 相当于rm
int remove(const char* path)
Linux系统错误 <errno.h>
获取错误代码的详细信息
char* strerror(int errnum)
int strerror_r(int errnum, char* buf, size_t buflen)
控制台显示最近一次系统错误的详细信息
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) 不然就只会处理一次咯!!
进程终止
- main函数中,return返回
- 任意函数调用exit, _exit() , Exit()
- exit()不会调用局部变量的析构,但是会调用全局变量的析构
- _exit() 和 Exit() 直接退出,不会进行任何操作
退出线程:pthread_exit() 线程主函数return
abort()异常终止、接收到信号、最后一个线程对取消请求做出响应
终止的状态就是main中 return 几
- exit(5) 可以把状态变成5 退出后,用echo $?查看
- 异常终止的话,状态非零
- 用atexit(func)注册退出函数,当正常退出/exit()退出,会调用,最多注册32个!!!
调用可执行程序
- system() 成功返回0或非零(正常退出,但是不等于退出状态),失败返回非零
- 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!!!很重要
创建进程
- linux的进程是树形结构 0号祖先创建1号systemd(内核初始化和系统配置) 2号kthreadd(线程调度管理)
- pstree查看
- fork()创建子进程,fork()后面的代码子进程父进程都执行,调用一次但返回两次,子进程的返回值是0 父进程是子进程的pid
- 所以写了pid_t i = fork() 后 通过判断返回值分别执行父子进程的代码
- 子进程是用的父进程副本,而不是共享,父子进程执行顺序不确定!!
- 父子进程共享文件偏移量,先后的写不会被覆盖
- vfork()创建一个新进程,且立即调用exec,可保证子进程先运行!!
僵尸进程
子进程结束,父进程没结束,所以就没处理子进程的信息,叫僵尸进程
- 父进程结束,子进程没结束就被1号进程托管,这其实是一种运行在后台的方式!!(原始的方式是./demo &)
if(fork()>0) return 0
- 这样可以让父进程退出,子进程挂给一号进程继续运行!!
- 僵尸进程危害:系统的可用pid有限,僵尸进程太多影响性能
- 避免僵尸进程:子进程退出,内核会通知父进程SIGCHLD,如果父进程忽略这个信号,表示“我早知道他要退出了” 那么子进程退出后立即释放!
signal(SIGCHLD, SIG_IGN)
- 也可以wait,waitpid()...
- 当然也可以选择捕获SIGCHLD信号,并在处理函数中wait() waitpid()
发送信号
- 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);
}
共享内存
共享内存没提供锁的机制,一边读一边写容易出错!!!
创建/获取(如已创建)共享内存
int shmget(key_t key, size_t size, int shmflg)
- key说白了是unsigned int 一般用十六进制
- size以字节为单位
- 访问权限,0666表示全部用户可读写 IPC_CREAT表示不存在时创建
- 成功返回共享内存id(正整数),失败(内存不足或无权限)返回-1
ipcs -m 查看 ipcrm -m xxx 手动删除
进程中使用共享内存
void* shmat(int shmid, const void* shmaddr, int shmflg)
- shmget() 返回的shmid
- 第二个填0,让系统选择在那块地址共享 第三个填0
- 然后就可以对返回的指针进行操作了,相当于在操作这个内存!!
- 把共享内存从当前进程里分离出去
shmdt(const void* shmaddr)
- 参数填shmat()返回值
- 所有进程都不用了 就删除共享内存
int shmctl(int id, int command, shmid_ds* buf)
- 要删除共享内存,第二个填IPC_RMID宏 第三个填0
- root用户创建的,任何普通用户都删不掉!!
- 不能用STL容器!!因为STL会在堆区动态分配内存,堆不属于共享内存!
循环队列
队列是一种实现共享内存的数据结构
- 循环队列,队头出 队尾进
- 循环队列用的数组实现,所以当满了之后,就要从第一个位置进入,并且把队尾指针指向第一个!!
- 也就是说,比如用数组长度n的模拟,尾指针索引是m,每次插入就是(m+1)%n
信号量
- P操作减一 V操作加一
- ipcs -s查看系统中的信号量 ipcrm sem id 删除信号量
网络编程
linux文件描述符
分配规则是找到最小的,没被占用的!!
- /proc/进程id/fd 存放了这个进程的所有文件描述符(open返回值)!!
- 一个进程默认存在三个,标准输入cin-0 标准输出cout-1 标准错误cerr-2 也就是0--键盘 1--显示器
- 可以用close(0/1/2)关闭标准输入输出错误等。。
- 文件和socket是一个东西,,所以socket也是文件描述符。。。
- send可以改成write!!! recv()可以改成read!!!
SOCKET
- 创建套接字
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,就是最大的文件描述符数量!!
- 数据占用大于一字节时,操作系统的存储方式就有两种:大端小端
- 比如存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字节
// 最终都是用网络序传输!!!
- 结构体们:
sockaddr结构体,bind和connect要用到
我们发现,用14字节表示端口和地址,我不会表示啊,所以定义了sockaddr_in结构体,可以强转为sockaddr
为啥不能直接用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
- netstat -na 表示 列出数字形式的ip port 、全部 网络连接
- netstat -natu 可以只显示tcp udp 不显示那一堆烦人的本地socket
- bind,普通用户只能用1024+的端口号 root用户任意
- listen的参数加一,表示的是establish队列的长度,也就是握手完了还没被accept的
- 主动断开的四次挥手,socket状态是time_wait, 一般是2分钟
- 客户端主动断开,TIME_WAIT无所谓,因为客户端一般就一个socket,而且随机分配的,等就等吧
- 服务器端主动断开,socket不会立即释放,会占用资源,表现为不能立即重启,会bind失败,2MSL后才能使用这个端口
解决方案:
TCP缓存
发送=向发送缓存中写入 接收=从接收缓存中读取
#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;
}
- 如果我方发送缓冲区满了,或对方接收缓冲区满了,就会阻塞send()
- 因为缓冲区的存在,所以客户端发送完关闭socket,服务端照样能收到
- Negle算法定义:任意时刻只能有一小块未被确认的值,否则都大块MSS发送! 除此之外,还有个ack延迟机制,就是发出去包,等待40ms再发送,因为他想40ms内收到ack并连下一包一起发送!!
解决方案:
#include <netinet/tcp>
int opt = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
IO复用
select模型
- 读事件:有新的客户端连上来、对端发送报文已到达、对端关闭了链接
- 写事件:可以向对端发送报文(缓冲区没满)
- 如果没有及时处理事件 也会留存到下一次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;
}
可以在客户端写一个循环发送数万次,然后写sh脚本多运行几个客户端程序(不要带cout 会影响性能) 测试发现大约7s处理一百万个事件
sock增多 bitmap每次都要拷贝(代码) 然后把副本拷贝到内核态 效率较低
poll模型
- poll结构是数组,传入内核后换成链表
- 调用一次poll只拷贝一次结构体数组,比select强点
- 没有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边缘触发写
- select poll只能水平触发
- epoll可以边缘触发 默认水平触发
- 水平触发:对于读/写事件 如果没读/写完,再次epoll_wait() 继续读写(缓冲区没满的清空下)
- 边缘触发:epoll_wait触发后不管有没有读,新数据来之前都不会再次触发,写事件是当满变成不满 才再次触发
- 如果是边缘触发,会发现一个问题:如果正在accept时,又来一个连接,那么他可能不会触发,是因为不accept就不会改变缓冲区,就不会边缘触发,那他就没有事件通知,就被忽略了!!!!
- 解决方案:把accept套在while(true)里 判断accept返回负数且errno=EAGAIN 表示队列中没有socket了 这时候break!!!
epoll
- epoll_event是结构体,成员是
uint32_t events
和epoll_data_t data
的联合体,所谓联合体就是只有一个成员生效,一般用fd,还有void* ptr、u32、u64
阻塞/非阻塞IO
- 阻塞指的是在进程/线程中,发起一个调用,调用返回前进/线程会被阻塞等待
- 非阻塞指的是,发起调用时立即返回
- 会阻塞的函数:accept(暂时无客户端来) connect(三次握手或失败) send(发送区满) recv(暂时无数据来)
- IO复用的模型,事件循环不能被阻塞 所以要采用非阻塞IO
- 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) {
// 连接成功
// 可以开始发送或接收数据
}
- accept recv send 函数,设置非阻塞后 立即返回失败errno=EAGAIN
优化手段
- accept改为accept4,最后加一个SOCK_NONBLOCK属性
int listenfd = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK, 0);
- 成员变量通常返回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函数,踩得坑:
- new左边必须是指针,比如Stu *s = new Stu("Jack");
- h文件声明时不要带{},不然被视为定义!!!
- epoll的data用void* 更方便!
回调函数
include <functional.h>
- 常见用法:
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))
c++从零实现reactor高并发服务器!!!的更多相关文章
- PHP写的异步高并发服务器,基于libevent
PHP写的异步高并发服务器,基于libevent 博客分类: PHP PHPFPSocketLinuxQQ 本文章于2013年11月修改. swoole已使用C重写作为PHP扩展来运行.项目地址:h ...
- Linux + C + Epoll实现高并发服务器(线程池 + 数据库连接池)(转)
转自:http://blog.csdn.net/wuyuxing24/article/details/48758927 一, 背景 先说下我要实现的功能,server端一直在linux平台下面跑,当客 ...
- JAVA NIO non-blocking模式实现高并发服务器(转)
原文链接:JAVA NIO non-blocking模式实现高并发服务器 Java自1.4以后,加入了新IO特性,NIO. 号称new IO. NIO带来了non-blocking特性. 这篇文章主要 ...
- 高并发服务器建议调小 TCP 协议的 time_wait 超时时间。
1. [推荐]高并发服务器建议调小 TCP 协议的 time_wait 超时时间. 说明:操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服 务器端会因为处于 ...
- 第15章 高并发服务器编程(2)_I/O多路复用
3. I/O多路复用:select函数 3.1 I/O多路复用简介 (1)通信领域的时分多路复用 (2)I/O多路复用(I/O multiplexing) ①同一线程,通过“拨开关”方式,来同时处理多 ...
- 第15章 高并发服务器编程(1)_非阻塞I/O模型
1. 高性能I/O (1)通常,recv函数没有数据可用时会阻塞等待.同样,当socket发送缓冲区没有足够多空间来发送消息时,函数send会阻塞. (2)当socket在非阻塞模式下,这些函数不会阻 ...
- JAVA NIO non-blocking模式实现高并发服务器
JAVA NIO non-blocking模式实现高并发服务器 分类: JAVA NIO2014-04-14 11:12 1912人阅读 评论(0) 收藏 举报 目录(?)[+] Java自1.4以后 ...
- 为一个支持GPRS的硬件设备搭建一台高并发服务器用什么开发比较容易?
高并发服务器开发,硬件socket发送数据至服务器,服务器对数据进行判断,需要实现心跳以保持长连接. 同时还要接收另外一台服务器的消支付成功消息,接收到消息后控制硬件执行操作. 查了一些资料,java ...
- linux学习之多高并发服务器篇(一)
高并发服务器 高并发服务器 并发服务器开发 1.多进程并发服务器 使用多进程并发服务器时要考虑以下几点: 父最大文件描述个数(父进程中需要close关闭accept返回的新文件描述符) 系统内创建进程 ...
- linux学习之高并发服务器篇(二)
高并发服务器 1.线程池并发服务器 两种模型: 预先创建阻塞于accept多线程,使用互斥锁上锁保护accept(减少了每次创建线程的开销) 预先创建多线程,由主线程调用accept 线程池 3.多路 ...
随机推荐
- centos 6.4下fdisk分区、格式化、挂载新硬盘
centos 6.4下fdisk分区.格式化.挂载新硬盘 作者: cat 日期: 2013 年 9 月 10 日 发表评论 (0) 查看评论 1.# fdisk -l 查看当前磁盘信息,就会发现最下面 ...
- spring cloud 学习笔记 客户端(本地)均衡负载(三)
前言 在微服务中,一个服务可能即是服务端也是客户端,当别的服务调用该服务的时候这个服务就是服务端,当这个服务主动调用另外一个服务的时候,那么就是服务端. 作为客户端通过服务注册与发现获取某个服务的注册 ...
- JavaScript中的变量提升本质
JavaScript中奇怪的一点是你可以在变量和函数声明之前使用它们.就好像是变量声明和函数声明被提升了代码的顶部一样. sayHi() // Hi there! function sayHi() { ...
- 使用Skyline 新型UI管理OpenStack技术方案
使用Skyline 新型UI管理OpenStack [摘要] Skyline 是一个经过 UI 和 UE 优化过的 OpenStack 仪表盘,支持 OpenStack Train 及以上版本.Sky ...
- 《c#高级编程》第4章C#4.0中的更改(七)——命名参数和可选参数
一.概念 C#中的命名参数和可选参数是两种函数参数的特殊形式,它们可以提高代码的可读性和灵活性. 命名参数 命名参数允许我们在调用函数时指定参数名称,从而不必按照函数定义时的参数顺序进行传参.这样做可 ...
- 他来了他来了,.net开源智能家居之苹果HomeKit的c#原生sdk【Homekit.Net】1.0.0发布,快来打造你的私人智能家居吧
背景介绍 hi 大家好,我是三合,作为一个非著名懒人,每天上完班回到家,瘫在沙发上一动都不想动,去开个灯我都嫌累,此时,智能家居拯救了我,只需要在手机点点点,开关灯,空调,窗帘就都搞定了,一开始我用的 ...
- 【笔记】oracle INTERSECT指令&邮箱的正则匹配&trim()函数
[笔记]oracle INTERSECT 和 UNION 指令类似, INTERSECT 也是对两个 SQL 语句所产生的结果做处理的. 不同的地方是, UNION 基本上是一个 OR (如果这个值存 ...
- 力扣142(Java)-环形链表Ⅱ(中等)
题目: 给定一个链表的头节点 head ,返回链表开始入环的第一个节点. 如果链表无环,则返回 null. 如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环. 为了表示 ...
- 牛客网-SQL专项训练21
①Mysql中表student_info(id,name,birth,sex),字段类型都是varchar,插入如下记录:('1014' , '张三' , '2002-01-06' , '男'); S ...
- HarmonyOS NEXT应用开发之MpChart图表实现案例
介绍 MpChart是一个包含各种类型图表的图表库,主要用于业务数据汇总,例如销售数据走势图,股价走势图等场景中使用,方便开发者快速实现图表UI.本示例主要介绍如何使用三方库MpChart实现柱状图U ...