C++ 之动手写 Reactor 服务器模型(一):网络编程基础复习总结

基础

IP 地址可以在网络环境中唯一标识一台主机。

端口号可以在主机中唯一标识一个进程。

所以在网络环境中唯一标识一个进程可以使用 IP 地址与端口号 Port 。

字节序

TCP/IP协议规定,网络数据流应采用大端字节序

大端:低地址存高位,高地址存低位;

小端:低地址存低位,高地址存高位(x86采用小端存储)。

网络字节序,就是在网络中进行传输的字节序列,采用的是大端法。

主机字节序,就是本地计算机中存储数据采用的字节序列,采用的是小端法。

相关 API 函数

cpp 复制代码
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);//h = host n = network l = long s = short
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);


#include <arpa/inet.h>
//点分十进制字符串转换为网络字节序
int inet_pton(int af, const char *src, void *dst);
//网络字节序转换为点分十进制字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);


#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h> //包含了<netinet/in.h>,后者包含了<sys/socket.h>

typedef uint32_t in_addr_t;
struct in_addr
{
	in_addr_t s_addr;
};

//将cp所指C字符串转换成一个32位的网络字节序二进制值,并通过inp指针来存储,成功返回1,失败返回0
int inet_aton(const char *cp, struct in_addr *inp);

//将一个32位的网络字节序二进制IPv4地址转换成相应的点分十进制数串,由该函数的返回值所指向的
//字符串驻留在静态内存中,这意味着该函数是不可重入的
char *inet_ntoa(struct in_addr in);

//inet_addr函数转换网络主机地址(如192.168.1.10)为网络字节序二进制值,如果参数char
//*cp无效,函数返回-1(INADDR_NONE),这个函数在处理地址为255.255.255.255时也返回-
//1,255.255.255.255是一个有效的地址,不过inet_addr无法处理;
//返回值为32位的网络字节序二进制
in_addr_t inet_addr(const char *cp);//ok
in_addr_t inet_network(const char *cp);
struct in_addr inet_makeaddr(in_addr_t net, in_addr_t host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);


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

常用结构体

man 7 ip 可以查看相应的结构体,也可以使用命令sudo grep -rn "struct sockaddr_in {" /usr 进行搜索。

cpp 复制代码
struct sockaddr
{
	sa_family_t sa_family; /* address family, AF_xxx */
	char sa_data[14]; /* 14 bytes of protocol address */
};

struct sockaddr_in
{
	sa_family_t sin_family; /* address family: AF_INET */
	in_port_t sin_port; /* port in network byte order */
	struct in_addr sin_addr; /* internet address */
};

/* Internet address. */
struct in_addr
{
	uint32_t s_addr; /* address in network byte order */
};

IPv4IPv6 的地址格式定义在 netinet/in.h 中,IPv4 地址用 sockaddr_in 结构体表示,IPv6 地址使用 sockaddr_in6 结构体表示。UNIX Domain Socket 的地址格式定义在 sys/un.h 中,使用 sockaddr_un 结构体表示。所有的地址类型分别定义为常数 AF_INETAF_INET6AF_UNIX

cpp 复制代码
struct sockaddr_in addr;
addr.sin_family = AF_INET/AF_INET6/AF_UNIX;
addr.sin_port = htons/ntohs;
addr.sin_addr.s_addr = htonl/ntohl/inet_pton/inet_ntop

网络编程相关函数

socket 函数

创建套接字:

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

//创建套接字函数
int socket(int domain, int type, int protocol);

domain:AF_INET/AF_INET6/AF_UNIX
type:SOCK_STREAM/SOCK_DGRAM 前者默认是TCP,后者默认是UDP
protocol:传0表示使用默认协议
//函数返回值
成功,返回指向新创建的socket的文件描述符,失败返回-1,设置errno

bind 函数

因为服务器程序所监听的网络地址与端口号是固定不变的,所以需要使用bind函数进行绑定。bind 函数将 sockfd 与 addr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 addr 所描述的地址和端口号。

绑定 IP 与端口号:

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

//绑定服务的端口号与IP地址
sockfd:上面socket创建的套接字
addr:所要绑定的ip地址与端口号
addrlen:前面addr结构体的长度
//函数返回值
成功,返回指向新创建的socket的文件描述符,失败返回-1,设置errno

listen 函数

