Linux网络编程

1 网络基础概念

1.1 协议
概念:协议实现约定好,大家共同遵守的一组规则,协议可以理解为数据传输和数据解释的规则。
1.2 分层模型
OSI 7层模型:物数网传会表应

  • 应用层:主要就是应用程序,ftp ssh email http
  • 表示层:进行编解码和翻译工作
  • 会话层:建立会话和保持会话
  • 传输层:定义了端到端的传输,TCP UDP协议
  • 网络层:定义了点到点的传输,IP协议 --路由器
  • 数据链路层:数据校验,定义了数据格式 --帧,ARP协议 RARP协议
  • 物理层:通信介质-双绞线,光纤,调制解调器modemn

TCP四层模型:

  • 应用层:对应会话层,表示层,应用层
  • 传输层:对应传输层
  • 网络层:对应网络层
  • 网络接口层:对应于物理层和数据链路层

1.3数据通信过程
发送端层层打包,接收方层层解包
在这里插入图片描述
1.4网络应用程序的设计模式
C/S设计模式的优缺点:
优点:可以安装在本地,可以缓存数据,协议的选择灵活
缺点:客户端工具需要由程序员开发,开发周期长工作量大;需要本地安装,对客户的电脑安全有一定影响。
B/S模式:
优点:浏览器不用开发,开发周期短,工作量小
缺点,只能选择http协议,协议选择受限制,不能缓存数据,效率受影响。
1.5 以太网帧格式
以太网帧格式就是包装在网络接口层(数据链路层)的协议
ARP协议:
在这里插入图片描述
IP协议:在网络层
在这里插入图片描述
协议版本: ipv4, ipv6
16位总长度: 最大65536
8位生存时间ttl(网络连接下一跳的次数): 为了防止网络阻塞
32位源ip地址, 共个4字节!我们熟悉的ip都是点分十进制的,4字节, 每字节对应一个点分位,最大为255 ,实际上就是整形数!
32位目的ip地址
8位协议: 用来区分上层协议是TCP, UDP, ICMP还是IGMP协议.
16位首部校验和: 只校验IP首部, 数据的校验由更高层协议负责.

UDP数据报格式:面向无连接的、不安全的、不可靠的数据报传输。
在这里插入图片描述
通过IP地址来确定网络环境中的唯一的一台主机;
主机上使用端口号来区分不同的应用程序.
IP+端口唯一确定唯一一台主机上的一个应用程序.

TCP数据流格式:面向连接的、安全的、可靠的数据流传输协议
在这里插入图片描述
序号: TCP是安全可靠的, 每个数据包都带有序号, 当数据包丢失的时候, 需要重传, 要使用序号进行重传. 控制数据有序, 丢包重传.
确认序号: 使用确认序号可以知道对方是否已经收到了, 通过确认序号可以知道哪个序号的数据需要重传.
16位窗口大小–滑动窗口(主要进行流量控制)

2 SOCKET编程

传统的进程间通信借助内核提供的IPC机制进行,但是只能限于本机通信。若要跨机通信,就必须使用网络通信,这就需要用到内核提供给用户的socket API函数库。
2.1 网络字节序
大端字节序:也叫高端字节序(网络字节序),是高端地址存放低位数据,低端地址存放高位数据
小端字节序:也叫低端字节序,是低地址存放低位数据,高地址存放高位数据。

大小端进行转换的函数:
#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
h表示主机host,n表示网络nework,s表示short,l表示long
IP地址转化函数:
p->表示点分十进制的字符串形式
to->到
n->表示network网络

int inet_pton(int af,const char *src,woid *dst);
	函数说明:将字符串形式的点分十进制IP转换为大端模式的网络IP
	参数说明:
		af:AF_INET
		src:字符串形式的点分十进制的IP地址
		dst:存放转换后的变量的地址
例如:int_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);
手工也可以计算:192.168.232.145, 先将4个正数分别转换为16进制数, 
	192--->0xC0  168--->0xA8   232--->0xE8   145--->0x91
	最后按照大端字节序存放: 0x91E8A8C0, 这个就是4字节的整形值.

const char *inet_ntop(int af,const void *src,char *dst,socklen_t size);
	函数说明:网络IP转换为字符串形式的点分十进制IP
	af:AF_INET
	src:网络的整形的IP地址
	dst:转换后的IP地址,一般为字符串数组
	size:dst的长度
	返回值:成功--返回指向dst的指针
		   失败--返回null,并设置errno
例如: IP地址为010aa8c0, 转换为点分十进制的格式:
	01---->1    0a---->10   a8---->168   c0---->192
	由于从网络中的IP地址是高端模式, 所以转换为点分十进制后应该为: 192.168.10.1

2.2 SOCKET编程主要的API函数

int socket(int domain,int type,int protocol);
	函数描述:创建socket
	参数说明:
		domain:协议版本
		AF_INET IPV4
		AF_INET6 IPV6
		AF_UNIX AF_LOCAL 本低套接字使用
	type:协议类型
		SOCK_STREAM 流式,默认使用的协议是TCP协议
		SOCK_DGRAM  报式,默认使用的是UDP协议
	protocol:
		一般填0,表示使用对应类型的默认协议
	返回值:成功:返回一个大于0的文件描述符
		   失败返回-1,并设置errno
当调用socket函数以后,返回一个文件描述符,内核会提供与该文件描述符相对于的读和写缓冲区,同时还有两个队列,分别是请求队列和已连接队列。

int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
	函数描述:将socket文件描述符和IP,PORT绑定
	参数说明:
		socket:调用socket函数返回的文件描述符
		addr:本地服务器的IP地址和PORT
			struct sockaddr_in serv;
			serv.sin_family = AF_INET;
			serv.sin_port = htons(8888);
			serv.sin_addr.s_addr = htonl(INADDR_ANY); INADDR_ANY表示使用本机任意有效的可以IP
		addrlen:addr变量的占用的内存大小
	返回值:成功:返回0;
		   失败:返回-1,并设置errno

int listen(int sockfd,int backlog);
	函数描述:将套接字由主动态变为被动态
	参数说明:
		sockfd:调用socket函数返回的文件描述符	
		backlog:同时请求连接的最大个数
	返回值:	成功:返回0;
		   失败:返回-1,并设置errno

int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
	函数说明:获得一个连接,若当前没有连接则会阻塞等待
	函数参数:
		sockfd:调用socket函数返回的文件描述符
		addr:传出参数,保存客户端的地址信息
		addrlen:传入传出参数,addr变量所占内存空间大小
	返回值:
		成功:返回一个新的文件描述符,用于与客户端通信
		失败:返回-1,并设置errno值
accept函数是一个阻塞函数, 若没有新的连接请求, 则一直阻塞.
	从已连接队列中获取一个新的连接, 并获得一个新的文件描述符, 该文件描述符用于和客户端通信.  (内核会负责将请求队列中的连接拿到已连接队列中)

int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
	函数说明:连接服务器
	函数参数:
		sockfd:调用socket函数返回的文件描述符
		addr:服务端的地址信息
		addrlen:addr变量的内存大小
	返回值:
		成功:返回0
		失败:返回-1,并设置errno值

接下来就可以使用write和read函数进行读写操作了
读取数据和发送数据:
	ssize_t read(int fd, void *buf, size_t count);
	ssize_t write(int fd, const void *buf, size_t count);
	ssize_t recv(int sockfd, void *buf, size_t len, int flags);
	ssize_t send(int sockfd, const void *buf, size_t len, int flags);	
	对应recv和send这两个函数flags直接填0就可以了.

2.3 使用socket的API函数编写服务端和客户端程序步骤:
在这里插入图片描述
服务端:

  1. 创建socket,返回一个文件描述符lfd --socket() 该文件描述符用于监听客户端连接
  2. 将lfd和IP PORT进行绑定 – bind()
  3. 将lfd由主动变为被动监听 --listen()
  4. 接受一个新的连接,得到一个文件描述符cfd --accept() – 该文件描述符是用于和客户端进行通信的
  5. while(1){ 接收数据–read或者recv;发送数据–write或者send;}
  6. 关闭文件描述符 --close(lfd) close(cfd)
int main(){
	//创建socket用于和服务端进行通信
	int lfd = socket(AF_INET,SOCK_STREAM,0);
	if(lfd<0){
		perror("socket error");
		return -1;
	}
	
	struct sockaddr_in serv;
	//初始化将前n个字节清零
	bzero(&serv,sizeof(serv));
	serv.sin_family =AF_INET;
	serv.sin_port = htons(8888);
	//标识适用本机任意可用IP
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	int ret = bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
	if(ret<0){
		perror("bind error");
		return -1;
	}
	//最大是128
	listen(lfd,128);
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
	int cfd = accept(lfd ,(struct sockaddr *)&client,&len);

	//获取client端的IP和port端口
	char sIP[16];
	memset(sIP,0x00,sizeof(sIP));
	printf("client-->IP:[%s],PORT:[%d]\n",inet_ntop(AF_INET,&client.sin_addr.s_addr,sIP,sizeof(sIP)),ntohs(client.sin_port));
	printf("lfd==[%d], cfd==[%d] n", lfd,cfd);
	
	int n = 0;
	char buf[1024];
	int i = 0;
	while(1){
		//读数据
		memset(buf,0x00,sizeof(buf));
		n = read(cfd,buf,sizeof(buf))
		if(n<=0){
			printf("read error or client close,n==[%d]\n",n);
			break;
		}
		printf("n==[%d] , buf==[%s] n",n, buf);
		for(i=0;i<=n;i++){
			buf[i] = toupper(buf[i]);
		}
		// 发送数据
		write(cfd,buf,n);
	}
	//关闭监听文件描述符和通信文件描述符
	close(lfd);
	close(cfd);
	return 0;
}

客户端:

  1. 创建socket,返回一个文件描述符cfd – socket() 该文件描述符是用于和服务端通信
  2. 连接服务端 --connect()
  3. while(1){ 发送数据write或者send;接收数据 read或者recv;}
  4. close(cfd);
