Linux网络编程学习笔记

课程链接

练习代码


复习Linux系统编程

如何避免死锁:

  1. 保证资源的获取顺序,要求每个线程获取资源的顺序一致
  2. 当得不到已有的资源时,放弃已经获取的资源,等待

读写锁:当读次数远大于写次数时

  1. 写独占,读共享
  2. 写锁优先级高
  3. 锁只有一把

条件变量指明了共享数据区值不值得访问(有无空位或产品),是访问共享数据区的通行证

信号量semaphore可以看作进化版互斥锁:1-->N,保证同步的同时,提高了并发

sem_wait:给信号量加锁,sem–,lock

sem_post:给信号量解锁,sem++,unlock,同时唤醒阻塞在该信号上的线程


网络基础

网络层次模型

TCP协议侧重数据的传输;http协议注重数据的解释

OSI参考模型:

  1. 应用层
  2. 表示层
  3. 会话层
  4. 传输层
  5. 网络层
  6. 数据链路层
  7. 物理层

TCP/IP四层模型:

  1. 应用层(http,ftp,nfs,ssh,telnet)
  2. 传输层(TCP,UDP)
  3. 网络层(IP,ICMP,IGMP)
  4. 网络接口层(以太网帧协议, ARP协议)

网络传输数据封装流程:

在这里插入图片描述

只有应用层协议在用户态可见,往下的都处在内核


以太网帧和ARP协议

TCP/IP数据包封装:

在这里插入图片描述
以太网帧格式:

在这里插入图片描述

以太网帧中的目的地址和源地址是指MAC地址

ARP协议:根据IP地址获取MAC地址

ARP数据报格式:

在这里插入图片描述

刚开始时以太网目的地址以太网源地址都未知,都填入FF:FF:FF:FF:FF:FF


IP协议

在这里插入图片描述
TTL:time to live,跳的次数上限,每经过一个路由结点,该值–,减到0丢弃


UDP协议

在这里插入图片描述

IP地址可以在网络环境中唯一标定一台主机,而端口号可以在网络的一台主机上唯一的表示一个进程。所以IP地址+端口号可以在网络环境中唯一地标定一个进程

80端口:http协议

多数应用程序使用5000以下的端口


TCP协议

在这里插入图片描述


BS和CS模型

B/S:Browser/Server

优点:1. 安全性好;2. 跨平台方便;3. 开发工作量小

缺点:1. 不能缓存大量数据;2. 必须严格遵守http协议

C/S:Client/Server

优点:1. 可以缓存大量数据;2. 可以自定义协议, 协议选择灵活(腾讯);3. 速度快

缺点:1. 安全性差;2. 开发工作量大;3. 跨平台难

网络套接字(socket):在通信过程中,套接字一定是成对出现

Linux内核套接字实现:

在这里插入图片描述
一个文件描述符指向一个套接字(该套接字内部由内核借助两个缓冲区实现)


字节序转换

小端法:高位存在高地址,低位存在低地址(本地字节序)

大端法:高位存在低地址,低位存在高地址(网络字节序)

调用库函数做网络字节序主机字节序的转换

#include<arpa/inet.h>
uint32_t htonl(uint32_t hostlong);			//主要针对IP
uint16_t htons(uint16_t hostshort);			//主要针对port
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

早期的程序语言没有int类型,与之等效的是long类型


IP地址转换

由于如192.168.45.2这种的IP地址为点分十进制表示,需要转化为uint32_t型,有现成的函数(IPv4和IPv6都可以转换):

int inet_pton(int af,const char* src,void* dst);	//p表示点分十进制的ip,n网络上的二进制ip
const char* inet_ntop(int af,const char* src,char* dst,socklen_t size);
int inet_pton(int af,const char* src,void* dst);

参数:

  • af:AF_INETAF_INET6
  • src:传入参数,待转换的点分十进制的IP地址
  • dst:传出参数,转换后符合网络字节序的IP地址

返回值:

  • 成功返回1
  • 若参2无效返回0(异常)
  • 失败返回-1
const char* inet_ntop(int af,const char* src,char* dst,socklen_t size)

参数:

  • af:AF_INETAF_INET6
  • src:传入参数,待转换的网络字节序的IP地址
  • dst:传出参数,转换后的点分十进制IP地址,是一块缓冲区
  • size指定了缓冲区的大小

返回值:

  • 成功返回dst指针
  • 失败返回NULL指针,设置errorno

sockaddr_in地址结构

int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

/*struct sockaddr是早已废弃的数据结构,已不再使用,用新的时注意强转一下*/
struct sockaddr_in addr;
int ret = bind(sockfd,(struct sockaddr*)&addr,size);
/*相关结构体定义,在man 7 ip*/
struct sockaddr_in{
	sa_family_t		sin_family;
	in_port_t		sin_port;
	struct in_addr	sin_addr;
};
struct in_addr{
    uint32_t s_addr;
};

初始化方法:

addr.sin_family=AF_INET/AF_INET6;
addr.sin_port=htons(9527);					//端口号为short类型(16bit)

int dst;
inet_pton(AF_INET,"192.168.10.2",(void*)&dst);
addr.sin_addr.s_addr=dst;
/*或者采取下面的方法*/
addr.sin_addr.s_addr=htonl(INADDR_ANY)		//取出系统中任意有效的IP地址

socket编程

socket模型创建流程分析:模型中需要3个套接字

在这里插入图片描述

socket():创建一个套接字, 用fd索引

bind():绑定IP和port

listen():设置监听上限(同时与Server建立连接数)

accpet():阻塞监听客户端连接(传入一个上面创建的套接字, 传出一个连接的套接字)

在客户端中的connect()中绑定IP和port,并建立连接(阻塞)

socket和bind函数

socket:创建一个套接字

int socket(int domain,int type,int protocol);

1.domain指定使用的协议(IPv4或IPv6)

  • AF_INET
  • AF_INET6
  • AF_UNIX或AF_LOCAL

2.type指定数据传输协议(流式或报式)

  • SOCK_STREAM
  • SOCK_DGRAM

3.指定代表协议(一般传0)

  • 流式以TCP为代表
  • 报式以UDP为代表

成功返回新套接字的fd,失败返回-1并设置errno

bind:给socket绑定一个地址结构(IP+port)

#include<sys/types.h>
#include<sys/socket.h>
int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

struct sockaddr_in addr;

addr.sin_family=AF_INET/AF_INET6;
addr.sin_port=htons(9527);					//端口号为short类型(16bit)

int dst;
inet_pton(AF_INET,"192.168.10.2",(void*)&dst);
addr.sin_addr.s_addr=dst;
/*或者采取下面的方法*/
addr.sin_addr.s_addr=htonl(INADDR_ANY)		//取出系统中任意有效的IP地址

/*struct sockaddr是早已废弃的数据结构,已不再使用,用新的时注意强转一下*/
int bind(int sockfd,(struct sockaddr*)&addr,sizeof(addr));

addr.family应该与domain保持一致

成功返回0,失败返回-1

listen和accept函数

listen设置最大连接数或者说能同时进行三次握手的最大连接数

int listen(int sockfd,int backlog);

sockfd:仍然传入socket函数的返回值

backlog:上限数值, 最大128

成功返回0,失败返回-1并设置errno

accept阻塞等待客户端建立连接,成功的话返回一个与客户端成功连接的socket文件描述符(通信套接字)

int accept(int sockfd,sockaddr* addr, socklen_t* addrlen);

sockfd:socket函数的返回值(监听套接字)

addr:传出参数,成功与Sever建立连接的那个客户端的地址结构

addrlen:传入传出参数

​ 入:传入addr的大小:socklen_t clit_addr_len=sizeof(addr)
​ 出:客户端addr的实际大小

返回值:成功返回能与Server进行通信的socket对应的文件描述符;​ 失败返回-1并设置errno

connect函数

客户端调用connect使用现有的socket与服务器建立连接

int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);

sockfd:socket函数返回值

addr:传入参数,服务器的地址结构

addrlen服务器地址结构的长度

返回值:成功返回0,失败返回-1并设置errno

如果不使用bind()函数绑定客户端的地址结构,会采用"隐式绑定"

服务器程序实现

Server:

  1. socket():创建socket
  2. bind():绑定Server地址结构
  3. listen():设置监听上限
  4. accept():阻塞监听客户端建立连接
  5. read():读socket获取客户端数据
  6. toupper():事务处理
  7. write():写回数据到客户端
  8. close()

Client:

  1. socket():创建socket
  2. connect():与服务器建立连接
  3. write():向socket(Server)写入数据
  4. read():读出处理后的数据
  5. close()

测试命令:nc 127.0.0.1 9527,向这个服务发送信息并打印回执

//server-simple.c
#define SERVER_PORT 9527

void perr_exit(const char* str) {
	perror(str);
	exit(1);
}

int main() {
	//创建监听套接字
	int sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (sockfd == -1) {
		perr_exit("socket error");
	}

	struct sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	//绑定地址结构
	int ret = bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret == -1) {
		perr_exit("bind error");
	}

	ret = listen(sockfd, 128);
	if (ret == -1) {
		perr_exit("listen error");
	}

	struct sockaddr_in client_addr;
	socklen_t client_addr_len = sizeof(client_addr);
	//阻塞监听
	int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &client_addr_len);
	if (clientfd == -1) {
		perr_exit("accept error");
	}
	char client_ip[128];
	printf("client ip = %s, client port = %d\n", inet_ntop(AF_INET, &(client_addr.sin_addr.s_addr), client_ip, sizeof(client_ip)), ntohs(client_addr.sin_port));
	char buf[128];
	while (1) {
		ssize_t len = read(clientfd, buf, sizeof(buf));
		for (int i = 0; i < len; ++i) {
			buf[i] = toupper(buf[i]);
		}
		write(clientfd, buf, len);
		sleep(1);
	}
	close(sockfd);
	close(clientfd);

	return 0;
}

获取客户端地址结构:

printf("client ip = %s, client port = %d\n", inet_ntop(AF_INET, &(client_addr.sin_addr.s_addr), client_ip, sizeof(client_ip)), ntohs(client_addr.sin_port));

client_ip是前面定义的客户端IP字符串的缓冲区,大小为128

客户端程序实现

//client-simple.c
#define SERVER_PORT 9527

void perr_exit(const char* str) {
	perror(str);
	exit(1);
}

int main() {
	int clientfd = socket(AF_INET, SOCK_STREAM, 0);
	if (clientfd < 0) {
		perr_exit("socket error");
	}
	struct sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	int server_ip;
	inet_pton(AF_INET, "192.168.163.128", &server_ip);
	server_addr.sin_addr.s_addr = server_ip;

	int ret = connect(clientfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
	if (ret < 0) {
		perr_exit("connect error");
	}
	const char* str = "hello, world";
	char buf[128];
	for (int i = 0; i < 10; ++i) {
		write(clientfd, str, strlen(str));
		read(clientfd, buf, sizeof(buf));
		printf("%s\n", buf);
		sleep(1);
	}
	close(clientfd);
	return 0;
}

Linux的特殊文件类型(伪文件):管道;套接字;块设备;字符设备。对于套接字:一个fd可以索引读写2个缓冲区

TCP连接管理

三次握手建立连接

在这里插入图片描述

SYN包(1号包)不携带数据(0)

3次握手由内核完成,体现在用户态的是accpt()函数和connect()函数调用成功

数据通信

采用滑动窗口增大传输速率:

在这里插入图片描述

批量发送,服务器ACK回执最后一个数据包,且可以看到,滑动窗口的大小win是动态变化的

四次挥手关闭连接

半关闭:由原来的双工通信变为了单工通信,客户端只能接收数据(缓冲区中的数据)

实现原理:关闭了客户端套接字的写缓冲区

在这里插入图片描述
半关闭补充说明:

在这里插入图片描述

之所以半关闭后Client仍能向Server发送ACK数据包,是因为Client关闭的只是写缓冲,内核的连接还在

连接在内核层面,写缓冲在用户层面

如果Server没有收到Client最后发来的ACK数据包,它会一直发送FIN数据包,直到Client回执为止

TCP通信时序总结

三次握手:

  • 主动发起连接请求端:发送SYN标志位,请求建立连接。携带序号,数据字节数(0),滑动窗口大小
  • 被动接受连接请求端:发送ACK标志位,同时携带SYN请求标志位,携带序号,确认序号,数据字节数(0),滑动窗口大小
  • 主动发起连接请求端:发送ACK标志位,应答服务器连接请求,携带确认序号

四次挥手:

  • 主动关闭连接请求端:发送FIN标志位
  • 被动关闭连接请求端:发送ACK标志位(半关闭完成)
  • 被动关闭连接请求端:发送FIN标志位
  • 主动关闭连接请求端:发送ACK标志位

通信时序与代码对应关系:

服务器经过socket, bind, listen, accept后,阻塞监听

客户端用socket建立套接字后,调用connect函数,对应到内核就是发送SYN标志位,开始三次握手

在这里插入图片描述

无论是谁调用close,对应到kernel就是发了一个FIN数据包

并发服务器

错误处理函数的封装

为了保证程序的健壮性,错误处理是必要的,但如果用以前的sys_err()函数会很零散,打乱了代码的整体逻辑,于是提出错误处理函数封装:将原函数首字母大写进行错误处理,这样还可以跳到原函数的manPage

在这里插入图片描述

//wrap.c
#include "wrap.h"

void perr_exit(const char* str) {
	perror(str);
	exit(1);
}

int Accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen) {
	int n;

again:
	if ((n = accept(sockfd, addr, addrlen)) < 0) {
		/*If the error is caused by a  singal,not due to the accept itself,try again*/
		if ((errno == EINTR) || (errno == ECONNABORTED)) {
			goto again;
		} else {
			perr_exit("accept error");
		}
	}
	return n;
}

int Bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen) {
	int n = bind(sockfd, addr, addrlen);
	if (n < 0) {
		perr_exit("bind error");
	}
	return n;
}

int Connect(int sockfd, const struct sockaddr* addr, socklen_t addrlen) {
	int n = connect(sockfd, addr, addrlen);
	if (n < 0) {
		perr_exit("connect error");
	}
	return n;
}

int Listen(int sockfd, int backlog) {
	int n = listen(sockfd, backlog);
	if (n < 0) {
		perr_exit("listen error");
	}
	return n;
}

int Socket(int domain, int type, int protocol) {
	int n = socket(domain, type, protocol);
	if (n < 0) {
		perr_exit("listen error");
	}
	return n;
}

ssize_t Readn(int fd, void* vptr, size_t n) {
	size_t nleft = n;
	ssize_t nread;
	char* ptr = vptr;

	while (nleft > 0) {
		if ((nread = read(fd, ptr, nleft)) < 0) {
			if (errno == EINTR) {
				nread = 0;
			} else {
				return -1;
			}
		} else if (nread == 0) {
			break;
		}
		nleft = nleft - nread;
		ptr = ptr - nread;
	}
	return n - nleft;
}

ssize_t Writen(int fd, const void* vptr, size_t n) {
	size_t nleft = n;
	ssize_t nwritten;
	const char* ptr = vptr;

	while (nleft > 0) {
		if ((nwritten = write(fd, ptr, nleft)) <= 0) {
			if ((errno == EINTR) && (nwritten < 0)) {
				nwritten = 0;
			} else {
				return -1;
			}
		}
		nleft = nleft - nwritten;
		ptr = ptr - nwritten;
	}
	return n;
}

多进程并发服务器思路分析

在这里插入图片描述
服务器端伪码描述:

Socket();		//创建监听套接字lfd
Bind();			//绑定服务器地址结构
Listen();		//设置监听上限
while(1){
	cfd=Accept();
	pid=fork();
	if(pid==0){
		close(lfd);		//子进程用不到lfd
		read(cfd);
		数据处理;
		write(cfd);
	}else if(pid>0){
		close(cfd);		//父进程用不到cfd
	}
}

父进程:

  1. 注册信号捕捉函数:SIGCHLD
  2. 在回调函数中完成子进程回收:while(waitpid())

多线程并发服务器思路分析

服务器端伪码描述:

Socket();		//创建监听套接字lfd
Bind();			//绑定服务器地址结构
Listen();		//设置监听上限
while(1){
    cfd=Accept(lfd,);
    pthread_create(&tid,NULL,&tfn,NULL);
    /*
    	*detach设置线程分离,但是这样不能获取线程退出状态
    	*如果想获取子线程退出状态,用pthread_join()函数,但是这样会造成主线程阻塞
    	*解决方案:create出一个新的子线程调用pthread_join()专门用于回收
    */
    pthread_detach(tid);
}

//子线程:
void* tfn(void* arg){
    close(lfd);
    read(cfd,);
    数据处理;
    write(cfd,);
    pthread_exit((void*)out);		//线程退出状态
}

注意:兄弟线程之间是可以进行回收的,但是兄弟进程之间不可以进行回收,爷孙也不行

多进程并发服务器实现

将结构体中的内容清零:

#include<strings.h>
bzero(&serverAddr,sizeof(serverAddr));

多进程并发服务器模型程序:

//server-multiprocess.c
#include "wrap.h"

#define SERVER_PORT 9527

void catch_child(int signum) {
	while (waitpid(0, NULL, WNOHANG) > 0)
		;
}

int main(int argc, char* argv[]) {
	int sockfd = Socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(SERVER_PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	Listen(sockfd, 128);

	pid_t pid;
	int clientfd;
	while (1) {
		struct sockaddr_in clientaddr;
		socklen_t clientaddr_len = sizeof(clientaddr);
		clientfd = Accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len);
		pid = fork();
		if (pid < 0) {
			perr_exit("fork error");
		} else if (pid == 0) {
			close(sockfd);
			break;
		} else {
			close(clientfd);
			struct sigaction act;
			act.sa_handler = catch_child;
			act.sa_flags = 0;
			sigemptyset(&act.sa_mask);
			int ret = sigaction(SIGCHLD, &act, NULL);
			if (ret < 0) {
				perr_exit("sigemptyset error");
			}
		}
	}

	//子进程专属执行流
	if (pid == 0) {
		char buf[128];
		while (1) {
			ssize_t n = read(clientfd, buf, sizeof(buf));
			if (n == 0) {
				close(clientfd);
				exit(1);
			}
			for (int i = 0; i < n; ++i) {
				buf[i] = toupper(buf[i]);
			}
			write(clientfd, buf, n);
		}
	}
	return 0;
}

多机测试时注意指定IP地址。将本地程序拷贝到远端服务器上去的命令:

scp -r ./test/ liudanbing@101.200.170.171:/home/liudanbing/socket_server

多线程并发服务器实现

//server-multithread.c
#include "wrap.h"

#define SERVER_PORT 9527

struct AddrFd {
	struct sockaddr_in clientaddr;
	int clientfd;
};
typedef struct AddrFd AddrFd;

void* work(void* arg) {
	AddrFd* p_addr_fd = (AddrFd*)arg;
	char client_ip[128];
	printf("new connection: ip = %s, port = %d\n", inet_ntop(AF_INET, &(p_addr_fd->clientaddr.sin_addr), client_ip, sizeof(client_ip)), htons(p_addr_fd->clientaddr.sin_port));
	char buf[128];
	while (1) {
		ssize_t n = read(p_addr_fd->clientfd, buf, sizeof(buf));
		if (n == 0) {
			printf("connection closed\n");
			break;
		}
		for (int i = 0; i < n; ++i) {
			buf[i] = toupper(buf[i]);
		}
		write(p_addr_fd->clientfd, buf, n);
	}
	close(p_addr_fd->clientfd);
	pthread_exit(0);
}

int main() {
	int sockfd = Socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(SERVER_PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	Listen(sockfd, 128);

	pthread_t tid;
	AddrFd addr_fd[128];
	int i = 0;
	while (1) {
		socklen_t clientaddr_len = sizeof(addr_fd[i].clientaddr);
		addr_fd[i].clientfd = Accept(sockfd, (struct sockaddr*)&(addr_fd[i].clientaddr), &clientaddr_len);
		pthread_create(&tid, NULL, work, (void*)&addr_fd[i]);
		pthread_detach(tid);
		i = (i + 1) % 128;
	}
	close(sockfd);
	return 0;
}

这种多线程服务器是你能写出的最基本的具有使用价值的服务器程序了

read返回值:

在这里插入图片描述

TCP连接状态

在这里插入图片描述

使用netstat -apn | grep clientnetstat -apn | grep 9527查看TCP连接状态:

在这里插入图片描述

主动关闭连接

在这里插入图片描述

只有主动关闭连接一方会经历TIME_WAIT状态,换句话说也只有主动关闭连接的一方会经历等待2MSL时长

在这里插入图片描述

被动接受连接

被动接受连接请求端:CLOSE–LISTEN–接收SYN–LISTEN–发送ACK;SYN–SYN_RECD–接受ACK–ESTABLISHED(数据通信状态)

被动关闭连接

被动关闭连接:ESTABLISHED(数据通信状态)–接受FIN–ESTABLISHED(数据通信状态)–发送ACK–CLOSE_WAIT(说明对端[主动关闭连接请求端]处于半关闭状态)–发送FIN–LAST_ACK–接受ACK–CLOSE

2MSL时长

在这里插入图片描述

2MSL存在的意义:保证最后一个ACK回执能被Server接受到,如果ACK丢失,Server重发FIN,则在等待时长内能及时处理

2MSL时常一定出现在主动关闭连接请求一端

其他状态:建立连接时,如果最后一个ACK没有收到,RST重启

端口复用函数

为了不等2MSL的时长

在这里插入图片描述

设置套接字复用函数原型:

int setsockopt(int sockfd,int level,int optname,const void* optval,socklen_t optlen);

一个最佳实践:

int opt=1;	//只有两种取值,0或1
setsockopt(listenFd,SOL_SOCKET,SO_REUSEADDR,(void*)&opt,sizeof(opt));

成功返回0;失败返回-1设置errno

半关闭及shutdown函数:

当TCP连接中A发送FIN请求关闭,B端回应ACK后(A端进入FIN_WAIT_2状态),B没有立即发送FIN给A时,A方处于半连接状态,此时A可以接受B发送的数据,但是A已经不能再向B发送数据

int shotdown(int connectFd,int how);

how的取值:

  • SHUT_RD(0)
  • SHUT_WR(1)
  • SHUT_RDWR(2)

使用close关闭一个连接,他只是减少描述符的引用次数,并不直接关闭连接,只用当描述符引用次数为0时才关闭连接

而shutdown直接全部关闭所有描述符,这会影响其他使用该套接字的进程

IO多路复用

select

在这里插入图片描述

select函数参数简介:

int select(int nfds, fd_set* readfds, fd_set* writefds,fd_set* exceptfds, struct timeval* timeout);

nfds:所监听的最大套接字文件描述符+1,在select内部用作一个for循环的上限

fd_set*:都是传入传出参数

  • readfds:读文件描述符监听集合
  • writefds:写文件描述符监听集合
  • exceptfds:异常文件描述符监听集合

重点在于readfds:当客户端有数据发到服务器上时,触发服务器的可读事件,后面两个一般传NULL

三个传入传出参数都是位图,每个二进制位代表了一个文件描述符的状态

传入的是你想监听的文件描述符集合(对应位置一),传出来的是实际有事件发生的文件描述符集合(将没有事件发生的位置零)

返回值:

  • 所有你所监听的文件描述符当中有事件发生的总个数(读写异常三个参数综合考虑)
  • -1说明发生异常,设置errno

关于timeout:

  • NULL:永久等待
  • 非 0 timeval,等待固定时间
  • timeval 里时间均为 0,检查描述字后立即返回,轮询

在这里插入图片描述

由于对位图的运算涉及到位操作,系统提供了相应的函数:

void FD_CLR(int fd,fd_set* set);			//将给定的套接字fd从位图set中清除出去
void FD_SET(int fd,fd_set* set);			//将给定的套接字fd设置到位图set中
void FD_ZERO(fd_set* set);					//将整个位图set置零
int FD_ISSET(int fd,fd_set* set);			//检查给定的套接字fd是否在位图里面,返回0或1

select实现多路IO转接设计思路:

listenFd = Socket();  // 创建套接字
Bind();               // 绑定地址结构
Listen();             // 设置监听上限
fd_set rset;          // 创建读监听集合
fd_set allset;
FD_ZERO(&allset);           // 将读监听集合清空
FD_SET(listenFd, &allset);  // 将listenFd添加到所有读集合当中
while (1) {
    rset = allset;  // 保存监听集合
    ret = select(listenFd + 1, &rset, NULL, NULL, NULL);  // 监听文件描述符集合对应事件
    if (ret > 0) {
        if (FD_ISSET(listenFd, &rset)) {
            cfd = accept();
            FD_SET(cfd, &allset);  // 添加到监听通信描述符集合中
        }
        for (i = listenFd + 1; i <= cfd; ++i) {
            FD_ISSET(i, &rset);  // 有read,write事件
            read();
            toupper();
            write();
        }
    }
}

select实现多路IO转接代码实现:

//server-select.c
#include "wrap.h"

#define SERVER_PORT 9527

int main(int argc, char* argv[]) {
	int sockfd = Socket(AF_INET, SOCK_STREAM, 0);
	int opt = 1;
	Setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
	struct sockaddr_in serveraddr;
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(SERVER_PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

	Bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
	Listen(sockfd, 128);

	fd_set rdset, allset;
	FD_ZERO(&allset);
	FD_SET(sockfd, &allset);
	int maxfd = sockfd;

	while (1) {
		rdset = allset;
		int N = select(maxfd + 1, &rdset, NULL, NULL, NULL);
		if (N == -1) {
			perr_exit("select error");
		}
		if (FD_ISSET(sockfd, &rdset)) {
			struct sockaddr_in clientaddr;
			socklen_t clientaddr_len = sizeof(clientaddr);
			int clientfd = Accept(sockfd, (struct sockaddr*)&clientaddr, &clientaddr_len);
			FD_SET(clientfd, &allset);
			maxfd = (maxfd > clientfd) ? maxfd : clientfd;
			if (N == 1) {
				continue;
			}
		}
		for (int i = sockfd; i <= maxfd; ++i) {
			if (FD_ISSET(i, &rdset)) {
				char buf[128];
				ssize_t n = read(i, buf, sizeof(buf));
				if (n == 0) {
					printf("connection closed\n");
					FD_CLR(i, &allset);
				} else {
					write(STDOUT_FILENO, buf, n);
					for (int j = 0; j < n; ++j) {
						buf[j] = toupper(buf[j]);
					}
					write(i, buf, n);
				}
			}
		}
	}
	close(sockfd);
	return 0;
}

这个版本的缺陷:当你只需要监听几个指定的套接字时,需要对整个1024的数组进行轮询,效率降低

select优缺点:

缺点:监听上限受文件描述符限制,最大1024个;要检测满足条件的fd,要自己添加业务逻辑,提高了编码难度

优点:跨平台,各种系统都能支持

添加一个自己定义数组提高效率:

//server-select.c
#include "wrap.h"

#define SERVER_PORT 9527
#define MAX(a, b) ((a) > (b)) ? (a) : (b)

int main(int argc, char *argv[]) {
	int i, j, n, maxi, maxfd;
	int client[FD_SETSIZE];
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];

	struct sockaddr_in serveraddr, clientaddr;
	socklen_t clientaddr_len;

	int listen_fd = Socket(AF_INET, SOCK_STREAM, 0);

	fd_set rset, allset;
	FD_ZERO(&allset);
	FD_SET(listen_fd, &allset);

	/*Set the address can be reused*/
	int opt = 1;
	setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&opt, sizeof(opt));

	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVER_PORT);

	Bind(listen_fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
	Listen(listen_fd, 128);

	maxfd = listen_fd;
	maxi = -1;

	for (i = 0; i < FD_SETSIZE; ++i)
		client[i] = -1;

	while (1) {
		/*Initialize the rset by allset*/
		rset = allset;
		int N = select(maxfd + 1, &rset, NULL, NULL, NULL);
		if (N == -1)
			perr_exit("select error");

		/*If there is a new connection request*/
		if (FD_ISSET(listen_fd, &rset)) {
			clientaddr_len = sizeof(clientaddr);
			int connect_fd = Accept(listen_fd, (struct sockaddr *)&clientaddr, &clientaddr_len);
			printf("client address: %s port: %d\n", inet_ntop(AF_INET, &(clientaddr.sin_addr.s_addr), str, sizeof(str)), ntohs(clientaddr.sin_port));

			for (i = 0; i < FD_SETSIZE; ++i) {
				if (client[i] < 0) {
					client[i] = connect_fd;
					break;
				}
			}

			if (i == FD_SETSIZE) {
				fputs("too many clients\n", stderr);
				exit(1);
			}

			FD_SET(connect_fd, &allset);

			/*Renewal the maxfd*/
			maxfd = MAX(maxfd, connect_fd);
			maxi= MAX(maxi, i); 

			/*Only the connection request,no data read request(no connect_fd is valid)*/
			if(--N == 0) continue;
		}
		int socket_fd = 0;
		/*There is data read request(s),traverse the fd set to corresponding them*/
		for (i = 0; i <= maxi; ++i) {
			if ((socket_fd = client[i]) < 0)
				continue;

			if (FD_ISSET(socket_fd, &rset)) {
				if ((n = read(socket_fd, buf, sizeof(buf))) == 0) {
					close(socket_fd);
					FD_CLR(socket_fd, &allset);
					client[i] = -1;
				} else if (n > 0) {
					for (j = 0; j < n; ++j)
						buf[j] = toupper(buf[j]);
					write(socket_fd, buf, n);
					//write(STDOUT_FILENO, buf, n);
				}
				if (--N == 0) break;
			}
		}
	}
	close(listen_fd);
	return 0;
}

