Linux网络编程学习笔记

课程链接: https://www.bilibili.com/video/BV1iJ411S7UA

课程视频资源和笔记: 链接:https://pan.baidu.com/s/10sKjOHfKKEE4CcZaqlYeTw 提取码:unix

练习代码:https://gitee.com/daniel187/Linux_Network


复习Linux系统编程

如何避免死锁:

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

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

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

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

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

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

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


网络基础

网络层次模型

FTP文件传输协议(雏形):

  1. 文件名
  2. 文件大小
  3. 文件内容

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协议)

网络传输数据封装流程:

Linux网络编程学习笔记_第1张图片
网络传输流程: 数据在没有封装之前是不能在网络中进行传递的

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


以太网帧和ARP协议

TCP/IP数据包封装:

Linux网络编程学习笔记_第2张图片
以太网帧格式:

Linux网络编程学习笔记_第3张图片

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

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

ARP数据报格式:

Linux网络编程学习笔记_第4张图片
刚开始时以太网目的地址目的以太网地址都未知, 都填入FF:FF:FF:FF:FF:FF


IP协议

IP数据报格式:

在这里插入图片描述

TTL: time to live, 跳的次数上限, 每经过一个路由结点, 该值–, 减到0丢弃


UDP协议

UDP:

​ 16位源端口号(上限2^16-1=65535)

​ 16位目的端口号

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

IP地址+端口号可以在网络环境中唯一的标定一个进程

80端口: http协议

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


TCP协议

TCP报文格式:
Linux网络编程学习笔记_第5张图片


BS和CS模型

B/S–Browser/Server

优点:

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

缺点:

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

C/S–Client/Server

优点:

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

缺点:

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

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

Linux内核套接字实现:

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


字节序转换

小端法: 高位存在高地址, 低位存在低地址(计算机intel处理器本地采用)

大端法: 高位存在低地址, 低位存在高地址(网络通信采用)

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

#include
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地址结构

#include
#include

int bind(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
/*struct sockaddr是早已废弃的数据结构,已不再使用,用新的时注意强转一下*/
struct sockaddr_in addr;
int bind(int 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个套接字

Linux网络编程学习笔记_第6张图片

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

bind()–绑定IP和port

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

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

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

socket和bind函数

socket创建一个套接字

#include
#include

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
#include
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–传入传出参数

​ socklen_t clit_addr_len=sizeof(addr)

​ 入: 传入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字符串的缓冲区, 大小为1024

客户端程序实现

//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可以索引读写两个缓冲区

TCP连接管理

三次握手建立连接

Linux网络编程学习笔记_第7张图片

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

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

数据通信

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

Linux网络编程学习笔记_第8张图片
批量发送, 服务器ACK回执最后一个数据包, 且可以看到, 滑动窗口的大小win是动态变化的

四次挥手关闭连接

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

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

Linux网络编程学习笔记_第9张图片
半关闭补充说明:

Linux网络编程学习笔记_第10张图片

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

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

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

TCP报文格式:

Linux网络编程学习笔记_第11张图片

窗口大小16位–滑动窗口不能超过65535

TCP通信时序总结

三次握手:

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

四次挥手:

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

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

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

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

Linux网络编程学习笔记_第12张图片

无论是谁调用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;
}

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

Linux网络编程学习笔记_第13张图片
服务器端伪码描述:

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
	}
}

总结:

子进程:c

  1. close(lfd);
  2. read();
  3. 数据处理;
  4. wirte();

父进程:

  1. 注册信号捕捉函数:SIGNAL;
  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
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);
			}
			write(STDOUT_FILENO, buf, n);
			for (int i = 0; i < n; ++i) {
				buf[i] = toupper(buf[i]);
			}
			write(clientfd, buf, n);
		}
	}
	return 0;
}

服务器测试IP地址调整, 设置虚拟机网络为桥接模式

Linux网络编程学习笔记_第14张图片

在虚拟机中手动指定IP地址:

Linux网络编程学习笔记_第15张图片

多机测试时注意指定IP地址

将本地程序拷贝到远端服务器上去的命令: scp -r ./test/ [email protected]:/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;
		}
		write(STDOUT_FILENO, buf, n);
		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返回值:

Linux网络编程学习笔记_第16张图片

TCP连接状态

Linux网络编程学习笔记_第17张图片

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

Linux网络编程学习笔记_第18张图片

主动关闭连接

在这里插入图片描述

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

Linux网络编程学习笔记_第19张图片

被动接受连接

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

被动关闭连接

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

2MSL时长

Linux网络编程学习笔记_第20张图片

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

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

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

端口复用函数

为了不等2MSL的时长

Linux网络编程学习笔记_第21张图片

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

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直接全部关闭所有描述符, 这会影响其他使用该套接字的进程

select多路IO转接服务器

Linux网络编程学习笔记_第22张图片

这种思想很像CPU对IO的处理的发展历程, select的地位就像中断管理器, IO设备有中断请求时才通知CPU, 对应的, 只有当客户端有连接请求时才会通知server进行处理. 也就是说只要server收到通知, 就一定有数据待处理或连接待响应, 不会再被阻塞而浪费资源了

select函数参数简介:

#include 
#include 
#include 
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:

Linux网络编程学习笔记_第23张图片

Linux网络编程学习笔记_第24张图片

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

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,&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转接代码实现:

#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优缺点:

缺点:

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

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

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