int main(){
//创建socket用于和服务端进行通信
	int cfd = socket(AF_INET,SOCK_STREAM,0);
	if(cfd<0){
		perror("socket error");
		return -1;
	}
	//连接服务器
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);
	printf("[%p]\n",serv.sin_addr.s_addr);
	int ret = connect(cfd,(struct sockaddr *)&serv,sizeof(serv));
	if(ret<0){
		perror("connect error");
		return -1;
	}
	int n = 0;
	char buf[256];
	while(1){
		//读标准输入数据
		memset(buf,0x00,sizeof(buf));
		n = read(STDIN_FILENO,buf,sizeof(buf));
		//发送数据
		write(cfd,buf,n);
		//读服务端发来的数据
		memset(buf,0x00,sizeof(buf));
		n = read(cfd,buf,sizeof(buf));
		if(n<=0){
			printf("read error or server cosed , n==[%d] n",n);
			break;
		}
		printf("n==[%d], buf==[%s]\n",n, buf);
	}
	close(cfd);
	return 0;
}

得到两个文件描述符lfd和cfd,lfd负责监听连接,不参与收发数据;cfd负责和客户端进行通信,有多少个客户端建立了连接,就有多少个cfd,客户端也有一个cfd负责和服务端通信
2.4 三次握手和四次挥手
在客户端与服务端建立连接的时候要经过三次握手的过程,客户端与服务端断开连接的时候要经历四次挥手
在这里插入图片描述
SYN:表示请求,ACK:表示确认,服务端发送的SYN和客户端发送的SYN本身也会占1位
三次握手:
在这里插入图片描述
通信的时候不再需要SYN标识位了,只有在请求连接的时候需要SYN标识位。
在这里插入图片描述
ACK确认包表示给对方发送数据的一个确认,表示你发送的数据都收到了,同时告诉对方下次发送该序号开始的数据,由于每次发送数据都会受到对方发来的确认包,所以可以确认对方是否收到,若没有收到对方法来的确认包,则会重发。
mss最大报文长度,告诉对方最多一次能收多少,不能超过这个长度;
win表示最大缓存空间
FIN是四次挥手结束时的标志。

2.5 滑动窗口
主要作用是进行流量控制的,如果发送端发送的速度较快,接收端接收到数据后处理的速度较慢,而接收缓冲区的大小是固定的,就会导致接收缓冲区满而丢失数据,通过滑动窗口来解决这一问题。
在这里插入图片描述
win表示告诉对方握着比缓冲区大小是多少,mss表示告诉对方我这边最多一次可以接收多少数据。
客户端给服务端发送包的时候,不一定是非要等到对方返回响应包,由于客户端知道服务端的窗口大小,所以可以持续多次发送,当发送数据达到对方窗口大小了就不再发送,需要等对方处理之后,才可以继续发送。

MTU: 最大传输单元
MTU:通信术语最大传输单元(Maximum Transmission Unit,MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为 单位). 最大传输单元这个参数通常与通信接口有关(网络接口卡、串 口等), 这个值如果设置为太大会导致丢包重传的时候重传的数据量较大, 图中的最大值是1500, 其实是一个经验值.

2.6 粘包
多次数据发送,首尾相连,接收端接收的时候不能正确区分第一次发送多少,第二次发送多少。
在这里插入图片描述

粘包问题解决方法:
方案1:包头+数据;有四位的数据长度+数据:00100123456789
其中0010表示数据长度,0123456789表示10个字节长度的数据;另外发送端和接收端可以协商更为复杂的报文结构,相当于双方约定的一个协议

方案2:添加结尾标记:如:\n、$等

方案3:数据包定长:如发送方和接收方约定,每次只发送128个字节的内容,接收方接收定长128个字节就可以了

2.7高并发服务器:
如何支持多个客户端–支持多并发的服务器:由于accept和read函数都会阻塞,当read的时候,不能调用accept接受新的连接,当accept阻塞等待的时候不能read读函数
方案1:使用多进程,可以让父进程接受新连接,让子进程处理与客户端通信;父进程accept接受新连接,然后fork子进程,让子进程处理通信,子进程处理完成后退出,父进程使用SIGCHLD信号回收子进程
处理流程:

  1. 创建socket,得到一个监听文件描述符lfd --socket()
  2. 将lfd和IP和端口port进行绑定 – bind()
  3. 设置监听 – listen()
  4. 进入while(1){};

多进程版本的网络服务器:父进程接受新的连接,子进程进行通信

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"
int main()
	//创建 socket
	int lfd = Socket(AF_INET,SOCK_STREAM,0);
	//绑定
	struct sockaddr_in serv;
	bzero(&serv,sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示本机任意可用IP
	//&serv本地服务器的IP和port
	Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
	//设置监听
	Listen(lfd,128);
	pid_t pid;
	int cfd;
	char sIP[16];//保存IP
	socklen_t len;
	struct sockaddr_in client;
	while(1){
		//接受新的连接
		len = sizeof(client);
		memset(sIP,0x00,sizeof(sIP));
		//client保存客户端的地址信息,len是传出参数client所占的内存大小,返回cfd用于与客户端进行通信
		cfd = Accept(lfd,(struct sockaddr *)&client, &len);
		printf("client:[%s) [%dJ\n", inet_ntop(AF_INET, &client.sin_addr.s.addr, sIp, sizeofsIP)), ntohs(client.sin_port));
		//接受一个新的连接,创建一个子进程,让子进程完成数据的收发操作
		pid = fork();
		if(pid<0){
			perror("fork error");
			exit(-1);
		}else if(pid>0){
			//关闭通信文件描述符 cfd
			close(cfd);
		}else if(pid==0){
			//关闭监听文件描述符
			close(lfd);
			int i=0;
			int n;
			char buf[1024];
			while(1){
				//读数据
				n = Read(cfd,buf,sizeof(buf));
				if(n<=0)
					printf("read error or client closed, n==[%d]\n",n);
					break;
				}
				printf("[%d]---->:n==[%d], buf==[%s]\n",ntohs(client.sin_port),n, buf);
				//将小写转换为大写
				for(i=0; i<n; i++){
					buf[i] = toupper(buf[i]);
				}
				//发送数据
				Write(cfd,buf,n);
			}
			//关闭 cfd
			close(cfd);
			exit(0);
		}
	}
	//关闭监听文件描述符
	close(lfd);
	return 0;
}

多线程版本的网络服务器:主线程接受新连接,子线程进行通信

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"
#include <pthread.h>
void *mythread(void *arg){
	int cfd=*(int *)arg;
	char buf[1024];
	int n;
	int i;
	while(1){
		memset(buf,0x00,sizeof( buf));
		n = Read(cfd,buf,sizeof(buf));
		if(n<=0){
			printf("read error or client closed ,n==[%d] n",n);
			break;
		}
		printf("n==[%d] , buf==[%s] n",n,buf);
		for(i=0;i<n;i++){
			buf[i]=toupper(buf[i]);
		}
		write(cfd,buf,n);
	}
	close(cfd);
	pthread_exit(NULL);
}