poll

对select做出的小改进,但是比较鸡肋,不如epoll

//fds监听的文件描述符数组
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

struct pollfd {
	int fd;         /*待监听的文件描述符*/
	short events;     /*requested events:待监听的文件描述符对应的监听事件->POLLIN,POLLOUT,POLLERR*/
	short revents;    /*returned events:传入时给0,如果满足对应事件的话被置为非零->POLLIN,POLLOUT,POLLERR*/
};
  • fds:监听的文件描述符数组
  • nfds:监听数组的实际有效监听个数
  • timeout:含义同select,但是单位是ms;-1表示阻塞
  • 返回值:返回满足对应监听事件的文件描述符总个数

在这里插入图片描述

/*自定义结构体数组并初始化*/
struct pollfd pfds[1024];

pfds[0].fd=lfd;
pfds[0].events=POLLIN;
pfds[0].revents=0;

pfds[1].fd=cfd1;
pfds[1].events=POLLIN;
pfds[1].revents=0;

pfds[2].fd=cfd2;
pfds[2].events=POLLIN;
pfds[2].revets=0;
...
while(1){
	//pollfd=fds, nfds=5, timeout=-1
    int N=poll(pfds, 5, -1);
    /*轮询是否有POLLIN需求*/
    for(i = 0; i < 5; i++) {
        if(pfds[i].revents & POLLIN){
            if (i == 0) Accept();
            else Read()/Write();            
        }
   }
}

poll函数实现服务器:

read函数返回值:

  • > 0:实际读到的字节数
  • = 0:socket 中,表示对端关闭,要 close()
  • -1:
    • 如果 errno == EINTR,表示被异常中断,需要重启;
    • 如果 errno == EAGIN 或 EWOULDBLOCK,表示以非阻塞方式读数据,但是没有数据,需要再次读;
    • 如果 errno == ECONNRESET,说明连接被重置,需要 close(),移除监听队列
    • 错误
//server-poll.c
#include "wrap.h"

#define MAXLINE 80
#define SERVER_PORT 9527
#define OPEN_MAX 1024

int main(int argc, char* argv[]) {
	char buf[MAXLINE], str[INET_ADDRSTRLEN];
	struct pollfd client[OPEN_MAX];
	struct sockaddr_in clientaddr, serveraddr;
	int listen_fd = Socket(AF_INET, SOCK_STREAM, 0);

	int opt = 0;
	int ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));
	if (ret == -1) perr_exit("setsockopt error");

	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serveraddr.sin_port = htons(SERVER_PORT);

	Bind(listen_fd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
	Listen(listen_fd, 128);

	client[0].fd = listen_fd;
	client[0].events = POLLIN;
	int i = 0, j = 0, maxi = 0;
	for (i = 1; i < OPEN_MAX; ++i)
		client[i].fd = -1;

	while (1) {
		int N = poll(client, maxi + 1, -1);
		if (N == -1)
			perr_exit("poll error");
		if (client[0].revents & POLLIN) {
			socklen_t clientaddr_len = sizeof(clientaddr);
			int connect_fd = Accept(listen_fd, (struct sockaddr*)&clientaddr, &clientaddr_len);
			printf("client address: %s, port: %d\n", inet_ntop(AF_INET, &(clientaddr.sin_addr.s_addr), str, sizeof(str)), ntohs(clientaddr.sin_port));
			for (i = 1; i < OPEN_MAX; ++i) {
				if (client[i].fd < 0) {
					client[i].fd = connect_fd;
					break;
				}
			}

			if (i == OPEN_MAX)
				perr_exit("too many clients, dying\n");
			client[i].events = POLLIN;

			if (i > maxi) maxi = i;
			if (--N <= 0) continue;
		}
		for (i = 1; i <= maxi; ++i) {
			int socket_fd = 0, n = 0;
			if ((socket_fd = client[i].fd) < 0)
				continue;
			if (client[i].revents & POLLIN) {
				if ((n = read(socket_fd, buf, sizeof(buf))) < 0) {
					if (errno = ECONNRESET) {
						printf("client[%d] aborted connection\n", i);
						close(socket_fd);
						client[i].fd = -1;
					} else {
						perr_exit("read error");
					}
				} else if (n == 0) {
					printf("client[%d] closed connection\n", i);
					close(socket_fd);
					client[i].fd = -1;
				} else {
					for (j = 0; j < n; ++j)
						buf[j] = toupper(buf[j]);
					write(STDOUT_FILENO, buf, n);
					write(socket_fd, buf, n);
				}
				if (--N == 0)
					break;
			}
		}
	}
	close(listen_fd);
	return 0;
}

优缺点分析:

优点:自带数组结构,可以将监听事件集合和返回事件集合分开;可以拓展监听上限,超出1024的限制

缺点:不能跨平台,只适合于Linux系统;无法直接定位到满足监听事件的文件描述符,编码难度较大

突破1024文件描述符设置

在这里插入图片描述

cat /proc/sys/fs/file-max:查看当前计算机所能打开的最大文件个数,受硬件影响

ulimit -a:当前用户进程所能打开的最大文件描述符个数(缺省为1024)

修改配置文件:

sudo vim /etc/security/limits.conf

* soft nofile 65536:设置默认值(可以直接借助命令修改)

* hard nofile 100000:命令修改上限

ulimit -n 17000:更改最大个数

epoll

epoll_create()会创建一个监听红黑树:

int epoll_create(int size);

在这里插入图片描述

size:创建红黑树的监听节点数量(仅供内核参考)

返回值:成功返回指向新创建的红黑树的根节点的fd,失败返回-1并设置errno

epoll_ctl():操作监听红黑树

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* 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 */
};

epfd:epoll_create的返回值

op:对该监听红黑树所做的操作:

  • EPOLL_ CTL_ADD:添加fd到监听红黑树
  • EPOLL_CTL_MOD:修改fd在监听红黑树上的监听事件
  • EPOLL_CTL_DEL:将一个fd从监听红黑树上摘下(取消监听)

fd:待操作的fd

event:struct poll_event*,传出参数

  • events:EPOLLIN,EPOLLOUT,EPOLLERR
  • data:联合体:
    • int fd:对应监听事件的fd
    • void* ptr
    • uint_32 u32
    • uint_64 u64

epoll_wait函数:

int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout);

epfd:epoll_create的返回值

events:传出参数,是一个数组,存放的是满足监听条件的那些fd结构体

maxevents:数组中元素的总个数,例如如果定义了struct epoll_events events[1024],那么他就是1024,也就是数组大小

timeout:

  • -1阻塞
  • 0非阻塞
  • >0超时时长(ms)

返回值:

  • >0:满足监听的总个数,可以作为后面的循环上限
  • 0:没有fd满足监听事件
  • -1:出错

epoll函数实现多路IO转接:

//server-epoll.c
#include "wrap.h"

#define SERVER_PORT 9527
#define MAXLINE 80
#define OPEN_MAX 1024

int main(int argc, char* argv[]) {
    int i = 0;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];

    struct sockaddr_in serveraddr, clientaddr;

    int listen_fd = Socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (void*)&opt, sizeof(opt));

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERVER_PORT);

    Bind(listen_fd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
    Listen(listen_fd, 128);

    int efd = Epoll_create(OPEN_MAX);

    /*将listenFd加入监听红黑树中*/
    struct epoll_event node;  // 临时节点
    node.events = EPOLLIN;
    node.data.fd = listen_fd;  // 构造临时节点
    int res = Epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &node);

    struct epoll_event ep[OPEN_MAX];  // 监听数组
    while (1) {
        int N = Epoll_wait(efd, ep, OPEN_MAX, -1);  // 阻塞监听

        for (i = 0; i < N; ++i) {
            if (!(ep[i].events & EPOLLIN)) continue;

            if (ep[i].data.fd == listen_fd) {		//建立连接请求
                socklen_t clientaddr_len = sizeof(clientaddr);
                int connect_fd = Accept(listen_fd, (struct sockaddr*)&clientaddr, &clientaddr_len);

                node.events = EPOLLIN;
                node.data.fd = connect_fd;
                res = Epoll_ctl(efd, EPOLL_CTL_ADD, connect_fd, &node);
            } else {	//数据处理请求
                int socket_fd = ep[i].data.fd;
                int n = read(socket_fd, buf, sizeof(buf));

                if (n == 0) {
                    res = Epoll_ctl(efd, EPOLL_CTL_DEL, socket_fd, NULL);
                    close(socket_fd);
                    printf("client[%d] closed connection\n", socket_fd);
                } else if (n < 0) {
                    perr_exit("read n < 0 error");
                    res = Epoll_ctl(efd, EPOLL_CTL_DEL, socket_fd, NULL);
                    close(socket_fd);
                } else {
                    for (i = 0; i < n; ++i) buf[i] = toupper(buf[i]);
                    write(STDOUT_FILENO, buf, n);
                    write(socket_fd, buf, n);
                }
            }
        }
    }

    close(listen_fd);
    close(efd);
    return 0;
}

epoll实现多路IO转接思路回顾:

在这里插入图片描述

ET和LT模式

在这里插入图片描述

epoll有两种事件模型:

EdgeTriggered:边沿触发,只有数据到来才触发,不管缓冲区中是否还有数据

LevelTriggered:水平触发,只要有数据都会触发(默认模式)

//epoll-ETLT-demo.c
#include "wrap.h"

#define MAXLINE 10

int main() {
    char buf[MAXLINE], ch = 'a';
    memset(buf, 0, sizeof(buf));

    int pfd[2];
    pipe(pfd);
    pid_t pid = fork();

    int i = 0;
    if (pid == 0) { //child process, write
        close(pfd[0]);
        while (1) {
            //aaaa\n
            for (i = 0; i < (MAXLINE >> 1); ++i) buf[i] = ch;
            buf[i - 1] = '\n';
            ch++;
            //bbbb\n
            for (; i < MAXLINE; ++i) buf[i] = ch;
            buf[i - 1] = '\n';
            ch++;
            //aaaa\nbbbb\n
            write(pfd[1], buf, sizeof(buf));
            sleep(3);
        }
        close(pfd[1]);
    } else if (pid > 0) {   // parent process, read
        struct epoll_event resevent[10];

        close(pfd[1]);
        int efd = epoll_create(10);

        struct epoll_event event;
        //event.events = EPOLLIN | EPOLLET;       // edge triger
        event.events = EPOLLIN;    // level triger, by default
        event.data.fd = pfd[0];
        epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);

        while (1) {
            int N = epoll_wait(efd, resevent, 10, -1);
            //printf("N = %d\n", N);  //N = 1

            if (resevent[0].data.fd == pfd[0]) {
                int len = read(pfd[0], buf, MAXLINE / 2);
                write(STDOUT_FILENO, buf, len);
            }
        }

        close(pfd[0]);
        close(efd);
    } else {
        perr_exit("fork error");
    }
    return 0;
}