#include "033-035_wrap.h"
#define SERVER_PORT 9527
int main(int argc, char *argv[]){
	int i, j, n, maxi;
    /*将需要轮询的客户端套接字放入数组client[FD_SETSIZE]*/
	int nready, client[FD_SETSIZE];
	int listenFd, connectFd, maxFd, socketFd;
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];

	struct sockaddr_in serverAddr, clientAddr;
	socklen_t clientAddrLen;
	
	/*得到监听套接字*/
	listenFd = Socket(AF_INET, SOCK_STREAM, 0);
	
	/*定义两个集合,将listenFd放入allset集合当中*/
	fd_set rset, allset;
	FD_ZERO(&allset);
	FD_SET(listenFd, &allset);
	
	/*设置地址复用*/
	int opt = 1;
	setsockopt(listenFd, 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(listenFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
	Listen(listenFd, 128);
	
	/*将listenFd设置为数组中最大的Fd*/
	maxFd = listenFd;
	maxi = -1;
	/*初始化自己的数组为-1*/
	for (i = 0; i < FD_SETSIZE; ++i)
		client[i] = -1;

	while (1){
		/*把allset给rest,让他去用*/
		rset = allset;
		nready = select(maxFd + 1, &rset, NULL, NULL, NULL);

		if (nready == -1)
			perr_exit("select error");

		/*如果有了新的连接请求,得到connectFd,并将其放入自定义数组中*/
		if (FD_ISSET(listenFd, &rset)){
			clientAddrLen = sizeof(clientAddr);
			connectFd = Accept(listenFd, (struct sockaddr *)&clientAddr, &clientAddrLen);
			printf("Recived from %s at 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] = connectFd;
					break;
				}
			/*自定义数组满了*/
			if(i==FD_SETSIZE){
				fputs("Too many clients\n",stderr);
				exit(1);
			}
			/*connectFd加入监听集合*/
			FD_SET(connectFd, &allset);

			/*更新最大的Fd*/
			if (maxFd < connectFd)
				maxFd = connectFd;
			/*更新循环上限*/
			if(i>maxi)
				maxi=i;
			/*select返回1,说明只有建立连接请求,没有数据传送请求,跳出while循环剩余部分(下面的for循环轮询过程)*/
			if (--nready == 0)
				continue;
		}
		/*select返回不是1,说明有connectFd有数据传输请求,遍历自定义数组*/
		for (i = 0; i <= maxi; ++i){
			if((socketFd=client[i])<0)
				continue;
			/*遍历检查*/
			if (FD_ISSET(socketFd, &rset)){
                /*read返回0说明传输结束,关闭连接*/
				if ((n=read(socketFd,buf,sizeof(buf)))==0){
					close(socketFd);
					FD_CLR(socketFd, &allset);
					client[i]=-1;
				}else if(n>0){
					for (j = 0; j < n; ++j)
						buf[j] = toupper(buf[j]);
					write(socketFd, buf, n);
					write(STDOUT_FILENO, buf, n);
				}
                /*不懂:需要处理的个数减1?*/
				if(--nready==0)
					break;
			}
		}
	}
	close(listenFd);
	return 0;
}

066-poll函数原型分析

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

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

067-poll函数使用注意事项实例

Linux网络编程学习笔记_第25张图片

/*自定义结构体数组并初始化*/
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 ret=poll(fds,5,-1);
    /*轮询是否有POLLIN需求*/
    for(i=0;i<5;i++)
        if(pfds[i].revents&POLLIN){
            Accept();
            Read()/Write();            
        }
}

068-poll函数实现服务器

read函数返回值:

Linux网络编程学习笔记_第26张图片

#include"033-035_wrap.h"

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

int main(int argc,char* argv[]){
	int ret=0;
	/*poll函数返回值*/
	int nready=0;
	int i,j,maxi;
	int connectFd,listenFd,socketFd;
	ssize_t n;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	socklen_t clientLen;
	/*创建结构体数组*/	
	struct pollfd client[OPEN_MAX];
	/*创建客户端地址结构和服务器地址结构*/
	struct sockaddr_in clientAddr,serverAddr;
	/*得到监听套接字listenFd*/
	listenFd=Socket(AF_INET,SOCK_STREAM,0);
	/*设置地址可复用*/
	int opt=0;
	ret=setsockopt(listenFd,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(listenFd,(const struct sockaddr*)&serverAddr,sizeof(serverAddr));
	Listen(listenFd,128);
	
	/*初始化第一个pollfd为监听套接字*/
	client[0].fd=listenFd;
	client[0].events=POLLIN;
	/*将pollfd数组的余下内容的fd属性置为-1*/
	for(i=1;i<OPEN_MAX;++i)
		client[i].fd=-1;
	maxi=0;
	while(1){
		/*nready是有多少套接字有POLLIN请求*/
		nready=poll(client,maxi+1,-1);
		if(nready==-1)
			perr_exit("poll error");
		/*如果listenFd的revents有POLLIN请求,则调用Accept函数得到connectFd*/
		if(client[0].revents&POLLIN){
			clientLen=sizeof(clientAddr);
			connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientLen);
			/*打印客户端地址结构信息*/
			printf("Received from %s at PORT %d\n",inet_ntop(AF_INET,&(clientAddr.sin_addr.s_addr),str,sizeof(str)),ntohs(clientAddr.sin_port));
			/*将创建出来的connectFd加入到pollfd数组中*/
			for(i=1;i<OPEN_MAX;++i)
				if(client[i].fd<0){
					client[i].fd=connectFd;
					break;
				}

			if(i==OPEN_MAX)
				perr_exit("Too many clients,I'm going to die...");
			/*当没有错误时,将对应的events设置为POLLIN*/
			client[i].events=POLLIN;

			if(i>maxi)
				maxi=i;
			if(--nready<=0)
				continue;
		}
		
		/*开始从1遍历pollfd数组*/
		for(i=1;i<=maxi;++i){
			/*到结尾了或者有异常*/
			if((socketFd=client[i].fd)<0)
				continue;
			/*第i个客户端有连接请求,进行处理*/
			if(client[i].revents&POLLIN){
				if((n=read(socketFd,buf,sizeof(buf)))<0){
					/*出错时进一步判断errno*/
					if(errno=ECONNRESET){
						printf("client[%d] aborted connection\n",i);
						close(socketFd);
						client[i].fd=-1;
					}else
						perr_exit("read error");
				}else if(n==0){
					/*read返回0,说明读到了结尾,关闭连接*/
					printf("client[%d] closed connection\n",i);
					close(socketFd);
					client[i].fd=-1;
				}else{
					/*数据处理*/
					for(j=0;j<n;++j)
						buf[j]=toupper(buf[j]);
					Writen(STDOUT_FILENO,buf,n);
					Writen(socketFd,buf,n);
				}
				if(--nready==0)
					break;
			}
		}
	}
	return 0;
}