int main(){
	int lfd = Socket(AF_INET,SOCK_STREAM,0);
	
	struct sockaddr_in serv;
	bzero(&serv,sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
	
	Listen(lfd,128);
	
	struct sockaddr client;
	socklen_t len;
	int cfd;
	pthread_t pthreadIP;
	while(1){
		cfd = Accept(lfd,(struct sockaddr *)&client,&len);
		//创建子线程
		pthread_create(&pthreadIP,NULL,mythread,&cfd);
		//设置线程分离
		pthread_detach(pthreadIP)
	}
	close(lfd);
	return 0;
}

子线程能否关闭lfd?子线程不能关闭监听文件描述符lfd,子线程和主线程共享文件描述符,而不是复制的
主线程能否关闭cfd?主线程不能关闭cfd,主线程和子线程共享一个cfd,而不是复制的,close之后cfd才被关闭
多个子线程共享cfd,会有什么问题发生?只有最后一个连接能够与服务端进行通信,因为前面的cfd都被覆盖掉。

TCP状态转换图:
在这里插入图片描述

2.8 2MSL
2MSL即两倍的MSL,TCP的TIME_WAIT状态也称为2MSL等待状态,当TCP的一端发起主动关闭,在发出最后一个ACK包后,即第3次挥手完成后发送了第四次挥手的ACK包后就进入了TIME_WAIT状态,必须在此状态上停留两倍的MSL时间,等待2MSL时间主要目的是怕最后一个ACK包对方没收到,那么对方在超时后将重发第三次握手的FIN包,主动关闭端接到重发的FIN包后可以再发一个ACK应答包。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

为什么需要2MSL?

  1. 让四次挥手的过程更可靠,确保最后一个发送给对方的ACK到达;若对方没有收到ACK应答,对方会再次发送FIN请求关闭,此时在2MSL时间内被动关闭方仍然可以发送ACK给对方。
  2. 为了保证在2MSL时间内,不能启动相同的SOCKET-PAIR。TIME_WAIT一定是出现在主动关闭的一方,也就是说2MSL是针对主动关闭一方来说的;由于TCP有可能存在丢包重传,丢包重传若发给了已经断开连接之后相同的socket-pair(新的连接,但与原来的socket-pair相同,双方使用的是相同的IP和端口),这样会对之后的连接造成困扰,引起程序异常。

2.9 端口复用
解决端口复用的问题: bind error: Address already in use, 发生这种情况是在服务端主动关闭连接以后, 接着立刻启动就会报这种错误.

setsockopt函数:
int setsockopt(int sockfd,int level,int optname,const void *optval,socklen_t optlen);
	setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
	setsockopt(lfd,SOL_SOCKET,SO_REUSEport,&opt,sizeof(int));
在bind之前,socket之后调用

2.10 半关闭状态:如果一方close,另一方没有close,则认为是半关闭状态,处于半关闭状态的时候,可以接收数据,但不能发送数据,相当于把文件描述符的写缓冲区操作关闭了
长连接和短链接的概念:
长连接:连接建立之后一直不关闭的
短链接:连接收发数据完毕之后就关闭

shutdown和close的区别:
shutdown能够把文件描述符上的读或者写操作关闭, 而close关闭文件描述符只是将连接的引用计数的值减1, 当减到0就真正关闭文件描述符了.
如: 调用dup函数或者dup2函数可以复制一个文件描述符, close其中一个并不影响另一个文件描述符, 而shutdown就不同了, 一旦shutdown了其中一个文件描述符, 对所有的文件描述符都有影响 .

2.11 心跳包
用于检查与对方的网络连接是否正常.一般心跳包用于长连接
通信双方需要协商规则(协议),如4个字节长度+数据部分
发送心跳过程:服务A个B发送心跳数据AAAA,服务B收到AAAA之后,给A回复BBBB,此时A收到BBBB之后,认为连接正常,假如A连续发送了多次之后,仍然没有收到B的回复,则认为是连接异常,异常之后A应该重新建立连接.
如何让心跳数据和正常的业务数据不混淆?双方协商协议:例如:4个字节长度+具体数据,先收4个字节的报头数据,如何计算长度,若长度为4,且数据为AAAA,则认为是心跳数据,B会返回应答数据:0004BBBB.

3 高并发服务器模型

3.1 多路IO计数select,同时监听多个文件描述符,将监控的操作交给内核去处理.
数据类型fd_set:文件描述符集合,本质是位图

int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
函数介绍:委托内核监控该文件描述符对应的读,写或者错误事件的发生
参数说明:
	nfds:最大的文件描述符+1
	readfds:读集合,是一个传入传出参数
		传入:指的是告诉内核那些文件描述符需要监控
		传出:指的是内核告诉应用程序那些文件描述符发生了变化
	writefds:写文件描述符集合(传入传出参数)
	execptfds:异常文件描述符集合(传入传出参数)
	timeout:
		NULL表示永久阻塞,直到有事件发生
		0   表示不阻塞,立刻返回,不管是否有监控的事件发生
		>0 到指定事件或者有事发生了就返回
返回值:成功返回发生变化的文件描述符的个数
	   失败返回-1,并设置errno值

宏:
void FD_CLR(int fd,fd_set *set);
	功能:将fd从set集合中清除
int FD_ISSET(int fd,fd_set *set);
	功能:判断fd是否在集合中
	返回值:如果fd在set集合中,返回1,否则返回0
void FD_SET(int fd,fd_set *set);
	功能:将fd设置到set集合中
void FD_ZERO(fd_set *set);
	功能:初始化set集合

多客户端连接(在不使用多线程和多进程的情况下)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <ctype.h>
#include "wrap.h"

int main(){
	//创建socket
	int lfd = Socket(AF_INET,SOCK_STREAM,0);
	
	//设置端口复用
	int opt = 1;
	setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
	
	//绑定
	struct sockaddr_in serv;
	bzero(&serv,sizeof(serv));
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
	
	//监听
	Listen(lfd,128);
	
	//定义fd_set类型的变量
	fd_set readfds;
	fd_set tmpfds;//readfds是传入传出参数,所以需要临时变量来存储
	
	//清空readfds和tmpfds集合
	FD_ZERO(&readfds);
	FD_ZER0(&tmpfds);
	
	//将lfd加入到readfds中,委托内核监控
	FD_SET(lfd,&readfds);
	
	int maxfd = fd;
	int nready;
	int cfd;
	int i=0;
	int sockfd;
	char buf[1024];
	int n=0;
	while(1){
		tmpfds = readfds;
		
		//tmpfds是输入输出参数
		//输入:告诉内核要监测那些文件描述符
		//输出:内核告诉应用程序有哪些文件描述符发生了变化
		nready = select(maxfd+1,&tmpfds,NULL,NULL,NULL);//select返回发生变化的文件描述符个数
		if(nready<0){
			//被信号中断
			if(errno == EINTR){
				continue;
			}
			break;
		}
		
		//有客户端连接请求到来
		if(FD_ISSET(lfd,&tmpfds)){
			//接受新的客户端连接请求
			cfd = Accept(lfd,NULL,NULL);
			//将cfd加入到readfds集合中
			FD_SET(cfd,&readfds);
			//修改内核的监控范围
			if(maxfd<cfd){
				maxfd=cfd;
			}
			if(--nready==0){
				continue;
			}
		}
		
		//有数据发来的情况
		for(i=lfd+1;i<=maxfd;i++){
			sockfd = i;
			//判断sockfds文件描述符是否发生变化
			if(FD_ISSET(sockfd,&tmpfds)){
				//读数据
				memset(buf ,0x00,sizeof(buf));
				n=Read(sockfd,buf,sizeof(buf));
				if(n<=0){
					//关闭连接
					close(sockfd);
					//将sockfd从readfds中删除
					FD_CLR(sockfd,&readfds);
				}else{
					printf("n==[%d], buf==[%s] n",n,buf);
					int k= 0;
					for(k = 0;k<n;k++){
						buf[k]=toupper(buf[k]);
					}
					Write(sockfd,buf,n);
				}
				if(--nready==0){
					break;
				}
			}
		}
	}
	close(lfd);
	return 0;
}

3.2 多路IO-poll

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
函数说明:跟select类似,监控多路IO,但poll不能跨平台
参数说明:
	fds:传入传出参数,实际上是一个结构体数组
		fds.fd:要监控的文件描述符
		fds.events:
			POLLIN:读事件
			POLLOUT:写事件
		fds.revents:返回的事件
	nfds:数组实际有效内容的个数
	timeout:超时时间,单位是毫秒
		-1:永久阻塞,知道监控的事件发生
		0:不管是否有事件发生,立刻返回
		>0:知道监控的事件发生或者超时
返回值:
	成功:返回就绪事件的个数
	失败:返回-1,若timeout=0,poll函数不阻塞,且没有事件发生,此时返回-1,并且errno=EAGAIN,这种情况不应视为错误.

struct pollfd{
	int fd;           //监控的文件描述符
	short events;     //要监控的事件 -- 不会被修改
	short revents;    //返回发生变化的事件 -- 由内核返回
};

当poll函数返回的时候, 结构体当中的fd和events没有发生变化, 究竟有没有事件发生由revents来判断, 所以poll是请求和返回分离.
struct pollfd结构体中的fd成员若赋值为-1, 则poll不会监控.

int main(){
	//创建socket
	int lfd = Socket(AF_INET,SOCK_STREAM,0);
	
	//允许端口复用
	int opt = 1;
	setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
	
	//绑定bind
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
	
	//监听listen
	Listen(lfd,128);
	
	struct pollfd client[1024];

	//监听文件描述符委托给内核监控 监控读事件
	client[0].fd=lfd;
	client[0].events=POLLIN;
	
	int i=l;
	int nready=0;
	int maxi=0; //内核监控范围
	
	for(i=1;i<1024;i++){
		client[i].fd=-1;
	}
	int k=0;
	int n=0;
	int cfd=0;
	int sockfd=0;
	char buf[1024];
	while(1){
		nready = poll(client,maxi+1,-1);
		if(nready<0){
			if(errno==EINTR){
				continue;
			}
			break;
		}

		//有客户端连接请求
		if(client[0].revents==POLLIN){
			cfd = Accept(lfd,NULL,NULL);
			//寻找client数组中的可用位置
			for(i=1;i<1024;i++){
				if(client[i].fd==-1){
					client[i].fd = cfd;
					client[i].events = POLLIN;
					break;
				}
			}
			//没有可用位置,则关闭连接
			if(i==1024){
				Close(cfd);
				continue;
			}
			if(maxi<i){
				maxi = i;
			}
			if(--nready==0){
				continue;
			}
		}
		//有数据到来的情况
		for(i=1;i<=maxi;i++){
			sockfd = client[i].fd;
			memset(buf,0x00,sizeof(buf));
			//若fd为-1,表示连接已经关闭或者没有连接
			if(client[i].fd == -1){
				continue;
			}
			if(client[i].revents==POLLIN){
				n=Read(sockfd,buf,sizeof(buf));
				if(n<=0){
					close(sockfd);
					client[i].fd = -1; //fd=-1,表示不再让内核监控
				}else{
					printf("n==[%d],buf==[%s] n",n, buf);
					write(sockfd,buf,n);
				}
				if(--nready==0){
					break;
				}
			}
		}
	}
	close(lfd);
	return 0;
}

3.3 多路IO-epoll
将检测文件描述符的变化委托给内核去处理,然后内核发生变化的文件描述符对应的事件返回给应用程序.

int epoll_create(int size);
函数说明:创建一个树根
参数:
	size:最大节点数
返回值:
	成功:返回一个大于0的文件描述符,代表整个树的树根
	失败:返回-1,并设置errno值

int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
函数说明:将要监听的节点在epoll树上添加,删除和修改
参数:
	epfd:epoll树根
	op:
		EPOLL_CTL_ADD:添加事件节点到树上
		EOPLL_CTL_DEL:从树上删除事件节点
		EOPLL_CTL_MOD:修改树上对应的事件节点
	fd:事件节点对应的文件描述符
	event:要操作的事件节点
	typedef union epoll_data {
               void        *ptr;
               int          fd;
               uint32_t     u32;
               uint64_t     u64;
           } epoll_data_t;
	struct epoll_event {
    	uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
     };
	event.events常用的有:
	EPOLLIN: 读事件
	EPOLLOUT: 写事件
	EPOLLERR: 错误事件
    EPOLLET: 边缘触发模式
	event.fd: 要监控的事件对应的文件描述符

int epoll_wait(int pefd,struct epoll_event *events,int maxevents,int timeout);
函数说明:等待内核返回事件发生
参数说明:
	epfd:epoll树根
	events:传出参数,时间结构体数组
	maxevents:数组大小
	timeout:
		-1:表示永久阻塞
		0:立即返回
		>0:表示超时等待事件
返回值:
	成功:返回发生事件的个数
	失败:若timeout=0,没有事件发生,则返回;返回-1,设置errno值

epoll_wait的events是一个传出参数, 调用epoll_ctl传递给内核什么值, 当epoll_wait返回的时候, 内核就传回什么值,不会对struct event的结构体变量的值做任何修改.

测试代码:

int main(){
	int lfd = Socket(AF_INET,SOCK_STREAM,0);
	
	int opt = 1;
	setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
	
	struct sockaddr_in serv;
	bzero(&serv,sizeof(serv));
	serv.sin_family=AF_INET;
	serv.sin_port=htons(8888);
	serv.sin_addr.s_addr=htonl(INADDR_ANY);
	Bind(lfd,(struct sockaddr *)&serv,sizeof(serv));
	
	Listen(lfd,128);

	//创建epoll树
	int epfd = epoll_create(1024);

	//将lfd上树
	struct epoll_event ev; 
	ev.events = EPOLLIN;
	ev.data.fd=lfd;
	//要监听的节点添加到epoll树上
	epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);
	
	struct epoll_event events[1024]; //定义一个传出数组
	int nready=0;
	int i=0;
	int n=0;
	char buf[1024];
	int cfd=0;
	int sockfd=0;
	while(1){
		//阻塞,内核等待事件发生,保存修改或者发生事件的文件描述符
		nready = epoll_wait(epfd,events,1024,-1);
		if(nready<0){
			if(errno==EINTR){
				continue;
			}
			break;
		}
		//有事件发生时,有客户端连接请求
		for(i=0;i<nready;i++){
			sockfd = events[i].data.fd;
			//判断是否是lfd,是则accept
			if(sockfd==lfd){
				cfd=Accept(lfd,NULL,NULL);
				ev.data.fd=cfd;
				ev.events = EPOLLIN;
				//cfd上树
				epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
				continue;
			}
			//有客户端发送数据过来
			memset(buf,0x00,sizeof(buf));
			n = Read(sockfd,buf,sizeof(buf));
			if(n<=0){
				//读不到数据,关闭通信文件描述符,从树上删除
				close(sockfd);
				epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
				perror("read error or client close");
				continue;
			}
			printf("n==[%d], buf==[%s] n",n,buf);
			Write(sockfd,buf,n);
		}
	}
	close(epfd);
	close(lfd);
	return 0;
}