用来指定监听上限数值(允许同时多少个客户端与服务器建立连接),指定最大同时发起连接数

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

sockfd:socket创建的文件描述符
backlog:排队建立3次握手队列和刚刚建立3次握手队列的连接数和。

可以使用命令进行最大发起连接数限定值的查看:

shell 复制代码
cat /proc/sys/net/ipv4/tcp_max_syn_backlog 1

accept 函数

接收连接请求的函数,阻塞等待客户端发起连接

如果客户端还没有来得及连接,此时 accept 函数会处于阻塞状态。

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

sockfd:socket创建的文件描述符
addr:传出参数,返回连接客户端地址信息,包含IP地址与端口号
addrlen:传入传出参数(值-结果),传入sizeof(addr)大小,函数返回时返回真正接收到地址结构
体的大小。
//函数返回值
成功返回一个新的socket文件描述符,用于和客户端通信,失败返回-1,设置errno

connect 函数

客户端调用该函数,连接到服务器上。

发起连接:

cpp 复制代码
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

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

sockfd:是客户端自己使用socket得到的文件描述符。
addr:传入参数,指定服务器端地址信息,包含IP地址与端口号
addrlen:传入参数,传入sizeof(addr)大小
返回值:成功返回0,失败返回-1,设置errno

客户端需要调用 connect 连接服务器,connect 和 bind 的采纳数形式一致,区别在于 bind 的参数是自己的地址,而 connect 的参数是对方的地址。

close 函数

关闭套接字创建的文件描述符。

cpp 复制代码
#include <unistd.h>

int close(int fd);

客户端其实也是需要 bind 端口号与 IP 地址,如果没有显示绑定的话,操作系统会自动分配一个 IP 地址与端口号。但是服务器是不能不使用 bind 函数,让操作系统随机分配 IP 地址与端口号,这样的话客户端就不知道服务器的 IP 地址与端口号,就不知道怎么连接到服务器上了,也不知道连接到那个服务器上。

本地随机的有效数字类型的 IP,INADDR_ANY

INADDR_ANY解析:转换过来就是 0.0.0.0,泛指本机的意思,表示本机的所有IP,因为有些电脑不止一块网卡,如果某个应用程序只监听某个端口,那么其他端口过来的数据就接收不了。

网络编程代码

逻辑示例图

端口复用

让同一个端口可以进行重复使用,不至于等待 2MSL的时间:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

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

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

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

int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));

服务器端源码

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666

int main()
{
	int sfd, cfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	char buf[BUFSIZ], clie_IP[BUFSIZ];
	int nByte, idx;
	sfd = socket(AF_INET, SOCK_STREAM, 0);
	int opt = 1;
	setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));//允许端口复用
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	bind(sfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	listen(sfd, 128);
	clie_addr_len = sizeof(clie_addr);
	cfd = accept(sfd, (struct sockaddr*)&clie_addr, &clie_addr_len);
	printf("client IP: %s, port: %d\n",
		inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, clie_IP,
			sizeof(clie_IP)),
		ntohs(clie_addr.sin_port));
	while (1)
	{
		nByte = read(cfd, buf, sizeof(buf));
		for (idx = 0; idx < nByte; ++idx)
		{
			buf[idx] = toupper(buf[idx]);
		}
		write(cfd, buf, nByte);
	}
	close(sfd);
	close(cfd);
	return 0;
}

客户端源码

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 6666

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

read 返回值

1、大于0,实际读到的字节数,并且buf=1024

如果read读到的数据的长度等于buf,返回的就是1024

如果read读到的数据长度小于buf,那就是小于1024的数值。

2、返回值为0,数据读完(读到文件、管道、socket末尾 ---对端关闭)

3、返回值为-1,表明出现异常

errno == EINTR 说明被信号中断 所以需要重启或者退出;

errno == EAGAIN(EWOULDBLOCK)非阻塞方式读,并且没有数据;

其他值的出现表示出现错误使用 perror 打印然后 exit 退出

readn/writen 函数的封装

因为以太网帧一次只能传送 1500 字节的数据,所以使用 read 函数一次最多只能读到 1500 字节,就返回退出。

