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

news2024/12/28 19:02:01

基础

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

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

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

字节序

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

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

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

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

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

相关 API 函数

#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 进行搜索。

在这里插入图片描述

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

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 函数

创建套接字:

#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 与端口号:

#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 函数

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

#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>

int listen(int sockfd, int backlog);

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

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

cat /proc/sys/net/ipv4/tcp_max_syn_backlog 1

accept 函数

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

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

#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 函数

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

发起连接:

#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 函数

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

#include <unistd.h>

int close(int fd);

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

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

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

网络编程代码

逻辑示例图

在这里插入图片描述

端口复用

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

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

服务器端源码

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

客户端源码

#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 字节,就返回退出。

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

接口解析

#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种情况:
1NULL,永远等下去
2、设置timeval,等待固定时间
3、设置timeval里时间均为0,检查描述字后立即返回,轮询。
fd_set:本质是个位图。
struct timeval
{
	long tv_sec; /* seconds */
	long tv_usec; /* microseconds */
};
返回值:
成功:所监听的所有的监听集合中,满足条件的总数。
失败:返回 -1.
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 端

#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 端

#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

接口解析

#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 硬限制。

soft nofile 65536
hard nofile 100000

代码实现(C语言)

server 端

#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 端

#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队列的描述符集合就行了

#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端

#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端

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2036192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[陇剑杯 2021]wifi WP

9.1小王往upload-labs上传木马后进行了cat /flag&#xff0c;flag内容为_____________。&#xff08;压缩包里有解压密码的提示&#xff0c;需要额外添加花括号&#xff09; 附件信息&#xff1a; 拿到附件 先看服务器.pcapng 可以发现只有发出去的包&#xff0c;且为哥斯…

Golang实现简单的HTTP服务,响应RESTful请求判断形状大小

题目要求&#xff1a; 题目 1.shape 接口有面积Area() float64和 周长Perimeter()fioat64 两个法。为Circle Rectangle实现shape 接口。 2.实现isGreater(shape1&#xff0c;shape2 shape)boo1 函数&#xff0c;用于比较两个形状的大小,并使用单元测试验证 3.实现http.Handle…

反常识心理学——受助者恶意 / 如何防备受助者恶意的发生

原创 大渔 大渔大师课 贯穿电影《消失的她&#xff08;2013年上映&#xff09;》中全片的两个反常识心理学效应&#xff1a;曼德拉效应、受助者恶意。 「被篡改的记忆—曼德拉效应 」 何非与妻子去国外旅行&#xff0c;妻子却离奇失踪&#xff0c;正在何非焦急寻找之时&…

根据字典值回显,有颜色的

背景 本项目以若依前端vue2版本为例&#xff0c;项目中有根据字典值回显文本的函数selectDictLabel&#xff0c;但是有时候我们需要带颜色的回显&#xff0c;大概这样的 用法 <template v-slotscope><dict-label :options"dangerLevelOptions" :value&qu…

2024年办公协作新趋势:8种值得瞩目的工作方式

过去两年中&#xff0c;疫情的爆发推动了远程办公业务的发展&#xff0c;并且随着疫情的常态化和企业数字化转型的加速&#xff0c;中国企业对协作办公软件的需求显著增加。数据显示&#xff0c;2021年中国协作办公市场规模已达264.2亿元&#xff0c;预计到2023年将增长至330.1…

Asymmetric Loss For Multi-Label Classification

从b站视频区看到的一篇论文&#xff0c;来自阿里。据他所说&#xff0c;他的多标签分类数据长尾最大到100:1&#xff0c;再做增广也没用&#xff0c;用了这篇论文的loss直接起飞。 链接在此 首先&#xff0c;常规的loss 既然是多标签分类&#xff0c;那么最基础的方法肯定是…

《探索 Unity 开发:创新与挑战》

《探索 Unity 开发&#xff1a;创新与挑战》 在当今的游戏开发和虚拟现实领域&#xff0c;Unity 已经成为了一款备受青睐的引擎。它的强大功能和灵活性&#xff0c;为开发者们提供了无限的创作可能。在这篇博客中&#xff0c;我们将深入探讨 Unity 开发的各个方面&#xff0c;包…

HarmonyOS应用二之代办事项案例

目录&#xff1a; 1、代码分析2、ArkTS的基本组成3、重点扩展 1、代码分析 1.1代码&#xff1a; 在鸿蒙&#xff08;‌HarmonyOS&#xff09;‌的ArkTS框架中&#xff0c;‌aboutToAppear() 是一个自定义组件的生命周期函数&#xff0c;‌它在组件即将显示时被系统自动调用1。…

生信入门:序列比对之ncbi_blast在线使用