3.4 epoll的两种工作模式
epoll的两种模式ET和LT模式
水平触发LT:高电平代表1,只要缓冲区中有数据,就一直通知,epoll默认情况下是LT模式,在这种模式下若读数据一次性没读完,缓冲区中还有可读数据,则epoll_wait还会再通知.
边缘触发ET:电平有变化就代表1,缓冲区中有数据指挥通知一次,之后再有数据才会通知(若是读数据的时候没有读完,则剩余的数据不会再通知,直到有新的数据到来)epoll设置为ET模式,若读数据的时候一次性没有读完,则epoll_wait不再通知,知道下次有新的数据发来.

在ET模式下,如何在epoll_wait返回一次的情况下读完数据:循环读数据,直到读完数据,但是读完数据之后会阻塞
若一次性读完还需设置什么?将通信文件描述符设置为非阻塞(fcntl函数)

3.5 epoll反应堆
反应堆:一个小事件触发一系列反应
epoll反应堆思想:c++封装思想(把数据和操作封装到一起),将描述符,事件,对应的处理方法封装在一起,当描述符对应的事件发生了,自动调用处理方法.
epoll反应堆的核心思想是:在调用epoll_ctl函数的时候,将events上树的时候,利用epoll_data_t的ptr成员,将一个文件描述符,事件和回调函数封装成一个结构体,然后让ptr指向这个结构体,然后调用epoll_wait函数返回的时候,可用得到具体的events,然后获得events结构体中的events.data.ptr指针,ptr指针指向的结构体中有回调函数,最终可以调用这个回调函数.

4 线程池

线程池:若干个线程组合到一起,形成线程池.
线程池和任务池:任务池相当于共享资源,所以需要使用互斥锁,当任务池中没有任务的时候需要让线程池阻塞,所以需要使用条件变量.
在这里插入图片描述
线程池流程:

  1. 初始化操作:线程数量,任务总数,malloc内存,互斥锁和条件变量初始化,创建指定数量的子线程
  2. 主线程:负责往线程池中添加任务.先加锁,然后判断任务池中是否已满,若满,则调用pthread_cond_wait阻塞等待;若未满,则往任务池中添加任务,添加完成之后调用pthread_cond_signal通知子线程取任务;最后解锁
  3. 子线程:负责从任务池中取任务并处理任务;先加锁,然后判断任务池中是否有任务;若任务池中没有任务,则调用pthread_cond_wait函数等待主线程添加任务;若任务池中有任务,则取任务并处理任务;处理完成之后,通知pthread_cond_signal主线程继续添加任务;若shutdown为1,先解锁,然后自行退出pthread_exit,最后释放锁.
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "threadpool.h"

#define DEFAULT_TIME 10                 /*10s检测一次*/
#define MIN_WAIT_TASK_NUM 10            /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/ 
#define DEFAULT_THREAD_VARY 10          /*每次创建和销毁线程的个数*/
#define true 1
#define false 0

typedef struct 
{
    void *(*function)(void *);          /* 函数指针,回调函数 */
    void *arg;                          /* 上面函数的参数 */
} threadpool_task_t;                    /* 各子线程任务结构体 */

/* 描述线程池相关信息 */
struct threadpool_t 
{
    pthread_mutex_t lock;               /* 用于锁住本结构体 */    
    pthread_mutex_t thread_counter;     /* 记录忙状态线程个数de琐 -- busy_thr_num */

    pthread_cond_t queue_not_full;      /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
    pthread_cond_t queue_not_empty;     /* 任务队列里不为空时,通知等待任务的线程 */

    pthread_t *threads;                 /* 存放线程池中每个线程的tid。数组 */
    pthread_t adjust_tid;               /* 存管理线程tid */
    threadpool_task_t *task_queue;      /* 任务队列(数组首地址) */

    int min_thr_num;                    /* 线程池最小线程数 */
    int max_thr_num;                    /* 线程池最大线程数 */
    int live_thr_num;                   /* 当前存活线程个数 */
    int busy_thr_num;                   /* 忙状态线程个数 */
    int wait_exit_thr_num;              /* 要销毁的线程个数 */

    int queue_front;                    /* task_queue队头下标 */
    int queue_rear;                     /* task_queue队尾下标 */
    int queue_size;                     /* task_queue队中实际任务数 */
    int queue_max_size;                 /* task_queue队列可容纳任务数上限 */

    int shutdown;          /* 标志位,线程池使用状态,true或false */
};

void *threadpool_thread(void *threadpool);

void *adjust_thread(void *threadpool);

int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);

//threadpool_create(3,100,100);  
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
    int i;
    threadpool_t *pool = NULL;
    do 
	{
        if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL) 
		{  
            printf("malloc threadpool fail");
            break;                      /*跳出do while*/
        }

        pool->min_thr_num = min_thr_num;
        pool->max_thr_num = max_thr_num;
        pool->busy_thr_num = 0;
        pool->live_thr_num = min_thr_num;   /* 活着的线程数 初值=最小线程数 */
        pool->wait_exit_thr_num = 0;
        pool->queue_size = 0;                 /* 有0个产品 */
        pool->queue_max_size = queue_max_size;
        pool->queue_front = 0;
        pool->queue_rear = 0;
        pool->shutdown = false;       /* 不关闭线程池 */

        /* 根据最大线程上限数, 给工作线程数组开辟空间, 并清零 */
        pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num); 
        if (pool->threads == NULL) 
		{
            printf("malloc threads fail");
            break;
        }
        memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);

        /* 队列开辟空间 */
        pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
        if (pool->task_queue == NULL) 
		{
            printf("malloc task_queue fail\n");
            break;
        }

        /* 初始化互斥琐、条件变量 */
        if (pthread_mutex_init(&(pool->lock), NULL) != 0
                || pthread_mutex_init(&(pool->thread_counter), NULL) != 0
                || pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
                || pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
        {
            printf("init the lock or cond fail\n");
            break;
        }

		//启动工作线程
		pthread_attr_t attr;
		pthread_attr_init(&attr);
		pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
        for (i = 0; i < min_thr_num; i++) 
		{
            pthread_create(&(pool->threads[i]), &attr, threadpool_thread, (void *)pool);/*pool指向当前线程池*/
            printf("start thread 0x%x...\n", (unsigned int)pool->threads[i]);
        }

		//创建管理者线程
        pthread_create(&(pool->adjust_tid), &attr, adjust_thread, (void *)pool);

        return pool;

    } while (0);

	/* 前面代码调用失败时,释放poll存储空间 */
    threadpool_free(pool);

    return NULL;
}

/* 向线程池中 添加一个任务 */
//threadpool_add(thp, process, (void*)&num[i]);   /* 向线程池中添加任务 process: 小写---->大写*/

int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
    pthread_mutex_lock(&(pool->lock));

    /* ==为真,队列已经满, 调wait阻塞 */
    while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown)) 
	{
        pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
    }

    if (pool->shutdown) 
	{
        pthread_cond_broadcast(&(pool->queue_not_empty));
        pthread_mutex_unlock(&(pool->lock));
        return 0;
    }

    /* 清空 工作线程 调用的回调函数 的参数arg */
    if (pool->task_queue[pool->queue_rear].arg != NULL) 
	{
        pool->task_queue[pool->queue_rear].arg = NULL;
    }

    /*添加任务到任务队列里*/
    pool->task_queue[pool->queue_rear].function = function;
    pool->task_queue[pool->queue_rear].arg = arg;
    pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size;       /* 队尾指针移动, 模拟环形 */
    pool->queue_size++;

    /*添加完任务后,队列不为空,唤醒线程池中 等待处理任务的线程*/
    pthread_cond_signal(&(pool->queue_not_empty));
    pthread_mutex_unlock(&(pool->lock));

    return 0;
}

/* 线程池中各个工作线程 */
void *threadpool_thread(void *threadpool)
{
    threadpool_t *pool = (threadpool_t *)threadpool;
    threadpool_task_t task;

    while (true) 
	{
        /* Lock must be taken to wait on conditional variable */
        /*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
        pthread_mutex_lock(&(pool->lock));

        /*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
        while ((pool->queue_size == 0) && (!pool->shutdown)) 
		{  
            printf("thread 0x%x is waiting\n", (unsigned int)pthread_self());
            pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));//暂停到这

            /*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
            if (pool->wait_exit_thr_num > 0) 
			{
                pool->wait_exit_thr_num--;

                /*如果线程池里线程个数大于最小值时可以结束当前线程*/
                if (pool->live_thr_num > pool->min_thr_num) 
				{
                    printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
                    pool->live_thr_num--;
                    pthread_mutex_unlock(&(pool->lock));
					//pthread_detach(pthread_self());
                    pthread_exit(NULL);
                }
            }
        }

        /*如果指定了true,要关闭线程池里的每个线程,自行退出处理---销毁线程池*/
        if (pool->shutdown) 
		{
            pthread_mutex_unlock(&(pool->lock));
            printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
            //pthread_detach(pthread_self());
            pthread_exit(NULL);     /* 线程自行结束 */
        }

        /*从任务队列里获取任务, 是一个出队操作*/
        task.function = pool->task_queue[pool->queue_front].function;
        task.arg = pool->task_queue[pool->queue_front].arg;

        pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size;       /* 出队,模拟环形队列 */
        pool->queue_size--;

        /*通知可以有新的任务添加进来*/
        pthread_cond_broadcast(&(pool->queue_not_full));

        /*任务取出后,立即将 线程池琐 释放*/
        pthread_mutex_unlock(&(pool->lock));

        /*执行任务*/ 
        printf("thread 0x%x start working\n", (unsigned int)pthread_self());
        pthread_mutex_lock(&(pool->thread_counter));                            /*忙状态线程数变量琐*/
        pool->busy_thr_num++;                                                   /*忙状态线程数+1*/
        pthread_mutex_unlock(&(pool->thread_counter));

        (*(task.function))(task.arg);                                           /*执行回调函数任务*/
        //task.function(task.arg);                                              /*执行回调函数任务*/

        /*任务结束处理*/ 
        printf("thread 0x%x end working\n", (unsigned int)pthread_self());
        pthread_mutex_lock(&(pool->thread_counter));
        pool->busy_thr_num--;         /*处理掉一个任务,忙状态数线程数-1*/
        pthread_mutex_unlock(&(pool->thread_counter));
    }

    pthread_exit(NULL);
}