cpp 复制代码
ssize_t readn(int fd, void* vptr, size_t n)
{
	size_t nleft;//usigned int剩余未读取的字节数
	size_t nread;//int 实际读到的字节数
	char* ptr;
	nleft = n;//n未读取字节数
	ptr = vptr;
	while (nleft > 0)
	{
		if ((nread = read(fd, ptr, nleft)) < 0)
		{
			if (errno == EINTR)
			{
				nread = 0;
			}
			else
			{
				return -1;
			}
		}
		else if (0 == nread)
		{
			break;
		}
		nleft -= nread;
		ptr += nread;
	}
	return (n - nleft);
}
cpp 复制代码
ssize_t writen(int fd, const void* vptr, size_t n)
{
	size_t nleft;
	size_t nwritten;
	const char* ptr;
	nleft = n;
	ptr = vptr;
	while (nleft > 0)
	{
		if ((nwritten = write(fd, ptr, nleft)) <= 0)
		{
			if (nwritten < 0 && errno == EINTR)
			{
				nwritten = 0;
			}
			else
			{
				return -1;
			}
		}
		nleft -= nwritten;
		ptr += nwritten;
	}
	return n;
}

IO多路复用

概念与原理图

多进程与多线程并发服务器,不经常使用这种作为大型服务器开发的原因是,所有的监听与访问请求都由服务器操作

可以使用多路IO转接服务器(也叫多任务IO服务器),思想:不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件

select

接口解析

cpp 复制代码
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds,
	struct timeval* timeout);
	
nfds:监控的文件描述符集里最大文件描述符 + 1,因为此参数会告诉内核检测前多少个文件描述符的状态。
readfs / writes / exceptfds : 监控有读数据 / 写数据 / 异常发生到达文件描述符集合,三个都是传入传出参数。
timeout : 定时阻塞监控时间,3种情况:
1、NULL,永远等下去
2、设置timeval,等待固定时间
3、设置timeval里时间均为0,检查描述字后立即返回,轮询。
fd_set:本质是个位图。
struct timeval
{
	long tv_sec; /* seconds */
	long tv_usec; /* microseconds */
};
返回值:
成功:所监听的所有的监听集合中,满足条件的总数。
失败:返回 -1.
cpp 复制代码
void FD_ZERO(fd_set *set);//将set清空为0
void FD_SET(int fd, fd_set *set);//将fd设置到set集合中
void FD_CLR(int fd, fd_set *set);//将fd从set中清除出去
int FD_ISSET(int fd, fd_set *set);//判断fd是否在集合中

优缺点

1、文件描述符上限(1024),同时监听的文件描述符1024个,历史原因,不好修改,除非重新编译Linux内核。

2、当监听的文件描述符个数比较稀疏的时候(比如3, 600, 1023),循环判断比较麻烦,所以需要自定义数据结构:数组。

3、监听集合与满足监听条件的集合是同一个,需要将原有集合保存。

代码实现(C语言)

server 端

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>

#define SERV_PORT 8888

int main()
{
	int listenfd, connfd, sockfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	int ret, maxfd, maxi, i, j, nready, nByte;
	fd_set rset, allset;
	int client[FD_SETSIZE];
	char buf[BUFSIZ], str[BUFSIZ];
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == listenfd)
	{
		perror("socket error");
		exit(-1);
	}
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr*)&serv_addr,
		sizeof(serv_addr)); 
	if (-1 == ret)
	{
		perror("bind error");
		exit(-1);
	}
	ret = listen(listenfd, 128);
	if (-1 == ret)
	{
		perror("listen error");
		exit(-1);
	}
	maxfd = listenfd;
	maxi = -1;
	for (i = 0; i < FD_SETSIZE; ++i)
	{
		client[i] = -1;
	}
	FD_ZERO(&allset);
	FD_SET(listenfd, &allset);
	while (1)
	{
		rset = allset;
		nready = select(maxfd + 1, &rset, NULL, NULL, NULL);
		if (nready < 0)
		{
			perror("select error");
			exit(-1);
		}
		if (FD_ISSET(listenfd, &rset))
		{
			clie_addr_len = sizeof(clie_addr);
			connfd = accept(listenfd, (struct sockaddr*)&clie_addr,
				&clie_addr_len);
			if (-1 == connfd)
			{
				perror("accept error");
				exit(-1);
			}
			printf("receive from %s from port %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr, str,
					sizeof(str)),
				ntohs(clie_addr.sin_port));
			for (i = 0; i < FD_SETSIZE; ++i)
			{
				if (client[i] < 0)
				{
					client[i] = connfd;
					break;
				}
			}
			if (i == FD_SETSIZE)
			{
				fputs("too many clients\n", stderr);
				exit(1);
			}
			FD_SET(connfd, &allset);
			if (connfd > maxfd)
			{
				maxfd = connfd;
			}
			if (i > maxi)
			{
				maxi = i;
			}
			if (--nready == 0)
			{
				continue;
			}
		}
		for (i = 0; i <= maxi; ++i)
		{
			if ((sockfd = client[i]) < 0)
			{
				continue;
			}
			if (FD_ISSET(sockfd, &rset))
			{
				if ((nByte = read(sockfd, buf, sizeof(buf))) == 0)
				{
					close(sockfd);
					FD_CLR(sockfd, &allset);
					client[i] = -1;
				}
				else if (nByte > 0)
				{
					for (j = 0; j < nByte; ++j)
					{
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, nByte);
					write(STDOUT_FILENO, buf, nByte);
				}
				if (--nready == 0)
				{
					break;
				}
			}
		}
	}
	close(listenfd); 
	close(connfd);
	return 0;
}