069-poll总结

优缺点分析:

优点:

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

缺点:

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

070-突破1024文件描述符设置

Linux网络编程学习笔记_第27张图片

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

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

修改配置文件:

sudo vim /etc/security/limits.conf:

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

* hard nofile 100000:设置修改上限;

ulimit -n 17000:更改最大个数;

更改前查看到的:
Linux网络编程学习笔记_第28张图片

071-epoll_create和epoll_ctl

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

#include 
int epoll_create(int size);

Linux网络编程学习笔记_第29张图片

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

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

epoll_ctl()会操作监听红黑树;

#include 
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;
    • void* ptr;
    • uint_32 u32;
    • uint_64 u64;

072-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:出错;

073-epoll函数实现多路IO转接

#include "033-035_wrap.h"

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

int main(int argc,char* argv[]){
    int i=0,n=0,num=0;
    int clientAddrLen=0;
    int listenFd=0,connectFd=0,socketFd=0;
    ssize_t nready,efd,res;
    char buf[MAXLINE],str[INET_ADDRSTRLEN];

    struct sockaddr_in serverAddr,clientAddr;
    /*创建一个临时节点temp和一个数组ep*/
    struct epoll_event temp;
    struct epoll_event ep[OPEN_MAX];

    /*创建监听套接字*/
    listenFd=Socket(AF_INET,SOCK_STREAM,0);
    /*设置地址可复用*/
    int opt=1;
    setsockopt(listenFd,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(listenFd,(const struct sockaddr*)&serverAddr,sizeof(serverAddr));
    /*设置监听上限*/
    Listen(listenFd,128);

    /*创建监听红黑树*/
    efd=epoll_create(OPEN_MAX);
    if(efd==-1)
        perr_exit("epoll_create error");

    /*将listenFd加入监听红黑树中*/
    temp.events=EPOLLIN;
    temp.data.fd=listenFd;
    res=epoll_ctl(efd,EPOLL_CTL_ADD,listenFd,&temp);
    if(res==-1)
        perr_exit("epoll_ctl error");

    while(1){
        /*阻塞监听写事件*/
        nready=epoll_wait(efd,ep,OPEN_MAX,-1);
        if(nready==-1)
            perr_exit("epoll_wait error");

        /*轮询整个数组(红黑树)*/
        for(i=0;i<nready;++i){
            if(!(ep[i].events&EPOLLIN))
                continue;

            /*如果是建立连接请求*/
            if(ep[i].data.fd==listenFd){
                clientAddrLen=sizeof(clientAddr);
                connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientAddrLen);
                printf("Received from %s at PORT %d\n",inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,str,sizeof(str)),ntohs(clientAddr.sin_port));
                printf("connectFd=%d,client[%d]\n",connectFd,++num);

                /*将新创建的连接套接字加入红黑树*/
                temp.events=EPOLLIN;
                temp.data.fd=connectFd;
                res=epoll_ctl(efd,EPOLL_CTL_ADD,connectFd,&temp);
                if(res==-1)
                    perr_exit("epoll_ctl errror");
            }else{
                /*不是建立连接请求,是数据处理请求*/
                socketFd=ep[i].data.fd;
                n=read(socketFd,buf,sizeof(buf));
                
                /*读到0说明客户端关闭*/
                if(n==0){
                    res=epoll_ctl(efd,EPOLL_CTL_DEL,socketFd,NULL);
                    if(res==-1)
                        perr_exit("epoll_ctl error");
                    close(socketFd);
                    printf("client[%d] closed connection\n",socketFd);
                }else if(n<0){
                    /*n<0报错*/
                    perr_exit("read n<0 error");
                    res=epoll_ctl(efd,EPOLL_CTL_DEL,socketFd,NULL);
                    close(socketFd);
                }else{
                    /*数据处理*/
                    for(i=0;i<n;++i)
                        buf[i]=toupper(buf[i]);
                    write(STDOUT_FILENO,buf,n);
                    Writen(socketFd,buf,n);
                }
            }
        }
    }

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