/* 管理线程 */
void *adjust_thread(void *threadpool)
{
    int i;
    threadpool_t *pool = (threadpool_t *)threadpool;
    while (!pool->shutdown) 
	{

        sleep(DEFAULT_TIME);             /*定时 对线程池管理*/

        pthread_mutex_lock(&(pool->lock));
        int queue_size = pool->queue_size;           /* 关注 任务数 */
        int live_thr_num = pool->live_thr_num;      /* 存活 线程数 */
        pthread_mutex_unlock(&(pool->lock));

        pthread_mutex_lock(&(pool->thread_counter));
        int busy_thr_num = pool->busy_thr_num;        /* 忙着的线程数 */
        pthread_mutex_unlock(&(pool->thread_counter));

        /* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/
        if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) 
		{
            pthread_mutex_lock(&(pool->lock));  
            int add = 0;

            /*一次增加 DEFAULT_THREAD 个线程*/
            for (i = 0; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY
                    && pool->live_thr_num < pool->max_thr_num; i++) 
			{
                if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i])) 
				{
                    pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
                    add++;
                    pool->live_thr_num++;
                }
            }

            pthread_mutex_unlock(&(pool->lock));
        }

        /* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 存活的线程数 大于 最小线程数时*/
        if ((busy_thr_num * 2) < live_thr_num  &&  live_thr_num > pool->min_thr_num) 
		{
            /* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
            pthread_mutex_lock(&(pool->lock));
            pool->wait_exit_thr_num = DEFAULT_THREAD_VARY;      /* 要销毁的线程数 设置为10 */
            pthread_mutex_unlock(&(pool->lock));

            for (i = 0; i < DEFAULT_THREAD_VARY; i++) 
			{
                /* 通知处在空闲状态的线程, 他们会自行终止*/
                pthread_cond_signal(&(pool->queue_not_empty));
            }
        }
    }

    return NULL;
}

int threadpool_destroy(threadpool_t *pool)
{
    int i;
    if (pool == NULL) 
	{
        return -1;
    }
    pool->shutdown = true;

    /*先销毁管理线程*/
    //pthread_join(pool->adjust_tid, NULL);

    for (i = 0; i < pool->live_thr_num; i++) 
	{
        /*通知所有的空闲线程*/
        pthread_cond_broadcast(&(pool->queue_not_empty));
    }

    /*for (i = 0; i < pool->live_thr_num; i++) 
	{
        pthread_join(pool->threads[i], NULL);
    }*/

    threadpool_free(pool);

    return 0;
}

int threadpool_free(threadpool_t *pool)
{
    if (pool == NULL) 
	{
        return -1;
    }

    if (pool->task_queue) 
	{
        free(pool->task_queue);
    }

    if (pool->threads) 
	{
        free(pool->threads);
        pthread_mutex_lock(&(pool->lock));
        pthread_mutex_destroy(&(pool->lock));
        pthread_mutex_lock(&(pool->thread_counter));
        pthread_mutex_destroy(&(pool->thread_counter));
        pthread_cond_destroy(&(pool->queue_not_empty));
        pthread_cond_destroy(&(pool->queue_not_full));
    }

    free(pool);
    pool = NULL;

    return 0;
}

int threadpool_all_threadnum(threadpool_t *pool)
{
    int all_threadnum = -1;

    pthread_mutex_lock(&(pool->lock));
    all_threadnum = pool->live_thr_num;
    pthread_mutex_unlock(&(pool->lock));

    return all_threadnum;
}

int threadpool_busy_threadnum(threadpool_t *pool)
{
    int busy_threadnum = -1;

    pthread_mutex_lock(&(pool->thread_counter));
    busy_threadnum = pool->busy_thr_num;
    pthread_mutex_unlock(&(pool->thread_counter));

    return busy_threadnum;
}

int is_thread_alive(pthread_t tid)
{
    int kill_rc = pthread_kill(tid, 0);     //发0号信号,测试线程是否存活
    if (kill_rc == ESRCH) 
	{
        return false;
    }

    return true;
}

/*测试*/ 
#if 1
/* 线程池中的线程,模拟处理业务 */
void *process(void *arg)
{
    printf("thread 0x%x working on task %d\n ",(unsigned int)pthread_self(),*(int *)arg);
    sleep(1);
    printf("task %d is end\n", *(int *)arg);

    return NULL;
}

int main(void)
{
    /*threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);*/
    threadpool_t *thp = threadpool_create(3,100,100);   /*创建线程池,池里最小3个线程,最大100,队列最大100*/
    printf("pool inited");

    //int *num = (int *)malloc(sizeof(int)*20);
    int num[20], i;
    for (i = 0; i < 20; i++) 
	{
        num[i]=i;
        printf("add task %d\n",i);
        threadpool_add(thp, process, (void*)&num[i]);   /* 向线程池中添加任务 */
    }

    sleep(10);                /* 等子线程完成任务 */
    threadpool_destroy(thp);

    return 0;
}
#endif

5 UDP通信

TCP:传输控制协议,面向连接的,稳定的,可靠的,安全的数据流传递
稳定和可靠:丢包重传
数据有序:序号和确认序号(SYN和ACK)
流量控制:滑动窗口
UDP:用户数据报协议,面向无连接的,不稳定,不可靠,不安全的数据报传递,更像是收发短信,UDP传输不需要建立连接,传输效率更高,在稳定的局域网环境内相对可靠

UDP通信相关函数:

size_t recvfrom(int sockfd,void *buf,size_t len,int flags,struct sockaddr *src_addr,socklen_t *addrlen);
函数说明:接收消息
参数说明:
	sockfd:套接字(文件描述符)
	buf:要接受的缓冲区
	len:缓冲区的长度
	flags:标志位,一般填0
	src_addr:原地址 传出参数
	addrlen:发送方地址长度
返回值: 成功返回读到的字节数
		失败:返回-1,设置errno
调用该函数相当于TCP通信的recv+accept函数

size_t sendto(int sockfd,const void *buf,size_t len,int flags,const struct sockaddr *dest_addr,socklen_t addrlen);
函数说明:发送数据
参数说明:
	sockfd:套接字
	dest_addr:目的地址
	addrlen:目的地址长度
返回值:成功返回写入的字节数
	   失败:返回-1,设置errno

UDP通信服务端流程:

  1. 创建socket,cfd=socket(AF_INET,SOCK_DGREAM,0);
  2. 绑定bind
  3. while(1){读取数据recvfrom;发送数据sendto;}
  4. 关闭文件
#include<stdio.h>
#include<stdlib.h>
#include <string.h>
#include<sys/types.h>
#include<unistd.h>
#include<arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <netinet/udp.h>
#include <ctype.h>
int main(){
	int cfd = socket(AF_INET,SOCK_DGRAM,0);
	if(cfd<0){
		perror("socket error");
		return -1;
	}
	struct sockaddr in serv;
	struct sockaddr in client;
	bzero(&serv,sizeof(serv));
	serv.sin_family=AF_INET;
	serv.sin_port = htons(8888);
	serv.sin_addr.s_addr = htonl(INADDR_ANY);

	bind(cfd,(struct sockaddr *)&serv,sizeof(serv));

	int n;
	char buf[1024];
	socklen_t len;
	int i=0;
	while(1){
		memset(buf,0x00,sizeof(buf));
		len = sizeof(client);
		//读取数据
		n=recvfrom(cfd,buf,sizeof(buf),0,(struct sockaddr *)&client,&len);
		for(i=0;i<n;i++){
			buf[i] = toupper(buf[i]);
		}
		printf("[%d]:n==[%d],buf==[%s]\n",ntohs(client.sin_port),n,buf);
		//发送数据
		sendto(cfd,buf,n,0,(struct sockaddr *)&client,len);
	}
	//关闭套接字
	close(cfd);
	return 0;
}

//可连接多个客户端,当服务端关闭客户端依然建立连接,但是不能发送数据

UDP客户端流程:

  1. 创建套接字:socket,cfd=socket(AF_INET,SOCK_DGREAM,0);
  2. while(1){收消息recvfrom;发消息sendto;}
  3. 关闭套接字 close
int main(){
	//创建socket
	int cfd = socket(AF_INET,SOCK_DGRAM,0);
	if(cfd<0){
		perror("socket error");
		return -1;
	}
	struct sockaddr_in serv;
	serv.sin_family = AF_INET;
	serv.sin_port = htons(8888);
	inet_pton(AF_INET,"127.0.0.1",&serv.sin_addr.s_addr);
	
	int n;
	char buf[1024];
	socklen_t len;
	int i=0;
	while(1){
		//读标准输入数据
		memset(buf,0x00,sizeof(buf));
		n= read(STDIN_FILENO,buf,sizeof(buf));
		//发送数据
		sendto(cfd,buf,n,0,(struct sockaddr *)&serv,sizeof(serv));
		//读取数据
		memset(buf,0x00,sizeof(buf));
		n=recvfrom(cfd,buf,sizeof(buf),0,NULL,NULL);
		printf("n==[%d], buf==[%s] n",n,buf);
	}
	close(cfd);
	return 0;
}

使用nc命令进行测试: nc -u 127.1 8888

本地socket通信:

man 7 unix

int socket(int domain,int type,int protocol);
函数说明:创建本地域socket
函数参数:
	domain:AF_UNIX or AF_LOCAL
	type:SOCK_STREAM 或者 SOCK_DGRAM
	PROTOCOL:0表示使用默认协议
函数返回值: 成功:返回文件描述符
		   失败:返回-1,并设置errno值
		 
int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
函数说明:绑定套接字
函数参数:
	socket:由socket函数返回的文件描述符
	addr:本地地址
	addlen:本地地址长度
函数返回值:成功:返回文件描述符
			失败:返回-1,并设置errno值
bind函数会自动创建socket文件,若在调用bind函数之前,socket文件已经存在,则调用bind会报错,可用使用unlink函数在bind之前先删除文件