client 端

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == cfd)
	{
		perror("socket error");
		exit(-1);
	}
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

poll

接口解析

cpp 复制代码
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

struct pollfd
{
	int fd; /* file descriptor */
	short events; /* requested events */
	short revents; /* returned events */
};

fds:文件描述符数组。
events:POLLIN/POLLOUT/POLLERR
nfds:监控数组中有多少文件描述符需要被监控。
timeout 毫秒级等待:
	-1:阻塞等,#define INFTIM -1 Linux中没有定义此宏
	0:立即返回,不阻塞进程
	>0:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值。
函数返回值:满足监听条件的文件描述符的数目。

优缺点

优点:

1、突破文件描述符1024的上限

2、监听与返回的集合分离

3、搜索范围变小(已经知道是哪几个数组)

缺点:

1、监听1000个文件描述符,但是只有3个满足条件,这样也需要全部遍历,效率依旧低。

2、cat /proc/sys/fs/file-max 查看一个进程可以打开的文件描述符的上限数。

3、sudo vi /etc/security/limits.conf。在文件尾部写入以下配置,soft 软限制,hard 硬限制。

cpp 复制代码
soft nofile 65536
hard nofile 100000

代码实现(C语言)

server 端

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>

#define SERV_PORT 8888
#define OPEN_MAX 1024

int main()
{
	int i, j, n, maxi;
	int nready, ret;
	int listenfd, connfd, sockfd;
	char buf[BUFSIZ], str[INET_ADDRSTRLEN];
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	struct pollfd client[OPEN_MAX];
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == listenfd)
	{
		perror("socket error");
		exit(-1);
	}
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);//本地字节序port与ip都要转换为网络字节序
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);//因为要在网络上传输
	ret = bind(listenfd, (struct sockaddr*)&serv_addr,
		sizeof(serv_addr));
	if (-1 == ret)
	{
		perror("bind error");
		exit(-1);
	}
	ret = listen(listenfd, 128);
	if (-1 == ret)
	{
		perror("listen error");
		exit(-1);
	}
	client[0].fd = listenfd;
	client[0].events = POLLIN;
	for (i = 1; i < OPEN_MAX; ++i)
	{
		client[i].fd = -1;//将数组初始化为-1
	}
	maxi = 0;
	while (1)
	{
		nready = poll(client, maxi + 1, -1);
		if (nready < 0)
		{
			perror("poll error");
			exit(-1);
		}
		if (client[0].revents & POLLIN)
		{
			clie_addr_len = sizeof(clie_addr);
			connfd = accept(listenfd, (struct sockaddr*)&clie_addr,
				&clie_addr_len);//立即连接,此时不会阻塞等
			if (-1 == connfd)
			{
				perror("accept error");
				exit(-1);
			}
			printf("received from %s at port %d\n",
				inet_ntop(AF_INET, &clie_addr.sin_addr.s_addr, str,
					sizeof(str)),
				ntohs(clie_addr.sin_port));
			for (i = 1; i < OPEN_MAX; ++i)
			{
				if (client[i].fd < 0)//因为初始化为-1,所以在此作为判断条件
				{
					client[i].fd = connfd;
					break;//直接跳出,免得继续判断,浪费时间
				}
			}
			if (i == OPEN_MAX)//select监听的文件描述符有上限,最大只能监听1024个
			{
				fputs("too many clients\n", stderr);
				exit(1);
			}
			client[i].events = POLLIN;
			if (i > maxi)
			{
				maxi = i;//因为文件描述符有新增,导致自定义数组有变化,所以需要重新
				修改maxi的值
			}
			if (--nready == 0)//意思不明确
			{
				continue;
			}
		}
		for (i = 1; i <= maxi; ++i)
		{
			if ((sockfd = client[i].fd) < 0)
			{
				continue;
			}
			if (client[i].revents & POLLIN)
			{
				if ((n = read(sockfd, buf, sizeof(buf))) < 0)
				{
					if (errno == ECONNRESET)
					{
						printf("client[%d] abort connect\n", i);
						close(sockfd);
						client[i].fd = -1;
					}
					else
					{
						perror("read n = 0 error");
					}
				}
				else if (n > 0)
				{
					for (j = 0; j < n; ++j)
					{
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, n);
					write(STDOUT_FILENO, buf, n);
				}
				else
				{
					close(sockfd);
					client[i].fd = -1;
				}
				if (--nready == 0)
				{
					break;
				}
			}
		}
	}
	close(listenfd);
	close(connfd);
	return 0;
}