现象:

  • 当设置水平触发时,只要管道中有数据,epoll_wait就会返回,触发父进程读取数据,所以虽然父进程每次只读取一半的数据,但读完一半后剩下的一半又会马上触发父进程读取,所以10个字节的数据都会显示出来

  • 当设置边沿触发时,父进程阻塞读取,而只有当子进程向管道中进行一次写入时才会触发父进程进行读取,所以每次只会打印一半的数据

总结:

  • 边沿触发:缓冲区未读尽的数据不会导致epoll_wait返回,新的数据写入才会触发
  • 水平触发:缓冲区未读尽的数据会导致epoll_wait返回

网络中的ET和LT模式:

服务器程序:

//server-epollET.c
#include "wrap.h"

#define SERVER_PORT 9527
#define MAXLINE 10

int main(int argc, char* argv[]) {
    struct sockaddr_in serveraddr, clientaddr;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];

    int listen_fd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERVER_PORT);

    Bind(listen_fd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
    Listen(listen_fd, 128);

    struct epoll_event event;
    struct epoll_event resevent[10];

    int efd = epoll_create(10);
    event.events = EPOLLIN | EPOLLET;   //edge triger
    //event.events=EPOLLIN;     //level triger, by default

    socklen_t clientaddr_len = sizeof(clientaddr);
	int connect_fd = Accept(listen_fd, (struct sockaddr*)&clientaddr, &clientaddr_len);
    printf("accepting connections...\n");

    event.data.fd = connect_fd;
    epoll_ctl(efd, EPOLL_CTL_ADD, connect_fd, &event);

    while (1) {
        int res = epoll_wait(efd, resevent, 10, -1);    //res = 1

        if (resevent[0].data.fd = connect_fd) {
            int len = read(connect_fd, buf, MAXLINE / 2);
            write(STDOUT_FILENO, buf, len);
        }
    }
    return 0;
}

客户端程序:

//client-epollET.c
#include "wrap.h"

#define SERVER_PORT 9527
#define MAXLINE 10

int main(int argc, char* argv[]) {
    struct sockaddr_in serveraddr;
    char buf[MAXLINE];
    char ch = 'a';

    int connect_fd = Socket(AF_INET, SOCK_STREAM, 0);
    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(SERVER_PORT);
    inet_pton(AF_INET, "192.168.93.11", &serveraddr.sin_addr);

    Connect(connect_fd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    int i = 0;
    while (1) {
        for (i = 0; i < (MAXLINE >> 1); ++i) buf[i] = ch;
        buf[i - 1] = '\n';
        ch++;

        for (; i < MAXLINE; ++i) buf[i] = ch;
        buf[i - 1] = '\n';
        ch++;

        write(connect_fd, buf, sizeof(buf));
        sleep(3);
    }
    close(connect_fd);
    return 0;
}

epoll的ET非阻塞模式

如果epoll_wait设置为阻塞模式,则当你调用Readn/Readline这种自己会阻塞的函数时,会出大问题:阻塞在了Read函数上,不会被唤醒了

Level Triggered是缺省的工作方式,同时支持block和none-block模式,在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作. 如果你不做任何操作,内核还是会继续通知你。所以,这种模式编程出错的可能性要小一点,传统的select/poll都是这种模型的代表

Edge Triggered是高速工作方式,只支持none-block模式。在这种模式下,当文件描述符从未就绪变为就绪时,内核通过epoll告诉你。然后他会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知。如果一直不对这个fd作IO操作(从而导致它再次变为未就绪),内核不会发送更多的通知(only once)

//server-epollET-nonblock.c
#include "wrap.h"

#define SERVER_PORT 9527
#define MAXLINE 10

int main(int argc, char* argv[]) {
    struct sockaddr_in serveraddr, clientaddr;
    char buf[MAXLINE], str[INET_ADDRSTRLEN];

    int listen_fd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(SERVER_PORT);

    Bind(listen_fd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
    Listen(listen_fd, 128);

    struct epoll_event event;
    struct epoll_event resevent[10];

    int efd = epoll_create(10);
    event.events = EPOLLIN | EPOLLET;
    // event.events=EPOLLIN;

    socklen_t clientaddr_len = sizeof(clientaddr);
    int connect_fd = Accept(listen_fd, (struct sockaddr*)&clientaddr, &clientaddr_len);
    printf("accepting connections...\n");

    int flag = fcntl(connect_fd, F_GETFL);
    flag = flag | O_NONBLOCK;
    fcntl(connect_fd, F_SETFL, flag);

    event.data.fd = connect_fd;
    epoll_ctl(efd, EPOLL_CTL_ADD, connect_fd, &event);

    while (1) {
        int res = epoll_wait(efd, resevent, 10, -1);  // res = 1

        int len = 0;
        if (resevent[0].data.fd = connect_fd) {
            while (len = read(connect_fd, buf, MAXLINE / 2) > 0) {
                write(STDOUT_FILENO, buf, len);
            }
        }
    }
    return 0;
}

在这里插入图片描述

epoll优缺点总结:

优点:高效;能突破1024文件描述符限制

缺点:只支持Linux,不能跨平台

对比ET和LT:

LT:缓冲区有多少就读多少

ET:不全部都读,只读一部分,剩下的如果没用可以直接丢弃掉

比如对于一个大文件,你只想读取文件头中的属性信息,就可以采用ET模式

采用非阻塞模式,放入一个循环中:忙轮询

epoll反应堆模型

epoll反应堆模型:ET模式 + 非阻塞 + 回调函数void* ptr
在这里插入图片描述

比如:

struct evt{
	int fd;
	void(*func)(int fd);
}*ptr;

函数指针封装在结构体里,让他自动回调(用C实现面向对象):

原来的步骤:

int connect_fd = socket();
bind();
listen();
int efd = epoll_create(size);
while (1) {
	int N = epoll_wait();
	for(i = 0; i < N; ++i) {
		if(listen_fd) {
			int connect_fd = accept();
		} else {
			read();
			toupper();
			write();
		}
	}
}

考虑到实际的网络情况,对端可能半关闭或滑动窗口已满,可能写失败,所以在写之前要检查可写与否

反应堆模型:

int listen_fd = socket();
bind();
listen();
int epfd = epoll_create(size);
struct epoll_event node;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &node);

struct epoll_event ep[size];

while(1){int N = epoll_wait(epfd, ep, size, -1);
	for(int i = 0; i < N; ++i) {
		if(ep[i].data.fd == listen_fd) {
			int connect_fd = accept();
			epoll_ctl(epfd, EPOLL_CTL_ADD, connect_fd, &node);			
		} else {
			read();
			toupper();
			epoll_ctl(epfd, EPOLL_CTL_DEL, connect_fd, &node);	//将cfd从监听红黑树上摘下
			node.events = EPOLLOUT;		//POLLIN改为EPOLLOUT监听写事件
			node.data.call_back();
			epoll_ctl(epfd, EPOLL_CTL_ADD, connect_fd, &node);	//重新放到红黑上监听写事件
			epoll_wait(epfd, ep, size, -1);		//等待cfd可写
			write();
			
			epoll_ctl(epfd, EPOLL_CTL_DEL, connect_fd, &node);	//再将cfd从监听红黑树上摘下
			node.events = EPOLLIN;		//POLLIN改为EPOLLIN监听写事件
			epoll_ctl(epfd, EPOLL_CTL_ADD, connect_fd, &node);	//重新放到红黑上监听读事件
	}
}

所以,反应堆模型不但要监听反应堆的读事件,还要监听写事件

epoll反应堆的main逻辑:

int main(int argc, char* argv[]) {
    unsigned short port = SERVER_PORT;

    g_efd = epoll_create(MAX_EVENTS + 1);
    if (g_efd <= 0) perr_exit("epoll_create error");

    init_listen_socket(g_efd, port);
    struct epoll_event events[MAX_EVENTS + 1];
    int chekpos = 0, i = 0;

    while (1) {
        /*验证超时,每次测试100个连接,不测试listenFd.当客户端60s没有和服务器通信,则关闭连接*/
        long now = time(NULL);
        for (i = 0; i < 100; ++i, ++chekpos) {
            if (chekpos == MAX_EVENTS) chekpos = 0;
            if (g_events[chekpos].status != 1) continue;

            /*时间间隔*/
            long duration = now - g_events[chekpos].last_active;

            if (duration >= 60) {
                close(g_events[chekpos].fd);
                printf("fd=%d timeout\n", g_events[chekpos].fd);
                event_del(g_efd, &g_events[chekpos]);
            }
        }
        // 等待1s,若无结果直接返回
        int N = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000);
        if (N < 0) {
            perr_exit("epoll_wait error");
        }

        for (i = 0; i < N; ++i) {
            my_event* ev = (my_event*)events[i].data.ptr;

            if ((events[i].events & EPOLLIN) &&
                (ev->events & EPOLLIN)) {  // 读就绪
                ev->call_back(ev->fd, events[i].events, ev->this);
            }
            if ((events[i].events & EPOLLOUT) &&
                (ev->events & EPOLLOUT)) {  // 写就绪
                ev->call_back(ev->fd, events[i].events, ev->this);
            }
        }
    }
    return 0;
}

给lfd和cfd指定回调函数:

void init_listen_socket(int efd, unsigned short port) {
    struct sockaddr_in serveraddr;

    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);

    int flag = fcntl(listen_fd, F_GETFL);
    flag = flag | O_NONBLOCK;
    fcntl(listen_fd, F_SETFL, flag);

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(port);

    bind(listen_fd, (const struct sockaddr*)&serveraddr, sizeof(serveraddr));
    listen(listen_fd, 128);

    /*把g_events数组的最后一个元素设置为listen_fd,回调函数设置为accept_connection*/
    event_set(&g_events[MAX_EVENTS], listen_fd, accept_connection,
              &g_events[MAX_EVENTS]);
    event_add(efd, EPOLLIN, &g_events[MAX_EVENTS]);

    return;
}

给my_event结构体赋值:

void event_set(my_event* event, int fd, void (*call_back)(int, int, void*),
               void* this) {
    event->fd = fd;
    event->events = 0;
    event->call_back = call_back;
    event->this = this;
    event->status = 0;
    memset(event->buf, 0, sizeof(event->buf));
    event->len = 0;
    event->last_active = time(NULL);

    return;
}

accept得到connect_fd,设置回调函数并加入监听红黑树:

void accept_connection(int listen_fd, int events, void* this) {
    struct sockaddr_in clientaddr;
    socklen_t clientaddr_len = sizeof(clientaddr);
    int i;
    int connect_fd = accept(listen_fd, (struct sockaddr*)&clientaddr, &clientaddr_len);

    if (connect_fd == -1) {
        if ((errno != EAGAIN) && (errno != EINTR)) {
            /*暂时不做错误处理*/
        }
        perr_exit("accept error");
    }

    do {
        for (i = 0; i < MAX_EVENTS; ++i) {
            if (g_events[i].status == 0) break;
        }

        if (i == MAX_EVENTS) {
            printf("%s: max connections limit[%d]\n", __func__, MAX_EVENTS);
            break;
        }

        int flag = fcntl(connect_fd, F_GETFL);
        flag = flag | O_NONBLOCK;
        fcntl(connect_fd, F_SETFL, flag);

        event_set(&g_events[i], connect_fd, recv_data, &g_events[i]);
        event_add(g_efd, EPOLLIN, &g_events[i]);

    } while (0);

    return;
}

event_add函数:将一个fd添加到监听红黑树,设置监听读事件还是写事件

/*向epoll监听红黑树添加一个文件描述符*/
void event_add(int efd, int event, my_event* ev) {
    struct epoll_event epv = {0, {0}};
    int op = 0;
    /*将联合体中的ptr指向传入的my_events实例*/
    epv.data.ptr = ev;
    /*将成员变量events设置为传入的event,ev->events保持相同*/
    epv.events = ev->events = event;

    if (ev->status == 0) {
        op = EPOLL_CTL_ADD;
        ev->status = 1;
    }
    if (epoll_ctl(efd, op, ev->fd, &epv) < 0) {
        printf("event add failed: fd = %d, event = %d\n", ev->fd, event);
    } else {
        printf("event add OK: fd = %d, event = %0X, op = %d\n", ev->fd, event, op);
    }

    return;
}

recv_data:

void recv_data(int fd, int event, void* this) {
    my_event* ev = (my_event*)this;
    int len;

    /*从fd中接收数据到ev的buf中*/
    len = recv(fd, ev->buf, sizeof(ev->buf), 0);
    event_del(g_efd, ev);

    if (len > 0) {
        ev->len = len;
        ev->buf[len] = '\0';
        printf("C[%d]:%s", fd, ev->buf);

        event_set(ev, fd, send_data, ev);
        event_add(g_efd, EPOLLOUT, ev);
    } else if (len == 0) {
        /*recv返回0,说明对端关闭了连接*/
        close(ev->fd);
        printf("fd = %d, pos = %ld closed\n", fd, ev - g_events);
    } else {
        /*返回值<0,出错了*/
        close(ev->fd);
        perr_exit("recv error");
    }
    return;
}

从红黑树上摘除节点:

/*从epoll监听红黑树上摘除节点*/
void event_del(int efd, my_event* ev) {
    struct epoll_event epv = {0, {0}};

    if (ev->status != 1) return;

    /*联合体中的指针归零,状态归零*/
    epv.data.ptr = NULL;
    ev->status = 0;
    /*将epv从树上摘下来*/
    epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);

    return;
}