本地套接字服务器的流程:
	可以使用TCP的方式, 必须按照tcp的流程 
	也可以使用UDP的方式, 必须按照udp的流程 
 
tcp的本地套接字服务器流程:
	创建套接字  socket(AF_UNIX,SOCK_STREAM,0)
	绑定 struct sockaddr_un &强转
	侦听 listen 
	获得新连接 accept 
	循环通信 read-write 
	关闭文件描述符 close

tcp本地套接字客户端流程:
	调用socket创建套接字
	调用bind函数将socket文件描述和socket文件进行绑定.
	不是必须的, 若无显示绑定会进行隐式绑定,但服务器不知道谁连接了.
	调用connect函数连接服务端
	循环通信read-write
	关闭文件描述符 close

服务器端代码:

int main()
{
	//创建socket
	int lfd = socket(AF_UNIX, SOCK_STREAM, 0);
	if(lfd<0)
	{
		perror("socket error");
		return -1;
	}

	//删除socket文件,避免bind失败
	unlink("./server.sock");

	//绑定bind
	struct sockaddr_un serv;
	bzero(&serv, sizeof(serv));
	serv.sun_family = AF_UNIX;
	strcpy(serv.sun_path, "./server.sock"); 
	int ret = bind(lfd, (struct sockaddr *)&serv, sizeof(serv));
	if(ret<0)
	{
		perror("bind error");
		return -1;
	}

	//监听listen
	listen(lfd, 10);

	//接收新的连接-accept
	struct sockaddr_un client;
	bzero(&client, sizeof(client));
	int len = sizeof(client);
	int cfd = accept(lfd, (struct sockaddr *)&client, &len);
	if(cfd<0)
	{
		perror("accept error");	
		return -1;
	}
	printf("client->[%s]\n", client.sun_path);

	int n;
	char buf[1024];

	while(1)
	{
		//读数据
		memset(buf, 0x00, sizeof(buf));		
		n = read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			printf("read error or client close, n==[%d]\n", n);
			break;
		}
		printf("n==[%d], buf==[%s]\n", n, buf);

		//发送数据
		write(cfd, buf, n);
	}
	close(lfd);
	return 0;
}

客户端代码:

int main()
{
	//创建socket
	int cfd = socket(AF_UNIX, SOCK_STREAM, 0);
	if(cfd<0)
	{
		perror("socket error");
		return -1;
	}

	//删除socket文件,避免bind失败
	unlink("./client.sock");

	//绑定bind
	struct sockaddr_un client;
	bzero(&client, sizeof(client));
	client.sun_family = AF_UNIX;
	strcpy(client.sun_path, "./client.sock"); 
	int ret = bind(cfd, (struct sockaddr *)&client, sizeof(client));
	if(ret<0)
	{
		perror("bind error");
		return -1;
	}

	struct sockaddr_un serv;
	bzero(&serv, sizeof(serv));
	serv.sun_family = AF_UNIX;
	strcpy(serv.sun_path, "./server.sock");
	ret = connect(cfd, (struct sockaddr *)&serv, sizeof(serv));
	if(ret<0)
	{
		perror("connect error");	
		return -1;
	}

	int n;
	char buf[1024];

	while(1)
	{
		memset(buf, 0x00, sizeof(buf));
		n = read(STDIN_FILENO, buf, sizeof(buf));

		//发送数据
		write(cfd, buf, n);

		//读数据
		memset(buf, 0x00, sizeof(buf));		
		n = read(cfd, buf, sizeof(buf));
		if(n<=0)
		{
			printf("read error or client close, n==[%d]\n", n);
			break;
		}
		printf("n==[%d], buf==[%s]\n", n, buf);
	}

	close(cfd);
	return 0;
}

TCP和UDP的区别:TCP是建立连接的一对一的服务,类似于打电话,必须对方接听才能进行通话,UDP是无连接的,类似于发短信,无论对方什么状态都可以进行通信.

6 libevent的使用

6.1 libevent的地基event_base:使用libevent函数之前需要分配一个或者多个event_base结构体,每个event_base结构体持有一个事件集合,可以检测以确定哪个事件是激活的,event_base结构相当于epoll红黑树的树根节点,每个event_base都有一种用于检测某种事件已经就绪的方法.

struct event_base *event_base_new(void);
函数说明:获得event_base结构
返回值:成功返回event_base结构
	   失败返回NULL
	 
void event_base_free(struct event_base *);
函数说明:释放event_base指针

int event_reinit(struct event_base *base);
函数说明:如果有子进程,且子进程也要使用base,则子进程需要对event_base重新初始化,此时需要调用event_reinit函数
函数参数:由event_base_new返回的执行event_base结构的指针
返回值:成功返回0,失败返回-1

const char **event_get_supported_methods(void);
函数说明:获得当前系统支持的方法有哪些
返回值:返回二维数组

const char *event_base_get_method(const struct event_base *base);
函数说明:获得当前base节点使用的多路IO方法
函数参数:event_base结构的base指针
返回值:获得当前base节点使用的多路IO方法的指针

查看当前系统支持的多路IO方法和当前所使用的方法:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<event2/event.h>
int main(){
	int i=0;
	//获取当前系统支持的方法
	const char **p = event_get_supported_methods();
	while(p[i]!=NULL){
		printf("%s \t",p[i++]);
	}
	printf("\n");

	//获取地基节点
	struct event_base*base = event_base_new();
	if(base==NULL){
		printf("event_base_new errorn");
		return -1;
	}
	//获取当前系统使用的方法
	const char *pp = event_base_get_method(base);
	printf("%s\n",pp);
	//释放节点
	event_base_free(base);
}

//ln -s /usr/lib/libevent-2.0.so.5 /usr/lib64/libevent-2.0.so.5

等待事件产生-循环等待event_loop:libevent在地基打好之后,需要等待事件的产生,也就是等待事件被激活,所以程序不能退出,对于epoll来说,我们需要自己控制循环,接口如下:

int event_base_loop(struct event_base *base,int flags);
函数说明:进入循环等待事件
参数说明:
	base:由event_base_new函数返回的指向event_base结构的指针
	flags的取值:
		#define EVLOOP_ONCE 0x01 只触发一次,如果事件没有被触发,阻塞等待
		#define EVLOOP_NONBLOCK 0x02 非阻塞方式检测事件是否被触发,不管事件触发与否,都会立即返回

int event_base_dispatch(struct event_base *base);
函数说明:进入循环等待事件
参数说明:由event_base_neew函数返回的指向event_base结构的指针
调用该函数,相当于没有设置标志位的event_base_loop.程序将会一直运行,直到没有需要检测的事情了,或者被结束循环的API终止.

int event_base_loopexit(struct event_base *base,const struct timeval *tv);
int event_base_loopbreak(struct event_base *base);
struct timeval{
	long tv_sec;
	long tv_usec;
};
两个函数的区别是如果正在执行激活时间的回调函数,那么event_base_loopexit将在事件回调执行结束后终止循环,而event_base_loopbreak会立即终止循环.

使用libevent库的步骤:

  1. 创建根节点 event_base_new
  2. 设置监听事件和数据可读可写的事件的回调函数,设置事件对应的回调函数之后,事件产生时会自动调用回调函数
  3. 事件循环event_base_dispatch,相当于while(1),在循环内部等待事件发生,若事件发生则会触发事件对应的回调函数
  4. 释放根节点event_base_free,释放由event_base_new和event_new创建的资源,分别调用event_base_free和event_free函数.
typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
函数说明:event_new负责创建event结构指针,同时指定对应的地基base,还有对应的文件描述符,事件,以及回调函数和回调函数参数
参数说明:
	base:对应的根节点 -- 地基
	fd:要监听的文件描述符
	events:要监听的事件
		#define EV_TIMEOUT 0x01 //超时事件
		#define EV_READ    0x02 //读事件
		#define EV_WRITE   0x04 //写事件
		#define EV_SIGNAL  0x08 //信号事件
		#define EV_PERSIST 0x10 //周期性触发
		#define EV_ET      0x20 //边缘触发,如果底层模型支持设置,则有效,若不支持则无效
		若想设置持续的读事件则:EV_READ | EV_PERSIST
	cd回调函数:
		typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
		参数:对应于event_new函数的fd, event和arg

int event_add(struct event *ev,const struct timeval *timeout);
函数说明:将非未决态事件转为未决态,相当于调用epoll_ctl函数(EPOLL_CTL_ADD),开始监听事件是否产生,相当于epoll的上树操作
参数说明:
	ev:调用event_new创建的事件
	timeout:限时等待事件的产生,也可以设置NULL,没有限时

int event_del(struct event *ev);
函数说明:将事件从未决态变为非未决态,相当于epoll的下树(epoll_ctl(EPOLL_CTL_DEL))操作
参数说明:
	ev指的是由event_new创建的事件

void event_free(struct event *ev);
函数说明:释放由event_new申请的event节点

编写基于event实现的tcp服务器

  1. 创建socket – socket()
  2. 设置端口复用 – setsockopt(lfd,SOLSOCKET,SO_REUSEADDR,&opt,sizeof(int));
  3. 绑定 – bind()
  4. 设置监听 – listen()
  5. 创建地基:struct event_base *base = event_base_new();
  6. 创建lfd对应的事件struct event *ev = event_new(base,lfd,EV_REAF|EV_PERSIST,conncb,&arg)
  7. 上event_base地基event_add(ev,NULL);
  8. 进入事件循环event_base_dispatch(base);
  9. 释放资源:event_base_free(base);event_free(ev);
//编写libevent服务端
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <event2/event.h>

struct event *connev = NULL;

//typedef void (*event_callback_fn)(evutil_socket_t fd, short events, void *arg);
void readcb(evutil_socket_t fd, short events, void *arg)
{
	int n;
	char buf[1024];
	memset(buf, 0x00, sizeof(buf));
	n = read(fd, buf, sizeof(buf));
	if(n<=0)
	{
		close(fd);
		//将通信文件描述符对应的事件从base地基上删除
		event_del(connev);
	}
	else
	{
		write(fd, buf, n);
	}
}

void conncb(evutil_socket_t fd, short events, void *arg)
{
	struct event_base *base = (struct event_base *)arg;

	//接受新的客户端连接
	int cfd = accept(fd, NULL, NULL);
	if(cfd>0)
	{
		//创建通信文件描述符对应的事件并设置回调函数为readcb
		connev = event_new(base, cfd, EV_READ|EV_PERSIST, readcb, NULL);
		if(connev==NULL)
		{
			//退出循环
			event_base_loopexit(base, NULL);
		}
		
		//将通信文件描述符对应的事件上event_base地基
		event_add(connev, NULL);	
	}	
}