client 端

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == cfd)
	{
		perror("socket error");
		exit(-1);
	}
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}

epoll

接口解析

是Linux下IO多路复用接口select/poll的增强版本,能显著提高程序在大量并发连接中只有少量活跃 的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不是迫使开发者每次等待事件之前都必须重新准备要侦听的文件描述符集合,另一个原因是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历哪些被内核IO事件唤醒而加入Ready队列的描述符集合就行了

cpp 复制代码
#include <sys/epoll.h>

int epoll_create(int size);

size:参数size用来告知内核监听的文件描述符的个数,与内存大小有关。

//控制某个epoll监控的文件描述符上的事件:注册、修改、删除
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epfd:epoll_create函数返回的值
op:EPOLL_CTL_ADD / EPOLL_CTL_MOD / EPOLL_CTL_DEL
fd:将哪个文件描述符以op的方式加在以epfd建立的树上
event:告诉内核需要监听的事情。

struct epoll_event
{
	uint32_t events;
	epoll_data_t data;
};

typedef union epoll_data
{
	void* ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
} epoll_data_t;

//等待所监控文件描述符上有事件的产生
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, 
				int timeout);
				
events:用来存内核得到事件的集合(这里是个传出参数)
maxevents:告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create时的size
timeout:是超时时间
	-1:阻塞
	=0:立即返回,非阻塞
	>0:指定毫秒
返回值:成功返回有多少文件描述符就绪,时间到时返回0,出错返回 - 1

优缺点

优点:

1、文件描述符数目没有上限:通过 epoll_ctl() 来注册一个文件描述符,内核中使用红黑树的数据结构来

管理所有需要监控的文件描述符。

2、基于事件就绪通知方式:一旦被监听的某个文件描述符就绪,内核会采用类似于 callback 的回调机制,迅速激活这个文件描述符,这样随着文件描述符数量的增加,也不会影响判定就绪的性能。

3、维护就绪队列:当文件描述符就绪,就会被放到内核中的一个就绪队列中,这样调用 epoll_wait 获取就绪文件描述符的时候,只要取队列中的元素即可,操作的时间复杂度恒为 O(1) 。

图解

类型区别

水平触发(level-triggered)

只要文件描述符关联的读内核缓冲区 非空,有数据可以读取,就一直发出可读信号进行通知;当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知LT模式支持阻塞和非阻塞两种方式。

epoll默认的模式是LT

边缘触发(edge-triggered)

当文件描述符关联的读内核缓冲区由空转化为非空 的时候,则发出可读信号进行通知;当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知。

两种类型区别

两者的区别在哪里呢?

水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次。

LT(level triggered) 是缺省的工作方式,并且同时支持 block 和 no-block socket.

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

当设置了边缘触发以后,以可读事件为例,对"有数据到来"这事件为触发。

select/poll/epoll 除了应用于 fd 外,像管道、文件也是可以的。

代码实现(C语言)

server端

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>

#define SERV_PORT 8888
#define OPEN_MAX 5000