074-中午复习

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

Linux网络编程学习笔记_第30张图片

075-ET和LT模式

Linux网络编程学习笔记_第31张图片

epoll有两种事件模型:

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

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

#define MAXLINE 10
int main(){
    int efd,i;
    int pfd[2];
    pid_t pid;
    char buf[MAXLINE],ch='a';

    pipe(pfd);
    pid=fork();
    /*子进程每次向管道的写端写入10个字节:aaaa\nbbbb\n*/
    if(pid==0){
        close(pfd[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(pfd[1],buf,sizeof(buf));
            sleep(3);
        }
        close(pfd[1]);
    }else if(pid>0){
        struct epoll_event event;
        struct epoll_event resevent[10];
        
        int res,len;
        close(pfd[1]);
        efd=epoll_create(10);
		/*设置边沿触发方式*/
        event.events=EPOLLIN|EPOLLET;
        //event.events=EPOLLIN;
        
        event.data.fd=pfd[0];
        epoll_ctl(efd,EPOLL_CTL_ADD,pfd[0],&event);
		/*父进程死循环阻塞监听,每次读取一半*/
        while(1){
            res=epoll_wait(efd,resevent,10,-1);
            printf("res=%d\n",res);

            if(resevent[0].data.fd==pfd[0]){
                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返回;

076-网络中的ET和LT模式

服务器程序:

#define SERVER_PORT 9527
#define MAXLINE 10

int main(int argc,char* argv[]){
    struct sockaddr_in serverAddr,clientAddr;
    socklen_t clientAddrLen=0;
    int listenFd,connectFd;
    char buf[MAXLINE];
    char str[INET_ADDRSTRLEN];
    int len=0;

    listenFd=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(listenFd,(const struct sockaddr*)&serverAddr,sizeof(serverAddr));
    Listen(listenFd,128);

    struct epoll_event event;
    struct epoll_event resevent[10];
    int res,efd;

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

    printf("Accepting connections...\n");
    clientAddrLen=sizeof(clientAddr);
    connectFd=Accept(listenFd,(struct sockaddr*)&clientAddr,&clientAddrLen);
    printf("Received from %s at port %d\n",inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,str,sizeof(str)),ntohs(clientAddr.sin_port));
    event.data.fd=EPOLLIN|EPOLLET;
    epoll_ctl(efd,EPOLL_CTL_ADD,connectFd,&event);

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

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

客户端程序:

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

    socketFd=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,"127.0.0.1",&serverAddr.sin_addr);

    Connect(socketFd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));

    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(socketFd,buf,sizeof(buf));
        sleep(3);
    }
    close(socketFd);
    return 0;
}

077-epoll的ET非阻塞模式

Linux网络编程学习笔记_第32张图片

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

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

EdgeTriggered是高速工作方式, 只支持none-block模式. 在这种模式下, 当文件描述符从未就绪变为就绪时, 内核通过epoll告诉你. 然后他会假设你知道文件描述符已经就绪, 并且不会再为那个文件描述符发送更多的就绪通知. 如果一直不对这个fd作IO操作(从而导致它再次变为未就绪), 内核不会发送更多的通知(only once);
Linux网络编程学习笔记_第33张图片

078-epoll优缺点总结

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

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

079-补充对比ET和LT

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

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

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

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

080-epoll反应堆模型总述

epoll: ET模式+非阻塞+回调函数void* ptr;
Linux网络编程学习笔记_第34张图片

比如:

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

函数指针封装在结构体里, 让他自动回调(什么叫用C实现面向对象啊,战术后仰);
在这里插入图片描述

原来的步骤:

socket, bind, listen;

epoll_create创建监听红黑树, 返回epfd;

epoll_ctl()向树上添加一个监听fd;

while(1){

​ epoll_wait监听;

​ 对应监听fd有事件产生;

​ 返回监听满足数组, 判断返回数组元素;

​ 若lfd满足, 调用Accept建立连接;

​ 若cfd满足, read(), 小写转大写, write写回去;

}

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

反应堆模型:

socket, bind, listen;

epoll_create创建监听红黑树, 返回epfd;

epoll_ctl()向树上添加一个监听fd;

while(1){

​ epoll_wait监听;

​ 对应监听fd有事件产生;

​ 返回监听满足数组, 判断返回数组元素;

​ 若lfd满足, 调用Accept建立连接;

​ 若cfd满足, read(), 小写转大写;

​ 将cfd从监听红黑树上摘下;

​ EPOLLIN改为EPOLLOUT监听写事件;

​ 回调函数;

​ epoll_cntl(EPOLL_CTL_ADD)重新放到红黑上监听写事件;

​ 等待epoll_wait返回, 说明cfd可写;

​ write写回去;

​ 再将cfd从监听红黑树上摘下;

​ EPOLLOUT改为EPOLLIN监听读事件;

​ epoll_cntl(EPOLL_CTL_ADD)重新放到红黑上监听读事件;

​ epoll_wait监听;

}

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

081-epoll反应堆main逻辑

int main(int argc,char* argv[]){
    /*选择默认端口号或指定端口号*/
    unsigned short port=SERVER_PORT;
    if(argc==2)
        port=atoi(argv[1]);
    /*创建红黑树根节点*/
    g_efd=epoll_create(MAX_EVENTS+1);
    if(g_efd<0)
        perr_exit("epoll_create error");
	/*初始化连接*/
    initlistensocket(g_efd,port);
    /*创建一个系统的epoll_event的数组,与my_events的规模相同*/
    struct epoll_event events[MAX_EVENTS+1];
    printf("Server running port[%d]\n",port);

    int chekpos=0;
    int 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);
                eventdel(g_efd,&g_events[chekpos]);
            }
        }
        /*监听红黑树g_efd*/
        int nfd=epoll_wait(g_efd,events,MAX_EVENTS+1,1000);
        if(nfd<0){
            printf("epoll_wait error\n");
            break;
        }

        for(i=0;i<nfd;++i){
            struct my_events* ev=(struct my_events*)(events[i].data.ptr);
            /*读就绪事件*/
            if((events[i].events&EPOLLIN)&&(ev->events&EPOLLIN))
                ev->call_back(ev->fd,events[i].events,ev->arg);
            /*写就绪事件*/
            if((events[i].events&EPOLLOUT)&&(ev->events&EPOLLOUT))
                ev->call_back(ev->fd,events[i].events,ev->arg);
        }
    }
    return 0;
}