int main()
{
	//创建socket
	int lfd = socket(AF_INET, SOCK_STREAM, 0);

	//设置端口复用
	int opt = 1;
	setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	//绑定
	struct sockaddr_in serv;
	bzero(&serv, sizeof(serv));
	serv.sin_addr.s_addr = htonl(INADDR_ANY);
	serv.sin_port = htons(8888);
	serv.sin_family = AF_INET;
	bind(lfd, (struct sockaddr*)&serv, sizeof(serv));

	//监听
	listen(lfd, 120);

	//创建地基
	struct event_base *base = event_base_new();
	if(base==NULL)
	{
		printf("event_base_new error\n");
		return -1;
	}

	//创建监听文件描述符对应的事件
	//struct event *event_new(struct event_base *base, evutil_socket_t fd, short events, event_callback_fn cb, void *arg);
	struct event *ev = event_new(base, lfd, EV_READ|EV_PERSIST, conncb, base);
	if(ev==NULL)
	{
		printf("event_new error\n");
		return -1;
	}

	//将新的事件节点上base地基
	event_add(ev, NULL);

	//进入事件循环等待
	event_base_dispatch(base);

	//释放资源
	event_base_free(base);
	event_free(ev);

	close(lfd);
	return 0;
}
//假如有3个客户端连接,将第一个关闭时,后面两个将不能通信,若直接关闭第三个,则前两个还可以通信.是因为,connev是全局变量,任何进程均可访问,event_del(connev)时会将最后一个下树.
//解决方法:通过事件来判断把那个客户端下树,fd和事件在结构体中是一一对应的

6.2 自带buffer事件,bufferevent内部有两个缓冲区,以及一个文件描述符.一个文件描述符有读和写两个缓冲区,bufferevent同样也带有两个缓冲区,还有就是libevent事件驱动的核心回调函数,四个缓冲区以及触发回调的关系如下:
在这里插入图片描述
一个bufferevent对应两个缓冲区, 三个回调函数, 分别是写回调, 读回调和事件回调.
bufferevent的三个回调函数:

  • 读回调:当bufferevent将底层读缓冲区的数据读到自身的读缓冲区时触发读事件回调
  • 写回调:当bufferevent将自身写缓冲的数据写到底层写缓冲区时触发写事件回调,由于数据最终是写入了内核的写缓冲区中,应用程序已经无法控制,这个事件基本只是起到通知功能
  • 事件回调:当bufferevent绑定socket连接,断开或者异常的时候触发事件回调.

主要函数:

struct bufferevent *bufferevent_socket_new(struct event_base *base,evutil_socket_t fd,int options);
函数说明:bufferevent_socket_new对已经存在socket创建bufferevent事件,可用于后面的连接监听器的回调函数中.
参数说明:
	base:对应根节点
	fd:文件描述符
	options:bufferevent的选项
		BEV_OPT_CLOSE_ON_FREE 释放bufferevent自动关闭底层接口(当bufferevent被释放后,文件描述符也随之被close)
		BEV_OPT_THREADSAFE 使bufferevent能够在多线程下是安全的

int bufferevent_socket_connect(struct bufferevent *bev,struct sockaddr *serv,int socklen);
函数说明:该函数封装了底层的socket与connect接口,通过调用此函数可以将bufferevent事件与通信的socket进行绑定
参数说明:
	bev:需要提前初始化bufferevent事件
	serv:对端的ip地址,端口,协议的结构指针
	socklen:描述serv的长度
说明:调用此函数以后,通信的socket与bufferevent缓冲区做了绑定,
后面调用bufferevent_setcb函数以后,会对bufferevent缓冲区的读写操作的事件设置回调函数,
当往缓冲区中写数据的时候会触发写回调函数,
当数据从socket的内核缓冲区读到bufferevent读缓冲区中的时候会触发读回调函数
	
void bufferevent_free(struct bufferevent *bufev);
函数说明:释放bufferevent

void bufferevent_setcb(struct bufferevent *bufev,
	bufferevent_data_cb readcb,bufferevent_data_cb writecb,
	buffer_event_cb eventcb,void *cbarg);
函数说明:bufferevent_setcb用于设置bufferevent的回调函数,readcb,writecb,eventcb分别对应了读回调,写回调和事件回调,cbarg代表回调函数的参数.

回调函数的原型:
typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev, short what, void *ctx);
	What 代表 对应的事件
	BEV_EVENT_EOF--遇到文件结束指示
	BEV_EVENT_ERROR--发生错误
	BEV_EVENT_TIMEOUT--发生超时
	BEV_EVENT_CONNECTED--请求的过程中连接已经完成

int bufferevent_write(struct bufferevent *bufev,const void *data,size_t size);
函数说明:bufferevent_write是将data的数据写到bufferevent的写缓冲区

size_t bufferevent_read(struct bufferevent *bufev,void *data,size_t size);
函数说明:bufferevent_read是将bufferevent的读缓冲区数据读到data中,同时将读到的数据从bufferevent的读缓冲区中清除

int bufferevent_enable(struct bufferevent *bufev,short event);
int bufferevent_disable(struct bufferevent *bufev,short event);
函数说明:bufferevent_enable与bufferevent_disable是设置事件是否生效, 如果设置为disable, 事件回调将不会被触发

6.3 链接监听器evconnlistener
链接监听器封装了底层的socket通信相关函数,比如:socket,bind,listen,accept.链接监听器创建后实际上调用了socket,bind,listen.此时等待新的客户端连接到来,如果有新的客户端连接,那么内部先进性调用accept处理,然后调用用户指定的回调函数.

struct evconnlistener *evconnlistener_new_bind(struct event_base *base,
    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog,
	const struct sockaddr *sa, int socklen);
函数说明:在没有套接字的情况下对连接监听器进行初始化
参数:
	cb:是有新连接之后的回调函数,但是注意这个回调函数触发的时候,链接器已经处理好新连接了,并将与新连接通信的描述符交给回调函数
	ptr是回调函数的参数
	backlog是listen函数的关键参数,若backlog是-1,那么监听器会自动选择一个合适的值,如果填0,那么监听器会认为listen函数已经被调用过了
	sa和socklen是bind函数的参数
	flags:
		LEV_OPT_LEAVE_SOCKETS_BLOCKING   文件描述符为阻塞的
		LEV_OPT_CLOSE_ON_FREE            关闭时自动释放
		LEV_OPT_REUSEABLE                端口复用
		LEV_OPT_THREADSAFE               分配锁, 线程安全

回调函数:
typedef void (*evconnlistener_cb)(struct evconnlistener *evl, evutil_socket_t fd, struct sockaddr *cliaddr, int socklen, void *ptr);
参数:
	fd:与客户端通信的描述符,cliaaddr对应的也是新连接的对端地址信息

void evconnlistener_free(struct evconnlistener *lev);
函数说明: 释放链接监听器

int evconnlistener_enable(struct evconnlistener *lev);
函数说明: 使链接监听器生效

int evconnlistener_disable(struct evconnlistener *lev);
函数说明: 使链接监听器失效

在这里插入图片描述
libevent开发流程图:
在这里插入图片描述

7 HTML超文本标记语言

<html> 开始 和</html> 结束,属于html的根标签
<head></head> 头部标签,头部标签内一般有 <title></title>
<body></body> 主体标签,一般用于显示内容

题目标签:
	共有6,<h1>,<h2>,<h6>,其中<h1>最大,<h6>最小
文本标签:
	<font size={1-7} color=red></font>
换行标签:<br/>
水平线:<hr/>
列表标签:
	<ul><ol>无序和有序
	<ul>
		<li>列表内容1</li>
		<li>列表内容2</li></ul>
	无序列表可以设置type属性:
	实心圆圈:type=disc
	空心圆圈:type=circle
	小方块:  type=square
	有序列表的格式如下:
	<ol>
		<li>列表内容1</li>
		<li>列表内容2</li></ol>
	有序列表同样可以设置type属性
	数字:type=1,也是默认方式
	英文字母:type=a或type=A
	罗马数字:type=i或type=I
图片标签:
	<img>
	src=3.gif” 图片来源,必写
	alt=”小岳岳” 图片不显示时,显示的内容
	title=”我的天呐” 鼠标移动到图片上时显示的文字
	width=600” 图片显示的宽度
	height=400” 图片显示的高度
超链接:
	<a>
	href="http://www.itcast.cn",前往地址,必填,注意要写http://
	title="百度" 鼠标移动到链接上时显示的文字
	target="_self"或者”_blank”,_self是默认值,在自身页面打开,_blank是新开页面前往连接地址:
<a href="http://www.baidu.com" title="百度" target="_self" >百度</a>

http超文本传输协议:
http请求消息:
分为四部分:

  1. 请求行 说明请求类型,要访问的资源,以及使用的http版本
  2. 请求头 说明服务器使用的附加信息,都是键值对,比如表明浏览器类型
  3. 空行 不能省略-而且是\r\n,包括请求行和请求头都是以\r\n结尾
  4. 请求数据 表明请求的特定数据内容,可以省略-如登陆时,会将用户名和密码内容作为请求数据

请求类型:http协议有很多种请求类型,最多的是get和post请求。
get:请求指定的页面信息,并返回实体主体
post:向指定资源提交数据进行处理请求。数据被包含在请求体中。post请求可能会导致新的资源的建立和/或已有资源的修改。
区别:post方式不显示提交的数据;get会显示,如用户名和密码之类的数据;使用post比get安全。

http响应消息:
响应消息是代表服务器收到请求消息后,给浏览器做的反馈,所以响应消息是服务器发送给浏览器的,响应消息分为四部分:

  1. 状态行 包括http版本号,状态码,状态信息
  2. 消息报头 说明客户端要使用的一些附加信息,也是键值对
  3. 空行 \r\n 同样不能省略
  4. 响应正文 服务器返回给客户端的文本信息

http常见状态码:

  1. 1xx 指示信息–表示请求已接收,继续处理
  2. 2xx 成功–表示请求已被成功接收、理解、接受
  3. 3xx 重定向–要完成请求必须进行更进一步的操作
  4. 4xx 客户端错误–请求有语法错误或请求无法实现
  5. 5xx 服务器端错误–服务器未能实现合法的请求