int main()
{
	int listenfd, connfd, sockfd, epfd;
	struct sockaddr_in serv_addr, clie_addr;
	socklen_t clie_addr_len;
	int ret, i, j, nready, nByte;
	char buf[BUFSIZ], str[BUFSIZ];
	struct epoll_event evt, ep[OPEN_MAX];
	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == listenfd)
	{
		perror("socket error");
		exit(-1);
	}
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	bzero(&serv_addr, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	ret = bind(listenfd, (struct sockaddr*)&serv_addr,
		sizeof(serv_addr));
	if (-1 == ret)
	{
		perror("bind error");
		exit(-1);
	}
	ret = listen(listenfd, 128);
	if (-1 == ret)
	{
		perror("listen error");
		exit(-1);
	}
	epfd = epoll_create(OPEN_MAX);
	if (-1 == epfd)
	{
		perror("epoll_create error");
		exit(-1);
	}
	evt.events = EPOLLIN;
	evt.data.fd = listenfd;
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &evt);
	if (-1 == ret)
	{
		perror("epoll_ctl error");
		exit(-1);
	}
	while (1)
	{
		nready = epoll_wait(epfd, ep, OPEN_MAX, -1);
		if (nready < 0)
		{
			perror("select error");
			exit(-1);
		}
		for (i = 0; i < nready; ++i)
		{
			if (!(ep[i].events & EPOLLIN))
			{
				continue;
			}
			if (ep[i].data.fd == listenfd)//如果是连接事件
			{
				clie_addr_len = sizeof(clie_addr);
				connfd = accept(listenfd, (struct sockaddr*)&clie_addr,
					&clie_addr_len);
				if (-1 == connfd)
				{
					perror("accept error");
					exit(-1);
				}
				printf("receive from %s from port %d\n",
					inet_ntop(AF_INET, &clie_addr.sin_addr, str,
						sizeof(str)),
					ntohs(clie_addr.sin_port));
				evt.events = EPOLLIN;
				evt.data.fd = connfd;
				epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &evt);
			}
			else //不是连接建立事件,而是读写事件(信息传递事件)
			{
				sockfd = ep[i].data.fd;
				nByte = read(sockfd, buf, sizeof(buf));
				if (nByte == 0)
				{
					ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
					if (-1 == ret)
					{
						perror("epoll_ctl error");
					}
					close(sockfd);
					printf("client[%d] closed connection\n", sockfd);
				}
				else if (nByte < 0)
				{
					perror("epoll_ctl error");
					ret = epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);
					if (-1 == ret)
					{
						perror("epoll_ctl error");
					}
					close(sockfd);
				}
				else
				{
					for (j = 0; j < nByte; ++j)
					{
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, nByte);
					write(STDOUT_FILENO, buf, nByte);
				}
			}
		}
	}
	close(listenfd);
	close(connfd);
	return 0;
}

client端

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define SERV_IP "127.0.0.1"
#define SERV_PORT 8888

int main()
{
	int cfd;
	struct sockaddr_in serv_addr;
	char buf[BUFSIZ];
	int nByte;
	cfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == cfd)
	{
		perror("socket error");
		exit(-1);
	}
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(SERV_PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	/* inet_pton(cfd, SERV_IP, &serv_addr.sin_addr.s_addr); */
	connect(cfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
	while (1)
	{
		fgets(buf, sizeof(buf), stdin);//hello world ----> hello world\n\0
		write(cfd, buf, strlen(buf));
		nByte = read(cfd, buf, sizeof(buf));
		write(STDOUT_FILENO, buf, nByte);
	}
	close(cfd);
	return 0;
}
相关推荐
vvw&4 分钟前
如何在 Ubuntu 22.04 上安装 Graylog 开源日志管理平台
linux·运维·服务器·ubuntu·开源·github·graylog
大哥_ZH8 分钟前
Linux umami在国产麒麟系统安装网站统计工具(只能上国内网站的系统)
linux·服务器
CodeClimb12 分钟前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
奶香臭豆腐44 分钟前
C++ —— 模板类具体化
开发语言·c++·学习
不想当程序猿_1 小时前
【蓝桥杯每日一题】分糖果——DFS
c++·算法·蓝桥杯·深度优先
不爱学英文的码字机器1 小时前
[Linux] Shell 命令及运行原理
linux·运维·服务器
cdut_suye1 小时前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
qq_433618441 小时前
shell 编程(三)
linux·运维·服务器
网安墨雨1 小时前
常用网络协议
网络·网络协议
波音彬要多做1 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法