send_data:

void send_data(int fd, int event, void* this) {
    my_event* ev = (my_event*)this;
    int len;

    /*从ev的buf中发送数据给fd*/
    len = send(fd, ev->buf, sizeof(ev->buf), 0);
    event_del(g_efd, ev);

    if (len > 0) {
        printf("send fd = %d, len = %d:%s\n", fd, len, ev->buf);
        event_set(ev, fd, recv_data, ev);
        event_add(g_efd, EPOLLIN, ev);
    } else {
        close(fd);
        printf("send %d error:%s\n", fd, strerror(errno));
    }
    return;
}

超时时间:

...
 /*验证超时,每次测试100个连接,不测试listenFd.当客户端60s没有和服务器通信,则关闭连接*/
    long now=time(NULL);
    for(i=0;i<100;++i,++chekpos){
        if(chekpos==MAX_EVENTS)
            chekpos=0;
        if(g_events[chekpos].status!=1)
            continue;
         /*时间间隔*/
        long duration=now-g_events[chekpos].last_active;
         if(duration>=60){
            close(g_events[chekpos].fd);
            printf("fd=%d timeout\n",g_events[chekpos].fd);
            eventdel(g_efd,&g_events[chekpos]);
        }
    }
...

read函数返回值:

在这里插入图片描述

如何突破1024文件描述符限制:

在这里插入图片描述

epoll_create函数:
在这里插入图片描述

epoll_ctl函数:

在这里插入图片描述

epoll_wait函数:

在这里插入图片描述

epoll进阶:
在这里插入图片描述

桩函数:打一个桩在这,自己去实现

线程池

以往的服务模式:当有一个Client建立连接,服务器就Create一个线程进行数据处理,处理完后就销毁

多路IO转接相比多线程/多进程模型的优势:需要创建的线程数大大减小,节省系统资源

由于线程创建的开销很大,而维护成本较低,于是提出一次创建多个线程, 构成线程池(线程聚集的虚拟的地方,预线程化)

在这里插入图片描述

刚开始所有线程阻塞在共享数据区的条件变量上,有数据到来需要处理时Server唤醒一个线程进行数据处理,处理完毕后让他回到线程池,等待下次调用

  • 线程池初始线程数:thread_init_num=38
  • 线程池最大线程数:thread_max_num=500
  • 线程池中忙的线程数:thread_busy_num
  • 线程池中存活的线程数:thread_live_num
  • 当忙线程数和存活线程数的比例达到一定范围时,要对线程池进行"扩容"和"瘦身"
  • 扩容或瘦身的步长:thread_step,不能一个一个的起线程或销毁线程了,效率太低

上面的这些任务都是专门的管理者线程完成;ps -Lf pid查看一个进程起的线程数

线程池描述结构体

//threadpool.h
#define MANAGE_INTERVAL 10		//管理者线程轮询间隔
#define MIN_WAIT_TASK_NUM 10	//最小任务数
#define DEFAULT_THREAD_VARY 10	//增减线程的步长

/*线程任务结构体,包含线程的回调函数和其参数*/
typedef struct {
	void* (*callback)(void* arg);
	void* arg;
} threadpool_task_t;

/*描述线程池相关信息*/
struct threadpool_t {
	pthread_mutex_t self_lock;			//锁住本结构体
	pthread_mutex_t busy_thr_num_lock;	//记录忙状态的线程个数的锁,busy_thr_num
	pthread_cond_t taskq_not_full;		//当任务队列满时,添加任务的线程阻塞,等待此条件变量
	pthread_cond_t taskq_not_empty;		//当任务队列不为空时,通知等待任务的线程

	pthread_t* worker_tids;	 //存放线程池中每个线程的tid,数组
	pthread_t manager_tid;	 //管理者线程tid

	threadpool_task_t* task_queue;	//任务队列,每一个队员是(回调函数和其参数)结构体
	int taskq_front;				// task_queue队头下标
	int taskq_rear;					// task_queue队尾下标
	int taskq_size;					// task_queue队中实际任务数
	int taskq_capacity;				// task_queue队列可容纳任务数上限

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

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

main测试函数:

  • 创建线程池
  • 向线程池中添加任务,借助回调函数处理任务
  • 销毁线程池
//threadpool-test.c
#include "threadpool.h"

int main(void) {
	threadpool_t* thp = threadpool_create(3, 100, 100);

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

	sleep(10);	//等待子线程完成任务
	threadpool_destroy(thp);
	return 0;
}

threadpool_create

  • 创建线程结构体指针
  • 初始化线程结构体(N个成员变量)
  • 创建N个任务线程
  • 创建1个管理者线程
  • 失败时,销毁开辟的所有空间
threadpool_t* threadpool_create(int min_thr_num, int max_thr_num, int taskq_capacity) {
	threadpool_t* pool = NULL;
	do {
		pool = (threadpool_t*)malloc(sizeof(threadpool_t));
		if (pool == NULL) {
			printf("%s: malloc error\n", __func__);
			break;
		}
		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->dying_thr_num = 0;

		pool->taskq_front = 0;
		pool->taskq_rear = 0;
		pool->taskq_size = 0;
		pool->taskq_capacity = taskq_capacity;
		pool->shutdown = false;

		pool->worker_tids = (pthread_t*)malloc(sizeof(pthread_t) * max_thr_num);
		if (pool->worker_tids == NULL) {
			printf("%s: malloc error\n", __func__);
			break;
		}
		memset(pool->worker_tids, 0, sizeof(pthread_t) * max_thr_num);

		pool->task_queue = (threadpool_task_t*)malloc(sizeof(threadpool_task_t) * taskq_capacity);
		if (pool->task_queue == NULL) {
			printf("%s: malloc error\n", __func__);
			break;
		}

		/*初始化互斥锁和条件变量*/
		if (pthread_mutex_init(&(pool->self_lock), NULL) || pthread_mutex_init(&(pool->busy_thr_num_lock), NULL) || pthread_cond_init(&(pool->taskq_not_empty), NULL) || pthread_cond_init(&(pool->taskq_not_full), NULL)) {
			printf("init mutex or cond fail\n");
			break;
		}

		/*创建N个任务线程*/
		for (int i = 0; i < min_thr_num; ++i) {
			pthread_create(&(pool->worker_tids[i]), NULL, worker_callback, (void*)pool);  // pool指向当前线程池
			printf("start thread 0x%x\n", (unsigned int)pool->worker_tids[i]);
		}

		/*创建管理者线程,管理者线程的回调函数为manager_tid,参数为整个线程池描述体指针*/
		pthread_create(&(pool->manager_tid), NULL, manager_callback, (void*)pool);
		return pool;
	} while (0);
	threadpool_free(pool);
	return NULL;
}

worker_callback

  • 进入子线程回调函数
  • 接受参数void* arg到pool指针
  • 加锁锁住整个结构体
  • 判断条件变量(被阻塞了)
void* worker_callback(void* threadpool) {
	threadpool_t* pool = (threadpool_t*)threadpool;
	threadpool_task_t task;

	while (true) {
		pthread_mutex_lock(&(pool->self_lock));

		/*taskq_size==0说明没有任务(并且线程池没有关闭),调用wait函数阻塞在条件变量上,若有任务,跳过该while*/
		while (pool->taskq_size == 0 && !pool->shutdown) {
			/*刚创建线程池时,没有任务,所有线程都阻塞在queue_not_empty这个条件变量上*/
			printf("thread 0X%x is waiting\n", (unsigned int)pthread_self());
			pthread_cond_wait(&(pool->taskq_not_empty), &(pool->self_lock));

			/*清除指定数目的空闲线程,如果要结束的线程个数>0,结束线程*/
			if (pool->dying_thr_num > 0) {
				pool->dying_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->self_lock));
					pthread_exit(NULL);
				}
			}
		}

		if (pool->shutdown) {
			pthread_mutex_unlock(&(pool->self_lock));
			printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
			// pthread_detach(pthread_self());
			pthread_exit(NULL);
		}

		task.callback = pool->task_queue[pool->taskq_front].callback;
		task.arg = pool->task_queue[pool->taskq_front].arg;

		pool->taskq_front = (pool->taskq_front + 1) % pool->taskq_capacity;
		pool->taskq_size--;

		/*通知线程有新的任务被处理*/
		pthread_cond_broadcast(&(pool->taskq_not_full));

		pthread_mutex_unlock(&(pool->self_lock));

		printf("thread 0x%x start working\n", (unsigned int)pthread_self());
		pthread_mutex_lock(&(pool->busy_thr_num_lock));
		pool->busy_thr_num++;
		pthread_mutex_unlock(&(pool->busy_thr_num_lock));

		/*执行任务处理函数*/
		(*(task.callback))(task.arg);

		printf("thread 0x%x finish working\n", (unsigned int)pthread_self());
		pthread_mutex_lock(&(pool->busy_thr_num_lock));
		pool->busy_thr_num--;
		pthread_mutex_unlock(&(pool->busy_thr_num_lock));
	}
	pthread_exit(NULL);
}

manager_callback

  • 进入管理者线程回调函数
  • 每10s循环一次
  • 接收参数void*到pool指针
  • 给lock加锁,锁住整个结构体
  • 获取管理线程池要用到的变量
  • 根据既定的算法,根据上述变量,判断是否应该创建,销毁指定步长的线程