082-epoll反应堆-给lfd和cfd指定回调函数

/*调用者main:initlistensocket(g_efd,port)*/
void initlistensocket(int efd,unsigned short port){
    struct sockaddr_in serverAddr;
    int lfd=0;
    int flg=0;

    lfd=socket(AF_INET,SOCK_STREAM,0);

    /*设置lfd非阻塞*/
    flg=fcntl(lfd,F_GETFL);
    flg=flg|O_NONBLOCK;
    fcntl(lfd,F_SETFL,flg);

    /*设置地址结构*/
    bzero(&serverAddr,sizeof(serverAddr));
    serverAddr.sin_family=AF_INET;
    serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
    serverAddr.sin_port=htons(port);

    bind(lfd,(const struct sockaddr*)&serverAddr,sizeof(serverAddr));
    listen(lfd,128);

    /*void eventset(struct my_events* ev,int fd,void(*call_back)(int,int,void*),void* arg);*/
    /*把g_events数组的最后一个元素设置为lfd,回调函数设置为acceptconn*/
    eventset(&g_events[MAX_EVENTS],lfd,acceptconn,&g_events[MAX_EVENTS]);

    /*void eventadd(int efd,int event,struct my_events* ev)*/
    /*挂上树*/
    eventadd(efd,EPOLLIN,&g_events[MAX_EVENTS]);

    return;
}

给my_event结构体赋值:

/*挨个赋值*/
void eventset(struct my_events* ev,int fd,void (*call_back)(int,int,void*),void* arg){
    ev->fd=fd;
    ev->events=0;
    ev->call_back=call_back;					//设置回调函数
    ev->arg=arg;
    ev->status=0;
    memset(ev->buf,0,sizeof(ev->buf));
    ev->len=0;
    ev->last_active=time(NULL);					//调用eventset的时间

    return;
}

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

void acceptconn(int lfd,int events,void* arg){
    struct sockaddr_in clientAddr;
    socklen_t clientAddrLen=sizeof(clientAddr);
    int connectFd,i;

    if((connectFd=accept(lfd,(struct sockaddr*)&clientAddr,&clientAddrLen))==-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(connectFd,F_GETFL);
        flag=flag|O_NONBLOCK;
        if(fcntl(connectFd,F_SETFL,flag)==-1)
            perr_exit("fcntl error");
        /*给cfd设置一个my_events结构体,回调函数设置为recvdata*/
        eventset(&g_events[i],connectFd,recvdata,&g_events[i]);
        /*将connectFd加入监听红黑树g_efd中*/
        eventadd(g_efd,EPOLLIN,&g_events[i]);

    }while(0);
    return;
}

083-epoll反应堆-initlistensocket小总结

void eventadd(int efd,int event,struct my_events* ev){
    /*这是系统的epoll_event结构体*/
    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;
    /*没在树上:设置op,并把epv挂在树上*/
    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",ev->fd,event);
    else
        printf("event add OK:fd=%d,event=%0X,op=%d",ev->fd,event,op);

    return;
}

eventset函数(设置回调函数):

  • lfd–>acceptconn();
  • cfd–>recvdata();

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

084-epoll反应堆-wait被触发后read和write回调及监听

recvdata:

/*回调函数*/
void recvdata(int fd,int event,void* arg){
    struct my_events* ev=(struct my_events*)arg;
    int len;
    /*从fd中接收数据到ev的buf中*/
    len=recv(fd,ev->buf,sizeof(ev->buf),0);
    eventdel(g_efd,ev);

    if(len>0){
        ev->len=len;
        ev->buf[len]='\0';
        printf("C[%d]:%s",fd,ev->buf);
		/*设置为写回调*/
        eventset(ev,fd,senddata,ev);
        eventadd(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);
        printf("fd=%d,error=%d:%s\n",fd,errno,strerror(errno));
    }
    return;
}

从红黑树上摘除节点

/*从epoll监听红黑树上摘除节点*/
void eventdel(int efd,struct my_events* 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;
}

senddata:

