本文是牛客网Linux 高并发服务器开发视频教程的笔记
1、预备知识
1.1 Linux与远程
使用ssh在widows中控制Linux系统,使用vscode控制代码
使用g++编译
1.1 静态库与动态库
静态库与动态库的制作、区别
1.2 makefile
makefile文件操作就是指定所有源文件的编译顺序,因为一个正式的项目会有很多很多源文件,不可能一个个的手动g++,而且各种文件的编译可能还有先后关系,所有要有一个专门的东西来确定编译顺序和编译关系,这个命令就是make。makefile文件
1.3 GDB调试工具
编译时 -g
1.4 文件的io
标准c库io的实现是调用Linux本身的文件操作方式,这种涉及到内存与硬盘的交互的,在不同的平台都可以使用标准c库实现,但是c库回去调用不同平台对应的方法。c库对于文件操作是有缓冲区的存在的,对文件的操作会先对缓冲区操作,最后一下写进磁盘,因此一定要特别注意flush命令的使用
1.5 文件的创建与打开、读写
使用open()打开文件,如果打开成功则返回文件描述符,如果打开失败则返回最近的错误码errno,errno是linux内置的错误号。可以通过perror查看erron对应的错误描述。
使用open()也可以创建一个新的文件,在open函数中多一个O_CREAT参数即可
当一个文件被open函数打开后可以通过read和write函数进行读写操作
下面的代码是将一个文件中的内容复制到一个本来不存在的文件中:
#include<unistd.h>
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
using namespace std;
int main()
{
//1、读取待复制的文件
int srcfd=open("/home/lanpangzi/Desktop/Linux/lession02/test01.cpp",O_RDONLY);
if(srcfd==-1){
perror("open");
return -1;
}
printf("文件打开成功\n");
//2、创建一个待粘贴的新的空文件,用来写入
int targetfd= open("cpy.cpp",O_WRONLY|O_CREAT,0664);
if(targetfd==-1){
perror("open");
return -1;
}
printf("文件创建成功\n");
//3、循环读写将文件完全拷贝进去
char buff[1024]{};
int read_re = 0;
while((read_re=read(srcfd,buff,sizeof(buff)))>0){
write(targetfd, buff,read_re);
}
printf("文件复制成功\n");
//4、关闭所有打开的文件
close(srcfd);
close(targetfd);
printf("文件关闭成功\n");
return 0;
}
2、 Linux 多进程开发
2.1 创建和使用子进程
子进程注意需要使用wait()进行回收,否则容易产生僵尸进程
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
int main(){
printf("公共段1\n"); //这一句只会执行两遍
// 创建子进程
pid_t n_pid = fork(); // fork之后的语句父子进程都会执行
printf("公共段2\n"); //这一句会执行两遍
// 原来的父进程和新创建的子进程pid不同,通过判断n_pid来判断当前是父进程还是子进程
if(n_pid>0){
printf("当前是父进程!\n");
printf("当前进程的pid: %d, 当前进程的父进程pid: %d\n", getpid(),getppid());
}
else if(n_pid==0){
printf("当前是子进程!\n");
printf("当前进程的pid: %d, 当前进程的父进程pid: %d\n", getpid(),getppid());
}
printf("卧槽真牛逼!\n"); //这一句会执行两遍
return 0;
}
2.2 GDB多进程调试
GDB只能跟踪一个进程,但是可以在fork之前设置GDB调试工具是跟踪父进程还是跟踪子进程,以及在跟踪这个进程时另一个进程的状态
2.3 调用其他程序
使用execl() 函数可以直接跳出当前程序直接去执行其他的程序,exec函数族是几个长得长不多的函数,功能也都是调用一个程序,只是参数传递方式或环境变量设置不同
2.4 进程间的通信
2.4.1 使用匿名pipe管道进行通信
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
// 创建管道标识,用于接收但会的两个文件描述符
// pipefd[0]->读取pipefd[1]->写入
int pipefd[2];
// 创建管道,返回值用来判断是否创建成功
int ret = pipe(pipefd);
if(ret==-1){
perror("pipe");
exit(0);
}
pid_t pid = fork();
if(pid>0){
// 父进程,向管道中写入信息
char * str = "我是父进程写入的信息";
char buf[1024]={0};
while (1)
{
read(pipefd[0],buf,sizeof(buf));
printf("这里是父进程,读取的信息为:%s\n",buf);
write(pipefd[1],str,strlen(str));
printf("这里是父进程, pid:%d,正在写入文件\n", getpid());
sleep(1);
}
}
else{
// 子进程
// 从管道中读取信息
char * str = "我是子进程写入的信息";
char buf[1024]={0};
while (1)
{
write(pipefd[1],str,strlen(str));
printf("这里是子进程, pid:%d,正在写入文件\n", getpid());
sleep(1);
read(pipefd[0],buf,sizeof(buf));
printf("这里是子进程,读取的信息为:%s\n",buf);
}
}
return 0;
}
2.4.2 使用有名管道 FIFO进行通信
写端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main()
{
// 创建之前需要先判断文件是否已经存在
int ret = access("fifo1",F_OK);
if(ret==-1){
// 如果不存在则创建管道
ret = mkfifo("fifo1",0664);
if (ret==-1){
perror("fifo");
exit(0);
}
}
//打开管道准备写入
int fd=open("fifo1",O_WRONLY);
if(fd==-1){
perror("open");
exit(0);
}
for(int i=0;i<100;i++){
char buf[1024];
sprintf(buf, "hello, %d\n", i);
printf("write data : %s\n", buf);
write(fd, buf, strlen(buf));
}
close(fd);
return 0;
}
读端:
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
int main()
{
int fd = open("fifo1",O_RDONLY);
if(fd==-1){
perror("read");
exit(0);
}
while(1){
char buf[1024]={0};
int len = read(fd,buf,sizeof(buf));
if(len==0){
printf("写端断开连接\n");
break;
}
printf("read buf: %s\n", buf);
}
return 0;
}
2.4.3 使用内存映射进行通信
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t length);
内存映射就是将磁盘中的文件映射到内存中,直接进行修改,效率比较高
2.4.4 使用信号通信
和QT那个SIGNAL差不多,就是告诉一个进程发生了什么事,也称为软件中断,信号是由内核执行的,信号使用之前需要先注册一下,这样当信号发生时可以执行对应的函数。
信号集是一个存储一组信号的数据结构,可以使用一系列的函数来操作信号集
2.4.5 共享内存通信
多个进程通过唯一的内存标识符访问同一块内存,可以通过ftok()根据key生成标识符,然后通过这个标识符创建或者访问某一块共享内存。
2.4.6 守护进程
了解守护进程之前先需要了解 终端、进程组(一堆进程)、会话(一堆进程组)。守护进程就是一个一直在后台运行的进程,断掉了它与控制台一切联系,就连输入输出都是重定向到指定的文件的。
3、Linux多线程开发
子线程就像是在主线程里面运行一个新的函数,但是这个函数的执行不会阻塞主线程。由于和主线程是一个cpp文件,所以它们的变量啥的都是共享的,如果主线程是由pthread_exit()退出主线程的话,不会影响其他正常运行的线程。
其他的术语:线程分离(pthread_detach())、线程取消(pthread_cancel())、线程属性(pthread_attr_***)、线程同步(), 互斥锁、死锁、读写锁、生产者消费者模型、信号量(sem_t)
4、网络编程
4.1 网络结构模式
C/S, B/S
4.2 MAC地址、IP地址、端口
4.3 网络模型
4.3.1 七层网络模型
OSI
4.3.1 TCP/IP四层模型
常见的协议 (protocol)
应用层协议:FTP, HTTP, NFS
传输层协议:TCP, UDP
网络层协议:IP, ICMP, IGMP
网络接口层协议:ARP, RARP
协议最终体现在传输的数据的格式
4.4 socket
socket(套接字),直译为插座,在linux中用于表示进程间进行网络通信的特殊文件类型,也就是说他是一个文件,用来实现进程间的通信,但是进行通信的进程在不同的主机中,主机之间通过网络连接。
4.5 字节序
就跟阅读文字一样,计算机处理数据的单位是字节,就像是一个个的字,那么处理的时候是从左往右处理还是从右往左处理呢?这个就是字节序,一个字节就没有字节序的说法。字节序有两种,大端字节序(Big-Endian)和小端字节序(Little-Endian),不同的计算机可能采用不同的字节序,因此当收到文件后需要判断字节序与本机字节序是否相同。
注意,高位字节序低位字节序是相对与一个字节来说的,所以判断的时候不是看数的增长方向,而是一个int型变量中不同位置放的是什么,另外可以借助数组中索引越小内存地址越小的特性。
union
{
short value;
char bytes[sizeof(short)];
} test;
test.value=0x0102;// 01是这个数的高位,02是这个数的低位
// 01(高位)在内存中地址更小,所以是大端字节序,整数的高位在内存的低处
if(test.bytes[0]==1 && test.bytes[1]==2) printf("大端字节序\n");
// 01(高位)在内存中地址更大,所以是小端字节序,整数的高位在内存的高处
else if (test.bytes[0]==2 && test.bytes[1]==1) printf("小端字节序\n");
为了防止字节序不同导致的问题,将数据上传到网络之前统统转换为大端字节序,然后进入网络,这样接收方每次接收到文件时都是大端的,然后根据自己的字节序判断是否需要转换
4.6 socket地址
socket地址封装了ip和端口,方便进行通信
4.7 TCP通信流程
4.8 本地创建服务器和客户端进行通信
一个可以双向通信的本地server和client,但是只能一人一句,而且第一句话必须由client发起
server.cpp
// TCP通信服务器端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include<iostream>
int main(){
// 1、创建监听的socket
int lfd = socket(AF_INET, SOCK_STREAM,0);
if (lfd==-1){
perror("socket");
exit(-1);
}
// 2、绑定本地的IP和端口,为了统一ip和端口的格式,将其封装在 sockaddr_in类的结构体中
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
// ip可以是任意的,这样这个主机才可以接受到来自任意ip的客户端的连接
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port=htons(9999); // 端口为9999,通过htons转换为网络字节序
int bind_ret = bind(lfd, (sockaddr *)&saddr, sizeof(saddr));
if (bind_ret==-1){
perror("bind");
exit(-1);
}
// 3、监听,监听这个socket上有无连接进来,8是未连接和已连接的客户端数量之和的最大值
// 相当于开启一个后开进程,不会阻塞当前程序
int listen_ret=listen(lfd,8);
if(listen_ret==-1){
perror("listen");
exit(-1);
}
// 4、接收客户端连接
struct sockaddr_in clientaddr; // 定义客户端的地址,将来接收到客户端相关信息后放到这个里面
socklen_t len=sizeof(clientaddr);
// accept是阻塞函数,直到有客户端连接上,否则一直阻塞
int cfd = accept(lfd,(struct sockaddr*)&clientaddr, &len);
if(cfd==-1){
perror("accept");
exit(-1);
}
// 输出客户端信息
char client_ip[16];
// 将字符串转换为可以192.168.1.1 的字符串形式
inet_ntop(AF_INET, &clientaddr.sin_addr.s_addr, client_ip, sizeof(client_ip));
unsigned short clientport = ntohs(clientaddr.sin_port);
printf("connected! client ip is %s, port is %d\n", client_ip, clientport);
while(1){
// 5、获取客户端的数据并发送数据
char rec_buf[1024]={0};
// read是阻塞函数,连接上之后accept继续往下走,但是如果客户端没有信息发送过来read会阻塞住
int read_ret = read(cfd, rec_buf, sizeof(rec_buf));
if (read_ret==-1){
perror("read");
exit(-1);
} else if (read_ret>0){
printf("cliend: %s", rec_buf);
}else if(read_ret==0){
printf("client is closed!\n");
break;
}
// 给客户端发送数据
char data[1024];
fgets(data, sizeof(data),stdin);
write(cfd, data,strlen(data));
}
// 6、关闭所有打开的文件描述符
close(cfd);
close(lfd);
return 0;
}
client.cpp
// TCP通信客户端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include<iostream>
int main(){
// 1、创建socket,
int fd = socket(AF_INET, SOCK_STREAM,0);
if (fd==-1){
perror("socket");
exit(-1);
}
// 2、设置服务器的 IP 192.168.18.128和端口 9999,这样客户端才知道要去哪里找
struct sockaddr_in serveraddr;
serveraddr.sin_family = AF_INET;
inet_pton(AF_INET, "192.168.18.128",&serveraddr.sin_addr.s_addr);
serveraddr.sin_port = htons(9998);
// 3、连接服务器
int con_ret = connect(fd, (struct sockaddr*)&serveraddr,sizeof(serveraddr));
if (con_ret==-1){
perror("connect");
exit(-1);
}
// 如果进行到这里说明客户端已经成功连接到了主机
printf("connected!\n");
while(1){
// 4、向服务器端发送数据,客户端是先发送send,再接受recv
char data[1024];
fgets(data, sizeof(data),stdin);
write(fd,data,strlen(data));// 写进去了socket会自动发送
// 5、获取服务器发送回来的数据
char recv_buf[1024]={0};
int read_ret = read(fd, recv_buf, sizeof(recv_buf));// 返回的是读取到的文件的长度
if (read_ret==-1){
perror("read");
exit(-1);
} else if (read_ret>0){
printf("server: %s", recv_buf);
}else if(read_ret==0){
printf("server is closed!\n");
break;
}
}
// 6、关闭所有打开的文件描述符
close(fd);
return 0;
}
4.9 使用多进程实现并发
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<signal.h>
#include<wait.h>
#include<error.h>
void recyleChild(int arg){
while(1){
int wait_ret = waitpid(-1,NULL,WNOHANG);
if(wait_ret==-1){
// 所有在子进程都已回收完毕,没有还活着的了
break;
}
else if(wait_ret==0){
// 所有能回收的子进程都已经回收了,没有需要回收的了,但是还有子进程还活着
break;
} else if(wait_ret>0){
// 有子进程被回收了
printf("子进程%d被回收了\n",wait_ret);
}
}
}
int main(){
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = recyleChild;
// 注册信号捕捉,用来回收结束了的子进程
// 如果子进程exit会向父进程发送一个SIGCHLD信号,这个注册的函数act就是用来处理这个信号的
sigaction(SIGCHLD,&act,NULL);
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd==-1){
perror("socket");
exit(-1
);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port=htons(9999);
int bind_ret = bind(lfd,(sockaddr *) &saddr,sizeof(saddr));
if(bind_ret==-1){
perror("bind");
exit(-1);
}
// 开启监听的开关,不会阻塞,就相当于打开了一个开关
int listen_ret = listen(lfd,8);
if(listen_ret==-1){
perror("listen");
exit(-1);
}
// 将accept放在循环里面,每一次连接都创建一个子进程来进行通讯
while(1){
struct sockaddr_in clientaddr;
socklen_t clientaddr_len = sizeof(clientaddr);
int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &clientaddr_len);
if(cfd==-1){
// 如果accept被信号打断会报EINTR错误,直接continue即可
// 因为这个程序会需要使用信号来回收结束的子进程,但是子进程会打断accept的阻塞状态
if(errno==EINTR){
continue;
}
perror("accept");
exit(-1);
}
// 有连接到的客户端,创建一个新的子进程进行通讯
pid_t pid=fork();
if(pid==0){
// 进入子进程
printf("服务器接收到客户端连接,并创建了我这个子进程,我的pid: %d\n", getpid());
// 输出客户端信息,并进行通讯
char client_ip[16];
inet_ntop(AF_INET,&clientaddr.sin_addr,client_ip,sizeof(client_ip));
unsigned short clientport = ntohs(clientaddr.sin_port);
printf("connected! client ip is %s, port is %d\n", client_ip,clientport);
// 开始进行通讯
while(1){
// 获取客户端的数据并发送数据
char rec_buf[1024]={0};
// read是阻塞函数只有当cfd中有数据过来时才会继续,否则while直接循环起飞了
int read_ret = read(cfd, rec_buf, sizeof(rec_buf));
if (read_ret==-1){
perror("read");
exit(-1);
} else if (read_ret>0){
printf("cliend: %s", rec_buf);
}else if(read_ret==0){
printf("client is closed!\n");
break;
}
// 给客户端发送数据
char data[1024];
fgets(data, sizeof(data),stdin);
write(cfd, data,strlen(data));
}
close(cfd);
exit(0);// 退出当前子进程,如果不进行其他的操作这个子进程的资源就无法回收了
}
}
close(lfd);// 关闭服务器socket
return 0;
}
4.10 使用多线程实现并发
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<pthread.h>
#include<thread>
struct sockInfo
{
int fd;
pthread_t tid;
struct sockaddr_in addr;
};
// 为了能让working函数使用那些局部变量,将这个局部变量统统存放在这个数组中
// 128表示最多连接128个客户端,并将它们的数据存在在这个数组中
struct sockInfo sockinfos[128];
void* working(void * arg){
// 进入子线程
// 子线程函数,用于和客户端通信,需要传入相关的参数
struct sockInfo * pinfo=(struct sockInfo *) arg;
std::cout<<"服务器接收到客户端连接,并创建了我这个子线程,我的id:"<<std::this_thread::get_id()<<std::endl;
// 输出客户端信息,并进行通讯
char client_ip[16];
inet_ntop(AF_INET,&pinfo->addr.sin_addr,client_ip,sizeof(client_ip));
unsigned short clientport = ntohs(pinfo->addr.sin_port);
printf("connected! client ip is %s, port is %d\n", client_ip,clientport);
// 开始进行通讯
while(1){
// 获取客户端的数据并发送数据
char rec_buf[1024]={0};
// read是阻塞函数只有当cfd中有数据过来时才会继续,否则while直接循环起飞了
int read_ret = read(pinfo->fd, rec_buf, sizeof(rec_buf));
if (read_ret==-1){
perror("read");
exit(-1);
} else if (read_ret>0){
printf("cliend: %s", rec_buf);
}else if(read_ret==0){
printf("client is closed!\n");
break;
}
// 给客户端发送数据
char data[1024];
fgets(data, sizeof(data),stdin);
write(pinfo->fd, data,strlen(data));
}
close(pinfo->fd);
pinfo->fd=-1;
return NULL;
}
int main(){
int lfd = socket(AF_INET,SOCK_STREAM,0);
if(lfd==-1){
perror("socket");
exit(-1
);
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port=htons(9999);
int bind_ret = bind(lfd,(sockaddr *) &saddr,sizeof(saddr));
if(bind_ret==-1){
perror("bind");
exit(-1);
}
// 开启监听的开关,不会阻塞,就相当于打开了一个开关
int listen_ret = listen(lfd,8);
if(listen_ret==-1){
perror("listen");
exit(-1);
}
// 初始化数据,每一个,addr初始化成0,fd和tid初始化成-1
int max = sizeof(sockinfos)/ sizeof(sockinfos[0]);
for(int i=0;i<max;i++){
bzero(&sockinfos[i],sizeof(sockinfos[i]));
sockinfos[i].fd=-1;
sockinfos[i].tid=-1;
}
while(1){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int cfd=accept(lfd, (struct sockaddr*)&client_addr, &len);
// 寻找一个全部变量去放接受到的客户端的相关信息
struct sockInfo *pinfo;
for(int i=0;i<max;i++){
if(sockinfos[i].fd==-1){
pinfo=&sockinfos[i];
break;
}
if(i==max-1){
// 128个位置全部满了,等待一秒再从头开始
sleep(1);
i=-1;// for这个循环体结束后会自动+1
}
}
// 这个时候pinfo里面对应的就是一个全局的变量的
pinfo->fd=cfd;
memcpy(&pinfo->addr, &client_addr,len);// 结构体不能直接相等,要使用memcpy拷贝内存
// 创建子线程用于通信
// 有个问题,创建子线程的时候传入的参数是定义在这个while函数中的,但是working函数是在main函数之外的
// 因此working函数并不能使用这些参数,因为working函数执行的时候这个主线程的这个while已经结束了,这些
// 局部变量全部都已经释放了
pthread_create(&pinfo->tid,NULL,working,pinfo);
// 设置线程分离,再不阻塞的情况下实现线程的自动回收
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
4.11 TCP状态转换
三次握手建立连接、数据传输、四次挥手断开连接
4.12 半关闭、端口复用
半关闭可以用来进行单向传输,可以使用函数 int shutdown()
;实现
端口复用可以通过setsockpt
函数来实现,端口复用可以解决当前程序要使用的端口仍然被其他程序占用的问题,就是可以让大家同时使用一个端口进行通讯
4.13 I/O多路复用(I/O多路转接)
I/O输入输出指的是对内存的操作
I/O多路复用使得程序可以同时监听多个文件描述符,能够提高程序的性能,Linux中实现多路复用的方式主要有select、epoll、和poll
4.13.1 select的使用
server.cpp
#include<iostream>
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
#include<sys/types.h>
#include<sys/select.h>
int main(){
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
int bind_ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(bind_ret==-1){
perror("bind");
exit(-1);
}
int listen_ret = listen(lfd,8);
if(listen_ret==-1){
perror("listen");
exit(-1);
}
// 创建fd_set,用来存放要监测的文件描述符
fd_set rdset,tmp; // 一个用来保存原来的状态,一个用来送给内核
FD_ZERO(&rdset);
FD_SET(lfd,&rdset);
int maxfd=lfd;
while(1){
tmp=rdset;
// 调用select,让内核来检测哪些文件描述符有数据
int select_ret = select(maxfd+1,&tmp,NULL,NULL,NULL);
if(select_ret==-1){
perror("select");
exit(-1);
}else if(select_ret==0){
continue;
}else if(select_ret>0){
// 有select_ret个文件描述符产生了变化
// 这个if用来判断有没有新的连接产生,如果有则创建新的cfd用于通信
if (FD_ISSET(lfd,&tmp)){
// lfd这个文件描述符专门用来检测是否有新的客户端链接进来了,如果有
// lfd就会发生变化,但是lfd并不进行后续的通信,通信交给下面创建的cfd
printf("client connected!\n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd=accept(lfd,(struct sockaddr*)&clientaddr,&len);
FD_SET(cfd,&rdset); // 将新的文件描述符添加进集合中
FD_SET(cfd,&tmp);
maxfd = maxfd>cfd?maxfd:cfd;
}
// 遍历所有的fd,从lfd+1开始,因为lfd只是用来判断是否有新的连接,不用来进行通信
printf("start detect fd\n");
for(int i=lfd+1;i<=maxfd;++i){
printf("now fd is:%d\n",i);
if(FD_ISSET(i,&tmp)){
// 进了这个if说明文件描述符i发生了变化,可以进行通讯
char buf[1024]={0};
// read会阻塞住,直到client向server发送数据
int len =read(i,buf,sizeof(buf));
if(len==-1){
perror("read");
exit(-1);
}else if(len==0){
printf("client closed!\n");
close(i);
FD_CLR(i,&rdset);
}else if(len>0){
printf("read buf: %s\n",buf);
write(i,buf,strlen(buf)+1); // 回射
}
}
}
}
}
close(lfd);
return 0;
}
4.13.2 poll的使用
poll与select相比,好像只解决了1024个的个数问题,其他的问题比如遍历、从用户态到内核态的转换都还是存在的。poll只是比select更灵活更容易管理
#include<iostream>
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
#include<sys/types.h>
#include<sys/poll.h>
int main(){
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
int bind_ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(bind_ret==-1){
perror("bind");
exit(-1);
}
int listen_ret = listen(lfd,8);
if(listen_ret==-1){
perror("listen");
exit(-1);
}
// 定义一个存放所有pollfd的数组,这个数组和select中的文件描述符不同的是我可以设置的很大,可以超过1024
struct pollfd pfds[2048];
// 初始化
for(int i=0;i<2048;++i){
pfds[i].fd=-1; // 初始化为-1表示还没有监听
pfds[i].events=POLLIN; // 默认监听读时间
}
pfds[0].fd=lfd; // 将最开始的监听描述符放进去,这玩意仅仅用来监听是否有连接进来,不用于通信
int nfds = 0; // 和select一样,需要记录最大的那个文件描述符的索引
while(1){
// 调用poll,让内核来检测哪些文件描述符有数据
int poll_rect = poll(pfds, nfds+1, -1);
if(poll_rect==-1){
perror("poll");
exit(-1);
}else if(poll_rect==0){
continue;
}else if(poll_rect>0){
// 有poll_ret个文件描述符产生了变化
// 这个if用来判断有没有新的连接产生,如果有则创建新的cfd用于通信
// 注意是将监听描述符与检测的状态进行与运算
if (pfds[0].revents&POLLIN){
// lfd这个文件描述符专门用来检测是否有新的客户端链接进来了,如果有
// lfd就会发生变化,但是lfd并不进行后续的通信,通信交给下面创建的cfd
printf("client connected!\n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd=accept(lfd,(struct sockaddr*)&clientaddr,&len);
// 将新的文件描述符添加进集合中
for(int i=1;i<2048;++i){
if (pfds[i].fd==-1){
pfds[i].fd=cfd;
pfds[i].events=POLLIN;
if(nfds<i){
nfds=i;
}
break;
}
}
}
// 遍历所有的fd,从lfd+1开始,因为lfd只是用来判断是否有新的连接,不用来进行通信
printf("start detect fd\n");
for(int i=1;i<=nfds;++i){
printf("now fd is:%d\n",pfds[i].fd);
if(pfds[i].revents&POLLIN){
// 进了这个if说明文件描述符pfds[i].fd发生了变化,可以进行通讯
char buf[1024]={0};
// read会阻塞住,直到client向server发送数据
int len =read(pfds[i].fd,buf,sizeof(buf));
if(len==-1){
perror("read");
exit(-1);
}else if(len==0){
printf("client closed!\n");
close(pfds[i].fd);
pfds[i].fd=-1;
}else if(len>0){
printf("read buf: %s\n",buf);
write(pfds[i].fd,buf,strlen(buf)+1); // 回射
}else{
printf("不知道什么原因,信息未能发送\n");
}
}
}
}
}
close(lfd);
return 0;
}
4.13.3 epoll的使用
epoll有两种工作模式,分别为:
LT模式(水平触发)和ET模式(边沿触发) ,LT模式就是如果内核通知你有一个文件描述符已就绪但是你未处理那么内核就会一直通知你,而ET则相反,只会通知你一次,不管你有没有处理这个文件操作符。
LT模式是epoll默认的触发模式
使用ET模式时需要注意read是默认阻塞的,需要将read读取的文件描述符的属性设置为非阻塞
epoll_server.cpp
#include<iostream>
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
#include<sys/types.h>
#include<sys/epoll.h>
int main(){
int lfd=socket(PF_INET,SOCK_STREAM,0);
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
int bind_ret = bind(lfd,(struct sockaddr *)&saddr,sizeof(saddr));
if(bind_ret==-1){
perror("bind");
exit(-1);
}
int listen_ret = listen(lfd,8);
if(listen_ret==-1){
perror("listen");
exit(-1);
}
// 使用epoll检测哪些文件描述符发生了变化
// 使用create创建epoll实例
int epfd = epoll_create(1);
// 将监听描述符lfd加入到epfd中
struct epoll_event epev;
epev.events = EPOLLIN;
epev.data.fd = lfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&epev);
// epevs用来保存epoll检测到的发生变化的文件描述符
struct epoll_event epevs[1024];
while(1){
// epoll_wait_ret表示有几个文件描述符发生了变化,具体是哪些发生了变化见epevs数组
// 1024表示最大检测的个数是1024,-1 表示阻塞
int epoll_wait_ret = epoll_wait(epfd,epevs,1024,-1);
if (epoll_wait_ret==-1){
perror("epoll_wait");
exit(-1);
}
for(int i=0;i<epoll_wait_ret;++i){
if (epevs[i].data.fd==lfd){
// 如果发生变化的文件描述符中包括lfd,表示有新的客户端连接进来了
printf("client connected!\n");
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int cfd=accept(lfd,(struct sockaddr*)&clientaddr,&len);
epev.events = EPOLLIN;
epev.data.fd=cfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&epev); // 将cfd添加进epoll实例中
}else{
// 文件描述符epevs[i].data.fd发生了变化,表示有文件过来了
char buf[1024]={0};
// read会阻塞住,直到client向server发送数据
int len =read(epevs[i].data.fd,buf,sizeof(buf));
if(len==-1){
perror("read");
exit(-1);
}else if(len==0){
printf("client closed!\n");
// 将epevs[i].data.fd这个文件描述符移出epoll实例
epoll_ctl(epfd,EPOLL_CTL_DEL,epevs[i].data.fd,NULL);
close(epevs[i].data.fd);
}else if(len>0){
printf("read buf: %s\n",buf);
write(epevs[i].data.fd,buf,strlen(buf)+1); // 回射
}else{
printf("不知道什么原因,信息未能发送\n");
}
}
}
}
close(lfd);
close(epfd); //注意创建的epoll实例对应的文件描述符也需要关闭
return 0;
}
4.14 UDP通信
UDP通信的流程,比tcp的握手挥手啥的要简单很多,直接创建socket然后一个发送一个接受就好了
udp_server.cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string>
#include<arpa/inet.h>
#include<string.h>
int main(){
// UDP使用的是SOCK_DGRAM不是TCP的SOCK_STREAM
int fd = socket(PF_INET,SOCK_DGRAM,0);
if (fd==-1){
perror("socket");
exit(-1);
}
struct sockaddr_in saddr;
saddr.sin_port = htons(9999);
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
// 绑定
int bind_ret = bind(fd,(struct sockaddr *)&saddr,sizeof(saddr));
if(bind_ret==-1){
perror("bind");
exit(-1);
}
// 通信
while(1){
// 接收数据
char recvbuf[128];
char ipbuf[16];
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int num = recvfrom(fd,recvbuf,sizeof(recvbuf),0,(struct sockaddr *)&clientaddr, &len);
if(num==-1){
perror("recvfrom");
exit(-1);
}else{
inet_ntop(AF_INET,&clientaddr.sin_addr.s_addr,ipbuf,sizeof(ipbuf));
unsigned short clientport = ntohs(clientaddr.sin_port);
printf("client IP: %s,port: %d\n",ipbuf,clientport);
printf("client: %s\n",recvbuf);
}
// 发送数据
sendto(fd,recvbuf,strlen(recvbuf)+1,0,(struct sockaddr *)&clientaddr, sizeof(clientaddr));
}
close(fd);
return 0;
}
udp_client.cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string>
#include<arpa/inet.h>
#include<string.h>
int main(){
// UDP使用的是SOCK_DGRAM不是TCP的SOCK_STREAM
int fd = socket(PF_INET,SOCK_DGRAM,0);
if (fd==-1){
perror("socket");
exit(-1);
}
// 2、设置服务器的 IP 192.168.18.128和端口 9999,这样客户端才知道要去哪里找
struct sockaddr_in serveraddr;
serveraddr.sin_port = htons(9999);
serveraddr.sin_family=AF_INET;
inet_pton(AF_INET, "192.168.18.128",&serveraddr.sin_addr.s_addr);
int num=0;
// 通信
while(1){
// 发送数据
char sendbuf[128];
sprintf(sendbuf,"hello I'm cliend, this is %d\n",num++); // 格式化数据
sendto(fd,sendbuf,strlen(sendbuf)+1,0,(struct sockaddr *)&serveraddr, sizeof(serveraddr));
// 接受数据
char recvbuf[128];
socklen_t len = sizeof(serveraddr);
// 接受数据是通过fd接受的,和ip啥的没有关系了,直接设置为NULL也行
int num = recvfrom(fd,recvbuf,sizeof(recvbuf),0,NULL, NULL);
if(num==-1){
perror("recvfrom");
exit(-1);
}else{
printf("server: %s\n",recvbuf);
}
sleep(1);
}
close(fd);
return 0;
}
4.14 广播
1、广播就是主机向子网中的所有计算机发送消息,广播消息包含一个特殊的ip地址,这个ip地址的主机标志部分二进制全部为1
2、什么是ip地址的主机标志部分: 192.168.23.21,前面的192.168.23是网络id后面的21是主机id,主机标志部分全部为1即255。当然,前几位是网络id后几位是主机id是由子网掩码决定的
3、需要注意,广播只能在局域网中使用,而且客户端必须要已经绑定了服务器广播使用的端口才能收到广播消息,因为虽然主机是向局域网内的所有计算机发送的消息,但是你不监听这个port当然是收不到的
注意:要想一个socket可以发送广播需要使用setsockopt()进行设置,就像设置端口复用那样
broad_server.cpp
// TCP通信服务器端
#include<stdio.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
#include<iostream>
int main(){
// 1、创建监听的socket,注意使用的是UDP不是TCP,因此是SOCK_DGRAM,实际上广播使用哪个都可以,只是通常时UDP
int lfd = socket(PF_INET,SOCK_DGRAM,0);
if (lfd==-1){
perror("socket");
exit(-1);
}
// 2、要想进行广播,需要设置socket的广播属性
int op=1;
setsockopt(lfd,SOL_SOCKET,SO_BROADCAST,&op,sizeof(op));
// 3、创建广播的地址
struct sockaddr_in saddrs;
saddrs.sin_family = AF_INET;
saddrs.sin_port=htons(9999); // 端口为9999,通过htons转换为网络字节序
inet_pton(AF_INET, "192.168.18.255",&saddrs.sin_addr.s_addr); // 255是特殊的,表示向192.168.18.内所有设备广播
// 服务端只是发送广播,不需要bind,每次sendto时操作系统会自己指定一个网络端口进行发送
// 4、通信,发送广播
int num=0;
while(1){
char brosend[128];
sprintf(brosend,"hello I'm server, this is %d brocast\n",num++); // 格式化数据
// 直接向saddrs对应的地址发送消息,因为最后一位是255,所以是广播
// 打印我广播的目标ip
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(saddrs.sin_addr.s_addr), ip, INET_ADDRSTRLEN);
printf("Bro IP: %s\n", ip);
printf("Bro Port: %d\n", ntohs(saddrs.sin_port));
// 发送广播信息,由于最后一位是255,因为这个消息会发送给这个网络id下所有的设备
sendto(lfd,brosend,strlen(brosend)+1,0,(struct sockaddr *)&saddrs, sizeof(saddrs));
sleep(1);
}
// 5、关闭所有打开的文件描述符
close(lfd);
return 0;
}
broatclient.cpp
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string>
#include<arpa/inet.h>
#include<string.h>
// 这个客户端只接受来自服务器的广播消息,不发送信息
int main(){
// UDP使用的是SOCK_DGRAM不是TCP的SOCK_STREAM
int fd = socket(PF_INET,SOCK_DGRAM,0);
if (fd==-1){
perror("socket");
exit(-1);
}
// 2、设置可能发送消息过来的ip地址和端口,才可以接收到这个ip主机发送的广播信息
struct sockaddr_in addr;
addr.sin_port = htons(9999);
addr.sin_family=AF_INET;
// 注意,这里是将网络接口设置为这个计算机上的任意网络接口,表示我接受来自这台计算机上所有接口的数据
addr.sin_addr.s_addr = INADDR_ANY;
// 因为要在同一台主机上进行测试但是每一次运行这个程序都会使用bind相同的addr
// 因此都会绑定到0.0.0.0,虽然这个ip很特殊,但是也不能被绑定两次,除非设置成可以复用
int reuse = 1;// 设置为可以复用
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) == -1) {
perror("setsockopt");
exit(-1);
}
// 绑定
int bind_rect = bind(fd,(struct sockaddr*)&addr,sizeof(addr));
if(bind_rect==-1){
perror("bind");
exit(-1);
}
// 查看这个进程绑定的ip和端口
struct sockaddr_in taddr;
socklen_t addrlen = sizeof(taddr);
// 获取套接字绑定的本地地址
if (getsockname(fd, (struct sockaddr *)&taddr, &addrlen) == 0) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(taddr.sin_addr.s_addr), ip, INET_ADDRSTRLEN);
printf("Local IP: %s\n", ip);
printf("Local Port: %d\n", ntohs(taddr.sin_port));
} else {
perror("getsockname");
}
// 不需要监听listen,listen通常时服务端使用,用于监听是否有新的连接请求,广播一般使用的时UDP,不需要建立连接
// 即使是tcp连接,客户端也不需要listen,因为client是发起连接的一方,监测有无新的连接请求是服务器干的事情
// 通信
while(1){
// 接受数据
char recvbuf[128];
// recvfrom默认是阻塞的,因此不用sleep
int num=recvfrom(fd,recvbuf,sizeof(recvbuf),0,NULL,NULL);
printf("server broadcast message: %s\n",recvbuf);
}
close(fd);
return 0;
}
4.15 组播(多播)
1、与广播类似,但是目标是一组特定的客户端,而且组播既可以应用于局域网也可以用于广域网
2、只有加入了多播组的客户端才会接收到组播的数据
3、组播也有特定的ip限制
4.16 本地套接字实现进程间通信
使用本地套接字进行本地进程间的通信
注意:创建本地套接字之前应该先使用unlink删除可能存在的且无人使用的这个本地套接字文件,创建的本地套接字文件并不会自动删除
ipc_server.cpp
// 使用tcp进行本地进程间通讯
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/un.h>
int main(){
// 上来先清楚可能重名的本地套接字
unlink("server.sock");
// 1、 创佳套接字
int lfd = socket(AF_LOCAL, SOCK_STREAM,0);
if(lfd==-1){
perror("socket");
exit(-1);
}
// 2、创建并绑定本地的套接字地址
struct sockaddr_un addr;
addr.sun_family=AF_LOCAL;
strcpy(addr.sun_path,"server.sock");
int bind_rect = bind(lfd, (struct sockaddr*)&addr,sizeof(addr));
if(bind_rect== -1){
perror("bind");
exit(-1);
}
// 3、 监听
int listen_rect=listen(lfd,100);
if (listen_rect==-1){
perror("listen");
exit(-1);
}
// 4、等待客户端连接
struct sockaddr_un clientaddr;
socklen_t len=sizeof(clientaddr);
int cfd = accept(lfd,(struct sockaddr*)&clientaddr,&len);
if(cfd==-1){
perror("accept");
exit(-1);
}
printf("client socket filename:%s\n",clientaddr.sun_path);
// 5、通信
while(1){
char buf[128];
int recv_rect = recv(cfd,buf,sizeof(buf),0);
if (recv_rect==-1){
perror("recv");
exit(-1);
}else if(recv_rect==0){
printf("client is closed\n");
close(cfd);
break;
}else{
printf("client: %s\n", buf);
send(cfd,buf,len,0);
}
}
close(lfd);
return 0;
}
ipc_client.cpp
// 使用tcp进行本地进程间通讯
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<arpa/inet.h>
#include<sys/un.h>
int main(){
// 上来先清楚可能重名的本地套接字
unlink("client.sock");
// 1、 创佳套接字
int cfd = socket(AF_LOCAL, SOCK_STREAM,0);
if(cfd==-1){
perror("socket");
exit(-1);
}
// 2、创建并绑定本地的套接字地址
struct sockaddr_un addr;
addr.sun_family=AF_LOCAL;
strcpy(addr.sun_path,"client.sock");
int bind_rect = bind(cfd, (struct sockaddr*)&addr,sizeof(addr));
if(bind_rect== -1){
perror("bind");
exit(-1);
}
// 3、指定服务器并连服务器,因此客户端需要提前知道服务器生成的本地的套接字的名称
struct sockaddr_un serveraddr;
serveraddr.sun_family=AF_LOCAL;
strcpy(serveraddr.sun_path,"server.sock");
// connect默认是阻塞的,直到连接建立
int con_rect = connect(cfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr));
if(con_rect==-1){
perror("connect");
exit(-1);
}
printf("connected!\n");
// 4、通信
int num=0;
while(1){
// 先发送数据
char buf[128];
sprintf(buf,"hello I am client %d\n",num++);
send(cfd,buf,strlen(buf)+1,0);
// 接受数据
int recv_rect = recv(cfd,buf,sizeof(buf),0);
if (recv_rect==-1){
perror("recv");
exit(-1);
}else if(recv_rect==0){
printf("server is closed\n");
close(cfd);
break;
}else{
printf("server: %s\n", buf);
}
sleep(1);
}
return 0;
}
5、项目实战
5.1 阻塞/非阻塞、同步/异步 (网络IO)
1、异步io是非阻塞的,同步io是阻塞的。教程一直以来使用的都是同步io,在使用recv等这个函数从cfd中读取信息时其实进程都是阻塞的,哪怕cfd中已经有了数据,只不过读取的速度很快,没有察觉而已。异步的话数据在读取时进程不会阻塞,当读取完了在通知进程可以使用了,进程直接去使用就好了
2、异步io需要使用特殊的系统调用函数,比如aio_read()、aio_write(),异步io使用起来很麻烦,一般不会使用
3、一个典型的网络io接口调用分为两个阶段,“数据就绪”(等待数据过来)和"数据读取"(从tcp缓冲区搬到内存)这两个阶段都是需要花费时间的
5.2 5种IO模型
- 阻塞 blocking,常规的read、recv,进程停下来等待文件的到达
- 非阻塞 Non-blocking,非阻塞的调用,结果总是返回,根据返回的结果判断文件是否已经到达
- IO复用 IO multiplexing, select、poll、epoll实现IO多路复用,同时检测多个文件描述符有无数据到达
- 信号驱动 signal-driven, 当io事件准备就绪后发送SIGIO信号告诉进程去处理
- 异步 asynchronous,使用异步io接口去处理文件
5.3 web服务器及HTTP协议
web server( 网页服务器),是一个服务器软件,主要功能就是对客户端发来的http请求做出回应
5.4 服务器编程的基本框架
服务器的程序种类繁多,但是基本的额框架都是一样的,不同的只是逻辑处理
一般有4块:
- I/O处理单元,等待并处理客户发送过来的请求,读写网络数据
- 逻辑处理单元,根据解析的信息进行一些逻辑处理,就是判断该干啥
- 网络存储单元,就是数据库啥的
- 请求队列,各个单元之间的通信方式
5.5 事件处理模式
服务器干的活主要是三种:I/O,信号和定时事件,有两种高效的事件处理模式,分别为Reactor和Proactor
1、Reactor模式:
主线程只负责监听文件描述符上是否有事件发生,有的话就通知其他工作线程进行处理,除此之外主线程不干其他事情。
2、proactor模式:
将所有的I/O操作都交给主线程来处理,工作线程只负责业务逻辑。
这两种模式都可以使用同步或者异步io实现,但是linux中由于异步io接口并不完善所以一般都是使用同步io去模拟proactor和reactor文章来源:https://www.toymoban.com/news/detail-665062.html
5.6 线程池
线程池就是一系列的线程,当没有任务时所有线程睡眠,来一个唤醒一个去执行任务,因为唤醒一个线程比创建一个新的线程要快
特点:文章来源地址https://www.toymoban.com/news/detail-665062.html
- 空间换时间
- 是一组静态资源
- 所有的资源在创建时就已经分配好了,不需要动态分配
- 当服务器处理完一个客户连接后可以直接将资源放回池中,无需执行系统调用释放资源
到了这里,关于C++ web server服务器 开发的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!