1.背景 blast作为一种序列相似性比对工具&#xff0c;是生物信息分析最常用的一款软件&#xff0c;必须掌握。不管是做两序列相似性的简单比对&#xff0c;还是引物特异性、序列的来源等个性化分析&#xff0c;都会用到blast比对。许多看似高大上的基因分析&#xff0c;都可归…

1688商品详情API返回值中的物流与配送信息

在阿里巴巴1688平台上&#xff0c;商品详情API的返回值通常会包含丰富的商品信息&#xff0c;但具体到“物流与配送信息”这部分&#xff0c;它可能不直接包含在API的标准返回字段中&#xff0c;因为物流和配送信息往往与订单处理、库存状态以及物流服务商的实时数据相关联&…

探索Linux -- 冯诺依曼体系、初始操作系统、初始进程、fork函数

一、冯诺依曼体系结构 1、概念 冯诺依曼结构也称普林斯顿结构&#xff0c;是一种将程序指令存储器和数据存储器合并在一起的存储器结构。 最早的计算机器仅内含固定用途的程序。若想要改变此机器的程序&#xff0c;就必须更改线路、更改结构甚至重新设计此机器。当然最早的计…

安卓应用开发学习:查看手机传感器信息

一、引言 在手机app的开发中经常会用到手机的传感器&#xff0c;在《Android App 开发进阶与项目实战》一书的第10章就介绍了传感器的一些功能和用法。要想使用传感器&#xff0c;首先得知道手机具备哪些传感器。书中有传感器类型取值的说明&#xff0c;并提供了一个查看手机传…

vulnhub系列:DC-9

vulnhub系列&#xff1a;DC-9 靶机下载 一、信息收集 nmap扫描存活&#xff0c;根据mac地址寻找IP nmap 192.168.23.0/24nmap扫描IP 端口等信息 nmap 192.168.23.144 -p- -sV -Pn -O访问80端口 dirsearch目录扫描 python3 dirsearch.py -u http://192.168.23.144/页面查看…

8.12-基于gtids的主从复制搭建+lvs

一、LVS 1.角色 主机名ip地址功能web01192.168.2.101rsweb02192.168.2.102realserveenat内网:192.168.2.103 外网:192.168.2.120directorserver,ntpdns192.168.2.105dns 2..web服务器 [rootweb01 ~]# yum -y install nginx ​ [rootweb01 ~]# echo "web01" > …

【kruskal】最小生成树算法详解

最小生成树kruskal 洛谷 P3366 【模板】最小生成树 算法介绍 最小生成树&#xff08;Minimum Spanning Tree, MST&#xff09;是一个无向图中连接所有顶点的边的集合&#xff0c;这个集合满足两点&#xff1a;第一&#xff0c;它是一棵树&#xff0c;即任意两个顶点之间恰好有…

打靶记录9——Vikings

靶机下载地址&#xff1a;https://www.vulnhub.com/entry/vikings-1,741/ 难度&#xff1a; 低&#xff08;中&#xff09;&#xff0c;CTF风格的靶机 目标&#xff1a; 取得 root 权限 2 个flag 涉及的攻击方法&#xff1a; 主机发现端口扫描Web信息收集编码转换/文件还…

ThinkPHP5.1.C+CmsEasy-SQL注入

目录 1、ThinkPHP 中存在的 SQL注入 漏洞&#xff08; select 方法注入&#xff09; 1.1环境配置 1.1.1将 composer.json 文件的 require 字段设置成如下&#xff1a; 1.1.2设置application/index/controller/Index.php 文件 1.1.3在 application/database.php 文件中配置…

Xcode 在原生集成flutter项目

笔者公司有一个从2017年就开始开发的iOS和安卓原生项目&#xff0c;现在计划从外到内开始进行项目迁徙。 1》从gitee拉取flutter端的代码&#xff1b;&#xff08;Android报错Exception: Podfile missing&#xff09; 2》替换Xcode里的cocopods里Podfile的路径 然后报警 然后…

centos7.9删除home分区扩容至根分区

一、说明 拿到新服务器查看磁盘空间分为根(/),home,swap,boot/efi。home分区站到整个分区的87%以上。和预设的不一致&#xff0c;需要把home删除&#xff0c;扩容至根分区。 新服务器的分区是通过lvm划分的。 二、查看磁盘 三、查看磁盘分区 四、卸载home 卸载前做好备份&a…

自由职业四年,我整理了一些建议

我是勋荣&#xff0c;一个独立开发者。运营了自己的社群&#xff0c;有自己的软件产品。目前还在探索各种副业的路上~ 1我的独立开发之路 刚毕业就找不到Android岗位的我瑟瑟发抖。在广州&#xff0c;稀里糊涂做了Java后端开发。有一天加班 通宵&#xff0c;早上借住在同事家…