/*回调函数*/
void senddata(int fd,int event,void* arg){
    struct my_events* ev=(struct my_events*)arg;
    int len;
    /*从ev的buf中发送数据给fd*/
    len=send(fd,ev->buf,sizeof(ev->buf),0);
    eventdel(g_efd,ev);

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

085-epoll反应堆-超时时间

...
 /*验证超时,每次测试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]);
        }
    }
...

086-总结

read函数返回值:
Linux网络编程学习笔记_第35张图片

如何突破1024文件描述符限制:
Linux网络编程学习笔记_第36张图片

087-复习

Linux网络编程学习笔记_第37张图片

回调函数由内核完成;

epoll_create函数:
Linux网络编程学习笔记_第38张图片

epoll_ctl函数:
Linux网络编程学习笔记_第39张图片

epoll_wait函数:
Linux网络编程学习笔记_第40张图片

epoll进阶:
Linux网络编程学习笔记_第41张图片

088-补充说明EPOLL和man手册

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

090-ctags使用

ctags ./* -R在项目目录下生成ctags文件;

Ctrl+]跳转到函数定义的位置;

Ctrl+t返回此前的跳转位置;

Ctrl+o屏幕左边列出文件列表, 再按关闭;

F4屏幕右边列出函数列表, 再按关闭;

(还是VSCode比较香)

线程池

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

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

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

Linux网络编程学习笔记_第42张图片
刚开始所有线程阻塞在共享数据区的条件变量上, 有数据到来需要处理时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;
}

线程池释放和销毁

Linux网络编程学习笔记_第43张图片

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客户端和服务器

Linux网络编程学习笔记_第44张图片
UDP通信Server和Client流程:

Linux网络编程学习笔记_第45张图片

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

UDP实现的C/S模型:

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

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

Server:

lfd=socket(AF_INET,SOCK_DGRAM,0);
bind();

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

Client:

connectFd=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服务器本身就是并发的, 因为它无需建立连接

服务器:

int main(void){
	/*服务器地址结构和客户端地址结构*/
	struct sockaddr_in server_addr,client_addr;
	socklen_t client_addr_len;
	int sockfd;
	ssize_t n=0;
	int i=0;
	char buf[BUFSIZ];
	char str[INET_ADDRSTRLEN];
	/*得到套接字,注意采用报式协议*/
	sockfd=socket(AF_INET,SOCK_DGRAM,0);
	/*初始化服务器地址结构*/
	bzero(&server_addr,sizeof(server_addr));
	server_addr.sin_family=AF_INET;
	server_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	server_addr.sin_port=htons(SERVER_PORT);
	/*绑定地址结构到sockfd*/
	bind(sockfd,(const struct sockaddr*)&server_addr,sizeof(server_addr));

	printf("Accepting connection...\n");
	while(1){
		client_addr_len=sizeof(client_addr);
        /*recvfrom相当于accept的作用,后两个是传出参数*/
		n=recvfrom(sockfd,buf,BUFSIZ,0,(struct sockaddr*)&client_addr,&client_addr_len);
		if(n==-1)
			perror("recvfrom error");
		/*打印客户端地址结构*/
		printf("Received from %s at port:%d\n",
               inet_ntop(AF_INET,&client_addr.sin_addr.s_addr,str,sizeof(str)),
               htons(client_addr.sin_port));
		/*数据处理*/
		for(i=0;i<n;++i)
			buf[i]=toupper(buf[i]);
        /*发送回给客户端*/
		n=sendto(sockfd,buf,n,0,(struct sockaddr*)&client_addr,sizeof(client_addr));
		if(n==-1)
			perror("sendto error");
	}
	close(sockfd);
	return 0;
}

客户端:

int main(int argc,char* argv[]){
	struct sockaddr_in serverAddr;
	int sockfd,n;
	char buf[BUFSIZ];
	/*获取套接字*/
	sockfd=socket(AF_INET,SOCK_DGRAM,0);
	/*初始化服务器地址结构*/
	bzero(&serverAddr,sizeof(serverAddr));
	serverAddr.sin_family=AF_INET;
	serverAddr.sin_port=htons(SERVER_PORT);
	inet_pton(AF_INET,"127.0.0.1",&serverAddr.sin_addr);
	/*不必bind*/
	//bind(sockfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
	while(fgets(buf,BUFSIZ,stdin)!=NULL){
        /*发送给服务器*/
		n=sendto(sockfd,buf,strlen(buf),0,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
		if(n==-1)
			perr_exit("sendto error");
		/*从服务器收回数据*/
		n=recvfrom(sockfd,buf,BUFSIZ,0,NULL,0);
		if(n==-1)
			perr_exit("recvfrom error");
		/*写到标准输出*/
		write(STDOUT_FILENO,buf,n);
	}
	close(sockfd);
	return 0;
}

105-本地套接字和网络套接字比较

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

注意地址结构变为sockaddr_un:

Linux网络编程学习笔记_第46张图片

地址结构中的相关信息:
在这里插入图片描述

Linux网络编程学习笔记_第47张图片

106-本地套接字通信

#include;

socket是伪文件, 文件大小始终是0;

服务器端:

#define SERVER_ADDR "server.socket"

int main(int argc,char* argv[]){
	int lfd,cfd,len,size,i;
	struct sockaddr_un serverAddr,clientAddr;
	char buf[BUFSIZ];

	lfd=Socket(AF_UNIX,SOCK_STREAM,0);

	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);

	unlink(SERVER_ADDR);
	Bind(lfd,(struct sockaddr*)&serverAddr,len);
	Listen(lfd,4);
	printf("Accepting...\n");

	while(1){
		len=sizeof(clientAddr);
		cfd=Accept(lfd,(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(cfd,buf,sizeof(buf)))>0){
			for(i=0;i<size;++i)
				buf[i]=toupper(buf[i]);
			write(cfd,buf,size);
		}
		close(cfd);
	}
	close(lfd);
	return 0;
}