void* manager_callback(void* threadpool) {
	int i = 0;
	threadpool_t* pool = (threadpool_t*)threadpool;

	while (!pool->shutdown) {
		/*管理者线程每隔10s管理一次*/
		sleep(MANAGE_INTERVAL);

		/*从线程池中获取线程状态,由于要访问共享数据区,要进行加锁和解锁*/
		pthread_mutex_lock(&(pool->self_lock));
		int taskq_size = pool->taskq_size;
		int live_thr_num = pool->live_thr_num;
		pthread_mutex_unlock(&(pool->self_lock));

		pthread_mutex_lock(&(pool->busy_thr_num_lock));
		int busy_thr_num = pool->busy_thr_num;
		pthread_mutex_unlock(&(pool->busy_thr_num_lock));

		/*创建新线程:任务数大于最小线程个数,且存活线程数小于最大线程个数*/
		if ((taskq_size >= MIN_WAIT_TASK_NUM) && (live_thr_num <= pool->max_thr_num)) {
			pthread_mutex_lock(&(pool->self_lock));
			int cnt = 0;
			/*每次增加DEFAULT_THREAD_VARY个子线程*/
			for (i = 0; i < pool->max_thr_num && pool->live_thr_num < pool->max_thr_num && cnt < DEFAULT_THREAD_VARY; ++i) {
				if (pool->worker_tids[i] == 0 && !is_thread_alive(pool->worker_tids[i])) {
					pthread_create(&(pool->worker_tids[i]), NULL, worker_callback, (void*)pool);
					cnt++;
					pool->live_thr_num++;
				}
			}
			pthread_mutex_unlock(&(pool->self_lock));
		}

		/*销毁多余子线程*/
		if (busy_thr_num * 2 < live_thr_num && live_thr_num > pool->min_thr_num) {
			pthread_mutex_lock(&(pool->self_lock));
			pool->dying_thr_num = DEFAULT_THREAD_VARY;
			pthread_mutex_unlock(&(pool->self_lock));

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

threadpool_addtask

  • 加锁
  • 初始化任务队列结构体成员
  • 利用环形队列机制,实现添加任务
  • 唤醒阻塞在条件变量上的线程
  • 解锁
int threadpool_addtask(threadpool_t* pool, void* (*callback)(void* arg), void* arg) {
	pthread_mutex_lock(&(pool->self_lock));

	while ((pool->taskq_size == pool->taskq_capacity) && (!pool->shutdown)) {
		pthread_cond_wait(&(pool->taskq_not_full), &(pool->self_lock));
	}

	/*如果线程池被关闭了,通知所有线程自杀*/
	if (pool->shutdown) {
		pthread_cond_broadcast(&(pool->taskq_not_empty));
		pthread_mutex_unlock(&(pool->self_lock));
		return 0;
	}

	/*任务队列的队尾元素的参数设置为NULL(为什么)*/
	if (pool->task_queue[pool->taskq_rear].arg != NULL) {
		pool->task_queue[pool->taskq_rear].arg = NULL;
	}

	/*给任务队列的队尾添加任务:设置回调函数和其参数*/
	pool->task_queue[pool->taskq_rear].callback = callback;
	pool->task_queue[pool->taskq_rear].arg = arg;
	pool->taskq_rear = (pool->taskq_rear + 1) % pool->taskq_capacity;
	pool->taskq_size++;

	/*唤醒阻塞在条件变量上的线程*/
	pthread_cond_signal(&(pool->taskq_not_empty));
	pthread_mutex_unlock(&(pool->self_lock));
	return 0;
}

线程池释放和销毁

在这里插入图片描述

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

	int i = 0;
	for (i = 0; i < pool->live_thr_num; ++i) {
		pthread_cond_broadcast(&(pool->taskq_not_empty));
	}

	/*销毁任务线程*/
	for (i = 0; i < pool->live_thr_num; ++i) {
		pthread_join(pool->worker_tids[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->worker_tids) {
		free(pool->worker_tids);
	}
	pthread_mutex_destroy(&(pool->self_lock));
	pthread_mutex_destroy(&(pool->busy_thr_num_lock));
	pthread_cond_destroy(&(pool->taskq_not_empty));
	pthread_cond_destroy(&(pool->taskq_not_full));

	free(pool);
	return 0;
}

UDP客户端和服务器

在这里插入图片描述
UDP通信Server和Client流程:

在这里插入图片描述

UDP的socket函数的参2传入SOCK_DGRAM,表示报式协议

UDP实现的C/S模型:

recv()和send()只能用于TCP通信,以代替read()和write()

由于UDP无连接的特性,accept()和connect()过程被舍弃,流程分析如下:

Server:

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bind();

while(1){
	recvfrom();
	小写->大写;
	sendto();
}
close(sockfd);

Client:

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sendto();
recvfrom();
写到屏幕;
close();

recvfrom和sendto函数

recvfrom:

ssize_t recvfrom(int sockfd, void* buf, size_t len, int flags,struct sockaddr* src_addr, socklen_t* addrlen);

recvfrom涵盖accpet传出地址结构的作用

src_addr是传出参数:传出对端地址结构

addrlen是传入传出参数:传入本端地址结构大小,传出对端地址结构大小

成功返回接受数据字节数,失败返回-1并设置errno

sendto:

ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,const struct sockaddr* dest_addr,socklen_t addrlen);

dest_addr是传入参数:传入对端的地址结构

成功返回写出的字节数,失败返回-1并设置errno

UDP实现的服务器和客户端

UDP服务器本身就是并发的,因为它无需建立连接

服务器:

//UDPServer.c
#include "wrap.h"
#define SERVER_PORT 9527

int main() {
	int sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
	struct sockaddr_in serveraddr;
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(SERVER_PORT);
	serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
	Bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

	char buf[BUFSIZ];
	struct sockaddr_in clientaddr;
	bzero(&clientaddr, sizeof(clientaddr));
	socklen_t clientaddr_len = sizeof(clientaddr);
	while (1) {
		ssize_t n = recvfrom(sockfd, buf, BUFSIZ, 0, (struct sockaddr*)&clientaddr, &clientaddr_len);
		if (n == -1) {
			perr_exit("recvfrom error");
		}
		for (int i = 0; i < n; ++i) {
			buf[i] = toupper(buf[i]);
		}
		n = sendto(sockfd, buf, n, 0, (struct sockaddr*)&clientaddr, clientaddr_len);
		if (n == -1) {
			perr_exit("sendto error");
		}
	}
	close(sockfd);
	return 0;
}

客户端:

UDPClient.c
#include "wrap.h"

#define SERVER_PORT 9527

int main(int argc, char* argv[]) {
	int sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
	struct sockaddr_in serveraddr;
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sin_family = AF_INET;
	serveraddr.sin_port = htons(SERVER_PORT);
	int serverip;
	inet_pton(AF_INET, "192.168.93.11", &serveraddr.sin_addr.s_addr);

	//Bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));

	char buf[1024];
	while (fgets(buf, 1024, stdin) != NULL) {
		ssize_t n = sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
		if (n == -1) {
			perr_exit("sendto error");
		}
		n = recvfrom(sockfd, buf, 1024, 0, NULL, 0);
		if (n == -1) {
			perr_exit("recvfrom error");
		}
		write(STDOUT_FILENO, buf, n);
	}
	close(sockfd);
	return 0;
}

本地套接字通信

int socket(int domain, int type, int protocol);
  • 此时domain传入AF_UNIXAF_LOCAL
  • type传入SOCK_STREAMSOCK_DGRAM都可
  • protocol传0

注意地址结构变为sockaddr_un

在这里插入图片描述

地址结构中的相关信息:

struct sockaddr_un {
	__kernel_sa_family sun_family;
	char sun_path[UNIX_PATH_MAX];
}

在这里插入图片描述

本地套接字通信;socket是伪文件,文件大小始终是0

服务器端:

#include "wrap.h"

#define SERVER_ADDR "server.socket"

int main(int argc, char* argv[]) {
	int size, i;
	struct sockaddr_un serveraddr, clientaddr;
	char buf[BUFSIZ];

	int listen_fd = Socket(AF_UNIX, SOCK_STREAM, 0);

	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sun_family = AF_UNIX;
	strcpy(serveraddr.sun_path, SERVER_ADDR);

	socklen_t len = offsetof(struct sockaddr_un, sun_path) + strlen(serveraddr.sun_path);

	unlink(SERVER_ADDR);
	Bind(listen_fd, (struct sockaddr*)&serveraddr, len);
	Listen(listen_fd, 4);

	while (1) {
		len = sizeof(clientaddr);

		int connect_fd = Accept(listen_fd, (struct sockaddr*)&clientaddr, (socklen_t*)&len);

		len -= offsetof(struct sockaddr_un, sun_path);
		clientaddr.sun_path[len] = '\0';

		printf("client bind filename:%s\n", clientaddr.sun_path);

		while ((size = read(connect_fd, buf, sizeof(buf))) > 0) {
			for (i = 0; i < size; ++i)
				buf[i] = toupper(buf[i]);
			write(connect_fd, buf, size);
		}

		close(connect_fd);
	}
	close(listen_fd);
	return 0;
}

客户端:

#include "wrap.h"

#define SERVER_ADDR "server.socket"
#define CLIENT_ADDR "client.socket"

int main(void) {
	int len;
	struct sockaddr_un clientaddr, serveraddr;
	char buf[BUFSIZ];

	int connect_fd = Socket(AF_UNIX, SOCK_STREAM, 0);

	/*绑定客户端的地址结构*/
	bzero(&clientaddr, sizeof(clientaddr));
	clientaddr.sun_family = AF_UNIX;
	strcpy(clientaddr.sun_path, CLIENT_ADDR);
	len = offsetof(struct sockaddr_un, sun_path) + strlen(clientaddr.sun_path);
	unlink(CLIENT_ADDR);
	Bind(connect_fd, (struct sockaddr*)&clientaddr, len);

	/*服务器的地址结构也要绑定*/
	bzero(&serveraddr, sizeof(serveraddr));
	serveraddr.sun_family = AF_UNIX;
	strcpy(serveraddr.sun_path, SERVER_ADDR);
	len = offsetof(struct sockaddr_un, sun_path) + strlen(serveraddr.sun_path);

	Connect(connect_fd, (struct sockaddr*)&serveraddr, len);

	while (fgets(buf, sizeof(buf), stdin) != NULL) {
		write(connect_fd, buf, strlen(buf));
		len = read(connect_fd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, len);
	}
	close(connect_fd);
	return 0;
}

本地套接字和网络套接字实现对比:

服务器端对比:
在这里插入图片描述

客户端对比:
在这里插入图片描述

总结:

在这里插入图片描述

在这里插入图片描述

libevent

在这里插入图片描述

libevent库的下载和安装

源码包安装三部曲(参考README):

  • ./configure:检查安装环境,生成makefile
  • make:生成.o文件和可执行文件
  • sudo make install:将必要的资源cp至系统指定目录

安装完成后进入sample目录,运行demo验证库安装情况

编译使用库的.c源码时, 要指定库名称:-levent

库路径:

/usr/local/lib

helloworld

libevent特性:基于"事件"的异步通信模型——回调

异步:函数"注册"时间和函数真正被执行的时间不同,函数真正是被内核调用(等待某一条件满足)

struct event_base* base = event_base_new();
struct evconnlistener* listener = evconnlistener_new_bind(...);
struct event* signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base);

base相当于插排,后面到来的事件都插在base上

event_base_dispatch(base);

evconnlistener_free(listener);
event_free(signal_event);
event_base_free(base);

printf("done\n");
return 0;

event_base_dispatch相当于在while(1)中的epoll,所以后面三行几乎执行不到

libevent封装的框架思想

应用五部曲:

//创建event_base(事件底座)
struct event_base* base = event_base_new();

//创建事件event
常规事件event:event_new();
bufferevent:bufferevent_socket_new();

//将事件添加到base上
int event_add(struct event* ev, const struct timeval* tv);

//循环监听事件满足
int event_base_dispatch(struct event_base* base);
event_base_dispatch(base);					//调用

//释放event_base
event_base_free(base);

在这里插入图片描述

在这里插入图片描述

//event-demo.c
int main(int argc, char* argv[]) {
    int i = 0;
    struct event_base* base = event_base_new();

    const char** buf;
    const char* str;

    buf = event_get_supported_methods();

    str = event_base_get_method(base);
    printf("str=%s\n", str);

    for (i = 0; i < 10; ++i) {
        printf("buf[i]=%s\n", buf[i]);
    }

    return 0;
}

创建事件对象event:

struct event* event_new(struct event_base* base,evutil_socket_t fd,short what,event_callback_fd cb,void* arg);

参数:

  • base:基事件, 也就是event_base_new()的返回值
  • fd:绑定到event上的文件描述符
  • what:文件描述符对应的事件(r/w/e)
  • cb:一旦满足监听条件,回调的函数
  • arg:回调函数的参数

返回值:成功返回创建的事件对象event

what的取值:

EV_READ:读一次

EV_WRITE:写一次

EV_PERSIST:持续触发,可以理解为while(read())或while(write())

回调函数:

typedef void (*event_callback_fn)(evutil_socket_t fd,short what,void* arg);

事件event操作

将事件添加到event_base上:

int event_add(struct event* ev,const strcut timeval* tv);

参数:

  • ev是要添加的事件对象,就是event_new的返回值
  • tv一般传NULL,表示一直等到事件被触发,回调函数才会被调用。如果传非0,会等待事件被触发,如果事件一直不触发,时间到,回调函数依然会被调用

返回值:成功返回0;失败返回-1

从event_base上摘下事件(不常用):

int event_del(struct event* ev);

ev是要摘下的事件对象,就是event_new的返回值

销毁事件:

int event_free(strcut event* ev);

ev是要销毁的事件对象,就是event_new的返回值

使用FIFO的读写编码实现

读进程:

void perr_exit(const char* str) {
    perror(str);
    exit(1);
}

void read_cb(evutil_socket_t fd, short what, void* arg) {
    char buf[BUFSIZ] = {0};
    read(fd, buf, sizeof(buf));

    printf("Read from writer:%s\n", buf);
    printf("what=%s\n", what & EV_READ ? "Yes" : "No");
    sleep(1);

    return;
}

int main(int argc, char* argv[]) {
    int ret = 0;
    int fd = 0;

    unlink("myfifo");
    mkfifo("myfifo", 0644);

    fd = open("myfifo", O_RDONLY | O_NONBLOCK);
    if (fd == -1)
        perr_exit("open error");

    struct event_base* base = event_base_new();

    struct event* ev = NULL;
    ev = event_new(base, fd, EV_READ | EV_PERSIST, read_cb, NULL);

    event_add(ev, NULL);

    event_base_dispatch(base);

    event_base_free(base);
    close(fd);
    return 0;
}

写进程:

/*读回调函数*/
void read_cb(struct bufferevent* bev, void* arg) {
	char buf[1024] = {0};

	/*使用bufferevnet_read从被包裹的套接字中读数据到buf*/
	bufferevent_read(bev, buf, sizeof(buf));
	printf("Client says:%s\n", buf);

	char* p = "fuckyou\n";

	/*给客户端回应*/
	bufferevent_write(bev, p, strlen(p) + 1);
	sleep(1);

	return;
}

/*写回调函数,向buffer中写完后调用此函数打印提示信息*/
void write_cb(struct bufferevent* bev, void* arg) {
	printf("I'm server,write to client successfully |:)\n");
	return;
}

/*事件回调函数,处理异常*/
void event_cb(struct bufferevent* bev, short events, void* arg) {
	if (events & BEV_EVENT_EOF)
		printf("Connection closed\n");
	else if (events & BEV_EVENT_ERROR)
		printf("Connectiong error\n");

	bufferevent_free(bev);
	printf("bufferevent has been free\n");

	return;
}

/*监听器的监听器的回调函数*/
void cb_listener(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr* addr, int len, void* ptr) {
	printf("New client connected\n");
	/*把传进来的基事件指针接收下来*/
	struct event_base* base = (struct event_base*)ptr;

	/*创建bufferevnet事件对象*/
	struct bufferevent* bev;
	bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

	/*设置上回调函数*/
	bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);

	/*设置读缓冲使能*/
	bufferevent_enable(bev, EV_READ);
	return;
}

int main(int argc, char* argv[]) {
	/*定义并初始化服务器的地址结构*/
	struct sockaddr_in serverAddr;
	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(g_port);

	/*创建基事件*/
	struct event_base* base;
	base = event_base_new();

	/*创建监听器,把基事件作为参数传递给他*/
	struct evconnlistener* listener;
	listener = evconnlistener_new_bind(base, cb_listener, base, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 36, (struct sockaddr*)&serverAddr, sizeof(serverAddr));

	/*循环监听*/
	event_base_dispatch(base);

	/*释放资源*/
	evconnlistener_free(listener);
	event_base_free(base);

	return 0;
}

未决和非未决

未决:有资格被处理,但还没有被处理

非未决:没有资格被处理
在这里插入图片描述

libevent框架:

在这里插入图片描述

创建事件:
在这里插入图片描述

将事件添加到基事件:

int event_add(struct event* ev, const struct timeval* tv);

摘除和销毁事件:

int event_del(struct event* ev);
int event_free(struct event* ev);

bufferevent

在这里插入图片描述

原理:bufferent利用队列实现两个缓冲区(数据读走就没, FIFO)

读:有数据,读回调函数被调用,使用bufferevent_read()读数据

写:使用bufferevent_write,向写缓冲中写数据,该缓冲区中有数据自动写出,写完后,回调函数被调用(鸡肋)

bufferevent事件对象创建和销毁

创建bufferevent:

struct bufferevent* bufferevent_socket_new(struct event_base* base,
                                           evutil_socket_t fd,
                                           enum bfferevent_options options)

base:基事件,event_base_new函数的返回值

fd:封装到bufferevent内的fd(绑定在一起)

enum表示枚举类型,一般取BEV_OPT_CLOSE_ON_FREE

成功返回bufferevent事件对象

销毁bufferevent:

void bufferevent_socket_free(struct bufferevent* ev)

给bufferevent事件设置回调

void bufferevent_setcb(struct bufferevent* bufev,
                      bufferevent_data_cb readcb,
                      bufferevent_data_cb writecb,
                      bufferevent_event_cb eventcb,
                      void* cbarg);

bufev:bufferevent_socket_new()函数的返回值

readcb:读缓冲对应的回调,自己封装,在其内部读数据(注意是用bufferevent_read()读,而不是read())

writecb:鸡肋,传NULL即可

eventcb:可传NULL

cbarg:回调函数的参数

readcb对应的回调函数:

typedef void (*bufferevent_data_cb)(struct bufferevent* bev,void* ctx);
void read_cb(struct bufferevent* bev,void* arg){
    ...
    bufferevent_read();
}

读数据:从bufferevent输入缓冲区中移除数据

size_t bufferevent_read(struct bufferevent* bufev,void* data,size_t size);

写数据:

int bufferevent_write(struct bufferevent* bufev,const void* data,size_t size)

eventcb对应的回调函数:

typedef void (*bufferevent_event_cb)(struct bufferevent* bev,short events,void* ctx)

在这里插入图片描述

缓冲区开启和关闭

默认新建的bufferevent写缓冲是enable的,而读缓冲是disable的

通过两个函数操作缓冲区读写使能:

void bufferevent_enable(struct bufferevent* bufev,short events);		//启用缓冲区
void bufferevnet_disable(struct bufferevent* bufev,short events);		//禁用

/*例如:开启读缓冲*/
void bufferevent_enable(bufev,EV_READ);

events的值可传入三个宏:

  • EV_READ
  • EV_WRITE
  • EV_READ|EV_WRITE

获取缓冲区的禁用状态:

short bufferevent_get_enable(struct bufferevent* bufev)

具体的状态需要借助&来得到

客户端和服务器连接和监听

客户端建立连接:

int bufferevent_socket_connect(struct bufferevent* bev,struct sockaddr* address,int addrlen);

bev:bufferevent事件对象(封装了fd)

address,len:等同于connect()的参2和参3

服务器创建监听器:

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:回调函数的参数
  • flags:可识别的标志,通常传:
    ​ LEV_OPT_CLOSE_ON_FREE(释放bufferevent时关闭底层传输端口, 这将关闭底层套接字, 释放底层bufferevent等)
    ​ LEV_OPT_REUSEABLE(可以端口复用)
  • backlog:相当于listen的参2, 传-1表示使用默认的最大值
  • sa:服务器自己的地址结构
  • socklen:sa的大小

这一个函数可以完成socket(),bind(),listen(),accept()四个函数的作用

回调函数的类型:

typedef void (*evconnlistener_cb)(struct evconnlistener* listener,
                                 evutil_socker_t sock,
                                 struct sockaddr* addr,
                                 int len,
                                 void* ptr);
  • listener:evconnlistener_new_bind(0函数的返回值
  • sock:用于通信的文件描述符
  • addr:客户端的地址结构
  • len:客户端地址结构的长度
  • ptr:外部ptr传进来的值

该回调函数不由我们调用,是框架自动调用,因此只需知晓参数含义即可

libevent实现TCP服务器流程

  1. 创建基事件event_base
  2. 创建bufferevent事件对象:bufferevent_socket_new()
  3. 使用bufferevent_setcb()函数给bufferevent的read,write,event设置回调函数
  4. 当监听的事件满足时,read_cb会被调用,在其内部bufferevent_read(),读
  5. 使用evconlistener_new_bind()创建监听服务器,设置其回调函数,当有客户端成功连接时,这个回调函数会被调用
  6. 封装listner_cb()在函数内部,完成与客户端通信
  7. 设置读写缓冲enable
  8. 启动循环event_base_dispath()
  9. 释放连接

libevent实现TCP服务器源码分析

//event-server.c
/*读回调函数*/
void read_cb(struct bufferevent* bev, void* arg) {
	char buf[1024] = {0};

	/*使用bufferevnet_read从被包裹的套接字中读数据到buf*/
	bufferevent_read(bev, buf, sizeof(buf));
	printf("Client says:%s\n", buf);

	char* p = "fuckyou\n";

	/*给客户端回应*/
	bufferevent_write(bev, p, strlen(p) + 1);
	sleep(1);

	return;
}

/*写回调函数,向buffer中写完后调用此函数打印提示信息*/
void write_cb(struct bufferevent* bev, void* arg) {
	printf("I'm server,write to client successfully |:)\n");
	return;
}

/*事件回调函数,处理异常*/
void event_cb(struct bufferevent* bev, short events, void* arg) {
	if (events & BEV_EVENT_EOF)
		printf("Connection closed\n");
	else if (events & BEV_EVENT_ERROR)
		printf("Connectiong error\n");

	bufferevent_free(bev);
	printf("bufferevent has been free\n");

	return;
}

/*监听器的监听器的回调函数*/
void cb_listener(struct evconnlistener* listener, evutil_socket_t fd, struct sockaddr* addr, int len, void* ptr) {
	printf("New client connected\n");
	/*把传进来的基事件指针接收下来*/
	struct event_base* base = (struct event_base*)ptr;

	/*创建bufferevnet事件对象*/
	struct bufferevent* bev;
	bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

	/*设置上回调函数*/
	bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);

	/*设置读缓冲使能*/
	bufferevent_enable(bev, EV_READ);
	return;
}

int main(int argc, char* argv[]) {
	/*定义并初始化服务器的地址结构*/
	struct sockaddr_in serverAddr;
	memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
	serverAddr.sin_port = htons(g_port);

	/*创建基事件*/
	struct event_base* base;
	base = event_base_new();

	/*创建监听器,把基事件作为参数传递给他*/
	struct evconnlistener* listener;
	listener = evconnlistener_new_bind(base, cb_listener, base, LEV_OPT_CLOSE_ON_FREE | LEV_OPT_REUSEABLE, 36, (struct sockaddr*)&serverAddr, sizeof(serverAddr));

	/*循环监听*/
	event_base_dispatch(base);

	/*释放资源*/
	evconnlistener_free(listener);
	event_base_free(base);

	return 0;
}

客户端流程简析和回顾

  1. 创建event_base
  2. 使用bufferevent_socket_new()创建一个用跟服务器通信的bufferevent事件对象
  3. 使用bufferevent_socket_connect连接服务器
  4. 使用bufferevent_setcb()给bufferevent对象的read,write,event设置回调
  5. 设置bufferevent对象的读写缓冲区使能
  6. 接受,发送数据bufferevent_read()/bufferevent_write()
  7. 启动循环监听event_base_dispath()
  8. 释放资源
/*读回调函数*/
void read_cb(struct bufferevent* bev, void* arg) {
    char buf[1024] = {0};

    bufferevent_read(bev, buf, sizeof(buf));

    printf("Server says:%s\n", buf);

    bufferevent_write(bev, buf, strlen(buf) + 1);

    sleep(1);
    return;
}

/*写回调函数*/
void write_cb(struct bufferevent* bev, void* arg) {
    printf("I'm client's write_cb,I'm usless,:(\n");
    return;
}

/*事件回调函数*/
void event_cb(struct bufferevent* bev, short events, void* arg) {
    if (events & BEV_EVENT_EOF)
        printf("End of file\n");
    else if (events & BEV_EVENT_ERROR)
        printf("Something error\n");
    else if (events & BEV_EVENT_CONNECTED)
        printf("Server connected\n");

    bufferevent_free(bev);
    return;
}

/*读终端的回调函数*/
void read_terminal(evutil_socket_t fd, short what, void* arg) {
    char buf[1024] = {0};
    struct bufferevent* bev = (struct bufferevent*)arg;
    int len = 0;
    len = read(fd, buf, sizeof(buf));

    bufferevent_write(bev, buf, len + 1);

    return;
}

int main(int argc, char* argv[]) {
    int fd = 0;
    struct sockaddr_in serverAddr;
    memset(&serverAddr, 0, sizeof(serverAddr));

    struct event_base* base = NULL;
    base = event_base_new();

    struct bufferevent* bev;
    bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);

    fd = socket(AF_INET, SOCK_STREAM, 0);
    serverAddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr.s_addr);
    serverAddr.sin_port = htons(g_port);

    bufferevent_socket_connect(bev, (struct sockaddr*)&serverAddr,
                               sizeof(serverAddr));
    bufferevent_setcb(bev, read_cb, write_cb, event_cb, NULL);

    bufferevent_enable(bev, EV_READ);

    struct event* ev =
        event_new(base, STDIN_FILENO, EV_READ | EV_PERSIST, read_terminal, bev);
    event_add(ev, NULL);

    event_base_dispatch(base);

    event_free(ev);
    event_base_free(base);
    return 0;
}