常见状态码:

  • 200 OK 客户端请求成功
  • 301 Moved Permanently 重定向
  • 400 Bad Request 客户端请求有语法错误,不能被服务器所理解
  • 401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
  • 403 Forbidden 服务器收到请求,但是拒绝提供服务
  • 404 Not Found 请求资源不存在,eg:输入了错误的URL
  • 500 Internal Server Error 服务器发生不可预期的错误
  • 503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常

web服务器:
使用http协议传送html文件,这只是应用层协议,我们需要一个传输层协议来完成我们的传输数据的工作,可选择TCP+HTTP
在这里插入图片描述
基于epoll的web服务器

//web服务端程序--使用epoll模型
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
#include <dirent.h>
#include "pub.h"
#include "wrap.h"

int http_request(int cfd);
int main()
{
	//改变当前进程的工作目录
	char path[255] = {0};
	sprintf(path, "%s/%s", getenv("HOME"), "webpath");
	chdir(path);
	
	//创建socket--设置端口复用---bind
	int lfd = tcp4bind(9999, NULL);
	
	//设置监听
	Listen(lfd, 128);

	//创建epoll树
	int epfd = epoll_create(1024);
	if(epfd<0)
	{
		perror("epoll_create error");
		close(lfd);
		return -1;
	}
	
	//将监听文件描述符lfd上树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	
	int i;
	int cfd;
	int nready;
	int sockfd;
	struct epoll_event events[1024];
	while(1)
	{
		//等待事件发生
		nready = epoll_wait(epfd, events, 1024, -1);
		if(nready<0)
		{
			if(errno==EINTR)
			{
				continue;
			}
			break;
		}
		
		for(i=0; i<nready; i++)
		{
			sockfd = events[i].data.fd;
			//有客户端连接请求
			if(sockfd==lfd)
			{
				//接受新的客户端连接
				cfd = Accept(lfd, NULL, NULL);
				
				//设置cfd为非阻塞
				int flag = fcntl(cfd, F_GETFL);
				flag |= O_NONBLOCK;
				fcntl(cfd, F_SETFL, flag);
				
				//将新的cfd上树
				ev.data.fd = cfd;
				ev.events = EPOLLIN;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
			}
			else 
			{
				//有客户端数据发来
				http_request(cfd);
			}			
		}		
	}
}

int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
	char buf[1024] = {0};
	sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg);
	sprintf(buf+strlen(buf), "Content-Type:%s\r\n", fileType);
	if(len>0)
	{
		sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
	}
	strcat(buf, "\r\n");
	Write(cfd, buf, strlen(buf));
	return 0;
}

int send_file(int cfd, char *fileName)
{
	//打开文件
	int fd = open(fileName, O_RDONLY);
	if(fd<0)
	{
		perror("open error");
		return -1;
	}
	
	//循环读文件, 然后发送
	int n;
	char buf[1024];
	while(1)
	{
		memset(buf, 0x00, sizeof(buf));
		n = read(fd, buf, sizeof(buf));
		if(n<=0)
		{
			break;
		}
		else 
		{
			Write(cfd, buf, n);
		}
	}
}

int http_request(int cfd)
{
	int n;
	char buf[1024];
	//读取请求行数据, 分析出要请求的资源文件名
	memset(buf, 0x00, sizeof(buf));
	Readline(cfd, buf, sizeof(buf));
	printf("buf==[%s]\n", buf);
	//GET /hanzi.c HTTP/1.1
	char reqType[16] = {0};
	char fileName[255] = {0};
	char protocal[16] = {0};
	sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);
	printf("[%s]\n", reqType);
	printf("[%s]\n", fileName);
	printf("[%s]\n", protocal);
	
	char *pFile = fileName+1;
	printf("[%s]\n", pFile);
	
	//循环读取完剩余的数据
	while((n=Readline(cfd, buf, sizeof(buf)))>0);
	
	//判断文件是否存在
	struct stat st;
	if(stat(pFile, &st)<0)
	{
		printf("file not exist\n");
		
		//发送头部信息
		send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
		
		//发送文件内容
		send_file(cfd, "error.html");	
	}
	else //若文件存在
	{
		//判断文件类型
		//普通文件
		if(S_ISREG(st.st_mode))
		{
			printf("file exist\n");
			//发送头部信息
			send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
			
			//发送文件内容
			send_file(cfd, pFile);
		}
		//目录文件
		else if(S_ISDIR(st.st_mode))
		{
			
		}
	}
}

web服务端开发:

//web服务端程序--使用epoll模型
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <signal.h>
#include <dirent.h>
#include "pub.h"
#include "wrap.h"

int http_request(int cfd);

int main()
{
	//若客户端请求文件较大,传输过程中客户端关闭,则服务端还在写入,此时会造成管道破裂
	//设置信号,忽略SIGPIPE信号
	struct sigaction act;
    act.sa_handler = SIG_IGN;
    sigemptyset(&act.sa_mask);
    act.sa_flags=0;
    sigaction(SIGPIPE,&act,NULL);
	
	//改变当前进程的工作目录
	char path[255] = {0};
	sprintf(path, "%s/%s", getenv("HOME"), "webpath");
	chdir(path);
	
	//创建socket--设置端口复用---bind
	int lfd = tcp4bind(9999, NULL);
	
	//设置监听
	Listen(lfd, 128);

	//创建epoll树
	int epfd = epoll_create(1024);
	if(epfd<0)
	{
		perror("epoll_create error");
		close(lfd);
		return -1;
	}
	
	//将监听文件描述符lfd上树
	struct epoll_event ev;
	ev.data.fd = lfd;
	ev.events = EPOLLIN;
	epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
	
	int i;
	int cfd;
	int nready;
	int sockfd;
	struct epoll_event events[1024];
	while(1)
	{
		//等待事件发生
		nready = epoll_wait(epfd, events, 1024, -1);
		if(nready<0)
		{
			if(errno==EINTR)
			{
				continue;
			}
			break;
		}
		
		for(i=0; i<nready; i++)
		{
			sockfd = events[i].data.fd;
			//有客户端连接请求
			if(sockfd==lfd)
			{
				//接受新的客户端连接
				cfd = Accept(lfd, NULL, NULL);
				
				//设置cfd为非阻塞
				int flag = fcntl(cfd, F_GETFL);
				flag |= O_NONBLOCK;
				fcntl(cfd, F_SETFL, flag);
				
				//将新的cfd上树
				ev.data.fd = cfd;
				ev.events = EPOLLIN;
				epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
			}
			else 
			{
				//有客户端数据发来
				http_request(cfd);
			}			
		}		
	}
}

int send_header(int cfd, char *code, char *msg, char *fileType, int len)
{
	char buf[1024] = {0};
	//把格式化的数据写入某个字符串缓冲区。
	sprintf(buf, "HTTP/1.1 %s %s\r\n", code, msg);
	sprintf(buf+strlen(buf), "Content-Type:%s\r\n", fileType);
	if(len>0)
	{
		sprintf(buf+strlen(buf), "Content-Length:%d\r\n", len);
	}
	strcat(buf, "\r\n");
	Write(cfd, buf, strlen(buf));
	return 0;
}

int send_file(int cfd, char *fileName)
{
	//打开文件
	int fd = open(fileName, O_RDONLY);
	if(fd<0)
	{
		perror("open error");
		return -1;
	}
	
	//循环读文件, 然后发送
	int n;
	char buf[1024];
	while(1)
	{
		memset(buf, 0x00, sizeof(buf));
		n = read(fd, buf, sizeof(buf));
		if(n<=0)
		{
			break;
		}
		else 
		{
			Write(cfd, buf, n);
		}
	}
}

int http_request(int cfd)
{
	int n;
	char buf[1024];
	//读取请求行数据, 分析出要请求的资源文件名
	memset(buf, 0x00, sizeof(buf));
	Readline(cfd, buf, sizeof(buf));
	printf("buf==[%s]\n", buf);
	//GET /hanzi.c HTTP/1.1
	char reqType[16] = {0};
	char fileName[255] = {0};
	char protocal[16] = {0};
	sscanf(buf, "%[^ ] %[^ ] %[^ \r\n]", reqType, fileName, protocal);
	printf("[%s]\n", reqType);
	printf("[%s]\n", fileName);
	printf("[%s]\n", protocal);
	
	char *pFile = fileName;
    if(strlen(fileName)<=1){
        strcpy(pFile,"./");
    }else{
        pFile = fileName+1;
    }
    //转换汉字编码
    strdecode(pFile,pFile);
    printf("pFile==[%s]\n",pFile);
 	
	//循环读取完剩余的数据,避免产生粘包
	while((n=Readline(cfd, buf, sizeof(buf)))>0);
	
	//判断文件是否存在
	struct stat st;
	if(stat(pFile, &st)<0)
	{
		printf("file not exist\n");
		
		//发送头部信息
		send_header(cfd, "404", "NOT FOUND", get_mime_type(".html"), 0);
		
		//发送文件内容
		send_file(cfd, "error.html");	
	}
	else //若文件存在
	{
		//判断文件类型
		//普通文件
		if(S_ISREG(st.st_mode))
		{
			printf("file exist\n");
			//发送头部信息
			send_header(cfd, "200", "OK", get_mime_type(pFile), st.st_size);
			
			//发送文件内容
			send_file(cfd, pFile);
		}
		//目录文件
		else if(S_ISDIR(st.st_mode))
		{
			printf("***********dir file******************\n");
			//发送头部信息
            send_header(cfd,"200","OK",get_mime_type(".html"),0);
			
			//发送html文件头部
            send_file(cfd,"html/dir_header.html");
            char buffer[1024];
            //文件列表信息
            struct dirent **namelist;
            int num;
			//返回目录下满足条件的文件,并返回保存在namelist中
            num = scandir(pFile, &namelist, NULL, alphasort);
            if (num == -1) {
                perror("scandir");
                close(cfd);
                epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
                return -1;
            }else{
                while (num--) {
                    printf("%s\n", namelist[num]->d_name);
                    memset(buffer,0x00,sizeof(buffer));
                    if(namelist[num]->d_type==DT_DIR){
                    	//href=%s比下面多一个/
                        sprintf(buffer,"<li><a href=%s/>%s</a></li>",namelist[num]->d_name,namelist[num]->d_name);
                    }else{
                        sprintf(buffer,"<li><a href=%s>%s</a></li>",namelist[num]->d_name,namelist[num]->d_name);
                    }
                    free(namelist[num]);
                    write(cfd,buffer,strlen(buffer));
                }
                free(namelist);
            }
            //发送html尾部
			send_file(cfd,"html/dir_tail.html");
		}
	}
}

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值