客户端:

int main(void){
	int cfd,len;
	struct sockaddr_un clientAddr,serverAddr;
	char buf[BUFSIZ];

	cfd=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(cfd,(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(cfd,(struct sockaddr*)&serverAddr,len);
	while(fgets(buf,sizeof(buf),stdin)!=NULL){
		write(cfd,buf,strlen(buf));
		len=read(cfd,buf,sizeof(buf));
		write(STDOUT_FILENO,buf,len);
	}
	close(cfd);
	return 0;
}

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

服务器端对比:
Linux网络编程学习笔记_第48张图片

客户端对比:
Linux网络编程学习笔记_第49张图片

总结:

Linux网络编程学习笔记_第50张图片

109-复习

Linux网络编程学习笔记_第51张图片

110-libevent简介

Linux网络编程学习笔记_第52张图片

111-libevent库的下载和安装

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

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

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

编译使用库的.c源码时, 要指定库路径:-l event;

库路径:

/usr/local/lib

Linux网络编程学习笔记_第53张图片

112-结合helloworld初识libevent

一些静态函数声明, cb是callback的缩写:
在这里插入图片描述

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

异步:函数"注册"时间和函数真正被执行的时间不同, 函数真正是被内核调用(等待某一条件满足);
Linux网络编程学习笔记_第54张图片

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

event_base_dispatch相当于在while(1)中的epoll, 所以后面三行几乎执行不到了;
Linux网络编程学习笔记_第55张图片

113-libevent封装的框架思想

应用五部曲:

1-创建event_base(事件底座);

struct event_base* base=event_base_new();

2-创建事件event;

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

3-将事件添加到base上;

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

4-循环监听事件满足;

int event_base_dispatch(struct event_base* base);
event_base_dispatch(base);					//调用

5-释放event_base;

event_base_free(base);

114-框架相关的不常用函数

Linux网络编程学习笔记_第56张图片

Linux网络编程学习笔记_第57张图片

int main(int argc,char* argv[]){
	int i=0;
	struct event_base* base=event_base_new();

	const char** buf;
	const char* str;
	/*查看支持哪些多路IO*/
	buf=event_get_supported_methods();
	/*查看当前使用的多路IO*/
	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;
}

115-创建事件对象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);

116-事件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的返回值;

118-使用FIFO的读写编码实现

读进程:

/*回调函数*/
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 fd=0;
	/*创建一个fifo*/
	unlink("myfifo");
	mkfifo("myfifo",0644);
	/*以只读的方式打开fifo,拿到fd*/
	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);
    event_free(ev);
	close(fd);
	return 0;
}

写进程:

void write_cb(evutil_socket_t fd,short what,void* arg){
	char buf[]="hello libevent";
	write(fd,buf,strlen(buf)+1);
	sleep(1);
	return;
}

int main(int argc,char* argv[]){
	int fd=0;
	/*以只写的方式打开fifo,拿到fd*/
	fd=open("myfifo",O_WRONLY|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_WRITE|EV_PERSIST,write_cb,NULL);
	/*将事件对象插到基事件上*/
	event_add(ev,NULL);
	/*由内核阻塞监听去了*/
	event_base_dispatch(base);
	/*释放内存资源*/
	event_base_free(base);
	event_free(ev);
	close(fd);
	return 0;
}

119-未决和非未决

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

非未决: 没有资格被处理;
Linux网络编程学习笔记_第58张图片

120-中午复习

libevent框架:

Linux网络编程学习笔记_第59张图片

创建事件:

Linux网络编程学习笔记_第60张图片

将事件添加到基事件:

Linux网络编程学习笔记_第61张图片

摘除和销毁事件:

Linux网络编程学习笔记_第62张图片

121-bufferevent特性

#include
Linux网络编程学习笔记_第63张图片

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

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

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

122-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)

123-给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)

Linux网络编程学习笔记_第64张图片

124-缓冲区开启和关闭

默认新建的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)

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

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

客户端建立连接:

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函数的返回值;
  • sock:用于通信的文件描述符;
  • addr:客户端的地址结构;
  • len:客户端地址结构的长度;
  • ptr:外部ptr传进来的值;

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

126-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. 释放连接;

127-libevent实现TCP服务器源码分析

/*读回调函数*/
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(SERVER_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;
}

129-客户端流程简析和回顾

  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(SERVER_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;
}

131-复习

Linux网络编程学习笔记_第65张图片

Web大练习

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

html

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


DOCTYPE html>
<html>
	<head>
		<title>I'm headtitle>
	head>

	<body>
		
		<h1>I'm header1h1>
		<h2>I'm header2h2>
		<h3>I'm header3h3>
		
		<font color="red" size="7">I'm bodyfont>
		<br/>

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

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

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

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

		
		<p>I'm pp>
		<p>I'm pp>

		
		<hr size=70 color="green"/>

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

	body>

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>404NotFoundtitle>
head>

<body>
    <h1>404 not foundh1>
body>

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

html>