在这里插入图片描述

Web服务器

实现一个简单的web服务器myhttpd。能够给浏览器提供服务,供用户借助浏览器访问主机中的文件

html

大多数标签成对儿出现,不成对儿出现的被称为短标签

<!--html-demo.html-->
<!DOCTYPE html>
<html>
	<head>
		<title>I'm head</title>
	</head>

	<body>
		<!--I'm a comment-->
		<h1>I'm header1</h1>
		<h2>I'm header2</h2>
		<h3>I'm header3</h3>
		<!--size取1~7-->
		<font color="red" size="7">I'm body</font>
		<br/>

		<strong>I'm strong</strong>
		<br/>

		<em>I'm em</em>
		<br/>

		<del>I'm delete</del>
		<br/>

		<ins>I'm ins</ins>
		<br/>

		<!--p标签自动换行,块元素-->
		<p>I'm p</p>
		<p>I'm p</p>

		<!--水平线-->
		<hr size=70 color="green"/>

		<div align="center">
			I'm div1
		</div>

	</body>

</html>

404页面html:

<!--404.html-->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>404NotFound</title>
</head>

<body>
    <h1>404 not found</h1>
</body>

<style>
    h1 {
        width: 500px;
        margin: 0px auto;
    }
</style>

</html>

列表&图片和超链接:

<!--html-demo2.html-->
<!DOCTYPE html>
<html>
	<head>
		<title>A Demo</title>
	</head>

	<body>
		<!--锚点-->
		<p id="top">

		<ul type="circle">
			<li>option1</li>
			<li>option2</li>
			<li>option3</li>
			<li>option4</li>
		</ul>

		<ol type="A">
			<li>option1</li>
			<li>option2</li>
			<li>option3</li>
			<li>option4</li>
		</ol>

		<img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" width="300"/>
		<img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
		<img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
		<img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />
		<img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" />

		<a href="http://jd.com" target="_blank" title="去京东">请跳转至京东</a>

		<!--给图片设置超链接-->
		<a href="http://jd.com" target="_blank" title="去京东">
			<img src="/home/daniel/图片/Vincent-Willem-Van-Gogh.jpg" alt="图片加载失败" title="VanGogh" width="100"/>
		</a>

		<a href="#top">回到顶部</a>

	</body>
</html>

http协议格式

通常HTTP消息包括客户机向服务器的请求和服务器向客户机的响应消息

请求消息(浏览器发给服务器):

  1. 请求行:说明请求类型,要访问的资源以及使用的http版本
  2. 请求头:说明服务器要使用的附加信息
  3. 空行:必须有!即使没有请求数据
  4. 请求数据:也叫主体,可以添加任意的其他数据

以下是浏览器发送给服务器的http协议头内容举例:

GET /hello.c HTTP/1.1
Host:localhost:2222
User-Agent:Mozilla/5.0(X11;Ubuntu;Linux i686;rv:24.0)Gecko/201001	01 Firefox/24.0
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language:zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding:gzip,deflate
Connection:keep-alive
If-Modified-Since:Fri,18 Jul 2014 08:36:36 GMT
\r\n

在这里插入图片描述

响应消息(服务器发给浏览器):

  1. 状态行:包括http协议版本号,状态码,状态信息
  2. 消息报头:说明客户端要使用的一些附加信息
  3. 空行:必须!
  4. 响应正文:服务器返回给客户端的文本信息(或数据流)
HTTP/1.1 200 OK
Server:xhttpd
Date:Fri,18 Jul 2014 14:34:26 GMT
Content-Type:text/plain;charset=iso-8859-1
Content-Length:32
Content-Language:zh-CN
Last-Modified:Fri,18,Jul 2014 08:36:36 GMT
Connection:close
\r\n

响应正文

在这里插入图片描述

Server框架:

int main(int argc, char* argv[]) {
    if (argc < 3) {
        printf("Usage: ./server <port> <path>\n");
        exit(1);
    }
    g_port = atoi(argv[1]);
    int ret = chdir(argv[2]);	//expect "http/"
    if (ret != 0) {
        perr_exit("chdir error");
	}

    epoll_run(g_port);

    return 0;
}