列表&图片和超链接:


DOCTYPE html>
<html>
	<head>
		<title>A Demotitle>
	head>

	<body>
		
		<p id="top">

		<ul type="circle">
			<li>option1li>
			<li>option2li>
			<li>option3li>
			<li>option4li>
		ul>

		<ol type="A">
			<li>option1li>
			<li>option2li>
			<li>option3li>
			<li>option4li>
		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>

136-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

Linux网络编程学习笔记_第66张图片

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

  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

响应正文

Linux网络编程学习笔记_第67张图片

137-服务器框架复习和getline函数

Server框架:

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(SERVER_PORT);

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

	/*给lfd绑定地址结构*/
	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");

	/*将lfd挂到监听红黑树*/
	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){
	int cfd=0;
	int flag=0;
	int ret=0;

	struct sockaddr_in clientAddr;
	socklen_t clientAddrLen=sizeof(clientAddr);

	cfd=accept(lfd,(struct sockarr*)&clientAddr,&clientAddrLen);
	if(cfd==-1)
		perr_exit("accept error");
	
	char client_ip[64]={0};
	printf("New Client IP=%s,Port=%d,cfd=%d\n",
			inet_ntop(AF_INET,&clientAddr.sin_addr.s_addr,client_ip,sizeof(client_ip)),
			ntohs(clientAddr.sin_port),
			cfd);
	
	/*设置非阻塞*/
	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;
	ret=epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);
	if(ret==-1)
		perr_exit("epoll_ctl add cfd error");

	return;
}


void do_read(int cfd,int epfd){
	/*读取一行http协议,拆分,获取get文件名和协议号*/

	return;
}

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

	/*创建监听红黑树树根节点*/
	int epfd=epoll_create(MAXSIZE);
	if(epfd==-1){
		perror("epoll_create error");
		exit(1);
	}

	/*创建lfd,并添加至监听树*/
	int lfd=init_listen_fd(port,epfd);

	while(1){
		ret=epoll_wait(epfd,all_events,MAXSIZE,-1);
		if(ret==-1)
			perr_exit("epoll_wait error");
		
		for(i=0;i<ret;++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);
		}
	}

	return NULL;
}

int main(int argc,char* argv[]){
	/*从cmd参数中获取端口和server提供的目录*/
	if(argc<3)
		printf("Please input in this format:./server port path\n");
	
	/*用户输入的端口*/
	int port=atoi(argv[1]);

	/*切换进程的工作目录*/
	int ret=chdir(argv[2]);
	if(ret!=0){
		perror("chdir error");
		exit(1);
	}

	/*启动epoll监听*/
	epoll_run(port);

	return 0;
}

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;
}

138-复习

Linux网络编程学习笔记_第68张图片

139-单文件通信流程分析

  • getline()获取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
  • 再写文件数据;

140-处理出错返回

错误处理函数:

void disconnect(int cfd,int epfd){
	int ret=epoll_ctl(epfd,EPOLL_CTL_DEL,cfd,NULL);
	if(ret!=0)
		perr_exit("epoll_ctl del error");
	close(cfd);
	return;
}

读数据的思路:

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;
}

141-正则表达式获取文件名

sscanf()函数:

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

Linux网络编程学习笔记_第69张图片

正则表达式没有语言隔阂, 所有语言通用;

142-判断文件是否存在

下面贴的源码跟刚开始的几集不一致(下面的是成熟的, 视频里的是演示);

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;
}

143-写出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;
}

144-写数据给浏览器

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;
}

145-文件类型区分

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";
}

147-错误页面展示

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

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,"%d %s\n",status,title);
	sprintf(buf+strlen(buf),"

%d %s

\n"
,status,title); sprintf(buf+strlen(buf),"%s\n",text); sprintf(buf+strlen(buf),"
\n\n\n"
); send(cfd,buf,strlen(buf),0); return; }

148-关于浏览器请求ico文件

Linux网络编程学习笔记_第70张图片

149-浏览器请求目录

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

	char buf[4096]={0};
	sprintf(buf,"目录名:%s",dirname);
	sprintf(buf+strlen(buf),"

当前目录:%s

",dirname);char enstr[1024]={0};char path[1024]={0};structdirent** 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);structstat st;stat(path,&st);/*编码生成Unicode编码:诸如%E5%A7...等*/encode_str(enstr,sizeof(enstr),name);if(S_ISREG(st.st_mode)){sprintf(buf+strlen(buf),"%ld", enstr,name,(long)st.st_size);}elseif(S_ISDIR(st.st_mode)){sprintf(buf+strlen(buf),"", enstr,name,(long)st.st_size);} ret=send(cfd,buf,strlen(buf),0);if(ret==-1){if(errno==EAGAIN){perror("send error:");continue;}elseif(errno==EINTR){perror("send error:");continue;}else{perror("send error:");exit(1);}}memset(buf,0,sizeof(buf));}sprintf(buf+strlen(buf),"
%s
%s%ld
"
); send(cfd,buf,strlen(buf),0); printf("dir message send OK\n"); return; }

150-判断文件类型

/*判断文件类型*/
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";
}

151-汉字字符编码和解码

每一个汉字在浏览器前端中会被转码成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;
}

153-telnet调试

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

telnet 127.0.0.1 9527

GET /hello.c http/1.1

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

你可能感兴趣的:(Linux,c语言,linux,socket,网络通信,web)