void epoll_run(int port) {
    int i = 0;
    struct epoll_event all_events[MAXSIZE];

    int epfd = epoll_create(MAXSIZE);
    if (epfd == -1) {
		perr_exit("epoll_create error");
    }

    int lfd = init_listen_fd(port, epfd);

    while (1) {
        int N = epoll_wait(epfd, all_events, MAXSIZE, -1);
        if (N == -1) {
            perr_exit("epoll_wait error");
        }

        for (i = 0; i < N; ++i) {
            struct epoll_event* pev = all_events + i;

            if (!(pev->events & EPOLLIN))
                continue;

            if (pev->data.fd == lfd) {
                do_accept(lfd, epfd);
            } else {
                do_read(pev->data.fd, epfd);
            }
        }
    }
}

int init_listen_fd(int port, int epfd) {
    int ret = 0;
    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    if (lfd == -1) {
        perr_exit("socket error");
    }

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(g_port);

    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    ret = bind(lfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
    if (ret == -1) {
        perr_exit("bind error");
    }

    ret = listen(lfd, 128);
    if (ret == -1) {
        perr_exit("listen error");
	}

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = lfd;

    ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    if (ret == -1) {
        perr_exit("epoll_ctl error");
	}

    return lfd;
}

void do_accept(int lfd, int epfd) {
    struct sockaddr_in clientaddr;
    socklen_t clientaddr_len = sizeof(clientaddr);

    int cfd = accept(lfd, (struct sockaddr*)&clientaddr, &clientaddr_len);
    if (cfd == -1) {
        perr_exit("accept error");
	}

    int flag = fcntl(cfd, F_GETFL);
    flag = flag | O_NONBLOCK;
    fcntl(cfd, F_SETFL, flag);

    struct epoll_event ev;
    ev.data.fd = cfd;

    ev.events = EPOLLIN | EPOLLET;	//ET模式
    int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
    if (ret == -1) {
        perr_exit("epoll_ctl add cfd error");
	}
}

getline函数,用于读取http协议头:

int get_line(int cfd, char* buf, int size) {
    int i = 0;
    char c = '\0';
    int n = 0;

    while ((i < size - 1) && (c != '\n')) {
        n = recv(cfd, &c, 1, 0);
        if (n > 0) {
            if (c == '\r') {
                n = recv(cfd, &c, 1, MSG_PEEK);	//拷贝读一次
                if ((n > 0) && (c == '\n')) {
                    recv(cfd, &c, 1, 0);	//实际读一次
                } else {
                    c = '\n';
                }
            }
            buf[i] = c;
            i++;
        } else {
            c = '\n';
        }
    }
    buf[i] = '\0';
    if (n == -1) {
        i = n;
    }
    return i;
}

在这里插入图片描述

单文件通信流程

  • get_line()获取http协议的第一行
  • 从首行中拆分GET,文件名,协议版本。获取用户请求的文件名
  • 判断文件是否存在,用stat()函数
  • 如果文件存在:open()打开,read()内容,写回给浏览器
  • 先写http应答协议头
HTTP/1.1 200 OK
Server:xhttpd
Date:Fri,18 Jul 2014 14:34:26 GMT
Content-Type:text/plain;charset=iso-8859-1
Content-Length:32
Content-Language:zh-CN
Last-Modified:Fri,18,Jul 2014 08:36:36 GMT
Connection:close
\r\n
  • 再写文件数据

错误处理函数:

//epoll delete; close
void disconnect(int cfd, int epfd) {
    int ret = epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, NULL);
    if (ret == -1) {
        perr_exit("epoll_ctl del error");
	}
    close(cfd);
}

读数据的思路:

void do_read(int cfd,int epfd){
	/*读取一行http协议,拆分,获取get文件名和协议号*/
	char line[1024]={0};
	int len=get_line(cfd,line,sizeof(line));
	if(len==0){
		printf("Client close\n");
		disconnect(cfd,epfd);
	}else{
		/*字符串拆分*/
	}
	return;
}

正则表达式获取文件名

sscanf()函数:

int sscanf(const char* str, const char* format, ...);

在这里插入图片描述

判断文件是否存在

void do_read(int cfd,int epfd){
	/*读取一行http协议,拆分,获取get文件名和协议号*/
	char line[1024]={0};
	int len=get_line(cfd,line,sizeof(line));
	if(len==0){
		printf("Client close\n");
		disconnect(cfd,epfd);
	}else{
		printf("----请求头----\n");
		printf("请求行数据:%s\n",line);
		/*清除多余数据,不让他们拥塞缓冲区*/
		while(1){
			char buf[1024]={0};
			len=get_line(cfd,buf,sizeof(buf));
			if(buf[0]=='\n'){
				break;
			}else if(len==-1){
				break;
			}
		}
		printf("----请求尾----\n");
	}
	/*确定是GET方法(忽略大小写比较字符串前n个字符)*/
	if(strncasecmp("get",line,3)==0){
		http_request(line,cfd);
	}
	disconnect(cfd,epfd);
	return;
}

strncasecmp:忽略大小写比较字符串前n个字节

/*处理http请求-OK*/
void http_request(const char* request,int cfd){
	/*拆分http请求行*/
	char method[12],path[1024],protocol[12];
	sscanf(request,"%[^ ] %[^ ] %[^ ]",method,path,protocol);
	printf("method=%s,path=%s,protocol=%s\n",method,path,protocol);
	/*解码:将不能识别的中文乱码转换为中文*/
	decode_str(path,path);
	char* file=path+1;
	/*如果没有指定访问的资源,默认显示资源目录中的内容*/
	if(!strcmp(path,"/")){
		file="./";
	}
	/*获取文件属性*/
	struct stat st;
	int ret=0;
	ret=stat(file,&st);

	if(ret==-1){
		send_error(cfd,404,"Not Found","No such file or direntry");
		return;
	}

	if(S_ISDIR(st.st_mode)){
		send_respond_head(cfd,200,"OK",get_file_type(".html"),-1);
		send_dir(cfd,file);
	}
	if(S_ISREG(sbuf.st_mode)){
		/*回发http协议头*/
		send_respond_head(cfd,200,"OK",get_file_type(filename),sbuf.st_size);
		send_file(cfd,file);
	}
	return;
}

写出http应答协议头

void send_respond_head(int cfd,int no,const char* desp,const char* type,long len){
	char buf[1024]={0};

	sprintf(buf,"HTTP/1.1 %d %s\r\n",no,desp);
	send(cfd,buf,strlen(buf),0);

	sprintf(buf,"Content-Type:%s\r\n",type);
	sprintf(buf+strlen(buf),"Content-Length:%d\r\n",len);
	send(cfd,buf,strlen(buf),0);

	send(cfd,"\r\n",2,0);
	return;
}

写数据给浏览器

void send_file(int cfd,const char* filename){
	int n=0;
	int ret=0;
	int fd=0;
	char buf[BUFSIZ]={0};

	fd=open(filename,O_RDONLY);
	if(fd==-1){
		send_error(cfd,404,"Not Found","No such file or direntry");
		exit(1);
	}

	while((n=read(fd,buf,sizeof(buf)))>0){
		ret=send(cfd,buf,n,0);
		if(ret==-1){
			if(errno==EAGAIN){
				perror("send error:");
				continue;
			}else if(errno==EINTR){
				perror("send error:");
				continue;
			}else{
				perror("send error:");
				exit(1);
			}
		}
	}
	if(n==-1)
		perr_exit("read file error");
	close(fd);
	return;
}

文件类型区分

const char* get_file_type(const char* name){
	char* dot;
	dot=strrchr(name,'.');

	if(dot==NULL)
		return "text/plain; charset=utf-8";
	if(strcmp(dot,".html")==0||strcmp(dot,"htm")==0)
		return "text/html; charset=utf-8";
	if(strcmp(dot,".jpg")==0||strcmp(dot,"jpeg")==0)
		return "image/jpeg";
	if(strcmp(dot,".gif")==0)
		return "image/gif";
	if(strcmp(dot,".png")==0)
		return "image/png";
	if(strcmp(dot,".css")==0)
		return "text/css";
	if(strcmp(dot,".wav")==0)
		return "audio/wav";
	if(strcmp(dot,".mp3")==0)
		return "audio/mpeg";
	if(strcmp(dot,".avi")==0)
		return "video/x-msvideo";

	return "text/plain; charset=utf-8";
}

错误页面展示

返回值一定要检查,尤其在开发初期

void send_error(int cfd,int status,char* title,char* text){
	char buf[BUFSIZ]={0};

	sprintf(buf,"%s %d %s\r\n","HTTP/1.1",status,title);
	sprintf(buf+strlen(buf),"Content-Type:%s\r\n","text/html");
	sprintf(buf+strlen(buf),"Content-Length:%d\r\n",-1);
	sprintf(buf+strlen(buf),"Contention:close\r\n");
	send(cfd,buf,strlen(buf),0);
	send(cfd,"\r\n",2,0);

	memset(buf,0,BUFSIZ);

	sprintf(buf,"<html><head><title>%d %s</title></head>\n",status,title);
	sprintf(buf+strlen(buf),"<body bgcolor=\"#cc99cc\"><h3 align=\"center\">%d %s</h4>\n",status,title);
	sprintf(buf+strlen(buf),"%s\n",text);
	sprintf(buf+strlen(buf),"<hr>\n</body>\n</html>\n");
	send(cfd,buf,strlen(buf),0);

	return;
}

在这里插入图片描述

浏览器请求目录

/*发送目录数据-OK*/
void send_dir(int cfd,const char* dirname){
	int i=0;
	int ret=0;
	int num=0;

	char buf[4096]={0};
	sprintf(buf,"<html><head><title>目录名:%s</title></head>",dirname);
	sprintf(buf+strlen(buf),"<body><h1>当前目录:%s</h1><table>",dirname);

	char enstr[1024]={0};
	char path[1024]={0};

	struct dirent** ptr;
	num=scandir(dirname,&ptr,NULL,alphasort);

	for(i=0;i<num;++i){
		char* name=ptr[i]->d_name;

		sprintf(path,"%s/%s",dirname,name);
		printf("path=%s\n",path);
		struct stat st;
		stat(path,&st);
		/*编码生成Unicode编码:诸如%E5%A7...等*/
		encode_str(enstr,sizeof(enstr),name);

		if(S_ISREG(st.st_mode)){
			sprintf(buf+strlen(buf),"<tr><td><a href=\"%s\">%s</a></td>%ld</td></tr>",
                    enstr,name,(long)st.st_size);
		}else if(S_ISDIR(st.st_mode)){
			sprintf(buf+strlen(buf),"<tr><td><a href=\"%s/\">%s</a></td><td>%ld</td></tr>",
                    enstr,name,(long)st.st_size);
		}

		ret=send(cfd,buf,strlen(buf),0);
		if(ret==-1){
			if(errno==EAGAIN){
				perror("send error:");
				continue;
			}else if(errno==EINTR){
				perror("send error:");
				continue;
			}else{
				perror("send error:");
				exit(1);
			}
		}
		memset(buf,0,sizeof(buf));
	}

	sprintf(buf+strlen(buf),"</table></body></html>");
	send(cfd,buf,strlen(buf),0);

	printf("dir message send OK\n");
	return;
}

判断文件类型

/*判断文件类型*/
const char* get_file_type(const char* name){
	char* dot;
	dot=strrchr(name,'.');

	if(dot==NULL)
		return "text/plain; charset=utf-8";
	if(strcmp(dot,".html")==0||strcmp(dot,"htm")==0)
		return "text/html; charset=utf-8";
	if(strcmp(dot,".jpg")==0||strcmp(dot,"jpeg")==0)
		return "image/jpeg";
	if(strcmp(dot,".gif")==0)
		return "image/gif";
	if(strcmp(dot,".png")==0)
		return "image/png";
	if(strcmp(dot,".css")==0)
		return "text/css";
	if(strcmp(dot,".wav")==0)
		return "audio/wav";
	if(strcmp(dot,".mp3")==0)
		return "audio/mpeg";
	if(strcmp(dot,".avi")==0)
		return "video/x-msvideo";
	/*其他的文件一律当作文本文件处理*/
	return "text/plain; charset=utf-8";
}

汉字字符编码和解码

每一个汉字在浏览器前端中会被转码成Unicode码进行显示

因此在访问带有汉字的文件时,应该在服务器回发数据给浏览器时进行编码操作,在浏览器请求资源目录的汉字文件时进行解码操作

/*16进制字符转化为10进制-OK*/
int hexit(char c){
	if(c>='0'&&c<='9')
		return c-'0';
	if(c>='a'&&c<='f')
		return c-'a'+10;
	if(c>='A'&&c<='F')
		return c-'A'+10;

	return 0;
}
/*解码函数-OK*/
void decode_str(char* to,char* from){
	for(;*from!='\0';++to,++from){
		if(from[0]=='%'&&isxdigit(from[1])&&isxdigit(from[2])){
			*to=hexit(from[1])*16+hexit(from[2]);
			from+=2;
		}else{
			*to=*from;
		}
	}
	*to='\0';
	return;
}
/*编码函数-OK*/
void encode_str(char* to,int tosize,const char* from){
	int tolen=0;
	for(tolen=0;(*from!='\0')&&(tolen+4<tosize);++from){
		if(isalnum(*from)||strchr("/_.-~",*from)!=(char*)0){
			*to=*from;
			++to;
			++tolen;
		}else{
			sprintf(to,"%%%02x",(int)*from&0xff);
			to+=3;
			tolen+=3;
		}
	}
	*to='\0';
	return;
}

telnet调试

可使用telnet命令,借助IP和port,模拟浏览器行为,在终端中对访问的服务器进行调试,方便查看服务器会发给浏览器的http协议数据:

$ telnet 127.0.0.1 9527
GET /hello.c http/1.1

此时在终端中可查看到服务器回发给浏览器的http应答协议及数据内容,可根据该信息进行调试

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值